Skip to content

Commit

Permalink
Add ability to marshal yaml-tagged structs (#10282)
Browse files Browse the repository at this point in the history
Possible solution for
#10139 (comment)

More thorough explanation here:
#10139 (comment)

---------

Co-authored-by: Evan Bradley <11745660+evan-bradley@users.noreply.github.com>
  • Loading branch information
djaglowski and evan-bradley authored Jun 17, 2024
1 parent 7a3c35c commit 654cb24
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 12 deletions.
25 changes: 25 additions & 0 deletions .chloggen/yaml-hook.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: bug_fix

# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
component: confmap

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Fix issue where structs with only yaml tags were not marshaled correctly.

# One or more tracking issues or pull requests related to the change
issues: [10282]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:

# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: []
1 change: 1 addition & 0 deletions confmap/confmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ func decodeConfig(m *Conf, result any, errorUnused bool, skipTopLevelUnmarshaler
func encoderConfig(rawVal any) *encoder.EncoderConfig {
return &encoder.EncoderConfig{
EncodeHook: mapstructure.ComposeDecodeHookFunc(
encoder.YamlMarshalerHookFunc(),
encoder.TextMarshalerHookFunc(),
marshalerHookFunc(rawVal),
),
Expand Down
33 changes: 33 additions & 0 deletions confmap/internal/mapstructure/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"

"github.com/go-viper/mapstructure/v2"
"gopkg.in/yaml.v3"
)

const (
Expand Down Expand Up @@ -228,3 +229,35 @@ func TextMarshalerHookFunc() mapstructure.DecodeHookFuncValue {
return string(out), nil
}
}

// YamlMarshalerHookFunc returns a DecodeHookFuncValue that checks for structs
// that have yaml tags but no mapstructure tags. If found, it will convert the struct
// to map[string]any using the yaml package, which respects the yaml tags. Ultimately,
// this allows mapstructure to later marshal the map[string]any in a generic way.
func YamlMarshalerHookFunc() mapstructure.DecodeHookFuncValue {
return func(from reflect.Value, _ reflect.Value) (any, error) {
if from.Kind() == reflect.Struct {
for i := 0; i < from.NumField(); i++ {
if _, ok := from.Type().Field(i).Tag.Lookup("mapstructure"); ok {
// The struct has at least one mapstructure tag so don't do anything.
return from.Interface(), nil
}

if _, ok := from.Type().Field(i).Tag.Lookup("yaml"); ok {
// The struct has at least one yaml tag, so convert it to map[string]any using yaml.
yamlBytes, err := yaml.Marshal(from.Interface())
if err != nil {
return nil, err
}
var m map[string]any
err = yaml.Unmarshal(yamlBytes, &m)
if err != nil {
return nil, err
}
return m, nil
}
}
}
return from.Interface(), nil
}
}
91 changes: 79 additions & 12 deletions confmap/internal/mapstructure/encoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@ import (
)

type TestComplexStruct struct {
Skipped TestEmptyStruct `mapstructure:",squash"`
Nested TestSimpleStruct `mapstructure:",squash"`
Slice []TestSimpleStruct `mapstructure:"slice,omitempty"`
Pointer *TestSimpleStruct `mapstructure:"ptr"`
Map map[string]TestSimpleStruct `mapstructure:"map,omitempty"`
Remain map[string]any `mapstructure:",remain"`
Interface encoding.TextMarshaler
Skipped TestEmptyStruct `mapstructure:",squash"`
Nested TestSimpleStruct `mapstructure:",squash"`
Slice []TestSimpleStruct `mapstructure:"slice,omitempty"`
Pointer *TestSimpleStruct `mapstructure:"ptr"`
Map map[string]TestSimpleStruct `mapstructure:"map,omitempty"`
Remain map[string]any `mapstructure:",remain"`
TranslatedYaml TestYamlStruct `mapstructure:"translated"`
SquashedYaml TestYamlStruct `mapstructure:",squash"`
PointerTranslatedYaml *TestPtrToYamlStruct `mapstructure:"translated_ptr"`
PointerSquashedYaml *TestPtrToYamlStruct `mapstructure:",squash"`
Interface encoding.TextMarshaler
}

type TestSimpleStruct struct {
Expand All @@ -34,6 +38,26 @@ type TestEmptyStruct struct {
Value string `mapstructure:"-"`
}

type TestYamlStruct struct {
YamlValue string `yaml:"yaml_value"`
YamlOmitEmpty string `yaml:"yaml_omit,omitempty"`
YamlInline TestYamlSimpleStruct `yaml:",inline"`
}

type TestPtrToYamlStruct struct {
YamlValue string `yaml:"yaml_value_ptr"`
YamlOmitEmpty string `yaml:"yaml_omit_ptr,omitempty"`
YamlInline *TestYamlPtrToSimpleStruct `yaml:",inline"`
}

type TestYamlSimpleStruct struct {
Inline string `yaml:"yaml_inline"`
}

type TestYamlPtrToSimpleStruct struct {
InlinePtr string `yaml:"yaml_inline_ptr"`
}

type TestID string

func (tID TestID) MarshalText() (text []byte, err error) {
Expand All @@ -51,7 +75,10 @@ type TestStringLike string

func TestEncode(t *testing.T) {
enc := New(&EncoderConfig{
EncodeHook: TextMarshalerHookFunc(),
EncodeHook: mapstructure.ComposeDecodeHookFunc(
YamlMarshalerHookFunc(),
TextMarshalerHookFunc(),
),
})
testCases := map[string]struct {
input any
Expand Down Expand Up @@ -116,17 +143,57 @@ func TestEncode(t *testing.T) {
"remain2": "value",
},
Interface: TestID("value"),
TranslatedYaml: TestYamlStruct{
YamlValue: "foo_translated",
YamlOmitEmpty: "",
YamlInline: TestYamlSimpleStruct{
Inline: "bar_translated",
},
},
SquashedYaml: TestYamlStruct{
YamlValue: "foo_squashed",
YamlOmitEmpty: "",
YamlInline: TestYamlSimpleStruct{
Inline: "bar_squashed",
},
},
PointerTranslatedYaml: &TestPtrToYamlStruct{
YamlValue: "foo_translated_ptr",
YamlOmitEmpty: "",
YamlInline: &TestYamlPtrToSimpleStruct{
InlinePtr: "bar_translated_ptr",
},
},
PointerSquashedYaml: &TestPtrToYamlStruct{
YamlValue: "foo_squashed_ptr",
YamlOmitEmpty: "",
YamlInline: &TestYamlPtrToSimpleStruct{
InlinePtr: "bar_squashed_ptr",
},
},
},
want: map[string]any{
"value": "nested",
"slice": []any{map[string]any{"value": "slice"}},
"map": map[string]any{
"Key": map[string]any{"value": "map"},
},
"ptr": map[string]any{"value": "pointer"},
"interface": "value_",
"remain1": 23,
"remain2": "value",
"ptr": map[string]any{"value": "pointer"},
"interface": "value_",
"yaml_value": "foo_squashed",
"yaml_inline": "bar_squashed",
"translated": map[string]any{
"yaml_value": "foo_translated",
"yaml_inline": "bar_translated",
},
"yaml_value_ptr": "foo_squashed_ptr",
"yaml_inline_ptr": "bar_squashed_ptr",
"translated_ptr": map[string]any{
"yaml_value_ptr": "foo_translated_ptr",
"yaml_inline_ptr": "bar_translated_ptr",
},
"remain1": 23,
"remain2": "value",
},
},
}
Expand Down

0 comments on commit 654cb24

Please sign in to comment.