diff --git a/doc.go b/doc.go index 3b0fa79..7025259 100644 --- a/doc.go +++ b/doc.go @@ -148,6 +148,36 @@ Note: the Server slice must already have members inside it (i.e. from loading of Maps and map values cannot be populated from the environment. +# Squashing + +Fields to nested structs can be flattened by annotating with `fig:",squash"`. Fig +treats values into these structs as if they were part of the parent. + + type Base struct { + Env string `fig:"env"` + Logging struct { + Level string `fig:"level"` + } `fig:"logging"` + } + + type Config struct { + Base `fig:",squash"` + Addr string `fig:"addr"` + } + +Using the above example, the configuration file would look like: + + env: prod + logging: + level: info + addr: 0.0.0.0:8080 + +And similarly, the corresponding environment variables: + + ENV=prod + LOGGING_LEVEL=info + ADDR=0.0.0.0:8080 + # Time Change the layout fig uses to parse times using `TimeLayout()`. diff --git a/field.go b/field.go index 8756761..c78bc9a 100644 --- a/field.go +++ b/field.go @@ -139,20 +139,35 @@ func (f *field) name() string { // path is a dot separated path consisting of all the names of // the field's ancestors starting from the topmost parent all the // way down to the field itself. -func (f *field) path() (path string) { - var visit func(f *field) - visit = func(f *field) { +// +// Squashed structs will omit themselves from the path +func (f *field) path(tagKey string) (path string) { + var visit func(f *field, squashed bool) + visit = func(f *field, squashed bool) { if f.parent != nil { - visit(f.parent) + visit(f.parent, false) + + // Check if we are squashed or not + // + // type Base struct { Env string } + // type Config struct { Base `fig:",squash"`} + // + // In the above example, path to 'env' should be 'Env', not 'Base.Env' + if f.parent.t.Kind() == reflect.Struct { + parentField, ok := f.parent.t.FieldByName(f.st.Name) + squashed = ok && parentField.Tag.Get(tagKey) == ",squash" + } + } + if !squashed { + path += f.name() } - path += f.name() // if it's a slice/array we don't want a dot before the slice indexer // e.g. we want A[0].B instead of A.[0].B if f.t.Kind() != reflect.Slice && f.t.Kind() != reflect.Array && f.t.Kind() != reflect.Map { path += "." } } - visit(f) + visit(f, false) return strings.Trim(path, ".") } diff --git a/field_test.go b/field_test.go index 923c92b..1d07c2d 100644 --- a/field_test.go +++ b/field_test.go @@ -186,12 +186,44 @@ func Test_parseTag(t *testing.T) { } } +func Test_squashPath(t *testing.T) { + cfg := struct { + Base struct { + Env string + ServiceName string + Nested []struct{ Val string } + Logging struct { + Level int + } + } `fig:",squash"` + App struct { + Custom string + } + }{} + cfg.Base.Nested = []struct{ Val string }{{}, {}} + + fields := flattenCfg(&cfg, "fig") + if len(fields) != 10 { + t.Fatalf("len(fields) == %d, expected %d", len(fields), 10) + } + checkField(t, fields[0], "Base", "") + checkField(t, fields[1], "Env", "Env") + checkField(t, fields[2], "ServiceName", "ServiceName") + checkField(t, fields[3], "Nested", "Nested") + checkField(t, fields[4], "Val", "Nested[0].Val") + checkField(t, fields[5], "Val", "Nested[1].Val") + checkField(t, fields[6], "Logging", "Logging") + checkField(t, fields[7], "Level", "Logging.Level") + checkField(t, fields[8], "App", "App") + checkField(t, fields[9], "Custom", "App.Custom") +} + func checkField(t *testing.T, f *field, name, path string) { t.Helper() if f.name() != name { t.Errorf("f.name() == %s, expected %s", f.name(), name) } - if f.path() != path { - t.Errorf("f.path() == %s, expected %s", f.path(), path) + if f.path("fig") != path { + t.Errorf("f.path() == %s, expected %s", f.path("fig"), path) } } diff --git a/fig.go b/fig.go index 7632ae4..7564566 100644 --- a/fig.go +++ b/fig.go @@ -213,7 +213,8 @@ func stringToRegexpHookFunc() mapstructure.DecodeHookFunc { return func( f reflect.Type, t reflect.Type, - data interface{}) (interface{}, error) { + data interface{}, + ) (interface{}, error) { if f.Kind() != reflect.String { return data, nil } @@ -264,7 +265,7 @@ func (f *fig) processCfg(cfg interface{}) error { for _, field := range fields { if err := f.processField(field); err != nil { - errs[field.path()] = err + errs[field.path(f.tag)] = err } } @@ -283,7 +284,7 @@ func (f *fig) processField(field *field) error { } if f.useEnv { - if err := f.setFromEnv(field.v, field.path()); err != nil { + if err := f.setFromEnv(field.v, field.path(f.tag)); err != nil { return fmt.Errorf("unable to set from env: %w", err) } }