diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa4381c..8298836 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,12 @@ The format is based on [Keep a Changelog], and this project adheres to
[keep a changelog]: https://keepachangelog.com/en/1.0.0/
[semantic versioning]: https://semver.org/spec/v2.0.0.html
+### Unreleased
+
+### Added
+
+- Added `SeeAlso()` method to all builders, which links to another variable for documentation purposes
+
## [0.4.2] - 2023-03-11
### Added
diff --git a/bool.go b/bool.go
index 47180e1..bd6966d 100644
--- a/bool.go
+++ b/bool.go
@@ -69,6 +69,12 @@ func (b *BoolBuilder[T]) WithDefault(v T) *BoolBuilder[T] {
return b
}
+// SeeAlso creates a relationship between this variable and those used by i.
+func (b *BoolBuilder[T]) SeeAlso(i Input, options ...SeeAlsoOption) *BoolBuilder[T] {
+ seeAlsoInput(&b.spec, i, options...)
+ return b
+}
+
// Required completes the build process and registers a required variable with
// Ferrite's validation system.
func (b *BoolBuilder[T]) Required(options ...RequiredOption) Required[T] {
diff --git a/builder.go b/builder.go
index f19cac3..4983a1b 100644
--- a/builder.go
+++ b/builder.go
@@ -15,12 +15,9 @@ func undefinedError(v variable.Any) error {
}
// isBuilderOf makes a static assertion that B meats
-type isBuilderOf[T any, B builderOf[T]] struct{}
-
-// builderOf is an interface and type constriant common to all builders that
-// produce a value of type T.
-type builderOf[T any] interface {
+type isBuilderOf[T any, B interface {
+ SeeAlso(input Input, options ...SeeAlsoOption) B
Required(options ...RequiredOption) Required[T]
Optional(options ...OptionalOption) Optional[T]
Deprecated(options ...DeprecatedOption) Deprecated[T]
-}
+}] struct{}
diff --git a/duration.go b/duration.go
index 077044b..8a8eb45 100644
--- a/duration.go
+++ b/duration.go
@@ -66,6 +66,12 @@ func (b *DurationBuilder) WithMaximum(v time.Duration) *DurationBuilder {
return b
}
+// SeeAlso creates a relationship between this variable and those used by i.
+func (b *DurationBuilder) SeeAlso(i Input, options ...SeeAlsoOption) *DurationBuilder {
+ seeAlsoInput(&b.spec, i, options...)
+ return b
+}
+
// Required completes the build process and registers a required variable with
// Ferrite's validation system.
func (b *DurationBuilder) Required(options ...RequiredOption) Required[time.Duration] {
diff --git a/enum.go b/enum.go
index 673cea9..2fc8955 100644
--- a/enum.go
+++ b/enum.go
@@ -85,6 +85,12 @@ func (b *EnumBuilder[T]) WithDefault(v T) *EnumBuilder[T] {
return b
}
+// SeeAlso creates a relationship between this variable and those used by i.
+func (b *EnumBuilder[T]) SeeAlso(i Input, options ...SeeAlsoOption) *EnumBuilder[T] {
+ seeAlsoInput(&b.spec, i, options...)
+ return b
+}
+
// Required completes the build process and registers a required variable with
// Ferrite's validation system.
func (b *EnumBuilder[T]) Required(options ...RequiredOption) Required[T] {
diff --git a/file.go b/file.go
index 5dd074f..11ed534 100644
--- a/file.go
+++ b/file.go
@@ -35,6 +35,12 @@ func (b *FileBuilder) WithDefault(v string) *FileBuilder {
return b
}
+// SeeAlso creates a relationship between this variable and those used by i.
+func (b *FileBuilder) SeeAlso(i Input, options ...SeeAlsoOption) *FileBuilder {
+ seeAlsoInput(&b.spec, i, options...)
+ return b
+}
+
// Required completes the build process and registers a required variable with
// Ferrite's validation system.
func (b *FileBuilder) Required(options ...RequiredOption) Required[FileName] {
diff --git a/go.mod b/go.mod
index bf83179..af16369 100644
--- a/go.mod
+++ b/go.mod
@@ -18,9 +18,11 @@ require (
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
+ github.com/kr/pretty v0.1.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
+ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
)
diff --git a/go.sum b/go.sum
index 11d4c81..159ac86 100644
--- a/go.sum
+++ b/go.sum
@@ -31,6 +31,11 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jmalloc/gomegax v0.0.0-20200507221434-64fca4c0e03a h1:Gk7Gkwl1KUJII/FiAjvBjRgEz/lpvTV8kNYp+9jdpuk=
github.com/jmalloc/gomegax v0.0.0-20200507221434-64fca4c0e03a/go.mod h1:TZpc8ObQEKqTuy1/VXpPRfcMU80QFDU4zK3nchXts/k=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -75,8 +80,9 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
diff --git a/input.go b/input.go
new file mode 100644
index 0000000..5fe4b81
--- /dev/null
+++ b/input.go
@@ -0,0 +1,15 @@
+package ferrite
+
+import "github.com/dogmatiq/ferrite/variable"
+
+// An Input is the application-facing interface for obtaining a value from
+// environment variables.
+//
+// Typically each input is sourced from exactly one environment variable,
+// however it is possible that a value collates values from multiple variables.
+//
+// An input is created using one of the various builder-functions, for example
+// String().
+type Input interface {
+ variables() []variable.Any
+}
diff --git a/builderdeprecated.go b/inputdeprecated.go
similarity index 62%
rename from builderdeprecated.go
rename to inputdeprecated.go
index 7376401..b016d24 100644
--- a/builderdeprecated.go
+++ b/inputdeprecated.go
@@ -4,18 +4,19 @@ import (
"github.com/dogmatiq/ferrite/variable"
)
-// Deprecated is the application-facing interface for a value that is sourced
+// Deprecated is a specialization of the Input interface for values obtained
// from deprecated environment variables.
-//
-// It is obtained by calling Deprecated() on a variable builder.
type Deprecated[T any] interface {
- // DeprecatedValue returns the parsed and validated value of the environment
- // variable, if it is defined.
+ Input
+
+ // DeprecatedValue returns the parsed and validated value built from the
+ // environment variable(s).
//
- // If the environment variable is not defined (and there is no default
- // value), ok is false; otherwise, ok is true and v is the value.
+ // If the constituent environment variable(s) are not defined and there is
+ // no default value, ok is false; otherwise, ok is true and v is the value.
//
- // It panics if the environment variable is defined but invalid.
+ // It panics if any of one of the constituent environment variable(s) has an
+ // invalid value.
DeprecatedValue() (T, bool)
}
@@ -41,6 +42,7 @@ func deprecated[T any, S variable.TypedSchema[T]](
// interface is currently empty so we don't need an implementation
return deprecatedFunc[T]{
+ []variable.Any{v},
func() (T, bool, error) {
return v.NativeValue()
},
@@ -50,13 +52,18 @@ func deprecated[T any, S variable.TypedSchema[T]](
// deprecatedFunc is an implementation of Deprecated[T] that obtains the value
// from an arbitrary function.
type deprecatedFunc[T any] struct {
- fn func() (T, bool, error)
+ vars []variable.Any
+ fn func() (T, bool, error)
}
-func (d deprecatedFunc[T]) DeprecatedValue() (T, bool) {
- n, ok, err := d.fn()
+func (i deprecatedFunc[T]) DeprecatedValue() (T, bool) {
+ n, ok, err := i.fn()
if err != nil {
panic(err.Error())
}
return n, ok
}
+
+func (i deprecatedFunc[T]) variables() []variable.Any {
+ return i.vars
+}
diff --git a/builderoptional.go b/inputoptional.go
similarity index 54%
rename from builderoptional.go
rename to inputoptional.go
index 3b80134..9a11a27 100644
--- a/builderoptional.go
+++ b/inputoptional.go
@@ -4,18 +4,19 @@ import (
"github.com/dogmatiq/ferrite/variable"
)
-// Optional is the application-facing interface for a value that is sourced
-// from optional environment variables.
-//
-// It is obtained by calling Deprecated() on a variable builder.
+// Optional is a specialization of the Input interface for values obtained
+// from deprecated environment variables.
type Optional[T any] interface {
- // Value returns the parsed and validated value of the environment variable,
- // if it is defined.
+ Input
+
+ // Value returns the parsed and validated value built from the environment
+ // variable(s).
//
- // If the environment variable is not defined (and there is no default
- // value), ok is false; otherwise, ok is true and v is the value.
+ // If the constituent environment variable(s) are not defined and there is
+ // no default value, ok is false; otherwise, ok is true and v is the value.
//
- // It panics if the environment variable is defined but invalid.
+ // It panics if any of one of the constituent environment variable(s) has an
+ // invalid value.
Value() (T, bool)
}
@@ -37,22 +38,28 @@ func optional[T any, S variable.TypedSchema[T]](
)
return optionalFunc[T]{
+ []variable.Any{v},
func() (T, bool, error) {
return v.NativeValue()
},
}
}
-// optionalFunc is an implementation of Optional[T] that obtains the value from an
-// arbitrary function.
+// optionalFunc is an implementation of Optional[T] that obtains the value from
+// an arbitrary function.
type optionalFunc[T any] struct {
- fn func() (T, bool, error)
+ vars []variable.Any
+ fn func() (T, bool, error)
}
-func (d optionalFunc[T]) Value() (T, bool) {
- n, ok, err := d.fn()
+func (i optionalFunc[T]) Value() (T, bool) {
+ n, ok, err := i.fn()
if err != nil {
panic(err.Error())
}
return n, ok
}
+
+func (i optionalFunc[T]) variables() []variable.Any {
+ return i.vars
+}
diff --git a/builderrequired.go b/inputrequired.go
similarity index 68%
rename from builderrequired.go
rename to inputrequired.go
index 090ad85..b959fb3 100644
--- a/builderrequired.go
+++ b/inputrequired.go
@@ -4,14 +4,15 @@ import (
"github.com/dogmatiq/ferrite/variable"
)
-// Required is the application-facing interface for a value that is sourced
-// from required environment variables.
-//
-// It is obtained by calling Deprecated() on a variable builder.
+// Required is a specialization of the Input interface for values obtained
+// from required (mandatory) environment variables.
type Required[T any] interface {
+ Input
+
// Value returns the parsed and validated value of the environment variable.
//
- // It panics if the environment variable is undefined or invalid.
+ // It panics if any of one of the constituent environment variable(s) is
+ // undefined or has an invalid value.
Value() T
}
@@ -35,6 +36,7 @@ func required[T any, S variable.TypedSchema[T]](
)
return requiredFunc[T]{
+ []variable.Any{v},
func() (T, error) {
n, ok, err := v.NativeValue()
if ok || err != nil {
@@ -45,16 +47,21 @@ func required[T any, S variable.TypedSchema[T]](
}
}
-// requiredFunc is an implementation of Required[T] that obtains the value
-// from an arbitrary function.
+// requiredFunc is an implementation of Required[T] that obtains the value from
+// an arbitrary function.
type requiredFunc[T any] struct {
- fn func() (T, error)
+ vars []variable.Any
+ fn func() (T, error)
}
-func (d requiredFunc[T]) Value() T {
- n, err := d.fn()
+func (i requiredFunc[T]) Value() T {
+ n, err := i.fn()
if err != nil {
panic(err.Error())
}
return n
}
+
+func (i requiredFunc[T]) variables() []variable.Any {
+ return i.vars
+}
diff --git a/internal/mode/usage/markdown/complete_test.go b/internal/mode/usage/markdown/complete_test.go
new file mode 100644
index 0000000..feb32bf
--- /dev/null
+++ b/internal/mode/usage/markdown/complete_test.go
@@ -0,0 +1,59 @@
+package markdown_test
+
+import (
+ "github.com/dogmatiq/ferrite"
+ "github.com/dogmatiq/ferrite/variable"
+ . "github.com/onsi/ginkgo/v2"
+)
+
+var _ = DescribeTable(
+ "func Run()",
+ tableTest("complete"),
+ Entry(
+ "no variables",
+ "empty.md",
+ func(reg *variable.Registry) {},
+ ),
+ Entry(
+ "non-normative examples",
+ "non-normative.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ String("READ_DSN", "database connection string for read-models").
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "platform examples",
+ "platform-examples.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Bool("DEBUG", "enable or disable debugging features").
+ Optional(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "platform examples use default values as examples when available",
+ "platform-examples-use-defaults.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ NetworkPort("PORT", "an environment variable that has a default value").
+ WithDefault("ftp").
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "see also",
+ "see-also.md",
+ func(reg *variable.Registry) {
+ verbose := ferrite.
+ Bool("VERBOSE", "enable verbose logging").
+ Optional(variable.WithRegistry(reg))
+
+ ferrite.
+ Bool("DEBUG", "enable or disable debugging features").
+ SeeAlso(verbose).
+ Optional(variable.WithRegistry(reg))
+ },
+ ),
+)
diff --git a/internal/mode/usage/markdown/usage.go b/internal/mode/usage/markdown/platform.go
similarity index 92%
rename from internal/mode/usage/markdown/usage.go
rename to internal/mode/usage/markdown/platform.go
index 659feed..b9fb2ac 100644
--- a/internal/mode/usage/markdown/usage.go
+++ b/internal/mode/usage/markdown/platform.go
@@ -2,15 +2,15 @@ package markdown
import "github.com/dogmatiq/ferrite/variable"
-func (r *renderer) renderUsage() {
+func (r *renderer) renderPlatformExamples() {
r.line("## Usage Examples")
r.gap()
- r.renderKubernetesUsage()
+ r.renderKubernetesExample()
r.gap()
- r.renderDockerUsage()
+ r.renderDockerExample()
}
-func (r *renderer) renderKubernetesUsage() {
+func (r *renderer) renderKubernetesExample() {
r.line("")
r.line("Kubernetes
")
@@ -82,7 +82,7 @@ func (r *renderer) renderKubernetesUsage() {
r.line(" ")
}
-func (r *renderer) renderDockerUsage() {
+func (r *renderer) renderDockerExample() {
r.line("")
r.line("Docker
")
r.gap()
diff --git a/internal/mode/usage/markdown/preamble_test.go b/internal/mode/usage/markdown/preamble_test.go
deleted file mode 100644
index e18afac..0000000
--- a/internal/mode/usage/markdown/preamble_test.go
+++ /dev/null
@@ -1,77 +0,0 @@
-package markdown_test
-
-import (
- "bytes"
- "os"
- "path/filepath"
-
- "github.com/dogmatiq/ferrite"
- "github.com/dogmatiq/ferrite/internal/mode"
- . "github.com/dogmatiq/ferrite/internal/mode/usage/markdown"
- "github.com/dogmatiq/ferrite/variable"
- . "github.com/jmalloc/gomegax"
- . "github.com/onsi/ginkgo/v2"
- . "github.com/onsi/gomega"
-)
-
-var _ = Describe("func Run()", func() {
- var reg *variable.Registry
-
- BeforeEach(func() {
- reg = &variable.Registry{
- Environment: &variable.MemoryEnvironment{},
- }
- })
-
- DescribeTable(
- "it generates the correct preamble",
- func(
- file string,
- setup func(*variable.Registry),
- ) {
- setup(reg)
-
- expect, err := os.ReadFile(
- filepath.Join(
- "testdata",
- "markdown",
- "preamble",
- file,
- ),
- )
- Expect(err).ShouldNot(HaveOccurred())
-
- actual := &bytes.Buffer{}
- exited := false
-
- Run(
- mode.Options{
- Registry: reg,
- Args: []string{""},
- Out: actual,
- Exit: func(code int) {
- exited = true
- Expect(code).To(Equal(0))
- },
- },
- WithoutUsageExamples(),
- )
- ExpectWithOffset(1, actual.String()).To(EqualX(string(expect)))
- Expect(exited).To(BeTrue())
- },
- Entry(
- "no variables",
- "empty.md",
- func(reg *variable.Registry) {},
- ),
- Entry(
- "non-normative examples",
- "non-normative.md",
- func(reg *variable.Registry) {
- ferrite.
- String("READ_DSN", "database connection string for read-models").
- Required(variable.WithRegistry(reg))
- },
- ),
- )
-})
diff --git a/internal/mode/usage/markdown/relationship.go b/internal/mode/usage/markdown/relationship.go
new file mode 100644
index 0000000..46bd268
--- /dev/null
+++ b/internal/mode/usage/markdown/relationship.go
@@ -0,0 +1,45 @@
+package markdown
+
+import (
+ "github.com/dogmatiq/ferrite/variable"
+ "golang.org/x/exp/slices"
+)
+
+func (r *specRenderer) renderRelationships() {
+ c := &relationshipCollator{
+ spec: r.spec,
+ }
+
+ for _, rel := range r.spec.Relationships() {
+ rel.AcceptVisitor(c)
+ }
+
+ if len(c.seeAlso) != 0 {
+ r.ren.gap()
+ r.ren.line("#### See Also")
+ r.ren.gap()
+
+ slices.SortFunc(
+ c.seeAlso,
+ func(a, b variable.SeeAlso) bool {
+ return a.To.Name() < b.To.Name()
+ },
+ )
+
+ for _, rel := range c.seeAlso {
+ r.ren.renderIndexItem(rel.To)
+ }
+ }
+
+}
+
+type relationshipCollator struct {
+ spec variable.Spec
+ seeAlso []variable.SeeAlso
+}
+
+func (r *relationshipCollator) VisitSeeAlso(rel variable.SeeAlso) {
+ if r.spec == rel.From {
+ r.seeAlso = append(r.seeAlso, rel)
+ }
+}
diff --git a/internal/mode/usage/markdown/renderer.go b/internal/mode/usage/markdown/renderer.go
index e9d30b6..c371407 100644
--- a/internal/mode/usage/markdown/renderer.go
+++ b/internal/mode/usage/markdown/renderer.go
@@ -45,7 +45,7 @@ func (r *renderer) Render() {
if !r.withoutUsageExamples {
r.gap()
- r.renderUsage()
+ r.renderPlatformExamples()
}
}
diff --git a/internal/mode/usage/markdown/run_test.go b/internal/mode/usage/markdown/run_test.go
new file mode 100644
index 0000000..d62e9b7
--- /dev/null
+++ b/internal/mode/usage/markdown/run_test.go
@@ -0,0 +1,59 @@
+package markdown_test
+
+import (
+ "bytes"
+ "os"
+ "path/filepath"
+
+ "github.com/dogmatiq/ferrite/internal/mode"
+ . "github.com/dogmatiq/ferrite/internal/mode/usage/markdown"
+ "github.com/dogmatiq/ferrite/variable"
+ . "github.com/jmalloc/gomegax"
+ . "github.com/onsi/gomega"
+)
+
+func tableTest(
+ path string,
+ options ...Option,
+) func(
+ file string,
+ setup func(*variable.Registry),
+) {
+ return func(
+ file string,
+ setup func(*variable.Registry),
+ ) {
+ reg := &variable.Registry{
+ Environment: &variable.MemoryEnvironment{},
+ }
+
+ setup(reg)
+
+ expect, err := os.ReadFile(
+ filepath.Join(
+ "testdata",
+ path,
+ file,
+ ),
+ )
+ Expect(err).ShouldNot(HaveOccurred())
+
+ actual := &bytes.Buffer{}
+ exited := false
+
+ Run(
+ mode.Options{
+ Registry: reg,
+ Args: []string{""},
+ Out: actual,
+ Exit: func(code int) {
+ exited = true
+ Expect(code).To(Equal(0))
+ },
+ },
+ options...,
+ )
+ ExpectWithOffset(1, actual.String()).To(EqualX(string(expect)))
+ Expect(exited).To(BeTrue())
+ }
+}
diff --git a/internal/mode/usage/markdown/spec.go b/internal/mode/usage/markdown/spec.go
index cc8a22f..8f01293 100644
--- a/internal/mode/usage/markdown/spec.go
+++ b/internal/mode/usage/markdown/spec.go
@@ -41,6 +41,7 @@ func (r *specRenderer) Render() {
}
r.renderUnimportantDocumentation()
+ r.renderRelationships()
}
// VisitNumeric renders the primary requirement for spec that uses the "numeric"
diff --git a/internal/mode/usage/markdown/spec_test.go b/internal/mode/usage/markdown/spec_test.go
index 872e777..23f23e3 100644
--- a/internal/mode/usage/markdown/spec_test.go
+++ b/internal/mode/usage/markdown/spec_test.go
@@ -2,13 +2,10 @@ package markdown_test
import (
"bytes"
- "os"
- "path/filepath"
"strings"
"time"
"github.com/dogmatiq/ferrite"
- "github.com/dogmatiq/ferrite/internal/mode"
. "github.com/dogmatiq/ferrite/internal/mode/usage/markdown"
"github.com/dogmatiq/ferrite/variable"
. "github.com/jmalloc/gomegax"
@@ -16,837 +13,797 @@ import (
. "github.com/onsi/gomega"
)
-var _ = Describe("func Run()", func() {
- var reg *variable.Registry
+var _ = DescribeTable(
+ "func Run()",
+ tableTest(
+ "spec",
+ WithoutPreamble(),
+ WithoutIndex(),
+ WithoutUsageExamples(),
+ ),
- BeforeEach(func() {
- reg = &variable.Registry{
- Environment: &variable.MemoryEnvironment{},
- }
- })
+ // BOOL
- DescribeTable(
- "it describes the environment variable",
- func(
- file string,
- setup func(*variable.Registry),
- ) {
- setup(reg)
-
- expect, err := os.ReadFile(
- filepath.Join(
- "testdata",
- "markdown",
- "spec",
- file,
- ),
- )
- Expect(err).ShouldNot(HaveOccurred())
-
- actual := &bytes.Buffer{}
- exited := false
-
- Run(
- mode.Options{
- Registry: reg,
- Args: []string{""},
- Out: actual,
- Exit: func(code int) {
- exited = true
- Expect(code).To(Equal(0))
- },
- },
- WithoutPreamble(),
- WithoutIndex(),
- WithoutUsageExamples(),
- )
- ExpectWithOffset(1, actual.String()).To(EqualX(string(expect)))
- Expect(exited).To(BeTrue())
+ Entry(
+ "bool + optional + default",
+ "bool/with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Bool("DEBUG", "enable or disable debugging features").
+ WithDefault(false).
+ Optional(variable.WithRegistry(reg))
},
+ ),
+ Entry(
+ "bool + optional",
+ "bool/optional.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Bool("DEBUG", "enable or disable debugging features").
+ Optional(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "bool + required + default",
+ "bool/with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Bool("DEBUG", "enable or disable debugging features").
+ WithDefault(false).
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "bool + required",
+ "bool/required.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Bool("DEBUG", "enable or disable debugging features").
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "bool + deprecated + default",
+ "bool/deprecated-with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Bool("DEBUG", "enable or disable debugging features").
+ WithDefault(false).
+ Deprecated(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "bool + deprecated",
+ "bool/deprecated.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Bool("DEBUG", "enable or disable debugging features").
+ Deprecated(variable.WithRegistry(reg))
+ },
+ ),
- // BOOL
-
- Entry(
- "bool + optional + default",
- "bool/with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- Bool("DEBUG", "enable or disable debugging features").
- WithDefault(false).
- Optional(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "bool + optional",
- "bool/optional.md",
- func(reg *variable.Registry) {
- ferrite.
- Bool("DEBUG", "enable or disable debugging features").
- Optional(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "bool + required + default",
- "bool/with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- Bool("DEBUG", "enable or disable debugging features").
- WithDefault(false).
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "bool + required",
- "bool/required.md",
- func(reg *variable.Registry) {
- ferrite.
- Bool("DEBUG", "enable or disable debugging features").
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "bool + deprecated + default",
- "bool/deprecated-with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- Bool("DEBUG", "enable or disable debugging features").
- WithDefault(false).
- Deprecated(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "bool + deprecated",
- "bool/deprecated.md",
- func(reg *variable.Registry) {
- ferrite.
- Bool("DEBUG", "enable or disable debugging features").
- Deprecated(variable.WithRegistry(reg))
- },
- ),
-
- // ENUM
+ // ENUM
- Entry(
- "enum + optional + default",
- "enum/with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- Enum("LOG_LEVEL", "the minimum log level to record").
- WithMember("debug", "show information for developers").
- WithMember("info", "standard log messages").
- WithMember("warn", "important, but don't need individual human review").
- WithMember("error", "a healthy application shouldn't produce any errors").
- WithMember("fatal", "the application cannot proceed").
- WithDefault("error").
- Optional(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "enum + optional",
- "enum/optional.md",
- func(reg *variable.Registry) {
- ferrite.
- Enum("LOG_LEVEL", "the minimum log level to record").
- WithMember("debug", "show information for developers").
- WithMember("info", "standard log messages").
- WithMember("warn", "important, but don't need individual human review").
- WithMember("error", "a healthy application shouldn't produce any errors").
- WithMember("fatal", "the application cannot proceed").
- Optional(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "enum + required + default",
- "enum/with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- Enum("LOG_LEVEL", "the minimum log level to record").
- WithMember("debug", "show information for developers").
- WithMember("info", "standard log messages").
- WithMember("warn", "important, but don't need individual human review").
- WithMember("error", "a healthy application shouldn't produce any errors").
- WithMember("fatal", "the application cannot proceed").
- WithDefault("error").
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "enum + required",
- "enum/required.md",
- func(reg *variable.Registry) {
- ferrite.
- Enum("LOG_LEVEL", "the minimum log level to record").
- WithMember("debug", "show information for developers").
- WithMember("info", "standard log messages").
- WithMember("warn", "important, but don't need individual human review").
- WithMember("error", "a healthy application shouldn't produce any errors").
- WithMember("fatal", "the application cannot proceed").
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "enum + deprecated + default",
- "enum/deprecated-with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- Enum("LOG_LEVEL", "the minimum log level to record").
- WithMember("debug", "show information for developers").
- WithMember("info", "standard log messages").
- WithMember("warn", "important, but don't need individual human review").
- WithMember("error", "a healthy application shouldn't produce any errors").
- WithMember("fatal", "the application cannot proceed").
- WithDefault("error").
- Deprecated(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "enum + deprecated",
- "enum/deprecated.md",
- func(reg *variable.Registry) {
- ferrite.
- Enum("LOG_LEVEL", "the minimum log level to record").
- WithMember("debug", "show information for developers").
- WithMember("info", "standard log messages").
- WithMember("warn", "important, but don't need individual human review").
- WithMember("error", "a healthy application shouldn't produce any errors").
- WithMember("fatal", "the application cannot proceed").
- Deprecated(variable.WithRegistry(reg))
- },
- ),
+ Entry(
+ "enum + optional + default",
+ "enum/with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Enum("LOG_LEVEL", "the minimum log level to record").
+ WithMember("debug", "show information for developers").
+ WithMember("info", "standard log messages").
+ WithMember("warn", "important, but don't need individual human review").
+ WithMember("error", "a healthy application shouldn't produce any errors").
+ WithMember("fatal", "the application cannot proceed").
+ WithDefault("error").
+ Optional(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "enum + optional",
+ "enum/optional.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Enum("LOG_LEVEL", "the minimum log level to record").
+ WithMember("debug", "show information for developers").
+ WithMember("info", "standard log messages").
+ WithMember("warn", "important, but don't need individual human review").
+ WithMember("error", "a healthy application shouldn't produce any errors").
+ WithMember("fatal", "the application cannot proceed").
+ Optional(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "enum + required + default",
+ "enum/with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Enum("LOG_LEVEL", "the minimum log level to record").
+ WithMember("debug", "show information for developers").
+ WithMember("info", "standard log messages").
+ WithMember("warn", "important, but don't need individual human review").
+ WithMember("error", "a healthy application shouldn't produce any errors").
+ WithMember("fatal", "the application cannot proceed").
+ WithDefault("error").
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "enum + required",
+ "enum/required.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Enum("LOG_LEVEL", "the minimum log level to record").
+ WithMember("debug", "show information for developers").
+ WithMember("info", "standard log messages").
+ WithMember("warn", "important, but don't need individual human review").
+ WithMember("error", "a healthy application shouldn't produce any errors").
+ WithMember("fatal", "the application cannot proceed").
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "enum + deprecated + default",
+ "enum/deprecated-with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Enum("LOG_LEVEL", "the minimum log level to record").
+ WithMember("debug", "show information for developers").
+ WithMember("info", "standard log messages").
+ WithMember("warn", "important, but don't need individual human review").
+ WithMember("error", "a healthy application shouldn't produce any errors").
+ WithMember("fatal", "the application cannot proceed").
+ WithDefault("error").
+ Deprecated(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "enum + deprecated",
+ "enum/deprecated.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Enum("LOG_LEVEL", "the minimum log level to record").
+ WithMember("debug", "show information for developers").
+ WithMember("info", "standard log messages").
+ WithMember("warn", "important, but don't need individual human review").
+ WithMember("error", "a healthy application shouldn't produce any errors").
+ WithMember("fatal", "the application cannot proceed").
+ Deprecated(variable.WithRegistry(reg))
+ },
+ ),
- // DURATION
+ // DURATION
- Entry(
- "duration + optional + default",
- "duration/with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- Duration("GRPC_TIMEOUT", "gRPC request timeout").
- WithDefault(10 * time.Millisecond).
- Optional(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "duration + optional",
- "duration/optional.md",
- func(reg *variable.Registry) {
- ferrite.
- Duration("GRPC_TIMEOUT", "gRPC request timeout").
- Optional(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "duration + required + default",
- "duration/with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- Duration("GRPC_TIMEOUT", "gRPC request timeout").
- WithDefault(10 * time.Millisecond).
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "duration + required",
- "duration/required.md",
- func(reg *variable.Registry) {
- ferrite.
- Duration("GRPC_TIMEOUT", "gRPC request timeout").
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "duration + maximum",
- "duration/with-max.md",
- func(reg *variable.Registry) {
- ferrite.
- Duration("GRPC_TIMEOUT", "gRPC request timeout").
- WithMaximum(24 * time.Hour).
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "duration + deprecated + default",
- "duration/deprecated-with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- Duration("GRPC_TIMEOUT", "gRPC request timeout").
- WithDefault(10 * time.Millisecond).
- Deprecated(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "duration + deprecated",
- "duration/deprecated.md",
- func(reg *variable.Registry) {
- ferrite.
- Duration("GRPC_TIMEOUT", "gRPC request timeout").
- Deprecated(variable.WithRegistry(reg))
- },
- ),
+ Entry(
+ "duration + optional + default",
+ "duration/with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Duration("GRPC_TIMEOUT", "gRPC request timeout").
+ WithDefault(10 * time.Millisecond).
+ Optional(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "duration + optional",
+ "duration/optional.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Duration("GRPC_TIMEOUT", "gRPC request timeout").
+ Optional(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "duration + required + default",
+ "duration/with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Duration("GRPC_TIMEOUT", "gRPC request timeout").
+ WithDefault(10 * time.Millisecond).
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "duration + required",
+ "duration/required.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Duration("GRPC_TIMEOUT", "gRPC request timeout").
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "duration + maximum",
+ "duration/with-max.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Duration("GRPC_TIMEOUT", "gRPC request timeout").
+ WithMaximum(24 * time.Hour).
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "duration + deprecated + default",
+ "duration/deprecated-with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Duration("GRPC_TIMEOUT", "gRPC request timeout").
+ WithDefault(10 * time.Millisecond).
+ Deprecated(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "duration + deprecated",
+ "duration/deprecated.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Duration("GRPC_TIMEOUT", "gRPC request timeout").
+ Deprecated(variable.WithRegistry(reg))
+ },
+ ),
- // KUBERNETES SERVICE
+ // KUBERNETES SERVICE
- Entry(
- "k8s service + optional + default",
- "k8s-service/with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- KubernetesService("redis").
- WithDefault("redis.example.org", "6379").
- Optional(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "k8s service + optional",
- "k8s-service/optional.md",
- func(reg *variable.Registry) {
- ferrite.
- KubernetesService("redis").
- Optional(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "k8s service + required + default",
- "k8s-service/with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- KubernetesService("redis").
- WithDefault("redis.example.org", "6379").
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "k8s service + required",
- "k8s-service/required.md",
- func(reg *variable.Registry) {
- ferrite.
- KubernetesService("redis").
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "k8s service + deprecated + default",
- "k8s-service/deprecated-with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- KubernetesService("redis").
- WithDefault("redis.example.org", "6379").
- Deprecated(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "k8s service + deprecated",
- "k8s-service/deprecated.md",
- func(reg *variable.Registry) {
- ferrite.
- KubernetesService("redis").
- Deprecated(variable.WithRegistry(reg))
- },
- ),
- // NETWORK PORT
+ Entry(
+ "k8s service + optional + default",
+ "k8s-service/with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ KubernetesService("redis").
+ WithDefault("redis.example.org", "6379").
+ Optional(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "k8s service + optional",
+ "k8s-service/optional.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ KubernetesService("redis").
+ Optional(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "k8s service + required + default",
+ "k8s-service/with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ KubernetesService("redis").
+ WithDefault("redis.example.org", "6379").
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "k8s service + required",
+ "k8s-service/required.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ KubernetesService("redis").
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "k8s service + deprecated + default",
+ "k8s-service/deprecated-with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ KubernetesService("redis").
+ WithDefault("redis.example.org", "6379").
+ Deprecated(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "k8s service + deprecated",
+ "k8s-service/deprecated.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ KubernetesService("redis").
+ Deprecated(variable.WithRegistry(reg))
+ },
+ ),
+ // NETWORK PORT
- Entry(
- "network port + optional + default",
- "network-port/with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- NetworkPort("PORT", "listen port for the HTTP server").
- WithDefault("8080").
- Optional(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "network port + optional",
- "network-port/optional.md",
- func(reg *variable.Registry) {
- ferrite.
- NetworkPort("PORT", "listen port for the HTTP server").
- Optional(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "network port + required + default",
- "network-port/with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- NetworkPort("PORT", "listen port for the HTTP server").
- WithDefault("8080").
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "network port + required",
- "network-port/required.md",
- func(reg *variable.Registry) {
- ferrite.
- NetworkPort("PORT", "listen port for the HTTP server").
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "network port + deprecated + default",
- "network-port/deprecated-with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- NetworkPort("PORT", "listen port for the HTTP server").
- WithDefault("8080").
- Deprecated(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "network port + deprecated",
- "network-port/deprecated.md",
- func(reg *variable.Registry) {
- ferrite.
- NetworkPort("PORT", "listen port for the HTTP server").
- Deprecated(variable.WithRegistry(reg))
- },
- ),
+ Entry(
+ "network port + optional + default",
+ "network-port/with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ NetworkPort("PORT", "listen port for the HTTP server").
+ WithDefault("8080").
+ Optional(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "network port + optional",
+ "network-port/optional.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ NetworkPort("PORT", "listen port for the HTTP server").
+ Optional(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "network port + required + default",
+ "network-port/with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ NetworkPort("PORT", "listen port for the HTTP server").
+ WithDefault("8080").
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "network port + required",
+ "network-port/required.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ NetworkPort("PORT", "listen port for the HTTP server").
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "network port + deprecated + default",
+ "network-port/deprecated-with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ NetworkPort("PORT", "listen port for the HTTP server").
+ WithDefault("8080").
+ Deprecated(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "network port + deprecated",
+ "network-port/deprecated.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ NetworkPort("PORT", "listen port for the HTTP server").
+ Deprecated(variable.WithRegistry(reg))
+ },
+ ),
- // NUMBER - FLOAT
+ // NUMBER - FLOAT
- Entry(
- "float + optional + default",
- "float/with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- Float[float32]("WEIGHT", "weighting for this node").
- WithDefault(123.5).
- Optional(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "float + optional",
- "float/optional.md",
- func(reg *variable.Registry) {
- ferrite.
- Float[float32]("WEIGHT", "weighting for this node").
- Optional(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "float + required + default",
- "float/with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- Float[float32]("WEIGHT", "weighting for this node").
- WithDefault(123.5).
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "float + required",
- "float/required.md",
- func(reg *variable.Registry) {
- ferrite.
- Float[float32]("WEIGHT", "weighting for this node").
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "float + min",
- "float/with-min.md",
- func(reg *variable.Registry) {
- ferrite.
- Float[float32]("WEIGHT", "weighting for this node").
- WithMinimum(-10.5).
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "float + max",
- "float/with-max.md",
- func(reg *variable.Registry) {
- ferrite.
- Float[float32]("WEIGHT", "weighting for this node").
- WithMaximum(+20.5).
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "float + min/max",
- "float/with-minmax.md",
- func(reg *variable.Registry) {
- ferrite.
- Float[float32]("WEIGHT", "weighting for this node").
- WithMinimum(-10.5).
- WithMaximum(+20.5).
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "float + deprecated + default",
- "float/deprecated-with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- Float[float32]("WEIGHT", "weighting for this node").
- WithDefault(123.5).
- Deprecated(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "float + deprecated",
- "float/deprecated.md",
- func(reg *variable.Registry) {
- ferrite.
- Float[float32]("WEIGHT", "weighting for this node").
- Deprecated(variable.WithRegistry(reg))
- },
- ),
+ Entry(
+ "float + optional + default",
+ "float/with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Float[float32]("WEIGHT", "weighting for this node").
+ WithDefault(123.5).
+ Optional(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "float + optional",
+ "float/optional.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Float[float32]("WEIGHT", "weighting for this node").
+ Optional(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "float + required + default",
+ "float/with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Float[float32]("WEIGHT", "weighting for this node").
+ WithDefault(123.5).
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "float + required",
+ "float/required.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Float[float32]("WEIGHT", "weighting for this node").
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "float + min",
+ "float/with-min.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Float[float32]("WEIGHT", "weighting for this node").
+ WithMinimum(-10.5).
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "float + max",
+ "float/with-max.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Float[float32]("WEIGHT", "weighting for this node").
+ WithMaximum(+20.5).
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "float + min/max",
+ "float/with-minmax.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Float[float32]("WEIGHT", "weighting for this node").
+ WithMinimum(-10.5).
+ WithMaximum(+20.5).
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "float + deprecated + default",
+ "float/deprecated-with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Float[float32]("WEIGHT", "weighting for this node").
+ WithDefault(123.5).
+ Deprecated(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "float + deprecated",
+ "float/deprecated.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Float[float32]("WEIGHT", "weighting for this node").
+ Deprecated(variable.WithRegistry(reg))
+ },
+ ),
- // NUMBER - SIGNED
+ // NUMBER - SIGNED
- Entry(
- "signed + optional + default",
- "signed/with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- Signed[int8]("WEIGHT", "weighting for this node").
- WithDefault(100).
- Optional(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "signed + optional",
- "signed/optional.md",
- func(reg *variable.Registry) {
- ferrite.
- Signed[int8]("WEIGHT", "weighting for this node").
- Optional(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "signed + required + default",
- "signed/with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- Signed[int8]("WEIGHT", "weighting for this node").
- WithDefault(100).
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "signed + required",
- "signed/required.md",
- func(reg *variable.Registry) {
- ferrite.
- Signed[int8]("WEIGHT", "weighting for this node").
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "signed + min",
- "signed/with-min.md",
- func(reg *variable.Registry) {
- ferrite.
- Signed[int8]("WEIGHT", "weighting for this node").
- WithMinimum(-10).
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "signed + max",
- "signed/with-max.md",
- func(reg *variable.Registry) {
- ferrite.
- Signed[int8]("WEIGHT", "weighting for this node").
- WithMaximum(+20).
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "signed + min/max",
- "signed/with-minmax.md",
- func(reg *variable.Registry) {
- ferrite.
- Signed[int8]("WEIGHT", "weighting for this node").
- WithMinimum(-10).
- WithMaximum(+20).
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "signed + deprecated + default",
- "signed/deprecated-with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- Signed[int8]("WEIGHT", "weighting for this node").
- WithDefault(100).
- Deprecated(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "signed + deprecated",
- "signed/deprecated.md",
- func(reg *variable.Registry) {
- ferrite.
- Signed[int8]("WEIGHT", "weighting for this node").
- Deprecated(variable.WithRegistry(reg))
- },
- ),
+ Entry(
+ "signed + optional + default",
+ "signed/with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Signed[int8]("WEIGHT", "weighting for this node").
+ WithDefault(100).
+ Optional(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "signed + optional",
+ "signed/optional.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Signed[int8]("WEIGHT", "weighting for this node").
+ Optional(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "signed + required + default",
+ "signed/with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Signed[int8]("WEIGHT", "weighting for this node").
+ WithDefault(100).
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "signed + required",
+ "signed/required.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Signed[int8]("WEIGHT", "weighting for this node").
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "signed + min",
+ "signed/with-min.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Signed[int8]("WEIGHT", "weighting for this node").
+ WithMinimum(-10).
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "signed + max",
+ "signed/with-max.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Signed[int8]("WEIGHT", "weighting for this node").
+ WithMaximum(+20).
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "signed + min/max",
+ "signed/with-minmax.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Signed[int8]("WEIGHT", "weighting for this node").
+ WithMinimum(-10).
+ WithMaximum(+20).
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "signed + deprecated + default",
+ "signed/deprecated-with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Signed[int8]("WEIGHT", "weighting for this node").
+ WithDefault(100).
+ Deprecated(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "signed + deprecated",
+ "signed/deprecated.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Signed[int8]("WEIGHT", "weighting for this node").
+ Deprecated(variable.WithRegistry(reg))
+ },
+ ),
- // NUMBER - UNSIGNED
+ // NUMBER - UNSIGNED
- Entry(
- "unsigned + optional + default",
- "unsigned/with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- Unsigned[uint16]("WEIGHT", "weighting for this node").
- WithDefault(900).
- Optional(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "unsigned + optional",
- "unsigned/optional.md",
- func(reg *variable.Registry) {
- ferrite.
- Unsigned[uint16]("WEIGHT", "weighting for this node").
- Optional(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "unsigned + required + default",
- "unsigned/with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- Unsigned[uint16]("WEIGHT", "weighting for this node").
- WithDefault(900).
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "unsigned + required",
- "unsigned/required.md",
- func(reg *variable.Registry) {
- ferrite.
- Unsigned[uint16]("WEIGHT", "weighting for this node").
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "unsigned + min",
- "unsigned/with-min.md",
- func(reg *variable.Registry) {
- ferrite.
- Unsigned[uint16]("WEIGHT", "weighting for this node").
- WithMinimum(10).
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "unsigned + max",
- "unsigned/with-max.md",
- func(reg *variable.Registry) {
- ferrite.
- Unsigned[uint16]("WEIGHT", "weighting for this node").
- WithMaximum(20).
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "unsigned + min/max",
- "unsigned/with-minmax.md",
- func(reg *variable.Registry) {
- ferrite.
- Unsigned[uint16]("WEIGHT", "weighting for this node").
- WithMinimum(10).
- WithMaximum(20).
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "unsigned + deprecated + default",
- "unsigned/deprecated-with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- Unsigned[uint16]("WEIGHT", "weighting for this node").
- WithDefault(900).
- Deprecated(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "unsigned + deprecated",
- "unsigned/deprecated.md",
- func(reg *variable.Registry) {
- ferrite.
- Unsigned[uint16]("WEIGHT", "weighting for this node").
- Deprecated(variable.WithRegistry(reg))
- },
- ),
+ Entry(
+ "unsigned + optional + default",
+ "unsigned/with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Unsigned[uint16]("WEIGHT", "weighting for this node").
+ WithDefault(900).
+ Optional(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "unsigned + optional",
+ "unsigned/optional.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Unsigned[uint16]("WEIGHT", "weighting for this node").
+ Optional(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "unsigned + required + default",
+ "unsigned/with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Unsigned[uint16]("WEIGHT", "weighting for this node").
+ WithDefault(900).
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "unsigned + required",
+ "unsigned/required.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Unsigned[uint16]("WEIGHT", "weighting for this node").
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "unsigned + min",
+ "unsigned/with-min.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Unsigned[uint16]("WEIGHT", "weighting for this node").
+ WithMinimum(10).
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "unsigned + max",
+ "unsigned/with-max.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Unsigned[uint16]("WEIGHT", "weighting for this node").
+ WithMaximum(20).
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "unsigned + min/max",
+ "unsigned/with-minmax.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Unsigned[uint16]("WEIGHT", "weighting for this node").
+ WithMinimum(10).
+ WithMaximum(20).
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "unsigned + deprecated + default",
+ "unsigned/deprecated-with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Unsigned[uint16]("WEIGHT", "weighting for this node").
+ WithDefault(900).
+ Deprecated(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "unsigned + deprecated",
+ "unsigned/deprecated.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ Unsigned[uint16]("WEIGHT", "weighting for this node").
+ Deprecated(variable.WithRegistry(reg))
+ },
+ ),
- // STRING
+ // STRING
- Entry(
- "string + optional + default",
- "string/with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- String("READ_DSN", "database connection string for read-models").
- WithDefault("host=localhost dbname=readmodels user=projector").
- Optional(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "string + optional",
- "string/optional.md",
- func(reg *variable.Registry) {
- ferrite.
- String("READ_DSN", "database connection string for read-models").
- Optional(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "string + required + default",
- "string/with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- String("READ_DSN", "database connection string for read-models").
- WithDefault("host=localhost dbname=readmodels user=projector").
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "string + required",
- "string/required.md",
- func(reg *variable.Registry) {
- ferrite.
- String("READ_DSN", "database connection string for read-models").
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "string + deprecated + default",
- "string/deprecated-with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- String("READ_DSN", "database connection string for read-models").
- WithDefault("host=localhost dbname=readmodels user=projector").
- Deprecated(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "string + deprecated",
- "string/deprecated.md",
- func(reg *variable.Registry) {
- ferrite.
- String("READ_DSN", "database connection string for read-models").
- Deprecated(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "string + sensitive + optional + default",
- "string/sensitive-with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- String("PASSWORD", "a very secret password").
- WithDefault("hunter2").
- WithSensitiveContent().
- Optional(
- variable.WithRegistry(reg),
- )
- },
- ),
- Entry(
- "string + sensitive + optional",
- "string/sensitive-optional.md",
- func(reg *variable.Registry) {
- ferrite.
- String("PASSWORD", "a very secret password").
- WithSensitiveContent().
- Optional(
- variable.WithRegistry(reg),
- )
- },
- ),
- Entry(
- "string + sensitive + required + default",
- "string/sensitive-with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- String("PASSWORD", "a very secret password").
- WithDefault("hunter2").
- WithSensitiveContent().
- Required(
- variable.WithRegistry(reg),
- )
- },
- ),
- Entry(
- "string + sensitive + required",
- "string/sensitive-required.md",
- func(reg *variable.Registry) {
- ferrite.
- String("PASSWORD", "a very secret password").
- WithSensitiveContent().
- Required(
- variable.WithRegistry(reg),
- )
- },
- ),
+ Entry(
+ "string + optional + default",
+ "string/with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ String("READ_DSN", "database connection string for read-models").
+ WithDefault("host=localhost dbname=readmodels user=projector").
+ Optional(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "string + optional",
+ "string/optional.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ String("READ_DSN", "database connection string for read-models").
+ Optional(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "string + required + default",
+ "string/with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ String("READ_DSN", "database connection string for read-models").
+ WithDefault("host=localhost dbname=readmodels user=projector").
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "string + required",
+ "string/required.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ String("READ_DSN", "database connection string for read-models").
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "string + deprecated + default",
+ "string/deprecated-with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ String("READ_DSN", "database connection string for read-models").
+ WithDefault("host=localhost dbname=readmodels user=projector").
+ Deprecated(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "string + deprecated",
+ "string/deprecated.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ String("READ_DSN", "database connection string for read-models").
+ Deprecated(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "string + sensitive + optional + default",
+ "string/sensitive-with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ String("PASSWORD", "a very secret password").
+ WithDefault("hunter2").
+ WithSensitiveContent().
+ Optional(
+ variable.WithRegistry(reg),
+ )
+ },
+ ),
+ Entry(
+ "string + sensitive + optional",
+ "string/sensitive-optional.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ String("PASSWORD", "a very secret password").
+ WithSensitiveContent().
+ Optional(
+ variable.WithRegistry(reg),
+ )
+ },
+ ),
+ Entry(
+ "string + sensitive + required + default",
+ "string/sensitive-with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ String("PASSWORD", "a very secret password").
+ WithDefault("hunter2").
+ WithSensitiveContent().
+ Required(
+ variable.WithRegistry(reg),
+ )
+ },
+ ),
+ Entry(
+ "string + sensitive + required",
+ "string/sensitive-required.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ String("PASSWORD", "a very secret password").
+ WithSensitiveContent().
+ Required(
+ variable.WithRegistry(reg),
+ )
+ },
+ ),
- // URL
+ // URL
- Entry(
- "url + optional + default",
- "url/with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- URL("API_URL", "the URL of the REST API").
- WithDefault("http://localhost:8080").
- Optional(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "url + optional",
- "url/optional.md",
- func(reg *variable.Registry) {
- ferrite.
- URL("API_URL", "the URL of the REST API").
- Optional(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "url + required + default",
- "url/with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- URL("API_URL", "the URL of the REST API").
- WithDefault("http://localhost:8080").
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "url + required",
- "url/required.md",
- func(reg *variable.Registry) {
- ferrite.
- URL("API_URL", "the URL of the REST API").
- Required(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "url + deprecated + default",
- "url/deprecated-with-default.md",
- func(reg *variable.Registry) {
- ferrite.
- URL("API_URL", "the URL of the REST API").
- WithDefault("http://localhost:8080").
- Deprecated(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "url + deprecated",
- "url/deprecated.md",
- func(reg *variable.Registry) {
- ferrite.
- URL("API_URL", "the URL of the REST API").
- Deprecated(variable.WithRegistry(reg))
- },
- ),
- )
-})
+ Entry(
+ "url + optional + default",
+ "url/with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ URL("API_URL", "the URL of the REST API").
+ WithDefault("http://localhost:8080").
+ Optional(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "url + optional",
+ "url/optional.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ URL("API_URL", "the URL of the REST API").
+ Optional(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "url + required + default",
+ "url/with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ URL("API_URL", "the URL of the REST API").
+ WithDefault("http://localhost:8080").
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "url + required",
+ "url/required.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ URL("API_URL", "the URL of the REST API").
+ Required(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "url + deprecated + default",
+ "url/deprecated-with-default.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ URL("API_URL", "the URL of the REST API").
+ WithDefault("http://localhost:8080").
+ Deprecated(variable.WithRegistry(reg))
+ },
+ ),
+ Entry(
+ "url + deprecated",
+ "url/deprecated.md",
+ func(reg *variable.Registry) {
+ ferrite.
+ URL("API_URL", "the URL of the REST API").
+ Deprecated(variable.WithRegistry(reg))
+ },
+ ),
+)
// expectLines verifies that text consists of the given lines.
func expectLines(buf *bytes.Buffer, lines ...string) {
diff --git a/internal/mode/usage/markdown/syntax.go b/internal/mode/usage/markdown/syntax.go
deleted file mode 100644
index b868feb..0000000
--- a/internal/mode/usage/markdown/syntax.go
+++ /dev/null
@@ -1 +0,0 @@
-package markdown
diff --git a/internal/mode/usage/markdown/testdata/markdown/preamble/empty.md b/internal/mode/usage/markdown/testdata/complete/empty.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/preamble/empty.md
rename to internal/mode/usage/markdown/testdata/complete/empty.md
diff --git a/internal/mode/usage/markdown/testdata/complete/non-normative.md b/internal/mode/usage/markdown/testdata/complete/non-normative.md
new file mode 100644
index 0000000..859dea4
--- /dev/null
+++ b/internal/mode/usage/markdown/testdata/complete/non-normative.md
@@ -0,0 +1,107 @@
+# Environment Variables
+
+This document describes the environment variables used by ``.
+
+If any of the environment variable values do not meet the requirements herein,
+the application will print usage information to `STDERR` then exit with a
+non-zero exit code. Please note that **undefined** variables and **empty**
+values are considered equivalent.
+
+⚠️ This document includes **non-normative** example values. While these values
+are syntactically correct, they may not be meaningful to this application.
+
+⚠️ The application may consume other undocumented environment variables; this
+document only shows those variables declared using [Ferrite].
+
+The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**,
+**SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this
+document are to be interpreted as described in [RFC 2119].
+
+## Index
+
+- [`READ_DSN`](#READ_DSN) — database connection string for read-models
+
+## Specification
+
+### `READ_DSN`
+
+> database connection string for read-models
+
+The `READ_DSN` variable **MUST NOT** be left undefined.
+
+```bash
+export READ_DSN=foo # (non-normative)
+```
+
+## Usage Examples
+
+
+Kubernetes
+
+This example shows how to define the environment variables needed by ``
+on a [Kubernetes container] within a Kubenetes deployment manifest.
+
+```yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: example-deployment
+spec:
+ template:
+ spec:
+ containers:
+ - name: example-container
+ env:
+ - name: READ_DSN # database connection string for read-models
+ value: foo
+```
+
+Alternatively, the environment variables can be defined within a [config map][kubernetes config map]
+then referenced a deployment manifest using `configMapRef`.
+
+```yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: example-config-map
+data:
+ READ_DSN: foo # database connection string for read-models
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: example-deployment
+spec:
+ template:
+ spec:
+ containers:
+ - name: example-container
+ envFrom:
+ - configMapRef:
+ name: example-config-map
+```
+
+
+
+
+Docker
+
+This example shows how to define the environment variables needed by ``
+when running as a [Docker service] defined in a Docker compose file.
+
+```yaml
+service:
+ example-service:
+ environment:
+ READ_DSN: foo # database connection string for read-models
+```
+
+
+
+
+
+[docker service]: https://docs.docker.com/compose/environment-variables/#set-environment-variables-in-containers
+[ferrite]: https://github.com/dogmatiq/ferrite
+[kubernetes config map]: https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#configure-all-key-value-pairs-in-a-configmap-as-container-environment-variables
+[kubernetes container]: https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/#define-an-environment-variable-for-a-container
+[rfc 2119]: https://www.rfc-editor.org/rfc/rfc2119.html
diff --git a/internal/mode/usage/markdown/testdata/markdown/usage/usage-shows-default.md b/internal/mode/usage/markdown/testdata/complete/platform-examples-use-defaults.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/usage/usage-shows-default.md
rename to internal/mode/usage/markdown/testdata/complete/platform-examples-use-defaults.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/usage/usage.md b/internal/mode/usage/markdown/testdata/complete/platform-examples.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/usage/usage.md
rename to internal/mode/usage/markdown/testdata/complete/platform-examples.md
diff --git a/internal/mode/usage/markdown/testdata/complete/see-also.md b/internal/mode/usage/markdown/testdata/complete/see-also.md
new file mode 100644
index 0000000..c3493db
--- /dev/null
+++ b/internal/mode/usage/markdown/testdata/complete/see-also.md
@@ -0,0 +1,127 @@
+# Environment Variables
+
+This document describes the environment variables used by ``.
+
+If any of the environment variable values do not meet the requirements herein,
+the application will print usage information to `STDERR` then exit with a
+non-zero exit code. Please note that **undefined** variables and **empty**
+values are considered equivalent.
+
+⚠️ The application may consume other undocumented environment variables; this
+document only shows those variables declared using [Ferrite].
+
+The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**,
+**SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this
+document are to be interpreted as described in [RFC 2119].
+
+## Index
+
+- [`DEBUG`](#DEBUG) — enable or disable debugging features
+- [`VERBOSE`](#VERBOSE) — enable verbose logging
+
+## Specification
+
+### `DEBUG`
+
+> enable or disable debugging features
+
+The `DEBUG` variable **MAY** be left undefined. Otherwise, the value **MUST** be
+either `true` or `false`.
+
+```bash
+export DEBUG=true
+export DEBUG=false
+```
+
+#### See Also
+
+- [`VERBOSE`](#VERBOSE) — enable verbose logging
+
+### `VERBOSE`
+
+> enable verbose logging
+
+The `VERBOSE` variable **MAY** be left undefined. Otherwise, the value **MUST**
+be either `true` or `false`.
+
+```bash
+export VERBOSE=true
+export VERBOSE=false
+```
+
+## Usage Examples
+
+
+Kubernetes
+
+This example shows how to define the environment variables needed by ``
+on a [Kubernetes container] within a Kubenetes deployment manifest.
+
+```yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: example-deployment
+spec:
+ template:
+ spec:
+ containers:
+ - name: example-container
+ env:
+ - name: DEBUG # enable or disable debugging features
+ value: "false"
+ - name: VERBOSE # enable verbose logging
+ value: "false"
+```
+
+Alternatively, the environment variables can be defined within a [config map][kubernetes config map]
+then referenced a deployment manifest using `configMapRef`.
+
+```yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: example-config-map
+data:
+ DEBUG: "false" # enable or disable debugging features
+ VERBOSE: "false" # enable verbose logging
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: example-deployment
+spec:
+ template:
+ spec:
+ containers:
+ - name: example-container
+ envFrom:
+ - configMapRef:
+ name: example-config-map
+```
+
+
+
+
+Docker
+
+This example shows how to define the environment variables needed by ``
+when running as a [Docker service] defined in a Docker compose file.
+
+```yaml
+service:
+ example-service:
+ environment:
+ DEBUG: "false" # enable or disable debugging features
+ VERBOSE: "false" # enable verbose logging
+```
+
+
+
+
+
+[docker service]: https://docs.docker.com/compose/environment-variables/#set-environment-variables-in-containers
+[ferrite]: https://github.com/dogmatiq/ferrite
+[kubernetes config map]: https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#configure-all-key-value-pairs-in-a-configmap-as-container-environment-variables
+[kubernetes container]: https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/#define-an-environment-variable-for-a-container
+[rfc 2119]: https://www.rfc-editor.org/rfc/rfc2119.html
diff --git a/internal/mode/usage/markdown/testdata/markdown/preamble/non-normative.md b/internal/mode/usage/markdown/testdata/markdown/preamble/non-normative.md
deleted file mode 100644
index 4e7ea94..0000000
--- a/internal/mode/usage/markdown/testdata/markdown/preamble/non-normative.md
+++ /dev/null
@@ -1,39 +0,0 @@
-# Environment Variables
-
-This document describes the environment variables used by ``.
-
-If any of the environment variable values do not meet the requirements herein,
-the application will print usage information to `STDERR` then exit with a
-non-zero exit code. Please note that **undefined** variables and **empty**
-values are considered equivalent.
-
-⚠️ This document includes **non-normative** example values. While these values
-are syntactically correct, they may not be meaningful to this application.
-
-⚠️ The application may consume other undocumented environment variables; this
-document only shows those variables declared using [Ferrite].
-
-The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**,
-**SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this
-document are to be interpreted as described in [RFC 2119].
-
-## Index
-
-- [`READ_DSN`](#READ_DSN) — database connection string for read-models
-
-## Specification
-
-### `READ_DSN`
-
-> database connection string for read-models
-
-The `READ_DSN` variable **MUST NOT** be left undefined.
-
-```bash
-export READ_DSN=foo # (non-normative)
-```
-
-
-
-[ferrite]: https://github.com/dogmatiq/ferrite
-[rfc 2119]: https://www.rfc-editor.org/rfc/rfc2119.html
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/bool/deprecated-with-default.md b/internal/mode/usage/markdown/testdata/spec/bool/deprecated-with-default.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/bool/deprecated-with-default.md
rename to internal/mode/usage/markdown/testdata/spec/bool/deprecated-with-default.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/bool/deprecated.md b/internal/mode/usage/markdown/testdata/spec/bool/deprecated.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/bool/deprecated.md
rename to internal/mode/usage/markdown/testdata/spec/bool/deprecated.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/bool/optional.md b/internal/mode/usage/markdown/testdata/spec/bool/optional.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/bool/optional.md
rename to internal/mode/usage/markdown/testdata/spec/bool/optional.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/bool/required.md b/internal/mode/usage/markdown/testdata/spec/bool/required.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/bool/required.md
rename to internal/mode/usage/markdown/testdata/spec/bool/required.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/bool/with-default.md b/internal/mode/usage/markdown/testdata/spec/bool/with-default.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/bool/with-default.md
rename to internal/mode/usage/markdown/testdata/spec/bool/with-default.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/duration/deprecated-with-default.md b/internal/mode/usage/markdown/testdata/spec/duration/deprecated-with-default.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/duration/deprecated-with-default.md
rename to internal/mode/usage/markdown/testdata/spec/duration/deprecated-with-default.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/duration/deprecated.md b/internal/mode/usage/markdown/testdata/spec/duration/deprecated.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/duration/deprecated.md
rename to internal/mode/usage/markdown/testdata/spec/duration/deprecated.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/duration/optional.md b/internal/mode/usage/markdown/testdata/spec/duration/optional.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/duration/optional.md
rename to internal/mode/usage/markdown/testdata/spec/duration/optional.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/duration/required.md b/internal/mode/usage/markdown/testdata/spec/duration/required.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/duration/required.md
rename to internal/mode/usage/markdown/testdata/spec/duration/required.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/duration/with-default.md b/internal/mode/usage/markdown/testdata/spec/duration/with-default.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/duration/with-default.md
rename to internal/mode/usage/markdown/testdata/spec/duration/with-default.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/duration/with-max.md b/internal/mode/usage/markdown/testdata/spec/duration/with-max.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/duration/with-max.md
rename to internal/mode/usage/markdown/testdata/spec/duration/with-max.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/enum/deprecated-with-default.md b/internal/mode/usage/markdown/testdata/spec/enum/deprecated-with-default.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/enum/deprecated-with-default.md
rename to internal/mode/usage/markdown/testdata/spec/enum/deprecated-with-default.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/enum/deprecated.md b/internal/mode/usage/markdown/testdata/spec/enum/deprecated.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/enum/deprecated.md
rename to internal/mode/usage/markdown/testdata/spec/enum/deprecated.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/enum/optional.md b/internal/mode/usage/markdown/testdata/spec/enum/optional.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/enum/optional.md
rename to internal/mode/usage/markdown/testdata/spec/enum/optional.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/enum/required.md b/internal/mode/usage/markdown/testdata/spec/enum/required.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/enum/required.md
rename to internal/mode/usage/markdown/testdata/spec/enum/required.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/enum/with-default.md b/internal/mode/usage/markdown/testdata/spec/enum/with-default.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/enum/with-default.md
rename to internal/mode/usage/markdown/testdata/spec/enum/with-default.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/float/deprecated-with-default.md b/internal/mode/usage/markdown/testdata/spec/float/deprecated-with-default.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/float/deprecated-with-default.md
rename to internal/mode/usage/markdown/testdata/spec/float/deprecated-with-default.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/float/deprecated.md b/internal/mode/usage/markdown/testdata/spec/float/deprecated.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/float/deprecated.md
rename to internal/mode/usage/markdown/testdata/spec/float/deprecated.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/float/optional.md b/internal/mode/usage/markdown/testdata/spec/float/optional.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/float/optional.md
rename to internal/mode/usage/markdown/testdata/spec/float/optional.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/float/required.md b/internal/mode/usage/markdown/testdata/spec/float/required.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/float/required.md
rename to internal/mode/usage/markdown/testdata/spec/float/required.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/float/with-default.md b/internal/mode/usage/markdown/testdata/spec/float/with-default.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/float/with-default.md
rename to internal/mode/usage/markdown/testdata/spec/float/with-default.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/float/with-max.md b/internal/mode/usage/markdown/testdata/spec/float/with-max.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/float/with-max.md
rename to internal/mode/usage/markdown/testdata/spec/float/with-max.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/float/with-min.md b/internal/mode/usage/markdown/testdata/spec/float/with-min.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/float/with-min.md
rename to internal/mode/usage/markdown/testdata/spec/float/with-min.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/float/with-minmax.md b/internal/mode/usage/markdown/testdata/spec/float/with-minmax.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/float/with-minmax.md
rename to internal/mode/usage/markdown/testdata/spec/float/with-minmax.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/k8s-service/deprecated-with-default.md b/internal/mode/usage/markdown/testdata/spec/k8s-service/deprecated-with-default.md
similarity index 88%
rename from internal/mode/usage/markdown/testdata/markdown/spec/k8s-service/deprecated-with-default.md
rename to internal/mode/usage/markdown/testdata/spec/k8s-service/deprecated-with-default.md
index 15d18ad..4b5cbbb 100644
--- a/internal/mode/usage/markdown/testdata/markdown/spec/k8s-service/deprecated-with-default.md
+++ b/internal/mode/usage/markdown/testdata/spec/k8s-service/deprecated-with-default.md
@@ -20,6 +20,10 @@ in a future version.
export REDIS_SERVICE_HOST=redis.example.org # (default)
```
+#### See Also
+
+- [`REDIS_SERVICE_PORT`](#REDIS_SERVICE_PORT) — kubernetes "redis" service port
+
### `REDIS_SERVICE_PORT`
> kubernetes "redis" service port
@@ -47,3 +51,7 @@ the system's service database, typically located in the `/etc/service` file on
UNIX-like systems. Standard service names are published by IANA.
+
+#### See Also
+
+- [`REDIS_SERVICE_HOST`](#REDIS_SERVICE_HOST) — kubernetes "redis" service host
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/k8s-service/deprecated.md b/internal/mode/usage/markdown/testdata/spec/k8s-service/deprecated.md
similarity index 87%
rename from internal/mode/usage/markdown/testdata/markdown/spec/k8s-service/deprecated.md
rename to internal/mode/usage/markdown/testdata/spec/k8s-service/deprecated.md
index 120343e..e6293d5 100644
--- a/internal/mode/usage/markdown/testdata/markdown/spec/k8s-service/deprecated.md
+++ b/internal/mode/usage/markdown/testdata/spec/k8s-service/deprecated.md
@@ -19,6 +19,10 @@ in a future version.
export REDIS_SERVICE_HOST=foo # (non-normative)
```
+#### See Also
+
+- [`REDIS_SERVICE_PORT`](#REDIS_SERVICE_PORT) — kubernetes "redis" service port
+
### `REDIS_SERVICE_PORT`
> kubernetes "redis" service port
@@ -45,3 +49,7 @@ the system's service database, typically located in the `/etc/service` file on
UNIX-like systems. Standard service names are published by IANA.
+
+#### See Also
+
+- [`REDIS_SERVICE_HOST`](#REDIS_SERVICE_HOST) — kubernetes "redis" service host
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/k8s-service/optional.md b/internal/mode/usage/markdown/testdata/spec/k8s-service/optional.md
similarity index 85%
rename from internal/mode/usage/markdown/testdata/markdown/spec/k8s-service/optional.md
rename to internal/mode/usage/markdown/testdata/spec/k8s-service/optional.md
index 7478aff..b7bf45e 100644
--- a/internal/mode/usage/markdown/testdata/markdown/spec/k8s-service/optional.md
+++ b/internal/mode/usage/markdown/testdata/spec/k8s-service/optional.md
@@ -16,6 +16,10 @@ typically does not need to be specified in the pod manifest.
export REDIS_SERVICE_HOST=foo # (non-normative)
```
+#### See Also
+
+- [`REDIS_SERVICE_PORT`](#REDIS_SERVICE_PORT) — kubernetes "redis" service port
+
### `REDIS_SERVICE_PORT`
> kubernetes "redis" service port
@@ -39,3 +43,7 @@ the system's service database, typically located in the `/etc/service` file on
UNIX-like systems. Standard service names are published by IANA.
+
+#### See Also
+
+- [`REDIS_SERVICE_HOST`](#REDIS_SERVICE_HOST) — kubernetes "redis" service host
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/k8s-service/required.md b/internal/mode/usage/markdown/testdata/spec/k8s-service/required.md
similarity index 84%
rename from internal/mode/usage/markdown/testdata/markdown/spec/k8s-service/required.md
rename to internal/mode/usage/markdown/testdata/spec/k8s-service/required.md
index f8f02d6..8f1bc6a 100644
--- a/internal/mode/usage/markdown/testdata/markdown/spec/k8s-service/required.md
+++ b/internal/mode/usage/markdown/testdata/spec/k8s-service/required.md
@@ -15,6 +15,10 @@ typically does not need to be specified in the pod manifest.
export REDIS_SERVICE_HOST=foo # (non-normative)
```
+#### See Also
+
+- [`REDIS_SERVICE_PORT`](#REDIS_SERVICE_PORT) — kubernetes "redis" service port
+
### `REDIS_SERVICE_PORT`
> kubernetes "redis" service port
@@ -37,3 +41,7 @@ the system's service database, typically located in the `/etc/service` file on
UNIX-like systems. Standard service names are published by IANA.
+
+#### See Also
+
+- [`REDIS_SERVICE_HOST`](#REDIS_SERVICE_HOST) — kubernetes "redis" service host
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/k8s-service/with-default.md b/internal/mode/usage/markdown/testdata/spec/k8s-service/with-default.md
similarity index 86%
rename from internal/mode/usage/markdown/testdata/markdown/spec/k8s-service/with-default.md
rename to internal/mode/usage/markdown/testdata/spec/k8s-service/with-default.md
index e4290c0..0820de4 100644
--- a/internal/mode/usage/markdown/testdata/markdown/spec/k8s-service/with-default.md
+++ b/internal/mode/usage/markdown/testdata/spec/k8s-service/with-default.md
@@ -17,6 +17,10 @@ typically does not need to be specified in the pod manifest.
export REDIS_SERVICE_HOST=redis.example.org # (default)
```
+#### See Also
+
+- [`REDIS_SERVICE_PORT`](#REDIS_SERVICE_PORT) — kubernetes "redis" service port
+
### `REDIS_SERVICE_PORT`
> kubernetes "redis" service port
@@ -41,3 +45,7 @@ the system's service database, typically located in the `/etc/service` file on
UNIX-like systems. Standard service names are published by IANA.
+
+#### See Also
+
+- [`REDIS_SERVICE_HOST`](#REDIS_SERVICE_HOST) — kubernetes "redis" service host
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/network-port/deprecated-with-default.md b/internal/mode/usage/markdown/testdata/spec/network-port/deprecated-with-default.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/network-port/deprecated-with-default.md
rename to internal/mode/usage/markdown/testdata/spec/network-port/deprecated-with-default.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/network-port/deprecated.md b/internal/mode/usage/markdown/testdata/spec/network-port/deprecated.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/network-port/deprecated.md
rename to internal/mode/usage/markdown/testdata/spec/network-port/deprecated.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/network-port/optional.md b/internal/mode/usage/markdown/testdata/spec/network-port/optional.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/network-port/optional.md
rename to internal/mode/usage/markdown/testdata/spec/network-port/optional.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/network-port/required.md b/internal/mode/usage/markdown/testdata/spec/network-port/required.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/network-port/required.md
rename to internal/mode/usage/markdown/testdata/spec/network-port/required.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/network-port/with-default.md b/internal/mode/usage/markdown/testdata/spec/network-port/with-default.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/network-port/with-default.md
rename to internal/mode/usage/markdown/testdata/spec/network-port/with-default.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/signed/deprecated-with-default.md b/internal/mode/usage/markdown/testdata/spec/signed/deprecated-with-default.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/signed/deprecated-with-default.md
rename to internal/mode/usage/markdown/testdata/spec/signed/deprecated-with-default.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/signed/deprecated.md b/internal/mode/usage/markdown/testdata/spec/signed/deprecated.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/signed/deprecated.md
rename to internal/mode/usage/markdown/testdata/spec/signed/deprecated.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/signed/optional.md b/internal/mode/usage/markdown/testdata/spec/signed/optional.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/signed/optional.md
rename to internal/mode/usage/markdown/testdata/spec/signed/optional.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/signed/required.md b/internal/mode/usage/markdown/testdata/spec/signed/required.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/signed/required.md
rename to internal/mode/usage/markdown/testdata/spec/signed/required.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/signed/with-default.md b/internal/mode/usage/markdown/testdata/spec/signed/with-default.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/signed/with-default.md
rename to internal/mode/usage/markdown/testdata/spec/signed/with-default.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/signed/with-max.md b/internal/mode/usage/markdown/testdata/spec/signed/with-max.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/signed/with-max.md
rename to internal/mode/usage/markdown/testdata/spec/signed/with-max.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/signed/with-min.md b/internal/mode/usage/markdown/testdata/spec/signed/with-min.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/signed/with-min.md
rename to internal/mode/usage/markdown/testdata/spec/signed/with-min.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/signed/with-minmax.md b/internal/mode/usage/markdown/testdata/spec/signed/with-minmax.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/signed/with-minmax.md
rename to internal/mode/usage/markdown/testdata/spec/signed/with-minmax.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/string/deprecated-with-default.md b/internal/mode/usage/markdown/testdata/spec/string/deprecated-with-default.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/string/deprecated-with-default.md
rename to internal/mode/usage/markdown/testdata/spec/string/deprecated-with-default.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/string/deprecated.md b/internal/mode/usage/markdown/testdata/spec/string/deprecated.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/string/deprecated.md
rename to internal/mode/usage/markdown/testdata/spec/string/deprecated.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/string/optional.md b/internal/mode/usage/markdown/testdata/spec/string/optional.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/string/optional.md
rename to internal/mode/usage/markdown/testdata/spec/string/optional.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/string/required.md b/internal/mode/usage/markdown/testdata/spec/string/required.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/string/required.md
rename to internal/mode/usage/markdown/testdata/spec/string/required.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/string/sensitive-optional.md b/internal/mode/usage/markdown/testdata/spec/string/sensitive-optional.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/string/sensitive-optional.md
rename to internal/mode/usage/markdown/testdata/spec/string/sensitive-optional.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/string/sensitive-required.md b/internal/mode/usage/markdown/testdata/spec/string/sensitive-required.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/string/sensitive-required.md
rename to internal/mode/usage/markdown/testdata/spec/string/sensitive-required.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/string/sensitive-with-default.md b/internal/mode/usage/markdown/testdata/spec/string/sensitive-with-default.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/string/sensitive-with-default.md
rename to internal/mode/usage/markdown/testdata/spec/string/sensitive-with-default.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/string/with-default.md b/internal/mode/usage/markdown/testdata/spec/string/with-default.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/string/with-default.md
rename to internal/mode/usage/markdown/testdata/spec/string/with-default.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/unsigned/deprecated-with-default.md b/internal/mode/usage/markdown/testdata/spec/unsigned/deprecated-with-default.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/unsigned/deprecated-with-default.md
rename to internal/mode/usage/markdown/testdata/spec/unsigned/deprecated-with-default.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/unsigned/deprecated.md b/internal/mode/usage/markdown/testdata/spec/unsigned/deprecated.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/unsigned/deprecated.md
rename to internal/mode/usage/markdown/testdata/spec/unsigned/deprecated.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/unsigned/optional.md b/internal/mode/usage/markdown/testdata/spec/unsigned/optional.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/unsigned/optional.md
rename to internal/mode/usage/markdown/testdata/spec/unsigned/optional.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/unsigned/required.md b/internal/mode/usage/markdown/testdata/spec/unsigned/required.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/unsigned/required.md
rename to internal/mode/usage/markdown/testdata/spec/unsigned/required.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/unsigned/with-default.md b/internal/mode/usage/markdown/testdata/spec/unsigned/with-default.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/unsigned/with-default.md
rename to internal/mode/usage/markdown/testdata/spec/unsigned/with-default.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/unsigned/with-max.md b/internal/mode/usage/markdown/testdata/spec/unsigned/with-max.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/unsigned/with-max.md
rename to internal/mode/usage/markdown/testdata/spec/unsigned/with-max.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/unsigned/with-min.md b/internal/mode/usage/markdown/testdata/spec/unsigned/with-min.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/unsigned/with-min.md
rename to internal/mode/usage/markdown/testdata/spec/unsigned/with-min.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/unsigned/with-minmax.md b/internal/mode/usage/markdown/testdata/spec/unsigned/with-minmax.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/unsigned/with-minmax.md
rename to internal/mode/usage/markdown/testdata/spec/unsigned/with-minmax.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/url/deprecated-with-default.md b/internal/mode/usage/markdown/testdata/spec/url/deprecated-with-default.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/url/deprecated-with-default.md
rename to internal/mode/usage/markdown/testdata/spec/url/deprecated-with-default.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/url/deprecated.md b/internal/mode/usage/markdown/testdata/spec/url/deprecated.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/url/deprecated.md
rename to internal/mode/usage/markdown/testdata/spec/url/deprecated.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/url/optional.md b/internal/mode/usage/markdown/testdata/spec/url/optional.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/url/optional.md
rename to internal/mode/usage/markdown/testdata/spec/url/optional.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/url/required.md b/internal/mode/usage/markdown/testdata/spec/url/required.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/url/required.md
rename to internal/mode/usage/markdown/testdata/spec/url/required.md
diff --git a/internal/mode/usage/markdown/testdata/markdown/spec/url/with-default.md b/internal/mode/usage/markdown/testdata/spec/url/with-default.md
similarity index 100%
rename from internal/mode/usage/markdown/testdata/markdown/spec/url/with-default.md
rename to internal/mode/usage/markdown/testdata/spec/url/with-default.md
diff --git a/internal/mode/usage/markdown/usage_test.go b/internal/mode/usage/markdown/usage_test.go
deleted file mode 100644
index dab76ab..0000000
--- a/internal/mode/usage/markdown/usage_test.go
+++ /dev/null
@@ -1,81 +0,0 @@
-package markdown_test
-
-import (
- "bytes"
- "os"
- "path/filepath"
-
- "github.com/dogmatiq/ferrite"
- "github.com/dogmatiq/ferrite/internal/mode"
- . "github.com/dogmatiq/ferrite/internal/mode/usage/markdown"
- "github.com/dogmatiq/ferrite/variable"
- . "github.com/jmalloc/gomegax"
- . "github.com/onsi/ginkgo/v2"
- . "github.com/onsi/gomega"
-)
-
-var _ = Describe("func Run()", func() {
- var reg *variable.Registry
-
- BeforeEach(func() {
- reg = &variable.Registry{
- Environment: &variable.MemoryEnvironment{},
- }
- })
-
- DescribeTable(
- "it describes the environment variable",
- func(
- file string,
- setup func(*variable.Registry),
- ) {
- setup(reg)
-
- expect, err := os.ReadFile(
- filepath.Join(
- "testdata",
- "markdown",
- "usage",
- file,
- ),
- )
- Expect(err).ShouldNot(HaveOccurred())
-
- actual := &bytes.Buffer{}
- exited := false
-
- Run(
- mode.Options{
- Registry: reg,
- Args: []string{""},
- Out: actual,
- Exit: func(code int) {
- exited = true
- Expect(code).To(Equal(0))
- },
- },
- )
- ExpectWithOffset(1, actual.String()).To(EqualX(string(expect)))
- Expect(exited).To(BeTrue())
- },
- Entry(
- "usage",
- "usage.md",
- func(reg *variable.Registry) {
- ferrite.
- Bool("DEBUG", "enable or disable debugging features").
- Optional(variable.WithRegistry(reg))
- },
- ),
- Entry(
- "usage shows the default value in examples if available",
- "usage-shows-default.md",
- func(reg *variable.Registry) {
- ferrite.
- NetworkPort("PORT", "an environment variable that has a default value").
- WithDefault("ftp").
- Required(variable.WithRegistry(reg))
- },
- ),
- )
-})
diff --git a/kubernetes.go b/kubernetes.go
index 127a019..659fac7 100644
--- a/kubernetes.go
+++ b/kubernetes.go
@@ -92,6 +92,9 @@ func KubernetesService(svc string) *KubernetesServiceBuilder {
buildNetworkPortSyntaxDocumentation(b.portSpec.Documentation())
+ seeAlso(b.hostSpec.Peek(), b.portSpec.Peek())
+ seeAlso(b.portSpec.Peek(), b.hostSpec.Peek())
+
return b
}
@@ -149,6 +152,13 @@ func (b *KubernetesServiceBuilder) WithDefault(host, port string) *KubernetesSer
return b
}
+// SeeAlso creates a relationship between this variable and those used by i.
+func (b *KubernetesServiceBuilder) SeeAlso(i Input, options ...SeeAlsoOption) *KubernetesServiceBuilder {
+ seeAlsoInput(&b.hostSpec, i, options...)
+ seeAlsoInput(&b.portSpec, i, options...)
+ return b
+}
+
// Required completes the build process and registers a required variable with
// Ferrite's validation system.
func (b *KubernetesServiceBuilder) Required(options ...RequiredOption) Required[KubernetesAddress] {
@@ -166,6 +176,7 @@ func (b *KubernetesServiceBuilder) Required(options ...RequiredOption) Required[
)
return requiredFunc[KubernetesAddress]{
+ []variable.Any{host, port},
func() (KubernetesAddress, error) {
h, ok, err := host.NativeValue()
if err != nil {
@@ -204,6 +215,7 @@ func (b *KubernetesServiceBuilder) Optional(options ...OptionalOption) Optional[
)
return optionalFunc[KubernetesAddress]{
+ []variable.Any{host, port},
b.optionalResolver(host, port),
}
}
@@ -225,6 +237,7 @@ func (b *KubernetesServiceBuilder) Deprecated(options ...DeprecatedOption) Depre
)
return deprecatedFunc[KubernetesAddress]{
+ []variable.Any{host, port},
b.optionalResolver(host, port),
}
}
diff --git a/network.go b/network.go
index 3452a8c..ae9d002 100644
--- a/network.go
+++ b/network.go
@@ -45,6 +45,12 @@ func (b *NetworkPortBuilder) WithDefault(v string) *NetworkPortBuilder {
return b
}
+// SeeAlso creates a relationship between this variable and those used by i.
+func (b *NetworkPortBuilder) SeeAlso(i Input, options ...SeeAlsoOption) *NetworkPortBuilder {
+ seeAlsoInput(&b.spec, i, options...)
+ return b
+}
+
// Required completes the build process and registers a required variable with
// Ferrite's validation system.
func (b *NetworkPortBuilder) Required(options ...RequiredOption) Required[string] {
diff --git a/numfloat.go b/numfloat.go
index f8147fd..4d47696 100644
--- a/numfloat.go
+++ b/numfloat.go
@@ -73,6 +73,12 @@ func (b *FloatBuilder[T]) WithMaximum(v T) *FloatBuilder[T] {
return b
}
+// SeeAlso creates a relationship between this variable and those used by i.
+func (b *FloatBuilder[T]) SeeAlso(i Input, options ...SeeAlsoOption) *FloatBuilder[T] {
+ seeAlsoInput(&b.spec, i, options...)
+ return b
+}
+
// Required completes the build process and registers a required variable with
// Ferrite's validation system.
func (b *FloatBuilder[T]) Required(options ...RequiredOption) Required[T] {
diff --git a/numsigned.go b/numsigned.go
index 6cd385a..128a886 100644
--- a/numsigned.go
+++ b/numsigned.go
@@ -73,6 +73,12 @@ func (b *SignedBuilder[T]) WithMaximum(v T) *SignedBuilder[T] {
return b
}
+// SeeAlso creates a relationship between this variable and those used by i.
+func (b *SignedBuilder[T]) SeeAlso(i Input, options ...SeeAlsoOption) *SignedBuilder[T] {
+ seeAlsoInput(&b.spec, i, options...)
+ return b
+}
+
// Required completes the build process and registers a required variable with
// Ferrite's validation system.
func (b *SignedBuilder[T]) Required(options ...RequiredOption) Required[T] {
diff --git a/numunsigned.go b/numunsigned.go
index 283db85..065b794 100644
--- a/numunsigned.go
+++ b/numunsigned.go
@@ -72,6 +72,12 @@ func (b *UnsignedBuilder[T]) WithMaximum(v T) *UnsignedBuilder[T] {
return b
}
+// SeeAlso creates a relationship between this variable and those used by i.
+func (b *UnsignedBuilder[T]) SeeAlso(i Input, options ...SeeAlsoOption) *UnsignedBuilder[T] {
+ seeAlsoInput(&b.spec, i, options...)
+ return b
+}
+
// Required completes the build process and registers a required variable with
// Ferrite's validation system.
func (b *UnsignedBuilder[T]) Required(options ...RequiredOption) Required[T] {
diff --git a/relationship.go b/relationship.go
new file mode 100644
index 0000000..ecef4d6
--- /dev/null
+++ b/relationship.go
@@ -0,0 +1,36 @@
+package ferrite
+
+import "github.com/dogmatiq/ferrite/variable"
+
+// SeeAlsoOption changes the behavior of a call the SeeAlso() method of the
+// various builder implementations.
+type SeeAlsoOption interface {
+ applySeeAlsoOption(*variable.SeeAlso)
+}
+
+func seeAlsoInput(
+ from variable.SpecBuilder,
+ to Input,
+ options ...SeeAlsoOption,
+) {
+ for _, v := range to.variables() {
+ seeAlso(from.Peek(), v.Spec(), options...)
+ }
+}
+
+func seeAlso(
+ from, to variable.Spec,
+ options ...SeeAlsoOption,
+) {
+ r := variable.SeeAlso{
+ From: from,
+ To: to,
+ }
+
+ for _, opt := range options {
+ opt.applySeeAlsoOption(&r)
+ }
+
+ from.AddRelationship(r)
+ to.AddRelationship(r)
+}
diff --git a/relationshipexample_test.go b/relationshipexample_test.go
new file mode 100644
index 0000000..f85bb6e
--- /dev/null
+++ b/relationshipexample_test.go
@@ -0,0 +1,18 @@
+package ferrite_test
+
+import (
+ "github.com/dogmatiq/ferrite"
+)
+
+func Example_seeAlso() {
+ defer example()()
+
+ verbose := ferrite.
+ Bool("VERBOSE", "enable verbose logging").
+ Optional()
+
+ ferrite.
+ Bool("DEBUG", "enable or disable debugging features").
+ SeeAlso(verbose).
+ Optional()
+}
diff --git a/string.go b/string.go
index f73ea2c..a868192 100644
--- a/string.go
+++ b/string.go
@@ -66,6 +66,12 @@ func (b *StringBuilder[T]) WithSensitiveContent() *StringBuilder[T] {
return b
}
+// SeeAlso creates a relationship between this variable and those used by i.
+func (b *StringBuilder[T]) SeeAlso(i Input, options ...SeeAlsoOption) *StringBuilder[T] {
+ seeAlsoInput(&b.spec, i, options...)
+ return b
+}
+
// Required completes the build process and registers a required variable with
// Ferrite's validation system.
func (b *StringBuilder[T]) Required(options ...RequiredOption) Required[T] {
diff --git a/url.go b/url.go
index 4c66c55..5a6821e 100644
--- a/url.go
+++ b/url.go
@@ -69,6 +69,12 @@ func (b *URLBuilder) WithDefault(v string) *URLBuilder {
return b
}
+// SeeAlso creates a relationship between this variable and those used by i.
+func (b *URLBuilder) SeeAlso(i Input, options ...SeeAlsoOption) *URLBuilder {
+ seeAlsoInput(&b.spec, i, options...)
+ return b
+}
+
// Required completes the build process and registers a required variable with
// Ferrite's validation system.
func (b *URLBuilder) Required(options ...RequiredOption) Required[*url.URL] {
diff --git a/variable/relationship.go b/variable/relationship.go
new file mode 100644
index 0000000..81f17a3
--- /dev/null
+++ b/variable/relationship.go
@@ -0,0 +1,32 @@
+package variable
+
+// Relationship represents a relationship between two variables.
+type Relationship interface {
+ // Specs returns the specs for the variables that are involved in the
+ // relationship.
+ Specs() []Spec
+
+ // AcceptVisitor passes the schema to the appropriate method of v.
+ AcceptVisitor(v RelationshipVisitor)
+}
+
+// RelationshipVisitor dispatches based on a relationship's type.
+type RelationshipVisitor interface {
+ VisitSeeAlso(SeeAlso)
+}
+
+// SeeAlso is a relationship that refers the user from one variable to another.
+type SeeAlso struct {
+ From, To Spec
+}
+
+// Specs returns the specs for the variables that are involved in the
+// relationship.
+func (r SeeAlso) Specs() []Spec {
+ return []Spec{r.From, r.To}
+}
+
+// AcceptVisitor passes r to the appropriate method of v.
+func (r SeeAlso) AcceptVisitor(v RelationshipVisitor) {
+ v.VisitSeeAlso(r)
+}
diff --git a/variable/schema.go b/variable/schema.go
index 11eca28..926a8be 100644
--- a/variable/schema.go
+++ b/variable/schema.go
@@ -15,7 +15,7 @@ type Schema interface {
Finalize() error
// AcceptVisitor passes the schema to the appropriate method of v.
- AcceptVisitor(SchemaVisitor)
+ AcceptVisitor(v SchemaVisitor)
}
// SchemaError indicates that a value is invalid because it violates its schema.
@@ -26,7 +26,7 @@ type SchemaError interface {
Schema() Schema
// AcceptVisitor passes the error to the appropriate method of v.
- AcceptVisitor(SchemaErrorVisitor)
+ AcceptVisitor(v SchemaErrorVisitor)
}
// SchemaVisitor dispatches based on a variable's schema.
diff --git a/variable/spec.go b/variable/spec.go
index 001370c..e59df0c 100644
--- a/variable/spec.go
+++ b/variable/spec.go
@@ -42,6 +42,12 @@ type Spec interface {
// Documentation returns a list of chunks of documentation text.
Documentation() []Documentation
+
+ // Relationships returns a list of relationships that involve this variable.
+ Relationships() []Relationship
+
+ // AddRelationship adds a relationship that involves this variable.
+ AddRelationship(r Relationship)
}
// IsDefault returns true if v is the default value of the given spec.
@@ -55,16 +61,17 @@ func IsDefault(s Spec, v Literal) bool {
// TypedSpec builds a specification for a variable depicted by type T.
type TypedSpec[T any] struct {
- name string
- desc string
- def maybe.Value[valueOf[T]]
- required bool
- sensitive bool
- deprecated bool
- schema TypedSchema[T]
- examples []Example
- docs []Documentation
- constraints []TypedConstraint[T]
+ name string
+ desc string
+ def maybe.Value[valueOf[T]]
+ required bool
+ sensitive bool
+ deprecated bool
+ schema TypedSchema[T]
+ examples []Example
+ docs []Documentation
+ constraints []TypedConstraint[T]
+ relationships []Relationship
}
// Name returns the name of the variable.
@@ -124,6 +131,16 @@ func (s *TypedSpec[T]) Documentation() []Documentation {
return s.docs
}
+// Relationships returns a list of relationships that involve this variable.
+func (s TypedSpec[T]) Relationships() []Relationship {
+ return s.relationships
+}
+
+// AddRelationship adds a relationship that involves this variable.
+func (s *TypedSpec[T]) AddRelationship(r Relationship) {
+ s.relationships = append(s.relationships, r)
+}
+
// CheckConstraints returns an error if v does not satisfy any one of the
// specification's constraints.
func (s *TypedSpec[T]) CheckConstraints(v T) ConstraintError {
diff --git a/variable/specbuilder.go b/variable/specbuilder.go
index ff68e55..7cc3a93 100644
--- a/variable/specbuilder.go
+++ b/variable/specbuilder.go
@@ -12,9 +12,11 @@ type SpecBuilder interface {
Name(string)
Description(string)
MarkRequired()
- MarkDeprecated(reason string)
+ MarkDeprecated()
MarkSensitive()
Documentation() DocumentationBuilder
+ Relationship(Relationship)
+ Peek() Spec
}
// TypedSpecBuilder builds a specification for a variable of type T.
@@ -108,6 +110,16 @@ func (b *TypedSpecBuilder[T]) Documentation() DocumentationBuilder {
}
}
+// Relationship adds a relationship that involves this variable.
+func (b *TypedSpecBuilder[T]) Relationship(rel Relationship) {
+ b.spec.AddRelationship(rel)
+}
+
+// Peek returns the (potentially invalid) spec that is being built.
+func (b *TypedSpecBuilder[T]) Peek() Spec {
+ return &b.spec
+}
+
// Done builds the specification and registers the variable.
func (b *TypedSpecBuilder[T]) Done(schema TypedSchema[T]) *TypedSpec[T] {
b.spec.schema = schema