diff --git a/README.md b/README.md index 6255165..f8041c5 100644 --- a/README.md +++ b/README.md @@ -86,8 +86,7 @@ Node's `process.env` only stores strings, but sometimes you want to retrieve oth URL, email address). To these ends, the following validation functions are available: - `str()` - Passes string values through, will ensure a value is present unless a - `default` value is given. Note that an empty string is considered a valid value - - if this is undesirable you can easily create your own validator (see below) + `default` value is given. Note that an empty string is not considered a valid value - `bool()` - Parses env var strings `"1", "0", "true", "false", "t", "f", "yes", "no", "on", "off"` into booleans - `num()` - Parses an env var (eg. `"42", "0.23", "1e5"`) into a Number - `email()` - Ensures an env var is an email address diff --git a/src/core.ts b/src/core.ts index a53fa52..fedc0b2 100644 --- a/src/core.ts +++ b/src/core.ts @@ -1,6 +1,6 @@ import { EnvError, EnvMissingError } from './errors' -import type { CleanOptions, SpecsOutput, Spec, ValidatorSpec } from './types' import { defaultReporter } from './reporter' +import type { CleanOptions, Spec, SpecsOutput, ValidatorSpec } from './types' /** * Validate a single env var, given a spec object @@ -40,8 +40,17 @@ export function formatSpecDescription(spec: Spec) { return `${spec.desc}${egText}${docsText}` } -const readRawEnvValue = (env: unknown, k: keyof T | 'NODE_ENV'): string | T[keyof T] => { - return (env as any)[k] +const readRawEnvValue = ( + env: unknown, + k: keyof T | 'NODE_ENV', +): string | undefined | T[keyof T] => { + const result = (env as any)[k] + + if (typeof result == 'string' && !result.trim()) { + return undefined + } + + return result } /** diff --git a/tests/requiredWhen.test.ts b/tests/requiredWhen.test.ts index 2389d6e..e1ac8f4 100644 --- a/tests/requiredWhen.test.ts +++ b/tests/requiredWhen.test.ts @@ -3,8 +3,8 @@ import { bool, cleanEnv, defaultReporter, EnvMissingError, num, EnvError } from import { formatSpecDescription } from '../src/core' vi.mock('../src/reporter') -const mockedDefaultReporter: vi.Mock = >defaultReporter; -mockedDefaultReporter.mockImplementation(() => { }) +const mockedDefaultReporter: vi.Mock = >defaultReporter +mockedDefaultReporter.mockImplementation(() => {}) describe('requiredWhen', () => { beforeEach(() => { @@ -13,7 +13,7 @@ describe('requiredWhen', () => { test("isn't required", () => { cleanEnv( { - autoExtractId: "true", + autoExtractId: 'true', }, { autoExtractId: bool(), @@ -29,14 +29,54 @@ describe('requiredWhen', () => { autoExtractId: true, id: undefined, }, - errors: {} + errors: {}, + }) + }) + + test("isn't required but empty string provided", () => { + cleanEnv( + { + id: '', + }, + { + id: num({ + default: undefined, + requiredWhen: () => false, + }), + }, + ) + expect(mockedDefaultReporter).toHaveBeenCalledTimes(1) + expect(mockedDefaultReporter).toHaveBeenCalledWith({ + env: { + id: undefined, + }, + errors: {}, + }) + + cleanEnv( + { + autoExtractId: '', + }, + { + autoExtractId: bool({ + default: undefined, + requiredWhen: () => false, + }), + }, + ) + expect(mockedDefaultReporter).toHaveBeenCalledTimes(2) + expect(mockedDefaultReporter).toHaveBeenCalledWith({ + env: { + autoExtractId: undefined, + }, + errors: {}, }) }) test('required but not provided', () => { cleanEnv( { - autoExtractId: "false", + autoExtractId: 'false', }, { autoExtractId: bool(), @@ -68,8 +108,8 @@ describe('requiredWhen', () => { test('required and provided', () => { cleanEnv( { - autoExtractId: "false", - id: "123" + autoExtractId: 'false', + id: '123', }, { autoExtractId: bool(), @@ -92,8 +132,8 @@ describe('requiredWhen', () => { test('required but failed to parse', () => { cleanEnv( { - autoExtractId: "false", - id: "abc" + autoExtractId: 'false', + id: 'abc', }, { autoExtractId: bool(), @@ -110,7 +150,7 @@ describe('requiredWhen', () => { id: undefined, }, errors: { - id: new EnvError(`Invalid number input: "abc"`) + id: new EnvError(`Invalid number input: "abc"`), }, }) }) diff --git a/tests/validators.test.ts b/tests/validators.test.ts index 60ead85..1ea1d14 100644 --- a/tests/validators.test.ts +++ b/tests/validators.test.ts @@ -39,8 +39,19 @@ test('bool() works with various formats', () => { const off = cleanEnv({ FOO: 'off' }, { FOO: bool() }) expect(off).toEqual({ FOO: false }) + expect(function withEmpty() { + return cleanEnv({ FOO: '' }, { FOO: bool() }, makeSilent) + }).toThrow() + const defaultF = cleanEnv({}, { FOO: bool({ default: false }) }) expect(defaultF).toEqual({ FOO: false }) + + const defaultTWithWhitespace = cleanEnv( + { FOO: ' ' }, + { FOO: bool({ default: true }) }, + makeSilent, + ) + expect(defaultTWithWhitespace).toEqual({ FOO: true }) }) test('num()', () => { @@ -132,8 +143,18 @@ test('url()', () => { }) test('str()', () => { - const withEmpty = cleanEnv({ FOO: '' }, { FOO: str() }) - expect(withEmpty).toEqual({ FOO: '' }) + expect(cleanEnv({ FOO: 'asdf' }, { FOO: str() })).toEqual({ FOO: 'asdf' }) + + expect(function withWhitespace() { + return cleanEnv({ FOO: ' ' }, { FOO: str() }, makeSilent) + }).toThrow() + + const defaultWithWhitespace = cleanEnv( + { FOO: ' ' }, + { FOO: str({ default: 'asdf' }) }, + makeSilent, + ) + expect(defaultWithWhitespace).toEqual({ FOO: 'asdf' }) expect(() => cleanEnv({ FOO: 42 }, { FOO: str() }, makeSilent)).toThrow() })