Skip to content

Commit 4b8028e

Browse files
committed
feat: adds non-$ prefixed options for $substitute directive
1 parent ec49170 commit 4b8028e

File tree

3 files changed

+179
-17
lines changed

3 files changed

+179
-17
lines changed

app-config-extensions/src/index.test.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,6 +1053,162 @@ describe('$substitute directive', () => {
10531053

10541054
expect(parsed.toJSON()).toEqual({ foo: 'foo' });
10551055
});
1056+
1057+
it('reads object with name', async () => {
1058+
process.env.FOO = 'foo';
1059+
1060+
const source = new LiteralSource({
1061+
foo: { $substitute: { name: 'FOO' } },
1062+
});
1063+
1064+
const parsed = await source.read([environmentVariableSubstitution()]);
1065+
1066+
expect(parsed.toJSON()).toEqual({ foo: 'foo' });
1067+
});
1068+
1069+
it('fails with name when not defined', async () => {
1070+
const source = new LiteralSource({
1071+
foo: { $substitute: { name: 'FOO' } },
1072+
});
1073+
1074+
await expect(source.read([environmentVariableSubstitution()])).rejects.toThrow();
1075+
});
1076+
1077+
it('uses name when fallback is defined', async () => {
1078+
process.env.FOO = 'foo';
1079+
1080+
const source = new LiteralSource({
1081+
foo: { $substitute: { name: 'FOO', fallback: 'bar' } },
1082+
});
1083+
1084+
const parsed = await source.read([environmentVariableSubstitution()]);
1085+
1086+
expect(parsed.toJSON()).toEqual({ foo: 'foo' });
1087+
});
1088+
1089+
it('uses fallback when name was not found', async () => {
1090+
const source = new LiteralSource({
1091+
foo: { $substitute: { name: 'FOO', fallback: 'bar' } },
1092+
});
1093+
1094+
const parsed = await source.read([environmentVariableSubstitution()]);
1095+
1096+
expect(parsed.toJSON()).toEqual({ foo: 'bar' });
1097+
});
1098+
1099+
it('allows null value when allowNull', async () => {
1100+
const source = new LiteralSource({
1101+
foo: { $substitute: { name: 'FOO', fallback: null, allowNull: true } },
1102+
});
1103+
1104+
const parsed = await source.read([environmentVariableSubstitution()]);
1105+
1106+
expect(parsed.toJSON()).toEqual({ foo: null });
1107+
});
1108+
1109+
it('does not allow number even when allowNull', async () => {
1110+
const source = new LiteralSource({
1111+
foo: { $substitute: { name: 'FOO', fallback: 42, allowNull: true } },
1112+
});
1113+
1114+
await expect(source.read([environmentVariableSubstitution()])).rejects.toThrow();
1115+
});
1116+
1117+
it('parses ints', async () => {
1118+
process.env.FOO = '11';
1119+
1120+
const source = new LiteralSource({
1121+
$substitute: { name: 'FOO', parseInt: true },
1122+
});
1123+
1124+
expect(await source.readToJSON([environmentVariableSubstitution()])).toEqual(11);
1125+
});
1126+
1127+
it('fails when int is invalid', async () => {
1128+
process.env.FOO = 'not a number';
1129+
1130+
const source = new LiteralSource({
1131+
$substitute: { name: 'FOO', parseInt: true },
1132+
});
1133+
1134+
await expect(source.read([environmentVariableSubstitution()])).rejects.toThrow();
1135+
});
1136+
1137+
it('parses float', async () => {
1138+
process.env.FOO = '11.2';
1139+
1140+
const source = new LiteralSource({
1141+
$substitute: { name: 'FOO', parseFloat: true },
1142+
});
1143+
1144+
expect(await source.readToJSON([environmentVariableSubstitution()])).toEqual(11.2);
1145+
});
1146+
1147+
it('fails when float is invalid', async () => {
1148+
process.env.FOO = 'not a number';
1149+
1150+
const source = new LiteralSource({
1151+
$substitute: { name: 'FOO', parseFloat: true },
1152+
});
1153+
1154+
await expect(source.read([environmentVariableSubstitution()])).rejects.toThrow();
1155+
});
1156+
1157+
it('parses boolean = true', async () => {
1158+
process.env.FOO = 'true';
1159+
1160+
const source = new LiteralSource({
1161+
$substitute: { name: 'FOO', parseBool: true },
1162+
});
1163+
1164+
expect(await source.readToJSON([environmentVariableSubstitution()])).toEqual(true);
1165+
});
1166+
1167+
it('parses boolean = 1', async () => {
1168+
process.env.FOO = '1';
1169+
1170+
const source = new LiteralSource({
1171+
$substitute: { name: 'FOO', parseBool: true },
1172+
});
1173+
1174+
expect(await source.readToJSON([environmentVariableSubstitution()])).toEqual(true);
1175+
});
1176+
1177+
it('parses boolean = 0', async () => {
1178+
process.env.FOO = '0';
1179+
1180+
const source = new LiteralSource({
1181+
$substitute: { name: 'FOO', parseBool: true },
1182+
});
1183+
1184+
expect(await source.readToJSON([environmentVariableSubstitution()])).toEqual(false);
1185+
});
1186+
1187+
it('parses boolean = false', async () => {
1188+
process.env.FOO = 'false';
1189+
1190+
const source = new LiteralSource({
1191+
$substitute: { name: 'FOO', parseBool: true },
1192+
});
1193+
1194+
expect(await source.readToJSON([environmentVariableSubstitution()])).toEqual(false);
1195+
});
1196+
1197+
it('doesnt visit fallback if name is defined', async () => {
1198+
const failDirective = forKey('$fail', () => () => {
1199+
throw new Error();
1200+
});
1201+
1202+
process.env.FOO = 'foo';
1203+
1204+
const source = new LiteralSource({
1205+
foo: { $substitute: { name: 'FOO', fallback: { $fail: true } } },
1206+
});
1207+
1208+
const parsed = await source.read([environmentVariableSubstitution(), failDirective]);
1209+
1210+
expect(parsed.toJSON()).toEqual({ foo: 'foo' });
1211+
});
10561212
});
10571213

