Skip to content

Commit 7967ca7

Browse files
authored
ENV Config values (#246)
# Changes * Secret config type - * By default `String()` method returns redacted * Configs can specify `env` tag to indicate an overriding environment variable (if present) * Events can be registered to force last * Fix to colorize goodbye message
1 parent c21995d commit 7967ca7

File tree

9 files changed

+304
-43
lines changed

9 files changed

+304
-43
lines changed

_datafiles/config.yaml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -420,9 +420,10 @@ Locked:
420420

421421
################################################################################
422422
#
423-
# ChatGPT Configurations
423+
# Secrets
424424
#
425425
################################################################################
426-
# - ChatGPTKey -
427-
# Your OpenAI Key. If empty, will not use ChatGPT.
428-
ChatGPTKey: ''
426+
# - DiscordWebhookUrl -
427+
# Optional webhook URL to send mud event messages to, such as joins/disconnects
428+
# Can also be set via environment variable: DISCORD_WEBHOOK_URL
429+
DiscordWebhookUrl: ''

internal/configs/config_types.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
package configs
22

33
import (
4+
"fmt"
45
"strconv"
56
"strings"
67
)
78

89
type ConfigInt int
910
type ConfigUInt64 uint64
1011
type ConfigString string
12+
type ConfigSecret string // special case string
1113
type ConfigFloat float64
1214
type ConfigBool bool
1315
type ConfigSliceString []string
16+
type ConfigMap map[string]string
1417

1518
type ConfigValue interface {
1619
String() string
@@ -30,6 +33,10 @@ func (c ConfigString) String() string {
3033
return string(c)
3134
}
3235

36+
func (c ConfigSecret) String() string {
37+
return `*** REDACTED ***`
38+
}
39+
3340
func (c ConfigFloat) String() string {
3441
return strconv.FormatFloat(float64(c), 'f', -1, 64)
3542
}
@@ -39,9 +46,16 @@ func (c ConfigBool) String() string {
3946
}
4047

4148
func (c ConfigSliceString) String() string {
49+
if len(c) == 0 {
50+
return `[]`
51+
}
4252
return `["` + strings.Join(c, `", "`) + `"]`
4353
}
4454

55+
func (c ConfigMap) String() string {
56+
return fmt.Sprintf(`%+v`, map[string]string(c))
57+
}
58+
4559
// Set
4660

4761
func (c *ConfigUInt64) Set(value string) error {
@@ -67,6 +81,11 @@ func (c *ConfigString) Set(value string) error {
6781
return nil
6882
}
6983

84+
func (c *ConfigSecret) Set(value string) error {
85+
*c = ConfigSecret(value)
86+
return nil
87+
}
88+
7089
func (c *ConfigFloat) Set(value string) error {
7190
v, err := strconv.ParseFloat(value, 64)
7291
if err != nil {

internal/configs/configs.go

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@ type Config struct {
103103
LeaderboardSize ConfigInt `yaml:"LeaderboardSize"` // Maximum size of leaderboard
104104
ContainerSizeMax ConfigInt `yaml:"ContainerSizeMax"` // How many objects containers can hold before overflowing
105105

106-
SeedInt int64 `yaml:"-"`
106+
DiscordWebhookUrl ConfigSecret `yaml:"DiscordWebhookUrl" env:"DISCORD_WEBHOOK_URL"` // Optional Discord URL to post updates to
107+
108+
seedInt int64 `yaml:"-"`
107109

108110
// Protected values
109111
turnsPerRound int // calculated and cached when data is validated.
@@ -253,36 +255,36 @@ func (c Config) AllConfigData(excludeStrings ...string) map[string]any {
253255
}
254256

255257
itm := items.Field(i)
256-
if itm.Type().Kind() == reflect.Slice {
257258

258-
v := reflect.Indirect(itm)
259-
list := []string{}
260-
for j := 0; j < v.Len(); j++ {
259+
if stringerVal, ok := itm.Interface().(fmt.Stringer); ok {
260+
output[name] = stringerVal.String()
261+
} else {
262+
if itm.Type().Kind() == reflect.Slice {
261263

262-
cmd := itm.Index(j).Interface().(string)
264+
v := reflect.Indirect(itm)
265+
list := []string{}
266+
for j := 0; j < v.Len(); j++ {
263267

264-
if len(excludeStrings) > 0 {
268+
cmd := itm.Index(j).Interface().(string)
269+
270+
if len(excludeStrings) > 0 {
265271

266-
}
267-
/*
268-
if len(cmd) > 27 {
269-
cmd = cmd[0:27]
270272
}
271-
*/
272-
list = append(list, cmd)
273-
//output[fmt.Sprintf(`%s.%d`, name, j)] = cmd
274-
}
275-
output[name] = strings.Join(list, `; `)
273+
list = append(list, cmd)
274+
//output[fmt.Sprintf(`%s.%d`, name, j)] = cmd
275+
}
276+
output[name] = strings.Join(list, `; `)
276277

277-
} else if itm.Type().Kind() == reflect.Map {
278-
// iterate the map
279-
keys := itm.MapKeys()
280-
for _, key := range keys {
281-
output[fmt.Sprintf(`%s.%d`, name, key.Int())] = itm.MapIndex(key).Float()
282-
}
278+
} else if itm.Type().Kind() == reflect.Map {
279+
// iterate the map
280+
keys := itm.MapKeys()
281+
for _, key := range keys {
282+
output[fmt.Sprintf(`%s.%d`, name, key.Int())] = itm.MapIndex(key).Float()
283+
}
283284

284-
} else {
285-
output[name] = itm.Interface()
285+
} else {
286+
output[name] = itm.Interface()
287+
}
286288
}
287289

288290
}
@@ -531,14 +533,47 @@ func (c *Config) Validate() {
531533
c.turnsPerSecond = int(1000 / c.TurnMs)
532534
c.roundsPerMinute = 60 / float64(c.RoundSeconds)
533535

534-
c.SeedInt = 0
536+
c.seedInt = 0
535537
for i, num := range util.Md5Bytes([]byte(string(c.Seed))) {
536-
c.SeedInt += int64(num) << i
538+
c.seedInt += int64(num) << i
537539
}
538540

539541
c.validated = true
540542
}
541543

544+
func (c *Config) setEnvAssignments(clear bool) {
545+
546+
// We use reflect.Indirect to handle if cfg is a pointer or not
547+
v := reflect.ValueOf(c).Elem()
548+
549+
// We'll need the struct type as well (to get field names).
550+
t := v.Type()
551+
552+
for i := 0; i < v.NumField(); i++ {
553+
fieldVal := v.Field(i)
554+
fieldType := t.Field(i)
555+
556+
if fieldVal.Type().Kind() != reflect.String {
557+
continue
558+
}
559+
560+
if envName := fieldType.Tag.Get(`env`); envName != `` {
561+
if fieldVal.CanSet() {
562+
if envVal := os.Getenv(envName); envVal != `` {
563+
564+
if clear {
565+
envVal = ``
566+
}
567+
568+
fieldVal.Set(reflect.ValueOf(ConfigSecret(envVal)))
569+
570+
}
571+
}
572+
}
573+
574+
}
575+
}
576+
542577
func (c Config) GetDeathXPPenalty() (setting string, pct float64) {
543578

544579
setting = string(c.OnDeathXPPenalty)
@@ -605,6 +640,10 @@ func (c Config) IsBannedName(name string) (string, bool) {
605640
return "", false
606641
}
607642

643+
func (c Config) SeedInt() int64 {
644+
return c.seedInt
645+
}
646+
608647
func GetConfig() Config {
609648
configDataLock.RLock()
610649
defer configDataLock.RUnlock()
@@ -666,6 +705,8 @@ func ReloadConfig() error {
666705
tmpConfigData.SetOverrides(map[string]any{})
667706
}
668707

708+
tmpConfigData.setEnvAssignments(false)
709+
669710
tmpConfigData.Validate()
670711

671712
configDataLock.Lock()
@@ -675,3 +716,8 @@ func ReloadConfig() error {
675716

676717
return nil
677718
}
719+
720+
// Usage: configs.GetSecret(c.DiscordWebhookUrl)
721+
func GetSecret(v ConfigSecret) string {
722+
return string(v)
723+
}

internal/configs/configs_test.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,166 @@ func Config_Typed(c Config) uint64 {
8080
func Config_Interface(c interface{}) uint64 {
8181
return uint64(c.(Config).turnsPerRound)
8282
}
83+
84+
func TestConfigUInt64String(t *testing.T) {
85+
tests := []struct {
86+
name string
87+
value ConfigUInt64
88+
expected string
89+
}{
90+
{name: "Zero", value: 0, expected: "0"},
91+
{name: "SmallNumber", value: 123, expected: "123"},
92+
{name: "LargeNumber", value: 9223372036854775808, expected: "9223372036854775808"},
93+
}
94+
95+
for _, tt := range tests {
96+
t.Run(tt.name, func(t *testing.T) {
97+
got := tt.value.String()
98+
if got != tt.expected {
99+
t.Errorf("ConfigUInt64.String() = %q, want %q", got, tt.expected)
100+
}
101+
})
102+
}
103+
}
104+
105+
func TestConfigIntString(t *testing.T) {
106+
tests := []struct {
107+
name string
108+
value ConfigInt
109+
expected string
110+
}{
111+
{name: "Zero", value: 0, expected: "0"},
112+
{name: "Positive", value: 42, expected: "42"},
113+
{name: "Negative", value: -100, expected: "-100"},
114+
}
115+
116+
for _, tt := range tests {
117+
t.Run(tt.name, func(t *testing.T) {
118+
got := tt.value.String()
119+
if got != tt.expected {
120+
t.Errorf("ConfigInt.String() = %q, want %q", got, tt.expected)
121+
}
122+
})
123+
}
124+
}
125+
126+
func TestConfigStringString(t *testing.T) {
127+
tests := []struct {
128+
name string
129+
value ConfigString
130+
expected string
131+
}{
132+
{name: "Empty", value: "", expected: ""},
133+
{name: "Hello", value: "Hello, world!", expected: "Hello, world!"},
134+
}
135+
136+
for _, tt := range tests {
137+
t.Run(tt.name, func(t *testing.T) {
138+
got := tt.value.String()
139+
if got != tt.expected {
140+
t.Errorf("ConfigString.String() = %q, want %q", got, tt.expected)
141+
}
142+
})
143+
}
144+
}
145+
146+
func TestConfigFloatString(t *testing.T) {
147+
tests := []struct {
148+
name string
149+
value ConfigFloat
150+
expected string
151+
}{
152+
{name: "Zero", value: 0.0, expected: "0"},
153+
{name: "Pi", value: 3.14, expected: "3.14"},
154+
{name: "WholeNumber", value: 2.0, expected: "2"},
155+
}
156+
157+
for _, tt := range tests {
158+
t.Run(tt.name, func(t *testing.T) {
159+
got := tt.value.String()
160+
if got != tt.expected {
161+
t.Errorf("ConfigFloat.String() = %q, want %q", got, tt.expected)
162+
}
163+
})
164+
}
165+
}
166+
167+
func TestConfigBoolString(t *testing.T) {
168+
tests := []struct {
169+
name string
170+
value ConfigBool
171+
expected string
172+
}{
173+
{name: "True", value: true, expected: "true"},
174+
{name: "False", value: false, expected: "false"},
175+
}
176+
177+
for _, tt := range tests {
178+
t.Run(tt.name, func(t *testing.T) {
179+
got := tt.value.String()
180+
if got != tt.expected {
181+
t.Errorf("ConfigBool.String() = %q, want %q", got, tt.expected)
182+
}
183+
})
184+
}
185+
}
186+
187+
func TestConfigSliceStringString(t *testing.T) {
188+
tests := []struct {
189+
name string
190+
value ConfigSliceString
191+
expected string
192+
}{
193+
{name: "EmptySlice", value: ConfigSliceString{}, expected: `[]`},
194+
{name: "SingleElement", value: ConfigSliceString{"one"}, expected: `["one"]`},
195+
{name: "MultipleElements", value: ConfigSliceString{"one", "two", "three"}, expected: `["one", "two", "three"]`},
196+
}
197+
198+
for _, tt := range tests {
199+
t.Run(tt.name, func(t *testing.T) {
200+
got := tt.value.String()
201+
if got != tt.expected {
202+
t.Errorf("ConfigSliceString.String() = %q, want %q", got, tt.expected)
203+
}
204+
})
205+
}
206+
}
207+
208+
func TestConfigMapString(t *testing.T) {
209+
tests := []struct {
210+
name string
211+
value ConfigMap
212+
expected string
213+
}{
214+
{
215+
name: "EmptyMap",
216+
value: ConfigMap{},
217+
expected: `map[]`,
218+
},
219+
{
220+
name: "SingleKeyValue",
221+
value: ConfigMap{"key": "value"},
222+
// fmt.Sprintf("%+v", map[string]string{"key":"value"}) => "map[key:value]"
223+
expected: `map[key:value]`,
224+
},
225+
{
226+
name: "MultipleKeyValue",
227+
value: ConfigMap{"one": "1", "two": "2"},
228+
// fmt.Sprintf("%+v", map[string]string{"one":"1","two":"2"}) => "map[one:1 two:2]"
229+
// The order of map iteration is not guaranteed. If order is important,
230+
// consider sorting the keys before formatting. For the default test:
231+
// "map[one:1 two:2]" or "map[two:2 one:1]" are both possible.
232+
// You might need a more robust comparison if order varies.
233+
expected: `map[one:1 two:2]`,
234+
},
235+
}
236+
237+
for _, tt := range tests {
238+
t.Run(tt.name, func(t *testing.T) {
239+
got := tt.value.String()
240+
if got != tt.expected {
241+
t.Errorf("ConfigMap.String() = %q, want %q", got, tt.expected)
242+
}
243+
})
244+
}
245+
}

0 commit comments

Comments
 (0)