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 }