Skip to content

Commit

Permalink
feat: load the JSON schema if it's a string
Browse files Browse the repository at this point in the history
  • Loading branch information
ngryman committed Nov 16, 2020
1 parent 4dee24e commit feec74d
Show file tree
Hide file tree
Showing 10 changed files with 88 additions and 77 deletions.
55 changes: 27 additions & 28 deletions src/loader.spec.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
import { promises as fs } from 'fs'
import { TestProject } from '../test/utils/testProject'
import { load } from './loader'
import { FaudaOptions } from './types'

async function getSchema() {
return JSON.parse(await fs.readFile('test/fixtures/schema.json', 'utf8'))
}

describe('given environment variables', () => {
it('loads them mixed with defaults', async () => {
describe('given a JSON schema', () => {
it('loads environment variables', async () => {
const options: FaudaOptions = {
args: [],
cwd: '',
env: {
PASTA_COOKING_TIME: '200',
PASTA_SEASONING: "['Salt', 'Pepper', 'Tomato Sauce']",
NODE_ENV: 'development'
},
namespace: 'pasta',
schema: await getSchema()
}
}
const configuration = await load(options)
const configuration = await load(
'pasta',
'test/fixtures/schema.json',
options
)
expect(configuration).toMatchInlineSnapshot(`
Object {
"cookingTime": 200,
Expand All @@ -31,22 +28,19 @@ describe('given environment variables', () => {
}
`)
})
})

describe('given command line arguments', () => {
it('loads them mixed with defaults', async () => {
const options = {
it('loads command-line arguments', async () => {
const options: FaudaOptions = {
args: [
'--cooking-time=200',
'--seasoning=Salt',
'--seasoning=Pepper',
"--seasoning='Tomato Sauce'"
],
cwd: '',
env: {},
schema: await getSchema()
env: {}
}
const config = await load(options)
const config = await load('pasta', 'test/fixtures/schema.json', options)
expect(config).toMatchInlineSnapshot(`
Object {
"cookingTime": 200,
Expand All @@ -59,20 +53,17 @@ describe('given command line arguments', () => {
}
`)
})
})

describe('given a configuration file', () => {
it('loads it mixed with defaults', async () => {
it('loads a configurationn file', async () => {
const testProject = new TestProject()
try {
await testProject.setup()
const config = await load({
const options: FaudaOptions = {
args: [],
cwd: testProject.rootDir,
env: {},
namespace: 'fauda',
schema: await getSchema()
})
env: {}
}
const config = await load('pasta', 'test/fixtures/schema.json', options)
expect(config).toMatchInlineSnapshot(`
Object {
"cookingTime": 200,
Expand All @@ -84,10 +75,18 @@ describe('given a configuration file', () => {
"type": "Fettuccine",
}
`)
} catch (err) {
throw err
} finally {
await testProject.teardown()
}
})
})

describe('given invalid arguments', () => {
it('throws an error if the schema does not exists', async () => {
await expect(() => load('pasta', '/the/void')).rejects
.toThrowErrorMatchingInlineSnapshot(`
"load: Error loading schema
ENOENT: no such file or directory, open '/the/void'"
`)
})
})
43 changes: 29 additions & 14 deletions src/loader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { merge, reduceRight } from 'lodash'
import { promises as fs } from 'fs'
import { isObject, merge, reduceRight } from 'lodash'
import { JsonObject } from 'type-fest'
import { loadFromArgs, loadFromEnv, loadFromFile } from './loaders'
import { normalize } from './normalizer'
import { FaudaOptions } from './types'
Expand All @@ -7,24 +9,30 @@ function normalizeOptions(options: Partial<FaudaOptions>): FaudaOptions {
const defaultOptions: FaudaOptions = {
args: process.argv,
env: process.env,
cwd: process.cwd(),
namespace: '',
schema: ''
cwd: process.cwd()
}

return { ...defaultOptions, ...options }
}

async function loadFromAll({
args,
cwd,
env,
namespace
}: FaudaOptions): Promise<{}> {
async function loadSchema(schema: string | JsonObject): Promise<JsonObject> {
try {
return isObject(schema)
? schema
: JSON.parse(await fs.readFile(schema, 'utf8'))
} catch (err) {
throw new Error('load: Error loading schema\n' + err.message)
}
}

async function loadFromAll(
namespace: string,
{ args, cwd, env }: FaudaOptions
): Promise<JsonObject> {
const resolvedConfig = await Promise.all([
loadFromEnv(env, namespace),
loadFromEnv(namespace, env),
loadFromArgs(args),
loadFromFile(cwd, namespace)
loadFromFile(namespace, cwd)
])
const mergedConfig = reduceRight(resolvedConfig, merge, {})
return mergedConfig
Expand All @@ -35,10 +43,17 @@ async function loadFromAll({
* and configuration files.
*/
export async function load<Configuration>(
namespace: string,
schema: string | JsonObject,
options: Partial<FaudaOptions> = {}
): Promise<Configuration> {
const safeOptions = normalizeOptions(options)
const resolvedConfig = await loadFromAll(safeOptions)
const safeConfig = normalize<Configuration>(resolvedConfig, safeOptions)
const resolvedSchema = await loadSchema(schema)
const resolvedConfig = await loadFromAll(namespace, safeOptions)
const safeConfig = normalize<Configuration>(
resolvedConfig,
resolvedSchema,
safeOptions.env
)
return safeConfig
}
8 changes: 6 additions & 2 deletions src/loaders/loadEnv.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { camelCase, chain, replace, trim, upperCase } from 'lodash'
import { JsonObject } from 'type-fest'
import { parseArray } from './utils'

/**
Expand All @@ -12,13 +13,16 @@ import { parseArray } from './utils'
* GRAPH0_SERVER_PORT=1337
* GRAPH0_DIRECTIVES="['@graph0/directives', './directives']"
*/
export function loadFromEnv(env: NodeJS.ProcessEnv, namespace: string): {} {
export function loadFromEnv(
namespace: string,
env: NodeJS.ProcessEnv
): JsonObject {
const prefix = `${upperCase(namespace)}_`
return chain(env)
.pickBy((_v, k) => k.startsWith(prefix))
.mapKeys((_v, k) => replace(k, prefix, ''))
.mapKeys((_v, k) => camelCase(k))
.mapValues(trim)
.mapValues(parseArray)
.value()
.value() as JsonObject
}
4 changes: 2 additions & 2 deletions src/loaders/loadFile.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TestProject } from '../../test/utils/testProject'
import { loadFromFile, getDefaultSearchPlaces } from './loadFile'

const SEARCH_PLACES = getDefaultSearchPlaces('fauda')
const SEARCH_PLACES = getDefaultSearchPlaces('pasta')

const TEST_CASES: [string, string][] = [
['load in current directory', '.'],
Expand All @@ -20,7 +20,7 @@ describe.each(SEARCH_PLACES)('given a %s file', variant => {

test.each(TEST_CASES)('%s', async (_title, cwd) => {
await expect(
loadFromFile(testProject.resolvePath(cwd), 'fauda')
loadFromFile('pasta', testProject.resolvePath(cwd))
).resolves.toEqual(EXCEPTED)
})

Expand Down
6 changes: 3 additions & 3 deletions src/loaders/loadFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ export function getDefaultSearchPlaces(namespace: string) {
* @see https://github.com/davidtheclark/cosmiconfig
*/
export async function loadFromFile(
cwd: string,
namespace: string
): Promise<{}> {
namespace: string,
cwd: string
): Promise<JsonObject> {
const options: CosmiconfigOptions = {
loaders: { '.ts': loadTs },
packageProp: `config.${namespace}`,
Expand Down
7 changes: 5 additions & 2 deletions src/normalizer/expandVars.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isArray, map, mapValues } from 'lodash'
import { Primitive } from 'type-fest'
import { JsonObject, Primitive } from 'type-fest'

const varRegex = /([\\])?\${([\w]+)}/g

Expand All @@ -9,7 +9,10 @@ function expandVar(value: Primitive, env: NodeJS.ProcessEnv): Primitive {
)
}

export function expandVars(config: {}, env: NodeJS.ProcessEnv): {} {
export function expandVars(
config: JsonObject,
env: NodeJS.ProcessEnv
): JsonObject {
return mapValues(config, (v: Primitive) =>
isArray(v)
? map(v, (vv: Primitive) => expandVar(vv, env))
Expand Down
21 changes: 11 additions & 10 deletions src/normalizer/normalize.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ async function getSchema() {

describe('given configuration', () => {
it('merges default configuration', async () => {
expect(normalize({ cookingTime: 200 }, { schema: await getSchema() }))
expect(normalize({ cookingTime: 200 }, await getSchema(), process.env))
.toMatchInlineSnapshot(`
Object {
"cookingTime": 200,
Expand All @@ -24,25 +24,26 @@ describe('given configuration', () => {

it('expands environment variables', async () => {
expect(
normalize(
{ cookingTime: '${NUMBER}' },
{ env: { NUMBER: '200' }, schema: await getSchema() }
)
normalize({ cookingTime: '${NUMBER}' }, await getSchema(), {
NUMBER: '200'
})
).toMatchObject({ cookingTime: 200 })
})

it('throws an error for invalid values', async () => {
await expect(async () =>
normalize({ cookingTime: 'nope' }, { schema: await getSchema() })
).rejects.toThrowErrorMatchingInlineSnapshot(
`".cookingTime should be number"`
)
normalize({ cookingTime: 'nope' }, await getSchema(), process.env)
).rejects.toThrowErrorMatchingInlineSnapshot(`
"validate: Validation failed
.cookingTime should be number"
`)
})
})

describe('given empty configuration', () => {
it('returns the default configuration', async () => {
expect(normalize({}, { schema: await getSchema() })).toMatchInlineSnapshot(`
expect(normalize({}, await getSchema(), process.env))
.toMatchInlineSnapshot(`
Object {
"cookingTime": 300,
"seasoning": Array [
Expand Down
17 changes: 4 additions & 13 deletions src/normalizer/normalize.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
import { flow } from 'lodash'
import { JsonObject } from 'type-fest'
import { expandVars } from './expandVars'
import { validate } from './validate'

type Options = {
env: NodeJS.ProcessEnv
schema: any
}

const defaultOptions: Options = {
env: process.env,
schema: {}
}

export function normalize<Configuration>(
config: {},
options: Partial<Options> = {}
config: JsonObject,
schema: JsonObject,
env: NodeJS.ProcessEnv
): Configuration {
const { env, schema } = { ...defaultOptions, ...options }
return flow(
_ => expandVars(_, env),
_ => validate<Configuration>(_, schema)
Expand Down
2 changes: 0 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,4 @@ export type FaudaOptions = {
args: string[]
env: NodeJS.ProcessEnv
cwd: string
namespace: string
schema: string | object
}
2 changes: 1 addition & 1 deletion test/fixtures/loaders/template_package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"config": {
"fauda": {
"pasta": {
"cookingTime": 200,
"seasoning": ["Salt", "Pepper", "Tomato Sauce"]
}
Expand Down

0 comments on commit feec74d

Please sign in to comment.