From 89c2570a79dfe3bc6ed2b715ec56198915d7e4f4 Mon Sep 17 00:00:00 2001 From: Simon Korzun Date: Tue, 27 Jun 2023 07:57:54 -0400 Subject: [PATCH 1/3] chore: update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6d8299a..28a2efc 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ export const log = { default: true }), flushFrequency: getNumber('LOG_FLUSH_DURATION', { - allowRange: [500, 10_000] + allowRange: [500, 10_000], default: 1000 }), level: getString('LOG_LEVEL', { @@ -35,7 +35,7 @@ export const log = { default: 'error' }), transport: getString('LOG_TRANSPORT', { - allowList: ['file', 'http', 'log'] + allowList: ['file', 'http', 'log'], allowUndefined: true, }), }; From e92eff5a94d628e64f1b1a545b6ad74cf5a0e999 Mon Sep 17 00:00:00 2001 From: Simon Korzun Date: Sat, 20 Apr 2024 17:32:41 -0700 Subject: [PATCH 2/3] feat: add `getStringArray` --- src/string.test.ts | 109 ++++++++++++++++++++++++++++++++++++++++++++- src/string.ts | 82 ++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+), 1 deletion(-) diff --git a/src/string.test.ts b/src/string.test.ts index 790147a..850a0d5 100644 --- a/src/string.test.ts +++ b/src/string.test.ts @@ -1,5 +1,5 @@ import { NotDefinedConfigError, ListValueConfigError } from './error'; -import { getString } from './string'; +import { getString, getStringArray } from './string'; describe('getString', () => { let env: Record; @@ -99,3 +99,110 @@ describe('getString', () => { }); }); }); + +describe('getStringArray', () => { + let env: Record; + beforeEach(() => { + env = Object.assign({}, process.env); + }); + Object.assign({}, process.env); + afterEach(() => { + process.env = env; + }); + describe('environment variable is set', () => { + it('returns the environment variable as an array of strings', () => { + process.env.TEST_STRING_ARRAY = 'foo,bar,baz'; + const value = getStringArray('TEST_STRING_ARRAY'); + expect(value).toEqual(['foo', 'bar', 'baz']); + }); + describe('allow undefined', () => { + it('returns the environment variable as an array of strings', () => { + process.env.TEST_STRING_ARRAY = 'foo,bar,baz'; + const value = getStringArray('TEST_STRING_ARRAY', { + allowUndefined: true, + }); + expect(value).toEqual(['foo', 'bar', 'baz']); + }); + }); + describe('default is set', () => { + it('returns the environment variable as an array of strings', () => { + process.env.TEST_STRING_ARRAY = 'foo,bar,baz'; + const value = getStringArray('TEST_STRING_ARRAY', { default: ['bar'] }); + expect(value).toEqual(['foo', 'bar', 'baz']); + }); + }); + describe('allow list is defined', () => { + describe('environmental variable is in allow list', () => { + it('return the environmental variable as an array of strings', () => { + process.env.TEST_STRING_ARRAY = 'foo,bar,baz'; + const value = getStringArray('TEST_STRING_ARRAY', { + allowList: ['foo', 'bar', 'baz'], + }); + expect(value).toEqual(['foo', 'bar', 'baz']); + }); + }); + describe('environmental variable is NOT in allow list', () => { + it('throws an error', () => { + process.env.TEST_STRING_ARRAY = 'foo,bar,baz'; + expect(() => { + getStringArray('TEST_STRING_ARRAY', { allowList: ['foo', 'baz'] }); + }).toThrow( + new ListValueConfigError('TEST_STRING_ARRAY', 'bar', [ + 'foo', + 'baz', + ]), + ); + }); + }); + }); + }); + describe('environment variable is set to an empty string', () => { + it('throws an error', () => { + process.env.TEST_STRING_ARRAY = ''; + expect(() => { + getStringArray('TEST_STRING_ARRAY'); + }).toThrow(new NotDefinedConfigError('TEST_STRING_ARRAY')); + }); + + describe('allow undefined', () => { + it('returns an empty array', () => { + process.env.TEST_STRING_ARRAY = ''; + const value = getStringArray('TEST_STRING_ARRAY', { + allowUndefined: true, + }); + expect(value).toEqual([]); + }); + }); + + describe('default is set', () => { + it('returns the default', () => { + process.env.TEST_STRING_ARRAY = ''; + const value = getStringArray('TEST_STRING_ARRAY', { default: ['bar'] }); + expect(value).toEqual(['bar']); + }); + }); + }); + describe('environment variable is not set', () => { + it('throws an error', () => { + expect(() => { + getStringArray('TEST_STRING_ARRAY'); + }).toThrow(new NotDefinedConfigError('TEST_STRING_ARRAY')); + }); + + describe('allow undefined', () => { + it('returns an empty array', () => { + const value = getStringArray('TEST_STRING_ARRAY', { + allowUndefined: true, + }); + expect(value).toEqual([]); + }); + }); + + describe('default is set', () => { + it('returns the default', () => { + const value = getStringArray('TEST_STRING_ARRAY', { default: ['bar'] }); + expect(value).toEqual(['bar']); + }); + }); + }); +}); diff --git a/src/string.ts b/src/string.ts index 50dec15..4de61fb 100644 --- a/src/string.ts +++ b/src/string.ts @@ -78,6 +78,88 @@ export function getString(name: string, options: GetStringOptions = {}) { throw error; } +type GetStringArrayOptionsAllow = { + allowList?: string[]; +}; + +type GetStringArrayOptionsAllowUndefined = { + allowUndefined: true; + default?: undefined; +} & GetStringArrayOptionsAllow; +type GetStringArrayOptionsNoDefault = { + allowUndefined?: false; + default?: undefined; +} & GetStringArrayOptionsAllow; +type GetStringArrayOptionsDefault = { + allowUndefined?: false; + default: string[]; +} & GetStringArrayOptionsAllow; + +export type GetStringArrayOptions = + | GetStringArrayOptionsAllowUndefined + | GetStringArrayOptionsNoDefault + | GetStringArrayOptionsDefault; + +/** + * Returns an environmental variable as a `string` array. Optionally the value + * can be validated by an `allowList`. + */ +export function getStringArray( + name: string, + options: { + allowList?: string[]; + allowUndefined: true; + default?: undefined; + }, +): string[]; +/** + * Returns an environmental variable as a `string` array or, if undefined, + * throws an error. Optionally the values can be validated by an `allowList`. + */ +export function getStringArray( + name: string, + options?: { + allowList?: string[]; + allowUndefined?: false; + default?: undefined; + }, +): string[]; +/** + * Returns an environmental variable as a `string` array or, if undefined, the + * provided default value. Optionally the values can be validated by an `allowList`. + */ +export function getStringArray( + name: string, + options: { + allowList?: string[]; + allowUndefined?: false; + default: string[]; + }, +): string[]; +export function getStringArray( + name: string, + options: GetStringArrayOptions = {}, +) { + const { allowList, allowUndefined, default: defaultValue } = options; + const value = process.env[name]; + if (value !== undefined && value !== '') { + const valueList = value.split(','); + valueList.forEach((value) => validateList(name, value, allowList)); + return valueList; + } + if (defaultValue !== undefined) { + return defaultValue; + } + if (allowUndefined) { + return []; + } + const error = new NotDefinedConfigError(name); + if (Error.captureStackTrace) { + Error.captureStackTrace(error, getString); + } + throw error; +} + const validateList = (name: string, value: string, list?: string[]): void => { if (list !== undefined && !list.includes(value)) { const error = new ListValueConfigError(name, value, list); From a5d954abc2a5bf7aecd3d4951a2aaa7ef1e50e44 Mon Sep 17 00:00:00 2001 From: Simon Korzun Date: Sun, 21 Apr 2024 18:55:22 -0700 Subject: [PATCH 3/3] feat: add `getNumberArray` --- src/number.test.ts | 124 +++++++++++++++++++++++++++++++++++++++++- src/number.ts | 131 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 254 insertions(+), 1 deletion(-) diff --git a/src/number.test.ts b/src/number.test.ts index 2d1f001..e54fb43 100644 --- a/src/number.test.ts +++ b/src/number.test.ts @@ -3,7 +3,7 @@ import { NotDefinedConfigError, RangeValueConfigError, } from './error'; -import { getNumber } from './number'; +import { getNumber, getNumberArray } from './number'; describe('getNumber', () => { let env: Record; @@ -122,3 +122,125 @@ describe('getNumber', () => { }); }); }); + +describe('getNumberArray', () => { + let env: Record; + beforeEach(() => { + env = Object.assign({}, process.env); + }); + Object.assign({}, process.env); + afterEach(() => { + process.env = env; + }); + + describe('environment variable is set', () => { + it('returns the environment variable as an array of numbers', () => { + process.env.TEST_NUMBER = '10.33,20.33,30.33'; + const value = getNumberArray('TEST_NUMBER'); + expect(value).toEqual([10.33, 20.33, 30.33]); + }); + describe('allow undefined', () => { + it('returns the environment variable as an array of numbers', () => { + process.env.TEST_NUMBER = '10.33,20.33,30.33'; + const value = getNumberArray('TEST_NUMBER', { allowUndefined: true }); + expect(value).toEqual([10.33, 20.33, 30.33]); + }); + }); + describe('default is set', () => { + it('returns the environment variable as an array of numbers', () => { + process.env.TEST_NUMBER = '10.33,20.33,30.33'; + const value = getNumberArray('TEST_NUMBER', { default: [10, 20] }); + expect(value).toEqual([10.33, 20.33, 30.33]); + }); + }); + describe('allow list is defined', () => { + describe('all environmental variables numbers are in allow list', () => { + it('return the environmental variable as an array of numbers', () => { + process.env.TEST_NUMBER = '10.33,20.33,30.33'; + const value = getNumberArray('TEST_NUMBER', { + allowList: [10.33, 20.33, 30.33], + }); + expect(value).toEqual([10.33, 20.33, 30.33]); + }); + }); + describe('one of the environmental variable numbers is NOT in allow list', () => { + it('throws an error', () => { + process.env.TEST_NUMBER = '10.33,20.33,30.33'; + expect(() => { + getNumberArray('TEST_NUMBER', { allowList: [10.33, 20.33] }); + }).toThrow( + new ListValueConfigError('TEST_NUMBER', 30.33, [10.33, 20.33]), + ); + }); + }); + }); + describe('allow range is defined', () => { + describe('all environmental variable numbers are within allowable range', () => { + it('return the environmental variables as an array of numbers', () => { + process.env.TEST_NUMBER = '10.33,20.33,30.33'; + const value = getNumberArray('TEST_NUMBER', { + allowRange: [10, 40], + }); + expect(value).toEqual([10.33, 20.33, 30.33]); + }); + }); + describe('one of the environmental variable numbers is NOT within allowable range', () => { + it('throws an error', () => { + process.env.TEST_NUMBER = '10.33,20.33,30.33'; + expect(() => { + getNumberArray('TEST_NUMBER', { allowRange: [10, 30] }); + }).toThrow(new RangeValueConfigError('TEST_NUMBER', 30.33, [10, 30])); + }); + }); + }); + }); + describe('environment variable is set to an empty string', () => { + it('throws an error', () => { + process.env.TEST_NUMBER = ''; + expect(() => { + getNumberArray('TEST_NUMBER'); + }).toThrow(new NotDefinedConfigError('TEST_NUMBER')); + }); + + describe('allow undefined', () => { + it('returns an empty array', () => { + process.env.TEST_NUMBER = ''; + const value = getNumberArray('TEST_NUMBER', { allowUndefined: true }); + expect(value).toEqual([]); + }); + }); + + describe('default is set', () => { + it('returns the default', () => { + process.env.TEST_NUMBER = ''; + const value = getNumberArray('TEST_NUMBER', { + default: [10.33, 20.33], + }); + expect(value).toEqual([10.33, 20.33]); + }); + }); + }); + describe('environment variable is not set', () => { + it('throws an error', () => { + expect(() => { + getNumberArray('TEST_NUMBER'); + }).toThrow(new NotDefinedConfigError('TEST_NUMBER')); + }); + + describe('allow undefined', () => { + it('returns an empty array', () => { + const value = getNumberArray('TEST_NUMBER', { allowUndefined: true }); + expect(value).toEqual([]); + }); + }); + + describe('default is set', () => { + it('returns the default', () => { + const value = getNumberArray('TEST_NUMBER', { + default: [10.33, 20.33], + }); + expect(value).toEqual([10.33, 20.33]); + }); + }); + }); +}); diff --git a/src/number.ts b/src/number.ts index 5165b89..8a7d0e8 100644 --- a/src/number.ts +++ b/src/number.ts @@ -128,6 +128,137 @@ export function getNumber(name: string, options: GetNumberOptions = {}) { throw error; } +type GetNumberArrayOptionsAllowList = { + allowList?: number[]; + allowRange?: undefined; +}; +type GetNumberArrayOptionsAllowRange = { + allowList?: undefined; + allowRange?: [number, number]; +}; +type GetNumberArrayOptionsAllow = + | GetNumberArrayOptionsAllowList + | GetNumberArrayOptionsAllowRange; + +type GetNumberArrayOptionsAllowUndefined = { + allowUndefined: true; + default?: undefined; +} & GetNumberArrayOptionsAllow; +type GetNumberArrayOptionsNoDefault = { + allowUndefined?: false; + default?: undefined; +} & GetNumberArrayOptionsAllow; +type GetNumberArrayOptionsDefault = { + allowUndefined?: false; + default: number[]; +} & GetNumberArrayOptionsAllow; + +export type GetNumberArrayOptions = + | GetNumberArrayOptionsAllowUndefined + | GetNumberArrayOptionsNoDefault + | GetNumberArrayOptionsDefault; + +/** + * Returns an environmental variable as a `number` array. Optionally the value + * can be validated by an `allowList` or `allowRange`. + */ +export function getNumberArray( + name: string, + options: { + allowUndefined: true; + default?: undefined; + } & ( + | { + allowList?: undefined; + allowRange?: undefined; + } + | { + allowList: number[]; + } + | { + allowRange: [number, number]; + } + ), +): number[]; +/** + * Returns an environmental variable as a `number` array or, if undefined, + * throws an error. Optionally the value can be validated by an `allowList` or + * `allowRange`. + */ +export function getNumberArray( + name: string, + options?: { + allowUndefined?: false; + default?: undefined; + } & ( + | { + allowList?: undefined; + allowRange?: undefined; + } + | { + allowList: number[]; + } + | { + allowRange: [number, number]; + } + ), +): number[]; +/** + * Returns an environmental variable as a `number` array or, if undefined, the + * provided default value. Optionally the value can be validated by an + * `allowList` or `allowRange`. + */ +export function getNumberArray( + name: string, + options: { + allowUndefined?: false; + default: number[]; + } & ( + | { + allowList?: undefined; + allowRange?: undefined; + } + | { + allowList: number[]; + } + | { + allowRange: [number, number]; + } + ), +): number[]; +export function getNumberArray( + name: string, + options: GetNumberArrayOptions = {}, +) { + const { + allowList, + allowRange, + allowUndefined, + default: defaultValue, + } = options; + const stringValue = process.env[name]; + if (stringValue !== undefined && stringValue !== '') { + const values = stringValue.split(',').map((value) => { + const numberValue = Number(value); + validateList(name, value, numberValue, allowList); + validateRange(name, value, numberValue, allowRange); + return numberValue; + }); + return values; + } + if (defaultValue !== undefined) { + return defaultValue; + } + if (allowUndefined) { + return []; + } + const error = new NotDefinedConfigError(name); + if (Error.captureStackTrace) { + Error.captureStackTrace(error, getNumberArray); + } + throw error; +} + const validateList = ( name: string, stringValue: string,