diff --git a/parser/parser.go b/parser/parser.go new file mode 100644 index 0000000..2b93e3a --- /dev/null +++ b/parser/parser.go @@ -0,0 +1,185 @@ +package beeParser + +import ( + "encoding/json" + "errors" + "go/ast" + "go/importer" + "go/parser" + "go/token" + "go/types" +) + +// StructField defines struct field +type StructField struct { + Name string + Type types.Type + NestedType *StructNode + Comment string + Doc string + Tag string +} + +// Key returns the key of the field +func (sf *StructField) Key() string { + return sf.Name +} + +// Value returns the value of the field +// if the field contains nested struct, it will return a nested result +func (sf *StructField) Value() interface{} { + if sf.NestedType != nil { + return sf.NestedType.ToKV() + } + + return "" +} + +// StructNode defines struct node +type StructNode struct { + Name string + Fields []*StructField +} + +// ToKV transfers struct to key value pair +func (sn *StructNode) ToKV() map[string]interface{} { + value := map[string]interface{}{} + for _, field := range sn.Fields { + value[field.Key()] = field.Value() + } + return value +} + +// StructParser parses structs in given file or string +type StructParser struct { + MainStruct *StructNode + Info types.Info +} + +// NewStructParser is the constructor of StructParser +// filePath and src follow the same rule with go/parser.ParseFile +// If src != nil, ParseFile parses the source from src and the filename is only used when recording position information. The type of the argument for the src parameter must be string, []byte, or io.Reader. If src == nil, ParseFile parses the file specified by filename. +// rootStruct is the root struct we want to use +func NewStructParser(filePath string, src interface{}, rootStruct string) (*StructParser, error) { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, filePath, src, parser.ParseComments) + if err != nil { + return nil, err + } + + info := types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + } + conf := types.Config{ + Importer: importer.ForCompiler(fset, "source", nil), + } + _, err = conf.Check("src", fset, []*ast.File{f}, &info) + if err != nil { + return nil, err + } + + cg := &StructParser{ + Info: info, + } + + ast.Inspect(f, func(n ast.Node) bool { + // ast.Print(nil, n) + ts, ok := n.(*ast.TypeSpec) + if !ok || ts.Type == nil { + return true + } + + structName := ts.Name.Name + if structName != rootStruct { + return true + } + + s, ok := ts.Type.(*ast.StructType) + if !ok { + return true + } + + cg.MainStruct = cg.ParseStruct(structName, s) + return false + }) + + if cg.MainStruct == nil { + return nil, errors.New("non-exist root struct") + } + + return cg, nil +} + +func (cg *StructParser) ToJSON() ([]byte, error) { + value := cg.MainStruct.ToKV() + return json.MarshalIndent(value, "", " ") +} + +// ParseField parses struct field in nested way +func (cg *StructParser) ParseField(field *ast.Field) *StructField { + // ast.Print(nil, field) + fieldName := field.Names[0].Name + fieldType := cg.Info.TypeOf(field.Type) + + fieldTag := "" + if field.Tag != nil { + fieldTag = field.Tag.Value + } + fieldComment := "" + if field.Comment != nil { + fieldComment = field.Comment.Text() + } + fieldDoc := "" + if field.Doc != nil { + fieldDoc = field.Doc.Text() + } + + var nestedStruct *StructNode + if s, isInlineStruct := field.Type.(*ast.StructType); isInlineStruct { + nestedStruct = cg.ParseStruct("", s) + } + + if _, isNamedStructorBasic := field.Type.(*ast.Ident); isNamedStructorBasic && field.Type.(*ast.Ident).Obj != nil { + ts, ok := field.Type.(*ast.Ident).Obj.Decl.(*ast.TypeSpec) + if !ok || ts.Type == nil { + return nil + } + + s, ok := ts.Type.(*ast.StructType) + if !ok { + return nil + } + nestedStruct = cg.ParseStruct(ts.Name.Name, s) + } + // fieldType.(*types.Basic) // basic type + // *ast.ArrayType: + // *ast.MapType: + // *ast.SelectorExpr: // third party + + return &StructField{ + Name: fieldName, + Type: fieldType, + Tag: fieldTag, + Comment: fieldComment, + Doc: fieldDoc, + NestedType: nestedStruct, + } +} + +// ParseStruct parses struct in nested way +func (cg *StructParser) ParseStruct(structName string, s *ast.StructType) *StructNode { + fields := []*StructField{} + for _, field := range s.Fields.List { + parsedField := cg.ParseField(field) + if parsedField != nil { + fields = append(fields, parsedField) + } + } + + return &StructNode{ + Name: structName, + Fields: fields, + } +} diff --git a/parser/parser_test.go b/parser/parser_test.go new file mode 100644 index 0000000..4ba578a --- /dev/null +++ b/parser/parser_test.go @@ -0,0 +1,60 @@ +package beeParser + +import ( + "fmt" + "log" +) + +func ExampleStructParser() { + const src = ` +package p + +import ( + "net/http" +) + +type StructB struct { + Field1 string +} +type StructA struct { + Field1 string + Field2 struct{ + a string + b string + } + Field3 []string + Field4 map[string]string + Field5 http.SameSite + Field6 func(int) + Field7 StructB +} + +` + cg, err := NewStructParser("src.go", src, "StructA") + if err != nil { + log.Fatal(err) + } + + b, err := cg.ToJSON() + if err != nil { + log.Fatal(err) + } + + fmt.Println(string(b)) + + // Output: + // { + // "Field1": "", + // "Field2": { + // "a": "", + // "b": "" + // }, + // "Field3": "", + // "Field4": "", + // "Field5": "", + // "Field6": "", + // "Field7": { + // "Field1": "" + // } + // } +}