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
MOUNT
(required) - Mount is a mount point.
-
FieldNames
FieldNames uses field names as env names.
QUUX
- Quux is a field with a tag.
-
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
MOUNT
(required) - Mount is a mount point.
-
FieldNames
FieldNames uses field names as env names.
QUUX
- Quux is a field with a tag.
-
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.
+
+ - 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.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`,
+ ``,
+ `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. `,
+ `
`,
+ ` `,
+ `
`,
+ ``,
``,
``,
``,
@@ -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 }}
+
+ {{- range $children -}}
+ {{ template "item" . -}}
+ {{- end }}
+
+ {{ end -}}
+
+{{- end -}}
+
@@ -83,42 +134,8 @@ p {
{{ .Doc }}
{{- end }}
-{{ range .Items }} -
- {{- $comma := false -}}
-
{{ .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 }}) {{ else }} {{ end -}}
- - {{ .Doc -}}
-
+{{- range .Items }}
+{{- template "item" . -}}
{{ 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 {