Skip to content

Commit

Permalink
Remove optional and throwing of errors in parseEnv, just return defau…
Browse files Browse the repository at this point in the history
…lt if value is invalid
  • Loading branch information
robertherber committed Jan 29, 2024
1 parent 0316efc commit 9ae1b02
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 81 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/eas-build-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ on:
pnpm_version:
required: false
type: string
default: '8'
default: "8"
description: If using pnpm - which version to use
google_service_account_key_path:
required: false
Expand All @@ -65,7 +65,7 @@ on:
bun_version:
required: false
type: string
default: 'latest'
default: latest
description: Set bun version
secrets:
EXPO_TOKEN:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/eas-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ on:
pnpm_version:
required: false
type: string
default: '8'
default: "8"
description: If using pnpm - which version to use
nodeModulePaths:
required: false
Expand All @@ -75,7 +75,7 @@ on:
bun_version:
required: false
type: string
default: 'latest'
default: latest
description: Set bun version
secrets:
EXPO_TOKEN:
Expand Down
46 changes: 32 additions & 14 deletions src/node/parseEnv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,53 @@ import {
} from './parseEnv'

test('parseEnvBoolean', () => {
const result = parseEnvBoolean('isSomething', { }, { isSomething: 'true' })
const result = parseEnvBoolean('isSomething', undefined, { isSomething: 'true' })

expect(result).toEqual(true)
})

