Skip to content

Commit

Permalink
Refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanoruth committed Apr 24, 2023
1 parent 25faf7a commit 984075b
Show file tree
Hide file tree
Showing 16 changed files with 279 additions and 235 deletions.
60 changes: 6 additions & 54 deletions src/EnvParser.test.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,11 @@
import { expect } from 'chai'
import { EnvConfig, envParser } from './EnvParser'
import { envParser } from './EnvParser'
import { test } from 'mocha'

describe('EnvParser', () => {
it('Custom env value', () => {
expect(envParser({ port: { type: 'number', env: 'FOO' } }, { FOO: '80' })).eql({ port: 80 })
expect(envParser({ port: { type: 'string', env: 'FOO' } }, { FOO: '80' })).eql({ port: '80' })
expect(envParser({ port: { type: 'boolean', env: 'FOO' } }, { FOO: 'true' })).eql({ port: true })
expect(envParser({ port: { type: 'number', env: 'FOO' } }, { FOO: '80', port: '81' })).eql({ port: 80 })
expect(envParser({ port: { type: 'number' } }, { FOO: '80', port: '81' })).eql({ port: 81 })
})

describe('Number', () => {
it('Parse Number', () => {
expect(envParser({ port: { type: 'number' } }, { port: '80' })).eql({ port: 80 })
})

it('Optional Number', () => {
expect(envParser({ port: { type: 'number', optional: true } }, {})).eql({ port: null })
})
})

describe('String', () => {
it('Parse String', () => {
expect(envParser({ folder: { type: 'string' } }, { folder: 'images' })).eql({ folder: 'images' })
})

it('Optional String', () => {
expect(envParser({ folder: { type: 'string', optional: true } }, {})).eql({ folder: null })
})
})

describe('Boolean', () => {
it('Parse Boolean', () => {
const config: EnvConfig = { force: { type: 'boolean' } }

expect(envParser(config, { force: 'true' })).eql({ force: true })
expect(envParser(config, { force: 'false' })).eql({ force: false })
expect(envParser(config, { force: '1' })).eql({ force: true })
expect(envParser(config, { force: '0' })).eql({ force: false })
expect(envParser(config, { force: 'TRUE' })).eql({ force: true })
expect(envParser(config, { force: 'FALSE' })).eql({ force: false })
})

it('Optional Boolean', () => {
expect(envParser({ force: { type: 'boolean', optional: true } }, {})).eql({ force: null })
})
})

describe('Const', () => {
it('Parse Const', () => {
//
})

it('Optional Const', () => {
//
})
test('it uses the correct enviroment key', () => {
expect(envParser({ port: { type: 'string' } }, { port: '80' })).eql({ port: '80' })
expect(envParser({ port: { type: 'string', env: 'PORT' } }, { PORT: '80' })).eql({ port: '80' })
expect(() => envParser({ port: { type: 'string', env: 'PORT' } }, { port: '80' })).throw
})
})
84 changes: 14 additions & 70 deletions src/EnvParser.ts
Original file line number Diff line number Diff line change
@@ -1,80 +1,24 @@
import { parseBoolean, parseConst, parseNumber, parseString } from './TypeParser'

type EnvNumber = { type: 'number'; defaultValue?: number; env?: string; optional?: boolean }
type EnvString = { type: 'string'; defaultValue?: string; env?: string; optional?: boolean }
type EnvBoolean = { type: 'boolean'; defaultValue?: boolean; env?: string; optional?: boolean }
type EnvConst<T = readonly string[]> = {
type: 'const'
options: T
defaultValue?: string
env?: string
optional?: boolean
}

type EnvInput = EnvNumber | EnvString | EnvBoolean | EnvConst

type ReturnType<T extends EnvInput> = T extends EnvNumber
? T extends Omit<EnvBoolean, 'optional'> & { optional: true }
? number | null
: number
: T extends EnvBoolean
? T extends Omit<EnvBoolean, 'optional'> & { optional: true }
? boolean | null
: boolean
: T extends EnvString
? T extends Omit<EnvString, 'optional'> & { optional: true }
? string | null
: string
: T extends EnvConst
? T extends Omit<EnvConst, 'optional'> & { optional: true }
? T['options'][number] | null
: T['options'][number]
: unknown

export type EnvConfig = Record<string, EnvInput>
type Output<T extends EnvConfig> = { [k in keyof T]: ReturnType<T[k]> }

export function envParser<T extends EnvConfig>(config: T, args?: NodeJS.ProcessEnv): Output<T> {
import { parseEnvironmentVariable } from './ParseVariable'
import { EnvInput, EnvResult } from './Types'

export type EnvConfig<T extends readonly string[]> = Record<string, EnvInput<T>>
type Output<T extends EnvConfig<I>, I extends readonly string[]> = { [k in keyof T]: EnvResult<T[k], I> }

// Prettier is ignored because of the usage of const here.
// prettier-ignore
export function envParser<T extends EnvConfig<I>, const I extends readonly string[]>(
config: T,
args?: NodeJS.ProcessEnv
): Output<T, I> {
const env = { ...process.env, ...args }
const parsedEnv: { [k: string]: string | number | boolean | null } = {}

for (const [entry, options] of Object.entries(config)) {
const key = options.env || entry
const value = env[key]

if (options.type === 'number') {
parsedEnv[entry] = parseNumber({
key,
value,
defaultValue: options.defaultValue,
optional: options.optional,
})
} else if (options.type === 'string') {
parsedEnv[entry] = parseString({
key,
value,
defaultValue: options.defaultValue,
optional: options.optional,
})
} else if (options.type === 'boolean') {
parsedEnv[entry] = parseBoolean({
key,
value,
defaultValue: options.defaultValue,
optional: options.optional,
})
} else if (options.type === 'const') {
parsedEnv[entry] = parseConst({
key,
value,
defaultValue: options.defaultValue,
options: options.options,
optional: options.optional,
})
} else {
throw new Error(`Env type not implemented`)
}
parsedEnv[entry] = parseEnvironmentVariable(key, value, options)
}

return parsedEnv as Output<T>
return parsedEnv as Output<T, I>
}
33 changes: 33 additions & 0 deletions src/ParseVariable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { parseBoolean, parseConst, parseNumber, parseString } from './Parsers'
import { EnvInput } from './Types'

export function parseEnvironmentVariable<T extends readonly string[]>(
argKey: string,
argValue: string | undefined,
config: EnvInput<T>
) {
try {
if (config.type === 'boolean') {
return parseBoolean({ value: argValue, defaultValue: config.defaultValue, optional: config.optional })
} else if (config.type === 'number') {
return parseNumber({ value: argValue, defaultValue: config.defaultValue, optional: config.optional })
} else if (config.type === 'const') {
return parseConst({
value: argValue,
options: config.options,
defaultValue: config.defaultValue,
optional: config.optional,
})
} else if (config.type === 'string') {
return parseString({ value: argValue, defaultValue: config.defaultValue, optional: config.optional })
}
} catch (error) {
if (error instanceof Error) {
throw new Error(`Unable to parse variable "${argKey}"`, { cause: error })
} else {
throw new Error('An invalid error happened')
}
}

throw new Error('Option type not implemented')
}
23 changes: 23 additions & 0 deletions src/Parsers/ParseBoolean.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, test } from 'mocha'
import { parseBoolean } from './ParseBoolean'
import { expect } from 'chai'

describe('ParseBoolean', () => {
test('errors out when value is not defined and the value is required', () => {
expect(() => parseBoolean({ value: undefined })).to.throw
expect(parseBoolean({ value: undefined, optional: true })).to.null
})

test('True values', () => {
expect(parseBoolean({ value: '' })).to.be.true
expect(parseBoolean({ value: '1' })).to.be.true
expect(parseBoolean({ value: 'true' })).to.be.true
expect(parseBoolean({ value: 'TRUE' })).to.be.true
})

test('False values', () => {
expect(parseBoolean({ value: '0' })).to.be.false
expect(parseBoolean({ value: 'false' })).to.be.false
expect(parseBoolean({ value: 'FALSE' })).to.be.false
})
})
34 changes: 34 additions & 0 deletions src/Parsers/ParseBoolean.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export function parseBoolean(args: {
value: string | undefined
defaultValue?: boolean
optional?: boolean
}): boolean | null {
if (typeof args.value === 'undefined') {
if (typeof args.defaultValue !== 'undefined') {
return args.defaultValue
}

if (args.optional === true) {
return null
}

throw new Error(`Value was not defined`)
}

if (args.value.trim().length === 0) {
return true // If the value is defined but not set to anything specific we treat it as true.
}

switch (args.value.trim().toLowerCase()) {
case 'true':
case '1':
return true

case 'false':
case '0':
return false

default:
throw new Error(`Unable to parse value`)
}
}
19 changes: 19 additions & 0 deletions src/Parsers/ParseConst.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, test } from 'mocha'
import { parseConst } from './ParseConst'
import { expect } from 'chai'

describe('ParseConst', () => {
test('Empty value', () => {
expect(parseConst({ value: '', options: ['foo', 'bar'], optional: true })).to.eql(null)
expect(() => parseConst({ value: '', options: ['foo', 'bar'] })).to.throw
})

test('Invalid value', () => {
expect(() => parseConst({ value: 'baz', options: ['FOO', 'BAR'] })).to.throw
expect(() => parseConst({ value: 'baz', options: ['FOO', 'BAR'], optional: true })).to.throw
})

test('Valid value', () => {
expect(parseConst({ value: 'foo', options: ['foo', 'bar'] })).to.eql('foo')
})
})
28 changes: 28 additions & 0 deletions src/Parsers/ParseConst.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export function parseConst<T extends readonly string[]>(args: {
value: string | undefined
options: T
defaultValue?: T[number]
optional?: boolean
}): string | null {
if (typeof args.value === 'undefined' || args.value.trim().length === 0) {
if (typeof args.defaultValue !== 'undefined') {
if (!args.options.includes(args.defaultValue)) {
throw new Error(`Invalid defaultValue "${args.defaultValue}"`)
}

return args.defaultValue
}

if (args.optional) {
return null
}

throw new Error(`Missing value`)
}

if (!args.options.includes(args.value)) {
throw new Error(`Invalid value "${args.value}"`)
}

return args.value
}
16 changes: 16 additions & 0 deletions src/Parsers/ParseNumber.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { describe, test } from 'mocha'
import { parseNumber } from './ParseNumber'
import { expect } from 'chai'

describe('ParseNumber', () => {
test('True values', () => {
expect(parseNumber({ value: '80' })).to.eql(80)
expect(parseNumber({ value: '', optional: true })).to.eql(null)
expect(parseNumber({ value: undefined, optional: true })).to.eql(null)
})

test('False values', () => {
expect(() => parseNumber({ value: '' })).to.throw
expect(() => parseNumber({ value: undefined })).to.throw
})
})
27 changes: 27 additions & 0 deletions src/Parsers/ParseNumber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export function parseNumber(args: {
value: string | undefined
defaultValue?: number
optional?: boolean
}): number | null {
const value = args.value?.trim()

if (typeof value === 'undefined' || value.length === 0) {
if (typeof args.defaultValue !== 'undefined') {
return args.defaultValue
}

if (args.optional) {
return null
}

throw new Error(`Missing value"`)
}

const data = parseFloat(value)

if (isNaN(data)) {
throw new Error(`Unable to parse value`)
}

return data
}
17 changes: 17 additions & 0 deletions src/Parsers/ParseString.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, test } from 'mocha'
import { parseString } from './ParseString'
import { expect } from 'chai'

describe('ParseString', () => {
test('True values', () => {
expect(parseString({ value: 'foo' })).to.eql('foo')
expect(parseString({ value: 'foo ' })).to.eql('foo ')
expect(parseString({ value: '', optional: true })).to.eql(null)
expect(parseString({ value: undefined, optional: true })).to.eql(null)
})

test('False values', () => {
expect(() => parseString({ value: '' })).to.throw
expect(() => parseString({ value: undefined })).to.throw
})
})
19 changes: 19 additions & 0 deletions src/Parsers/ParseString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export function parseString(args: {
value: string | undefined
defaultValue?: string
optional?: boolean
}): string | null {
if (typeof args.value === 'undefined' || args.value?.trim().length === 0) {
if (typeof args.defaultValue !== 'undefined' && args.defaultValue.trim().length > 0) {
return args.defaultValue
}

if (args.optional) {
return null
}

throw new Error(`Missing value`)
}

return args.value
}
4 changes: 4 additions & 0 deletions src/Parsers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './ParseBoolean'
export * from './ParseConst'
export * from './ParseNumber'
export * from './ParseString'
Loading

0 comments on commit 984075b

Please sign in to comment.