trpc.group/trpc-go/trpc-cmdline@v1.0.9/util/apidocs/path.go (about)

     1  // Tencent is pleased to support the open source community by making tRPC available.
     2  //
     3  // Copyright (C) 2023 THL A29 Limited, a Tencent company.
     4  // All rights reserved.
     5  //
     6  // If you have downloaded a copy of the tRPC source code from Tencent,
     7  // please note that tRPC source code is licensed under the  Apache 2.0 License,
     8  // A copy of the Apache 2.0 License is included in this file.
     9  
    10  package apidocs
    11  
    12  import (
    13  	"crypto/md5"
    14  	"fmt"
    15  	"regexp"
    16  	"sort"
    17  	"strings"
    18  
    19  	"github.com/hashicorp/go-multierror"
    20  
    21  	"trpc.group/trpc-go/trpc-cmdline/descriptor"
    22  	"trpc.group/trpc-go/trpc-cmdline/params"
    23  )
    24  
    25  // Paths is the set of Path.
    26  type Paths struct {
    27  	Elements map[string]Methods
    28  	Rank     map[string]int
    29  }
    30  
    31  // Put puts the element into the ordered map and records the element's ranking order in "Rank".
    32  func (paths *Paths) Put(key string, value Methods) {
    33  	paths.Elements[key] = value
    34  
    35  	if paths.Rank != nil {
    36  		if _, ok := paths.Rank[key]; !ok {
    37  			paths.Rank[key] = len(paths.Elements)
    38  		}
    39  	}
    40  }
    41  
    42  // UnmarshalJSON deserializes JSON data
    43  func (paths *Paths) UnmarshalJSON(b []byte) error {
    44  	return OrderedUnmarshalJSON(b, &paths.Elements, &paths.Rank)
    45  }
    46  
    47  // MarshalJSON serializes to JSON
    48  func (paths Paths) MarshalJSON() ([]byte, error) {
    49  	return OrderedMarshalJSON(paths.Elements, paths.Rank)
    50  }
    51  
    52  // NewPaths inits Path.
    53  func NewPaths(fd *descriptor.FileDescriptor, option *params.Option, defs *Definitions) (Paths, error) {
    54  	paths := Paths{
    55  		Elements: make(map[string]Methods),
    56  	}
    57  
    58  	if option.OrderByPBName {
    59  		paths.Rank = make(map[string]int)
    60  	}
    61  
    62  	var err error
    63  	for _, service := range fd.Services {
    64  		// service.RPC contains the original RPC method and its restful bindings.
    65  		for _, rpc := range service.RPC {
    66  			args := methodArgs{
    67  				RPC:  rpc,
    68  				Defs: defs,
    69  				Opt:  option,
    70  			}
    71  			if !option.AliasOn || option.KeepOrigRPCName ||
    72  				// If alias is set to true and keep-orig-rpcname is set to false, but the RPC method
    73  				// does not have an alias, the original RPC information should still be displayed.
    74  				len(service.MethodRPCx[rpc.Name]) == 0 {
    75  				args.Tags = []string{strings.ToLower(service.Name) + "." + "trpc"}
    76  				paths.addRPCMethod(args)
    77  			}
    78  			args.Tags = []string{strings.ToLower(service.Name) + "." + "restful"}
    79  			if e := paths.addRestfulMethod(args); e != nil {
    80  				err = multierror.Append(err, e).ErrorOrNil()
    81  			}
    82  		}
    83  		// service.RPCx only contains the alias RPC method which excludes any restful bindings.
    84  		for _, rpc := range service.RPCx {
    85  			args := methodArgs{
    86  				RPC:  rpc,
    87  				Defs: defs,
    88  				Opt:  option,
    89  			}
    90  			args.Tags = []string{strings.ToLower(service.Name) + "." + "trpc"}
    91  			paths.addRPCMethod(args)
    92  		}
    93  	}
    94  
    95  	paths.cleanOperationID()
    96  	return paths, err
    97  }
    98  
    99  // methodArgs adds a method to Paths with the given method arguments.
   100  type methodArgs struct {
   101  	RPC  *descriptor.RPCDescriptor
   102  	Defs *Definitions
   103  	Tags []string
   104  	Opt  *params.Option
   105  }
   106  
   107  func (args methodArgs) summary() string {
   108  	// Get the description of each rpc method defined in front (if any).
   109  	summary := args.RPC.LeadingComments
   110  	// Verify the names of the rpc methods collected in the "option" previously.
   111  	if len(args.RPC.SwaggerInfo.Title) != 0 {
   112  		summary = args.RPC.SwaggerInfo.Title
   113  	}
   114  	return trimExtraneous(summary)
   115  }
   116  
   117  func trimExtraneous(input string) string {
   118  	const marker = "@alias="
   119  	s := strings.Split(input, marker)
   120  	// Remove the alias if had any.
   121  	output := s[0]
   122  	// Remove comment slashes.
   123  	output = strings.ReplaceAll(output, "\n//", " ")
   124  	output = strings.ReplaceAll(output, "//", " ")
   125  	return strings.Trim(output, " \"'")
   126  }
   127  
   128  func (args methodArgs) method() *MethodStruct {
   129  	return &MethodStruct{
   130  		Summary:     args.summary(),
   131  		OperationID: args.RPC.Name,
   132  		Responses:   args.Defs.getMediaStruct(args.RPC.ResponseType),
   133  		Tags:        args.Tags,
   134  		Description: args.RPC.SwaggerInfo.Description,
   135  	}
   136  }
   137  
   138  func (args methodArgs) rpcParams() []*ParametersStruct {
   139  	queryParams := args.Defs.getQueryParameters(args.RPC.RequestType)
   140  	if args.Opt.SwaggerOptJSONParam {
   141  		queryParams = args.Defs.getBodyParameters(args.RPC.RequestType)
   142  	}
   143  
   144  	args.fillDescriptorToParams(queryParams)
   145  	return queryParams
   146  }
   147  
   148  func (args methodArgs) restfulParams(api descriptor.RESTfulAPIContent) ([]*ParametersStruct, error) {
   149  	pathParams := newPathParams(api.PathTmpl)
   150  
   151  	names := pathParams.getNames()
   152  	reqType := args.RPC.RequestType
   153  	if len(names) > 0 {
   154  		suffix := fmt.Sprintf("%x", md5.Sum([]byte(api.PathTmpl)))
   155  		args.Defs.filterFields(reqType, suffix, names)
   156  		reqType += "." + suffix
   157  	}
   158  
   159  	params := pathParams.getParameters()
   160  	method := strings.ToLower(api.Method)
   161  
   162  	if api.RequestBody == "" && (method == "get" || method == "delete") {
   163  		params = append(params, args.Defs.getQueryParameters(reqType)...)
   164  	}
   165  
   166  	if api.RequestBody == "*" {
   167  		params = append(params, args.Defs.getBodyParameters(reqType)...)
   168  	}
   169  
   170  	if api.RequestBody != "" && api.RequestBody != "*" {
   171  		param, err := args.Defs.getBodyParameter(reqType, api.RequestBody)
   172  		if err != nil {
   173  			return nil, fmt.Errorf("generate restful parameter error: %w", err)
   174  		}
   175  		params = append(params, param)
   176  	}
   177  
   178  	args.fillDescriptorToParams(params)
   179  
   180  	return params, nil
   181  }
   182  
   183  func (args methodArgs) fillDescriptorToParams(params []*ParametersStruct) {
   184  	for _, param := range params {
   185  		spd, ok := args.RPC.SwaggerInfo.Params[param.Name]
   186  		if ok {
   187  			param.Required = spd.Required
   188  			if spd.Default != "" {
   189  				param.Default = spd.Default
   190  			}
   191  		}
   192  	}
   193  }
   194  
   195  // GetPathsX converts to  openapi v3 structure.
   196  func (paths Paths) GetPathsX() PathsX {
   197  	pathsX := PathsX{Elements: make(map[string]MethodsX)}
   198  	pathsX.Rank = paths.Rank
   199  	paths.orderedEach(func(path string, methods Methods) {
   200  		pathsX.Elements[path] = methods.GetMethodsX()
   201  	})
   202  	return pathsX
   203  }
   204  
   205  func (paths Paths) addRPCMethod(args methodArgs) {
   206  	method := args.method()
   207  	method.Parameters = args.rpcParams()
   208  
   209  	mx := Methods{Elements: make(map[string]*MethodStruct)}
   210  	if paths.Rank != nil {
   211  		mx.Rank = make(map[string]int)
   212  	}
   213  	mx.Put(args.RPC.SwaggerInfo.Method, method)
   214  	paths.Put(args.RPC.FullyQualifiedCmd, mx)
   215  }
   216  
   217  func (paths Paths) addRestfulMethod(args methodArgs) error {
   218  	var err error
   219  	for _, api := range args.RPC.RESTfulAPIInfo.ContentList {
   220  		// Filter out the existing paths
   221  		path := api.PathTmpl
   222  		pathParams := newPathParams(api.PathTmpl)
   223  		for _, param := range pathParams {
   224  			if param.value != "" {
   225  				path = strings.Replace(path, param.origin, param.value, -1)
   226  			}
   227  		}
   228  
   229  		params, e := args.restfulParams(*api)
   230  		if e != nil {
   231  			err = multierror.Append(err, e).ErrorOrNil()
   232  		}
   233  		method := args.method()
   234  		method.Parameters = params
   235  
   236  		mx, ok := paths.Elements[path]
   237  		if !ok {
   238  			mx = Methods{
   239  				Elements: make(map[string]*MethodStruct),
   240  			}
   241  			if paths.Rank != nil {
   242  				mx.Rank = make(map[string]int)
   243  			}
   244  		}
   245  
   246  		mx.Put(strings.ToLower(api.Method), method)
   247  		paths.Put(path, mx)
   248  	}
   249  	return err
   250  }
   251  
   252  // orderedEach sort each element
   253  func (paths *Paths) orderedEach(f func(path string, methods Methods)) {
   254  	if paths == nil {
   255  		return
   256  	}
   257  
   258  	var keys []string
   259  	for k := range paths.Elements {
   260  		keys = append(keys, k)
   261  	}
   262  
   263  	if paths.Rank != nil {
   264  		sort.Slice(keys, func(i, j int) bool {
   265  			return paths.Rank[keys[i]] < paths.Rank[keys[j]]
   266  		})
   267  	} else {
   268  		sort.Strings(keys)
   269  	}
   270  
   271  	for _, k := range keys {
   272  		f(k, paths.Elements[k])
   273  	}
   274  }
   275  
   276  // cleanOperationID adds a suffix number to the end of the OperationID to avoid duplication.
   277  // The reason for using "order each" is that the loop over the map is random,
   278  // which leads to unstable results and cannot be tested stably.
   279  func (paths Paths) cleanOperationID() {
   280  	operationIDSet := make(map[string]int)
   281  	paths.orderedEach(func(path string, methods Methods) {
   282  		methods.orderedEach(func(k string, method *MethodStruct) {
   283  			operationIDSet[method.OperationID]++
   284  			if operationIDSet[method.OperationID] > 1 {
   285  				method.OperationID = fmt.Sprintf("%s%d",
   286  					method.OperationID, operationIDSet[method.OperationID])
   287  			}
   288  		})
   289  	})
   290  }
   291  
   292  type pathParam struct {
   293  	name   string
   294  	value  string
   295  	origin string
   296  }
   297  
   298  type pathParams []pathParam
   299  
   300  func (params pathParams) getParameters() []*ParametersStruct {
   301  	parameters := make([]*ParametersStruct, 0)
   302  	for _, param := range params {
   303  		if param.value != "" {
   304  			continue
   305  		}
   306  
   307  		parameters = append(parameters, &ParametersStruct{
   308  			Name:     param.name,
   309  			Default:  "",
   310  			Required: true,
   311  			Type:     "string",
   312  			In:       "path",
   313  		})
   314  	}
   315  
   316  	return parameters
   317  }
   318  
   319  func (params pathParams) getNames() []string {
   320  	var names []string
   321  	for _, param := range params {
   322  		names = append(names, param.name)
   323  	}
   324  	return names
   325  }
   326  
   327  var pathParamsRE = regexp.MustCompile("{.*?}")
   328  
   329  func newPathParams(path string) pathParams {
   330  	var params []pathParam
   331  	values := pathParamsRE.FindAllString(path, -1)
   332  
   333  	for _, v := range values {
   334  		pos := strings.Index(v, "=")
   335  		if pos == -1 {
   336  			params = append(params, pathParam{
   337  				origin: v,
   338  				name:   v[1 : len(v)-1],
   339  			})
   340  			continue
   341  		}
   342  		params = append(params, pathParam{
   343  			origin: v,
   344  			name:   v[1:pos],
   345  			value:  v[pos+1 : len(v)-1],
   346  		})
   347  	}
   348  	return params
   349  }