packer-cn/cmd/struct-markdown/main.go

179 lines
4.0 KiB
Go

package main
import (
"flag"
"fmt"
"go/ast"
"go/parser"
"go/token"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/fatih/camelcase"
"github.com/fatih/structtag"
)
func main() {
args := flag.Args()
if len(args) == 0 {
// Default: process the file
args = []string{os.Getenv("GOFILE")}
}
fname := args[0]
absFilePath, err := filepath.Abs(fname)
if err != nil {
panic(err)
}
projectRoot := os.Getenv("PROJECT_ROOT")
var paths []string
if projectRoot == "" {
// fall back to the packer root.
paths = strings.SplitAfter(absFilePath, "packer"+string(os.PathSeparator))
projectRoot = paths[0]
} else {
paths = strings.SplitAfter(absFilePath, projectRoot+string(os.PathSeparator))
}
builderName, _ := filepath.Split(paths[1])
builderName = strings.Trim(builderName, string(os.PathSeparator))
b, err := ioutil.ReadFile(fname)
if err != nil {
fmt.Printf("ReadFile: %+v", err)
os.Exit(1)
}
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, fname, b, parser.ParseComments)
if err != nil {
fmt.Printf("ParseFile: %+v", err)
os.Exit(1)
}
for _, decl := range f.Decls {
typeDecl, ok := decl.(*ast.GenDecl)
if !ok {
continue
}
typeSpec, ok := typeDecl.Specs[0].(*ast.TypeSpec)
if !ok {
continue
}
structDecl, ok := typeSpec.Type.(*ast.StructType)
if !ok {
continue
}
fields := structDecl.Fields.List
sourcePath := filepath.ToSlash(paths[1])
header := Struct{
SourcePath: sourcePath,
Name: typeSpec.Name.Name,
Filename: typeSpec.Name.Name + ".mdx",
Header: strings.TrimSpace(typeDecl.Doc.Text()),
}
required := Struct{
SourcePath: sourcePath,
Name: typeSpec.Name.Name,
Filename: typeSpec.Name.Name + "-required.mdx",
}
notRequired := Struct{
SourcePath: sourcePath,
Name: typeSpec.Name.Name,
Filename: typeSpec.Name.Name + "-not-required.mdx",
}
for _, field := range fields {
if len(field.Names) == 0 || field.Tag == nil {
continue
}
tag := field.Tag.Value[1:]
tag = tag[:len(tag)-1]
tags, err := structtag.Parse(tag)
if err != nil {
fmt.Printf("structtag.Parse(%s): err: %v", field.Tag.Value, err)
os.Exit(1)
}
// Leave undocumented tags out of markdown. This is useful for
// fields which exist for backwards compatability, or internal-use
// only fields
undocumented, _ := tags.Get("undocumented")
if undocumented != nil {
if undocumented.Name == "true" {
continue
}
}
mstr, err := tags.Get("mapstructure")
if err != nil {
continue
}
name := mstr.Name
if name == "" {
continue
}
var docs string
if field.Doc != nil {
docs = field.Doc.Text()
} else {
docs = strings.Join(camelcase.Split(field.Names[0].Name), " ")
}
if strings.Contains(docs, "TODO") {
continue
}
fieldType := string(b[field.Type.Pos()-1 : field.Type.End()-1])
fieldType = strings.ReplaceAll(fieldType, "*", `\*`)
switch fieldType {
case "time.Duration":
fieldType = `duration string | ex: "1h5m2s"`
case "config.Trilean":
fieldType = `boolean`
case "config.NameValues":
fieldType = `[]{name string, value string}`
case "config.KeyValues":
fieldType = `[]{key string, value string}`
}
field := Field{
Name: name,
Type: fieldType,
Docs: docs,
}
if req, err := tags.Get("required"); err == nil && req.Value() == "true" {
required.Fields = append(required.Fields, field)
} else {
notRequired.Fields = append(notRequired.Fields, field)
}
}
dir := filepath.Join(projectRoot, "website", "pages", "partials", builderName)
os.MkdirAll(dir, 0755)
for _, str := range []Struct{header, required, notRequired} {
if len(str.Fields) == 0 && len(str.Header) == 0 {
continue
}
outputPath := filepath.Join(dir, str.Filename)
outputFile, err := os.Create(outputPath)
if err != nil {
panic(err)
}
defer outputFile.Close()
err = structDocsTemplate.Execute(outputFile, str)
if err != nil {
fmt.Printf("%v", err)
os.Exit(1)
}
}
}
}