Skip to content

Commit

Permalink
[confmap] support unmarshaling for embedded structs with and without …
Browse files Browse the repository at this point in the history
…squashing (#9861)

This is taking a small slice of #9750 to document the behavior of
confmap and make sure we can unmarshal embedded structs.
  • Loading branch information
atoulme committed Apr 3, 2024
1 parent b8e4fa7 commit 26ee291
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 1 deletion.
25 changes: 25 additions & 0 deletions .chloggen/embedded_unmarshaler.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: enhancement

# 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: Clarify the use of embedded structs to make unmarshaling composable

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

# (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: 3 additions & 1 deletion confmap/confmap.go
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"reflect"
"slices"
"strings"

"github.com/go-viper/mapstructure/v2"
Expand Down Expand Up @@ -285,7 +286,8 @@ func unmarshalerEmbeddedStructsHookFunc() mapstructure.DecodeHookFuncValue {
}
for i := 0; i < to.Type().NumField(); i++ {
// embedded structs passed in via `squash` cannot be pointers. We just check if they are structs:
if to.Type().Field(i).IsExported() && to.Type().Field(i).Anonymous {
f := to.Type().Field(i)
if f.IsExported() && slices.Contains(strings.Split(f.Tag.Get("mapstructure"), ","), "squash") {
if unmarshaler, ok := to.Field(i).Addr().Interface().(Unmarshaler); ok {
if err := unmarshaler.Unmarshal(NewFromStringMap(fromAsMap)); err != nil {
return nil, err
Expand Down
57 changes: 57 additions & 0 deletions confmap/confmap_test.go
Expand Up @@ -701,3 +701,60 @@ func TestUnmarshalDouble(t *testing.T) {
assert.NoError(t, conf.Unmarshal(s2))
assert.Equal(t, "test", s2.Str)
}

type EmbeddedStructWithUnmarshal struct {
Foo string `mapstructure:"foo"`
success string
}

func (e *EmbeddedStructWithUnmarshal) Unmarshal(c *Conf) error {
if err := c.Unmarshal(e, WithIgnoreUnused()); err != nil {
return err
}
e.success = "success"
return nil
}

type configWithUnmarshalFromEmbeddedStruct struct {
EmbeddedStructWithUnmarshal
}

type topLevel struct {
Cfg *configWithUnmarshalFromEmbeddedStruct `mapstructure:"toplevel"`
}

// Test that Unmarshal is called on the embedded struct on the struct.
func TestUnmarshalThroughEmbeddedStruct(t *testing.T) {
c := NewFromStringMap(map[string]any{
"toplevel": map[string]any{
"foo": "bar",
},
})
cfg := &topLevel{}
err := c.Unmarshal(cfg)
require.NoError(t, err)
require.Equal(t, "success", cfg.Cfg.EmbeddedStructWithUnmarshal.success)
require.Equal(t, "bar", cfg.Cfg.EmbeddedStructWithUnmarshal.Foo)
}

type configWithOwnUnmarshalAndEmbeddedSquashedStruct struct {
EmbeddedStructWithUnmarshal `mapstructure:",squash"`
}

type topLevelSquashedEmbedded struct {
Cfg *configWithOwnUnmarshalAndEmbeddedSquashedStruct `mapstructure:"toplevel"`
}

// Test that the Unmarshal method is called on the squashed, embedded struct.
func TestUnmarshalOwnThroughEmbeddedSquashedStruct(t *testing.T) {
c := NewFromStringMap(map[string]any{
"toplevel": map[string]any{
"foo": "bar",
},
})
cfg := &topLevelSquashedEmbedded{}
err := c.Unmarshal(cfg)
require.NoError(t, err)
require.Equal(t, "success", cfg.Cfg.EmbeddedStructWithUnmarshal.success)
require.Equal(t, "bar", cfg.Cfg.EmbeddedStructWithUnmarshal.Foo)
}
97 changes: 97 additions & 0 deletions confmap/doc_test.go
@@ -0,0 +1,97 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package confmap_test

import (
"fmt"
"slices"
"time"

"go.opentelemetry.io/collector/confmap"
)

type DiskScrape struct {
Disk string `mapstructure:"disk"`
Scrape time.Duration `mapstructure:"scrape"`
}

// We can annotate a struct with mapstructure field annotations.
func Example_simpleUnmarshaling() {
conf := confmap.NewFromStringMap(map[string]any{
"disk": "c",
"scrape": "5s",
})
scrapeInfo := &DiskScrape{}
if err := conf.Unmarshal(scrapeInfo); err != nil {
panic(err)
}
fmt.Printf("Configuration contains the following:\nDisk: %q\nScrape: %s\n", scrapeInfo.Disk, scrapeInfo.Scrape)
//Output: Configuration contains the following:
// Disk: "c"
// Scrape: 5s
}

type CPUScrape struct {
Enabled bool `mapstructure:"enabled"`
}

type ComputerScrape struct {
DiskScrape `mapstructure:",squash"`
CPUScrape `mapstructure:",squash"`
}

// We can unmarshal embedded structs with mapstructure field annotations.
func Example_embeddedUnmarshaling() {
conf := confmap.NewFromStringMap(map[string]any{
"disk": "c",
"scrape": "5s",
"enabled": true,
})
scrapeInfo := &ComputerScrape{}
if err := conf.Unmarshal(scrapeInfo); err != nil {
panic(err)
}
fmt.Printf("Configuration contains the following:\nDisk: %q\nScrape: %s\nEnabled: %v\n", scrapeInfo.Disk, scrapeInfo.Scrape, scrapeInfo.Enabled)
//Output: Configuration contains the following:
// Disk: "c"
// Scrape: 5s
// Enabled: true
}

type NetworkScrape struct {
Enabled bool `mapstructure:"enabled"`
Networks []string `mapstructure:"networks"`
Wifi bool `mapstructure:"wifi"`
}

func (n *NetworkScrape) Unmarshal(c *confmap.Conf) error {
if err := c.Unmarshal(n, confmap.WithIgnoreUnused()); err != nil {
return err
}
if slices.Contains(n.Networks, "wlan0") {
n.Wifi = true
}
return nil
}

type RouterScrape struct {
NetworkScrape `mapstructure:",squash"`
}

// We can unmarshal an embedded struct with a custom `Unmarshal` method.
func Example_embeddedManualUnmarshaling() {
conf := confmap.NewFromStringMap(map[string]any{
"networks": []string{"eth0", "eth1", "wlan0"},
"enabled": true,
})
scrapeInfo := &RouterScrape{}
if err := conf.Unmarshal(scrapeInfo); err != nil {
panic(err)
}
fmt.Printf("Configuration contains the following:\nNetworks: %q\nWifi: %v\nEnabled: %v\n", scrapeInfo.Networks, scrapeInfo.Wifi, scrapeInfo.Enabled)
//Output: Configuration contains the following:
// Networks: ["eth0" "eth1" "wlan0"]
// Wifi: true
// Enabled: true
}

0 comments on commit 26ee291

Please sign in to comment.