diff --git a/marshal.go b/marshal.go index e664a96..81d6997 100644 --- a/marshal.go +++ b/marshal.go @@ -26,6 +26,7 @@ type marshalState struct { schemaComments bool seenStructs map[reflect.Type]bool allowExtra bool + defaultTransformer func(string) string } // Create a shallow clone with schema overridden. @@ -84,6 +85,13 @@ func WithSchemaComments(v bool) MarshalOption { } } +// WithDefaultTransformer allows custom processing of default values in struct tags. +func WithDefaultTransformer(transformer func(string) string) MarshalOption { + return func(options *marshalState) { + options.defaultTransformer = transformer + } +} + func asSchema() MarshalOption { return func(options *marshalState) { options.schema = true @@ -264,17 +272,17 @@ func fieldToAttr(field field, tag tag, opt *marshalState) (*Attribute, error) { if err != nil { return nil, err } - attr.Default, err = defaultValueFromTag(field, tag.defaultValue) + attr.Default, err = defaultValueFromTag(field, tag.defaultValue, opt) if err != nil { return nil, err } attr.Optional = (tag.optional || attr.Default != nil) && opt.schema - attr.Enum, err = enumValuesFromTag(field, tag.enum) + attr.Enum, err = enumValuesFromTag(field, tag.enum, opt) return attr, err } -func defaultValueFromTag(f field, defaultValue string) (Value, error) { - v, err := valueFromTag(f, defaultValue) +func defaultValueFromTag(f field, defaultValue string, opt *marshalState) (Value, error) { + v, err := valueFromTag(f, defaultValue, opt) if err != nil { return nil, fmt.Errorf("error parsing default value: %v", err) } @@ -282,7 +290,7 @@ func defaultValueFromTag(f field, defaultValue string) (Value, error) { } // enumValuesFromTag parses the enum string from tag into a list of Values -func enumValuesFromTag(f field, enum string) ([]Value, error) { +func enumValuesFromTag(f field, enum string, opt *marshalState) ([]Value, error) { if enum == "" { return nil, nil } @@ -290,7 +298,7 @@ func enumValuesFromTag(f field, enum string) ([]Value, error) { enums := strings.Split(enum, ",") list := make([]Value, 0, len(enums)) for _, e := range enums { - enumVal, err := valueFromTag(f, e) + enumVal, err := valueFromTag(f, e, opt) if err != nil { return nil, fmt.Errorf("error parsing enum: %v", err) } @@ -302,11 +310,15 @@ func enumValuesFromTag(f field, enum string) ([]Value, error) { } -func valueFromTag(f field, defaultValue string) (Value, error) { +func valueFromTag(f field, defaultValue string, opt *marshalState) (Value, error) { if defaultValue == "" { return nil, nil // nolint: nilnil } + if opt != nil && opt.defaultTransformer != nil { + defaultValue = opt.defaultTransformer(defaultValue) + } + k := f.v.Kind() if k == reflect.Ptr { k = f.v.Type().Elem().Kind() @@ -372,7 +384,7 @@ func valueFromTag(f field, defaultValue string) (Value, error) { t: reflect.StructField{}, v: reflect.New(valueType), } - val, err := defaultValueFromTag(valueField, v) + val, err := defaultValueFromTag(valueField, v, opt) if err != nil { return nil, fmt.Errorf("error parsing map %q into value, %v", v, err) } @@ -406,7 +418,7 @@ func valueFromTag(f field, defaultValue string) (Value, error) { v: reflect.New(valueType), } for _, item := range list { - value, err := defaultValueFromTag(valueField, item) + value, err := defaultValueFromTag(valueField, item, opt) if err != nil { return nil, fmt.Errorf("error applying %q to list: %v", item, err) } diff --git a/unmarshal.go b/unmarshal.go index 3b4b259..d61cf74 100644 --- a/unmarshal.go +++ b/unmarshal.go @@ -129,7 +129,7 @@ func unmarshalEntries(v reflect.Value, entries []Entry, opt *marshalState) error return fmt.Errorf("missing required attribute %q", tag.name) } // apply defaults here as there's no value for this field - v, err := defaultValueFromTag(field, tag.defaultValue) + v, err := defaultValueFromTag(field, tag.defaultValue, opt) if err != nil { return err } @@ -297,7 +297,7 @@ func checkEnum(v Value, f field, enum string) error { case reflect.Map, reflect.Struct, reflect.Array, reflect.Slice: return fmt.Errorf("enum on map, struct, array and slice are not supported on field %q", f.t.Name) default: - enums, err := enumValuesFromTag(f, enum) + enums, err := enumValuesFromTag(f, enum, nil) if err != nil { return err } diff --git a/unmarshal_test.go b/unmarshal_test.go index cb22765..d8bd5d4 100644 --- a/unmarshal_test.go +++ b/unmarshal_test.go @@ -3,6 +3,7 @@ package hcl import ( "fmt" "net" + "os" "reflect" "sort" "strconv" @@ -1102,3 +1103,87 @@ func TestUnmarshallInterfaces(t *testing.T) { } runTests(t, tests) } + +func TestWithDefaultTransformer(t *testing.T) { + expandVars := func(vars map[string]string) func(string) string { + return func(defaultValue string) string { + return os.Expand(defaultValue, func(key string) string { + if val, ok := vars[key]; ok { + return val + } + return os.Getenv(key) + }) + } + } + + t.Setenv("FALLBACK_VAR", "fallback-value") + + tests := []test{ + { + name: "shell-style variable expansion", + hcl: ``, + dest: struct { + BaseURL string `hcl:"base-url,optional" default:"${BASE_URL}/api/v1"` + }{ + BaseURL: "https://api.example.com/api/v1", + }, + options: []MarshalOption{WithDefaultTransformer(expandVars(map[string]string{ + "BASE_URL": "https://api.example.com", + }))}, + }, + { + name: "custom template syntax", + hcl: ``, + dest: struct { + Message string `hcl:"message,optional" default:"Hello {{NAME}}!"` + }{ + Message: "Hello World!", + }, + options: []MarshalOption{WithDefaultTransformer(func(s string) string { + if s == "Hello {{NAME}}!" { + return "Hello World!" + } + return s + })}, + }, + { + name: "string replacement", + hcl: ``, + dest: struct { + Path string `hcl:"path,optional" default:"__HOME__/config"` + }{ + Path: "/home/user/config", + }, + options: []MarshalOption{WithDefaultTransformer(func(s string) string { + if s == "__HOME__/config" { + return "/home/user/config" + } + return s + })}, + }, + { + name: "prefix addition", + hcl: ``, + dest: struct { + Topic string `hcl:"topic,optional" default:"events"` + }{ + Topic: "prod.events", + }, + options: []MarshalOption{WithDefaultTransformer(func(s string) string { + return "prod." + s + })}, + }, + { + name: "environment variable fallback", + hcl: ``, + dest: struct { + Value string `hcl:"value,optional" default:"${FALLBACK_VAR}"` + }{ + Value: "fallback-value", + }, + options: []MarshalOption{WithDefaultTransformer(expandVars(map[string]string{}))}, + }, + } + + runTests(t, tests) +}