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