github.com/wangyougui/gf/v2@v2.6.5/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/wangyougui/gf. 6 7 package goai 8 9 import ( 10 "net/http" 11 "reflect" 12 13 "github.com/wangyougui/gf/v2/container/garray" 14 "github.com/wangyougui/gf/v2/container/gmap" 15 "github.com/wangyougui/gf/v2/errors/gcode" 16 "github.com/wangyougui/gf/v2/errors/gerror" 17 "github.com/wangyougui/gf/v2/internal/json" 18 "github.com/wangyougui/gf/v2/os/gstructs" 19 "github.com/wangyougui/gf/v2/text/gstr" 20 "github.com/wangyougui/gf/v2/util/gconv" 21 "github.com/wangyougui/gf/v2/util/gmeta" 22 "github.com/wangyougui/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 Required: true, 203 Content: map[string]MediaType{}, 204 } 205 ) 206 // Supported mime types of request. 207 var ( 208 contentTypes = oai.Config.ReadContentTypes 209 tagMimeValue = gmeta.Get(inputObject.Interface(), gtag.Mime).String() 210 ) 211 if tagMimeValue != "" { 212 contentTypes = gstr.SplitAndTrim(tagMimeValue, ",") 213 } 214 for _, v := range contentTypes { 215 if isInputStructEmpty { 216 requestBody.Content[v] = MediaType{} 217 } else { 218 schemaRef, err := oai.getRequestSchemaRef(getRequestSchemaRefInput{ 219 BusinessStructName: inputStructTypeName, 220 RequestObject: oai.Config.CommonRequest, 221 RequestDataField: oai.Config.CommonRequestDataField, 222 }) 223 if err != nil { 224 return err 225 } 226 requestBody.Content[v] = MediaType{ 227 Schema: schemaRef, 228 } 229 } 230 } 231 operation.RequestBody = &RequestBodyRef{ 232 Value: &requestBody, 233 } 234 } 235 236 // ================================================================================================================= 237 // Response. 238 // ================================================================================================================= 239 if _, ok := operation.Responses[responseOkKey]; !ok { 240 var ( 241 response = Response{ 242 Content: map[string]MediaType{}, 243 XExtensions: make(XExtensions), 244 } 245 ) 246 if len(outputMetaMap) > 0 { 247 if err := oai.tagMapToResponse(outputMetaMap, &response); err != nil { 248 return err 249 } 250 } 251 // Supported mime types of response. 252 var ( 253 contentTypes = oai.Config.ReadContentTypes 254 tagMimeValue = gmeta.Get(outputObject.Interface(), gtag.Mime).String() 255 refInput = getResponseSchemaRefInput{ 256 BusinessStructName: outputStructTypeName, 257 CommonResponseObject: oai.Config.CommonResponse, 258 CommonResponseDataField: oai.Config.CommonResponseDataField, 259 } 260 ) 261 if tagMimeValue != "" { 262 contentTypes = gstr.SplitAndTrim(tagMimeValue, ",") 263 } 264 for _, v := range contentTypes { 265 // If customized response mime type, it then ignores common response feature. 266 if tagMimeValue != "" { 267 refInput.CommonResponseObject = nil 268 refInput.CommonResponseDataField = "" 269 } 270 schemaRef, err := oai.getResponseSchemaRef(refInput) 271 if err != nil { 272 return err 273 } 274 response.Content[v] = MediaType{ 275 Schema: schemaRef, 276 } 277 } 278 operation.Responses[responseOkKey] = ResponseRef{Value: &response} 279 } 280 281 // Remove operation body duplicated properties. 282 oai.removeOperationDuplicatedProperties(operation) 283 284 // Assign to certain operation attribute. 285 switch gstr.ToUpper(in.Method) { 286 case http.MethodGet: 287 // GET operations cannot have a requestBody. 288 operation.RequestBody = nil 289 path.Get = &operation 290 291 case http.MethodPut: 292 path.Put = &operation 293 294 case http.MethodPost: 295 path.Post = &operation 296 297 case http.MethodDelete: 298 // DELETE operations cannot have a requestBody. 299 operation.RequestBody = nil 300 path.Delete = &operation 301 302 case http.MethodConnect: 303 // Nothing to do for Connect. 304 305 case http.MethodHead: 306 path.Head = &operation 307 308 case http.MethodOptions: 309 path.Options = &operation 310 311 case http.MethodPatch: 312 path.Patch = &operation 313 314 case http.MethodTrace: 315 path.Trace = &operation 316 317 default: 318 return gerror.NewCodef(gcode.CodeInvalidParameter, `invalid method "%s"`, in.Method) 319 } 320 oai.Paths[in.Path] = path 321 return nil 322 } 323 324 func (oai *OpenApiV3) removeOperationDuplicatedProperties(operation Operation) { 325 if len(operation.Parameters) == 0 { 326 // Nothing to do. 327 return 328 } 329 330 var ( 331 duplicatedParameterNames []interface{} 332 dataField string 333 ) 334 335 for _, parameter := range operation.Parameters { 336 duplicatedParameterNames = append(duplicatedParameterNames, parameter.Value.Name) 337 } 338 339 // Check operation request body have common request data field. 340 dataFields := gstr.Split(oai.Config.CommonRequestDataField, ".") 341 if len(dataFields) > 0 && dataFields[0] != "" { 342 dataField = dataFields[0] 343 } 344 345 for _, requestBodyContent := range operation.RequestBody.Value.Content { 346 // Check request body schema 347 if requestBodyContent.Schema == nil { 348 continue 349 } 350 351 // Check request body schema ref. 352 if requestBodyContent.Schema.Ref != "" { 353 if schema := oai.Components.Schemas.Get(requestBodyContent.Schema.Ref); schema != nil { 354 newSchema := schema.Value.Clone() 355 requestBodyContent.Schema.Ref = "" 356 requestBodyContent.Schema.Value = newSchema 357 newSchema.Required = oai.removeItemsFromArray(newSchema.Required, duplicatedParameterNames) 358 newSchema.Properties.Removes(duplicatedParameterNames) 359 continue 360 } 361 } 362 363 // Check the Value public field for the request body. 364 if commonRequest := requestBodyContent.Schema.Value.Properties.Get(dataField); commonRequest != nil { 365 commonRequest.Value.Required = oai.removeItemsFromArray(commonRequest.Value.Required, duplicatedParameterNames) 366 commonRequest.Value.Properties.Removes(duplicatedParameterNames) 367 continue 368 } 369 370 // Check request body schema value. 371 if requestBodyContent.Schema.Value != nil { 372 requestBodyContent.Schema.Value.Required = oai.removeItemsFromArray(requestBodyContent.Schema.Value.Required, duplicatedParameterNames) 373 requestBodyContent.Schema.Value.Properties.Removes(duplicatedParameterNames) 374 continue 375 } 376 } 377 } 378 379 func (oai *OpenApiV3) removeItemsFromArray(array []string, items []interface{}) []string { 380 arr := garray.NewStrArrayFrom(array) 381 for _, item := range items { 382 if value, ok := item.(string); ok { 383 arr.RemoveValue(value) 384 } 385 } 386 return arr.Slice() 387 } 388 389 func (oai *OpenApiV3) doesStructHasNoFields(s interface{}) bool { 390 return reflect.TypeOf(s).NumField() == 0 391 } 392 393 func (oai *OpenApiV3) tagMapToPath(tagMap map[string]string, path *Path) error { 394 var mergedTagMap = oai.fillMapWithShortTags(tagMap) 395 if err := gconv.Struct(mergedTagMap, path); err != nil { 396 return gerror.Wrap(err, `mapping struct tags to Path failed`) 397 } 398 oai.tagMapToXExtensions(mergedTagMap, path.XExtensions) 399 return nil 400 } 401 402 // MarshalJSON implements the interface MarshalJSON for json.Marshal. 403 func (p Path) MarshalJSON() ([]byte, error) { 404 var ( 405 b []byte 406 m map[string]json.RawMessage 407 err error 408 ) 409 type tempPath Path // To prevent JSON marshal recursion error. 410 if b, err = json.Marshal(tempPath(p)); err != nil { 411 return nil, err 412 } 413 if err = json.Unmarshal(b, &m); err != nil { 414 return nil, err 415 } 416 for k, v := range p.XExtensions { 417 if b, err = json.Marshal(v); err != nil { 418 return nil, err 419 } 420 m[k] = b 421 } 422 return json.Marshal(m) 423 }