github.com/unionj-cloud/go-doudou/v2@v2.3.5/toolkit/astutils/interfacecollector.go (about)

     1  package astutils
     2  
     3  import (
     4  	"fmt"
     5  	"github.com/iancoleman/strcase"
     6  	"github.com/sirupsen/logrus"
     7  	"github.com/unionj-cloud/go-doudou/v2/toolkit/sliceutils"
     8  	"github.com/unionj-cloud/go-doudou/v2/toolkit/stringutils"
     9  	"go/ast"
    10  	"go/parser"
    11  	"go/token"
    12  	"net/http"
    13  	"regexp"
    14  	"strings"
    15  )
    16  
    17  // InterfaceCollector collect interfaces by parsing source code
    18  type InterfaceCollector struct {
    19  	Interfaces []InterfaceMeta
    20  	Package    PackageMeta
    21  	exprString func(ast.Expr) string
    22  	cmap       ast.CommentMap
    23  }
    24  
    25  // Visit traverse each node from source code
    26  func (ic *InterfaceCollector) Visit(n ast.Node) ast.Visitor {
    27  	return ic.Collect(n)
    28  }
    29  
    30  // Collect collects all interfaces from source code
    31  func (ic *InterfaceCollector) Collect(n ast.Node) ast.Visitor {
    32  	switch spec := n.(type) {
    33  	case *ast.Package:
    34  		return ic
    35  	case *ast.File: // actually it is package name
    36  		ic.Package = PackageMeta{
    37  			Name: spec.Name.Name,
    38  		}
    39  		return ic
    40  	case *ast.GenDecl:
    41  		if spec.Tok == token.TYPE {
    42  			comments := doc2Comments(spec.Doc)
    43  			for _, item := range spec.Specs {
    44  				typeSpec := item.(*ast.TypeSpec)
    45  				typeName := typeSpec.Name.Name
    46  				switch specType := typeSpec.Type.(type) {
    47  				case *ast.InterfaceType:
    48  					ic.Interfaces = append(ic.Interfaces, InterfaceMeta{
    49  						Name:     typeName,
    50  						Methods:  ic.field2Methods(specType.Methods.List),
    51  						Comments: comments,
    52  					})
    53  				}
    54  			}
    55  		}
    56  	}
    57  	return nil
    58  }
    59  
    60  // GetShelves_ShelfBooks_Book
    61  // shelves/:shelf/books/:book
    62  func Pattern(method string) (httpMethod string, endpoint string) {
    63  	httpMethods := []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
    64  	re1, err := regexp.Compile("_?[A-Z]")
    65  	if err != nil {
    66  		panic(err)
    67  	}
    68  	method = re1.ReplaceAllStringFunc(method, func(s string) string {
    69  		if strings.HasPrefix(s, "_") {
    70  			return "/:" + strings.ToLower(strings.TrimPrefix(s, "_"))
    71  		} else {
    72  			return "/" + strings.ToLower(s)
    73  		}
    74  	})
    75  	splits := strings.Split(method, "/")[1:]
    76  	httpMethod = httpMethods[1]
    77  	head := strings.ToUpper(splits[0])
    78  	if sliceutils.StringContains(httpMethods, head) {
    79  		httpMethod = head
    80  		splits = splits[1:]
    81  	}
    82  	return httpMethod, strings.Join(splits, "/")
    83  }
    84  
    85  func pathVariables(endpoint string) (ret []string) {
    86  	splits := strings.Split(endpoint, "/")
    87  	pvs := sliceutils.StringFilter(splits, func(item string) bool {
    88  		return stringutils.IsNotEmpty(item) && strings.HasPrefix(item, ":")
    89  	})
    90  	for _, v := range pvs {
    91  		ret = append(ret, strings.TrimPrefix(v, ":"))
    92  	}
    93  	return
    94  }
    95  
    96  func (ic *InterfaceCollector) field2Methods(list []*ast.Field) []MethodMeta {
    97  	var methods []MethodMeta
    98  	for _, method := range list {
    99  		if len(method.Names) == 0 {
   100  			panic("no method name")
   101  		}
   102  		mn := method.Names[0].Name
   103  
   104  		var mComments []string
   105  		var annotations []Annotation
   106  		if method.Doc != nil {
   107  			for _, comment := range method.Doc.List {
   108  				mComments = append(mComments, strings.TrimSpace(strings.TrimPrefix(comment.Text, "//")))
   109  				annotations = append(annotations, GetAnnotations(comment.Text)...)
   110  			}
   111  		}
   112  
   113  		ft, _ := method.Type.(*ast.FuncType)
   114  		var params []FieldMeta
   115  		if ft.Params != nil {
   116  			params = ic.field2Params(ft.Params.List)
   117  		}
   118  
   119  		httpMethod, endpoint := Pattern(mn)
   120  		pvs := pathVariables(endpoint)
   121  		for i := range params {
   122  			if sliceutils.StringContains(pvs, params[i].Name) {
   123  				params[i].IsPathVariable = true
   124  			}
   125  		}
   126  
   127  		var results []FieldMeta
   128  		if ft.Results != nil {
   129  			results = ic.field2Results(ft.Results.List)
   130  		}
   131  		methods = append(methods, MethodMeta{
   132  			Name:            mn,
   133  			Params:          params,
   134  			Results:         results,
   135  			Comments:        mComments,
   136  			Annotations:     annotations,
   137  			HasPathVariable: len(pvs) > 0,
   138  			HttpMethod:      httpMethod,
   139  		})
   140  	}
   141  	return methods
   142  }
   143  
   144  func (ic *InterfaceCollector) field2Params(list []*ast.Field) []FieldMeta {
   145  	var params []FieldMeta
   146  	pkeymap := make(map[string]int)
   147  	for _, param := range list {
   148  		pt := ic.exprString(param.Type)
   149  		if len(param.Names) > 0 {
   150  			for i, name := range param.Names {
   151  				field := FieldMeta{
   152  					Name: name.Name,
   153  					Type: pt,
   154  				}
   155  				var cnode ast.Node
   156  				if i == 0 {
   157  					cnode = param
   158  				} else {
   159  					cnode = name
   160  				}
   161  				if cmts, exists := ic.cmap[cnode]; exists {
   162  					for _, comment := range cmts[0].List {
   163  						field.Comments = append(field.Comments, strings.TrimSpace(strings.TrimPrefix(comment.Text, "//")))
   164  						field.Annotations = append(field.Annotations, GetAnnotations(comment.Text)...)
   165  					}
   166  				}
   167  				var validateTags []string
   168  				for _, item := range field.Annotations {
   169  					if item.Name == "@validate" {
   170  						validateTags = append(validateTags, item.Params...)
   171  					}
   172  				}
   173  				field.ValidateTag = strings.Join(validateTags, ",")
   174  				params = append(params, field)
   175  			}
   176  			continue
   177  		}
   178  		var pComments []string
   179  		var annotations []Annotation
   180  		if cmts, exists := ic.cmap[param]; exists {
   181  			for _, comment := range cmts[0].List {
   182  				pComments = append(pComments, strings.TrimSpace(strings.TrimPrefix(comment.Text, "//")))
   183  				annotations = append(annotations, GetAnnotations(comment.Text)...)
   184  			}
   185  		}
   186  		var pn string
   187  		elemt := strings.TrimPrefix(pt, "*")
   188  		if stringutils.IsNotEmpty(elemt) {
   189  			if strings.Contains(elemt, "[") {
   190  				elemt = elemt[strings.Index(elemt, "]")+1:]
   191  				elemt = strings.TrimPrefix(elemt, "*")
   192  			}
   193  			splits := strings.Split(elemt, ".")
   194  			_key := "p" + strcase.ToLowerCamel(splits[len(splits)-1][0:1])
   195  			if _, exists := pkeymap[_key]; exists {
   196  				pkeymap[_key]++
   197  				pn = _key + fmt.Sprintf("%d", pkeymap[_key])
   198  			} else {
   199  				pkeymap[_key]++
   200  				pn = _key
   201  			}
   202  		}
   203  		var validateTags []string
   204  		for _, item := range annotations {
   205  			if item.Name == "@validate" {
   206  				validateTags = append(validateTags, item.Params...)
   207  			}
   208  		}
   209  		params = append(params, FieldMeta{
   210  			Name:        pn,
   211  			Type:        pt,
   212  			Comments:    pComments,
   213  			Annotations: annotations,
   214  			ValidateTag: strings.Join(validateTags, ","),
   215  		})
   216  	}
   217  	return params
   218  }
   219  
   220  func (ic *InterfaceCollector) field2Results(list []*ast.Field) []FieldMeta {
   221  	var results []FieldMeta
   222  	rkeymap := make(map[string]int)
   223  	for _, result := range list {
   224  		var rComments []string
   225  		if cmts, exists := ic.cmap[result]; exists {
   226  			for _, comment := range cmts[0].List {
   227  				rComments = append(rComments, strings.TrimSpace(strings.TrimPrefix(comment.Text, "//")))
   228  			}
   229  		}
   230  		rt := ic.exprString(result.Type)
   231  		if len(result.Names) > 0 {
   232  			for _, name := range result.Names {
   233  				results = append(results, FieldMeta{
   234  					Name:     name.Name,
   235  					Type:     rt,
   236  					Tag:      "",
   237  					Comments: rComments,
   238  				})
   239  			}
   240  			continue
   241  		}
   242  		var rn string
   243  		elemt := strings.TrimPrefix(rt, "*")
   244  		if stringutils.IsNotEmpty(elemt) {
   245  			if strings.Contains(elemt, "[") {
   246  				elemt = elemt[strings.Index(elemt, "]")+1:]
   247  				elemt = strings.TrimPrefix(elemt, "*")
   248  			}
   249  			splits := strings.Split(elemt, ".")
   250  			_key := "r" + strcase.ToLowerCamel(splits[len(splits)-1][0:1])
   251  			if _, exists := rkeymap[_key]; exists {
   252  				rkeymap[_key]++
   253  				rn = _key + fmt.Sprintf("%d", rkeymap[_key])
   254  			} else {
   255  				rkeymap[_key]++
   256  				rn = _key
   257  			}
   258  		}
   259  		results = append(results, FieldMeta{
   260  			Name:     rn,
   261  			Type:     rt,
   262  			Tag:      "",
   263  			Comments: rComments,
   264  		})
   265  	}
   266  	return results
   267  }
   268  
   269  func doc2Comments(doc *ast.CommentGroup) []string {
   270  	var comments []string
   271  	if doc != nil {
   272  		for _, comment := range doc.List {
   273  			comments = append(comments, strings.TrimSpace(strings.TrimPrefix(comment.Text, "//")))
   274  		}
   275  	}
   276  	return comments
   277  }
   278  
   279  // NewInterfaceCollector initializes an InterfaceCollector
   280  func NewInterfaceCollector(exprString func(ast.Expr) string) *InterfaceCollector {
   281  	return &InterfaceCollector{
   282  		exprString: exprString,
   283  	}
   284  }
   285  
   286  // BuildInterfaceCollector initializes an InterfaceCollector and collects interfaces
   287  func BuildInterfaceCollector(file string, exprString func(ast.Expr) string) InterfaceCollector {
   288  	ic := NewInterfaceCollector(exprString)
   289  	fset := token.NewFileSet()
   290  	root, err := parser.ParseFile(fset, file, nil, parser.ParseComments)
   291  	if err != nil {
   292  		logrus.Panicln(err)
   293  	}
   294  	ic.cmap = ast.NewCommentMap(fset, root, root.Comments)
   295  	ast.Walk(ic, root)
   296  	return *ic
   297  }