goai_path.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. // Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
  2. //
  3. // This Source Code Form is subject to the terms of the MIT License.
  4. // If a copy of the MIT was not distributed with this file,
  5. // You can obtain one at https://github.com/gogf/gf.
  6. package goai
  7. import (
  8. "reflect"
  9. "github.com/gogf/gf/v2/container/garray"
  10. "github.com/gogf/gf/v2/errors/gcode"
  11. "github.com/gogf/gf/v2/errors/gerror"
  12. "github.com/gogf/gf/v2/internal/json"
  13. "github.com/gogf/gf/v2/os/gstructs"
  14. "github.com/gogf/gf/v2/text/gstr"
  15. "github.com/gogf/gf/v2/util/gconv"
  16. "github.com/gogf/gf/v2/util/gmeta"
  17. )
  18. type Path struct {
  19. Ref string `json:"$ref,omitempty"`
  20. Summary string `json:"summary,omitempty"`
  21. Description string `json:"description,omitempty"`
  22. Connect *Operation `json:"connect,omitempty"`
  23. Delete *Operation `json:"delete,omitempty"`
  24. Get *Operation `json:"get,omitempty"`
  25. Head *Operation `json:"head,omitempty"`
  26. Options *Operation `json:"options,omitempty"`
  27. Patch *Operation `json:"patch,omitempty"`
  28. Post *Operation `json:"post,omitempty"`
  29. Put *Operation `json:"put,omitempty"`
  30. Trace *Operation `json:"trace,omitempty"`
  31. Servers Servers `json:"servers,omitempty"`
  32. Parameters Parameters `json:"parameters,omitempty"`
  33. XExtensions XExtensions `json:"-"`
  34. }
  35. // Paths are specified by OpenAPI/Swagger standard version 3.0.
  36. type Paths map[string]Path
  37. const (
  38. responseOkKey = `200`
  39. )
  40. type addPathInput struct {
  41. Path string // Precise route path.
  42. Prefix string // Route path prefix.
  43. Method string // Route method.
  44. Function interface{} // Uniformed function.
  45. }
  46. func (oai *OpenApiV3) addPath(in addPathInput) error {
  47. if oai.Paths == nil {
  48. oai.Paths = map[string]Path{}
  49. }
  50. var (
  51. reflectType = reflect.TypeOf(in.Function)
  52. )
  53. if reflectType.NumIn() != 2 || reflectType.NumOut() != 2 {
  54. return gerror.NewCodef(
  55. gcode.CodeInvalidParameter,
  56. `unsupported function "%s" for OpenAPI Path register, there should be input & output structures`,
  57. reflectType.String(),
  58. )
  59. }
  60. var (
  61. inputObject reflect.Value
  62. outputObject reflect.Value
  63. )
  64. // Create instance according input/output types.
  65. if reflectType.In(1).Kind() == reflect.Ptr {
  66. inputObject = reflect.New(reflectType.In(1).Elem()).Elem()
  67. } else {
  68. inputObject = reflect.New(reflectType.In(1)).Elem()
  69. }
  70. if reflectType.Out(0).Kind() == reflect.Ptr {
  71. outputObject = reflect.New(reflectType.Out(0).Elem()).Elem()
  72. } else {
  73. outputObject = reflect.New(reflectType.Out(0)).Elem()
  74. }
  75. var (
  76. mime string
  77. path = Path{XExtensions: make(XExtensions)}
  78. inputMetaMap = gmeta.Data(inputObject.Interface())
  79. outputMetaMap = gmeta.Data(outputObject.Interface())
  80. isInputStructEmpty = oai.doesStructHasNoFields(inputObject.Interface())
  81. inputStructTypeName = oai.golangTypeToSchemaName(inputObject.Type())
  82. outputStructTypeName = oai.golangTypeToSchemaName(outputObject.Type())
  83. operation = Operation{
  84. Responses: map[string]ResponseRef{},
  85. XExtensions: make(XExtensions),
  86. }
  87. )
  88. // Path check.
  89. if in.Path == "" {
  90. in.Path = gmeta.Get(inputObject.Interface(), TagNamePath).String()
  91. if in.Prefix != "" {
  92. in.Path = gstr.TrimRight(in.Prefix, "/") + "/" + gstr.TrimLeft(in.Path, "/")
  93. }
  94. }
  95. if in.Path == "" {
  96. return gerror.NewCodef(
  97. gcode.CodeMissingParameter,
  98. `missing necessary path parameter "%s" for input struct "%s", missing tag in attribute Meta?`,
  99. TagNamePath, inputStructTypeName,
  100. )
  101. }
  102. if v, ok := oai.Paths[in.Path]; ok {
  103. path = v
  104. }
  105. // Method check.
  106. if in.Method == "" {
  107. in.Method = gmeta.Get(inputObject.Interface(), TagNameMethod).String()
  108. }
  109. if in.Method == "" {
  110. return gerror.NewCodef(
  111. gcode.CodeMissingParameter,
  112. `missing necessary method parameter "%s" for input struct "%s", missing tag in attribute Meta?`,
  113. TagNameMethod, inputStructTypeName,
  114. )
  115. }
  116. if err := oai.addSchema(inputObject.Interface(), outputObject.Interface()); err != nil {
  117. return err
  118. }
  119. if len(inputMetaMap) > 0 {
  120. if err := oai.tagMapToPath(inputMetaMap, &path); err != nil {
  121. return err
  122. }
  123. if err := oai.tagMapToOperation(inputMetaMap, &operation); err != nil {
  124. return err
  125. }
  126. // Allowed request mime.
  127. if mime = inputMetaMap[TagNameMime]; mime == "" {
  128. mime = inputMetaMap[TagNameConsumes]
  129. }
  130. }
  131. // =================================================================================================================
  132. // Request Parameter.
  133. // =================================================================================================================
  134. structFields, _ := gstructs.Fields(gstructs.FieldsInput{
  135. Pointer: inputObject.Interface(),
  136. RecursiveOption: gstructs.RecursiveOptionEmbeddedNoTag,
  137. })
  138. for _, structField := range structFields {
  139. if operation.Parameters == nil {
  140. operation.Parameters = []ParameterRef{}
  141. }
  142. parameterRef, err := oai.newParameterRefWithStructMethod(structField, in.Path, in.Method)
  143. if err != nil {
  144. return err
  145. }
  146. if parameterRef != nil {
  147. operation.Parameters = append(operation.Parameters, *parameterRef)
  148. }
  149. }
  150. // =================================================================================================================
  151. // Request Body.
  152. // =================================================================================================================
  153. if operation.RequestBody == nil {
  154. operation.RequestBody = &RequestBodyRef{}
  155. }
  156. if operation.RequestBody.Value == nil {
  157. var (
  158. requestBody = RequestBody{
  159. Required: true,
  160. Content: map[string]MediaType{},
  161. }
  162. )
  163. // Supported mime types of request.
  164. var (
  165. contentTypes = oai.Config.ReadContentTypes
  166. tagMimeValue = gmeta.Get(inputObject.Interface(), TagNameMime).String()
  167. )
  168. if tagMimeValue != "" {
  169. contentTypes = gstr.SplitAndTrim(tagMimeValue, ",")
  170. }
  171. for _, v := range contentTypes {
  172. if isInputStructEmpty {
  173. requestBody.Content[v] = MediaType{}
  174. } else {
  175. schemaRef, err := oai.getRequestSchemaRef(getRequestSchemaRefInput{
  176. BusinessStructName: inputStructTypeName,
  177. RequestObject: oai.Config.CommonRequest,
  178. RequestDataField: oai.Config.CommonRequestDataField,
  179. })
  180. if err != nil {
  181. return err
  182. }
  183. requestBody.Content[v] = MediaType{
  184. Schema: schemaRef,
  185. }
  186. }
  187. }
  188. operation.RequestBody = &RequestBodyRef{
  189. Value: &requestBody,
  190. }
  191. }
  192. // =================================================================================================================
  193. // Response.
  194. // =================================================================================================================
  195. if _, ok := operation.Responses[responseOkKey]; !ok {
  196. var (
  197. response = Response{
  198. Content: map[string]MediaType{},
  199. XExtensions: make(XExtensions),
  200. }
  201. )
  202. if len(outputMetaMap) > 0 {
  203. if err := oai.tagMapToResponse(outputMetaMap, &response); err != nil {
  204. return err
  205. }
  206. }
  207. // Supported mime types of response.
  208. var (
  209. contentTypes = oai.Config.ReadContentTypes
  210. tagMimeValue = gmeta.Get(outputObject.Interface(), TagNameMime).String()
  211. refInput = getResponseSchemaRefInput{
  212. BusinessStructName: outputStructTypeName,
  213. CommonResponseObject: oai.Config.CommonResponse,
  214. CommonResponseDataField: oai.Config.CommonResponseDataField,
  215. }
  216. )
  217. if tagMimeValue != "" {
  218. contentTypes = gstr.SplitAndTrim(tagMimeValue, ",")
  219. }
  220. for _, v := range contentTypes {
  221. // If customized response mime type, it then ignores common response feature.
  222. if tagMimeValue != "" {
  223. refInput.CommonResponseObject = nil
  224. refInput.CommonResponseDataField = ""
  225. }
  226. schemaRef, err := oai.getResponseSchemaRef(refInput)
  227. if err != nil {
  228. return err
  229. }
  230. response.Content[v] = MediaType{
  231. Schema: schemaRef,
  232. }
  233. }
  234. operation.Responses[responseOkKey] = ResponseRef{Value: &response}
  235. }
  236. // Remove operation body duplicated properties.
  237. oai.removeOperationDuplicatedProperties(operation)
  238. // Assign to certain operation attribute.
  239. switch gstr.ToUpper(in.Method) {
  240. case HttpMethodGet:
  241. // GET operations cannot have a requestBody.
  242. operation.RequestBody = nil
  243. path.Get = &operation
  244. case HttpMethodPut:
  245. path.Put = &operation
  246. case HttpMethodPost:
  247. path.Post = &operation
  248. case HttpMethodDelete:
  249. // DELETE operations cannot have a requestBody.
  250. operation.RequestBody = nil
  251. path.Delete = &operation
  252. case HttpMethodConnect:
  253. // Nothing to do for Connect.
  254. case HttpMethodHead:
  255. path.Head = &operation
  256. case HttpMethodOptions:
  257. path.Options = &operation
  258. case HttpMethodPatch:
  259. path.Patch = &operation
  260. case HttpMethodTrace:
  261. path.Trace = &operation
  262. default:
  263. return gerror.NewCodef(gcode.CodeInvalidParameter, `invalid method "%s"`, in.Method)
  264. }
  265. oai.Paths[in.Path] = path
  266. return nil
  267. }
  268. func (oai *OpenApiV3) removeOperationDuplicatedProperties(operation Operation) {
  269. var (
  270. duplicatedParameterNames []interface{}
  271. dataField string
  272. )
  273. for _, parameter := range operation.Parameters {
  274. duplicatedParameterNames = append(duplicatedParameterNames, parameter.Value.Name)
  275. }
  276. // Check operation request body have common request data field.
  277. dataFields := gstr.Split(oai.Config.CommonRequestDataField, ".")
  278. if len(dataFields) > 0 && dataFields[0] != "" {
  279. dataField = dataFields[0]
  280. }
  281. for _, requestBodyContent := range operation.RequestBody.Value.Content {
  282. // Check request body schema
  283. if requestBodyContent.Schema == nil {
  284. continue
  285. }
  286. // Check request body schema ref.
  287. if schema := oai.Components.Schemas.Get(requestBodyContent.Schema.Ref); schema != nil {
  288. schema.Value.Required = oai.removeItemsFromArray(schema.Value.Required, duplicatedParameterNames)
  289. schema.Value.Properties.Removes(duplicatedParameterNames)
  290. continue
  291. }
  292. // Check the Value public field for the request body.
  293. if commonRequest := requestBodyContent.Schema.Value.Properties.Get(dataField); commonRequest != nil {
  294. commonRequest.Value.Required = oai.removeItemsFromArray(commonRequest.Value.Required, duplicatedParameterNames)
  295. commonRequest.Value.Properties.Removes(duplicatedParameterNames)
  296. continue
  297. }
  298. // Check request body schema value.
  299. if requestBodyContent.Schema.Value != nil {
  300. requestBodyContent.Schema.Value.Required = oai.removeItemsFromArray(requestBodyContent.Schema.Value.Required, duplicatedParameterNames)
  301. requestBodyContent.Schema.Value.Properties.Removes(duplicatedParameterNames)
  302. continue
  303. }
  304. }
  305. }
  306. func (oai *OpenApiV3) removeItemsFromArray(array []string, items []interface{}) []string {
  307. arr := garray.NewStrArrayFrom(array)
  308. for _, item := range items {
  309. if value, ok := item.(string); ok {
  310. arr.RemoveValue(value)
  311. }
  312. }
  313. return arr.Slice()
  314. }
  315. func (oai *OpenApiV3) doesStructHasNoFields(s interface{}) bool {
  316. return reflect.TypeOf(s).NumField() == 0
  317. }
  318. func (oai *OpenApiV3) tagMapToPath(tagMap map[string]string, path *Path) error {
  319. var mergedTagMap = oai.fileMapWithShortTags(tagMap)
  320. if err := gconv.Struct(mergedTagMap, path); err != nil {
  321. return gerror.Wrap(err, `mapping struct tags to Path failed`)
  322. }
  323. oai.tagMapToXExtensions(mergedTagMap, path.XExtensions)
  324. return nil
  325. }
  326. func (p Path) MarshalJSON() ([]byte, error) {
  327. var (
  328. b []byte
  329. m map[string]json.RawMessage
  330. err error
  331. )
  332. type tempPath Path // To prevent JSON marshal recursion error.
  333. if b, err = json.Marshal(tempPath(p)); err != nil {
  334. return nil, err
  335. }
  336. if err = json.Unmarshal(b, &m); err != nil {
  337. return nil, err
  338. }
  339. for k, v := range p.XExtensions {
  340. if b, err = json.Marshal(v); err != nil {
  341. return nil, err
  342. }
  343. m[k] = b
  344. }
  345. return json.Marshal(m)
  346. }