Last active
September 9, 2024 11:07
-
-
Save Mic92/b5616013356d0dbe08c2495c8ebf9003 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
"go/ast" | |
"go/parser" | |
"go/token" | |
"log" | |
"os" | |
"strings" | |
"reflect" | |
"golang.org/x/tools/go/packages" | |
) | |
type DocData struct { | |
Package string `json:"package"` | |
Types []TypeDoc `json:"types"` | |
} | |
type TypeDoc struct { | |
Name string `json:"name"` | |
Doc string `json:"doc"` | |
Fields []FieldDoc `json:"fields"` | |
} | |
type FieldDoc struct { | |
Name string `json:"name"` | |
Type string `json:"type"` | |
Doc string `json:"doc"` | |
JSONName string `json:"json_name"` | |
Omitempty bool `json:"omitempty"` | |
Tags []string `json:"tags"` | |
} | |
func main() { | |
if len(os.Args) < 2 { | |
fmt.Println("Please provide a Go source file.") | |
return | |
} | |
filename := os.Args[1] | |
// Create the file set and parse the source file | |
fset := token.NewFileSet() | |
fileNode, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) | |
if err != nil { | |
log.Fatalf("Failed to parse file: %v", err) | |
} | |
// Load and type-check the package using go/packages | |
cfg := &packages.Config{ | |
Mode: packages.LoadSyntax, | |
Dir: ".", | |
Fset: fset, | |
} | |
pkgs, err := packages.Load(cfg) | |
if err != nil { | |
log.Fatalf("Failed to load package: %v", err) | |
} | |
// Get the package | |
if len(pkgs) == 0 { | |
log.Fatal("No packages found") | |
} | |
// Collect documentation data | |
docData := DocData{ | |
Package: fileNode.Name.Name, | |
Types: []TypeDoc{}, | |
} | |
// Extract types and their associated fields | |
for _, decl := range fileNode.Decls { | |
genDecl, ok := decl.(*ast.GenDecl) | |
if !ok || genDecl.Tok != token.TYPE { | |
continue | |
} | |
for _, spec := range genDecl.Specs { | |
typeSpec := spec.(*ast.TypeSpec) | |
typeName := typeSpec.Name.Name | |
doc := "" | |
if genDecl.Doc != nil { | |
doc = genDecl.Doc.Text() | |
} | |
// Only handle structs for now | |
structType, ok := typeSpec.Type.(*ast.StructType) | |
if !ok { | |
continue | |
} | |
typeDoc := TypeDoc{ | |
Name: typeName, | |
Doc: doc, | |
Fields: []FieldDoc{}, | |
} | |
// Process the fields of the struct | |
for _, field := range structType.Fields.List { | |
for _, fieldName := range field.Names { | |
jsonName, omitempty := parseJSONTag(getFieldTagString(field)) | |
fieldDoc := FieldDoc{ | |
Name: fieldName.Name, | |
Type: formatType(field.Type), | |
Doc: getFieldDocString(field), | |
JSONName: jsonName, | |
Omitempty: omitempty, | |
Tags: parseAllTags(getFieldTagString(field)), | |
} | |
typeDoc.Fields = append(typeDoc.Fields, fieldDoc) | |
} | |
} | |
docData.Types = append(docData.Types, typeDoc) | |
} | |
} | |
jsonData, err := json.MarshalIndent(docData, "", " ") | |
if err != nil { | |
log.Fatalf("Failed to marshal JSON: %v", err) | |
} | |
// Print JSON to standard output | |
fmt.Println(string(jsonData)) | |
} | |
func getFieldDocString(field *ast.Field) string { | |
if field.Doc != nil { | |
return field.Doc.Text() | |
} | |
return "" | |
} | |
func getFieldTagString(field *ast.Field) string { | |
if field.Tag != nil { | |
// Extract the tag value (strip quotes) | |
return strings.Trim(field.Tag.Value, "`") | |
} | |
return "" | |
} | |
func parseJSONTag(tag string) (string, bool) { | |
if tag == "" { | |
return "", false | |
} | |
// Split the tag string by spaces for multiple tags | |
tags := strings.Split(tag, " ") | |
for _, t := range tags { | |
if strings.HasPrefix(t, "json:") { | |
// Extract the json tag part | |
jsonTag := strings.TrimPrefix(t, "json:") | |
jsonTag = strings.Trim(jsonTag, "\"") | |
// Split by comma to get the field name and options | |
parts := strings.Split(jsonTag, ",") | |
jsonFieldName := parts[0] | |
omitempty := false | |
// Check if "omitempty" is one of the options | |
if len(parts) > 1 { | |
for _, option := range parts[1:] { | |
if option == "omitempty" { | |
omitempty = true | |
} | |
} | |
} | |
return jsonFieldName, omitempty | |
} | |
} | |
return "", false | |
} | |
// formatType extracts and formats the type of a field | |
func formatType(expr ast.Expr) string { | |
switch t := expr.(type) { | |
case *ast.Ident: | |
return t.Name | |
case *ast.SelectorExpr: | |
// For qualified types like time.Time | |
return fmt.Sprintf("%s.%s", formatType(t.X), t.Sel.Name) | |
case *ast.StarExpr: | |
// For pointer types | |
return "*" + formatType(t.X) | |
case *ast.ArrayType: | |
// For array types | |
return "[]" + formatType(t.Elt) | |
case *ast.MapType: | |
// For map types | |
return fmt.Sprintf("map[%s]%s", formatType(t.Key), formatType(t.Value)) | |
case *ast.FuncType: | |
return "func" | |
default: | |
return reflect.TypeOf(expr).String() | |
} | |
} | |
func parseAllTags(tag string) []string { | |
if tag == "" { | |
return []string{} | |
} | |
return strings.Split(tag, " ") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment