Skip to content

Commit 107283a

Browse files
authored
Merge pull request #14 from sei-protocol/feat/issue-13-slice-overrides
feat(overrides): support []string slice fields in ApplyOverrides
2 parents 9b85861 + ad1bcf7 commit 107283a

3 files changed

Lines changed: 231 additions & 0 deletions

File tree

config_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,111 @@ func TestApplyOverrides_Empty(t *testing.T) {
467467
}
468468
}
469469

470+
func TestApplyOverrides_StringSlice(t *testing.T) {
471+
cases := []struct {
472+
name string
473+
in string
474+
want []string
475+
}{
476+
{"single value", "kv", []string{"kv"}},
477+
{"multi value", "kv,psql", []string{"kv", "psql"}},
478+
{"trims whitespace", " kv , psql ", []string{"kv", "psql"}},
479+
{"empty string yields empty slice", "", []string{}},
480+
}
481+
for _, tc := range cases {
482+
t.Run(tc.name, func(t *testing.T) {
483+
cfg := Default()
484+
if err := ApplyOverrides(cfg, map[string]string{
485+
"tx_index.indexer": tc.in,
486+
}); err != nil {
487+
t.Fatalf("ApplyOverrides: %v", err)
488+
}
489+
got := cfg.TxIndex.Indexer
490+
if len(got) != len(tc.want) {
491+
t.Fatalf("indexer: got %v (len %d), want %v (len %d)",
492+
got, len(got), tc.want, len(tc.want))
493+
}
494+
for i := range got {
495+
if got[i] != tc.want[i] {
496+
t.Errorf("indexer[%d]: got %q, want %q", i, got[i], tc.want[i])
497+
}
498+
}
499+
if got == nil {
500+
t.Error("indexer slice must be non-nil to render into TOML")
501+
}
502+
})
503+
}
504+
}
505+
506+
func TestApplyOverrides_StringSliceRejectsEmptyEntries(t *testing.T) {
507+
cases := []string{"kv,,psql", ",kv", "kv,", ",,,", "kv, ,psql"}
508+
for _, in := range cases {
509+
t.Run(in, func(t *testing.T) {
510+
cfg := Default()
511+
err := ApplyOverrides(cfg, map[string]string{
512+
"tx_index.indexer": in,
513+
})
514+
if err == nil {
515+
t.Fatalf("expected error for input %q, got nil", in)
516+
}
517+
})
518+
}
519+
}
520+
521+
func TestApplyOverrides_StringSliceOverwritesDefault(t *testing.T) {
522+
cfg := Default()
523+
if err := ApplyOverrides(cfg, map[string]string{
524+
"tx_index.indexer": "kv",
525+
}); err != nil {
526+
t.Fatalf("ApplyOverrides: %v", err)
527+
}
528+
if len(cfg.TxIndex.Indexer) != 1 || cfg.TxIndex.Indexer[0] != "kv" {
529+
t.Errorf("indexer: got %v, want [kv]", cfg.TxIndex.Indexer)
530+
}
531+
}
532+
533+
func TestApplyOverrides_StringSliceRoundTripTOML(t *testing.T) {
534+
dir := t.TempDir()
535+
536+
cases := []struct {
537+
name string
538+
in string
539+
want []string
540+
}{
541+
{"non-empty list survives round-trip", "kv,psql", []string{"kv", "psql"}},
542+
{"empty list survives round-trip as []", "", []string{}},
543+
}
544+
for _, tc := range cases {
545+
t.Run(tc.name, func(t *testing.T) {
546+
cfg := DefaultForMode(ModeFull)
547+
if err := ApplyOverrides(cfg, map[string]string{
548+
"tx_index.indexer": tc.in,
549+
}); err != nil {
550+
t.Fatalf("ApplyOverrides: %v", err)
551+
}
552+
subdir := t.TempDir()
553+
if err := WriteConfigToDir(cfg, subdir); err != nil {
554+
t.Fatalf("WriteConfigToDir: %v", err)
555+
}
556+
loaded, err := ReadConfigFromDir(subdir)
557+
if err != nil {
558+
t.Fatalf("ReadConfigFromDir: %v", err)
559+
}
560+
got := loaded.TxIndex.Indexer
561+
if len(got) != len(tc.want) {
562+
t.Fatalf("after round-trip: got %v (len %d), want %v (len %d)",
563+
got, len(got), tc.want, len(tc.want))
564+
}
565+
for i := range got {
566+
if got[i] != tc.want[i] {
567+
t.Errorf("indexer[%d]: got %q, want %q", i, got[i], tc.want[i])
568+
}
569+
}
570+
})
571+
}
572+
_ = dir
573+
}
574+
470575
func TestResolveEnv(t *testing.T) {
471576
cfg := Default()
472577
t.Setenv("SEI_CHAIN_MIN_GAS_PRICES", "0.5usei")

resolve.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,19 @@ func setReflectValue(v reflect.Value, s string) error {
167167
return fmt.Errorf("value %g overflows %s", n, v.Type())
168168
}
169169
v.SetFloat(n)
170+
case reflect.Slice:
171+
if v.Type().Elem().Kind() != reflect.String {
172+
return fmt.Errorf("unsupported slice element kind: %s", v.Type().Elem().Kind())
173+
}
174+
out, err := parseStringSlice(s)
175+
if err != nil {
176+
return err
177+
}
178+
sliceVal := reflect.MakeSlice(v.Type(), len(out), len(out))
179+
for i, p := range out {
180+
sliceVal.Index(i).SetString(p)
181+
}
182+
v.Set(sliceVal)
170183
default:
171184
return fmt.Errorf("unsupported field type: %s", v.Type())
172185
}
@@ -190,3 +203,23 @@ func parseFloat64(s string) (float64, error) {
190203
_, err := fmt.Sscanf(s, "%f", &n)
191204
return n, err
192205
}
206+
207+
// parseStringSlice splits a comma-separated string, trims whitespace, and
208+
// rejects empty entries so operator typos fail loudly. Empty input yields a
209+
// non-nil zero-length slice: BurntSushi/toml encodes nil as omitted,
210+
// []string{} as "field = []".
211+
func parseStringSlice(s string) ([]string, error) {
212+
if s == "" {
213+
return []string{}, nil
214+
}
215+
parts := strings.Split(s, ",")
216+
out := make([]string, 0, len(parts))
217+
for _, p := range parts {
218+
trimmed := strings.TrimSpace(p)
219+
if trimmed == "" {
220+
return nil, fmt.Errorf("empty entry in string slice value %q", s)
221+
}
222+
out = append(out, trimmed)
223+
}
224+
return out, nil
225+
}

