Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ require (
github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7
github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.10
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0
Expand Down Expand Up @@ -101,6 +103,7 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudevents/sdk-go/v2 v2.16.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
Expand Down Expand Up @@ -143,10 +146,14 @@ require (
github.com/prometheus/procfs v0.20.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sanity-io/litter v1.5.5 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
Expand All @@ -155,6 +162,7 @@ require (
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sys v0.45.0 // indirect
Expand Down
21 changes: 20 additions & 1 deletion go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

78 changes: 51 additions & 27 deletions pkg/config/configdoc/configdoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,38 @@ import (
const (
FieldDefault = "# Default"
FieldExample = "# Example"
// FieldDocsOnly marks a field that is documented but left out of every example - both the
// document's example config and the code block of the table it belongs to. Use it for a
// setting that only applies in a mode the examples do not show, so that a reader can copy
// any example verbatim and get a configuration that works.
FieldDocsOnly = "# Docs only"

TokenAdvanced = "**ADVANCED**"
)

// Generate returns MarkDown documentation generated from the TOML string.
// - Each field but include a trailing comment of either FieldDefault or FieldExample.
// - Each field but include a trailing comment of FieldDefault, FieldExample or FieldDocsOnly.
// - If a description begins with TokenAdvanced, then a warning will be included.
// - The markdown wil begin with the header, followed by the example
// - Extended descriptions can be applied to top level tables
func Generate(toml, header, example string, extendedDescriptions map[string]string) (string, error) {
items, err := parseTOMLDocs(toml, extendedDescriptions)
return GenerateWith(TOML{}, toml, header, example, extendedDescriptions)
}

// GenerateWith is Generate for a document written in the syntax described by format. Generate
// is GenerateWith(TOML{}, ...).
func GenerateWith(format Format, doc, header, example string, extendedDescriptions map[string]string) (string, error) {
items, err := parseDocs(format, doc, extendedDescriptions)
var sb strings.Builder

sb.WriteString(header)
sb.WriteString(`
## Example

`)
sb.WriteString("```toml\n")
sb.WriteString("```")
sb.WriteString(format.Name())
sb.WriteString("\n")
sb.WriteString(example)
sb.WriteString("\n```\n\n")

Expand All @@ -53,16 +66,18 @@ func (d lines) String() string {
}

type table struct {
lang string
name string
codes lines
adv bool
desc lines
extended string
}

func newTable(line string, desc lines, extendedDescriptions map[string]string) *table {
func newTable(lang, line, name string, desc lines, extendedDescriptions map[string]string) *table {
t := &table{
name: strings.Trim(line, "[]"),
lang: lang,
name: name,
codes: []string{line},
desc: desc,
}
Expand All @@ -78,9 +93,10 @@ func newTable(line string, desc lines, extendedDescriptions map[string]string) *
return t
}

func newArrayOfTables(line string, desc lines, extendedDescriptions map[string]string) *table {
func newArrayOfTables(lang, line, name string, desc lines, extendedDescriptions map[string]string) *table {
t := &table{
name: strings.Trim(strings.Trim(line, FieldExample), "[]"),
lang: lang,
name: name,
codes: []string{line},
desc: desc,
}
Expand All @@ -105,7 +121,7 @@ func (t table) advanced() string {

func (t table) code() string {
if t.extended == "" {
return fmt.Sprint("```toml\n", t.codes, "\n```\n")
return fmt.Sprint("```", t.lang, "\n", t.codes, "\n```\n")
}
return ""
}
Expand All @@ -120,16 +136,18 @@ func (t *table) String() string {
}

type keyval struct {
lang string
name string
code string
adv bool
desc lines
}

func newKeyval(line string, desc lines) keyval {
func newKeyval(lang, line, name string, desc lines) keyval {
line = strings.TrimSpace(line)
kv := keyval{
name: line[:strings.Index(line, " ")],
lang: lang,
name: name,
code: line,
desc: desc,
}
Expand All @@ -155,38 +173,39 @@ func (k keyval) String() string {
}
return fmt.Sprint("### ", name, "\n",
k.advanced(),
"```toml\n",
"```", k.lang, "\n",
k.code,
"\n```\n",
k.desc)
}

func parseTOMLDocs(s string, extendedDescriptions map[string]string) (items []fmt.Stringer, err error) {
func parseDocs(format Format, s string, extendedDescriptions map[string]string) (items []fmt.Stringer, err error) {
defer func() { _, err = config.MultiErrorList(err) }()
globalTable := table{name: "Global"}
globalTable := table{lang: format.Name(), name: "Global"}
currentTable := &globalTable
items = append(items, currentTable)
var desc lines
defaultMarker, exampleMarker, docsOnlyMarker := format.DefaultMarker(), format.ExampleMarker(), format.DocsOnlyMarker()
for line := range strings.SplitSeq(s, "\n") {
if strings.HasPrefix(line, "#") {
// comment
desc = append(desc, strings.TrimSpace(line[1:]))
} else if strings.TrimSpace(line) == "" {
// empty
parsed := format.ParseLine(line)
switch parsed.Kind {
case LineComment:
desc = append(desc, parsed.Text)
case LineBlank:
if len(desc) > 0 {
items = append(items, desc)
desc = nil
}
} else if strings.HasPrefix(line, "[[") {
currentTable = newArrayOfTables(line, desc, extendedDescriptions)
case LineArrayOfTables:
currentTable = newArrayOfTables(format.Name(), line, parsed.Text, desc, extendedDescriptions)
items = append(items, currentTable)
desc = nil
} else if strings.HasPrefix(line, "[") {
currentTable = newTable(line, desc, extendedDescriptions)
case LineTable:
currentTable = newTable(format.Name(), line, parsed.Text, desc, extendedDescriptions)
items = append(items, currentTable)
desc = nil
} else {
kv := newKeyval(line, desc)
default:
kv := newKeyval(format.Name(), line, parsed.Text, desc)
shortName := kv.name
if currentTable != &globalTable {
// update to full name
Expand All @@ -197,12 +216,17 @@ func parseTOMLDocs(s string, extendedDescriptions map[string]string) (items []fm
} else if !strings.HasPrefix(kv.desc[0], shortName) {
err = errors.Join(err, fmt.Errorf("%s: description does not begin with %q", kv.name, shortName))
}
if !strings.HasSuffix(line, FieldDefault) && !strings.HasSuffix(line, FieldExample) {
err = errors.Join(err, fmt.Errorf(`%s: is not one of %v`, kv.name, []string{FieldDefault, FieldExample}))
docsOnly := strings.HasSuffix(line, docsOnlyMarker)
if !docsOnly && !strings.HasSuffix(line, defaultMarker) && !strings.HasSuffix(line, exampleMarker) {
err = errors.Join(err, fmt.Errorf(`%s: is not one of %v`, kv.name, []string{defaultMarker, exampleMarker, docsOnlyMarker}))
}

items = append(items, kv)
currentTable.codes = append(currentTable.codes, kv.code)
// A docs-only field still gets its own entry, but is kept out of the table's code
// block so every example in the document agrees on what a working config contains.
if !docsOnly {
currentTable.codes = append(currentTable.codes, kv.code)
}
desc = nil
}
}
Expand Down
53 changes: 53 additions & 0 deletions pkg/config/configdoc/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package configdoc

// LineKind classifies a line of a configuration document.
type LineKind int

const (
// LineBlank is an empty or whitespace-only line, which terminates a comment block.
LineBlank LineKind = iota
// LineComment is a description line.
LineComment
// LineTable opens a table (section) of fields.
LineTable
// LineArrayOfTables opens a repeated table.
LineArrayOfTables
// LineField is a key/value pair.
LineField
)

// Line is a parsed line: its kind, plus the piece of it that carries meaning - the comment
// text with its marker stripped, the table's name, or the field's key.
type Line struct {
Kind LineKind
Text string
}

// Format is a configuration file syntax. It both writes the pieces of a document (so callers
// can assemble one from Go structs) and recognizes them when reading one back (so a
// hand-written document can be turned into documentation). Implement it to document a format
// other than TOML; see TOML for the reference implementation.
Comment on lines +28 to +29

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What else did you have in mind? I thought we were leveraging TOMLs ease of parsing pretty directly here

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From call: I'm worried about this being premature abstraction, but only because it is public API. If this is useful for readability besides, then keeping it internal and evolving until it is ready to be public would be fine.

type Format interface {
// Comment renders text as a description line.
Comment(text string) string
// Table renders the opening of a section, given its dotted path (e.g. "chain.nodes").
Table(path string) string
// Field renders a key/value line. marker is DefaultMarker, ExampleMarker or
// DocsOnlyMarker, or empty for a plain value line with no documentation annotation.
Field(key, value, marker string) string
// Literal renders a Go value as a value literal of this format.
Literal(v any) string

// DefaultMarker annotates a field whose value is its real default.
DefaultMarker() string
// ExampleMarker annotates a field with no usable default, whose value is a placeholder.
ExampleMarker() string
// DocsOnlyMarker annotates a field that is documented but shown in no example.
DocsOnlyMarker() string

// ParseLine classifies one line of a document.
ParseLine(line string) Line

// Name returns the markdown name for code blocks
Name() string
}
Loading
Loading