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  }