Skip to content

Commit 62242a7

Browse files
committed
Fix JSON compatibility in conf format
Signed-off-by: Waldemar Quevedo <[email protected]>
1 parent 90f5371 commit 62242a7

File tree

4 files changed

+231
-12
lines changed

4 files changed

+231
-12
lines changed

conf/lex.go

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ const (
7878
topOptTerm = '}'
7979
blockStart = '('
8080
blockEnd = ')'
81+
mapEndString = string(mapEnd)
8182
)
8283

8384
type stateFn func(lx *lexer) stateFn

conf/lex_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -1471,6 +1471,8 @@ func TestJSONCompat(t *testing.T) {
14711471
expected: []item{
14721472
{itemKey, "http_port", 3, 28},
14731473
{itemInteger, "8223", 3, 40},
1474+
{itemKey, "}", 4, 25},
1475+
{itemEOF, "", 0, 0},
14741476
},
14751477
},
14761478
{
@@ -1486,6 +1488,8 @@ func TestJSONCompat(t *testing.T) {
14861488
{itemInteger, "8223", 3, 40},
14871489
{itemKey, "port", 4, 28},
14881490
{itemInteger, "4223", 4, 35},
1491+
{itemKey, "}", 5, 25},
1492+
{itemEOF, "", 0, 0},
14891493
},
14901494
},
14911495
{
@@ -1510,6 +1514,8 @@ func TestJSONCompat(t *testing.T) {
15101514
{itemBool, "true", 6, 36},
15111515
{itemKey, "max_control_line", 7, 28},
15121516
{itemInteger, "1024", 7, 47},
1517+
{itemKey, "}", 8, 25},
1518+
{itemEOF, "", 0, 0},
15131519
},
15141520
},
15151521
{
@@ -1521,6 +1527,7 @@ func TestJSONCompat(t *testing.T) {
15211527
{itemInteger, "8224", 1, 14},
15221528
{itemKey, "port", 1, 20},
15231529
{itemInteger, "4224", 1, 27},
1530+
{itemEOF, "", 0, 0},
15241531
},
15251532
},
15261533
{
@@ -1533,6 +1540,8 @@ func TestJSONCompat(t *testing.T) {
15331540
{itemInteger, "8225", 1, 14},
15341541
{itemKey, "port", 1, 20},
15351542
{itemInteger, "4225", 1, 27},
1543+
{itemKey, "}", 2, 25},
1544+
{itemEOF, "", 0, 0},
15361545
},
15371546
},
15381547
{
@@ -1557,6 +1566,8 @@ func TestJSONCompat(t *testing.T) {
15571566
{itemString, "nats://127.0.0.1:4224", 1, 140},
15581567
{itemArrayEnd, "", 1, 163},
15591568
{itemMapEnd, "", 1, 164},
1569+
{itemKey, "}", 14, 25},
1570+
{itemEOF, "", 0, 0},
15601571
},
15611572
},
15621573
{
@@ -1594,6 +1605,35 @@ func TestJSONCompat(t *testing.T) {
15941605
{itemString, "nats://127.0.0.1:4224", 11, 32},
15951606
{itemArrayEnd, "", 12, 30},
15961607
{itemMapEnd, "", 13, 28},
1608+
{itemKey, "}", 14, 25},
1609+
{itemEOF, "", 0, 0},
1610+
},
1611+
},
1612+
{
1613+
name: "should support JSON with blocks",
1614+
input: `{
1615+
"jetstream": {
1616+
"store_dir": "/tmp/nats"
1617+
"max_mem": 1000000,
1618+
},
1619+
"port": 4222,
1620+
"server_name": "nats1"
1621+
}
1622+
`,
1623+
expected: []item{
1624+
{itemKey, "jetstream", 2, 28},
1625+
{itemMapStart, "", 2, 41},
1626+
{itemKey, "store_dir", 3, 30},
1627+
{itemString, "/tmp/nats", 3, 43},
1628+
{itemKey, "max_mem", 4, 30},
1629+
{itemInteger, "1000000", 4, 40},
1630+
{itemMapEnd, "", 5, 28},
1631+
{itemKey, "port", 6, 28},
1632+
{itemInteger, "4222", 6, 35},
1633+
{itemKey, "server_name", 7, 28},
1634+
{itemString, "nats1", 7, 43},
1635+
{itemKey, "}", 8, 25},
1636+
{itemEOF, "", 0, 0},
15971637
},
15981638
},
15991639
} {

conf/parse.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -137,16 +137,18 @@ func parse(data, fp string, pedantic bool) (p *parser, err error) {
137137
}
138138
p.pushContext(p.mapping)
139139

140-
var prevItem itemType
140+
var prevItem item
141141
for {
142142
it := p.next()
143143
if it.typ == itemEOF {
144-
if prevItem == itemKey {
144+
// Here we allow the final character to be a bracket '}'
145+
// in order to support JSON like configurations.
146+
if prevItem.typ == itemKey && prevItem.val != mapEndString {
145147
return nil, fmt.Errorf("config is invalid (%s:%d:%d)", fp, it.line, it.pos)
146148
}
147149
break
148150
}
149-
prevItem = it.typ
151+
prevItem = it
150152
if err := p.processItem(it, fp); err != nil {
151153
return nil, err
152154
}

conf/parse_test.go

+185-9
Original file line numberDiff line numberDiff line change
@@ -442,15 +442,6 @@ func TestParseWithNoValuesAreInvalid(t *testing.T) {
442442
`,
443443
"config is invalid (:3:25)",
444444
},
445-
{
446-
// trailing brackets accidentally can become keys, these are also invalid.
447-
"trailing brackets after config",
448-
`
449-
accounts { users = [{}]}
450-
}
451-
`,
452-
"config is invalid (:4:25)",
453-
},
454445
} {
455446
t.Run(test.name, func(t *testing.T) {
456447
if _, err := parse(test.conf, "", true); err == nil {
@@ -494,6 +485,48 @@ func TestParseWithNoValuesEmptyConfigsAreValid(t *testing.T) {
494485
}
495486
}
496487

488+
func TestParseWithTrailingBracketsAreValid(t *testing.T) {
489+
for _, test := range []struct {
490+
name string
491+
conf string
492+
}{
493+
{
494+
"empty conf",
495+
"{}",
496+
},
497+
{
498+
"just comments with no values",
499+
`
500+
{
501+
# comments in the body
502+
}
503+
`,
504+
},
505+
{
506+
// trailing brackets accidentally can become keys,
507+
// this is valid since needed to support JSON like configs..
508+
"trailing brackets after config",
509+
`
510+
accounts { users = [{}]}
511+
}
512+
`,
513+
},
514+
{
515+
"wrapped in brackets",
516+
`{
517+
accounts { users = [{}]}
518+
}
519+
`,
520+
},
521+
} {
522+
t.Run(test.name, func(t *testing.T) {
523+
if _, err := parse(test.conf, "", true); err != nil {
524+
t.Errorf("unexpected error: %v", err)
525+
}
526+
})
527+
}
528+
}
529+
497530
func TestParseWithNoValuesIncludes(t *testing.T) {
498531
for _, test := range []struct {
499532
input string
@@ -564,3 +597,146 @@ func TestParseWithNoValuesIncludes(t *testing.T) {
564597
})
565598
}
566599
}
600+
601+
func TestJSONParseCompat(t *testing.T) {
602+
for _, test := range []struct {
603+
name string
604+
input string
605+
includes map[string]string
606+
expected map[string]interface{}
607+
}{
608+
{
609+
"JSON with nested blocks",
610+
`
611+
{
612+
"http_port": 8227,
613+
"port": 4227,
614+
"write_deadline": "1h",
615+
"cluster": {
616+
"port": 6222,
617+
"routes": [
618+
"nats://127.0.0.1:4222",
619+
"nats://127.0.0.1:4223",
620+
"nats://127.0.0.1:4224"
621+
]
622+
}
623+
}
624+
`,
625+
nil,
626+
map[string]interface{}{
627+
"http_port": int64(8227),
628+
"port": int64(4227),
629+
"write_deadline": "1h",
630+
"cluster": map[string]interface{}{
631+
"port": int64(6222),
632+
"routes": []interface{}{
633+
"nats://127.0.0.1:4222",
634+
"nats://127.0.0.1:4223",
635+
"nats://127.0.0.1:4224",
636+
},
637+
},
638+
},
639+
},
640+
{
641+
"JSON with nested blocks",
642+
`{
643+
"jetstream": {
644+
"store_dir": "/tmp/nats"
645+
"max_mem": 1000000,
646+
},
647+
"port": 4222,
648+
"server_name": "nats1"
649+
}
650+
`,
651+
nil,
652+
map[string]interface{}{
653+
"jetstream": map[string]interface{}{
654+
"store_dir": "/tmp/nats",
655+
"max_mem": int64(1_000_000),
656+
},
657+
"port": int64(4222),
658+
"server_name": "nats1",
659+
},
660+
},
661+
{
662+
"JSON empty object in one line",
663+
`{}`,
664+
nil,
665+
map[string]interface{}{},
666+
},
667+
{
668+
"JSON empty object with line breaks",
669+
`
670+
{
671+
}
672+
`,
673+
nil,
674+
map[string]interface{}{},
675+
},
676+
{
677+
"JSON includes",
678+
`
679+
accounts {
680+
foo { include 'foo.json' }
681+
bar { include 'bar.json' }
682+
quux { include 'quux.json' }
683+
}
684+
`,
685+
map[string]string{
686+
"foo.json": `{ "users": [ {"user": "foo"} ] }`,
687+
"bar.json": `{
688+
"users": [ {"user": "bar"} ]
689+
}`,
690+
"quux.json": `{}`,
691+
},
692+
map[string]interface{}{
693+
"accounts": map[string]interface{}{
694+
"foo": map[string]interface{}{
695+
"users": []interface{}{
696+
map[string]interface{}{
697+
"user": "foo",
698+
},
699+
},
700+
},
701+
"bar": map[string]interface{}{
702+
"users": []interface{}{
703+
map[string]interface{}{
704+
"user": "bar",
705+
},
706+
},
707+
},
708+
"quux": map[string]interface{}{},
709+
},
710+
},
711+
},
712+
} {
713+
t.Run(test.name, func(t *testing.T) {
714+
sdir := t.TempDir()
715+
f, err := os.CreateTemp(sdir, "nats.conf-")
716+
if err != nil {
717+
t.Fatal(err)
718+
}
719+
if err := os.WriteFile(f.Name(), []byte(test.input), 066); err != nil {
720+
t.Error(err)
721+
}
722+
if test.includes != nil {
723+
for includeFile, contents := range test.includes {
724+
inf, err := os.Create(filepath.Join(sdir, includeFile))
725+
if err != nil {
726+
t.Fatal(err)
727+
}
728+
if err := os.WriteFile(inf.Name(), []byte(contents), 066); err != nil {
729+
t.Error(err)
730+
}
731+
}
732+
}
733+
m, err := ParseFile(f.Name())
734+
if err != nil {
735+
t.Fatalf("Unexpected error: %v", err)
736+
}
737+
if !reflect.DeepEqual(m, test.expected) {
738+
t.Fatalf("Not Equal:\nReceived: '%+v'\nExpected: '%+v'\n", m, test.expected)
739+
}
740+
})
741+
}
742+
}

0 commit comments

Comments
 (0)