From 1d9d2379f7a7d14afd81bcf72a097c119832fe4e Mon Sep 17 00:00:00 2001 From: ggFROOK <93028567+NewBieCoderXD@users.noreply.github.com> Date: Mon, 23 Sep 2024 21:51:01 +0700 Subject: [PATCH] conditionally required field, requiredWhen validator spec (#223) --- README.md | 1 + src/core.ts | 13 ++++ src/types.ts | 2 + tests/requiredWhen.test.ts | 124 +++++++++++++++++++++++++++++++++++++ 4 files changed, 140 insertions(+) create mode 100644 tests/requiredWhen.test.ts diff --git a/README.md b/README.md index 274ce23..b42bc40 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ Each validation function accepts an (optional) object with the following attribu - `desc` - A string that describes the env var. - `example` - An example value for the env var. - `docs` - A URL that leads to more detailed documentation about the env var. +- `requiredWhen` - A boolean function that specify when the env var is required. Use With default: undefined (optional value). ## Custom validators diff --git a/src/core.ts b/src/core.ts index 5c37ec4..ea6fbb5 100644 --- a/src/core.ts +++ b/src/core.ts @@ -100,6 +100,19 @@ export function getSanitizedEnv( } } + for (const k of varKeys) { + if (errors[k] == undefined) { + const spec = castedSpecs[k] + if ( + cleanedEnv[k] == undefined && + spec.requiredWhen !== undefined && + spec.requiredWhen(cleanedEnv) + ) { + errors[k] = new EnvMissingError(formatSpecDescription(spec)) + } + } + } + const reporter = options?.reporter || defaultReporter reporter({ errors, env: cleanedEnv }) return cleanedEnv diff --git a/src/types.ts b/src/types.ts index 606b47d..cc0f772 100644 --- a/src/types.ts +++ b/src/types.ts @@ -24,6 +24,8 @@ export interface Spec { * This is handy for env vars that are required for production environments, but optional for development and testing. */ devDefault?: NonNullable | undefined + + requiredWhen?: (cleanedEnv: any) => boolean | undefined } type OptionalAttrs = diff --git a/tests/requiredWhen.test.ts b/tests/requiredWhen.test.ts new file mode 100644 index 0000000..0142b1b --- /dev/null +++ b/tests/requiredWhen.test.ts @@ -0,0 +1,124 @@ +// let defaultReporter: jest.Mock = jest.fn().mockImplementation(() => {}) +// jest.mock('../src/reporter.ts', () => { +// return { +// defaultReporter: defaultReporter, +// } +// }) +import { bool, cleanEnv, defaultReporter, EnvMissingError, Spec, num, EnvError } from '../src' +jest.mock('../src/reporter') +const mockedDefaultReporter: jest.Mock = > defaultReporter; +mockedDefaultReporter.mockImplementation((a) => {console.log(a)}) +describe('required when', () => { + beforeEach(() => { + mockedDefaultReporter.mockClear() + }) + test("isn't required", () => { + cleanEnv( + { + autoExtractId: "true", + }, + { + autoExtractId: bool(), + id: num({ + default: undefined, + requiredWhen: (cleanedEnv) => !cleanedEnv['autoExtractId'], + }), + }, + ) + expect(mockedDefaultReporter).toHaveBeenCalledTimes(1) + expect(mockedDefaultReporter).toHaveBeenCalledWith({ + env: { + autoExtractId: true, + id: undefined, + }, + errors: {} + }) + }) + + test("required but not provided", () => { + cleanEnv( + { + autoExtractId: "false", + }, + { + autoExtractId: bool(), + id: num({ + default: undefined, + requiredWhen: (cleanedEnv) => !cleanedEnv['autoExtractId'], + }), + }, + ) + expect(mockedDefaultReporter).toHaveBeenCalledTimes(1) + expect(mockedDefaultReporter).toHaveBeenCalledWith({ + env: { + autoExtractId: false, + id: undefined, + }, + errors: { + id: new EnvMissingError( + formatSpecDescription( + num({ + default: undefined, + requiredWhen: (cleanedEnv) => !cleanedEnv['autoExtractId'], + }), + ), + ), + }, + }) + }) + + test("required and provided", () => { + cleanEnv( + { + autoExtractId: "false", + id: "123" + }, + { + autoExtractId: bool(), + id: num({ + default: undefined, + requiredWhen: (cleanedEnv) => !cleanedEnv['autoExtractId'], + }), + }, + ) + expect(mockedDefaultReporter).toHaveBeenCalledTimes(1) + expect(mockedDefaultReporter).toHaveBeenCalledWith({ + env: { + autoExtractId: false, + id: 123, + }, + errors: {}, + }) + }) + + test("required but failed to parse", () => { + cleanEnv( + { + autoExtractId: "false", + id: "abc" + }, + { + autoExtractId: bool(), + id: num({ + default: undefined, + requiredWhen: (cleanedEnv) => !cleanedEnv['autoExtractId'], + }), + }, + ) + expect(mockedDefaultReporter).toHaveBeenCalledTimes(1) + expect(mockedDefaultReporter).toHaveBeenCalledWith({ + env: { + autoExtractId: false, + id: undefined, + }, + errors: { + id: new EnvError(`Invalid number input: "abc"`) + }, + }) + }) +}) +function formatSpecDescription(spec: Spec) { + const egText = spec.example ? ` (eg. "${spec.example}")` : '' + const docsText = spec.docs ? `. See ${spec.docs}` : '' + return `${spec.desc}${egText}${docsText}` +}