Skip to content

Commit 1f67a75

Browse files
authored
Merge pull request #114 from launchcodedev/fallbackable
Adds $try, $if and $eq macro directives for rudimentary control flow
2 parents c18bf40 + 43e51ac commit 1f67a75

File tree

8 files changed

+420
-16
lines changed

8 files changed

+420
-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: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@ module.exports = {
1414
environmentSourceNames,
1515
) {
1616
const {
17+
unescape$Directives,
18+
tryDirective,
19+
ifDirective,
20+
eqDirective,
1721
envDirective,
1822
extendsDirective,
1923
extendsSelfDirective,
2024
overrideDirective,
2125
timestampDirective,
22-
unescape$Directives,
2326
environmentVariableSubstitution,
2427
} = require('@app-config/extensions');
2528

@@ -28,14 +31,17 @@ module.exports = {
2831
const { default: gitRefDirectives } = require('@app-config/git');
2932

3033
return [
34+
unescape$Directives(),
35+
tryDirective(),
36+
ifDirective(),
37+
eqDirective(),
3138
v1Compat(),
3239
envDirective(aliases, environmentOverride, environmentSourceNames),
3340
extendsDirective(),
3441
extendsSelfDirective(),
3542
overrideDirective(),
3643
encryptedDirective(symmetricKey),
3744
timestampDirective(),
38-
unescape$Directives(),
3945
environmentVariableSubstitution(aliases, environmentOverride, environmentSourceNames),
4046
gitRefDirectives(),
4147
];
@@ -47,11 +53,23 @@ module.exports = {
4753
},
4854
defaultMetaExtensions() {
4955
const {
56+
unescape$Directives,
57+
tryDirective,
58+
ifDirective,
59+
eqDirective,
5060
extendsDirective,
5161
extendsSelfDirective,
5262
overrideDirective,
5363
} = require('@app-config/extensions');
5464

55-
return [extendsDirective(), extendsSelfDirective(), overrideDirective()];
65+
return [
66+
unescape$Directives(),
67+
tryDirective(),
68+
ifDirective(),
69+
eqDirective(),
70+
extendsDirective(),
71+
extendsSelfDirective(),
72+
overrideDirective(),
73+
];
5674
},
5775
};

app-config-extensions/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,12 @@
3434
"@app-config/extension-utils": "^2.1.8",
3535
"@app-config/logging": "^2.1.8",
3636
"@app-config/node": "^2.1.8",
37-
"@app-config/utils": "^2.1.8"
37+
"@app-config/utils": "^2.1.8",
38+
"lodash.isequal": "4"
3839
},
3940
"devDependencies": {
40-
"@app-config/test-utils": "^2.1.8"
41+
"@app-config/test-utils": "^2.1.8",
42+
"@types/lodash.isequal": "4"
4143
},
4244
"prettier": "@lcdev/prettier",
4345
"jest": {

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

Lines changed: 249 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
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,
7+
ifDirective,
8+
eqDirective,
69
envDirective,
710
extendsDirective,
811
extendsSelfDirective,
@@ -11,6 +14,220 @@ import {
1114
environmentVariableSubstitution,
1215
} from './index';
1316

17+
describe('$try directive', () => {
18+
it('uses main value', async () => {
19+
const source = new LiteralSource({
20+
$try: {
21+
$value: 'foobar',
22+
$fallback: 'barfoo',
23+
},
24+
});
25+
26+
expect(await source.readToJSON([tryDirective()])).toEqual('foobar');
27+
});
28+
29+
it('uses fallback value', async () => {
30+
const failDirective = forKey('$fail', () => () => {
31+
throw new Fallbackable();
32+
});
33+
34+
const source = new LiteralSource({
35+
$try: {
36+
$value: {
37+
$fail: true,
38+
},
39+
$fallback: 'barfoo',
40+
},
41+
});
42+
43+
expect(await source.readToJSON([tryDirective(), failDirective])).toEqual('barfoo');
44+
});
45+
46+
it('doesnt evaluate fallback if value works', async () => {
47+
const failDirective = forKey('$fail', () => () => {
48+
throw new Fallbackable();
49+
});
50+
51+
const source = new LiteralSource({
52+
$try: {
53+
$value: 'barfoo',
54+
$fallback: {
55+
$fail: true,
56+
},
57+
},
58+
});
59+
60+
expect(await source.readToJSON([tryDirective(), failDirective])).toEqual('barfoo');
61+
});
62+
63+
it('doesnt swallow plain errors', async () => {
64+
const failDirective = forKey('$fail', () => () => {
65+
throw new Error();
66+
});
67+
68+
const source = new LiteralSource({
69+
$try: {
70+
$value: {
71+
$fail: true,
72+
},
73+
$fallback: 'barfoo',
74+
},
75+
});
76+
77+
await expect(source.readToJSON([tryDirective(), failDirective])).rejects.toThrow(Error);
78+
});
79+
80+
it('swallows plain errors with "unsafe" option', async () => {
81+
const failDirective = forKey('$fail', () => () => {
82+
throw new Error();
83+
});
84+
85+
const source = new LiteralSource({
86+
$try: {
87+
$value: {
88+
$fail: true,
89+
},
90+
$fallback: 'barfoo',
91+
$unsafe: true,
92+
},
93+
});
94+
95+
expect(await source.readToJSON([tryDirective(), failDirective])).toEqual('barfoo');
96+
});
97+
});
98+
99+
describe('$if directive', () => {
100+
it('uses main value', async () => {
101+
const source = new LiteralSource({
102+
$if: {
103+
$check: true,
104+
$then: 'foobar',
105+
$else: 'barfoo',
106+
},
107+
});
108+
109+
expect(await source.readToJSON([ifDirective()])).toEqual('foobar');
110+
});
111+
112+
it('uses fallback value', async () => {
113+
const source = new LiteralSource({
114+
$if: {
115+
$check: false,
116+
$then: 'foobar',
117+
$else: 'barfoo',
118+
},
119+
});
120+
121+
expect(await source.readToJSON([ifDirective()])).toEqual('barfoo');
122+
});
123+
124+
it('doesnt evaluate the else branch', async () => {
125+
const source = new LiteralSource({
126+
$if: {
127+
$check: true,
128+
$then: 'barfoo',
129+
$else: {
130+
$fail: true,
131+
},
132+
},
133+
});
134+
135+
expect(await source.readToJSON([ifDirective()])).toEqual('barfoo');
136+
});
137+
138+
it('doesnt evaluate the other branch', async () => {
139+
const source = new LiteralSource({
140+
$if: {
141+
$check: false,
142+
$then: {
143+
$fail: true,
144+
},
145+
$else: 'barfoo',
146+
},
147+
});
148+
149+
expect(await source.readToJSON([ifDirective()])).toEqual('barfoo');
150+
});
151+
152+
it('disallows missing property', async () => {
153+
const source = new LiteralSource({
154+
$if: {
155+
$check: false,
156+
$else: 'barfoo',
157+
},
158+
});
159+
160+
await expect(source.readToJSON([ifDirective()])).rejects.toThrow();
161+
});
162+
163+
it('parses $check', async () => {
164+
const source = new LiteralSource({
165+
$if: {
166+
$check: {
167+
$env: {
168+
default: true,
169+
},
170+
},
171+
$then: 'foobar',
172+
$else: 'barfoo',
173+
},
174+
});
175+
176+
expect(await source.readToJSON([ifDirective(), envDirective()])).toEqual('foobar');
177+
});
178+
});
179+
180+
describe('$eq directive', () => {
181+
it('returns true for empty', async () => {
182+
const source = new LiteralSource({
183+
$eq: [],
184+
});
185+
186+
expect(await source.readToJSON([eqDirective()])).toBe(true);
187+
});
188+
189+
it('returns true for two numbers', async () => {
190+
const source = new LiteralSource({
191+
$eq: [42, 42],
192+
});
193+
194+
expect(await source.readToJSON([eqDirective()])).toBe(true);
195+
});
196+
197+
it('returns false for two numbers', async () => {
198+
const source = new LiteralSource({
199+
$eq: [42, 44],
200+
});
201+
202+
expect(await source.readToJSON([eqDirective()])).toBe(false);
203+
});
204+
205+
it('returns true for two objects', async () => {
206+
const source = new LiteralSource({
207+
$eq: [{ a: true }, { a: true }],
208+
});
209+
210+
expect(await source.readToJSON([eqDirective()])).toBe(true);
211+
});
212+
213+
it('returns false for two objects', async () => {
214+
const source = new LiteralSource({
215+
$eq: [{ a: true }, { b: true }],
216+
});
217+
218+
expect(await source.readToJSON([eqDirective()])).toBe(false);
219+
});
220+
221+
it('parses before checking equality', async () => {
222+
process.env.APP_CONFIG_ENV = 'test';
223+
const source = new LiteralSource({
224+
$eq: [{ $env: { default: { a: true } } }, { $env: { test: { a: true } } }],
225+
});
226+
227+
expect(await source.readToJSON([eqDirective(), envDirective()])).toBe(true);
228+
});
229+
});
230+
14231
describe('$extends directive', () => {
15232
it('fails if file is missing', async () => {
16233
const source = new LiteralSource({
@@ -892,4 +1109,35 @@ describe('extension combinations', () => {
8921109
expect(parsed.toJSON()).toEqual({ foo: 'bar' });
8931110
});
8941111
});
1112+
1113+
it('combines $try and $extends', async () => {
1114+
const source = new LiteralSource({
1115+
$try: {
1116+
$value: {
1117+
$extends: './test-file.json',
1118+
},
1119+
$fallback: {
1120+
fellBack: true,
1121+
},
1122+
},
1123+
});
1124+
1125+
await expect(source.readToJSON([extendsDirective(), tryDirective()])).resolves.toEqual({
1126+
fellBack: true,
1127+
});
1128+
});
1129+
1130+
it('combines $if and $eq', async () => {
1131+
const source = new LiteralSource({
1132+
$if: {
1133+
$check: {
1134+
$eq: ['foo', 'foo'],
1135+
},
1136+
$then: 'foo',
1137+
$else: 'bar',
1138+
},
1139+
});
1140+
1141+
await expect(source.readToJSON([ifDirective(), eqDirective()])).resolves.toEqual('foo');
1142+
});
8951143
});

0 commit comments

Comments
 (0)