Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AliasMangler #95

Merged
merged 3 commits into from
Aug 30, 2024
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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,23 @@ If you wish to watch the config file and make updates to your configuration, use
}
```

### Aliased Configuration Values
Dials supports aliases for fields where you want to change the name and support
an orderly transition from an old name to a new name. Just as there is a
`dials` struct tag there is also a `dialsalias` struct tag that you can use as
an alternate name. Any other casing or transformation rules on the original
Comment on lines +280 to +283
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should mention that this functionality is provided by a mangler upfront, so anyone who doesn't use the ez package isn't confused when it doesn't work out of the box.
(I think there's a section in the README that you can link to that describes manglers -- we should add on if there isn't)

tags also applies to the aliases. Only one of the original or alias versions of
the field may be set at any time. Setting both results in an error. Aliases
are supported in the other source-specific tags like `dialsenv`, `dialsflag`,
and `dialspflag` as well by appending "alias" to the flag name. The `ez`
package also wraps the appropriate file source's decoder so config files can
also make use of aliases.

Aliases are implemented using the
[Mangler](https://github.com/vimeo/dials/blob/402b4821e3f191d96580b9933583f1651325381d/transform/transformer.go#L17-L32)
interface. The `env`, `flag`, and `pflag` sources support aliasing without any
additional options.

### Flags
When setting commandline flags using either the pflag or flag sources, additional flag-types become available for simple slices and maps.

Expand Down
21 changes: 19 additions & 2 deletions common/tags.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
// Package common provides constants that are used among different dials sources
package common

// DialsTagName is the name of the dials tag.
const DialsTagName = "dials"
const (
// DialsTagName is the name of the dials tag.
DialsTagName = "dials"

// DialsEnvTagName is the name of the dialsenv tag.
DialsEnvTagName = "dialsenv"

// DialsFlagTagName is the name of the dialsflag tag.
DialsFlagTagName = "dialsflag"

// DialsPFlagTagName is the name of the dialspflag tag.
DialsPFlagTag = "dialspflag"

// DialsFlagAliasTag is the name of the dialsflagalias tag.
DialsPFlagShortTag = "dialspflagshort"

// HelpTextTag is the name of the struct tag for flag descriptions
DialsHelpTextTag = "dialsdesc"
)
23 changes: 12 additions & 11 deletions ez/ez.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,16 +171,6 @@ func ConfigFileEnvFlagDecoderFactoryParams[T any, TP ConfigWithConfigPath[T]](ct
flagSrc = fset
}

// If file-watching is not enabled, we should shutdown the monitor
// goroutine when exiting this function.
// Usually `dials.Config` is smart enough not to start a monitor when
// there are no `Watcher` implementations in the source-list, but the
// `Blank` source uses `Watcher` for its core functionality, so we need
// to shutdown the blank source to actually clean up resources.
if !params.WatchConfigFile {
defer blank.Done(ctx)
}

dp := dials.Params[T]{
// Set the OnNewConfig callback. It'll be suppressed by the
// CallGlobalCallbacksAfterVerificationEnabled until just before we return.
Expand All @@ -199,6 +189,16 @@ func ConfigFileEnvFlagDecoderFactoryParams[T any, TP ConfigWithConfigPath[T]](ct
return nil, err
}

// If file-watching is not enabled, we should shutdown the monitor
// goroutine when exiting this function.
// Usually `dials.Config` is smart enough not to start a monitor when
// there are no `Watcher` implementations in the source-list, but the
// `Blank` source uses `Watcher` for its core functionality, so we need
// to shutdown the blank source to actually clean up resources.
if !params.WatchConfigFile {
defer blank.Done(ctx)
}

Comment on lines +192 to +201
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we split this (unrelated) bug-fix into a different commit? (it can be the same PR, but it's super-confusing to have a move that's fixing a bug in the middle of a commit introducing a new feature)

basecfg := d.View()
cfgPath, filepathSet := (TP)(basecfg).ConfigPath()
if !filepathSet {
Expand All @@ -219,7 +219,8 @@ func ConfigFileEnvFlagDecoderFactoryParams[T any, TP ConfigWithConfigPath[T]](ct
return nil, fmt.Errorf("decoderFactory provided a nil decoder for path: %s", cfgPath)
}

manglers := make([]transform.Mangler, 0, 2)
manglers := make([]transform.Mangler, 0, 3)
manglers = append(manglers, transform.NewAliasMangler(common.DialsTagName))

if params.FileFieldNameEncoder != nil {
tagDecoder := params.DialsTagNameDecoder
Expand Down
31 changes: 27 additions & 4 deletions ez/ez_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
type config struct {
// Path will contain the path to the config file and will be set by
// environment variable
Path string `dials:"CONFIGPATH"`
Path string `dials:"CONFIGPATH" dialsalias:"ALTCONFIGPATH"`
Val1 int `dials:"Val1"`
Val2 string `dials:"Val2"`
Set map[string]struct{} `dials:"Set"`
Expand All @@ -36,9 +36,32 @@ func TestYAMLConfigEnvFlagWithValidConfig(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

envErr := os.Setenv("CONFIGPATH", "../testhelper/testconfig.yaml")
require.NoError(t, envErr)
defer os.Unsetenv("CONFIGPATH")
t.Setenv("CONFIGPATH", "../testhelper/testconfig.yaml")

c := &config{}
view, dialsErr := YAMLConfigEnvFlag(ctx, c, Params[config]{})
require.NoError(t, dialsErr)

// Val1 and Val2 come from the config file and Path will be populated from env variable
expectedConfig := config{
Path: "../testhelper/testconfig.yaml",
Val1: 456,
Val2: "hello-world",
Set: map[string]struct{}{
"Keith": {},
"Gary": {},
"Jack": {},
},
}
populatedConf := view.View()
assert.EqualValues(t, expectedConfig, *populatedConf)
}

func TestYAMLConfigEnvFlagWithValidConfigAndAlias(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

t.Setenv("ALTCONFIGPATH", "../testhelper/testconfig.yaml")

c := &config{}
view, dialsErr := YAMLConfigEnvFlag(ctx, c, Params[config]{})
Expand Down
12 changes: 6 additions & 6 deletions sources/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import (
"github.com/vimeo/dials/transform"
)

const envTagName = "dialsenv"

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

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

if e.Prefix != "" {
Expand Down
8 changes: 3 additions & 5 deletions sources/flag/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,6 @@ var (
_ dials.Source = (*Set)(nil)
)

const dialsFlagTag = "dialsflag"

// NameConfig defines the parameters for separating components of a flag-name
type NameConfig struct {
// FieldNameEncodeCasing is for the field names used by the flatten mangler
Expand Down Expand Up @@ -204,7 +202,7 @@ func (s *Set) parse() error {

func (s *Set) registerFlags(tmpl reflect.Value, ptyp reflect.Type) error {
fm := transform.NewFlattenMangler(common.DialsTagName, s.NameCfg.FieldNameEncodeCasing, s.NameCfg.TagEncodeCasing)
tfmr := transform.NewTransformer(ptyp, fm)
tfmr := transform.NewTransformer(ptyp, transform.NewAliasMangler(common.DialsTagName, common.DialsFlagTagName), fm)
val, TrnslErr := tfmr.Translate()
if TrnslErr != nil {
return TrnslErr
Expand Down Expand Up @@ -241,7 +239,7 @@ func (s *Set) registerFlags(tmpl reflect.Value, ptyp reflect.Type) error {
// If the field's dialsflag tag is a hyphen (ex: `dialsflag:"-"`),
// don't register the flag. Currently nested fields with "-" tag will
// still be registered
if dft, ok := sf.Tag.Lookup(dialsFlagTag); ok && (dft == "-") {
if dft, ok := sf.Tag.Lookup(common.DialsFlagTagName); ok && (dft == "-") {
continue
}

Expand Down Expand Up @@ -506,7 +504,7 @@ func willOverflow(val, target reflect.Value) bool {
// decoded field name and converting it into kebab case
func (s *Set) mkname(sf reflect.StructField) string {
// use the name from the dialsflag tag for the flag name
if name, ok := sf.Tag.Lookup(dialsFlagTag); ok {
if name, ok := sf.Tag.Lookup(common.DialsFlagTagName); ok {
return name
}
// check if the dials tag is populated (it should be once it goes through
Expand Down
16 changes: 6 additions & 10 deletions sources/pflag/pflag.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,9 @@ var (
)

const (
dialsPFlagTag = "dialspflag"
dialsPFlagShortTag = "dialspflagshort"
// HelpTextTag is the name of the struct tag for flag descriptions
HelpTextTag = "dialsdesc"
// DefaultFlagHelpText is the default help-text for fields with an
// unset dialsdesc tag.
DefaultFlagHelpText = "unset description (`" + HelpTextTag + "` struct tag)"
DefaultFlagHelpText = "unset description (`" + common.DialsHelpTextTag + "` struct tag)"
)

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

func (s *Set) registerFlags(tmpl reflect.Value, ptyp reflect.Type) error {
fm := transform.NewFlattenMangler(common.DialsTagName, s.NameCfg.FieldNameEncodeCasing, s.NameCfg.TagEncodeCasing)
tfmr := transform.NewTransformer(ptyp, fm)
tfmr := transform.NewTransformer(ptyp, transform.NewAliasMangler(common.DialsTagName, common.DialsPFlagTag, common.DialsPFlagShortTag), fm)
val, TrnslErr := tfmr.Translate()
if TrnslErr != nil {
return TrnslErr
Expand All @@ -235,7 +231,7 @@ func (s *Set) registerFlags(tmpl reflect.Value, ptyp reflect.Type) error {
for i := 0; i < t.NumField(); i++ {
sf := t.Field(i)
help := DefaultFlagHelpText
if x, ok := sf.Tag.Lookup(HelpTextTag); ok {
if x, ok := sf.Tag.Lookup(common.DialsHelpTextTag); ok {
help = x
}

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

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

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

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