//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 = "\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) } }