Skip to content

Commit 5d5cfaa

Browse files
AliasMangler
Add a new mangler that allows us to have aliases for dials tags to facilitate migrating from one name to another seamlessly. If both the old name and the new name are set, an error will be returned because the expectation is that you're using one or the other exclusively. Also, fix a bug in `ez` where an error thrown by a Source may cause the process to hang. And, lastly, specifically exclude TextUnmarshaller implementing types from mangling.
1 parent 402b482 commit 5d5cfaa

File tree

9 files changed

+384
-35
lines changed

9 files changed

+384
-35
lines changed

common/tags.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
// Package common provides constants that are used among different dials sources
22
package common
33

4-
// DialsTagName is the name of the dials tag.
5-
const DialsTagName = "dials"
4+
const (
5+
// DialsTagName is the name of the dials tag.
6+
DialsTagName = "dials"
7+
8+
// DialsEnvTagName is the name of the dialsenv tag.
9+
DialsEnvTagName = "dialsenv"
10+
11+
// DialsFlagTagName is the name of the dialsflag tag.
12+
DialsFlagTagName = "dialsflag"
13+
14+
// DialsPFlagTagName is the name of the dialspflag tag.
15+
DialsPFlagTag = "dialspflag"
16+
17+
// DialsFlagAliasTag is the name of the dialsflagalias tag.
18+
DialsPFlagShortTag = "dialspflagshort"
19+
20+
// HelpTextTag is the name of the struct tag for flag descriptions
21+
DialsHelpTextTag = "dialsdesc"
22+
)

ez/ez.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -171,16 +171,6 @@ func ConfigFileEnvFlagDecoderFactoryParams[T any, TP ConfigWithConfigPath[T]](ct
171171
flagSrc = fset
172172
}
173173

