Skip to content

Commit

Permalink
conditionally required field, requiredWhen validator spec (#223)
Browse files Browse the repository at this point in the history
  • Loading branch information
NewBieCoderXD authored Sep 23, 2024
1 parent d8bc647 commit 1d9d237
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 13 additions & 0 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,19 @@ export function getSanitizedEnv<S>(
}
}

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
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export interface Spec<T> {
* This is handy for env vars that are required for production environments, but optional for development and testing.
*/
devDefault?: NonNullable<T> | undefined

requiredWhen?: (cleanedEnv: any) => boolean | undefined
}

type OptionalAttrs<T> =
Expand Down
124 changes: 124 additions & 0 deletions tests/requiredWhen.test.ts
Original file line number Diff line number Diff line change
@@ -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 = <jest.Mock<typeof defaultReporter>> 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<T>(spec: Spec<T>) {
const egText = spec.example ? ` (eg. "${spec.example}")` : ''
const docsText = spec.docs ? `. See ${spec.docs}` : ''
return `${spec.desc}${egText}${docsText}`
}

0 comments on commit 1d9d237

Please sign in to comment.