github.com/unionj-cloud/go-doudou@v1.3.8-0.20221011095552-0088008e5b31/cmd/internal/svc/codegen/doc.go (about) 1 package codegen 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "github.com/iancoleman/strcase" 8 "github.com/sirupsen/logrus" 9 "github.com/unionj-cloud/go-doudou/cmd/internal/astutils" 10 v3helper "github.com/unionj-cloud/go-doudou/cmd/internal/openapi/v3" 11 "github.com/unionj-cloud/go-doudou/toolkit/constants" 12 v3 "github.com/unionj-cloud/go-doudou/toolkit/openapi/v3" 13 "github.com/unionj-cloud/go-doudou/toolkit/stringutils" 14 "go/ast" 15 "go/parser" 16 "go/token" 17 "io/ioutil" 18 "os" 19 "path/filepath" 20 "reflect" 21 "strings" 22 "text/template" 23 "time" 24 ) 25 26 func getSchemaNames(vofile string) []string { 27 fset := token.NewFileSet() 28 root, err := parser.ParseFile(fset, vofile, nil, parser.ParseComments) 29 if err != nil { 30 panic(err) 31 } 32 sc := astutils.NewStructCollector(ExprStringP) 33 ast.Walk(sc, root) 34 structs := sc.DocFlatEmbed() 35 var ret []string 36 for _, item := range structs { 37 if item.IsExport { 38 ret = append(ret, item.Name) 39 } 40 } 41 return ret 42 } 43 44 func schemasOf(vofile string) []v3.Schema { 45 fset := token.NewFileSet() 46 root, err := parser.ParseFile(fset, vofile, nil, parser.ParseComments) 47 if err != nil { 48 panic(err) 49 } 50 sc := astutils.NewStructCollector(ExprStringP) 51 ast.Walk(sc, root) 52 structs := sc.DocFlatEmbed() 53 var ret []v3.Schema 54 for _, item := range structs { 55 ret = append(ret, v3helper.NewSchema(item)) 56 } 57 return ret 58 } 59 60 func enumsOf(vofile string) (map[string][]astutils.MethodMeta, map[string][]string) { 61 fset := token.NewFileSet() 62 root, err := parser.ParseFile(fset, vofile, nil, parser.ParseComments) 63 if err != nil { 64 panic(err) 65 } 66 sc := astutils.NewEnumCollector(ExprStringP) 67 ast.Walk(sc, root) 68 return sc.Methods, sc.Consts 69 } 70 71 const ( 72 get = "GET" 73 post = "POST" 74 put = "PUT" 75 delete = "DELETE" 76 ) 77 78 func operationOf(method astutils.MethodMeta, httpMethod string) v3.Operation { 79 var ret v3.Operation 80 var params []v3.Parameter 81 82 ret.Description = strings.Join(method.Comments, "\n") 83 84 // If http method is "POST" and each parameters' type is one of v3.Int, v3.Int64, v3.Bool, v3.String, v3.Float32, v3.Float64, 85 // then we use application/x-www-form-urlencoded as Content-type, and we make one ref schema from them as request body. 86 var simpleCnt int 87 for _, item := range method.Params { 88 if v3helper.IsBuiltin(item) || item.Type == "context.Context" { 89 simpleCnt++ 90 } 91 } 92 if httpMethod == post && simpleCnt == len(method.Params) { 93 ret.RequestBody = postFormUrl(method) 94 } else { 95 // Simple parameters such as v3.Int, v3.Int64, v3.Bool, v3.String, v3.Float32, v3.Float64 and corresponding Array type 96 // will be put into query parameter as url search params no matter what http method is. 97 // Complex parameters such as structs in vo package, map and corresponding slice/array type 98 // will be put into request body as json content type. 99 // File and file array parameter will be put into request body as multipart/form-data content type. 100 upload := false 101 for _, item := range method.Params { 102 if item.Type == "context.Context" { 103 continue 104 } 105 pschemaType := v3helper.SchemaOf(item) 106 if reflect.DeepEqual(pschemaType, v3.FileArray) || pschemaType == v3.File { 107 upload = true 108 break 109 } 110 } 111 112 if upload { 113 ret.RequestBody = uploadFile(method) 114 } else { 115 for _, item := range method.Params { 116 if item.Type == "context.Context" { 117 continue 118 } 119 pschema := v3helper.CopySchema(item) 120 v3helper.RefAddDoc(&pschema, strings.Join(item.Comments, "\n")) 121 required := !v3helper.IsOptional(item.Type) 122 if v3helper.IsBuiltin(item) { 123 params = append(params, v3.Parameter{ 124 Name: strcase.ToLowerCamel(item.Name), 125 In: v3.InQuery, 126 Schema: &pschema, 127 Description: pschema.Description, 128 Required: required, 129 }) 130 } else { 131 var content v3.Content 132 mt := &v3.MediaType{ 133 Schema: &pschema, 134 } 135 reflect.ValueOf(&content).Elem().FieldByName("JSON").Set(reflect.ValueOf(mt)) 136 ret.RequestBody = &v3.RequestBody{ 137 Content: &content, 138 Required: required, 139 } 140 } 141 } 142 } 143 } 144 145 ret.Parameters = params 146 ret.Responses = response(method) 147 return ret 148 } 149 150 func response(method astutils.MethodMeta) *v3.Responses { 151 var respContent v3.Content 152 var hasFile bool 153 var fileDoc string 154 for _, item := range method.Results { 155 if item.Type == "*os.File" { 156 hasFile = true 157 fileDoc = strings.Join(item.Comments, "\n") 158 break 159 } 160 } 161 if hasFile { 162 respContent.Stream = &v3.MediaType{ 163 Schema: &v3.Schema{ 164 Type: v3.StringT, 165 Format: v3.BinaryF, 166 Description: fileDoc, 167 }, 168 } 169 } else { 170 title := method.Name + "Resp" 171 respSchema := v3.Schema{ 172 Type: v3.ObjectT, 173 Title: title, 174 Properties: make(map[string]*v3.Schema), 175 } 176 for _, item := range method.Results { 177 if item.Type == "error" { 178 continue 179 } 180 key := item.Name 181 if stringutils.IsEmpty(key) { 182 key = item.Type[strings.LastIndex(item.Type, ".")+1:] 183 } 184 rschema := v3helper.CopySchema(item) 185 v3helper.RefAddDoc(&rschema, strings.Join(item.Comments, "\n")) 186 prop := strcase.ToLowerCamel(key) 187 respSchema.Properties[prop] = &rschema 188 if !v3helper.IsOptional(item.Type) { 189 respSchema.Required = append(respSchema.Required, prop) 190 } 191 } 192 v3helper.Schemas[title] = respSchema 193 respContent.JSON = &v3.MediaType{ 194 Schema: &v3.Schema{ 195 Ref: "#/components/schemas/" + title, 196 }, 197 } 198 } 199 return &v3.Responses{ 200 Resp200: &v3.Response{ 201 Content: &respContent, 202 }, 203 } 204 } 205 206 func uploadFile(method astutils.MethodMeta) *v3.RequestBody { 207 title := method.Name + "Req" 208 reqSchema := v3.Schema{ 209 Type: v3.ObjectT, 210 Title: title, 211 Properties: make(map[string]*v3.Schema), 212 } 213 for _, item := range method.Params { 214 if item.Type == "context.Context" { 215 continue 216 } 217 pschemaType := v3helper.SchemaOf(item) 218 if reflect.DeepEqual(pschemaType, v3.FileArray) || pschemaType == v3.File || v3helper.IsBuiltin(item) { 219 pschema := v3helper.CopySchema(item) 220 pschema.Description = strings.Join(item.Comments, "\n") 221 prop := strcase.ToLowerCamel(item.Name) 222 reqSchema.Properties[prop] = &pschema 223 if !v3helper.IsOptional(item.Type) { 224 reqSchema.Required = append(reqSchema.Required, prop) 225 } 226 } 227 } 228 v3helper.Schemas[title] = reqSchema 229 mt := &v3.MediaType{ 230 Schema: &v3.Schema{ 231 Ref: "#/components/schemas/" + title, 232 }, 233 } 234 var content v3.Content 235 reflect.ValueOf(&content).Elem().FieldByName("FormData").Set(reflect.ValueOf(mt)) 236 return &v3.RequestBody{ 237 Content: &content, 238 Required: len(reqSchema.Required) > 0, 239 } 240 } 241 242 func postFormUrl(method astutils.MethodMeta) *v3.RequestBody { 243 title := method.Name + "Req" 244 reqSchema := v3.Schema{ 245 Type: v3.ObjectT, 246 Title: title, 247 Properties: make(map[string]*v3.Schema), 248 } 249 for _, item := range method.Params { 250 if item.Type == "context.Context" { 251 continue 252 } 253 pschema := v3helper.CopySchema(item) 254 pschema.Description = strings.Join(item.Comments, "\n") 255 prop := strcase.ToLowerCamel(item.Name) 256 reqSchema.Properties[prop] = &pschema 257 if !v3helper.IsOptional(item.Type) { 258 reqSchema.Required = append(reqSchema.Required, prop) 259 } 260 } 261 v3helper.Schemas[title] = reqSchema 262 mt := &v3.MediaType{ 263 Schema: &v3.Schema{ 264 Ref: "#/components/schemas/" + title, 265 }, 266 } 267 var content v3.Content 268 reflect.ValueOf(&content).Elem().FieldByName("FormURL").Set(reflect.ValueOf(mt)) 269 return &v3.RequestBody{ 270 Content: &content, 271 Required: len(reqSchema.Required) > 0, 272 } 273 } 274 275 func pathsOf(ic astutils.InterfaceCollector, routePatternStrategy int) map[string]v3.Path { 276 if len(ic.Interfaces) == 0 { 277 return nil 278 } 279 pathmap := make(map[string]v3.Path) 280 inter := ic.Interfaces[0] 281 for _, method := range inter.Methods { 282 endpoint := fmt.Sprintf("/%s", pattern(method.Name)) 283 if routePatternStrategy == 1 { 284 endpoint = fmt.Sprintf("/%s/%s", strings.ToLower(inter.Name), noSplitPattern(method.Name)) 285 } 286 hm := httpMethod(method.Name) 287 op := operationOf(method, hm) 288 if val, ok := pathmap[endpoint]; ok { 289 reflect.ValueOf(&val).Elem().FieldByName(strings.Title(strings.ToLower(hm))).Set(reflect.ValueOf(&op)) 290 pathmap[endpoint] = val 291 } else { 292 var v3path v3.Path 293 reflect.ValueOf(&v3path).Elem().FieldByName(strings.Title(strings.ToLower(hm))).Set(reflect.ValueOf(&op)) 294 pathmap[endpoint] = v3path 295 } 296 } 297 return pathmap 298 } 299 300 var gofileTmpl = `package {{.SvcPackage}} 301 302 import "github.com/unionj-cloud/go-doudou/framework/http/onlinedoc" 303 304 func init() { 305 onlinedoc.Oas = ` + "`" + `{{.Doc}}` + "`" + ` 306 } 307 ` 308 309 // GenDoc generates OpenAPI 3.0 description json file. 310 // Not support alias type in vo file. 311 func GenDoc(dir string, ic astutils.InterfaceCollector, routePatternStrategy int) { 312 var ( 313 err error 314 svcname string 315 docfile string 316 gofile string 317 fi os.FileInfo 318 api v3.API 319 data []byte 320 paths map[string]v3.Path 321 tpl *template.Template 322 sqlBuf bytes.Buffer 323 source string 324 ) 325 svcname = ic.Interfaces[0].Name 326 docfile = filepath.Join(dir, strings.ToLower(svcname)+"_openapi3.json") 327 fi, err = os.Stat(docfile) 328 if err != nil && !os.IsNotExist(err) { 329 panic(err) 330 } 331 if fi != nil { 332 logrus.Warningln("file " + docfile + " will be overwritten") 333 } 334 gofile = filepath.Join(dir, strings.ToLower(svcname)+"_openapi3.go") 335 fi, err = os.Stat(gofile) 336 if err != nil && !os.IsNotExist(err) { 337 panic(err) 338 } 339 if fi != nil { 340 logrus.Warningln("file " + gofile + " will be overwritten") 341 } 342 paths = pathsOf(ic, routePatternStrategy) 343 api = v3.API{ 344 Openapi: "3.0.2", 345 Info: &v3.Info{ 346 Title: svcname, 347 Description: strings.Join(ic.Interfaces[0].Comments, "\n"), 348 Version: fmt.Sprintf("v%s", time.Now().Local().Format(constants.FORMAT10)), 349 }, 350 Servers: []v3.Server{ 351 { 352 URL: fmt.Sprintf("http://localhost:%d", 6060), 353 }, 354 }, 355 Paths: paths, 356 Components: &v3.Components{ 357 Schemas: v3helper.Schemas, 358 }, 359 } 360 data, err = json.Marshal(api) 361 err = ioutil.WriteFile(docfile, data, os.ModePerm) 362 if err != nil { 363 panic(err) 364 } 365 if tpl, err = template.New("doc.go.tmpl").Parse(gofileTmpl); err != nil { 366 panic(err) 367 } 368 if err = tpl.Execute(&sqlBuf, struct { 369 SvcPackage string 370 Doc string 371 }{ 372 SvcPackage: ic.Package.Name, 373 Doc: string(data), 374 }); err != nil { 375 panic(err) 376 } 377 source = strings.TrimSpace(sqlBuf.String()) 378 astutils.FixImport([]byte(source), gofile) 379 } 380 381 func ParseVo(dir string) { 382 var ( 383 err error 384 vos []v3.Schema 385 allMethods map[string][]astutils.MethodMeta 386 allConsts map[string][]string 387 ) 388 vodir := filepath.Join(dir, "vo") 389 var files []string 390 err = filepath.Walk(vodir, astutils.Visit(&files)) 391 if err != nil { 392 panic(err) 393 } 394 for _, file := range files { 395 v3helper.SchemaNames = append(v3helper.SchemaNames, getSchemaNames(file)...) 396 } 397 allMethods = make(map[string][]astutils.MethodMeta) 398 allConsts = make(map[string][]string) 399 for _, file := range files { 400 methods, consts := enumsOf(file) 401 for k, v := range methods { 402 allMethods[k] = append(allMethods[k], v...) 403 } 404 for k, v := range consts { 405 allConsts[k] = append(allConsts[k], v...) 406 } 407 } 408 for k, v := range allMethods { 409 if astutils.IsEnum(v) { 410 v3helper.Enums[k] = astutils.EnumMeta{ 411 Name: k, 412 Values: allConsts[k], 413 } 414 } 415 } 416 for _, file := range files { 417 vos = append(vos, schemasOf(file)...) 418 } 419 for _, item := range vos { 420 v3helper.Schemas[item.Title] = item 421 } 422 }