goai_path.go 13 KB

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