Skip to content

Commit f941c66

Browse files
committed
feat(#134): adds $parse{Bool|Float|Int} directives
1 parent 8005c36 commit f941c66

File tree

4 files changed

+191
-0
lines changed

4 files changed

+191
-0
lines changed

app-config-default-extensions/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module.exports = {
1818
tryDirective,
1919
ifDirective,
2020
eqDirective,
21+
parseDirective,
2122
hiddenDirective,
2223
envDirective,
2324
envVarDirective,
@@ -37,6 +38,7 @@ module.exports = {
3738
tryDirective(),
3839
ifDirective(),
3940
eqDirective(),
41+
parseDirective(),
4042
hiddenDirective(),
4143
v1Compat(),
4244
envDirective(aliases, environmentOverride, environmentSourceNames),

app-config-extensions/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export { timestampDirective } from './timestamp-directive';
1212
export { envVarDirective } from './env-var-directive';
1313
export { substituteDirective } from './substitute-directive';
1414
export { substituteDirective as environmentVariableSubstitution } from './substitute-directive';
15+
export { parseDirective } from './parse-directive';
1516

1617
/** Marks all values recursively as fromSecrets, so they do not trigger schema errors */
1718
export function markAllValuesAsSecret(): ParsingExtension {
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { LiteralSource } from '@app-config/core';
2+
import { parseDirective } from './parse-directive';
3+
4+
describe('$parseBool', () => {
5+
it('should parse an existing boolean value', async () => {
6+
await expect(
7+
new LiteralSource({ $parseBool: true }).readToJSON([parseDirective()]),
8+
).resolves.toBe(true);
9+
10+
await expect(
11+
new LiteralSource({ $parseBool: false }).readToJSON([parseDirective()]),
12+
).resolves.toBe(false);
13+
});
14+
15+
it('should parse string values', async () => {
16+
await expect(
17+
new LiteralSource({ $parseBool: 'true' }).readToJSON([parseDirective()]),
18+
).resolves.toBe(true);
19+
20+
await expect(
21+
new LiteralSource({ $parseBool: 'false' }).readToJSON([parseDirective()]),
22+
).resolves.toBe(false);
23+
24+
await expect(
25+
new LiteralSource({ $parseBool: '1' }).readToJSON([parseDirective()]),
26+
).resolves.toBe(true);
27+
28+
await expect(
29+
new LiteralSource({ $parseBool: '0' }).readToJSON([parseDirective()]),
30+
).resolves.toBe(false);
31+
32+
await expect(
33+
new LiteralSource({ $parseBool: 'null' }).readToJSON([parseDirective()]),
34+
).resolves.toBe(false);
35+
});
36+
37+
it('should parse null as false', async () => {
38+
await expect(
39+
new LiteralSource({ $parseBool: null }).readToJSON([parseDirective()]),
40+
).resolves.toBe(false);
41+
});
42+
43+
it('should parse numbers', async () => {
44+
await expect(
45+
new LiteralSource({ $parseBool: 1 }).readToJSON([parseDirective()]),
46+
).resolves.toBe(true);
47+
48+
await expect(
49+
new LiteralSource({ $parseBool: 0 }).readToJSON([parseDirective()]),
50+
).resolves.toBe(false);
51+
});
52+
});
53+
54+
describe('$parseFloat', () => {
55+
it('should parse an existing number value', async () => {
56+
await expect(
57+
new LiteralSource({ $parseFloat: 12.12 }).readToJSON([parseDirective()]),
58+
).resolves.toBe(12.12);
59+
60+
await expect(
61+
new LiteralSource({ $parseFloat: 0 }).readToJSON([parseDirective()]),
62+
).resolves.toBe(0);
63+
});
64+
65+
it('should parse string values', async () => {
66+
await expect(
67+
new LiteralSource({ $parseFloat: '12.12' }).readToJSON([parseDirective()]),
68+
).resolves.toBe(12.12);
69+
70+
await expect(
71+
new LiteralSource({ $parseFloat: '0' }).readToJSON([parseDirective()]),
72+
).resolves.toBe(0);
73+
});
74+
75+
it('should reject invalid values', async () => {
76+
await expect(
77+
new LiteralSource({ $parseFloat: 'not a number' }).readToJSON([parseDirective()]),
78+
).rejects.toThrow('Failed to $parseFloat');
79+
80+
await expect(
81+
new LiteralSource({ $parseFloat: null }).readToJSON([parseDirective()]),
82+
).rejects.toThrow('Failed to $parseFloat');
83+
84+
await expect(
85+
new LiteralSource({ $parseFloat: [] }).readToJSON([parseDirective()]),
86+
).rejects.toThrow('Failed to $parseFloat');
87+
});
88+
});
89+
90+
describe('$parseInt', () => {
91+
it('should parse an existing number value', async () => {
92+
await expect(
93+
new LiteralSource({ $parseInt: 12.12 }).readToJSON([parseDirective()]),
94+
).resolves.toBe(12);
95+
96+
await expect(
97+
new LiteralSource({ $parseInt: 0 }).readToJSON([parseDirective()]),
98+
).resolves.toBe(0);
99+
});
100+
101+
it('should parse string values', async () => {
102+
await expect(
103+
new LiteralSource({ $parseInt: '12.12' }).readToJSON([parseDirective()]),
104+
).resolves.toBe(12);
105+
106+
await expect(
107+
new LiteralSource({ $parseInt: '0' }).readToJSON([parseDirective()]),
108+
).resolves.toBe(0);
109+
});
110+
111+
it('should reject invalid values', async () => {
112+
await expect(
113+
new LiteralSource({ $parseInt: 'not a number' }).readToJSON([parseDirective()]),
114+
).rejects.toThrow('Failed to $parseInt');
115+
116+
await expect(
117+
new LiteralSource({ $parseInt: null }).readToJSON([parseDirective()]),
118+
).rejects.toThrow('Failed to $parseInt');
119+
120+
await expect(
121+
new LiteralSource({ $parseInt: [] }).readToJSON([parseDirective()]),
122+
).rejects.toThrow('Failed to $parseInt');
123+
});
124+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { ParsingExtension } from '@app-config/core';
2+
import { AppConfigError } from '@app-config/core';
3+
import { named, forKey, validateOptions, composeExtensions } from '@app-config/extension-utils';
4+
5+
/** Provides string parsing */
6+
export function parseDirective(): ParsingExtension {
7+
return named(
8+
'$parse',
9+
composeExtensions([
10+
forKey('$parseBool', (value) => async (parse) => {
11+
const parsed = await parse(value);
12+
const primitive = parsed.asPrimitive();
13+
14+
if (typeof primitive === 'string') {
15+
return parse(primitive.toLowerCase() === 'true' || primitive.toLowerCase() === '1', {
16+
shouldFlatten: true,
17+
});
18+
}
19+
20+
return parse(!!parsed.toJSON(), { shouldFlatten: true });
21+
}),
22+
forKey('$parseFloat', (value) => async (parse) => {
23+
const parsed = await parse(value);
24+
const primitive = parsed.asPrimitive();
25+
26+
if (typeof primitive === 'number') {
27+
return parse(primitive, { shouldFlatten: true });
28+
}
29+
30+
if (typeof primitive === 'string') {
31+
const parsed = Number.parseFloat(primitive);
32+
33+
if (Number.isNaN(parsed)) {
34+
throw new AppConfigError(`Failed to $parseFloat(${primitive})`);
35+
}
36+
37+
return parse(parsed, { shouldFlatten: true });
38+
}
39+
40+
throw new AppConfigError(`Failed to $parseFloat(${parsed.toJSON()}) - invalid input type`);
41+
}),
42+
forKey('$parseInt', (value) => async (parse) => {
43+
const parsed = await parse(value);
44+
const primitive = parsed.asPrimitive();
45+
46+
if (typeof primitive === 'number') {
47+
return parse(primitive | 0, { shouldFlatten: true });
48+
}
49+
50+
if (typeof primitive === 'string') {
51+
const parsed = Number.parseInt(primitive, 10);
52+
53+
if (Number.isNaN(parsed)) {
54+
throw new AppConfigError(`Failed to $parseInt(${primitive})`);
55+
}
56+
57+
return parse(parsed, { shouldFlatten: true });
58+
}
59+
60+
throw new AppConfigError(`Failed to $parseInt(${parsed.toJSON()}) - invalid input type`);
61+
}),
62+
]),
63+
);
64+
}

0 commit comments

Comments
 (0)