Skip to content

Commit 6304de1

Browse files
committed
refactor(#129): changes parsing extensions to pass through some hierarchical context
1 parent cafaa98 commit 6304de1

File tree

7 files changed

+58
-60
lines changed

7 files changed

+58
-60
lines changed

app-config-core/src/config-source.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class FailingSource extends ConfigSource {
1515
}
1616
}
1717

18-
const flattenExtension: ParsingExtension = (value, [_, key]) => {
18+
const flattenExtension: ParsingExtension = (value, [[_, key]]) => {
1919
if (key === '$flatten') {
2020
return (parse) => parse(value, { shouldFlatten: true });
2121
}

app-config-core/src/parsed-value.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describe('parseValue', () => {
4242
return (parse) => parse(value, { marked: true });
4343
};
4444

45-
const markKeyExtension: ParsingExtension = (value, [keyType, key]) => {
45+
const markKeyExtension: ParsingExtension = (value, [[keyType, key]]) => {
4646
if (keyType === InObject && key === '$mark') {
4747
return (parse) => parse(value, { shouldFlatten: true, marked: true });
4848
}
@@ -60,23 +60,23 @@ describe('parseValue', () => {
6060
return false;
6161
};
6262

63-
const secretExtension: ParsingExtension = (_, [keyType, key]) => {
63+
const secretExtension: ParsingExtension = (_, [[keyType, key]]) => {
6464
if (keyType === InObject && key === '$secret') {
6565
return (parse) => parse('revealed!', { shouldFlatten: true });
6666
}
6767

6868
return false;
6969
};
7070

71-
const mergeExtension: ParsingExtension = (value, [keyType, key]) => {
71+
const mergeExtension: ParsingExtension = (value, [[keyType, key]]) => {
7272
if (keyType === InObject && key === '$merge') {
7373
return (parse) => parse(value, { shouldMerge: true });
7474
}
7575

7676
return false;
7777
};
7878

79-
const overrideExtension: ParsingExtension = (value, [keyType, key]) => {
79+
const overrideExtension: ParsingExtension = (value, [[keyType, key]]) => {
8080
if (keyType === InObject && key === '$override') {
8181
return (parse) => parse(value, { shouldOverride: true });
8282
}

app-config-core/src/parsed-value.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,19 @@ export type ParsingExtensionKey =
1717
| [typeof InArray, number]
1818
| [typeof Root];
1919

20-
export type ParsingExtension = (
21-
value: Json,
22-
key: ParsingExtensionKey,
23-
context: ParsingExtensionKey[],
24-
) => false | ParsingExtensionTransform;
20+
export type ParsingContext = Record<string, string>;
21+
22+
export type ParsingExtension<T extends Json = Json> = (
23+
value: T,
24+
parentKeys: ParsingExtensionKey[],
25+
context: ParsingContext,
26+
) => ParsingExtensionTransform | false;
2527

2628
export type ParsingExtensionTransform = (
2729
parse: (
2830
value: Json,
2931
metadata?: ParsedValueMetadata,
32+
context?: ParsingContext,
3033
source?: ConfigSource,
3134
extensions?: ParsingExtension[],
3235
) => Promise<ParsedValue>,
@@ -339,23 +342,22 @@ export async function parseValue(
339342
source: ConfigSource,
340343
extensions: ParsingExtension[] = [],
341344
metadata: ParsedValueMetadata = {},
345+
context: ParsingContext = {},
342346
): Promise<ParsedValue> {
343-
return parseValueInner(value, source, extensions, metadata, [[Root]], value);
347+
return parseValueInner(value, source, extensions, metadata, context, [[Root]], value);
344348
}
345349

346350
async function parseValueInner(
347351
value: Json,
348352
source: ConfigSource,
349353
extensions: ParsingExtension[],
350354
metadata: ParsedValueMetadata = {},
351-
context: ParsingExtensionKey[],
355+
context: ParsingContext = {},
356+
parentKeys: ParsingExtensionKey[],
352357
root: Json,
353358
parent?: JsonObject | Json[],
354359
visitedExtensions: ParsingExtension[] = [],
355360
): Promise<ParsedValue> {
356-
const [currentKey] = context.slice(-1);
357-
const contextualKeys = context.slice(0, context.length - 1);
358-
359361
let applicableExtension: ParsingExtensionTransform | undefined;
360362

361363
// before anything else, we check for parsing extensions that should be applied
@@ -369,7 +371,7 @@ async function parseValueInner(
369371
// we track visitedExtensions so that calling `parse` in an extension doesn't hit that same extension with the same value
370372
if (visitedExtensions.includes(extension)) continue;
371373

372-
const applicable = extension(value, currentKey, contextualKeys);
374+
const applicable = extension(value, parentKeys, context);
373375

374376
if (applicable && !applicableExtension) {
375377
applicableExtension = applicable;
@@ -378,18 +380,20 @@ async function parseValueInner(
378380
}
379381

380382
if (applicableExtension) {
381-
const parse = (
382-
inner: Json,
383-
metadataOverride?: ParsedValueMetadata,
384-
sourceOverride?: ConfigSource,
385-
extensionsOverride?: ParsingExtension[],
383+
const parse: Parameters<ParsingExtensionTransform>[0] = (
384+
inner,
385+
metadataOverride,
386+
contextOverride,
387+
sourceOverride,
388+
extensionsOverride,
386389
) =>
387390
parseValueInner(
388391
inner,
389392
sourceOverride ?? source,
390393
extensionsOverride ?? extensions,
391394
{ ...metadata, ...metadataOverride },
392-
context,
395+
{ ...context, ...contextOverride },
396+
parentKeys,
393397
root,
394398
parent,
395399
visitedExtensions,
@@ -407,7 +411,8 @@ async function parseValueInner(
407411
source,
408412
extensions,
409413
undefined,
410-
context.concat([[InArray, index]]),
414+
context,
415+
[[InArray, index], ...parentKeys],
411416
root,
412417
value,
413418
);
@@ -432,7 +437,8 @@ async function parseValueInner(
432437
source,
433438
extensions,
434439
undefined,
435-
context.concat([[InObject, key]]),
440+
context,
441+
[[InObject, key], ...parentKeys],
436442
root,
437443
value,
438444
);

app-config-default-extensions/tsconfig.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
"include": ["src"],
77
"exclude": ["node_modules"],
88
"references": [
9-
{ "path": "../app-config-core" },
10-
{ "path": "../app-config-v1-compat" },
11-
{ "path": "../app-config-git" }
9+
{ "path": "../app-config-core" }
1210
]
1311
}

app-config-extension-utils/src/index.ts

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
import type {
2-
ParsingExtension,
3-
ParsingExtensionKey,
4-
ParsingExtensionTransform,
5-
} from '@app-config/core';
1+
import type { ParsingExtension, ParsingExtensionKey } from '@app-config/core';
62
import { parseValue, Root, AppConfigError } from '@app-config/core';
3+
import { Json } from '@app-config/utils';
74
import { SchemaBuilder } from '@serafin/schema-builder';
85

96
export function composeExtensions(extensions: ParsingExtension[]): ParsingExtension {
10-
return (value, [k]) => {
7+
return (value, [[k]]) => {
118
if (k !== Root) return false;
129

1310
return (_, __, source) => parseValue(value, source, extensions, { shouldFlatten: true });
@@ -28,9 +25,9 @@ export function forKey(
2825
return key === k;
2926
};
3027

31-
return (value, ctxKey, ctx) => {
32-
if (shouldApply(ctxKey)) {
33-
return parsingExtension(value, ctxKey, ctx);
28+
return (value, parentKeys, context) => {
29+
if (shouldApply(parentKeys[0])) {
30+
return parsingExtension(value, parentKeys, context);
3431
}
3532

3633
return false;
@@ -39,18 +36,14 @@ export function forKey(
3936

4037
export class ParsingExtensionInvalidOptions extends AppConfigError {}
4138

42-
export function validateOptions<T>(
39+
export function validateOptions<T extends Json>(
4340
builder: (builder: typeof SchemaBuilder) => SchemaBuilder<T>,
44-
extension: (
45-
value: T,
46-
key: ParsingExtensionKey,
47-
context: ParsingExtensionKey[],
48-
) => ParsingExtensionTransform | false,
41+
extension: ParsingExtension<T>,
4942
{ lazy = false }: { lazy?: boolean } = {},
5043
): ParsingExtension {
5144
const validate: ValidationFunction<T> = validationFunction(builder);
5245

53-
return (value, ctxKey, ctx) => {
46+
return (value, parentKeys, context) => {
5447
return async (parse, ...args) => {
5548
let valid: unknown;
5649

@@ -60,9 +53,9 @@ export function validateOptions<T>(
6053
valid = (await parse(value)).toJSON();
6154
}
6255

63-
validate(valid, [...ctx, ctxKey]);
56+
validate(valid, parentKeys);
6457

65-
const call = extension(valid, ctxKey, ctx);
58+
const call = extension(valid, parentKeys, context);
6659

6760
if (call) {
6861
return call(parse, ...args);
@@ -91,7 +84,8 @@ export function validationFunction<T>(
9184
const message = error instanceof Error ? error.message : 'unknown';
9285

9386
const parents =
94-
ctx
87+
[...ctx]
88+
.reverse()
9589
.map(([, k]) => k)
9690
.filter((v) => !!v)
9791
.join('.') || 'root';

app-config-extensions/src/index.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function markAllValuesAsSecret(): ParsingExtension {
3131

3232
/** When a key $$foo is seen, change it to be $foo and mark with meta property fromEscapedDirective */
3333
export function unescape$Directives(): ParsingExtension {
34-
return (value, [_, key]) => {
34+
return (value, [[_, key]]) => {
3535
if (typeof key === 'string' && key.startsWith('$$')) {
3636
return async (parse) => {
3737
return parse(value, { rewriteKey: key.slice(1), fromEscapedDirective: true });
@@ -141,9 +141,9 @@ export function extendsSelfDirective(): ParsingExtension {
141141
stringSchema(),
142142
);
143143

144-
return forKey('$extendsSelf', (input, key, ctx) => async (parse, _, __, ___, root) => {
144+
return forKey('$extendsSelf', (input, parentKeys) => async (parse, _, __, ___, root) => {
145145
const value = (await parse(input)).toJSON();
146-
validate(value, [...ctx, key]);
146+
validate(value, parentKeys);
147147

148148
// we temporarily use a ParsedValue literal so that we get the same property lookup semantics
149149
const selected = ParsedValue.literal(root).property(value.split('.'));
@@ -255,7 +255,7 @@ export function envVarDirective(
255255
): ParsingExtension {
256256
const envType = environmentOverride ?? currentEnvironment(aliases, environmentSourceNames);
257257

258-
return forKey('$envVar', (value, key, ctx) => async (parse) => {
258+
return forKey('$envVar', (value, parentKeys) => async (parse) => {
259259
let name: string;
260260
let parseInt = false;
261261
let parseFloat = false;
@@ -264,11 +264,11 @@ export function envVarDirective(
264264
if (typeof value === 'string') {
265265
name = value;
266266
} else {
267-
validateObject(value, [...ctx, key]);
267+
validateObject(value, parentKeys);
268268
if (Array.isArray(value)) throw new AppConfigError('$envVar was given an array');
269269

270270
const resolved = (await parse(value.name)).toJSON();
271-
validateString(resolved, [...ctx, key, [InObject, 'name']]);
271+
validateString(resolved, [[InObject, 'name'], ...parentKeys]);
272272

273273
parseInt = !!(await parse(value.parseInt)).toJSON();
274274
parseFloat = !!(await parse(value.parseFloat)).toJSON();
@@ -317,9 +317,9 @@ export function envVarDirective(
317317
const allowNull = (await parse(value.allowNull)).toJSON();
318318

319319
if (allowNull) {
320-
validateStringOrNull(fallback, [...ctx, key, [InObject, 'fallback']]);
320+
validateStringOrNull(fallback, [[InObject, 'fallback'], ...parentKeys]);
321321
} else {
322-
validateString(fallback, [...ctx, key, [InObject, 'fallback']]);
322+
validateString(fallback, [[InObject, 'fallback'], ...parentKeys]);
323323
}
324324

325325
return parse(fallback, { shouldFlatten: true });
@@ -337,17 +337,17 @@ export function substituteDirective(
337337
): ParsingExtension {
338338
const envType = environmentOverride ?? currentEnvironment(aliases, environmentSourceNames);
339339

340-
return forKey(['$substitute', '$subs'], (value, key, ctx) => async (parse) => {
340+
return forKey(['$substitute', '$subs'], (value, parentKeys) => async (parse) => {
341341
if (typeof value === 'string') {
342342
return parse(performAllSubstitutions(value, envType), { shouldFlatten: true });
343343
}
344344

345-
validateObject(value, [...ctx, key]);
345+
validateObject(value, parentKeys);
346346
if (Array.isArray(value)) throw new AppConfigError('$substitute was given an array');
347347

348348
const name = (await parse(value.name)).toJSON();
349349

350-
validateString(name, [...ctx, key, [InObject, 'name']]);
350+
validateString(name, [[InObject, 'name'], ...parentKeys]);
351351

352352
let resolvedValue = process.env[name];
353353

@@ -396,9 +396,9 @@ export function substituteDirective(
396396
const allowNull = (await parse(value.allowNull)).toJSON();
397397

398398
if (allowNull) {
399-
validateStringOrNull(fallback, [...ctx, key, [InObject, 'fallback']]);
399+
validateStringOrNull(fallback, [[InObject, 'fallback'], ...parentKeys]);
400400
} else {
401-
validateString(fallback, [...ctx, key, [InObject, 'fallback']]);
401+
validateString(fallback, [[InObject, 'fallback'], ...parentKeys]);
402402
}
403403

404404
return parse(fallback, { shouldFlatten: true });

app-config-v1-compat/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { logger } from '@app-config/logging';
77

88
/** V1 app-config compatibility */
99
export default function v1Compat(): ParsingExtension {
10-
return (value, [_, key], context) => {
10+
return (value, [[_, key], ...context]) => {
1111
// only apply in top-level app-config property
1212
if (context[context.length - 1]?.[0] !== Root) {
1313
return false;

0 commit comments

Comments
 (0)