diff --git a/_examples/README.md b/_examples/README.md index ea8263d..5cac12f 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -12,7 +12,7 @@ There are few target files: [`x_complex.md`](./x_complex.md) with `-env-prefix` argument; and [`complex-nostyle.html`](./complex-nostyle.html) which is HTML documentation without built-in styles. - [`envprefix.go`](./envprefix.go) showcases a nested config structure with the `envPrefix` tag for a structure field. - It generates [`envprefix.md`](./envprefix.md). + It generates [`envprefix.md`](./envprefix.md), [`envprefix.txt`](./envprefix.txt) and [`envprefix.html`](./envprefix.html). The examples directory also contains helper script files: - `build-examples.sh` - modify any example Go file and regenerate all documentation outputs by executing it via `./build-examples.sh`. diff --git a/_examples/complex-nostyle.html b/_examples/complex-nostyle.html index f8ab3cf..71c8ca7 100644 --- a/_examples/complex-nostyle.html +++ b/_examples/complex-nostyle.html @@ -23,21 +23,18 @@

ComplexConfig

  • HOSTS (separated by ":", required) - Hosts is a list of hosts.
  • WORDS (comma-separated, from-file, default: one,two,three) - Words is just a list of words.
  • COMMENT (required, default: This is a comment.) - Just a comment.
  • -

    NextConfig

    FieldNames

    FieldNames uses field names as env names.

    diff --git a/_examples/complex.html b/_examples/complex.html index 8d1268d..16a25bb 100644 --- a/_examples/complex.html +++ b/_examples/complex.html @@ -89,21 +89,18 @@

    ComplexConfig

  • HOSTS (separated by ":", required) - Hosts is a list of hosts.
  • WORDS (comma-separated, from-file, default: one,two,three) - Words is just a list of words.
  • COMMENT (required, default: This is a comment.) - Just a comment.
  • -

    NextConfig

    FieldNames

    FieldNames uses field names as env names.

    diff --git a/_examples/config.html b/_examples/config.html index 1f2f6f3..b2b8da7 100644 --- a/_examples/config.html +++ b/_examples/config.html @@ -84,7 +84,6 @@

    Config

  • HOST (separated by ";", required) - Hosts name of hosts to listen on.
  • PORT (required, non-empty) - Port to listen on.
  • DEBUG (default: false) - Debug mode enabled.
  • - diff --git a/_examples/envprefix.go b/_examples/envprefix.go index a911878..9e07d18 100644 --- a/_examples/envprefix.go +++ b/_examples/envprefix.go @@ -1,15 +1,51 @@ package main +// Settings is the application settings. +// +//go:generate go run ../ -output envprefix.txt -format plaintext -type Settings //go:generate go run ../ -output envprefix.md -type Settings +//go:generate go run ../ -output envprefix.html -format html -type Settings type Settings struct { // Database is the database settings Database Database `envPrefix:"DB_"` + // Server is the server settings + Server ServerConfig `envPrefix:"SERVER_"` + // Debug is the debug flag Debug bool `env:"DEBUG"` } +// Database is the database settings. type Database struct { // Port is the port to connect to Port Int `env:"PORT,required"` + // Host is the host to connect to + Host string `env:"HOST,notEmpty" envDefault:"localhost"` + // User is the user to connect as + User string `env:"USER"` + // Password is the password to use + Password string `env:"PASSWORD"` + // DisableTLS is the flag to disable TLS + DisableTLS bool `env:"DISABLE_TLS"` +} + +// ServerConfig is the server settings. +type ServerConfig struct { + // Port is the port to listen on + Port Int `env:"PORT,required"` + + // Host is the host to listen on + Host string `env:"HOST,notEmpty" envDefault:"localhost"` + + // Timeout is the timeout settings + Timeout TimeoutConfig `envPrefix:"TIMEOUT_"` +} + +// TimeoutConfig is the timeout settings. +type TimeoutConfig struct { + // Read is the read timeout + Read Int `env:"READ" envDefault:"30"` + // Write is the write timeout + Write Int `env:"WRITE" envDefault:"30"` } diff --git a/_examples/envprefix.html b/_examples/envprefix.html new file mode 100644 index 0000000..9565723 --- /dev/null +++ b/_examples/envprefix.html @@ -0,0 +1,109 @@ + + + + + Environment Variables + + + +
    +
    +

    Environment Variables

    + +

    Settings

    +

    Settings is the application settings.

    + + +
    +
    + + diff --git a/_examples/envprefix.md b/_examples/envprefix.md index a6cd9b6..195a427 100644 --- a/_examples/envprefix.md +++ b/_examples/envprefix.md @@ -2,5 +2,18 @@ ## Settings - - `DB_PORT` (**required**) - Port is the port to connect to +Settings is the application settings. + + - Database is the database settings. + - `DB_PORT` (**required**) - Port is the port to connect to + - `DB_HOST` (**required**, non-empty, default: `localhost`) - Host is the host to connect to + - `DB_USER` - User is the user to connect as + - `DB_PASSWORD` - Password is the password to use + - `DB_DISABLE_TLS` - DisableTLS is the flag to disable TLS + - ServerConfig is the server settings. + - `SERVER_PORT` (**required**) - Port is the port to listen on + - `SERVER_HOST` (**required**, non-empty, default: `localhost`) - Host is the host to listen on + - TimeoutConfig is the timeout settings. + - `SERVER_TIMEOUT_READ` (default: `30`) - Read is the read timeout + - `SERVER_TIMEOUT_WRITE` (default: `30`) - Write is the write timeout - `DEBUG` - Debug is the debug flag diff --git a/_examples/envprefix.txt b/_examples/envprefix.txt new file mode 100644 index 0000000..7b36901 --- /dev/null +++ b/_examples/envprefix.txt @@ -0,0 +1,19 @@ +Environment Variables + +## Settings + +Settings is the application settings. + + * Database is the database settings. + * `DB_PORT` (required) - Port is the port to connect to + * `DB_HOST` (required, non-empty, default: `localhost`) - Host is the host to connect to + * `DB_USER` - User is the user to connect as + * `DB_PASSWORD` - Password is the password to use + * `DB_DISABLE_TLS` - DisableTLS is the flag to disable TLS + * ServerConfig is the server settings. + * `SERVER_PORT` (required) - Port is the port to listen on + * `SERVER_HOST` (required, non-empty, default: `localhost`) - Host is the host to listen on + * TimeoutConfig is the timeout settings. + * `SERVER_TIMEOUT_READ` (default: `30`) - Read is the read timeout + * `SERVER_TIMEOUT_WRITE` (default: `30`) - Write is the write timeout + * `DEBUG` - Debug is the debug flag diff --git a/debug.go b/debug.go new file mode 100644 index 0000000..dc02cba --- /dev/null +++ b/debug.go @@ -0,0 +1,12 @@ +package main + +import "fmt" + +const debugLogs = false + +func debug(f string, args ...any) { + if !debugLogs { + return + } + fmt.Printf("DEBUG: "+f+"\n", args...) +} diff --git a/inspector.go b/inspector.go index 851bfb4..eab83e3 100644 --- a/inspector.go +++ b/inspector.go @@ -22,6 +22,7 @@ type envField struct { doc string opts EnvVarOptions typeRef string + fieldName string envPrefix string } @@ -206,6 +207,11 @@ func (i *inspector) parseField(f *ast.Field) (out []envField) { item.kind = envFieldKindStruct fieldType := f.Type.(*ast.Ident) item.typeRef = fieldType.Name + fieldNames := make([]string, len(f.Names)) + for i, name := range f.Names { + fieldNames[i] = name.Name + } + item.fieldName = strings.Join(fieldNames, ", ") out = []envField{item} return } @@ -229,6 +235,7 @@ func (i *inspector) parseField(f *ast.Field) (out []envField) { } else { return } + docStr := strings.TrimSpace(f.Doc.Text()) if docStr == "" { docStr = strings.TrimSpace(f.Comment.Text()) @@ -288,51 +295,51 @@ func (i *inspector) buildScopes() ([]*EnvScope, error) { Doc: s.doc, } for _, f := range s.fields { - switch f.kind { - case envFieldKindPlain: - v := EnvDocItem{ - Name: f.name, - Doc: f.doc, - Opts: f.opts, - } - debug("[p] add docItem: %s <- %s", scope.Name, v.Name) - scope.Vars = append(scope.Vars, v) - case envFieldKindStruct: - envPrefix := f.envPrefix - var base *envStruct - for _, s := range i.items { - if s.name == f.typeRef { - base = s - break - } - } - if base == nil { - return nil, fmt.Errorf("struct %q not found", f.typeRef) - } - for _, f := range base.fields { - name := fmt.Sprintf("%s%s", envPrefix, f.name) - v := EnvDocItem{ - Name: name, - Doc: f.doc, - Opts: f.opts, - } - debug("[s] add docItem: %s <- %s (prefix: %s)", scope.Name, v.Name, envPrefix) - scope.Vars = append(scope.Vars, v) - } - default: - panic("unknown field kind") + item, err := i.buildItem(&f, "") + if err != nil { + return nil, err } + scope.Vars = append(scope.Vars, item) } scopes = append(scopes, scope) } return scopes, nil } -const debugLogs = false - -func debug(f string, args ...any) { - if !debugLogs { - return +func (i *inspector) buildItem(f *envField, envPrefix string) (EnvDocItem, error) { + switch f.kind { + case envFieldKindPlain: + return EnvDocItem{ + Name: fmt.Sprintf("%s%s", envPrefix, f.name), + Doc: f.doc, + Opts: f.opts, + debugName: f.name, + }, nil + case envFieldKindStruct: + envPrefix := fmt.Sprintf("%s%s", envPrefix, f.envPrefix) + var base *envStruct + for _, s := range i.items { + if s.name == f.typeRef { + base = s + break + } + } + if base == nil { + return EnvDocItem{}, fmt.Errorf("struct %q not found", f.typeRef) + } + parentItem := EnvDocItem{ + Doc: base.doc, + debugName: base.name, + } + for _, f := range base.fields { + item, err := i.buildItem(&f, envPrefix) + if err != nil { + return EnvDocItem{}, fmt.Errorf("build item `%s`: %w", f.name, err) + } + parentItem.Children = append(parentItem.Children, item) + } + return parentItem, nil + default: + panic("unknown field kind") } - fmt.Printf("DEBUG: "+f+"\n", args...) } diff --git a/inspector_test.go b/inspector_test.go index a4ccd88..823d37a 100644 --- a/inspector_test.go +++ b/inspector_test.go @@ -277,13 +277,114 @@ func TestInspector(t *testing.T) { }, }, { + /* + type Settings struct { + // Database is the database settings + Database Database `envPrefix:"DB_"` + + // Server is the server settings + Server ServerConfig `envPrefix:"SERVER_"` + + // Debug is the debug flag + Debug bool `env:"DEBUG"` + } + + // Database is the database settings. + type Database struct { + // Port is the port to connect to + Port Int `env:"PORT,required"` + // Host is the host to connect to + Host string `env:"HOST,nonempty" envDefault:"localhost"` + // User is the user to connect as + User string `env:"USER"` + // Password is the password to use + Password string `env:"PASSWORD"` + // DisableTLS is the flag to disable TLS + DisableTLS bool `env:"DISABLE_TLS"` + } + + // ServerConfig is the server settings. + type ServerConfig struct { + // Port is the port to listen on + Port Int `env:"PORT,required"` + + // Host is the host to listen on + Host string `env:"HOST,nonempty" envDefault:"localhost"` + + // Timeout is the timeout settings + Timeout TimeoutConfig `envPrefix:"TIMEOUT_"` + } + + // TimeoutConfig is the timeout settings. + type TimeoutConfig struct { + // Read is the read timeout + Read Int `env:"READ" envDefault:"30"` + // Write is the write timeout + Write Int `env:"WRITE" envDefault:"30"` + } + */ name: "envprefix.go", typeName: "Settings", expect: []EnvDocItem{ { - Name: "DB_PORT", - Doc: "Port is the port to connect to", - Opts: EnvVarOptions{Required: true}, + Doc: "Database is the database settings.", + debugName: "Database", + Children: []EnvDocItem{ + { + Name: "DB_PORT", + Doc: "Port is the port to connect to", + Opts: EnvVarOptions{Required: true}, + }, + { + Name: "DB_HOST", + Doc: "Host is the host to connect to", + Opts: EnvVarOptions{Required: true, NonEmpty: true, Default: "localhost"}, + }, + { + Name: "DB_USER", + Doc: "User is the user to connect as", + }, + { + Name: "DB_PASSWORD", + Doc: "Password is the password to use", + }, + { + Name: "DB_DISABLE_TLS", + Doc: "DisableTLS is the flag to disable TLS", + }, + }, + }, + { + Doc: "ServerConfig is the server settings.", + debugName: "Server", + Children: []EnvDocItem{ + { + Name: "SERVER_PORT", + Doc: "Port is the port to listen on", + Opts: EnvVarOptions{Required: true}, + }, + { + Name: "SERVER_HOST", + Doc: "Host is the host to listen on", + Opts: EnvVarOptions{Required: true, NonEmpty: true, Default: "localhost"}, + }, + { + Doc: "TimeoutConfig is the timeout settings.", + debugName: "Timeout", + Children: []EnvDocItem{ + { + Name: "SERVER_TIMEOUT_READ", + Doc: "Read is the read timeout", + Opts: EnvVarOptions{Default: "30"}, + }, + { + Name: "SERVER_TIMEOUT_WRITE", + Doc: "Write is the write timeout", + Opts: EnvVarOptions{Default: "30"}, + }, + }, + }, + }, }, { Name: "DEBUG", @@ -349,19 +450,30 @@ func inspectorTester(name string, typeName string, all bool, lineN int, expect [ t.Fatalf("[%d]scope: expect %d vars; was %d", i, len(e.Vars), len(s.Vars)) } } - for j, v := range s.Vars { - ev := e.Vars[j] - if v.Name != ev.Name { - t.Fatalf("[%d]scope: var[%d]: expect name %q; was %q", i, j, ev.Name, v.Name) - } - if v.Doc != ev.Doc { - t.Fatalf("[%d]scope: var[%d]: expect doc %q; was %q", i, j, ev.Doc, v.Doc) - } - if v.Opts != ev.Opts { - t.Fatalf("[%d]scope: var[%d]: expect opts %+v; was %+v", i, j, ev.Opts, v.Opts) - } + for j, actual := range s.Vars { + expect := e.Vars[j] + testScopeVar(t, fmt.Sprintf("[%d]scope: var[%d]", i, j), expect, actual) } - } } } + +func testScopeVar(t *testing.T, logPrefix string, expect, actual EnvDocItem) { + t.Helper() + + if expect.Name != actual.Name { + t.Fatalf("%s: expect name %q; was %q", logPrefix, expect.Name, actual.Name) + } + if expect.Doc != actual.Doc { + t.Fatalf("%s: expect doc %q; was %q", logPrefix, expect.Doc, actual.Doc) + } + if expect.Opts != actual.Opts { + t.Fatalf("%s: expect opts %+v; was %+v", logPrefix, expect.Opts, actual.Opts) + } + if len(expect.Children) != len(actual.Children) { + t.Fatalf("%s: expect %d children; was %d", logPrefix, len(expect.Children), len(actual.Children)) + } + for i, c := range expect.Children { + testScopeVar(t, fmt.Sprintf("%s -> child[%d]", logPrefix, i), c, actual.Children[i]) + } +} diff --git a/main.go b/main.go index 3d53aae..5bcbe9d 100644 --- a/main.go +++ b/main.go @@ -66,9 +66,20 @@ func main() { } } -func getConfig() (appConfig, error) { +type getConfigOpt func(f *flag.FlagSet) + +var getConfigSilent = func(f *flag.FlagSet) { + f.Usage = func() {} + nopWriter := os.NewFile(0, os.DevNull) + f.SetOutput(nopWriter) +} + +func getConfig(opts ...getConfigOpt) (appConfig, error) { var cfg appConfig flagSet := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) + for _, opt := range opts { + opt(flagSet) + } if err := cfg.parseFlags(flagSet); err != nil { flagSet.Usage() return cfg, fmt.Errorf("invalid CLI args: %w", err) diff --git a/main_test.go b/main_test.go index 6cc0827..69d66b5 100644 --- a/main_test.go +++ b/main_test.go @@ -22,10 +22,7 @@ func TestConfig(t *testing.T) { t.Setenv("GOFILE", "test.go") t.Setenv("GOLINE", "42") - cfg, err := getConfig() - if err != nil { - t.Fatal("Invalid CLI args:", err) - } + cfg := getTestConfig(t, false) if cfg.outputFileName != "test.md" { t.Fatal("Invalid output file name") } @@ -60,20 +57,12 @@ func TestConfig(t *testing.T) { }) t.Run("bad-args", func(t *testing.T) { os.Args = []string{"cmd", "-type"} - _, err := getConfig() - if err == nil { - t.Fatal("Expect error for invalid CLI args") - } - t.Logf("Got error as expected: %v", err) + _ = getTestConfig(t, true) }) t.Run("bad-env", func(t *testing.T) { t.Setenv("GOFILE", "") t.Setenv("GOLINE", "abc") - _, err := getConfig() - if err == nil { - t.Fatal("Expect error for invalid environment") - } - t.Logf("Got error as expected: %v", err) + _ = getTestConfig(t, true) }) } @@ -161,3 +150,18 @@ func TestMainRun(t *testing.T) { t.Logf("Got error as expected: %v", err) }) } + +func getTestConfig(t *testing.T, expectErr bool) appConfig { + t.Helper() + + cfg, err := getConfig(getConfigSilent) + if expectErr { + if err == nil { + t.Fatal("Expect error for invalid CLI args") + } + t.Logf("Got error as expected: %v", err) + } else if err != nil { + t.Fatal("Invalid CLI args:", err) + } + return cfg +} diff --git a/render.go b/render.go index b5d1328..6455177 100644 --- a/render.go +++ b/render.go @@ -4,6 +4,7 @@ import ( "embed" "fmt" "io" + "strings" htmltmpl "html/template" texttmpl "text/template" @@ -25,6 +26,19 @@ type renderItem struct { Expand bool NonEmpty bool FromFile bool + + children []renderItem + Indent int +} + +func (i renderItem) Children(indentInc int) []renderItem { + indent := i.Indent + indentInc + res := make([]renderItem, len(i.children)) + for j, child := range i.children { + child.Indent = indent + res[j] = child + } + return res } type renderContext struct { @@ -46,29 +60,50 @@ func newRenderContext(scopes []*EnvScope, envPrefix string, noStyles bool) rende Items: make([]renderItem, len(scope.Vars)), } for j, item := range scope.Vars { - section.Items[j] = renderItem{ - EnvName: fmt.Sprintf("%s%s", envPrefix, item.Name), - Doc: item.Doc, - EnvDefault: item.Opts.Default, - EnvSeparator: item.Opts.Separator, - Required: item.Opts.Required, - Expand: item.Opts.Expand, - NonEmpty: item.Opts.NonEmpty, - FromFile: item.Opts.FromFile, - } + item := newRenderItem(item, envPrefix) + item.Indent = 1 + section.Items[j] = item } res.Sections[i] = section } return res } +func newRenderItem(item EnvDocItem, envPrefix string) renderItem { + children := make([]renderItem, len(item.Children)) + debug("render item %s", item.Name) + for i, child := range item.Children { + debug("render child item %s", child.Name) + children[i] = newRenderItem(child, envPrefix) + } + return renderItem{ + EnvName: fmt.Sprintf("%s%s", envPrefix, item.Name), + Doc: item.Doc, + EnvDefault: item.Opts.Default, + EnvSeparator: item.Opts.Separator, + Required: item.Opts.Required, + Expand: item.Opts.Expand, + NonEmpty: item.Opts.NonEmpty, + FromFile: item.Opts.FromFile, + children: children, + } +} + //go:embed templ var templates embed.FS +var tplFuncs = map[string]any{ + "repeat": strings.Repeat, +} + +func _() { + // texttmpl.ParseFS +} + var ( - tmplMarkdown = texttmpl.Must(texttmpl.ParseFS(templates, "templ/markdown.tmpl")) - tmplHTML = htmltmpl.Must(htmltmpl.ParseFS(templates, "templ/html.tmpl")) - tmplPlaintext = texttmpl.Must(texttmpl.ParseFS(templates, "templ/plaintext.tmpl")) + tmplMarkdown = texttmpl.Must(texttmpl.New("markdown.tmpl").Funcs(tplFuncs).ParseFS(templates, "templ/markdown.tmpl")) + tmplHTML = htmltmpl.Must(htmltmpl.New("html.tmpl").Funcs(tplFuncs).ParseFS(templates, "templ/html.tmpl")) + tmplPlaintext = texttmpl.Must(texttmpl.New("plaintext.tmpl").Funcs(tplFuncs).ParseFS(templates, "templ/plaintext.tmpl")) ) type template interface { diff --git a/render_test.go b/render_test.go index 8bba770..3ec4e8e 100644 --- a/render_test.go +++ b/render_test.go @@ -26,6 +26,28 @@ var testRenderItems = []renderItem{ NonEmpty: true, FromFile: true, }, + { + Doc: "Nested item level one", + children: []renderItem{ + { + EnvName: "NESTED_ENV1", + Doc: "This is a first nested environment variable.", + }, + { + EnvName: "NESTED_ENV2", + Doc: "This is a second nested environment variable.", + }, + { + Doc: "Nested item level two", + children: []renderItem{ + { + EnvName: "NESTED_ENV3", + Doc: "This is a third nested environment variable.", + }, + }, + }, + }, + }, } var testRenderSections = []renderSection{ @@ -59,12 +81,24 @@ func TestRender(t *testing.T) { "# Simple", "- `TEST_ENV` - This is a test environment variable.", "- `TEST_ENV2` (comma-separated, default: `default value`) - This is another test environment variable.", - "- `TEST_ENV3` (**required**, expand, non-empty, from-file) - This is a third test environment variable.")) + "- `TEST_ENV3` (**required**, expand, non-empty, from-file) - This is a third test environment variable.", + "- Nested item level one", + " - `NESTED_ENV1` - This is a first nested environment variable.", + " - `NESTED_ENV2` - This is a second nested environment variable.", + " - Nested item level two", + " - `NESTED_ENV3` - This is a third nested environment variable.", + )) t.Run("plaintext", testRenderer(tmplPlaintext, rc, "Simple", " * `TEST_ENV` - This is a test environment variable.", " * `TEST_ENV2` (comma-separated, default: `default value`) - This is another test environment variable.", - " * `TEST_ENV3` (required, expand, non-empty, from-file) - This is a third test environment variable.")) + " * `TEST_ENV3` (required, expand, non-empty, from-file) - This is a third test environment variable.", + " * Nested item level one", + " * `NESTED_ENV1` - This is a first nested environment variable.", + " * `NESTED_ENV2` - This is a second nested environment variable.", + " * Nested item level two", + " * `NESTED_ENV3` - This is a third nested environment variable.", + )) t.Run("html", testRenderer(tmplHTML, rc, ``, ``, @@ -79,6 +113,17 @@ func TestRender(t *testing.T) { `
  • TEST_ENV - This is a test environment variable.
  • `, `
  • TEST_ENV2 (comma-separated, default: default value) - This is another test environment variable.
  • `, `
  • TEST_ENV3 (required, expand, non-empty, from-file) - This is a third test environment variable.
  • `, + `
  • Nested item level one`, + ``, + `
  • `, ``, ``, ``, @@ -137,6 +182,15 @@ func TestNewRenderContext(t *testing.T) { Name: "ONE", Doc: "First one", }, + { + Doc: "Nested", + Children: []EnvDocItem{ + { + Name: "NESTED_ONE", + Doc: "Nested one", + }, + }, + }, }, }, } @@ -155,8 +209,8 @@ func TestNewRenderContext(t *testing.T) { if section.Name != "First" { t.Errorf("expected section name %q, got %q", "First", section.Name) } - if len(section.Items) != 1 { - t.Fatalf("expected 1 variable, got %d", len(section.Items)) + if len(section.Items) != 2 { + t.Fatalf("expected 2 variable, got %d", len(section.Items)) } variable := section.Items[0] if variable.EnvName != "PREFIX_ONE" { @@ -165,6 +219,20 @@ func TestNewRenderContext(t *testing.T) { if variable.Doc != "First one" { t.Errorf("expected variable doc %q, got %q", "First one", variable.Doc) } + nested := section.Items[1] + if nested.Doc != "Nested" { + t.Errorf("expected nested doc %q, got %q", "Nested", nested.Doc) + } + if len(nested.children) != 1 { + t.Fatalf("expected 1 child, got %d", len(variable.children)) + } + child := nested.children[0] + if child.EnvName != "PREFIX_NESTED_ONE" { + t.Errorf("expected child name %q, got %q", "PREFIX_NESTED_ONE", child.EnvName) + } + if child.Doc != "Nested one" { + t.Errorf("expected child doc %q, got %q", "Nested one", child.Doc) + } } func testRenderer(tmpl template, c renderContext, expectLines ...string) func(*testing.T) { @@ -182,6 +250,10 @@ func testRenderer(tmpl template, c renderContext, expectLines ...string) func(*t line := strings.TrimSpace(scanner.Text()) logBuilder.WriteString(line) logBuilder.WriteRune('\n') + if len(expectLines) <= currentLine { + t.Log(logBuilder.String()) + t.Fatalf("unexpected line at %d: %q", currentLine, line) + } expect := strings.TrimSpace(expectLines[currentLine]) if line == expect { currentLine++ diff --git a/templ/html.tmpl b/templ/html.tmpl index ca8d076..6799714 100644 --- a/templ/html.tmpl +++ b/templ/html.tmpl @@ -1,3 +1,54 @@ +{{- define "item" }} +
  • + {{- $comma := false -}} + {{- if .EnvName -}} + {{ .EnvName }} + {{- if eq .EnvSeparator "," -}} + {{- $comma = true }} (comma-separated + {{- else if ne .EnvSeparator "" -}} + {{- $comma = true }} (separated by "{{.EnvSeparator}}" + {{- end -}} + {{- if .Required -}} + {{- if $comma }}, {{ else }} ({{ end -}} + {{- $comma = true -}} + required + {{- end -}} + {{- if .Expand -}} + {{- if $comma }}, {{ else }} ({{ end -}} + {{- $comma = true -}} + expand + {{- end -}} + {{- if .NonEmpty -}} + {{- if $comma }}, {{ else }} ({{ end -}} + {{- $comma = true -}} + non-empty + {{- end -}} + {{if .FromFile -}} + {{- if $comma }}, {{ else }} ({{ end -}} + {{- $comma = true -}} + from-file + {{- end -}} + {{- if ne .EnvDefault "" -}} + {{- if $comma }}, {{ else }} ({{ end -}} + {{- $comma = true -}} + default: {{ .EnvDefault }} + {{- end -}} + {{- if $comma }}){{ end -}} + {{- .Doc | printf " - %s" -}} + {{- else -}} + {{- .Doc | printf "%s" -}} + {{- end}} + {{- $children := .Children 0 -}} + {{- if $children }} + + {{ end -}} +
  • +{{- end -}} + @@ -83,42 +134,8 @@ p {

    {{ .Doc }}

    {{- end }} {{ end }} diff --git a/templ/markdown.tmpl b/templ/markdown.tmpl index 5ef20c6..3e7c140 100644 --- a/templ/markdown.tmpl +++ b/templ/markdown.tmpl @@ -1,3 +1,51 @@ +{{- define "item" }} + {{- $comma := false -}} + {{- repeat " " .Indent -}} + {{- if .EnvName }} + {{- .EnvName | printf "- `%s`" -}} + {{- if eq .EnvSeparator "," -}} + {{- $comma = true }} (comma-separated + {{- else if ne .EnvSeparator "" -}} + {{- $comma = true }} (separated by `{{.EnvSeparator}}` + {{- end -}} + {{- if .Required -}} + {{- if $comma }}, {{ else }} ({{ end -}} + {{- $comma = true -}} + **required** + {{- end -}} + {{- if .Expand -}} + {{- if $comma }}, {{ else }} ({{ end -}} + {{- $comma = true -}} + expand + {{- end -}} + {{- if .NonEmpty -}} + {{- if $comma }}, {{ else }} ({{ end -}} + {{- $comma = true -}} + non-empty + {{- end -}} + {{if .FromFile -}} + {{- if $comma }}, {{ else }} ({{ end -}} + {{- $comma = true -}} + from-file + {{- end -}} + {{- if ne .EnvDefault "" -}} + {{- if $comma }}, {{ else }} ({{ end -}} + {{- $comma = true -}} + {{- .EnvDefault | printf "default: `%s`" -}} + {{- end -}} + {{- if $comma }}){{ end -}} + {{- .Doc | printf " - %s"}} + {{- else }} + {{- .Doc | printf "- %s" }} + {{- end }} + {{- $children := .Children 2 }} + {{- if $children }} + {{- range $children }} +{{ template "item" . }} + {{- end }} + {{- end -}} +{{ end -}} + # {{ .Title }} {{ range .Sections -}} {{ if ne .Name "" }} @@ -7,39 +55,6 @@ {{ .Doc }} {{ end }} {{ range .Items }} - {{- $comma := false -}} - {{- .EnvName | printf " - `%s`" -}} - {{- if eq .EnvSeparator "," -}} - {{- $comma = true }} (comma-separated - {{- else if ne .EnvSeparator "" -}} - {{- $comma = true }} (separated by `{{.EnvSeparator}}` - {{- end -}} - {{- if .Required -}} - {{- if $comma }}, {{ else }} ({{ end -}} - {{- $comma = true -}} - **required** - {{- end -}} - {{- if .Expand -}} - {{- if $comma }}, {{ else }} ({{ end -}} - {{- $comma = true -}} - expand - {{- end -}} - {{- if .NonEmpty -}} - {{- if $comma }}, {{ else }} ({{ end -}} - {{- $comma = true -}} - non-empty - {{- end -}} - {{if .FromFile -}} - {{- if $comma }}, {{ else }} ({{ end -}} - {{- $comma = true -}} - from-file - {{- end -}} - {{- if ne .EnvDefault "" -}} - {{- if $comma }}, {{ else }} ({{ end -}} - {{- $comma = true -}} - {{- .EnvDefault | printf "default: `%s`" -}} - {{- end -}} - {{- if $comma }}) {{ else }} {{ end -}} - - {{.Doc}} +{{- template "item" . }} {{ end -}} {{ end -}} diff --git a/templ/plaintext.tmpl b/templ/plaintext.tmpl index fb22136..5f9250b 100644 --- a/templ/plaintext.tmpl +++ b/templ/plaintext.tmpl @@ -1,3 +1,51 @@ +{{- define "item" }} + {{- $comma := false -}} + {{- repeat " " .Indent -}} + {{- if .EnvName }} + {{- .EnvName | printf "* `%s`" -}} + {{- if eq .EnvSeparator "," -}} + {{- $comma = true }} (comma-separated + {{- else if ne .EnvSeparator "" -}} + {{- $comma = true }} (separated by `{{.EnvSeparator}}` + {{- end -}} + {{- if .Required -}} + {{- if $comma }}, {{ else }} ({{ end -}} + {{- $comma = true -}} + required + {{- end -}} + {{- if .Expand -}} + {{- if $comma }}, {{ else }} ({{ end -}} + {{- $comma = true -}} + expand + {{- end -}} + {{- if .NonEmpty -}} + {{- if $comma }}, {{ else }} ({{ end -}} + {{- $comma = true -}} + non-empty + {{- end -}} + {{if .FromFile -}} + {{- if $comma }}, {{ else }} ({{ end -}} + {{- $comma = true -}} + from-file + {{- end -}} + {{- if ne .EnvDefault "" -}} + {{- if $comma }}, {{ else }} ({{ end -}} + {{- $comma = true -}} + {{- .EnvDefault | printf "default: `%s`" -}} + {{- end -}} + {{- if $comma }}){{ end -}} + {{- .Doc | printf " - %s" }} + {{- else }} + {{- .Doc | printf "* %s" }} + {{- end }} + {{- $children := .Children 2 }} + {{- if $children }} + {{- range $children }} +{{ template "item" . }} + {{- end }} + {{- end -}} +{{ end -}} + {{ .Title }} {{ range .Sections }} ## {{ .Name }} @@ -5,39 +53,6 @@ {{ .Doc }} {{ end }} {{ range .Items }} - {{- $comma := false -}} - {{- .EnvName | printf " * `%s`" -}} - {{- if eq .EnvSeparator "," -}} - {{- $comma = true }} (comma-separated - {{- else if ne .EnvSeparator "" -}} - {{- $comma = true }} (separated by `{{.EnvSeparator}}` - {{- end -}} - {{- if .Required -}} - {{- if $comma }}, {{ else }} ({{ end -}} - {{- $comma = true -}} - required - {{- end -}} - {{- if .Expand -}} - {{- if $comma }}, {{ else }} ({{ end -}} - {{- $comma = true -}} - expand - {{- end -}} - {{- if .NonEmpty -}} - {{- if $comma }}, {{ else }} ({{ end -}} - {{- $comma = true -}} - non-empty - {{- end -}} - {{if .FromFile -}} - {{- if $comma }}, {{ else }} ({{ end -}} - {{- $comma = true -}} - from-file - {{- end -}} - {{- if ne .EnvDefault "" -}} - {{- if $comma }}, {{ else }} ({{ end -}} - {{- $comma = true -}} - {{- .EnvDefault | printf "default: `%s`" -}} - {{- end -}} - {{- if $comma }}) {{ else }} {{ end -}} - - {{.Doc}} +{{- template "item" . }} {{ end -}} {{ end -}} diff --git a/testdata/envprefix.go b/testdata/envprefix.go index 9d88b33..c91b966 100644 --- a/testdata/envprefix.go +++ b/testdata/envprefix.go @@ -1,14 +1,47 @@ -package testdata +package main +// Settings is the application settings. type Settings struct { // Database is the database settings Database Database `envPrefix:"DB_"` + // Server is the server settings + Server ServerConfig `envPrefix:"SERVER_"` + // Debug is the debug flag Debug bool `env:"DEBUG"` } +// Database is the database settings. type Database struct { // Port is the port to connect to Port Int `env:"PORT,required"` + // Host is the host to connect to + Host string `env:"HOST,notEmpty" envDefault:"localhost"` + // User is the user to connect as + User string `env:"USER"` + // Password is the password to use + Password string `env:"PASSWORD"` + // DisableTLS is the flag to disable TLS + DisableTLS bool `env:"DISABLE_TLS"` +} + +// ServerConfig is the server settings. +type ServerConfig struct { + // Port is the port to listen on + Port Int `env:"PORT,required"` + + // Host is the host to listen on + Host string `env:"HOST,notEmpty" envDefault:"localhost"` + + // Timeout is the timeout settings + Timeout TimeoutConfig `envPrefix:"TIMEOUT_"` +} + +// TimeoutConfig is the timeout settings. +type TimeoutConfig struct { + // Read is the read timeout + Read Int `env:"READ" envDefault:"30"` + // Write is the write timeout + Write Int `env:"WRITE" envDefault:"30"` } diff --git a/types.go b/types.go index 3b3180d..efa2965 100644 --- a/types.go +++ b/types.go @@ -8,6 +8,10 @@ type EnvDocItem struct { Doc string // Opts is a set of options for environment variable parsing. Opts EnvVarOptions + // Children is a list of child environment variables. + Children []EnvDocItem + + debugName string // item name for debug logs. } type EnvScope struct {