diff --git a/__tests__/defaults/unset.json b/__tests__/defaults/unset.json new file mode 100644 index 0000000..e90fd28 --- /dev/null +++ b/__tests__/defaults/unset.json @@ -0,0 +1,3 @@ +{ + "override": "unset:" +} \ No newline at end of file diff --git a/src/handlers.ts b/src/handlers.ts index 3c34cb3..b4d9b2e 100644 --- a/src/handlers.ts +++ b/src/handlers.ts @@ -5,9 +5,37 @@ import { loadJsonc } from './common'; type IntermediateConfigValue = ReturnType; -export async function resolveConfig( - inputData: IntermediateConfigValue, -) { +const filters = { + // Return the variable if it exists and is non-empty + '|u': (value?: unknown) => { + return value === '' ? undefined : value; + }, + // Return the variable as a number if it exists, or undefined + '|ud': (value?: unknown) => { + return value === '' || value === undefined || value === null + ? undefined + : parseInt(value.toString(), 10); + }, + // Return the value as a decimal + '|d': (value?: unknown) => { + return parseInt(value?.toString() || '', 10); + }, + // Return the value as a boolean - empty, false, 0 and undefined will be false + '|b': (value?: unknown) => { + return ( + value !== '' && value !== 'false' && value !== '0' && value !== undefined && value !== null + ); + }, + // Return the value as a boolean but inverted so that empty/undefined/0/false are true + '|!b': (value?: unknown) => { + return ( + value === '' || value === 'false' || value === '0' || value === undefined || value === null + ); + }, + '|': (value?: unknown) => value, +}; + +export async function resolveConfig(inputData: IntermediateConfigValue) { const shorty = createShortstopHandlers(); let data: unknown = inputData; @@ -16,7 +44,17 @@ export async function resolveConfig( shorty.use('config', (key: string) => { usedHandler = true; - const keys = key.split('.'); + let finalKey = key; + let transform: (value: unknown) => unknown = filters['|']; + + Object.entries(filters).some(([filter, fn]) => { + if (key.endsWith(filter)) { + transform = fn; + finalKey = key.slice(0, -filter.length); + } + }); + + const keys = finalKey.split('.'); let result: unknown = data; while (result && keys.length) { @@ -27,7 +65,10 @@ export async function resolveConfig( result = (result as Record)[prop]; } - return keys.length ? null : result; + if (keys.length) { + return null; + } + return transform(result); }); do { diff --git a/src/index.spec.ts b/src/index.spec.ts index e7b57ed..13c75cd 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -2,7 +2,7 @@ import path from 'path'; import { afterAll, describe, expect, test } from 'vitest'; -import { BaseConfitSchema, confit } from './index'; +import { BaseConfitSchema, confit, unsetHandler } from './index'; describe('confit', () => { const originalEnv = process.env.NODE_ENV; @@ -36,11 +36,13 @@ describe('confit', () => { }); test('use', async () => { - const config = await confit<{ - foo: { bar: string }; - bar: string; - arr: (number | string)[]; - } & BaseConfitSchema>().create(); + const config = await confit< + { + foo: { bar: string }; + bar: string; + arr: (number | string)[]; + } & BaseConfitSchema + >().create(); config.use({ foo: { bar: 'baz' } }); expect(typeof config.get().foo).toBe('object'); @@ -65,18 +67,20 @@ describe('confit', () => { test('import protocol', async () => { const basedir = path.join(__dirname, '..', '__tests__', 'import'); - const config = await confit<{ - name: string; - child: { + const config = await confit< + { name: string; - grandchild: { + child: { name: string; + grandchild: { + name: string; + }; + grandchildJson: { + name: string; + }; }; - grandchildJson: { - name: string; - }; - }; - } & BaseConfitSchema>({ basedir }).create(); + } & BaseConfitSchema + >({ basedir }).create(); expect(config.get().name).toBe('parent'); expect(config.get().child.name).toBe('child'); expect(config.get().child.grandchild.name).toBe('grandchild'); @@ -98,17 +102,19 @@ describe('confit', () => { test('config protocol', async () => { const basedir = path.join(__dirname, '..', '__tests__', 'config'); - const config = await confit<{ - name: string; - foo: string; - bar: string; - baz: string; - imported: { + const config = await confit< + { + name: string; foo: string; - }; - path: { to: { nested: { value: string } } }; - value: string; - } & BaseConfitSchema>({ basedir }).create(); + bar: string; + baz: string; + imported: { + foo: string; + }; + path: { to: { nested: { value: string } } }; + value: string; + } & BaseConfitSchema + >({ basedir }).create(); expect(config.get().name).toBe('config'); expect(config.get().foo).toBe(config.get().imported.foo); expect(config.get().bar).toBe(config.get().foo); @@ -118,16 +124,18 @@ describe('confit', () => { test('default file import', async () => { const basedir = path.join(__dirname, '..', '__tests__', 'import'); - const config = await confit<{ - name: string; - foo: string; - child: { + const config = await confit< + { name: string; - grandchild: { + foo: string; + child: { name: string; + grandchild: { + name: string; + }; }; - }; - } & BaseConfitSchema>({ basedir }) + } & BaseConfitSchema + >({ basedir }) .addDefault('./default.json') .create(); expect(config.get().name).toBe('parent'); @@ -157,10 +165,12 @@ describe('confit', () => { process.env.NODE_ENV = 'test'; const basedir = path.join(__dirname, '..', '__tests__', 'defaults'); - const config = await confit<{ - default: string; - override: string; - } & BaseConfitSchema>({ basedir }).create(); + const config = await confit< + { + default: string; + override: string; + } & BaseConfitSchema + >({ basedir }).create(); // File-based overrides expect(config.get().default).toBe('config'); @@ -178,10 +188,12 @@ describe('confit', () => { process.env.NODE_ENV = 'dev'; const basedir = path.join(__dirname, '..', '__tests__', 'defaults'); - const config = await confit<{ - default: string; - override: string; - } & BaseConfitSchema>({ basedir }).create(); + const config = await confit< + { + default: string; + override: string; + } & BaseConfitSchema + >({ basedir }).create(); // File-based overrides expect(config.get().default).toBe('config'); expect(config.get().override).toBe('development'); @@ -196,7 +208,9 @@ describe('confit', () => { test('confit addOverride as json object', async () => { const basedir = path.join(__dirname, '..', '__tests__', 'config'); - const config = await confit<{ name: string; foo?: string; tic: { tac: string } } & BaseConfitSchema>({ basedir }) + const config = await confit< + { name: string; foo?: string; tic: { tac: string } } & BaseConfitSchema + >({ basedir }) .addOverride({ tic: { tac: 'toe', @@ -210,11 +224,13 @@ describe('confit', () => { }); test('confit without files, using just json objects', async () => { - const config = await confit<{ - foo: 'bar'; - tic: { tac: string }; - blue: boolean; - } & BaseConfitSchema>() + const config = await confit< + { + foo: 'bar'; + tic: { tac: string }; + blue: boolean; + } & BaseConfitSchema + >() .addDefault({ foo: 'bar', tic: { @@ -232,10 +248,12 @@ describe('confit', () => { test('protocols', async () => { process.env.NODE_ENV = 'dev'; const basedir = path.join(__dirname, '..', '__tests__', 'defaults'); - const config = await confit<{ - misc: string; - path: string; - } & BaseConfitSchema>({ + const config = await confit< + { + misc: string; + path: string; + } & BaseConfitSchema + >({ basedir, protocols: { path: (value: string) => path.join(basedir, value), @@ -258,10 +276,12 @@ describe('confit', () => { }, }; - const config = await confit<{ - misc: string; - path: string; - } & BaseConfitSchema>(options).create(); + const config = await confit< + { + misc: string; + path: string; + } & BaseConfitSchema + >(options).create(); expect(config.get().misc).toBe(path.join(basedir, 'config.json!')); expect(config.get().path).toBe(path.join(basedir, 'development.json!')); @@ -288,13 +308,30 @@ describe('confit', () => { await expect(confit({ basedir }).create()).rejects.toThrow(); }); + test('unset', async () => { + const basedir = path.join(__dirname, '..', '__tests__', 'defaults'); + const config = await confit< + { + default: string; + override: string; + } & BaseConfitSchema + >({ basedir, protocols: { unset: unsetHandler() } }) + .addOverride('development.json') + .addOverride(path.join(basedir, 'unset.json')) + .create(); + + expect(config.get().override).toBeUndefined(); + }); + test('addOverride', async () => { process.env.NODE_ENV = 'test'; const basedir = path.join(__dirname, '..', '__tests__', 'defaults'); - const config = await confit<{ - default: string; - override: string; - } & BaseConfitSchema>({ basedir }) + const config = await confit< + { + default: string; + override: string; + } & BaseConfitSchema + >({ basedir }) .addOverride('development.json') .addOverride(path.join(basedir, 'supplemental.json')) .create(); @@ -314,7 +351,9 @@ describe('confit', () => { const config = await confit({ basedir }).addDefault('override.json').create(); expect((config.get() as ReturnType).child.grandchild.secret).toBe('santa'); - expect((config.get() as ReturnType).child.grandchild.name).toBe('grandchild'); + expect((config.get() as ReturnType).child.grandchild.name).toBe( + 'grandchild', + ); expect((config.get() as ReturnType).child.grandchild.another).toBe('claus'); }); @@ -345,14 +384,35 @@ describe('confit', () => { ignoreme: 'file:./path/to/mindyourbusiness', }); const basedir = path.join(__dirname, '..', '__tests__', 'defaults'); - const config = await confit<{ - fromlocal: string; - ignoreme?: string; - } & BaseConfitSchema>({ + const config = await confit< + { + fromlocal: string; + ignoreme?: string; + } & BaseConfitSchema + >({ basedir, excludeEnvVariables: ['ignoreme'], }).create(); expect(config.get().fromlocal).toBe(env.local); expect(config.get().ignoreme).toBeUndefined(); }); + + test('env via config', async () => { + const env = (process.env = { + SAMPLE: '8000', + sample: 'config:SAMPLE', + sampleNum: 'config:SAMPLE|d', + }); + const basedir = path.join(__dirname, '..', '__tests__', 'defaults'); + const config = await confit< + { + sample: string; + sampleNum: number; + } & BaseConfitSchema + >({ + basedir, + }).create(); + expect(config.get().sample).toBe(env.SAMPLE); + expect(config.get().sampleNum).toBe(Number(env.SAMPLE)); + }) }); diff --git a/src/shortstop/textHandlers.spec.ts b/src/shortstop/textHandlers.spec.ts new file mode 100644 index 0000000..f72feab --- /dev/null +++ b/src/shortstop/textHandlers.spec.ts @@ -0,0 +1,24 @@ +import { describe, test, expect } from 'vitest'; + +import { base64Handler, bufferHandler, unsetHandler } from '.'; + +describe('textHandlers', () => { + test('base64', () => { + const handler = base64Handler(); + expect(handler('aGVsbG8gd29ybGQ=')).toBeInstanceOf(Buffer); + expect(handler('aGVsbG8gd29ybGQ=')).toEqual(Buffer.from('hello world')); + expect(handler('aGVsbG8gd29ybGQ=|utf8')).toBe('hello world'); + }); + + test('base64url', () => { + const handler = bufferHandler('base64url'); + expect(handler('aGVsbG8gd29ybGQ')).toBeInstanceOf(Buffer); + expect(handler('aGVsbG8gd29ybGQ')).toEqual(Buffer.from('hello world')); + expect(handler('aGVsbG8gd29ybGQ|utf8')).toBe('hello world'); + }); + + test('unset', () => { + const handler = unsetHandler(); + expect(handler()).toBeUndefined(); + }); +}); diff --git a/src/shortstop/textHandlers.ts b/src/shortstop/textHandlers.ts index d15a823..728f9b8 100644 --- a/src/shortstop/textHandlers.ts +++ b/src/shortstop/textHandlers.ts @@ -1,5 +1,20 @@ +export function bufferHandler(type: 'base64' | 'base64url' | 'hex') { + return function bufferHandler(value: string) { + const match = value.match(/(.*)\|(binary|hex|utf8|ucs2|utf16le|ascii)$/); + if (match) { + return Buffer.from(match[1], type).toString(match[2] as BufferEncoding);; + } else { + return Buffer.from(value, type); + } + }; +} + export function base64Handler() { - return function base64Handler(value: string) { - return Buffer.from(value, 'base64'); + return bufferHandler('base64'); +} + +export function unsetHandler() { + return function unsetHandler() { + return undefined; }; }