174-
// If file-watching is not enabled, we should shutdown the monitor
175-
// goroutine when exiting this function.
176-
// Usually `dials.Config` is smart enough not to start a monitor when
177-
// there are no `Watcher` implementations in the source-list, but the
178-
// `Blank` source uses `Watcher` for its core functionality, so we need
179-
// to shutdown the blank source to actually clean up resources.
180-
if !params.WatchConfigFile {
181-
defer blank.Done(ctx)
182-
}
183-
184174
dp := dials.Params[T]{
185175
// Set the OnNewConfig callback. It'll be suppressed by the
186176
// CallGlobalCallbacksAfterVerificationEnabled until just before we return.
@@ -199,6 +189,16 @@ func ConfigFileEnvFlagDecoderFactoryParams[T any, TP ConfigWithConfigPath[T]](ct
199189
return nil, err
200190
}
201191

192+
// If file-watching is not enabled, we should shutdown the monitor
193+
// goroutine when exiting this function.
194+
// Usually `dials.Config` is smart enough not to start a monitor when
195+
// there are no `Watcher` implementations in the source-list, but the
196+
// `Blank` source uses `Watcher` for its core functionality, so we need
197+
// to shutdown the blank source to actually clean up resources.
198+
if !params.WatchConfigFile {
199+
defer blank.Done(ctx)
200+
}
201+
202202
basecfg := d.View()
203203
cfgPath, filepathSet := (TP)(basecfg).ConfigPath()
204204
if !filepathSet {
@@ -219,7 +219,8 @@ func ConfigFileEnvFlagDecoderFactoryParams[T any, TP ConfigWithConfigPath[T]](ct
219219
return nil, fmt.Errorf("decoderFactory provided a nil decoder for path: %s", cfgPath)
220220
}
221221

222-
manglers := make([]transform.Mangler, 0, 2)
222+
manglers := make([]transform.Mangler, 0, 3)
223+
manglers = append(manglers, &transform.AliasMangler{})
223224

224225
if params.FileFieldNameEncoder != nil {
225226
tagDecoder := params.DialsTagNameDecoder

ez/ez_test.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818
type config struct {
1919
// Path will contain the path to the config file and will be set by
2020
// environment variable
21-
Path string `dials:"CONFIGPATH"`
21+
Path string `dials:"CONFIGPATH" dialsalias:"ALTCONFIGPATH"`
2222
Val1 int `dials:"Val1"`
2323
Val2 string `dials:"Val2"`
2424
Set map[string]struct{} `dials:"Set"`
@@ -59,6 +59,33 @@ func TestYAMLConfigEnvFlagWithValidConfig(t *testing.T) {
5959
assert.EqualValues(t, expectedConfig, *populatedConf)
6060
}
6161

62+
func TestYAMLConfigEnvFlagWithValidConfigAndAlias(t *testing.T) {
63+
ctx, cancel := context.WithCancel(context.Background())
64+
defer cancel()
65+
66+
envErr := os.Setenv("ALTCONFIGPATH", "../testhelper/testconfig.yaml")
67+
require.NoError(t, envErr)
68+
defer os.Unsetenv("ALTCONFIGPATH")
69+
70+
c := &config{}
71+
view, dialsErr := YAMLConfigEnvFlag(ctx, c, Params[config]{})
72+
require.NoError(t, dialsErr)
73+
74+
// Val1 and Val2 come from the config file and Path will be populated from env variable
75+
expectedConfig := config{
76+
Path: "../testhelper/testconfig.yaml",
77+
Val1: 456,
78+
Val2: "hello-world",
79+
Set: map[string]struct{}{
80+
"Keith": {},
81+
"Gary": {},
82+
"Jack": {},
83+
},
84+
}
85+
populatedConf := view.View()
86+
assert.EqualValues(t, expectedConfig, *populatedConf)
87+
}
88+
6289
type beatlesConfig struct {
6390
YAMLPath string
6491
BeatlesMembers map[string]string

sources/env/env.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ import (
1313
"github.com/vimeo/dials/transform"
1414
)
1515

16-
const envTagName = "dialsenv"
17-
1816
// Source implements the dials.Source interface to set configuration from
1917
// environment variables.
2018
type Source struct {
@@ -36,10 +34,12 @@ func (e *Source) Value(_ context.Context, t *dials.Type) (reflect.Value, error)
3634
// reformat the tags so they are SCREAMING_SNAKE_CASE
3735
reformatTagMangler := tagformat.NewTagReformattingMangler(common.DialsTagName, caseconversion.DecodeGoTags, caseconversion.EncodeUpperSnakeCase)
3836
// copy tags from "dials" to "dialsenv" tag
39-
tagCopyingMangler := &tagformat.TagCopyingMangler{SrcTag: common.DialsTagName, NewTag: envTagName}
37+
tagCopyingMangler := &tagformat.TagCopyingMangler{SrcTag: common.DialsTagName, NewTag: common.DialsEnvTagName}
4038
// convert all the fields in the flattened struct to string type so the environment variables can be set
4139
stringCastingMangler := &transform.StringCastingMangler{}
42-
tfmr := transform.NewTransformer(t.Type(), flattenMangler, reformatTagMangler, tagCopyingMangler, stringCastingMangler)
40+
// allow aliasing to migrate from one name to another
41+
aliasMangler := &transform.AliasMangler{}
42+
tfmr := transform.NewTransformer(t.Type(), aliasMangler, flattenMangler, reformatTagMangler, tagCopyingMangler, stringCastingMangler)
4343

4444
val, err := tfmr.Translate()
4545
if err != nil {
@@ -49,11 +49,11 @@ func (e *Source) Value(_ context.Context, t *dials.Type) (reflect.Value, error)
4949
valType := val.Type()
5050
for i := 0; i < val.NumField(); i++ {
5151
sf := valType.Field(i)
52-
envTagVal := sf.Tag.Get(envTagName)
52+
envTagVal := sf.Tag.Get(common.DialsEnvTagName)
5353
if envTagVal == "" {
5454
// dialsenv tag should be populated because dials tag is populated
5555
// after flatten mangler and we copy from dials to dialsenv tag
56-
panic(fmt.Errorf("empty %s tag for field name %s", envTagName, sf.Name))
56+
panic(fmt.Errorf("empty %s tag for field name %s", common.DialsEnvTagName, sf.Name))
5757
}
5858

5959
if e.Prefix != "" {

sources/flag/flag.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,6 @@ var (
6767
_ dials.Source = (*Set)(nil)
6868
)
6969

70-
const dialsFlagTag = "dialsflag"
71-
7270
// NameConfig defines the parameters for separating components of a flag-name
7371
type NameConfig struct {
7472
// FieldNameEncodeCasing is for the field names used by the flatten mangler
@@ -204,7 +202,7 @@ func (s *Set) parse() error {
204202

205203
func (s *Set) registerFlags(tmpl reflect.Value, ptyp reflect.Type) error {
206204
fm := transform.NewFlattenMangler(common.DialsTagName, s.NameCfg.FieldNameEncodeCasing, s.NameCfg.TagEncodeCasing)
207-
tfmr := transform.NewTransformer(ptyp, fm)
205+
tfmr := transform.NewTransformer(ptyp, &transform.AliasMangler{}, fm)
208206
val, TrnslErr := tfmr.Translate()
209207
if TrnslErr != nil {
210208
return TrnslErr
@@ -241,7 +239,7 @@ func (s *Set) registerFlags(tmpl reflect.Value, ptyp reflect.Type) error {
241239
// If the field's dialsflag tag is a hyphen (ex: `dialsflag:"-"`),
242240
// don't register the flag. Currently nested fields with "-" tag will
243241
// still be registered
244-
if dft, ok := sf.Tag.Lookup(dialsFlagTag); ok && (dft == "-") {
242+
if dft, ok := sf.Tag.Lookup(common.DialsFlagTagName); ok && (dft == "-") {
245243
continue
246244
}
247245

@@ -506,7 +504,7 @@ func willOverflow(val, target reflect.Value) bool {
506504
// decoded field name and converting it into kebab case
507505
func (s *Set) mkname(sf reflect.StructField) string {
508506
// use the name from the dialsflag tag for the flag name
509-
if name, ok := sf.Tag.Lookup(dialsFlagTag); ok {
507+
if name, ok := sf.Tag.Lookup(common.DialsFlagTagName); ok {
510508
return name
511509
}
512510
// check if the dials tag is populated (it should be once it goes through

sources/pflag/pflag.go

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,9 @@ var (
7070
)
7171

7272
const (
73-
dialsPFlagTag = "dialspflag"
74-
dialsPFlagShortTag = "dialspflagshort"
75-
// HelpTextTag is the name of the struct tag for flag descriptions
76-
HelpTextTag = "dialsdesc"
7773
// DefaultFlagHelpText is the default help-text for fields with an
7874
// unset dialsdesc tag.
79-
DefaultFlagHelpText = "unset description (`" + HelpTextTag + "` struct tag)"
75+
DefaultFlagHelpText = "unset description (`" + common.DialsHelpTextTag + "` struct tag)"
8076
)
8177

8278
// NameConfig defines the parameters for separating components of a flag-name
@@ -214,7 +210,7 @@ func (s *Set) parse() error {
214210

215211
func (s *Set) registerFlags(tmpl reflect.Value, ptyp reflect.Type) error {
216212
fm := transform.NewFlattenMangler(common.DialsTagName, s.NameCfg.FieldNameEncodeCasing, s.NameCfg.TagEncodeCasing)
217-
tfmr := transform.NewTransformer(ptyp, fm)
213+
tfmr := transform.NewTransformer(ptyp, &transform.AliasMangler{}, fm)
218214
val, TrnslErr := tfmr.Translate()
219215
if TrnslErr != nil {
220216
return TrnslErr
@@ -235,7 +231,7 @@ func (s *Set) registerFlags(tmpl reflect.Value, ptyp reflect.Type) error {
235231
for i := 0; i < t.NumField(); i++ {
236232
sf := t.Field(i)
237233
help := DefaultFlagHelpText
238-
if x, ok := sf.Tag.Lookup(HelpTextTag); ok {
234+
if x, ok := sf.Tag.Lookup(common.DialsHelpTextTag); ok {
239235
help = x
240236
}
241237

@@ -251,7 +247,7 @@ func (s *Set) registerFlags(tmpl reflect.Value, ptyp reflect.Type) error {
251247
// If the field's dialspflag tag is a hyphen (ex: `dialspflag:"-"`),
252248
// don't register the flag. Currently nested fields with "-" tag will
253249
// still be registered
254-
if dpt, ok := sf.Tag.Lookup(dialsPFlagTag); ok && (dpt == "-") {
250+
if dpt, ok := sf.Tag.Lookup(common.DialsPFlagTag); ok && (dpt == "-") {
255251
continue
256252
}
257253

@@ -267,7 +263,7 @@ func (s *Set) registerFlags(tmpl reflect.Value, ptyp reflect.Type) error {
267263

268264
// get the concrete value of the field from the template
269265
fieldVal := transform.GetField(sf, tmpl)
270-
shorthand, _ := sf.Tag.Lookup(dialsPFlagShortTag)
266+
shorthand, _ := sf.Tag.Lookup(common.DialsPFlagShortTag)
271267
var f interface{}
272268

273269
switch {
@@ -516,7 +512,7 @@ func stripTypePtr(t reflect.Type) reflect.Type {
516512
// decoded field name and converting it into kebab case
517513
func (s *Set) mkname(sf reflect.StructField) string {
518514
// use the name from the dialspflag tag for the flag name
519-
if name, ok := sf.Tag.Lookup(dialsPFlagTag); ok {
515+
if name, ok := sf.Tag.Lookup(common.DialsPFlagTag); ok {
520516
return name
521517
}
522518
// check if the dials tag is populated (it should be once it goes through

transform/alias_mangler.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package transform
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
7+
"github.com/fatih/structtag"
8+
"github.com/vimeo/dials/common"
9+
)
10+
11+
const (
12+
dialsAliasTagSuffix = "alias"
13+
aliasFieldSuffix = "_alias9wr876rw3" // a random string to append to the alias field to avoid collisions
14+
)
15+
16+
// the list of tags that we should search for aliases
17+
var aliasSourceTags = []string{
18+
common.DialsTagName,
19+
common.DialsFlagTagName,
20+
common.DialsEnvTagName,
21+
common.DialsFlagTagName,
22+
common.DialsPFlagTag,
23+
common.DialsPFlagShortTag,
24+
}
25+
26+
// AliasMangler manages aliases for dials, dialsenv, dialsflag, and dialspflag
27+
// struct tags to make it possible to migrate from one name to another
28+
// conveniently.
29+
type AliasMangler struct{}
30+
31+
// Mangle implements the Mangler interface. If an alias tag is defined, the
32+
// struct field will be copied with the non-aliased tag set to the alias's
33+
// value.
34+
func (a AliasMangler) Mangle(sf reflect.StructField) ([]reflect.StructField, error) {
35+
originalVals := map[string]string{}
36+
aliasVals := map[string]string{}
37+
38+
sfTags, parseErr := structtag.Parse(string(sf.Tag))
39+
if parseErr != nil {
40+
return nil, fmt.Errorf("error parsing source tags %w", parseErr)
41+
}
42+
43+
anyAliasFound := false
44+
for _, tag := range aliasSourceTags {
45+
if originalVal, getErr := sfTags.Get(tag); getErr == nil {
46+
originalVals[tag] = originalVal.Name
47+
}
48+
49+
if aliasVal, getErr := sfTags.Get(tag + dialsAliasTagSuffix); getErr == nil {
50+
aliasVals[tag] = aliasVal.Name
51+
anyAliasFound = true
52+
53+
// remove the alias tag from the definition
54+
sfTags.Delete(tag + dialsAliasTagSuffix)
55+
}
56+
}
57+
58+
if !anyAliasFound {
59+
// we didn't find any aliases so just get out early
60+
return []reflect.StructField{sf}, nil
61+
}
62+
63+
aliasField := sf
64+
aliasField.Name += aliasFieldSuffix
65+
66+
// now that we've copied it, reset the struct tags on the source field to
67+
// not include the alias tags
68+
sf.Tag = reflect.StructTag(sfTags.String())
69+
70+
tags, parseErr := structtag.Parse(string(aliasField.Tag))
71+
if parseErr != nil {
72+
return nil, fmt.Errorf("error parsing struct tags: %w", parseErr)
73+
}
74+
75+
for _, tag := range aliasSourceTags {
76+
// remove the alias tag so it's not left on the copied StructField
77+
tags.Delete(tag + dialsAliasTagSuffix)
78+
79+
if aliasVals[tag] == "" {
80+
// if the particular flag isn't set at all just move on...
81+
continue
82+
}
83+
84+
newDialsTag := &structtag.Tag{
85+
Key: tag,
86+
Name: aliasVals[tag],
87+
}
88+
89+
if setErr := tags.Set(newDialsTag); setErr != nil {
90+
return nil, fmt.Errorf("error setting new value for dials tag: %w", setErr)
91+
}
92+
93+
// update dialsdesc if there is one
94+
if desc, getErr := tags.Get("dialsdesc"); getErr == nil {
95+
newDesc := &structtag.Tag{
96+
Key: "dialsdesc",
97+
Name: desc.Name + " (alias of `" + originalVals[tag] + "`)",
98+
}
99+
if setErr := tags.Set(newDesc); setErr != nil {
100+
return nil, fmt.Errorf("error setting amended dialsdesc for tag %q: %w", tag, setErr)
101+
}
102+
}
103+
}
104+
105+
// set the new flags on the alias field
106+
aliasField.Tag = reflect.StructTag(tags.String())
107+
108+
return []reflect.StructField{sf, aliasField}, nil
109+
}
110+
111+
// Unmangle implements the Mangler interface and unwinds the alias copying
112+
// operation. Note that if both the source and alias are both set in the
113+
// configuration, an error will be returned.
114+
func (a AliasMangler) Unmangle(sf reflect.StructField, fvs []FieldValueTuple) (reflect.Value, error) {
115+
switch len(fvs) {
116+
case 1:
117+
// if there's only one tuple that means there was no alias, so just
118+
// return...
119+
return fvs[0].Value, nil
120+
case 2:
121+
// two means there's an alias so we should continue on...
122+
default:
123+
return reflect.Value{}, fmt.Errorf("expected 1 or 2 tuples, got %d", len(fvs))
124+
}
125+
126+
if !fvs[0].Value.IsNil() && !fvs[1].Value.IsNil() {
127+
return reflect.Value{}, fmt.Errorf("both alias and original set for field %q", sf.Name)
128+
}
129+
130+
// return the first one that isn't nil
131+
for _, fv := range fvs {
132+
if !fv.Value.IsNil() {
133+
return fv.Value, nil
134+
}
135+
}
136+
137+
// if we made it this far, they were both nil, which is fine -- just return
138+
// one of them.
139+
return fvs[0].Value, nil
140+
}
141+
142+
// ShouldRecurse is called after Mangle for each field so nested struct
143+
// fields get iterated over after any transformation done by Mangle().
144+
func (a AliasMangler) ShouldRecurse(_ reflect.StructField) bool {
145+
return true
146+
}

0 commit comments

Comments
 (0)