Skip to content

Commit bf38777

Browse files
committed
[configoptional] Add configoptional module
1 parent ee2c784 commit bf38777

File tree

9 files changed

+382
-0
lines changed

9 files changed

+382
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
2+
# Use this changelog template to create an entry for release notes.
3+
4+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
5+
change_type: new_component
6+
7+
# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
8+
component: configoptional
9+
10+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
11+
note: Add a new configoptional module to support optional configuration fields.
12+
13+
# One or more tracking issues or pull requests related to the change
14+
issues: [12981]
15+
16+
# (Optional) One or more lines of additional information to render under the primary note.
17+
# These lines will be padded with 2 spaces and then inserted directly into the document.
18+
# Use pipe (|) for multiline entries.
19+
subtext:
20+
21+
# Optional: The change log or logs in which this entry should be included.
22+
# e.g. '[user]' or '[user, api]'
23+
# Include 'user' if the change is relevant to end users.
24+
# Include 'api' if there is a change to a library API.
25+
# Default: '[user]'
26+
change_logs: []

.github/workflows/utils/cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@
130130
"configmodels",
131131
"confignet",
132132
"configopaque",
133+
"configoptional",
133134
"configparser",
134135
"configretry",
135136
"configrpc",

config/configoptional/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include ../../Makefile.Common

config/configoptional/go.mod

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module go.opentelemetry.io/collector/config/configoptional
2+
3+
go 1.23.0
4+
5+
require (
6+
github.com/stretchr/testify v1.10.0
7+
go.opentelemetry.io/collector/confmap v1.31.0
8+
go.uber.org/goleak v1.3.0
9+
)
10+
11+
require (
12+
github.com/davecgh/go-spew v1.1.1 // indirect
13+
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
14+
github.com/gobwas/glob v0.2.3 // indirect
15+
github.com/hashicorp/go-version v1.7.0 // indirect
16+
github.com/knadh/koanf/maps v0.1.2 // indirect
17+
github.com/knadh/koanf/providers/confmap v1.0.0 // indirect
18+
github.com/knadh/koanf/v2 v2.2.0 // indirect
19+
github.com/mitchellh/copystructure v1.2.0 // indirect
20+
github.com/mitchellh/reflectwalk v1.0.2 // indirect
21+
github.com/pmezard/go-difflib v1.0.0 // indirect
22+
go.opentelemetry.io/collector/featuregate v1.32.0 // indirect
23+
go.uber.org/multierr v1.11.0 // indirect
24+
go.uber.org/zap v1.27.0 // indirect
25+
gopkg.in/yaml.v3 v3.0.1 // indirect
26+
sigs.k8s.io/yaml v1.4.0 // indirect
27+
)
28+
29+
replace go.opentelemetry.io/collector/confmap => ../../confmap
30+
31+
replace go.opentelemetry.io/collector/featuregate => ../../featuregate

config/configoptional/go.sum

