Skip to content

Commit 35af467

Browse files
committed
feat: adds $try directive
1 parent 8b2b991 commit 35af467

File tree

5 files changed

+170
-16
lines changed

5 files changed

+170
-16
lines changed

app-config-core/src/errors.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
/** Any generic error that comes directly from this package */
22
export class AppConfigError extends Error {}
33

4+
/** An error that can be recovered using $try */
5+
export class Fallbackable extends AppConfigError {}
6+
47
/** When a ConfigSource cannot be found */
5-
export class NotFoundError extends AppConfigError {}
8+
export class NotFoundError extends Fallbackable {}
69

710
/** Could not parse a string from a config source */
811
export class ParsingError extends AppConfigError {}

app-config-default-extensions/index.js

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ module.exports = {
1414
environmentSourceNames,
1515
) {
1616
const {
17+
unescape$Directives,
18+
tryDirective,
1719
envDirective,
1820
extendsDirective,
1921
extendsSelfDirective,
2022
overrideDirective,
2123
timestampDirective,
22-
unescape$Directives,
2324
environmentVariableSubstitution,
2425
} = require('@app-config/extensions');
2526

@@ -28,30 +29,43 @@ module.exports = {
2829
const { default: gitRefDirectives } = require('@app-config/git');
2930

3031
return [
32+
unescape$Directives(),
33+
tryDirective(),
3134
v1Compat(),
3235
envDirective(aliases, environmentOverride, environmentSourceNames),
3336
extendsDirective(),
3437
extendsSelfDirective(),
3538
overrideDirective(),
3639
encryptedDirective(symmetricKey),
3740
timestampDirective(),
38-
unescape$Directives(),
3941
environmentVariableSubstitution(aliases, environmentOverride, environmentSourceNames),
4042
gitRefDirectives(),
4143
];
4244
},
4345
defaultEnvExtensions() {
44-
const { unescape$Directives, markAllValuesAsSecret } = require('@app-config/extensions');
46+
const {
47+
unescape$Directives,
48+
tryDirective,
49+
markAllValuesAsSecret,
50+
} = require('@app-config/extensions');
4551

46-
return [unescape$Directives(), markAllValuesAsSecret()];
52+
return [unescape$Directives(), tryDirective(), markAllValuesAsSecret()];
4753
},
4854
defaultMetaExtensions() {
4955
const {
56+
unescape$Directives,
57+
tryDirective,
5058
extendsDirective,
5159
extendsSelfDirective,
5260
overrideDirective,
5361
} = require('@app-config/extensions');
5462

55-
return [extendsDirective(), extendsSelfDirective(), overrideDirective()];
63+
return [
64+
unescape$Directives(),
65+
tryDirective(),
66+
extendsDirective(),
67+
extendsSelfDirective(),
68+
overrideDirective(),
69+
];
5670
},
5771
};

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

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { withTempFiles } from '@app-config/test-utils';
2-
import { LiteralSource, NotFoundError } from '@app-config/core';
2+
import { LiteralSource, NotFoundError, Fallbackable } from '@app-config/core';
33
import { FileSource } from '@app-config/node';
44
import { forKey } from '@app-config/extension-utils';
55
import {
6+
tryDirective,
67
envDirective,
78
extendsDirective,
89
extendsSelfDirective,
@@ -11,6 +12,88 @@ import {
1112
environmentVariableSubstitution,
1213
} from './index';
1314

15+
describe('$try directive', () => {
16+
it('uses main value', async () => {
17+
const source = new LiteralSource({
18+
$try: {
19+
$value: 'foobar',
20+
$fallback: 'barfoo',
21+
},
22+
});
23+
24+
expect(await source.readToJSON([tryDirective()])).toEqual('foobar');
25+
});
26+
27+
it('uses fallback value', async () => {
28+
const failDirective = forKey('$fail', () => () => {
29+
throw new Fallbackable();
30+
});
31+
32+
const source = new LiteralSource({
33+
$try: {
34+
$value: {
35+
$fail: true,
36+
},
37+
$fallback: 'barfoo',
38+
},
39+
});
40+
41+
expect(await source.readToJSON([tryDirective(), failDirective])).toEqual('barfoo');
42+
});
43+
44+
it('doesnt evaluate fallback if value works', async () => {
45+
const failDirective = forKey('$fail', () => () => {
46+
throw new Fallbackable();
47+
});
48+
49+
const source = new LiteralSource({
50+
$try: {
51+
$value: 'barfoo',
52+
$fallback: {
53+
$fail: true,
54+
},
55+
},
56+
});
57+
58+
expect(await source.readToJSON([tryDirective(), failDirective])).toEqual('barfoo');
59+
});
60+
61+
it('doesnt swallow plain errors', async () => {
62+
const failDirective = forKey('$fail', () => () => {
63+
throw new Error();
64+
});
65+
66+
const source = new LiteralSource({
67+
$try: {
68+
$value: {
69+
$fail: true,
70+
},
71+
$fallback: 'barfoo',
72+
},
73+
});
74+
75+
await expect(source.readToJSON([tryDirective(), failDirective])).rejects.toThrow(Error);
76+
});
77+
78+
it('swallows plain errors with "unsafe" option', async () => {
79+
const failDirective = forKey('$fail', () => () => {
80+
throw new Error();
81+
});
82+
83+
const source = new LiteralSource({
84+
$try: {
85+
$value: {
86+
$fail: true,
87+
},
88+
$fallback: 'barfoo',
89+
$unsafe: true,
90+
},
91+
});
92+
93+
expect(await source.readToJSON([tryDirective(), failDirective])).toEqual('barfoo');
94+
});
95+
});
96+
1497
describe('$extends directive', () => {
1598
it('fails if file is missing', async () => {
1699
const source = new LiteralSource({
@@ -874,4 +957,21 @@ describe('extension combinations', () => {
874957
expect(parsed.toJSON()).toEqual({ foo: 'bar' });
875958
});
876959
});
960+
961+
it('combines $try and $extends', async () => {
962+
const source = new LiteralSource({
963+
$try: {
964+
$value: {
965+
$extends: './test-file.json',
966+
},
967+
$fallback: {
968+
fellBack: true,
969+
},
970+
},
971+
});
972+
973+
await expect(source.readToJSON([extendsDirective(), tryDirective()])).resolves.toEqual({
974+
fellBack: true,
975+
});
976+
});
877977
});

app-config-extensions/src/index.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
AppConfigError,
1313
NotFoundError,
1414
FailedToSelectSubObject,
15+
Fallbackable,
1516
InObject,
1617
} from '@app-config/core';
1718
import {
@@ -40,6 +41,35 @@ export function unescape$Directives(): ParsingExtension {
4041
};
4142
}
4243

44+
/** Try an operation, with a fallback ($try, $value and $fallback) */
45+
export function tryDirective(): ParsingExtension {
46+
return forKey(
47+
'$try',
48+
validateOptions(
49+
(SchemaBuilder) =>
50+
SchemaBuilder.emptySchema()
51+
.addProperty('$value', SchemaBuilder.fromJsonSchema({}))
52+
.addProperty('$fallback', SchemaBuilder.fromJsonSchema({}))
53+
.addBoolean('$unsafe', {}, false),
54+
(value) => async (parse) => {
55+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
56+
const { $value, $fallback, $unsafe } = value;
57+
58+
try {
59+
return await parse($value, { shouldFlatten: true });
60+
} catch (error) {
61+
if (error instanceof Fallbackable || $unsafe) {
62+
return parse($fallback, { shouldFlatten: true });
63+
}
64+
65+
throw error;
66+
}
67+
},
68+
{ lazy: true },
69+
),
70+
);
71+
}
72+
4373
/** Uses another file as overriding values, layering them on top of current file */
4474
export function overrideDirective(): ParsingExtension {
4575
return fileReferenceDirective('$override', { shouldOverride: true });

app-config-git/src/index.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import simpleGit from 'simple-git';
2-
import { ParsingExtension, AppConfigError } from '@app-config/core';
2+
import { ParsingExtension, AppConfigError, Fallbackable } from '@app-config/core';
33
import { forKey, validateOptions } from '@app-config/extension-utils';
44

5+
class GitError extends Fallbackable {}
6+
57
/** Access to the git branch and commit ref */
68
export default function gitRefDirectives(
79
getStatus: typeof gitStatus = gitStatus,
@@ -53,13 +55,18 @@ interface GitStatus {
5355

5456
async function gitStatus(): Promise<GitStatus> {
5557
const git = simpleGit({});
56-
const rev = await git.revparse(['HEAD']);
57-
const branch = await git.revparse(['--abbrev-ref', 'HEAD']);
58-
const tag = await git.tag(['--points-at', 'HEAD']);
5958

60-
return {
61-
commitRef: rev,
62-
branchName: branch,
63-
tag: (tag.trim() || undefined)?.split(' ')[0],
64-
};
59+
try {
60+
const rev = await git.revparse(['HEAD']);
61+
const branch = await git.revparse(['--abbrev-ref', 'HEAD']);
62+
const tag = await git.tag(['--points-at', 'HEAD']);
63+
64+
return {
65+
commitRef: rev,
66+
branchName: branch,
67+
tag: (tag.trim() || undefined)?.split(' ')[0],
68+
};
69+
} catch (error) {
70+
throw new GitError(error.message);
71+
}
6572
}

0 commit comments

Comments
 (0)