test('parseEnvEnum', () => {
const result = parseEnvEnum('isSomething', ['value1', 'value2'], { }, { isSomething: 'value1' })
test('parseEnvBoolean default', () => {
const result = parseEnvBoolean('isSomething', true, { })

expect(result).toEqual(true)
})

test('parseEnvBoolean handle 1 gracefully', () => {
const result = parseEnvBoolean('isSomething', undefined, { isSomething: '1' })

expect(result).toEqual(true)
})

test('parseEnvBoolean handle 0 gracefully', () => {
const result = parseEnvBoolean('isSomething', undefined, { isSomething: '0' })

expect(result).toEqual(false)
})

test('parseEnvEnum should return the value set in process.env', () => {
const result = parseEnvEnum('isSomething', ['value1', 'value2'], undefined, { isSomething: 'value1' })

expect(result).toEqual('value1')
})

test('parseEnvEnum when not in range', () => {
let error
try {
parseEnvEnum('isSomething', ['value1', 'value2'], { }, { isSomething: 'value3' })
} catch (e) {
error = e
}
expect(error).toBeDefined()
expect(error).toHaveProperty('message', 'Environment variable "isSomething" was "value3", should be one of [value1, value2]')
test('parseEnvEnum can be undefined if no default is set', () => {
const res = parseEnvEnum('isSomething', ['value1', 'value2'], undefined, { isSomething: 'value3' })
expect(res).toBeUndefined()
})

test('parseEnvEnum should use default value when not in range', () => {
const result = parseEnvEnum(
'isSomething',
['value1', 'value2'],
{ defaultValue: 'value1' },
'value1',
{ isSomething: 'value3' },
)

expect(result).toEqual('value1')
})

test('parseEnvNumber', () => {
const result = parseEnvNumber('aNumber', { }, { aNumber: '3' })
const result = parseEnvNumber('aNumber', undefined, { aNumber: '3' })

expect(result).toEqual(3)
})
Expand All @@ -47,3 +59,9 @@ test('parseEnvJSON', () => {

expect(result).toEqual({ something: 'cool' })
})

test('parseEnvJSON return defaultValue when null', () => {
const result = parseEnvJSON('isSomething', { defaultish: true }, { isSomething: undefined })

expect(result).toEqual({ defaultish: true })
})
101 changes: 38 additions & 63 deletions src/node/parseEnv.ts
Original file line number Diff line number Diff line change
@@ -1,136 +1,111 @@
import { isNotNullOrUndefined } from '..'

export function parseEnvNumber<T extends number | undefined, TOptional extends boolean = false>(
export function parseEnvNumber<
T extends number,
TDefault extends T | undefined
>(
prop: string,
opts?: { readonly defaultValue?: T, readonly optional?: TOptional},
defaultValue?: TDefault,
env = process.env,
): TOptional extends false ? number : (T extends number ? number : number | undefined) {
): T | TDefault {
const rawValue = env[prop]
if (!rawValue) {
const defaultValue = opts?.defaultValue
if (isNotNullOrUndefined(defaultValue)) {
return defaultValue as number
}

if (opts?.optional) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return undefined
return defaultValue
}

throw new Error(`Environment variable "${prop}" is required`)
return undefined as TDefault
}
try {
return parseFloat(rawValue)
return parseFloat(rawValue) as T
} catch (e) {
throw new Error(`Failed to parse environment variable "${prop}", expected number got "${rawValue}"`)
console.error(`Failed to parse environment variable "${prop}", expected number got "${rawValue}"`)
return undefined as TDefault
}
}

export function parseEnvJSON<T extends unknown | undefined, TOptional extends boolean = false>(
// todo [2024-02-01]: allow to only specify first type argument
export function parseEnvJSON<
T,
TDefault extends T | undefined
>(
prop: string,
opts?: { readonly defaultValue?: T, readonly optional?: TOptional},
defaultValue?: TDefault,
env = process.env,
): TOptional extends false ? NonNullable<T> : (T extends undefined ? T : NonNullable<T>) {
): T | TDefault {
const rawValue = env[prop]
if (!rawValue) {
const defaultValue = opts?.defaultValue
if (isNotNullOrUndefined(defaultValue)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return defaultValue as NonNullable<T>
}

if (opts?.optional) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return undefined
}

throw new Error(`Environment variable "${prop}" is required`)
return undefined as TDefault
}
try {
return JSON.parse(rawValue)
} catch (e) {
throw new Error(`Failed to parse environment variable "${prop}", expected JSON got "${rawValue}"`)
console.error(`Failed to parse environment variable "${prop}", expected JSON got "${rawValue}"`)
return undefined as TDefault
}
}

export function parseEnvBoolean<T extends boolean | undefined, TOptional extends boolean = false>(
export function parseEnvBoolean<T extends boolean, TDefault extends T | undefined>(
prop: string,
opts?: { readonly defaultValue?: T, readonly optional?: TOptional},
defaultValue?: T,
env = process.env,
): TOptional extends false ? boolean : (T extends boolean ? boolean : boolean | undefined) {
): T | TDefault {
const rawValue = env[prop]
if (!rawValue) {
const defaultValue = opts?.defaultValue
if (isNotNullOrUndefined(defaultValue)) {
return defaultValue as boolean
}

if (opts?.optional) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return undefined
return defaultValue
}

throw new Error(`Environment variable "${prop}" is required`)
return undefined as TDefault
}
try {
const value = JSON.parse(rawValue) as boolean

if (typeof value !== 'boolean') {
if (value === 1) {
return true
return true as T
}
if (value === 0) {
return false
return false as T
}
throw new Error(`Failed to parse environment variable "${prop}", expected boolean got "${rawValue}"`)
} else {
return value as T
}

return value
return defaultValue as TDefault
} catch (e) {
throw new Error(`Failed to parse environment variable "${prop}", expected boolean got "${rawValue}"`)
console.error(`Failed to parse environment variable "${prop}", expected boolean got "${rawValue}"`)
return defaultValue as TDefault
}
}

type EnumOrArrayOfLiterals<T extends string> = ArrayLike<T> | { readonly [s: string]: T; }

export function parseEnvEnum<
T extends string,
TOptional extends boolean = false,
TOpts extends { readonly defaultValue?: TOptional extends false ? NonNullable<T> : T | undefined, readonly optional?: TOptional} = { readonly defaultValue?: TOptional extends false ? NonNullable<T> : T | undefined, readonly optional?: TOptional}
TDefault extends T | undefined
>(
prop: string,
validValues: EnumOrArrayOfLiterals<T>,
opts?: TOptional extends false ? Required<TOpts> : TOpts,
defaultValue?: TDefault,
env = process.env,
): TOptional extends false ? NonNullable<T> : T | undefined {
): T | TDefault {
const rawValue = env[prop]

const enumOrArray = Object.values(validValues)
// @ts-expect-error this is contained
const isPartOfEnumOrArray = rawValue ? enumOrArray.includes(rawValue) : false

if (isPartOfEnumOrArray) {
return rawValue as NonNullable<T>
return rawValue as T & TDefault
}

const defaultValue = opts?.defaultValue
if (isNotNullOrUndefined(defaultValue) && enumOrArray.includes(defaultValue)) {
return defaultValue
}

if (opts?.optional) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return undefined
}

const error = rawValue
? new Error(`Environment variable "${prop}" was "${rawValue}", should be one of [${enumOrArray.join(', ')}]`)
: new Error(`Environment variable "${prop}" is required`)

throw error
return undefined as unknown as T & TDefault
}

0 comments on commit 9ae1b02

Please sign in to comment.