discordo/internal/config/gen.go

110 lines
2.8 KiB
Go

//go:build ignore
// Command gen writes docs/config.md from the config structs: a heading per table and option, named by its toml key, with its doc comment and default value.
package main
import (
"bytes"
"encoding"
"fmt"
"go/ast"
"go/parser"
"go/token"
"log"
"os"
"path/filepath"
"reflect"
"strings"
"github.com/BurntSushi/toml"
"github.com/ayn2op/discordo/internal/config"
)
const header = "<!-- GENERATED BY go generate; DO NOT EDIT. -->\n\n"
// docs maps "Type.Field" to the doc comment of that struct field.
var docs = map[string]string{}
type field struct {
key, doc string
value reflect.Value
}
// fields returns the fields of the struct v, with those of embedded structs in place.
func fields(v reflect.Value) []field {
var fs []field
t := v.Type()
for i := range t.NumField() {
f := t.Field(i)
if f.Anonymous {
fs = append(fs, fields(v.Field(i))...)
continue
}
fs = append(fs, field{f.Tag.Get("toml"), docs[t.Name()+"."+f.Name], v.Field(i)})
}
return fs
}
// isTable reports whether v is written as a table rather than as a value.
func isTable(v reflect.Value) bool {
switch v.Interface().(type) {
case toml.Marshaler, encoding.TextMarshaler:
return false
}
return v.Kind() == reflect.Struct
}
// write writes a section for each option of the table v at path: its doc comment, then its default value or, for a table, its own options.
func write(w *bytes.Buffer, path string, v reflect.Value) {
if path != "" {
path += "."
}
for _, f := range fields(v) {
name := path + f.key
fmt.Fprintf(w, "%s %s\n\n", strings.Repeat("#", strings.Count(name, ".")+1), name)
if f.doc != "" {
fmt.Fprintf(w, "%s\n", f.doc)
}
if isTable(f.value) {
write(w, name, f.value)
continue
}
// The encoder only writes whole documents, so the value is what follows the key.
var line bytes.Buffer
if err := toml.NewEncoder(&line).Encode(map[string]any{f.key: f.value.Interface()}); err != nil {
log.Fatal(err)
}
_, value, _ := strings.Cut(line.String(), " = ")
fmt.Fprintf(w, "Default: `%s`\n\n", strings.TrimSpace(value))
}
}
func main() {
names, _ := filepath.Glob("*.go")
for _, name := range names {
file, err := parser.ParseFile(token.NewFileSet(), name, nil, parser.ParseComments)
if err != nil {
log.Fatal(err)
}
ast.Inspect(file, func(n ast.Node) bool {
spec, ok := n.(*ast.TypeSpec)
if !ok {
return true
}
if s, ok := spec.Type.(*ast.StructType); ok {
for _, f := range s.Fields.List {
for _, name := range f.Names {
docs[spec.Name.Name+"."+name.Name] = f.Doc.Text()
}
}
}
return false
})
}
w := bytes.NewBufferString(header)
write(w, "", reflect.ValueOf(config.Default()))
if err := os.WriteFile("../../docs/config.md", append(bytes.TrimSpace(w.Bytes()), '\n'), 0o644); err != nil {
log.Fatal(err)
}
}