resolve_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package seiconfig
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
func TestSetReflectValue_StringSlice(t *testing.T) {
9+
var s []string
10+
v := reflect.ValueOf(&s).Elem()
11+
12+
if err := setReflectValue(v, "a, b ,c"); err != nil {
13+
t.Fatalf("setReflectValue: %v", err)
14+
}
15+
want := []string{"a", "b", "c"}
16+
if !reflect.DeepEqual(s, want) {
17+
t.Errorf("got %v, want %v", s, want)
18+
}
19+
}
20+
21+
func TestSetReflectValue_RejectsNonStringSlice(t *testing.T) {
22+
var s []int
23+
v := reflect.ValueOf(&s).Elem()
24+
25+
err := setReflectValue(v, "1,2,3")
26+
if err == nil {
27+
t.Fatal("expected error for []int slice")
28+
}
29+
if got := err.Error(); got != "unsupported slice element kind: int" {
30+
t.Errorf("got %q, want %q", got, "unsupported slice element kind: int")
31+
}
32+
}
33+
34+
func TestSetReflectValue_RejectsSliceOfSlice(t *testing.T) {
35+
var s [][]string
36+
v := reflect.ValueOf(&s).Elem()
37+
38+
err := setReflectValue(v, "anything")
39+
if err == nil {
40+
t.Fatal("expected error for [][]string")
41+
}
42+
if got := err.Error(); got != "unsupported slice element kind: slice" {
43+
t.Errorf("got %q, want %q", got, "unsupported slice element kind: slice")
44+
}
45+
}
46+
47+
func TestParseStringSlice(t *testing.T) {
48+
cases := []struct {
49+
name string
50+
in string
51+
want []string
52+
}{
53+
{"empty yields non-nil empty", "", []string{}},
54+
{"single value", "a", []string{"a"}},
55+
{"multi value", "a,b,c", []string{"a", "b", "c"}},
56+
{"trims whitespace", " a , b , c ", []string{"a", "b", "c"}},
57+
}
58+
for _, tc := range cases {
59+
t.Run(tc.name, func(t *testing.T) {
60+
got, err := parseStringSlice(tc.in)
61+
if err != nil {
62+
t.Fatalf("parseStringSlice(%q): %v", tc.in, err)
63+
}
64+
if !reflect.DeepEqual(got, tc.want) {
65+
t.Errorf("parseStringSlice(%q): got %v, want %v", tc.in, got, tc.want)
66+
}
67+
if got == nil {
68+
t.Errorf("parseStringSlice(%q) returned nil; want non-nil empty slice", tc.in)
69+
}
70+
})
71+
}
72+
}
73+
74+
func TestParseStringSlice_RejectsEmptyEntries(t *testing.T) {
75+
cases := []struct {
76+
name string
77+
in string
78+
}{
79+
{"leading comma", ",a"},
80+
{"trailing comma", "a,"},
81+
{"consecutive commas", "a,,b"},
82+
{"only whitespace entry", "a, ,b"},
83+
{"only commas", ",,,"},
84+
}
85+
for _, tc := range cases {
86+
t.Run(tc.name, func(t *testing.T) {
87+
_, err := parseStringSlice(tc.in)
88+
if err == nil {
89+
t.Fatalf("parseStringSlice(%q): expected error, got nil", tc.in)
90+
}
91+
})
92+
}
93+
}

0 commit comments

Comments
 (0)