Skip to content

Commit

Permalink
[confmap] confmap honors Unmarshal methods on config embedded structs.
Browse files Browse the repository at this point in the history
  • Loading branch information
atoulme committed Feb 25, 2024
1 parent 6be5fe9 commit 3a7b443
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 28 deletions.
25 changes: 25 additions & 0 deletions .chloggen/support_embedded_structs_confmap.yaml
@@ -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: confmap honors `Unmarshal` methods on config embedded structs.

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

# (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: []
4 changes: 2 additions & 2 deletions cmd/mdatagen/loader_test.go
Expand Up @@ -245,7 +245,7 @@ func Test_loadMetadata(t *testing.T) {
},
{
name: "testdata/unknown_value_type.yaml",
wantErr: "1 error(s) decoding:\n\n* error decoding 'metrics[system.cpu.time]': 1 error(s) decoding:\n\n* error decoding 'sum': 1 error(s) decoding:\n\n* error decoding 'value_type': invalid value_type: \"unknown\"",
wantErr: "1 error(s) decoding:\n\n* error decoding 'metrics[system.cpu.time]': 1 error(s) decoding:\n\n* error decoding 'sum': invalid value_type: \"unknown\"",
},
{
name: "testdata/no_aggregation.yaml",
Expand All @@ -255,7 +255,7 @@ func Test_loadMetadata(t *testing.T) {
{
name: "testdata/invalid_aggregation.yaml",
want: metadata{},
wantErr: "1 error(s) decoding:\n\n* error decoding 'metrics[default.metric]': 1 error(s) decoding:\n\n* error decoding 'sum': 1 error(s) decoding:\n\n* error decoding 'aggregation_temporality': invalid aggregation: \"invalidaggregation\"",
wantErr: "1 error(s) decoding:\n\n* error decoding 'metrics[default.metric]': 1 error(s) decoding:\n\n* error decoding 'sum': error decoding '': invalid aggregation: \"invalidaggregation\"",
},
{
name: "testdata/invalid_type_attr.yaml",
Expand Down
47 changes: 23 additions & 24 deletions cmd/mdatagen/metricdata.go
Expand Up @@ -28,18 +28,22 @@ type MetricData interface {
type AggregationTemporality struct {
// Aggregation describes if the aggregator reports delta changes
// since last report time, or cumulative changes since a fixed start time.
Aggregation pmetric.AggregationTemporality
}

// UnmarshalText implements the encoding.TextUnmarshaler interface.
func (agg *AggregationTemporality) UnmarshalText(text []byte) error {
switch vtStr := string(text); vtStr {
case "cumulative":
agg.Aggregation = pmetric.AggregationTemporalityCumulative
case "delta":
agg.Aggregation = pmetric.AggregationTemporalityDelta
default:
return fmt.Errorf("invalid aggregation: %q", vtStr)
Aggregation pmetric.AggregationTemporality `mapstructure:"aggregation_temporality"`
}

func (agg *AggregationTemporality) Unmarshal(parser *confmap.Conf) error {
v := parser.Get("aggregation_temporality")
if aggValue, ok := v.(pmetric.AggregationTemporality); ok {
agg.Aggregation = aggValue
} else {
switch v {
case "cumulative":
agg.Aggregation = pmetric.AggregationTemporalityCumulative
case "delta":
agg.Aggregation = pmetric.AggregationTemporalityDelta
default:
return fmt.Errorf("invalid aggregation: %q", v)
}
}
return nil
}
Expand Down Expand Up @@ -73,22 +77,17 @@ func (mit MetricInputType) String() string {
// MetricValueType defines the metric number type.
type MetricValueType struct {
// ValueType is type of the metric number, options are "double", "int".
ValueType pmetric.NumberDataPointValueType
ValueType pmetric.NumberDataPointValueType `mapstructure:"value_type"`
}

func (mvt *MetricValueType) Unmarshal(parser *confmap.Conf) error {
if !parser.IsSet("value_type") {
return errors.New("missing required field: `value_type`")
}
return nil
}

// UnmarshalText implements the encoding.TextUnmarshaler interface.
func (mvt *MetricValueType) UnmarshalText(text []byte) error {
switch vtStr := string(text); vtStr {
case "int":
switch vtStr := parser.Get("value_type"); vtStr {
case "int", pmetric.NumberDataPointValueTypeInt:
mvt.ValueType = pmetric.NumberDataPointValueTypeInt
case "double":
case "double", pmetric.NumberDataPointValueTypeDouble:
mvt.ValueType = pmetric.NumberDataPointValueTypeDouble
default:
return fmt.Errorf("invalid value_type: %q", vtStr)
Expand Down Expand Up @@ -116,7 +115,7 @@ func (mvt MetricValueType) BasicType() string {
}

type gauge struct {
MetricValueType `mapstructure:"value_type"`
MetricValueType `mapstructure:",squash"`
MetricInputType `mapstructure:",squash"`
}

Expand All @@ -141,9 +140,9 @@ func (d gauge) HasAggregated() bool {
}

type sum struct {
AggregationTemporality `mapstructure:"aggregation_temporality"`
AggregationTemporality `mapstructure:",squash"`
Mono `mapstructure:",squash"`
MetricValueType `mapstructure:"value_type"`
MetricValueType `mapstructure:",squash"`
MetricInputType `mapstructure:",squash"`
}

Expand Down
4 changes: 4 additions & 0 deletions component/config_test.go
Expand Up @@ -73,6 +73,10 @@ func newErrMapType() *errMapType {
return &et
}

type embedConfig struct {
errType
}

func TestValidateConfig(t *testing.T) {
var tests = []struct {
name string
Expand Down
40 changes: 40 additions & 0 deletions confmap/confmap.go
Expand Up @@ -157,6 +157,7 @@ func decodeConfig(m *Conf, result any, errorUnused bool) error {
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.TextUnmarshallerHookFunc(),
unmarshalerHookFunc(result),
embeddedStructsHookFunc(result),
zeroSliceHookFunc(),
),
}
Expand Down Expand Up @@ -261,6 +262,45 @@ func mapKeyStringToMapKeyTextUnmarshalerHookFunc() mapstructure.DecodeHookFuncTy
}
}

func embeddedStructsHookFunc(_ any) mapstructure.DecodeHookFuncValue {
return func(from reflect.Value, to reflect.Value) (any, error) {
if to.Type().Kind() != reflect.Struct {
return from.Interface(), nil
}

finalFrom := from.Interface()

for i := 0; i < to.Type().NumField(); i++ {
if to.Type().Field(i).IsExported() && to.Type().Field(i).Anonymous {
f := to.Field(i)
if unmarshaler, ok := f.Addr().Interface().(Unmarshaler); ok {
fromMap, ok := finalFrom.(map[string]any)
if !ok {
conf := New()
if err := conf.Marshal(finalFrom); err != nil {
return nil, err
}
fromMap = conf.ToStringMap()
}
if err := unmarshaler.Unmarshal(NewFromStringMap(fromMap)); err != nil {
return nil, err
}
conf := New()
if err := conf.Marshal(unmarshaler); err != nil {
return nil, err
}
resultMap := conf.ToStringMap()
for k, v := range resultMap {
fromMap[k] = v
}
finalFrom = fromMap
}
}
}
return finalFrom, nil
}
}

// Provides a mechanism for individual structs to define their own unmarshal logic,
// by implementing the Unmarshaler interface.
func unmarshalerHookFunc(result any) mapstructure.DecodeHookFuncValue {
Expand Down
34 changes: 32 additions & 2 deletions confmap/confmap_test.go
Expand Up @@ -309,8 +309,34 @@ func newConfFromFile(t testing.TB, fileName string) map[string]any {
}

type testConfig struct {
Next *nextConfig `mapstructure:"next"`
Another string `mapstructure:"another"`
Next *nextConfig `mapstructure:"next"`
Another string `mapstructure:"another"`
EmbeddedConfig `mapstructure:",squash"`
EmbeddedConfig2 `mapstructure:",squash"`
}

type EmbeddedConfig struct {
Some string `mapstructure:"some"`
}

func (ec *EmbeddedConfig) Unmarshal(component *Conf) error {
if err := component.Unmarshal(ec, WithIgnoreUnused()); err != nil {
return err
}
ec.Some += " is also called"
return nil
}

type EmbeddedConfig2 struct {
Some2 string `mapstructure:"some_2"`
}

func (ec *EmbeddedConfig2) Unmarshal(component *Conf) error {
if err := component.Unmarshal(ec, WithIgnoreUnused()); err != nil {
return err
}
ec.Some2 += " also called2"
return nil
}

func (tc *testConfig) Unmarshal(component *Conf) error {
Expand Down Expand Up @@ -340,12 +366,16 @@ func TestUnmarshaler(t *testing.T) {
"string": "make sure this",
},
"another": "make sure this",
"some": "make sure this",
"some_2": "this better be",
})

tc := &testConfig{}
assert.NoError(t, cfgMap.Unmarshal(tc))
assert.Equal(t, "make sure this", tc.Another)
assert.Equal(t, "make sure this is called", tc.Next.String)
assert.Equal(t, "make sure this is also called", tc.EmbeddedConfig.Some)
assert.Equal(t, "this better be also called2", tc.EmbeddedConfig2.Some2)
}

func TestUnmarshalerKeepAlreadyInitialized(t *testing.T) {
Expand Down

0 comments on commit 3a7b443

Please sign in to comment.