Skip to content

Commit 28c7cb6

Browse files
authored
feat: add WithVariableExpansion to expand in default struct tags (#46)
* feat: add WithVariableExpansion to expand in default struct tags * refactor: Change the implementation to generic WithDefaultTransformer
1 parent 6fa89a8 commit 28c7cb6

File tree

3 files changed

+108
-11
lines changed

3 files changed

+108
-11
lines changed

marshal.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type marshalState struct {
2626
schemaComments bool
2727
seenStructs map[reflect.Type]bool
2828
allowExtra bool
29+
defaultTransformer func(string) string
2930
}
3031

3132
// Create a shallow clone with schema overridden.
@@ -84,6 +85,13 @@ func WithSchemaComments(v bool) MarshalOption {
8485
}
8586
}
8687

88+
// WithDefaultTransformer allows custom processing of default values in struct tags.
89+
func WithDefaultTransformer(transformer func(string) string) MarshalOption {
90+
return func(options *marshalState) {
91+
options.defaultTransformer = transformer
92+
}
93+
}
94+
8795
func asSchema() MarshalOption {
8896
return func(options *marshalState) {
8997
options.schema = true
@@ -264,33 +272,33 @@ func fieldToAttr(field field, tag tag, opt *marshalState) (*Attribute, error) {
264272
if err != nil {
265273
return nil, err
266274
}
267-
attr.Default, err = defaultValueFromTag(field, tag.defaultValue)
275+
attr.Default, err = defaultValueFromTag(field, tag.defaultValue, opt)
268276
if err != nil {
269277
return nil, err
270278
}
271279
attr.Optional = (tag.optional || attr.Default != nil) && opt.schema
272-
attr.Enum, err = enumValuesFromTag(field, tag.enum)
280+
attr.Enum, err = enumValuesFromTag(field, tag.enum, opt)
273281
return attr, err
274282
}
275283

276-
func defaultValueFromTag(f field, defaultValue string) (Value, error) {
277-
v, err := valueFromTag(f, defaultValue)
284+
func defaultValueFromTag(f field, defaultValue string, opt *marshalState) (Value, error) {
285+
v, err := valueFromTag(f, defaultValue, opt)
278286
if err != nil {
279287
return nil, fmt.Errorf("error parsing default value: %v", err)
280288
}
281289
return v, nil
282290
}
283291

284292
// enumValuesFromTag parses the enum string from tag into a list of Values
285-
func enumValuesFromTag(f field, enum string) ([]Value, error) {
293+
func enumValuesFromTag(f field, enum string, opt *marshalState) ([]Value, error) {
286294
if enum == "" {
287295
return nil, nil
288296
}
289297

290298
enums := strings.Split(enum, ",")
291299
list := make([]Value, 0, len(enums))
292300
for _, e := range enums {
293-
enumVal, err := valueFromTag(f, e)
301+
enumVal, err := valueFromTag(f, e, opt)
294302
if err != nil {
295303
return nil, fmt.Errorf("error parsing enum: %v", err)
296304
}
@@ -302,11 +310,15 @@ func enumValuesFromTag(f field, enum string) ([]Value, error) {
302310

303311
}
304312

305-
func valueFromTag(f field, defaultValue string) (Value, error) {
313+
func valueFromTag(f field, defaultValue string, opt *marshalState) (Value, error) {
306314
if defaultValue == "" {
307315
return nil, nil // nolint: nilnil
308316
}
309317

318+
if opt != nil && opt.defaultTransformer != nil {
319+
defaultValue = opt.defaultTransformer(defaultValue)
320+
}
321+
310322
k := f.v.Kind()
311323
if k == reflect.Ptr {
312324
k = f.v.Type().Elem().Kind()
@@ -372,7 +384,7 @@ func valueFromTag(f field, defaultValue string) (Value, error) {
372384
t: reflect.StructField{},
373385
v: reflect.New(valueType),
374386
}
375-
val, err := defaultValueFromTag(valueField, v)
387+
val, err := defaultValueFromTag(valueField, v, opt)
376388
if err != nil {
377389
return nil, fmt.Errorf("error parsing map %q into value, %v", v, err)
378390
}
@@ -406,7 +418,7 @@ func valueFromTag(f field, defaultValue string) (Value, error) {
406418
v: reflect.New(valueType),
407419
}
408420
for _, item := range list {
409-
value, err := defaultValueFromTag(valueField, item)
421+
value, err := defaultValueFromTag(valueField, item, opt)
410422
if err != nil {
411423
return nil, fmt.Errorf("error applying %q to list: %v", item, err)
412424
}

unmarshal.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ func unmarshalEntries(v reflect.Value, entries []Entry, opt *marshalState) error
129129
return fmt.Errorf("missing required attribute %q", tag.name)
130130
}
131131
// apply defaults here as there's no value for this field
132-
v, err := defaultValueFromTag(field, tag.defaultValue)
132+
v, err := defaultValueFromTag(field, tag.defaultValue, opt)
133133
if err != nil {
134134
return err
135135
}
@@ -297,7 +297,7 @@ func checkEnum(v Value, f field, enum string) error {
297297
case reflect.Map, reflect.Struct, reflect.Array, reflect.Slice:
298298
return fmt.Errorf("enum on map, struct, array and slice are not supported on field %q", f.t.Name)
299299
default:
300-
enums, err := enumValuesFromTag(f, enum)
300+
enums, err := enumValuesFromTag(f, enum, nil)
301301
if err != nil {
302302
return err
303303
}

unmarshal_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package hcl
33
import (
44
"fmt"
55
"net"
6+
"os"
67
"reflect"
78
"sort"
89
"strconv"
@@ -1102,3 +1103,87 @@ func TestUnmarshallInterfaces(t *testing.T) {
11021103
}
11031104
runTests(t, tests)
11041105
}
1106+
1107+
func TestWithDefaultTransformer(t *testing.T) {
1108+
expandVars := func(vars map[string]string) func(string) string {
1109+
return func(defaultValue string) string {
1110+
return os.Expand(defaultValue, func(key string) string {
1111+
if val, ok := vars[key]; ok {
1112+
return val
1113+
}
1114+
return os.Getenv(key)
1115+
})
1116+
}
1117+
}
1118+
1119+
t.Setenv("FALLBACK_VAR", "fallback-value")
1120+
1121+
tests := []test{
1122+
{
1123+
name: "shell-style variable expansion",
1124+
hcl: ``,
1125+
dest: struct {
1126+
BaseURL string `hcl:"base-url,optional" default:"${BASE_URL}/api/v1"`
1127+
}{
1128+
BaseURL: "https://api.example.com/api/v1",
1129+
},
1130+
options: []MarshalOption{WithDefaultTransformer(expandVars(map[string]string{
1131+
"BASE_URL": "https://api.example.com",
1132+
}))},
1133+
},
1134+
{
1135+
name: "custom template syntax",
1136+
hcl: ``,
1137+
dest: struct {
1138+
Message string `hcl:"message,optional" default:"Hello {{NAME}}!"`
1139+
}{
1140+
Message: "Hello World!",
1141+
},
1142+
options: []MarshalOption{WithDefaultTransformer(func(s string) string {
1143+
if s == "Hello {{NAME}}!" {
1144+
return "Hello World!"
1145+
}
1146+
return s
1147+
})},
1148+
},
1149+
{
1150+
name: "string replacement",
1151+
hcl: ``,
1152+
dest: struct {
1153+
Path string `hcl:"path,optional" default:"__HOME__/config"`
1154+
}{
1155+
Path: "/home/user/config",
1156+
},
1157+
options: []MarshalOption{WithDefaultTransformer(func(s string) string {
1158+
if s == "__HOME__/config" {
1159+
return "/home/user/config"
1160+
}
1161+
return s
1162+
})},
1163+
},
1164+
{
1165+
name: "prefix addition",
1166+
hcl: ``,
1167+
dest: struct {
1168+
Topic string `hcl:"topic,optional" default:"events"`
1169+
}{
1170+
Topic: "prod.events",
1171+
},
1172+
options: []MarshalOption{WithDefaultTransformer(func(s string) string {
1173+
return "prod." + s
1174+
})},
1175+
},
1176+
{
1177+
name: "environment variable fallback",
1178+
hcl: ``,
1179+
dest: struct {
1180+
Value string `hcl:"value,optional" default:"${FALLBACK_VAR}"`
1181+
}{
1182+
Value: "fallback-value",
1183+
},
1184+
options: []MarshalOption{WithDefaultTransformer(expandVars(map[string]string{}))},
1185+
},
1186+
}
1187+
1188+
runTests(t, tests)
1189+
}

0 commit comments

Comments
 (0)