complement/cmd/gendoc/main.go
kegsay ecbe6eb278
Documentation: completely rework how env vars are documented (#468)
* Documentation: completely rework how env vars are documented

Automatically generate the markdown based on line comments
in `type Complement struct {...}`. Rejig `README.md` to include
a link to `ENVIRONMENT.md`. Update sytest stats.

* Default to internal/config/config.go
2022-09-22 17:24:19 +01:00

132 lines
3.2 KiB
Go

package main
import (
"flag"
"fmt"
"go/ast"
"go/parser"
"go/token"
"log"
"os"
"sort"
"strings"
)
var configPath = flag.String("config", "internal/config/config.go", "The path to internal/config/config.go")
type VarDoc struct {
Name string
Description string
Default string
Type string
}
func NewVarDoc(docstring string) (vd VarDoc) {
lines := strings.Split(docstring, "\n")
isDescription := false
for _, l := range lines {
if strings.HasPrefix(l, "Name:") {
isDescription = false
vd.Name = strings.TrimSpace(strings.TrimPrefix(l, "Name:"))
}
if strings.HasPrefix(l, "Default:") {
isDescription = false
vd.Default = strings.TrimSpace(strings.TrimPrefix(l, "Default:"))
}
if strings.HasPrefix(l, "Description:") {
l = strings.TrimPrefix(l, "Description:")
isDescription = true
}
if isDescription {
vd.Description += strings.TrimSpace(l) + " "
}
}
return
}
func findComplementStruct(path string) *ast.StructType {
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
if err != nil {
log.Fatal(err)
}
var complementConfigType *ast.TypeSpec
FindStruct:
for _, d := range node.Decls {
typeNode, ok := d.(*ast.GenDecl)
if !ok || typeNode.Tok != token.TYPE { // we want `type` keywords
continue
}
for _, s := range typeNode.Specs {
typeSpec, ok := s.(*ast.TypeSpec)
if !ok {
continue
}
if typeSpec.Name.Name == "Complement" {
complementConfigType = typeSpec
break FindStruct
}
}
}
sType, ok := complementConfigType.Type.(*ast.StructType)
if !ok {
return nil
}
return sType
}
func typeForExpr(ex ast.Expr) string {
switch typeDecl := ex.(type) {
case *ast.Ident:
return typeDecl.Name
case *ast.SelectorExpr:
return typeDecl.Sel.Name
case *ast.ArrayType:
return "[]" + typeForExpr(typeDecl.Elt)
case *ast.MapType:
return "map[" + typeForExpr(typeDecl.Key) + "]" + typeForExpr(typeDecl.Value)
default:
return "-"
}
}
func main() {
flag.Parse()
if *configPath == "" {
flag.Usage()
os.Exit(1)
}
complement := findComplementStruct(*configPath)
if complement == nil {
log.Fatal("file does not contain type Complement struct {...}")
}
var varDocs []VarDoc
// loop each field looking for valid comments
for _, f := range complement.Fields.List {
fieldComment := f.Doc.Text()
vd := NewVarDoc(fieldComment)
if vd.Name == "" {
continue // not valid comment
}
vd.Type = typeForExpr(f.Type)
varDocs = append(varDocs, vd)
}
sort.Slice(varDocs, func(i, j int) bool {
return varDocs[i].Name < varDocs[j].Name
})
mdFileLines := []string{
"*This file is automatically generated via ./cmd/gendoc*",
"",
"## Complement Configuration",
"Complement is configured exclusively through the use of environment variables. These variables are described below.",
}
for _, vd := range varDocs {
mdFileLines = append(mdFileLines, fmt.Sprintf("\n#### `%v`", vd.Name))
mdFileLines = append(mdFileLines, vd.Description)
mdFileLines = append(mdFileLines, fmt.Sprintf("- Type: `%v`", vd.Type))
if vd.Default != "" {
mdFileLines = append(mdFileLines, fmt.Sprintf("- Default: %v", vd.Default))
}
}
fmt.Println(strings.Join(mdFileLines, "\n"))
}