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  }