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: 21 additions & 9 deletions marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -264,33 +272,33 @@ 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)
}
return v, nil
}

// 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
}

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)
}
Expand All @@ -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()
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down
4 changes: 2 additions & 2 deletions unmarshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
85 changes: 85 additions & 0 deletions unmarshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package hcl
import (
"fmt"
"net"
"os"
"reflect"
"sort"
"strconv"
Expand Down Expand Up @@ -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)
}