Skip to content

Commit

Permalink
feat(parser): allow to add identifiers
Browse files Browse the repository at this point in the history
  • Loading branch information
RomainLanz committed Nov 17, 2023
1 parent f4de0fb commit 5bba7f4
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 13 deletions.
16 changes: 16 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import { schema as envSchema, type ValidateFn } from '@poppinss/validator-lite'
import { EnvValidator } from './validator.js'
import { EnvProcessor } from './processor.js'
import { EnvParser } from './parser.js'

/**
* A wrapper over "process.env" with types information.
Expand Down Expand Up @@ -53,6 +54,21 @@ export class Env<EnvValues extends Record<string, any>> {
return new Env(validator.validate(values))
}

/**
* Define an identifier for any environment value. The callback is invoked
* when the value match the identifier to modify its interpolation.
*/
static identifier(name: string, callback: (value: string) => Promise<string> | string): void {
EnvParser.identifier(name, callback)
}

/**
* Remove an identifier
*/
static removeIdentifier(name: string): void {
EnvParser.removeIdentifier(name)
}

/**
* The schema builder for defining validation rules
*/
Expand Down
8 changes: 7 additions & 1 deletion src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* file that was distributed with this source code.
*/

import { Exception } from '@poppinss/utils'
import { createError, Exception } from '@poppinss/utils'

/**
* Exception raised when one or more env variables
Expand All @@ -18,3 +18,9 @@ export const E_INVALID_ENV_VARIABLES = class EnvValidationException extends Exce
static code = 'E_INVALID_ENV_VARIABLES'
help: string = ''
}

export const E_IDENTIFIER_ALREADY_DEFINED = createError<[string]>(
'The identifier "%s" is already defined',
'E_IDENTIFIER_ALREADY_DEFINED',
500
)
50 changes: 45 additions & 5 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import dotenv, { DotenvParseOutput } from 'dotenv'
import { E_IDENTIFIER_ALREADY_DEFINED } from './errors.js'

/**
* Env parser parses the environment variables from a string formatted
Expand Down Expand Up @@ -52,6 +53,7 @@ import dotenv, { DotenvParseOutput } from 'dotenv'
export class EnvParser {
#envContents: string
#preferProcessEnv: boolean = true
static #identifiers: Record<string, (value: string) => Promise<string> | string> = {}

constructor(envContents: string, options?: { ignoreProcessEnv: boolean }) {
if (options?.ignoreProcessEnv) {
Expand All @@ -61,6 +63,25 @@ export class EnvParser {
this.#envContents = envContents
}

/**
* Define an identifier for any environment value. The callback is invoked
* when the value match the identifier to modify its interpolation.
*/
static identifier(name: string, callback: (value: string) => Promise<string> | string): void {
if (this.#identifiers[name]) {
throw new E_IDENTIFIER_ALREADY_DEFINED([name])
}

this.#identifiers[name] = callback
}

/**
* Remove an identifier
*/
static removeIdentifier(name: string): void {
delete this.#identifiers[name]
}

