From 22991b98b717c3384fa956740b1264b7c150c1b1 Mon Sep 17 00:00:00 2001 From: Benjamin Bengfort Date: Sat, 21 Mar 2026 14:52:19 -0500 Subject: [PATCH] Better Usage Output --- README.md | 56 ++++++++++++++++++++++++++++++++ usage/testdata/custom.txt | 3 +- usage/testdata/default_list.txt | 7 +++- usage/testdata/default_table.txt | 3 +- usage/testdata/fault.txt | 1 + usage/usage.go | 7 +++- usage/usage_test.go | 6 +++- 7 files changed, 78 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0e4eeb2..a88991a 100644 --- a/README.md +++ b/README.md @@ -374,6 +374,62 @@ If you do not want confire to perform any validation at all, use the `NoValidate confire.Process(&conf, confire.NoValidate) ``` +### Validation Errors + +If you write your own config that implements the `Validator` interface, then you can use Confire's built in `InvalidConfig` errors as follows: + +```go +import "go.rtnl.ai/confire" + +type MyConfig struct { + Interval time.Duration + Port int + Window TimeWindow +} + +type TimeWindow struct { + Date TimeDecoder + Length time.Duration +} + +type TimeDecoder time.Time + +func (c MyConfig) Validate() (err error) { + if t.Interval < 1 { + // A more extensive required check that also takes into account negative values + err = confire.Join(err, confire.Required("", "interval")) + } + + if t.Port < 2000 || t.Port > 65334 { + // A type specific invalid configuration error + err = confire.Join(err, confire.Invalid("", "port", "port must be in the integer range [2000, 65334]")) + } + + // Returns all configuration errors, not just a single configuration error. + return err +} + +func (c TimeWindow) Validate() (err error) { + if verr := CheckWindowLength(c.Length); verr != nil { + // Wrap another error as the invalid configuration error + // Note that you can use nesting to show that this is window.length that is erroring. + err = confire.Join(err, confire.Wrap("window", "length", "bad window length", verr)) + } + + return err +} + +func (t *TimeDecoder) Decode(s string) error { + if e := time.Parse(s, layout); e != nil { + // Return a special parsing error error + return confire.Parse("", "date", e) + } + return nil +} +``` + +This error handling allows you greater flexibility in returning invalid config typed errors that are the same types as the validator built in to confire. It also allows you to report all invalid fields all at once rather than one error at a time, using the `Join` functionality. + ## Parsing Environment variables and default values in struct tags are all strings that must be parsed into more complex types such as `bool`, `uint64`, `[]string`, `map[int]string` and others, therefore some parsing is required. diff --git a/usage/testdata/custom.txt b/usage/testdata/custom.txt index 372d5d3..36305f7 100644 --- a/usage/testdata/custom.txt +++ b/usage/testdata/custom.txt @@ -8,6 +8,7 @@ CONFIRE_RATE=value·between·0·and·1 CONFIRE_CANCEL= CONFIRE_LOG_LEVEL= CONFIRE_TIMEOUT=amount·of·time·to·wait·for·a·respone +DATABASE_URL=database·connection·DSN CONFIRE_COLORS_PRIMARY= CONFIRE_COLORS_SECONDARY= -CONFIRE_SENDGRID_API_KEY= +SENDGRID_API_KEY= diff --git a/usage/testdata/default_list.txt b/usage/testdata/default_list.txt index d906810..d57b94b 100644 --- a/usage/testdata/default_list.txt +++ b/usage/testdata/default_list.txt @@ -51,6 +51,11 @@ CONFIRE_TIMEOUT ··[type]········Duration ··[default]·····30s ··[required]···· +DATABASE_URL +··[description]·database·connection·DSN +··[type]········String +··[default]····· +··[required]····true CONFIRE_COLORS_PRIMARY ··[description]· ··[type]········String @@ -61,7 +66,7 @@ CONFIRE_COLORS_SECONDARY ··[type]········String ··[default]·····#eeffee ··[required]···· -CONFIRE_SENDGRID_API_KEY +SENDGRID_API_KEY ··[description]· ··[type]········String ··[default]····· diff --git a/usage/testdata/default_table.txt b/usage/testdata/default_table.txt index 4841952..bda6327 100644 --- a/usage/testdata/default_table.txt +++ b/usage/testdata/default_table.txt @@ -12,6 +12,7 @@ CONFIRE_RATE················Float··············· CONFIRE_CANCEL··············Unsigned·Integer·······························16···························· CONFIRE_LOG_LEVEL···········LogLevel·······································info·························· CONFIRE_TIMEOUT·············Duration·······································30s···························amount·of·time·to·wait·for·a·respone +DATABASE_URL················String···························································true········database·connection·DSN CONFIRE_COLORS_PRIMARY······String·········································#cc6699······················· CONFIRE_COLORS_SECONDARY····String·········································#eeffee······················· -CONFIRE_SENDGRID_API_KEY····String······································································· +SENDGRID_API_KEY············String······································································· diff --git a/usage/testdata/fault.txt b/usage/testdata/fault.txt index 0135ae8..896f855 100644 --- a/usage/testdata/fault.txt +++ b/usage/testdata/fault.txt @@ -11,3 +11,4 @@ {.Key} {.Key} {.Key} +{.Key} diff --git a/usage/usage.go b/usage/usage.go index 49862c8..0d50184 100644 --- a/usage/usage.go +++ b/usage/usage.go @@ -51,7 +51,12 @@ func Usagef(prefix string, spec interface{}, out io.Writer, format string) error // Specify the default usage template functions functions := template.FuncMap{ - "usage_key": func(v env.Info) string { return v.Key }, + "usage_key": func(v env.Info) string { + if v.Alt != "" { + return v.Alt + } + return v.Key + }, "usage_description": func(v env.Info) string { return v.Field.Tag("desc") }, "usage_type": func(v env.Info) string { return toTypeDescription(v.Field.Type()) }, "usage_default": func(v env.Info) string { return v.Field.Tag("default") }, diff --git a/usage/usage_test.go b/usage/usage_test.go index 49c2108..53d3529 100644 --- a/usage/usage_test.go +++ b/usage/usage_test.go @@ -106,7 +106,10 @@ type Specification struct { Cancel uint16 `default:"16"` LogLevel LogLevel `default:"info" split_words:"true"` Timeout time.Duration `default:"30s" desc:"amount of time to wait for a respone"` - Colors struct { + Database struct { + URL string `required:"true" env:"DATABASE_URL" desc:"database connection DSN"` + } + Colors struct { Primary Color `default:"#cc6699"` Secondary Color `default:"#eeffee"` } @@ -183,6 +186,7 @@ func (e *CustomURL) Decode(value string) (err error) { } func compareUsage(t *testing.T, path, actual string) { + t.Helper() data, err := os.ReadFile(path) assert.Ok(t, err) expected := string(data)