Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()`.
Expand Down
27 changes: 21 additions & 6 deletions field.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, ".")
}

Expand Down
36 changes: 34 additions & 2 deletions field_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
7 changes: 4 additions & 3 deletions fig.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
}

Expand All @@ -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)
}
}
Expand Down
Loading