Lines changed: 42 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/configoptional/optional.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package configoptional
5+
6+
import (
7+
"reflect"
8+
9+
"go.opentelemetry.io/collector/confmap"
10+
)
11+
12+
// Optional represents a value that may or may not be present.
13+
// It supports two flavors: Some(value) and None.
14+
// The zero value of Optional is None.
15+
type Optional[T any] struct {
16+
// Enabled indicates if the value is present.
17+
Enabled bool `mapstructure:"enabled"`
18+
value T
19+
}
20+
21+
// assertNoEnabledField checks that a given type, if being of struct kind,
22+
// does not have a field with a mapstructure tag "enabled".
23+
func assertNoEnabledField[T any]() {
24+
var i T
25+
t := reflect.TypeOf(i)
26+
27+
// Dereference pointer types to get the underlying type.
28+
for t.Kind() == reflect.Ptr {
29+
t = t.Elem()
30+
}
31+
32+
if t.Kind() != reflect.Struct {
33+
// Not a struct, no need to check for "enabled" field.
34+
return
35+
}
36+
37+
// Check if the struct has a field with the name "enabled".
38+
for i := 0; i < t.NumField(); i++ {
39+
field := t.Field(i)
40+
if field.Tag.Get("mapstructure") == "enabled" {
41+
panic("configoptional: underlying type cannot have a field with mapstructure tag 'enabled'")
42+
}
43+
}
44+
}
45+
46+
// Some creates an Optional with a value and no factory.
47+
// It panics if T has a field with the mapstructure tag "enabled".
48+
func Some[T any](value T) Optional[T] {
49+
assertNoEnabledField[T]()
50+
return Optional[T]{value: value, Enabled: true}
51+
}
52+
53+
// None has no value.
54+
// It panics if T has a field with the mapstructure tag "enabled".
55+
func None[T any]() Optional[T] {
56+
assertNoEnabledField[T]()
57+
return Optional[T]{}
58+
}
59+
60+
// Get returns the value of the Optional.
61+
// If the value is not present, it returns nil.
62+
func (o *Optional[T]) Get() *T {
63+
if !o.Enabled {
64+
return nil
65+
}
66+
return &o.value
67+
}
68+
69+
var _ confmap.Unmarshaler = (*Optional[any])(nil)
70+
71+
// Unmarshal implements the confmap.Unmarshaler interface.
72+
// Unmarshaling from an empty map will set Enabled to true.
73+
// It panics if T has a field with the mapstructure tag "enabled".
74+
func (o *Optional[T]) Unmarshal(conf *confmap.Conf) error {
75+
assertNoEnabledField[T]()
76+
77+
if !conf.IsSet("enabled") {
78+
o.Enabled = true
79+
}
80+
81+
if o.Enabled {
82+
return conf.Unmarshal(&o.value)
83+
}
84+
return nil
85+
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package configoptional
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
12+
"go.opentelemetry.io/collector/confmap"
13+
)
14+
15+
type Config[T any] struct {
16+
Sub1 Optional[T] `mapstructure:"sub"`
17+
}
18+
19+
type Sub struct {
20+
Foo string `mapstructure:"foo"`
21+
}
22+
23+
func ptr[T any](v T) *T {
24+
return &v
25+
}
26+
27+
func TestPanicsNestedOptional(t *testing.T) {
28+
assert.Panics(t, func() {
29+
_ = Some(Some(Sub{}))
30+
})
31+
32+
assert.Panics(t, func() {
33+
_ = Some(ptr(Some(Sub{})))
34+
})
35+
36+
assert.Panics(t, func() {
37+
_ = Some(ptr(ptr(Some(Sub{}))))
38+
})
39+
40+
assert.Panics(t, func() {
41+
_ = None[Optional[Sub]]()
42+
})
43+
}
44+
45+
func TestNoneZeroVal(t *testing.T) {
46+
var none Optional[Sub]
47+
require.False(t, none.Enabled)
48+
require.Nil(t, none.Get())
49+
}
50+
51+
func TestNone(t *testing.T) {
52+
none := None[Sub]()
53+
require.False(t, none.Enabled)
54+
require.Nil(t, none.Get())
55+
}
56+
57+
func TestSome(t *testing.T) {
58+
some := Some(Sub{
59+
Foo: "foobar",
60+
})
61+
require.True(t, some.Enabled)
62+
require.NotEqual(t, 1, *some.Get())
63+
}
64+
65+
func TestUnmarshalOptional(t *testing.T) {
66+
tests := []struct {
67+
name string
68+
config map[string]any
69+
defaultCfg Config[Sub]
70+
expectedSub bool
71+
expectedFoo string
72+
}{
73+
{
74+
name: "none_no_config",
75+
defaultCfg: Config[Sub]{
76+
Sub1: None[Sub](),
77+
},
78+
expectedSub: false,
79+
},
80+
{
81+
name: "none_with_config",
82+
config: map[string]any{
83+
"sub": map[string]any{
84+
"foo": "bar",
85+
},
86+
},
87+
defaultCfg: Config[Sub]{
88+
Sub1: None[Sub](),
89+
},
90+
expectedSub: true,
91+
expectedFoo: "bar", // input overrides default
92+
},
93+
{
94+
name: "some_no_config",
95+
defaultCfg: Config[Sub]{
96+
Sub1: Some(Sub{
97+
Foo: "foobar",
98+
}),
99+
},
100+
expectedSub: true,
101+
expectedFoo: "foobar", // value is not modified
102+
},
103+
{
104+
name: "some_with_config",
105+
config: map[string]any{
106+
"sub": map[string]any{
107+
"foo": "bar",
108+
},
109+
},
110+
defaultCfg: Config[Sub]{
111+
Sub1: Some(Sub{
112+
Foo: "foobar",
113+
}),
114+
},
115+
expectedSub: true,
116+
expectedFoo: "bar", // input overrides previous value
117+
},
118+
{
119+
name: "some_with_config_no_foo",
120+
config: map[string]any{
121+
"sub": nil,
122+
},
123+
defaultCfg: Config[Sub]{
124+
Sub1: Some(Sub{
125+
Foo: "foobar",
126+
}),
127+
},
128+
expectedSub: true,
129+
expectedFoo: "foobar", // default applies
130+
},
131+
}
132+
133+
for _, test := range tests {
134+
t.Run(test.name, func(t *testing.T) {
135+
cfg := test.defaultCfg
136+
conf := confmap.NewFromStringMap(test.config)
137+
require.NoError(t, conf.Unmarshal(&cfg))
138+
require.Equal(t, test.expectedSub, cfg.Sub1.Enabled)
139+
if test.expectedSub {
140+
require.Equal(t, test.expectedFoo, cfg.Sub1.Get().Foo)
141+
}
142+
})
143+
}
144+
}
145+
146+
func TestUnmarshalConfigPointer(t *testing.T) {
147+
cm := confmap.NewFromStringMap(map[string]any{
148+
"sub": map[string]any{
149+
"foo": "bar",
150+
},
151+
})
152+
153+
var cfg Config[*Sub]
154+
err := cm.Unmarshal(&cfg)
155+
require.NoError(t, err)
156+
assert.True(t, cfg.Sub1.Enabled)
157+
assert.Equal(t, "bar", (*cfg.Sub1.Get()).Foo)
158+
}
159+
160+
type MyIntConfig struct {
161+
Val int `mapstructure:"my_int"`
162+
}
163+
type MyConfig struct {
164+
Optional[MyIntConfig] `mapstructure:",squash"`
165+
}
166+
167+
func TestSquashedOptional(t *testing.T) {
168+
cm := confmap.NewFromStringMap(map[string]any{
169+
"my_int": 42,
170+
})
171+
172+
cfg := MyConfig{
173+
Some(MyIntConfig{1}),
174+
}
175+
176+
err := cm.Unmarshal(&cfg)
177+
require.NoError(t, err)
178+
179+
assert.True(t, cfg.Enabled)
180+
assert.Equal(t, 42, cfg.Get().Val)
181+
}

config/configoptional/package_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package configoptional
5+
6+
import (
7+
"testing"
8+
9+
"go.uber.org/goleak"
10+
)
11+
12+
func TestMain(m *testing.M) {
13+
goleak.VerifyTestMain(m)
14+
}

0 commit comments

Comments
 (0)