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

Add ability to marshal yaml-tagged structs #10282

Merged
merged 4 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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: []
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 {
evan-bradley marked this conversation as resolved.
Show resolved Hide resolved
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
Loading