Skip to content

Instantly share code, notes, and snippets.

@Mic92
Last active September 9, 2024 11:07
Show Gist options
  • Save Mic92/b5616013356d0dbe08c2495c8ebf9003 to your computer and use it in GitHub Desktop.
Save Mic92/b5616013356d0dbe08c2495c8ebf9003 to your computer and use it in GitHub Desktop.
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