diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be2b82f --- /dev/null +++ b/.gitignore @@ -0,0 +1,95 @@ +# ---> Go +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so +*.DS_Store + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof +*.sum + +# ---> JetBrains +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio + +*.iml + +## Directory-based project format: +.idea/ +# if you remove the above rule, at least ignore the following: + +# User-specific stuff: +# .idea/workspace.xml +# .idea/tasks.xml +# .idea/dictionaries + +# Sensitive or high-churn files: +# .idea/dataSources.ids +# .idea/dataSources.xml +# .idea/sqlDataSources.xml +# .idea/dynamic.xml +# .idea/uiDesigner.xml + +# Gradle: +# .idea/gradle.xml +# .idea/libraries + +# Mongo Explorer plugin: +# .idea/mongoSettings.xml + +## File-based project format: +*.ipr +*.iws + +## Plugin-specific files: + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties + +*/log/ + +bin/ +storage/ +coverage.out +count.out +*.out +dist/ + +static/dist + +cover.xml +coverage.html +coverage.txt +coverage.xml +report.html +report.xml + +vendor/ \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..f021ebb --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,45 @@ +project_name: type2md +builds: + - env: + - CGO_ENABLED=0 + - GOPROXY="https://goproxy.cn,direct" + flags: + - -trimpath + ldflags: + - -s -w + - -X "main.Version={{ .Version }}" + - -X "main.CommitID={{ .ShortCommit }}" + - -X "main.BuildTime={{ .Date }}" + main: . + +checksum: + name_template: 'checksums.txt' + +changelog: + sort: asc + use: gitlab + groups: + - title: Features + regexp: "^.*feat[(\\w)]*:+.*$" + order: 100 + - title: 'Bug fixes' + regexp: "^.*fix[(\\w)]*:+.*$" + order: 200 + - title: 'Documentation updates' + regexp: "^.*docs[(\\w)]*:+.*$" + order: 400 + - title: Others + order: 999 + filters: + exclude: + - '^test:' + - '^chore' + - 'merge conflict' + - '^ci' + - '^style' + - Merge pull request + - Merge remote-tracking branch + - Merge branch + +release: + footer: "Thanks for your support!" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..933399d --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +GO_BIN_PATH := $(if $(GOBIN),$(GOBIN),$(GOPATH)/bin) + +VERSION ?= dev +BUILD_TIME ?= `date "+%Y-%m-%d %H:%M:%S"` +COMMIT_ID ?= `git rev-parse --short HEAD` + +GO_BUILD := go build --trimpath --ldflags "-w -s \ +-X 'main.Version=$(VERSION)' \ +-X 'main.CommitID=$(COMMIT_ID)' \ +-X 'main.BuildTime=$(BUILD_TIME)'" + +all: + $(GO_BUILD) -o type2md ./*.go + mv type2md $(GO_BIN_PATH)/type2md \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7729bef --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# golang type define to markdown + + +## Usage + +```go +package docs + +//go:generate type2md github.com/eleztian/test Config + +``` + +```shell +go install github.com/eleztian/type2md +go generate ./... +``` +![img.png](docs/img.png) + diff --git a/docs/doc_config.md b/docs/doc_config.md new file mode 100644 index 0000000..58c3aae --- /dev/null +++ b/docs/doc_config.md @@ -0,0 +1,33 @@ +# Config Doc +Config doc. + +| key | 类型 | 必填 | 默认值 | 描述 | +|----------|----------|-----|------------------|--------------| +|Pre|[Hook](#ext.Hook)|false||| +|Post|[Hook](#ext.Hook)|false||| +|servers.{string}.host|string|false||| +|servers.{string}.port|int|true||- `22`
- `65522`| + +## ext.Mode +**Type:** int + +Mode mode define. + +| Value | 描述 | +|----------|--------------| +|1|mode q.| +|2|mode a.| + +## ext.Hook +Hook hook config. + +| key | 类型 | 必填 | 默认值 | 描述 | +|----------|----------|-----|------------------|--------------| +|name|string|true|example|hook name.| +|commands.[].|string|false||| +|envs.{string}.|string|false||| +|mode|[Mode](#ext.Mode)|false|1|run mode.| + +--- +GENERATED BY THE COMMAND [type2md](https://github.com/eleztian/type2md) +from github.com/eleztian/type2md/test.Config diff --git a/docs/img.png b/docs/img.png new file mode 100644 index 0000000..1953df6 Binary files /dev/null and b/docs/img.png differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..95346fb --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/eleztian/type2md + +go 1.18 + +require ( + github.com/KyleBanks/depth v1.2.1 + golang.org/x/tools v0.2.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + golang.org/x/mod v0.6.0 // indirect + golang.org/x/sys v0.1.0 // indirect +) diff --git a/main.go b/main.go new file mode 100644 index 0000000..04662d2 --- /dev/null +++ b/main.go @@ -0,0 +1,100 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "golang.org/x/mod/modfile" +) + +var ( + dstModPath string + dstTypes []string +) +var ( + fileName = flag.String("f", "", "file path") + title = flag.String("t", "", "file title") + showVersion = flag.Bool("v", false, "show version") +) + +func main() { + flag.Parse() + + if *showVersion { + PrintVersion() + os.Exit(0) + } + dstModPath = os.Args[len(os.Args)-2] + dstTypes = strings.Split(os.Args[len(os.Args)-1], ",") + + rootMod, _, _ := getModPath() + log.Println("Current Module:", rootMod) + + parser, err := NewParser(dstModPath) + if err != nil { + log.Fatalf(err.Error()) + } + for _, tp := range dstTypes { + log.Printf("start generate %s.%s\n", dstModPath, tp) + fs := parser.Parse(dstModPath, tp) + + md := Markdown{ + Title: *title, + MainStructName: dstModPath + "." + tp, + ObjTitleFunc: func(modPath string, typeName string) string { + modPath = strings.TrimPrefix(modPath, dstModPath) + modPath = strings.TrimPrefix(modPath, rootMod) + modPath = strings.TrimLeft(modPath, "./") + if modPath == "" { + return typeName + } else { + return fmt.Sprintf("%s.%s", modPath, typeName) + } + }, + } + if md.Title == "" { + md.Title = tp + " Doc" + } + + data := md.Generate(fs) + + filename := *fileName + if filename == "" { + filename = tp + "_doc.md" + } + log.Printf("start to save to %s\n", filename) + _ = os.WriteFile(filename, data, 0655) + } +} + +func getModPath() (string, string, error) { + current, _ := os.Getwd() + var path, _ = filepath.Abs(current) + for { + _, err := os.Stat(filepath.Join(path, "go.mod")) + if err != nil { + if !os.IsNotExist(err) { + return "", "", err + } + path, _ = filepath.Abs(filepath.Join(path, "../")) + if path == "" { + return "", "", nil + } + continue + } + content, err := os.ReadFile(filepath.Join(path, "go.mod")) + if err != nil { + return "", "", err + } + f, err := modfile.Parse("go.mod", content, nil) + if err != nil { + return "", "", err + } + + return f.Module.Mod.Path, strings.Replace(current, path, f.Module.Mod.Path, 1), nil + } +} diff --git a/struct_comment.go b/struct_comment.go new file mode 100644 index 0000000..89af1a2 --- /dev/null +++ b/struct_comment.go @@ -0,0 +1,26 @@ +package main + +import ( + "go/ast" + "strings" +) + +func TrimComment(src string) string { + res := strings.Trim(src, "\n ") + if len(res) != 0 && res[len(res)-1] != '.' { + res += "." + } + return res +} + +func GetDescribeFromComment(doc *ast.CommentGroup, comment *ast.CommentGroup) string { + res := "" + if doc != nil { + res += TrimComment(doc.Text()) + } + if comment != nil { + res += TrimComment(comment.Text()) + } + + return res +} diff --git a/struct_info.go b/struct_info.go new file mode 100644 index 0000000..bf0c0d5 --- /dev/null +++ b/struct_info.go @@ -0,0 +1,33 @@ +package main + +type StructInfo struct { + Describe string `json:"describe"` + Fields []FieldInfo `json:"fields"` + Enums *EnumInfo `json:"enums"` +} + +type FieldInfo struct { + Name string `json:"name"` + Type string `json:"type"` + Default string `json:"default"` + Require bool `json:"require"` + Enums EnumInfo `json:"enums"` + Describe string `json:"describe"` + Reference string `json:"reference"` + skipNum int +} + +func (fi FieldInfo) Copy() FieldInfo { + n := fi + enums := make([][2]string, 0, len(fi.Enums.Names)) + for _, d := range enums { + enums = append(enums, d) + } + n.Enums.Names = enums + return n +} + +type EnumInfo struct { + Type string + Names [][2]string // key: desc +} diff --git a/struct_info_test.go b/struct_info_test.go new file mode 100644 index 0000000..1a48cd6 --- /dev/null +++ b/struct_info_test.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + goparser "go/parser" + "go/token" + "testing" +) + +var codeStr = ` +package main + +// hello +type A struct { +} +` + +func TestName(t *testing.T) { + fset := token.NewFileSet() + f, err := goparser.ParseFile(fset, "", []byte(codeStr), goparser.ParseComments) + if err != nil { + panic(err) + } + + for _, c := range f.Comments { + fmt.Println(c.Text()) + } + fmt.Println(f.Doc) + //for _, c := range f.Decls { + //fmt.Println(c.(*ast.TypeSpec).Doc.Text()) + //} + +} diff --git a/struct_markdown.go b/struct_markdown.go new file mode 100644 index 0000000..6c6ce06 --- /dev/null +++ b/struct_markdown.go @@ -0,0 +1,114 @@ +package main + +import ( + "bytes" + "fmt" + "strings" +) + +type Markdown struct { + Title string + MainStructName string + Desc string + ObjTitleFunc func(modPath string, typeName string) string +} + +func (mk *Markdown) Generate(data map[string]StructInfo) []byte { + buf := bytes.NewBuffer(nil) + + buf.WriteString("# " + mk.Title + "\n") + + genStruct := func(structInfo StructInfo) { + buf.WriteString(structInfo.Describe) + buf.WriteString("\n") + + buf.WriteString( + "| key | 类型 | 必填 | 默认值 | 描述 |\n" + + "|----------|----------|-----|------------------|--------------|\n") + for _, item := range structInfo.Fields { + buf.WriteString("|") + buf.WriteString(item.Name) + buf.WriteString("|") + if item.Reference != "" { + buf.WriteString(fmt.Sprintf("[%s](#%s)", item.Type, mk.ObjTitleFunc(item.Reference, item.Type))) + } else { + buf.WriteString(item.Type) + } + buf.WriteString("|") + buf.WriteString(fmt.Sprintf("%v", item.Require)) + buf.WriteString("|") + buf.WriteString(item.Default) + buf.WriteString("|") + buf.WriteString(item.Describe) + if len(item.Enums.Names) != 0 { + if item.Describe != "" { + buf.WriteString("
") + } + for idx, nd := range item.Enums.Names { + if nd[1] != "" { + buf.WriteString(fmt.Sprintf("- `%s`:%s
", nd[0], nd[1])) + } else { + buf.WriteString(fmt.Sprintf("- `%s`", nd[0])) + } + if idx != len(item.Enums.Names)-1 { + buf.WriteString("
") + } + } + + } + buf.WriteString("|\n") + } + } + + genEnums := func(enums *EnumInfo, desc string) { + buf.WriteString(fmt.Sprintf("**Type:** %s\n\n", enums.Type)) + buf.WriteString(desc) + buf.WriteString("\n") + + if len(enums.Names) != 0 { + buf.WriteString("| Value | 描述 |\n") + buf.WriteString("|----------|--------------|\n") + for _, info := range enums.Names { + + buf.WriteString("|") + buf.WriteString(info[0]) + buf.WriteString("|") + buf.WriteString(info[1]) + buf.WriteString("|\n") + } + } + + } + if mk.MainStructName != "" { + genStruct(data[mk.MainStructName]) + delete(data, mk.MainStructName) + } + + for name, structInfo := range data { + var ( + modPath string + typeName string + ) + lastIdx := strings.LastIndex(name, ".") + if lastIdx < 0 { + typeName = name + } else { + typeName = name[lastIdx+1:] + modPath = name[:lastIdx] + } + + if structInfo.Enums != nil { + buf.WriteString(fmt.Sprintf("\n## %s\n", mk.ObjTitleFunc(modPath, typeName))) + genEnums(structInfo.Enums, structInfo.Describe) + } else { + buf.WriteString(fmt.Sprintf("\n## %s\n", mk.ObjTitleFunc(modPath, typeName))) + genStruct(structInfo) + } + } + + buf.WriteString(fmt.Sprintf("\n---\nGENERATED BY THE COMMAND [type2md](https://github."+ + "com/eleztian/type2md)\nfrom %s\n", + mk.MainStructName)) + + return buf.Bytes() +} diff --git a/struct_parser.go b/struct_parser.go new file mode 100644 index 0000000..72e4ced --- /dev/null +++ b/struct_parser.go @@ -0,0 +1,403 @@ +package main + +import ( + "fmt" + "go/ast" + goparser "go/parser" + "go/token" + "go/types" + "strings" + + "golang.org/x/tools/go/loader" +) + +type Parser struct { + Tag string + RootModPath string + program *loader.Program + loaderCfg *loader.Config + fileImportNamed map[*ast.File]map[string]string + fileTypeDoc map[*ast.File]map[string]string +} + +func NewParser(modPath string) (*Parser, error) { + cfg := &loader.Config{ + ParserMode: goparser.ParseComments, + } + cfg.Import(modPath) + + p, err := cfg.Load() + if err != nil { + return nil, err + } + res := &Parser{ + Tag: "json", + RootModPath: "", + program: p, + loaderCfg: cfg, + fileImportNamed: map[*ast.File]map[string]string{}, + fileTypeDoc: map[*ast.File]map[string]string{}, + } + + return res, nil +} + +func (p *Parser) getFileImportNamedMap(file *ast.File) map[string]string { + namedImportMap := map[string]string{} + for _, imp := range file.Imports { + modPath := strings.Trim(imp.Path.Value, "\"") + if imp.Name == nil { + namedImportMap[p.program.Package(modPath).Pkg.Name()] = modPath + } else { + namedImportMap[imp.Name.Name] = modPath + } + } + return namedImportMap +} + +func (p *Parser) getFileTypeDocMap(file *ast.File) map[string]string { + typeDocMap := map[string]string{} + for _, imp := range file.Decls { + genDecl, ok := imp.(*ast.GenDecl) + if !ok { + continue + } + if genDecl.Tok != token.TYPE { + continue + } + var comment = "" + if genDecl.Doc != nil { + comment = TrimComment(genDecl.Doc.Text()) + if comment != "" { + comment += "\n" + } + } + for _, spec := range genDecl.Specs { + if specType, ok := spec.(*ast.TypeSpec); ok { + var subComment = GetDescribeFromComment(specType.Doc, specType.Comment) + typeDocMap[specType.Name.Name] = comment + subComment + } + } + } + + return typeDocMap +} + +func (p *Parser) parseFileStruct( + modPath string, + file *ast.File, + exParseMap map[string]struct{}, + objName string, + objStructType *ast.StructType) map[string]StructInfo { + + res := make(map[string]StructInfo, 0) + typeKey := TypeKey(modPath, objName) + + namedImportMap := p.fileImportNamed[file] + typeDocMap := p.fileTypeDoc[file] + + structInfo := p.parseStruct(objStructType, typeDocMap[objName]) + res[typeKey] = structInfo + for idx, field := range structInfo.Fields { + if field.Reference == "" { + continue + } + subModPath := "" + if field.Reference == "." { + subModPath = modPath + } else { + subModPath = namedImportMap[field.Reference] + } + structInfo.Fields[idx].Reference = subModPath + + key := TypeKey(subModPath, field.Type) + if _, ok := exParseMap[key]; !ok { + for typeName, fields := range p.Parse(subModPath, field.Type) { + res[typeName] = fields + } + exParseMap[key] = struct{}{} + } + } + + return res +} + +func (p *Parser) Parse(modPath string, typeName string) map[string]StructInfo { + pktInfo := p.program.Package(modPath) + if pktInfo == nil { + return map[string]StructInfo{} + } + res := make(map[string]StructInfo, 0) + exParseMap := make(map[string]struct{}) + + for _, file := range pktInfo.Files { + if _, ok := p.fileImportNamed[file]; !ok { + p.fileImportNamed[file] = p.getFileImportNamedMap(file) + } + if _, ok := p.fileTypeDoc[file]; !ok { + p.fileTypeDoc[file] = p.getFileTypeDocMap(file) + } + obj := file.Scope.Lookup(typeName) + if obj == nil { + continue + } + objTypeSpec := obj.Decl.(*ast.TypeSpec) + var objType = objTypeSpec.Type + if v, ok := objType.(*ast.StarExpr); ok { + objType = v.X + } + switch ost := objType.(type) { + case *ast.StructType: + structMaps := p.parseFileStruct(modPath, file, exParseMap, obj.Name, ost) + for name, info := range structMaps { + res[name] = info + } + case *ast.Ident: + if ost.Obj == nil { // 基本数据类型, 枚举值 + enums := p.getEnumTypeValues(&pktInfo.Info, typeName, file.Decls) + res[TypeKey(modPath, typeName)] = StructInfo{ + Describe: p.fileTypeDoc[file][typeName], + Enums: &EnumInfo{ + Type: ost.Name, + Names: enums, + }} + } + case *ast.SelectorExpr: + subModPath := p.fileImportNamed[file][ost.X.(*ast.Ident).Name] + subTypeName := ost.Sel.Name + structMaps := p.Parse(subModPath, subTypeName) + oldKey := TypeKey(subModPath, subTypeName) + info := structMaps[oldKey] + info.Describe += fmt.Sprintf("alias `%s`", oldKey) + + structMaps[TypeKey(modPath, typeName)] = info + delete(structMaps, oldKey) + return structMaps + } + } + return res +} + +func (p *Parser) getEnumTypeValues(pktInfo *types.Info, name string, decls []ast.Decl) [][2]string { + res := make([][2]string, 0) + +EXIT: + for _, obj := range decls { + + if genDecl, ok := obj.(*ast.GenDecl); ok { + if genDecl.Tok != token.CONST || len(genDecl.Specs) == 0 { + continue + } + firstSpec := genDecl.Specs[0] + if valueSpec, ok := firstSpec.(*ast.ValueSpec); ok { + t, ok := valueSpec.Type.(*ast.Ident) + if !ok || t.Name != name { + continue + } + } else { + continue + } + + for _, s := range genDecl.Specs { + v := s.(*ast.ValueSpec) // safe because decl.Tok == token.CONST + for _, name := range v.Names { + c := pktInfo.ObjectOf(name).(*types.Const) + res = append(res, [2]string{ + c.Val().ExactString(), + GetDescribeFromComment(v.Doc, v.Comment), + }) + } + } + break EXIT + } + } + + return res +} + +func (p *Parser) parseTypeExpr(obj ast.Expr) []FieldInfo { + if v, ok := obj.(*ast.StarExpr); ok { + obj = v.X + } + var res []FieldInfo + switch ot := obj.(type) { + case *ast.SelectorExpr: + res = []FieldInfo{{Type: ot.Sel.Name, Reference: ot.X.(*ast.Ident).Name, skipNum: 1}} + case *ast.Ident: + field := FieldInfo{ + Type: ot.Name, + } + if ot.Obj != nil { + field.Reference = "." + } + res = append(res, field) + case *ast.StructType: + res = p.parseStruct(ot, "").Fields + case *ast.MapType: + prefix := fmt.Sprintf("{%s}.", ot.Key) + res = p.parseTypeExpr(ot.Value) + for idx := range res { + res[idx].Name = prefix + res[idx].Name + } + case *ast.SliceExpr: + prefix := "[]." + res = p.parseTypeExpr(ot.X) + for idx := range res { + res[idx].Name = prefix + res[idx].Name + } + case *ast.ArrayType: + prefix := "[]" + t, ok := ot.Len.(*ast.BasicLit) + if ok { + prefix = fmt.Sprintf("[%s]", t.Value) + } + res = p.parseTypeExpr(ot.Elt) + for idx := range res { + res[idx].Name = prefix + res[idx].Name + } + } + return res +} + +func (p *Parser) parseStruct(objStructType *ast.StructType, desc string) StructInfo { + res := StructInfo{ + Describe: desc, + Fields: make([]FieldInfo, 0, len(objStructType.Fields.List)), + } + + for _, f := range objStructType.Fields.List { + if f.Names[0].Name[0] <= 'Z' && f.Names[0].Name[0] >= 'A' { + res.Fields = append(res.Fields, p.parseStructField(f)...) + } + } + + return res +} + +func (p *Parser) parseStructField(f *ast.Field) []FieldInfo { + res := make([]FieldInfo, 0) + + tagStr := "" + if f.Tag != nil { + tagStr = strings.Trim(f.Tag.Value, "`") + } + tagInfo := ParseStructTag(p.Tag, tagStr) + if tagInfo.Name == "-" { + return res + } + + baseField := FieldInfo{ + Name: tagInfo.Name, + Default: tagInfo.Default, + Require: tagInfo.Require, + Enums: EnumInfo{ + Names: tagInfo.Enums, + }, + } + if baseField.Name == "" { + baseField.Name = f.Names[0].Name + } + + baseField.Describe += GetDescribeFromComment(f.Doc, f.Comment) + + if s, ok := f.Type.(*ast.StarExpr); ok { + f.Type = s.X + } + switch tt := f.Type.(type) { + case *ast.Ident: + baseField.Type = tt.Name + if tt.Obj != nil { + baseField.Reference = "." + } + if tagInfo.Inline && baseField.Reference == "." { + for _, field := range p.parseTypeExpr(tt.Obj.Decl.(*ast.TypeSpec).Type) { + res = append(res, field) + } + } else { + res = append(res, baseField) + } + case *ast.StructType: + for _, f := range p.parseStruct(tt, baseField.Describe).Fields { + if tagInfo.Inline { + res = append(res, f) + } else { + if f.skipNum != 0 { + field := baseField.Copy() + field.Type = f.Type + if f.Name != "" { + field.Name += "." + f.Name + } + res = append(res, field) + } else { + f.Name = baseField.Name + "." + f.Name + res = append(res, f) + } + } + } + case *ast.MapType: + baseField.Name += fmt.Sprintf(".{%s}", tt.Key) + + var subFields = p.parseTypeExpr(tt.Value) + for _, f := range subFields { + if f.skipNum != 0 { + field := baseField.Copy() + field.Type = f.Type + field.Reference = f.Reference + if f.Name != "" { + field.Name += "." + f.Name + } + res = append(res, field) + } else { + f.Name = baseField.Name + "." + f.Name + res = append(res, f) + } + } + case *ast.SliceExpr: + baseField.Name += ".[]" + for _, f := range p.parseTypeExpr(tt.X) { + if f.skipNum != 0 { + field := baseField.Copy() + field.Type = f.Type + field.Reference = f.Reference + if f.Name != "" { + field.Name += "." + f.Name + } + res = append(res, field) + } else { + f.Name = baseField.Name + "." + f.Name + res = append(res, f) + } + } + case *ast.ArrayType: + t, ok := tt.Len.(*ast.BasicLit) + if ok { + baseField.Name += fmt.Sprintf(".[%s]", t.Value) + } else { + baseField.Name += ".[]" + } + for _, f := range p.parseTypeExpr(tt.Elt) { + if f.skipNum != 0 { + field := baseField.Copy() + field.Type = f.Type + field.Reference = f.Reference + if f.Name != "" { + field.Name += "." + f.Name + } + res = append(res, field) + } else { + f.Name = baseField.Name + "." + f.Name + res = append(res, f) + } + } + case *ast.SelectorExpr: + baseField.Type = tt.Sel.Name + baseField.Reference = tt.X.(*ast.Ident).Name + res = append(res, baseField) + } + + return res +} + +func TypeKey(modPath string, typeName string) string { + return modPath + "." + typeName +} diff --git a/struct_tag.go b/struct_tag.go new file mode 100644 index 0000000..2590f9a --- /dev/null +++ b/struct_tag.go @@ -0,0 +1,55 @@ +package main + +import ( + "reflect" + "strings" +) + +type TagInfo struct { + Name string + Default string + Require bool + Inline bool + Enums [][2]string +} + +func ParseStructTag(tagType string, tagStr string) *TagInfo { + res := &TagInfo{} + tag := strings.TrimSpace(reflect.StructTag(tagStr).Get(tagType)) + if tag != "" { + sp := strings.Split(tag, ",") + res.Name = strings.TrimSpace(sp[0]) + for _, item := range sp[1:] { + item = strings.TrimSpace(item) + if item == "inline" { + res.Inline = true + } + } + } + res.Default, _ = reflect.StructTag(tagStr).Lookup("default") + _, res.Require = reflect.StructTag(tagStr).Lookup("require") + enumsStr, _ := reflect.StructTag(tagStr).Lookup("enums") + + if enumsStr != "" { + res.Enums = make([][2]string, 0) + sp := strings.Split(enumsStr, ",") + for _, k := range sp { + var ( + value string + desc string + ) + idx := strings.Index(k, ":") + if idx >= 0 { + value = k[:idx] + desc = k[idx+1:] + } else { + value = k + } + res.Enums = append(res.Enums, [2]string{ + value, desc, + }) + } + } + + return res +} diff --git a/test/Config_doc.md b/test/Config_doc.md new file mode 100644 index 0000000..ae880e0 --- /dev/null +++ b/test/Config_doc.md @@ -0,0 +1,33 @@ +# Config Doc +Config doc. + +| key | 类型 | 必填 | 默认值 | 描述 | +|----------|----------|-----|------------------|--------------| +|Pre|[Hook](#ext.Hook)|false||| +|Post|[Hook](#ext.Hook)|false||| +|servers.{string}.host|string|false||| +|servers.{string}.port|int|true||- `22`
- `65522`| + +## ext.Hook +Hook hook config. + +| key | 类型 | 必填 | 默认值 | 描述 | +|----------|----------|-----|------------------|--------------| +|name|string|true|example|hook name.| +|commands.[].|string|false||| +|envs.{string}.|string|false||| +|mode|[Mode](#ext.Mode)|false|1|run mode.| + +## ext.Mode +**Type:** int + +Mode mode define. + +| Value | 描述 | +|----------|--------------| +|1|mode q.| +|2|mode a.| + +--- +GENERATED BY THE COMMAND [type2md](https://github.com/eleztian/type2md) +from github.com/eleztian/type2md/test.Config diff --git a/test/ext/hook.go b/test/ext/hook.go new file mode 100644 index 0000000..b16e7f1 --- /dev/null +++ b/test/ext/hook.go @@ -0,0 +1,17 @@ +package ext + +// Hook hook config. +type Hook struct { + Name string `json:"name" require:"" default:"example"` // hook name + Commands []string `json:"commands"` // command list + Envs map[string]string `json:"envs"` // env key map + Mode Mode `json:"mode" default:"1"` // run mode +} + +// Mode mode define. +type Mode int + +const ( + Mode_Q Mode = iota + 1 // mode q + Mode_A // mode a +) diff --git a/test/test.go b/test/test.go new file mode 100644 index 0000000..4e32a95 --- /dev/null +++ b/test/test.go @@ -0,0 +1,15 @@ +package test + +import "github.com/eleztian/type2md/test/ext" + +//go:generate type2md -f ../docs/doc_config.md github.com/eleztian/type2md/test Config + +// Config doc. +type Config struct { + Pre ext.Hook + Post *ext.Hook + Servers map[string]struct { + Host string `json:"host"` + Port int `json:"port" enums:"22,65522" require:""` + } `json:"servers"` // server list +} diff --git a/version.go b/version.go new file mode 100644 index 0000000..ab25ccf --- /dev/null +++ b/version.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + "os" +) + +var ( + Version = "v1.0.0" + CommitID = "" + BuildTime = "" +) + +func PrintVersion() { + fmt.Printf(`%s +--- +Parse the source code through the ast syntax tree, +extract the specified structure definition and +convert it into a markdown file. +---- +Version : %s +CommitID : %s +BuildTime: %s +Author : MoreSec CPF 中间件团队 +`, os.Args[0], Version, CommitID, BuildTime) +}