10581214
describe('$timestamp directive', () => {

app-config-extensions/src/index.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -264,16 +264,14 @@ export function environmentVariableSubstitution(
264264
validateObject(value, [...ctx, key]);
265265
if (Array.isArray(value)) throw new AppConfigError('$substitute was given an array');
266266

267-
const { $name, $fallback, $allowNull, $parseInt, $parseFloat, $parseBool } = value;
267+
const name = (await parse(selectDefined(value.name, value.$name))).toJSON();
268268

269-
const name = (await parse($name)).toJSON();
270-
271-
validateString(name, [...ctx, key, [InObject, '$name']]);
269+
validateString(name, [...ctx, key, [InObject, 'name']]);
272270

273271
const resolvedValue = process.env[name];
274272

275273
if (resolvedValue) {
276-
const parseInt = (await parse($parseInt)).toJSON();
274+
const parseInt = (await parse(selectDefined(value.parseInt, value.$parseInt))).toJSON();
277275

278276
if (parseInt) {
279277
const parsed = Number.parseInt(resolvedValue, 10);
@@ -285,7 +283,7 @@ export function environmentVariableSubstitution(
285283
return parse(parsed, { shouldFlatten: true });
286284
}
287285

288-
const parseFloat = (await parse($parseFloat)).toJSON();
286+
const parseFloat = (await parse(selectDefined(value.parseFloat, value.$parseFloat))).toJSON();
289287

290288
if (parseFloat) {
291289
const parsed = Number.parseFloat(resolvedValue);
@@ -297,7 +295,7 @@ export function environmentVariableSubstitution(
297295
return parse(parsed, { shouldFlatten: true });
298296
}
299297

300-
const parseBool = (await parse($parseBool)).toJSON();
298+
const parseBool = (await parse(selectDefined(value.parseBool, value.$parseBool))).toJSON();
301299

302300
if (parseBool) {
303301
const parsed = resolvedValue.toLowerCase() !== 'false' && resolvedValue !== '0';
@@ -308,17 +306,17 @@ export function environmentVariableSubstitution(
308306
return parse(resolvedValue, { shouldFlatten: true });
309307
}
310308

311-
if ($fallback !== undefined) {
312-
const fallbackValue = (await parse($fallback)).toJSON();
313-
const allowNull = (await parse($allowNull)).toJSON();
309+
if (value.fallback !== undefined || value.$fallback !== undefined) {
310+
const fallback = (await parse(selectDefined(value.fallback, value.$fallback))).toJSON();
311+
const allowNull = (await parse(selectDefined(value.allowNull, value.$allowNull))).toJSON();
314312

315313
if (allowNull) {
316-
validateStringOrNull(fallbackValue, [...ctx, key, [InObject, '$fallback']]);
314+
validateStringOrNull(fallback, [...ctx, key, [InObject, 'fallback']]);
317315
} else {
318-
validateString(fallbackValue, [...ctx, key, [InObject, '$fallback']]);
316+
validateString(fallback, [...ctx, key, [InObject, 'fallback']]);
319317
}
320318

321-
return parse(fallbackValue, { shouldFlatten: true });
319+
return parse(fallback, { shouldFlatten: true });
322320
}
323321

324322
throw new AppConfigError(`$substitute could not find ${name} environment variable`);
@@ -447,3 +445,11 @@ function performAllSubstitutions(text: string, envType?: string): string {
447445

448446
return output;
449447
}
448+
449+
function selectDefined<T>(...args: (T | null | undefined)[]): T | null {
450+
for (const a of args) {
451+
if (a !== undefined) return a;
452+
}
453+
454+
return undefined as any;
455+
}

docs/guide/intro/extensions.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,17 +105,17 @@ username: { $substitute: '$USER' }
105105
Works essentially the same way as [bash variable substitution](https://tldp.org/LDP/abs/html/parameter-substitution.html)
106106
(including fallback syntax).
107107

108-
The other form of substitution is via `$name`:
108+
The other form of substitution is via `name`:
109109

110110
```yaml
111111
username:
112112
$substitute:
113113
# Look for $USER, use 'no-user' as fallback value
114-
$name: 'USER'
115-
$fallback: 'no-user'
114+
name: 'USER'
115+
fallback: 'no-user'
116116
```
117117

118-
Use `$allowNull: true` if your fallback is allowed to be nullable.
118+
Use `allowNull: true` if your fallback is allowed to be nullable.
119119

120120
## Decryption
121121

0 commit comments

Comments
 (0)