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  }