forked from open-telemetry/opentelemetry-collector
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Try WithDefault approach suggested by @mx-psi
- Loading branch information
1 parent
6934b6e
commit c8acf84
Showing
9 changed files
with
247 additions
and
79 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package optional | ||
|
||
import "go.opentelemetry.io/collector/confmap" | ||
|
||
// Optional is a type that can be used to represent a value that may or may not be present. | ||
// It supports three flavors: Some(value), None(), and WithDefault(defaultValue). | ||
type Optional[T any] struct { | ||
hasValue bool | ||
value T | ||
|
||
defaultFn DefaultFunc[T] | ||
} | ||
|
||
type DefaultFunc[T any] func() T | ||
|
||
var _ confmap.Unmarshaler = (*Optional[any])(nil) | ||
|
||
// Some creates an Optional with a value. | ||
func Some[T any](value T) Optional[T] { | ||
return Optional[T]{value: value, hasValue: true} | ||
} | ||
|
||
// None creates an Optional with no value. | ||
func None[T any]() Optional[T] { | ||
return Optional[T]{} | ||
} | ||
|
||
// WithDefault creates an Optional which has no value | ||
// unless user config provides some, in which case | ||
// the defaultValue is used as a starting point, | ||
// which may be overridden by the user provided values. | ||
func WithDefault[T any](defaultFn DefaultFunc[T]) Optional[T] { | ||
return Optional[T]{defaultFn: defaultFn} | ||
} | ||
|
||
func (o Optional[T]) HasValue() bool { | ||
return o.hasValue | ||
} | ||
|
||
func (o Optional[T]) Value() T { | ||
return o.value | ||
} | ||
|
||
func (o *Optional[T]) Unmarshal(conf *confmap.Conf) error { | ||
// we assume that Unmarshal will not be called if conf has no value. | ||
if o.defaultFn != nil { | ||
o.value = o.defaultFn() | ||
o.hasValue = true | ||
} | ||
if err := conf.Unmarshal(&o.value); err != nil { | ||
return err | ||
} | ||
o.hasValue = true | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
package optional | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
"testing" | ||
"unicode" | ||
|
||
"github.com/stretchr/testify/require" | ||
|
||
"go.opentelemetry.io/collector/confmap" | ||
"go.opentelemetry.io/collector/confmap/confmaptest" | ||
) | ||
|
||
type Config struct { | ||
Sub1 Optional[Sub] `mapstructure:"sub"` | ||
} | ||
|
||
type Sub struct { | ||
Foo string `mapstructure:"foo"` | ||
} | ||
|
||
func TestConfigValidateNoBackends(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
config string | ||
defaultCfg Config | ||
expectedSub bool | ||
expectedFoo string | ||
}{ | ||
{ | ||
name: "no_default_no_config", | ||
defaultCfg: Config{ | ||
Sub1: None[Sub](), | ||
}, | ||
expectedSub: false, | ||
}, | ||
{ | ||
name: "no_default_with_config", | ||
config: ` | ||
sub: | ||
foo: bar | ||
`, | ||
defaultCfg: Config{ | ||
Sub1: None[Sub](), | ||
}, | ||
expectedSub: true, | ||
expectedFoo: "bar", | ||
}, | ||
{ | ||
name: "with_default_no_config", | ||
defaultCfg: Config{ | ||
Sub1: WithDefault(func() Sub { | ||
return Sub{ | ||
Foo: "foobar", | ||
} | ||
}), | ||
}, | ||
expectedSub: false, | ||
}, | ||
{ | ||
name: "with_default_with_config", | ||
config: ` | ||
sub: | ||
foo: bar | ||
`, | ||
defaultCfg: Config{ | ||
Sub1: WithDefault(func() Sub { | ||
return Sub{ | ||
Foo: "foobar", | ||
} | ||
}), | ||
}, | ||
expectedSub: true, | ||
expectedFoo: "bar", // input overrides default | ||
}, | ||
{ | ||
// this test fails, because "sub:" is considered null value by mapstructure | ||
// and no additional processing happens for it, including custom unmarshaler. | ||
// https://github.com/go-viper/mapstructure/blob/0382e5b7e3987443c91311b7fdb60b92c69a47bf/mapstructure.go#L445 | ||
name: "with_default_with_config_no_foo", | ||
config: ` | ||
sub: | ||
`, | ||
defaultCfg: Config{ | ||
Sub1: WithDefault(func() Sub { | ||
return Sub{ | ||
Foo: "foobar", | ||
} | ||
}), | ||
}, | ||
expectedSub: true, | ||
expectedFoo: "barbar", // default applies | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
cfg := test.defaultCfg | ||
conf := strToConf(t, test.config) | ||
require.NoError(t, conf.Unmarshal(&cfg)) | ||
require.Equal(t, test.expectedSub, cfg.Sub1.HasValue()) | ||
if test.expectedSub { | ||
require.Equal(t, test.expectedFoo, cfg.Sub1.Value().Foo) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func strToConf(t *testing.T, config string) *confmap.Conf { | ||
config = stripWhitespacePrefix(config) | ||
d := t.TempDir() | ||
f := filepath.Join(d, "config.yaml") | ||
require.NoError(t, os.WriteFile(f, []byte(config), 0o644)) | ||
cm, err := confmaptest.LoadConf(f) | ||
require.NoError(t, err) | ||
return cm | ||
} | ||
|
||
// stripWhitespacePrefix finds the first non-blank line, | ||
// detects how much whitespace is in front of it, and removes | ||
// that much whitespace from the beginning of all lines. | ||
func stripWhitespacePrefix(s string) string { | ||
lines := strings.Split(s, "\n") | ||
var prefix string | ||
|
||
// Find the first non-blank line | ||
for _, line := range lines { | ||
if strings.TrimSpace(line) == "" { | ||
continue | ||
} | ||
nonSpace := strings.IndexFunc(line, func(r rune) bool { | ||
return !unicode.IsSpace(r) | ||
}) | ||
prefix = string([]rune(line)[:nonSpace]) | ||
break | ||
} | ||
|
||
// Remove the prefix from all lines | ||
var result []string | ||
for _, line := range lines { | ||
if strings.TrimSpace(line) == "" { | ||
continue | ||
} | ||
result = append(result, strings.TrimPrefix(line, prefix)) | ||
} | ||
|
||
return strings.Join(result, "\n") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.