Skip to content

Commit c6acf18

Browse files
Support comment in json key value (#205)
* support comment in json key value * update testcase * strip comment only when normal parse failed
1 parent 30e4d83 commit c6acf18

File tree

5 files changed

+127
-19
lines changed

5 files changed

+127
-19
lines changed

package-lock.json

Lines changed: 16 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"dependencies": {
5858
"@azure/app-configuration": "^1.6.1",
5959
"@azure/identity": "^4.2.1",
60-
"@azure/keyvault-secrets": "^4.7.0"
60+
"@azure/keyvault-secrets": "^4.7.0",
61+
"jsonc-parser": "^3.3.1"
6162
}
6263
}

rollup.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export default [
1111
"@azure/identity",
1212
"crypto",
1313
"dns/promises",
14+
"jsonc-parser",
1415
"@microsoft/feature-management"
1516
],
1617
input: "src/index.ts",

src/JsonKeyValueAdapter.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT license.
33

44
import { ConfigurationSetting, featureFlagContentType, secretReferenceContentType } from "@azure/app-configuration";
5+
import { stripComments } from "jsonc-parser";
56
import { parseContentType, isJsonContentType } from "./common/contentType.js";
67
import { IKeyValueAdapter } from "./IKeyValueAdapter.js";
78

@@ -25,18 +26,35 @@ export class JsonKeyValueAdapter implements IKeyValueAdapter {
2526
async processKeyValue(setting: ConfigurationSetting): Promise<[string, unknown]> {
2627
let parsedValue: unknown;
2728
if (setting.value !== undefined) {
28-
try {
29-
parsedValue = JSON.parse(setting.value);
30-
} catch (error) {
31-
parsedValue = setting.value;
29+
const parseResult = this.#tryParseJson(setting.value);
30+
if (parseResult.success) {
31+
parsedValue = parseResult.result;
32+
} else {
33+
// Try parsing with comments stripped
34+
const parseWithoutCommentsResult = this.#tryParseJson(stripComments(setting.value));
35+
if (parseWithoutCommentsResult.success) {
36+
parsedValue = parseWithoutCommentsResult.result;
37+
} else {
38+
// If still not valid JSON, return the original value
39+
parsedValue = setting.value;
40+
}
3241
}
33-
} else {
34-
parsedValue = setting.value;
3542
}
3643
return [setting.key, parsedValue];
3744
}
3845

3946
async onChangeDetected(): Promise<void> {
4047
return;
4148
}
49+
50+
#tryParseJson(value: string): { success: true; result: unknown } | { success: false } {
51+
try {
52+
return { success: true, result: JSON.parse(value) };
53+
} catch (error) {
54+
if (error instanceof SyntaxError) {
55+
return { success: false };
56+
}
57+
throw error;
58+
}
59+
}
4260
}

test/json.test.ts

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ describe("json", function () {
6060
createMockedJsonKeyValue("json.settings.object", "{}"),
6161
createMockedJsonKeyValue("json.settings.array", "[]"),
6262
createMockedJsonKeyValue("json.settings.number", "8"),
63-
createMockedJsonKeyValue("json.settings.string", "string"),
63+
createMockedJsonKeyValue("json.settings.string", "\"string\""),
6464
createMockedJsonKeyValue("json.settings.false", "false"),
6565
createMockedJsonKeyValue("json.settings.true", "true"),
6666
createMockedJsonKeyValue("json.settings.null", "null"),
@@ -88,4 +88,87 @@ describe("json", function () {
8888
expect(settings.get("json.settings.emptyString")).eq("", "is empty string");
8989
expect(settings.get("json.settings.illegalString")).eq("[unclosed", "is illegal string");
9090
});
91+
92+
it("should load json values with comments", async () => {
93+
// Test various comment styles and positions
94+
const mixedCommentStylesValue = `{
95+
// Single line comment at start
96+
"ApiSettings": {
97+
"BaseUrl": "https://api.example.com", // Inline single line
98+
/* Multi-line comment
99+
spanning multiple lines */
100+
"ApiKey": "secret-key",
101+
"Endpoints": [
102+
// Comment before array element
103+
"/users",
104+
/* Comment between elements */
105+
"/orders",
106+
"/products" // Comment after element
107+
]
108+
},
109+
// Test edge cases
110+
"StringWithSlashes": "This is not a // comment",
111+
"StringWithStars": "This is not a /* comment */",
112+
"UrlValue": "https://example.com/path", // This is a real comment
113+
"EmptyComment": "value", //
114+
/**/
115+
"AfterEmptyComment": "value2"
116+
/* Final multi-line comment */
117+
}`;
118+
119+
// Test invalid JSON with comments
120+
const invalidJsonWithCommentsValue = `// This is a comment
121+
{ invalid json structure
122+
// Another comment
123+
missing quotes and braces`;
124+
125+
// Test only comments (should be invalid JSON)
126+
const onlyCommentsValue = `
127+
// Just comments
128+
/* No actual content */
129+
`;
130+
131+
const keyValues = [
132+
createMockedJsonKeyValue("MixedCommentStyles", mixedCommentStylesValue),
133+
createMockedJsonKeyValue("InvalidJsonWithComments", invalidJsonWithCommentsValue),
134+
createMockedJsonKeyValue("OnlyComments", onlyCommentsValue)
135+
];
136+
137+
mockAppConfigurationClientListConfigurationSettings([keyValues]);
138+
139+
const connectionString = createMockedConnectionString();
140+
const settings = await load(connectionString);
141+
expect(settings).not.undefined;
142+
143+
// Verify mixed comment styles are properly parsed
144+
const mixedConfig = settings.get<any>("MixedCommentStyles");
145+
expect(mixedConfig).not.undefined;
146+
expect(mixedConfig.ApiSettings).not.undefined;
147+
expect(mixedConfig.ApiSettings.BaseUrl).eq("https://api.example.com");
148+
expect(mixedConfig.ApiSettings.ApiKey).eq("secret-key");
149+
expect(mixedConfig.ApiSettings.Endpoints).not.undefined;
150+
expect(Array.isArray(mixedConfig.ApiSettings.Endpoints)).eq(true);
151+
expect(mixedConfig.ApiSettings.Endpoints[0]).eq("/users");
152+
expect(mixedConfig.ApiSettings.Endpoints[1]).eq("/orders");
153+
expect(mixedConfig.ApiSettings.Endpoints[2]).eq("/products");
154+
155+
// Verify edge cases where comment-like text appears in strings
156+
expect(mixedConfig.StringWithSlashes).eq("This is not a // comment");
157+
expect(mixedConfig.StringWithStars).eq("This is not a /* comment */");
158+
expect(mixedConfig.UrlValue).eq("https://example.com/path");
159+
expect(mixedConfig.EmptyComment).eq("value");
160+
expect(mixedConfig.AfterEmptyComment).eq("value2");
161+
162+
// Invalid JSON should fall back to string value
163+
const invalidConfig = settings.get("InvalidJsonWithComments");
164+
expect(invalidConfig).not.undefined;
165+
expect(typeof invalidConfig).eq("string");
166+
expect(invalidConfig).eq(invalidJsonWithCommentsValue);
167+
168+
// Only comments should be treated as string value (invalid JSON)
169+
const onlyCommentsConfig = settings.get("OnlyComments");
170+
expect(onlyCommentsConfig).not.undefined;
171+
expect(typeof onlyCommentsConfig).eq("string");
172+
expect(onlyCommentsConfig).eq(onlyCommentsValue);
173+
});
91174
});

0 commit comments

Comments
 (0)