/**
* Returns the value from the parsed object
*/
Expand Down Expand Up @@ -174,12 +195,31 @@ export class EnvParser {
/**
* Parse the env string to an object of environment variables.
*/
parse(): DotenvParseOutput {
async parse(): Promise<DotenvParseOutput> {
const envCollection = dotenv.parse(this.#envContents.trim())
const identifiers = Object.keys(EnvParser.#identifiers)
let result: DotenvParseOutput = {}

$keyLoop: for (const key in envCollection) {
const value = this.#getValue(key, envCollection)

if (value.includes(':')) {
for (const identifier of identifiers) {
if (value.startsWith(identifier)) {
result[key] = await EnvParser.#identifiers[identifier](
value.substring(identifier.length + 1)
)

continue $keyLoop
}
}

result[key] = value
} else {
result[key] = value
}
}

return Object.keys(envCollection).reduce<DotenvParseOutput>((result, key) => {
result[key] = this.#getValue(key, envCollection)
return result
}, {})
return result
}
}
8 changes: 5 additions & 3 deletions src/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,17 @@ export class EnvProcessor {
/**
* Parse env variables from raw contents
*/
#processContents(envContents: string, store: Record<string, any>) {
async #processContents(envContents: string, store: Record<string, any>) {
/**
* Collected env variables
*/
if (!envContents.trim()) {
return store
}

const values = new EnvParser(envContents).parse()
const parser = new EnvParser(envContents)
const values = await parser.parse()

Object.keys(values).forEach((key) => {
let value = process.env[key]

Expand Down Expand Up @@ -70,7 +72,7 @@ export class EnvProcessor {
* Collected env variables
*/
const envValues: Record<string, any> = {}
envFiles.forEach(({ contents }) => this.#processContents(contents, envValues))
await Promise.all(envFiles.map(({ contents }) => this.#processContents(contents, envValues)))
return envValues
}

Expand Down
19 changes: 19 additions & 0 deletions test/env.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@ test.group('Env', (group) => {
delete process.env.ENV_HOST
})

test('define identifier', async ({ assert, cleanup, fs }) => {
assert.plan(1)

cleanup(() => {
Env.removeIdentifier('file')
})

Env.identifier('file', (_value: string) => {
assert.isTrue(true)

return '3000'
})

await fs.create('.env', 'PORT=file:romain')
await Env.create(fs.baseUrl, {
PORT: Env.schema.number(),
})
})

test('read values from process.env', ({ assert, expectTypeOf, cleanup }) => {
process.env.PORT = '4000'
cleanup(() => {
Expand Down
40 changes: 36 additions & 4 deletions test/parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ test.group('Env Parser', () => {
].join('\n')

const parser = new EnvParser(envString)
const parsed = parser.parse()
const parsed = await parser.parse()
expectTypeOf(parsed).toEqualTypeOf<DotenvParseOutput>()
assert.deepEqual(parsed, {
'PORT': '3333',
Expand All @@ -53,6 +53,38 @@ test.group('Env Parser', () => {
})
})

test('define identifier', async ({ assert, cleanup, expectTypeOf }) => {
cleanup(() => {
EnvParser.removeIdentifier('file')
})

EnvParser.identifier('file', (_value: string) => {
return '3000'
})

const envString = ['ENV_USER=file:romain'].join('\n')
const parser = new EnvParser(envString)
const parsed = await parser.parse()

expectTypeOf(parsed).toEqualTypeOf<DotenvParseOutput>()
assert.deepEqual(parsed, {
ENV_USER: '3000',
})
})

test('throw when identifier is already defined', async ({ assert, cleanup }) => {
cleanup(() => {
EnvParser.removeIdentifier('file')
})

EnvParser.identifier('file', (_value: string) => 'test')

assert.throws(
() => EnvParser.identifier('file', (_value: string) => 'test'),
'The identifier "file" is already defined'
)
})

test('give preference to the parsed values when interpolating values', async ({
assert,
expectTypeOf,
Expand All @@ -66,7 +98,7 @@ test.group('Env Parser', () => {
const envString = ['ENV_USER=romain', 'REDIS-USER=$ENV_USER'].join('\n')
const parser = new EnvParser(envString, { ignoreProcessEnv: true })

const parsed = parser.parse()
const parsed = await parser.parse()
expectTypeOf(parsed).toEqualTypeOf<DotenvParseOutput>()
assert.deepEqual(parsed, {
'ENV_USER': 'romain',
Expand All @@ -87,7 +119,7 @@ test.group('Env Parser', () => {
const envString = ['ENV_USER=romain', 'REDIS-USER=$ENV_USER'].join('\n')
const parser = new EnvParser(envString)

const parsed = parser.parse()
const parsed = await parser.parse()
expectTypeOf(parsed).toEqualTypeOf<DotenvParseOutput>()
assert.deepEqual(parsed, {
'ENV_USER': 'virk',
Expand All @@ -108,7 +140,7 @@ test.group('Env Parser', () => {
const envString = ['REDIS-USER=$ENV_USER'].join('\n')
const parser = new EnvParser(envString)

const parsed = parser.parse()
const parsed = await parser.parse()
expectTypeOf(parsed).toEqualTypeOf<DotenvParseOutput>()
assert.deepEqual(parsed, {
'REDIS-USER': 'virk',
Expand Down

0 comments on commit 5bba7f4

Please sign in to comment.