Skip to content

Commit 629c058

Browse files
authored
Introduce StringKeyValues configuration type (#1063)
1 parent 7c37e3f commit 629c058

File tree

9 files changed

+631
-45
lines changed

9 files changed

+631
-45
lines changed

internal/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ func GetConfig(cmd *cobra.Command, configFile string) (Config, Meta, error) {
164164
configtypes.StringToDurationHookFunc(),
165165
configtypes.StringToPEMDataHookFunc(),
166166
configtypes.StringToMapStringStringHookFunc(),
167+
configtypes.StringToStringKeyValuesHookFunc(),
167168
)))
168169

169170
if cmd != nil {

internal/config/config_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ func checkConfig(t *testing.T, conf Config) {
3737
require.Equal(t, "redis", conf.Engine.Type)
3838
require.Equal(t, 30*time.Second, time.Duration(conf.Engine.Redis.PresenceTTL))
3939
require.Equal(t, []string{"redis:6379"}, conf.Engine.Redis.Address)
40+
require.Equal(t, configtypes.MapStringString(map[string]string{"x": "y"}), conf.Client.Proxy.Connect.HTTP.StaticHeaders)
41+
require.Equal(t, configtypes.MapStringString(map[string]string{"X": "y"}), conf.Client.Proxy.Refresh.HTTP.StaticHeaders)
4042
}
4143

4244
func TestConfigJSON(t *testing.T) {

internal/config/testdata/config.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,19 @@
2222
"connect": {
2323
"http": {
2424
"static_headers": {
25-
"x": "y"
25+
"X": "y"
2626
}
2727
}
28+
},
29+
"refresh": {
30+
"http": {
31+
"static_headers": [
32+
{
33+
"key": "X",
34+
"value": "y"
35+
}
36+
]
37+
}
2838
}
2939
}
3040
},

internal/config/testdata/config.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ ping_interval = "12s"
4848
[client.proxy.connect.http.static_headers]
4949
x = "y"
5050

51+
[[client.proxy.refresh.http.static_headers]]
52+
key = "X"
53+
value = "y"
54+
5155
[channel.without_namespace]
5256
presence = true
5357

internal/config/testdata/config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ client:
4545
http:
4646
static_headers:
4747
x: "y"
48+
refresh:
49+
http:
50+
static_headers:
51+
- key: X
52+
value: y
4853
channel:
4954
without_namespace:
5055
presence: true
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package configtypes
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"reflect"
7+
8+
"github.com/go-viper/mapstructure/v2"
9+
)
10+
11+
type StringKeyValue struct {
12+
// Key is the key of the key/value pair. Can not be empty. Must be unique within a StringKeyValues list.
13+
Key string `json:"key" yaml:"key" toml:"key"`
14+
// Value is the value of the key/value pair.
15+
Value string `json:"value" yaml:"value" toml:"value"`
16+
}
17+
18+
type StringKeyValues []StringKeyValue
19+
20+
// ToMap converts StringKeyValues to a map[string]string for easier lookups.
21+
func (s *StringKeyValues) ToMap() map[string]string {
22+
if s == nil {
23+
return nil
24+
}
25+
m := make(map[string]string, len(*s))
26+
for _, kv := range *s {
27+
m[kv.Key] = kv.Value
28+
}
29+
return m
30+
}
31+
32+
func (s *StringKeyValues) Decode(value string) error {
33+
// Try decoding as map[string]string first (simpler syntax).
34+
var m map[string]string
35+
if err := json.Unmarshal([]byte(value), &m); err == nil {
36+
// Convert map to slice, applying env var expansion
37+
if err := expandEnvVars(m); err != nil {
38+
return err
39+
}
40+
result := make([]StringKeyValue, 0, len(m))
41+
for k, v := range m {
42+
result = append(result, StringKeyValue{Key: k, Value: v})
43+
}
44+
*s = result
45+
return nil
46+
}
47+
48+
// If that fails, try decoding as slice of key/value objects.
49+
var kvList []StringKeyValue
50+
if err := json.Unmarshal([]byte(value), &kvList); err != nil {
51+
return fmt.Errorf("cannot decode StringKeyValues: %w", err)
52+
}
53+
54+
// Validate and apply env var expansion
55+
m2 := make(map[string]string)
56+
for i, kv := range kvList {
57+
if kv.Key == "" {
58+
return fmt.Errorf("empty key at element %d", i)
59+
}
60+
if _, exists := m2[kv.Key]; exists {
61+
return fmt.Errorf("duplicate key %q at element %d", kv.Key, i)
62+
}
63+
m2[kv.Key] = kv.Value
64+
}
65+
66+
if err := expandEnvVars(m2); err != nil {
67+
return err
68+
}
69+
70+
// Update values with expanded env vars
71+
for i := range kvList {
72+
kvList[i].Value = m2[kvList[i].Key]
73+
}
74+
75+
*s = kvList
76+
return nil
77+
}
78+
79+
func StringToStringKeyValuesHookFunc() mapstructure.DecodeHookFunc {
80+
return func(f reflect.Type, t reflect.Type, data any) (any, error) {
81+
if t != reflect.TypeOf(StringKeyValues{}) {
82+
return data, nil
83+
}
84+
85+
// Only support slice of key/value objects.
86+
v, ok := data.([]any)
87+
if !ok {
88+
return nil, fmt.Errorf("unsupported type %T for StringKeyValues, expected slice of key/value objects", data)
89+
}
90+
91+
result := make([]StringKeyValue, 0, len(v))
92+
m := make(map[string]string)
93+
for i, item := range v {
94+
kvMap, ok := item.(map[string]any)
95+
if !ok {
96+
return nil, fmt.Errorf("expected map for element %d, got %T", i, item)
97+
}
98+
99+
keyI, ok := kvMap["key"].(string)
100+
if !ok {
101+
return nil, fmt.Errorf("missing or invalid key in element %d", i)
102+
}
103+
104+
if _, exists := m[keyI]; exists {
105+
return nil, fmt.Errorf("duplicate key %q at element %d", keyI, i)
106+
}
107+
108+
valI, ok := kvMap["value"].(string)
109+
if !ok {
110+
return nil, fmt.Errorf("missing or invalid value in element %d", i)
111+
}
112+
113+
m[keyI] = valI
114+
result = append(result, StringKeyValue{Key: keyI, Value: valI})
115+
}
116+
117+
// Apply env var expansion
118+
if err := expandEnvVars(m); err != nil {
119+
return nil, err
120+
}
121+
122+
// Update values with expanded env vars
123+
for i := range result {
124+
result[i].Value = m[result[i].Key]
125+
}
126+
127+
return StringKeyValues(result), nil
128+
}
129+
}

0 commit comments

Comments
 (0)