Skip to content

Commit

Permalink
Add config marshaler.
Browse files Browse the repository at this point in the history
  • Loading branch information
jefchien committed Sep 26, 2022
1 parent b7ac70a commit 81934ee
Show file tree
Hide file tree
Showing 11 changed files with 712 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@

- Add AppendEmpty and EnsureCapacity method to primitive pdata slices (#6060)
- Expose `AsRaw` and `FromRaw` `pcommon.Value` methods (#6090)
- Add config marshaler (#5566)

## v0.60.0 Beta

Expand Down
6 changes: 6 additions & 0 deletions config/configtelemetry/configtelemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const (
// that every component should generate.
type Level int32

var _ encoding.TextMarshaler = (*Level)(nil)
var _ encoding.TextUnmarshaler = (*Level)(nil)

func (l Level) String() string {
Expand All @@ -57,6 +58,11 @@ func (l Level) String() string {
return "unknown"
}

// MarshalText marshals Level to text.
func (l Level) MarshalText() (text []byte, err error) {
return []byte(l.String()), nil
}

// UnmarshalText unmarshalls text to a Level.
func (l *Level) UnmarshalText(text []byte) error {
if l == nil {
Expand Down
3 changes: 3 additions & 0 deletions config/configtelemetry/configtelemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ func TestLevelString(t *testing.T) {
for _, test := range tests {
t.Run(test.str, func(t *testing.T) {
assert.Equal(t, test.str, test.level.String())
got, err := test.level.MarshalText()
assert.NoError(t, err)
assert.Equal(t, test.str, string(got))
})
}
}
6 changes: 6 additions & 0 deletions config/identifiable.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ func (id ComponentID) Name() string {
return id.nameVal
}

// MarshalText implements the encoding.TextMarshaler interface.
// This marshals the type and name as one string in the config.
func (id ComponentID) MarshalText() (text []byte, err error) {
return []byte(id.String()), nil
}

// UnmarshalText implements the encoding.TextUnmarshaler interface.
func (id *ComponentID) UnmarshalText(text []byte) error {
idStr := string(text)
Expand Down
7 changes: 7 additions & 0 deletions config/identifiable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,10 @@ func TestIDFromString(t *testing.T) {
})
}
}

func TestMarshalText(t *testing.T) {
id := NewComponentIDWithName("test", "name")
got, err := id.MarshalText()
assert.NoError(t, err)
assert.Equal(t, id.String(), string(got))
}
62 changes: 62 additions & 0 deletions confmap/confmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"github.com/knadh/koanf/maps"
"github.com/knadh/koanf/providers/confmap"
"github.com/mitchellh/mapstructure"

encoder "go.opentelemetry.io/collector/confmap/internal/mapstructure"
)

const (
Expand Down Expand Up @@ -66,6 +68,20 @@ func (l *Conf) UnmarshalExact(result interface{}) error {
return decodeConfig(l, result, true)
}

// Marshal encodes the config and merges it into the Conf.
func (l *Conf) Marshal(rawVal interface{}) error {
enc := encoder.New(encoderConfig(rawVal))
data, err := enc.Encode(rawVal)
if err != nil {
return err
}
out, ok := data.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid config encoding")
}
return l.Merge(NewFromStringMap(out))
}

// Get can retrieve any value given the key to use.
func (l *Conf) Get(key string) interface{} {
return l.k.Get(key)
Expand Down Expand Up @@ -133,6 +149,18 @@ func decodeConfig(m *Conf, result interface{}, errorUnused bool) error {
return decoder.Decode(m.ToStringMap())
}

// encoderConfig returns a default encoder.EncoderConfig that includes
// an EncodeHook that handles both TextMarshaller and Marshaler
// interfaces.
func encoderConfig(rawVal interface{}) *encoder.EncoderConfig {
return &encoder.EncoderConfig{
EncodeHook: mapstructure.ComposeDecodeHookFunc(
encoder.TextMarshalerHookFunc(),
marshalerHookFunc(rawVal),
),
}
}

// In cases where a config has a mapping of something to a struct pointers
// we want nil values to resolve to a pointer to the zero value of the
// underlying struct just as we want nil values of a mapping of something
Expand Down Expand Up @@ -239,9 +267,43 @@ func unmarshalerHookFunc(result interface{}) mapstructure.DecodeHookFuncValue {
}
}

// marshalerHookFunc returns a DecodeHookFuncValue that checks structs that aren't
// the top level struct to see if they implement the Marshaler interface.
func marshalerHookFunc(top interface{}) mapstructure.DecodeHookFuncValue {
topValue := reflect.ValueOf(top)
return func(from reflect.Value, _ reflect.Value) (interface{}, error) {
if from.Kind() != reflect.Struct {
return from.Interface(), nil
}

// ignore top structure to avoid infinite loop.
if from == topValue {
return from.Interface(), nil
}
marshaler, ok := from.Interface().(Marshaler)
if !ok {
return from.Interface(), nil
}
conf := New()
if err := marshaler.Marshal(conf); err != nil {
return nil, err
}
return conf.ToStringMap(), nil
}
}

// Unmarshaler interface may be implemented by types to customize their behavior when being unmarshaled from a Conf.
type Unmarshaler interface {
// Unmarshal a Conf into the struct in a custom way.
// The Conf for this specific component may be nil or empty if no config available.
Unmarshal(component *Conf) error
}

// Marshaler defines an optional interface for custom configuration marshalling.
// A configuration struct can implement this interface to override the default
// marshalling.
type Marshaler interface {
// Marshal the config into a Conf in a custom way.
// The Conf will be empty and can be merged into.
Marshal(component *Conf) error
}
77 changes: 77 additions & 0 deletions confmap/confmap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,18 @@ type TestConfig struct {
MapStruct map[string]*Struct `mapstructure:"map_struct"`
}

func (t TestConfig) Marshal(conf *Conf) error {
if t.Boolean != nil && !*t.Boolean {
return errors.New("unable to marshal")
}
if err := conf.Marshal(t); err != nil {
return err
}
return conf.Merge(NewFromStringMap(map[string]interface{}{
"additional": "field",
}))
}

type Struct struct {
Name string
}
Expand All @@ -174,6 +186,14 @@ func (tID *TestID) UnmarshalText(text []byte) error {
return nil
}

func (tID TestID) MarshalText() (text []byte, err error) {
out := string(tID)
if !strings.HasSuffix(out, "_") {
out += "_"
}
return []byte(out), nil
}

type TestIDConfig struct {
Boolean bool `mapstructure:"bool"`
Map map[TestID]string `mapstructure:"map"`
Expand Down Expand Up @@ -232,6 +252,63 @@ func TestMapKeyStringToMapKeyTextUnmarshalerHookFuncErrorUnmarshal(t *testing.T)
assert.Error(t, conf.Unmarshal(cfg))
}

func TestMarshal(t *testing.T) {
conf := New()
cfg := &TestIDConfig{
Boolean: true,
Map: map[TestID]string{
"string": "this is a string",
},
}
assert.NoError(t, conf.Marshal(cfg))
assert.Equal(t, true, conf.Get("bool"))
assert.Equal(t, map[string]interface{}{"string_": "this is a string"}, conf.Get("map"))
}

func TestMarshalDuplicateID(t *testing.T) {
conf := New()
cfg := &TestIDConfig{
Boolean: true,
Map: map[TestID]string{
"string": "this is a string",
"string_": "this is another string",
},
}
assert.Error(t, conf.Marshal(cfg))
}

func TestMarshalError(t *testing.T) {
conf := New()
assert.Error(t, conf.Marshal(nil))
}

func TestMarshaler(t *testing.T) {
conf := New()
cfg := &TestConfig{
Struct: &Struct{
Name: "StructName",
},
}
assert.NoError(t, conf.Marshal(cfg))
assert.Equal(t, "field", conf.Get("additional"))

conf = New()
type NestedMarshaler struct {
TestConfig *TestConfig
}
nmCfg := &NestedMarshaler{
TestConfig: cfg,
}
assert.NoError(t, conf.Marshal(nmCfg))
sub, err := conf.Sub("testconfig")
assert.NoError(t, err)
assert.True(t, sub.IsSet("additional"))
assert.Equal(t, "field", sub.Get("additional"))
varBool := false
nmCfg.TestConfig.Boolean = &varBool
assert.Error(t, conf.Marshal(nmCfg))
}

// newConfFromFile creates a new Conf by reading the given file.
func newConfFromFile(t testing.TB, fileName string) map[string]interface{} {
content, err := os.ReadFile(filepath.Clean(fileName))
Expand Down
Loading

0 comments on commit 81934ee

Please sign in to comment.