Skip to content

Commit b501879

Browse files
JSON with comment support (#43)
* jsonc support * update * update
1 parent d6c5297 commit b501879

File tree

4 files changed

+310
-6
lines changed

4 files changed

+310
-6
lines changed

azureappconfiguration/azureappconfiguration.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"sync"
2828
"sync/atomic"
2929

30+
"github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration/internal/jsonc"
3031
"github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration/internal/refresh"
3132
"github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration/internal/tracing"
3233
"github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration/internal/tree"
@@ -398,8 +399,13 @@ func (azappcfg *AzureAppConfiguration) loadKeyValues(ctx context.Context, settin
398399
if isJsonContentType(setting.ContentType) {
399400
var v any
400401
if err := json.Unmarshal([]byte(*setting.Value), &v); err != nil {
401-
log.Printf("Failed to unmarshal JSON value: key=%s, error=%s", *setting.Key, err.Error())
402-
continue
402+
// If the value is not valid JSON, try to remove comments and parse again
403+
if err := json.Unmarshal(jsonc.StripComments([]byte(*setting.Value)), &v); err != nil {
404+
// If still invalid, log the error and treat it as a plain string
405+
log.Printf("Failed to unmarshal JSON value: key=%s, error=%s", *setting.Key, err.Error())
406+
kvSettings[trimmedKey] = setting.Value
407+
continue
408+
}
403409
}
404410
kvSettings[trimmedKey] = v
405411
if isAIConfigurationContentType(setting.ContentType) {

azureappconfiguration/azureappconfiguration_test.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -271,11 +271,10 @@ func TestLoadKeyValues_InvalidJson(t *testing.T) {
271271
err := azappcfg.loadKeyValues(ctx, mockClient)
272272
assert.NoError(t, err)
273273

274-
assert.Len(t, azappcfg.keyValues, 1)
274+
assert.Len(t, azappcfg.keyValues, 2)
275275
assert.Equal(t, &value1, azappcfg.keyValues["key1"])
276-
// The invalid JSON key should be skipped
277-
_, exists := azappcfg.keyValues["key2"]
278-
assert.False(t, exists)
276+
// The invalid JSON key should be treated as a plain string
277+
assert.Equal(t, &value2, azappcfg.keyValues["key2"])
279278
}
280279

281280
func TestDeduplicateSelectors(t *testing.T) {
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
// Package jsonc provides support for parsing JSON with comments (JSONC).
5+
// This package extends standard JSON support to handle line comments (//) and block comments (/* */).
6+
package jsonc
7+
8+
// StripComments removes comments from JSONC while preserving string literals.
9+
func StripComments(data []byte) []byte {
10+
var result []byte
11+
var inString bool
12+
var escaped bool
13+
14+
for i := 0; i < len(data); i++ {
15+
char := data[i]
16+
17+
if escaped {
18+
// Previous character was a backslash, so this character is escaped
19+
result = append(result, char)
20+
escaped = false
21+
continue
22+
}
23+
24+
if inString {
25+
result = append(result, char)
26+
if char == '\\' {
27+
escaped = true
28+
} else if char == '"' {
29+
inString = false
30+
}
31+
continue
32+
}
33+
34+
// Not in a string
35+
if char == '"' {
36+
inString = true
37+
result = append(result, char)
38+
continue
39+
}
40+
// Check for line comment
41+
if char == '/' && i+1 < len(data) && data[i+1] == '/' {
42+
// Skip until end of line
43+
i++ // skip the second '/'
44+
for i < len(data) && data[i] != '\n' && data[i] != '\r' {
45+
i++
46+
}
47+
// Include the newline character to preserve line numbers, but step back one
48+
// because the for loop will increment i again
49+
if i < len(data) {
50+
result = append(result, data[i])
51+
}
52+
continue
53+
}
54+
55+
// Check for block comment
56+
if char == '/' && i+1 < len(data) && data[i+1] == '*' {
57+
// Replace /* with spaces
58+
result = append(result, ' ', ' ')
59+
i += 2 // skip /*
60+
61+
// Find closing */ and replace content with spaces, preserving newlines
62+
for i < len(data) {
63+
if data[i] == '*' && i+1 < len(data) && data[i+1] == '/' {
64+
// Replace */ with spaces
65+
result = append(result, ' ', ' ')
66+
i += 2 // skip */
67+
break
68+
}
69+
// Preserve newlines and carriage returns to maintain line numbers
70+
if data[i] == '\n' || data[i] == '\r' {
71+
result = append(result, data[i])
72+
} else {
73+
// Replace other characters with spaces to preserve column positions
74+
result = append(result, ' ')
75+
}
76+
i++
77+
}
78+
// Step back one because the for loop will increment i again
79+
i--
80+
continue
81+
}
82+
83+
// Regular character, not in string, not a comment
84+
result = append(result, char)
85+
}
86+
87+
return result
88+
}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package jsonc
5+
6+
import (
7+
"testing"
8+
)
9+
10+
func TestStripComments(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
input string
14+
expected string
15+
}{
16+
{
17+
name: "no comments",
18+
input: `{"name": "value"}`,
19+
expected: `{"name": "value"}`,
20+
},
21+
{
22+
name: "line comment",
23+
input: `{"name": "value"} // this is a comment`,
24+
expected: `{"name": "value"} `,
25+
},
26+
{
27+
name: "block comment",
28+
input: `{"name": /* comment */ "value"}`,
29+
expected: `{"name": "value"}`,
30+
},
31+
{
32+
name: "comment in string should be preserved",
33+
input: `{"comment": "This // is not a comment"}`,
34+
expected: `{"comment": "This // is not a comment"}`,
35+
},
36+
{
37+
name: "escaped quote in string",
38+
input: `{"escaped": "She said \"Hello\""}`,
39+
expected: `{"escaped": "She said \"Hello\""}`,
40+
},
41+
{
42+
name: "multiline block comment",
43+
input: `{
44+
/* This is a
45+
multiline comment */
46+
"name": "value"
47+
}`,
48+
expected: `{
49+
50+
51+
"name": "value"
52+
}`,
53+
},
54+
// Edge cases
55+
{
56+
name: "empty string",
57+
input: ``,
58+
expected: ``,
59+
},
60+
{
61+
name: "only whitespace",
62+
input: `
63+
`,
64+
expected: `
65+
`,
66+
},
67+
{
68+
name: "only line comment",
69+
input: `// just a comment`,
70+
expected: ``,
71+
},
72+
{
73+
name: "only block comment",
74+
input: `/* just a comment */`,
75+
expected: ` `,
76+
},
77+
{
78+
name: "multiple line comments",
79+
input: `// comment 1
80+
// comment 2
81+
{"name": "value"} // comment 3`,
82+
expected: `
83+
84+
{"name": "value"} `,
85+
},
86+
{
87+
name: "nested-like comments in strings",
88+
input: `{"path": "/* not a comment */", "url": "http://example.com"}`,
89+
expected: `{"path": "/* not a comment */", "url": "http://example.com"}`,
90+
},
91+
{
92+
name: "comment markers in strings with escapes",
93+
input: `{"msg": "He said \"// not a comment\"", "note": "File: C:\\temp\\*test*.txt"}`,
94+
expected: `{"msg": "He said \"// not a comment\"", "note": "File: C:\\temp\\*test*.txt"}`,
95+
},
96+
{
97+
name: "block comment at start of line",
98+
input: `{
99+
/* comment */
100+
"name": "value"
101+
}`,
102+
expected: `{
103+
104+
"name": "value"
105+
}`,
106+
},
107+
{
108+
name: "block comment at end of line",
109+
input: `{
110+
"name": "value" /* comment */
111+
}`,
112+
expected: `{
113+
"name": "value"
114+
}`,
115+
},
116+
{
117+
name: "consecutive comments",
118+
input: `{"name": "value"} // comment1 /* still comment */`,
119+
expected: `{"name": "value"} `,
120+
},
121+
{
122+
name: "comment with forward slash in string",
123+
input: `{"url": "https://example.com/path"} // Real comment`,
124+
expected: `{"url": "https://example.com/path"} `,
125+
},
126+
{
127+
name: "incomplete block comment at end",
128+
input: `{"name": "value"} /* incomplete comment`,
129+
expected: `{"name": "value"} `,
130+
},
131+
{
132+
name: "incomplete line comment (just //)",
133+
input: `{"name": "value"} //`,
134+
expected: `{"name": "value"} `,
135+
},
136+
{
137+
name: "block comment with nested /* inside",
138+
input: `{"name": /* outer /* inner */ "value"}`,
139+
expected: `{"name": "value"}`,
140+
},
141+
{
142+
name: "multiple consecutive slashes",
143+
input: `{"name": "value"} /// triple slash comment`,
144+
expected: `{"name": "value"} `,
145+
},
146+
{
147+
name: "block comment with asterisks inside",
148+
input: `{"name": /* comment with * asterisks ** */ "value"}`,
149+
expected: `{"name": "value"}`,
150+
},
151+
{
152+
name: "string with backslash escape sequences",
153+
input: `{"path": "C:\\Users\\test\\file.txt", "newline": "line1\nline2"} // Comment`,
154+
expected: `{"path": "C:\\Users\\test\\file.txt", "newline": "line1\nline2"} `,
155+
},
156+
{
157+
name: "comment immediately after string quote",
158+
input: `{"name": "value"/* comment */, "other": "data"}`,
159+
expected: `{"name": "value" , "other": "data"}`,
160+
},
161+
{
162+
name: "comment between key and colon",
163+
input: `{"name" /* comment */ : "value"}`,
164+
expected: `{"name" : "value"}`,
165+
},
166+
{
167+
name: "comment between colon and value",
168+
input: `{"name": /* comment */ "value"}`,
169+
expected: `{"name": "value"}`,
170+
},
171+
{
172+
name: "unicode in comments and strings",
173+
input: `{"emoji": "😀🎉", "note": "test"} // Comment with émojis 🚀`,
174+
expected: `{"emoji": "😀🎉", "note": "test"} `,
175+
},
176+
{
177+
name: "windows line endings with comments",
178+
input: "{\r\n\t\"name\": \"value\" // comment\r\n}",
179+
expected: "{\r\n\t\"name\": \"value\" \r\n}",
180+
},
181+
{
182+
name: "mixed line endings",
183+
input: "{\n\t\"name\": \"value\" // comment\r\n\t\"other\": \"data\"\r}",
184+
expected: "{\n\t\"name\": \"value\" \r\n\t\"other\": \"data\"\r}",
185+
},
186+
{
187+
name: "comment at very end of input",
188+
input: `{"name": "value"}//`,
189+
expected: `{"name": "value"}`,
190+
},
191+
{
192+
name: "string with escaped backslash before quote",
193+
input: `{"path": "C:\\test\\"} // Comment`,
194+
expected: `{"path": "C:\\test\\"} `,
195+
},
196+
{
197+
name: "multiple escaped quotes in string",
198+
input: `{"msg": "Say \"Hello\" and \"Goodbye\""} /* comment */`,
199+
expected: `{"msg": "Say \"Hello\" and \"Goodbye\""} `,
200+
},
201+
}
202+
203+
for _, tt := range tests {
204+
t.Run(tt.name, func(t *testing.T) {
205+
result := StripComments([]byte(tt.input))
206+
if string(result) != tt.expected {
207+
t.Errorf("stripComments() = %q, want %q", string(result), tt.expected)
208+
}
209+
})
210+
}
211+
}

0 commit comments

Comments
 (0)