-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
402b482
commit 047e3d9
Showing
5 changed files
with
308 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
package transform | ||
|
||
import ( | ||
"fmt" | ||
"reflect" | ||
|
||
"github.com/fatih/structtag" | ||
) | ||
|
||
const ( | ||
dialsAliasTagSuffix = "alias" | ||
aliasFieldSuffix = "_alias9wr876rw3" // a random string to append to the alias field to avoid collisions | ||
) | ||
|
||
// AliasMangler manages aliases for dials, dialsenv, dialsflag, and dialspflag | ||
// struct tags to make it possible to migrate from one name to another | ||
// conveniently. | ||
type AliasMangler struct{} | ||
|
||
// Mangle implements the Mangler interface. If an alias tag is defined, the | ||
// struct field will be copied with the non-aliased tag set to the alias's | ||
// value. | ||
func (a AliasMangler) Mangle(sf reflect.StructField) ([]reflect.StructField, error) { | ||
sourceTags := []string{"dials", "dialsenv", "dialsflag", "dialspflag"} | ||
originalVals := map[string]string{} | ||
aliasVals := map[string]string{} | ||
|
||
sfTags, parseErr := structtag.Parse(string(sf.Tag)) | ||
if parseErr != nil { | ||
return nil, fmt.Errorf("error parsing source tags %w", parseErr) | ||
} | ||
|
||
anyAliasFound := false | ||
for _, tag := range sourceTags { | ||
if originalVal, getErr := sfTags.Get(tag); getErr == nil { | ||
originalVals[tag] = originalVal.Name | ||
} | ||
|
||
if aliasVal, getErr := sfTags.Get(tag + dialsAliasTagSuffix); getErr == nil { | ||
aliasVals[tag] = aliasVal.Name | ||
anyAliasFound = true | ||
|
||
// remove the alias tag from the definition | ||
sfTags.Delete(tag + dialsAliasTagSuffix) | ||
} | ||
} | ||
|
||
if !anyAliasFound { | ||
// we didn't find any aliases so just get out early | ||
return []reflect.StructField{sf}, nil | ||
} | ||
|
||
aliasField := sf | ||
aliasField.Name += aliasFieldSuffix | ||
|
||
// now that we've copied it, reset the struct tags on the source field to | ||
// not include the alias tags | ||
sf.Tag = reflect.StructTag(sfTags.String()) | ||
|
||
tags, parseErr := structtag.Parse(string(aliasField.Tag)) | ||
if parseErr != nil { | ||
return nil, fmt.Errorf("error parsing struct tags: %w", parseErr) | ||
} | ||
|
||
for _, tag := range sourceTags { | ||
// remove the alias tag so it's not left on the copied StructField | ||
tags.Delete(tag + dialsAliasTagSuffix) | ||
|
||
if aliasVals[tag] == "" { | ||
// if the particular flag isn't set at all just move on... | ||
continue | ||
} | ||
|
||
newDialsTag := &structtag.Tag{ | ||
Key: tag, | ||
Name: aliasVals[tag], | ||
} | ||
|
||
if setErr := tags.Set(newDialsTag); setErr != nil { | ||
return nil, fmt.Errorf("error setting new value for dials tag: %w", setErr) | ||
} | ||
|
||
// update dialsdesc if there is one | ||
if desc, getErr := tags.Get("dialsdesc"); getErr == nil { | ||
newDesc := &structtag.Tag{ | ||
Key: "dialsdesc", | ||
Name: desc.Name + " (alias of `" + originalVals[tag] + "`)", | ||
} | ||
if setErr := tags.Set(newDesc); setErr != nil { | ||
return nil, fmt.Errorf("error setting amended dialsdesc for tag %q: %w", tag, setErr) | ||
} | ||
} | ||
} | ||
|
||
// set the new flags on the alias field | ||
aliasField.Tag = reflect.StructTag(tags.String()) | ||
|
||
return []reflect.StructField{sf, aliasField}, nil | ||
} | ||
|
||
// Unmangle implements the Mangler interface and unwinds the alias copying | ||
// operation. Note that if both the source and alias are both set in the | ||
// configuration, an error will be returned. | ||
func (a AliasMangler) Unmangle(sf reflect.StructField, fvs []FieldValueTuple) (reflect.Value, error) { | ||
if len(fvs) != 2 { | ||
return reflect.Value{}, fmt.Errorf("expected 2 tuples, got %d", len(fvs)) | ||
} | ||
|
||
if !fvs[0].Value.IsNil() && !fvs[1].Value.IsNil() { | ||
return reflect.Value{}, fmt.Errorf("both alias and original set for field %q", sf.Name) | ||
} | ||
|
||
for _, fv := range fvs { | ||
if !fv.Value.IsNil() { | ||
return fv.Value, nil | ||
} | ||
} | ||
|
||
// if we made it this far, they were both nil, which is fine -- just return | ||
// one of them. | ||
return fvs[0].Value, nil | ||
} | ||
|
||
// ShouldRecurse is called after Mangle for each field so nested struct | ||
// fields get iterated over after any transformation done by Mangle(). | ||
func (a AliasMangler) ShouldRecurse(_ reflect.StructField) bool { | ||
return true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
package transform | ||
|
||
import ( | ||
"reflect" | ||
"testing" | ||
|
||
"github.com/fatih/structtag" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestAliasManglerMangle(t *testing.T) { | ||
|
||
for testName, tbl := range map[string]struct { | ||
tag string | ||
expectedOrig map[string]string | ||
expectedAlias map[string]string | ||
}{ | ||
"dialsOnly": { | ||
tag: `dials:"name" dialsalias:"anothername"`, | ||
expectedOrig: map[string]string{"dials": "name"}, | ||
expectedAlias: map[string]string{"dials": "anothername"}, | ||
}, | ||
"withDialsDesc": { | ||
tag: `dials:"name" dialsalias:"anothername" dialsdesc:"the name for this"`, | ||
expectedOrig: map[string]string{"dials": "name", "dialsdesc": "the name for this"}, | ||
expectedAlias: map[string]string{"dials": "anothername", "dialsdesc": "the name for this (alias of `name`)"}, | ||
}, | ||
"dialsDialsEnvFlagPFlag": { | ||
tag: `dials:"name" dialsflag:"flagname" dialspflag:"pflagname" dialsenv:"envname" dialsalias:"anothername" dialsflagalias:"flagalias" dialspflagalias:"pflagalias" dialsenvalias:"envalias"`, | ||
expectedOrig: map[string]string{"dials": "name", "dialsflag": "flagname", "dialspflag": "pflagname", "dialsenv": "envname"}, | ||
expectedAlias: map[string]string{"dials": "anothername", "dialsflag": "flagalias", "dialspflag": "pflagalias", "dialsenv": "envalias"}, | ||
}, | ||
} { | ||
t.Run(testName, func(t *testing.T) { | ||
sf := reflect.StructField{ | ||
Name: "Foo", | ||
Type: reflect.TypeOf(""), | ||
Tag: reflect.StructTag(tbl.tag), | ||
} | ||
|
||
aliasMangler := AliasMangler{} | ||
fields, mangleErr := aliasMangler.Mangle(sf) | ||
require.NoError(t, mangleErr) | ||
|
||
require.Len(t, fields, 2) | ||
|
||
originalTags, parseErr := structtag.Parse(string(fields[0].Tag)) | ||
require.NoError(t, parseErr) | ||
|
||
for k, v := range tbl.expectedOrig { | ||
val, err := originalTags.Get(k) | ||
require.NoError(t, err) | ||
assert.Equal(t, v, val.Name) | ||
} | ||
assert.Equal(t, len(tbl.expectedOrig), originalTags.Len()) | ||
|
||
aliasTags, parseErr := structtag.Parse(string(fields[1].Tag)) | ||
require.NoError(t, parseErr) | ||
|
||
for k, v := range tbl.expectedAlias { | ||
val, err := aliasTags.Get(k) | ||
require.NoError(t, err) | ||
assert.Equal(t, v, val.Name) | ||
} | ||
assert.Equal(t, len(tbl.expectedAlias), aliasTags.Len()) | ||
|
||
// for _, f := range fields { | ||
// t.Logf("name: %s tag: %s", f.Name, string(f.Tag)) | ||
// } | ||
}) | ||
} | ||
} | ||
|
||
func TestAliasManglerUnmangle(t *testing.T) { | ||
sf := reflect.StructField{ | ||
Name: "Foo", | ||
Type: reflect.TypeOf(""), | ||
} | ||
|
||
num := 42 | ||
var nilInt *int | ||
|
||
aliasMangler := &AliasMangler{} | ||
|
||
originalSet := []FieldValueTuple{ | ||
{ | ||
Field: sf, | ||
Value: reflect.ValueOf(&num), | ||
}, | ||
{ | ||
Field: sf, | ||
Value: reflect.ValueOf(nilInt), | ||
}, | ||
} | ||
|
||
val, err := aliasMangler.Unmangle(sf, originalSet) | ||
require.NoError(t, err) | ||
|
||
assert.Equal(t, 42, val.Elem().Interface()) | ||
|
||
aliasSet := []FieldValueTuple{ | ||
{ | ||
Field: sf, | ||
Value: reflect.ValueOf(nilInt), | ||
}, | ||
{ | ||
Field: sf, | ||
Value: reflect.ValueOf(&num), | ||
}, | ||
} | ||
|
||
val, err = aliasMangler.Unmangle(sf, aliasSet) | ||
require.NoError(t, err) | ||
|
||
assert.Equal(t, 42, val.Elem().Interface()) | ||
|
||
bothSet := []FieldValueTuple{ | ||
{ | ||
Field: sf, | ||
Value: reflect.ValueOf(&num), | ||
}, | ||
{ | ||
Field: sf, | ||
Value: reflect.ValueOf(&num), | ||
}, | ||
} | ||
|
||
_, err = aliasMangler.Unmangle(sf, bothSet) | ||
assert.NotNil(t, err) // there should be an error if both are set! | ||
|
||
neitherSet := []FieldValueTuple{ | ||
{ | ||
Field: sf, | ||
Value: reflect.ValueOf(nilInt), | ||
}, | ||
{ | ||
Field: sf, | ||
Value: reflect.ValueOf(nilInt), | ||
}, | ||
} | ||
|
||
val, err = aliasMangler.Unmangle(sf, neitherSet) | ||
require.NoError(t, err) | ||
|
||
assert.True(t, val.IsNil()) | ||
|
||
} |