Add ConfigReader.ts

This commit is contained in:
Jerko Steiner 2019-03-12 17:04:42 +05:00
parent 694d1857b3
commit a5ef6bd981
9 changed files with 263 additions and 4 deletions

6
package-lock.json generated
View File

@ -932,6 +932,12 @@
"integrity": "sha512-/kQvbVzdEpOq4tEWT79yAHSM4nH4xMlhJv2GrLVQt4Qmo8yYsPdioBM1QpN/2GX1wkfMnyXvdoftvLUr0LBj7Q==", "integrity": "sha512-/kQvbVzdEpOq4tEWT79yAHSM4nH4xMlhJv2GrLVQt4Qmo8yYsPdioBM1QpN/2GX1wkfMnyXvdoftvLUr0LBj7Q==",
"dev": true "dev": true
}, },
"@types/js-yaml": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.0.tgz",
"integrity": "sha512-UGEe/6RsNAxgWdknhzFZbCxuYc5I7b/YEKlfKbo+76SM8CJzGs7XKCj7zyugXViRbKYpXhSXhCYVQZL5tmDbpQ==",
"dev": true
},
"@types/mime": { "@types/mime": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.0.tgz",

View File

@ -15,6 +15,7 @@
"@types/express-session": "^1.15.11", "@types/express-session": "^1.15.11",
"@types/http-errors": "^1.6.1", "@types/http-errors": "^1.6.1",
"@types/jest": "^23.3.12", "@types/jest": "^23.3.12",
"@types/js-yaml": "^3.12.0",
"@types/node": "^10.12.18", "@types/node": "^10.12.18",
"@types/passport": "^1.0.0", "@types/passport": "^1.0.0",
"@types/passport-local": "^1.0.33", "@types/passport-local": "^1.0.33",
@ -26,10 +27,6 @@
"@types/supertest": "^2.0.7", "@types/supertest": "^2.0.7",
"@types/uuid": "^3.4.4", "@types/uuid": "^3.4.4",
"axios": "^0.18.0", "axios": "^0.18.0",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-redux": "^6.0.0",
"redux": "^4.0.1",
"browserify": "^16.2.3", "browserify": "^16.2.3",
"buildfile": "^1.2.17", "buildfile": "^1.2.17",
"jest": "^23.6.0", "jest": "^23.6.0",
@ -37,6 +34,10 @@
"loose-envify": "^1.4.0", "loose-envify": "^1.4.0",
"nodemon": "^1.18.9", "nodemon": "^1.18.9",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-redux": "^6.0.0",
"redux": "^4.0.1",
"std-mocks": "^1.0.1", "std-mocks": "^1.0.1",
"supertest": "^3.3.0", "supertest": "^3.3.0",
"terser": "^3.14.1", "terser": "^3.14.1",

View File

@ -0,0 +1,29 @@
export class Config {
constructor(protected readonly config: any) {}
get(key: string) {
let value = this.config
key.split('.').forEach(k => {
if (!value.hasOwnProperty(k)) {
throw new Error(`Property "${k}" from "${key}" does not exist`)
}
value = value[k]
})
return value
}
has(key: string) {
let c = this.config
return key.split('.').every(k => {
const has = c.hasOwnProperty(k)
if (has) {
c = c[k]
}
return has
})
}
value() {
return this.config
}
}

View File

@ -0,0 +1,74 @@
import {ConfigReader} from './ConfigReader'
import {join} from 'path'
import {closeSync, openSync} from 'fs'
describe('ConfigReader', () => {
beforeAll(() => {
closeSync(
openSync(
join(__dirname, 'test-files', 'package.json'),
'w',
),
)
})
describe('read', () => {
it('reads and merges configuration files from package root', () => {
const config = new ConfigReader(__dirname).read()
expect(config.get('app.name')).toEqual(jasmine.any(String))
})
it('reads and merges configuration files from CWD', () => {
const config = new ConfigReader(
join(__dirname, 'test-files', 'dir'),
'/tmp/path',
).read()
expect(config.value()).toEqual({
a: 1,
b: 'test',
c: {
d: 'value',
e: ['entry 3'],
f: 'extra value',
},
})
})
it('fails when not a single config file found', () => {
expect(() => new ConfigReader('/tmp/test', '/tmp/test').read())
.toThrowError('No config files found')
})
it('succeeds when custom filename is read', () => {
expect(() => new ConfigReader('/tmp/test', '/tmp/test').read())
.toThrowError('No config files found')
})
describe('environment variable', () => {
const origConfig = process.env.CONFIG
afterEach(() => {
process.env.CONFIG = origConfig
})
it('succeeds when config from env variable is read', () => {
process.env.CONFIG = '---\na: 2'
const config = new ConfigReader(
join(__dirname, 'test-files', 'dir'),
'/tmp/path',
).read()
expect(config.value()).toEqual({
a: 2,
b: 'test',
c: {
d: 'value',
e: ['entry 3'],
f: 'extra value',
},
})
})
})
})
})

View File

@ -0,0 +1,107 @@
import YAML from 'js-yaml'
import {Config} from './Config'
import {findPackageRoot} from '../files/Find'
import {join} from 'path'
import {readFileSync} from 'fs'
const isObject = (value: any) => value !== null && typeof value === 'object'
export class ConfigReader {
protected readonly config: any = {}
protected readonly locations: string[]
protected readonly filenames: string[]
constructor(
readonly path: string,
readonly cwd: string|undefined = process.cwd(),
readonly environment = 'CONFIG',
) {
const packageRoot = path && findPackageRoot(path)
this.locations = packageRoot ? [packageRoot] : []
if (cwd && cwd !== packageRoot) {
this.locations.push(cwd)
}
this.filenames = [
'default.yml',
`${process.env.NODE_ENV || 'development'}.yml`,
]
}
read(filename?: string) {
let success = 0
for (const location of this.locations) {
for (const fname of this.filenames) {
const configFilename = join(location, 'config', fname)
try {
this.readFile(configFilename)
} catch (err) {
if (err.code !== 'ENOENT') {
throw err
}
continue
}
success += 1
}
}
const env = process.env[this.environment]
if (!filename && !env && !success) {
throw new Error('No config files found')
}
if (filename) {
this.readFile(filename)
}
if (env) {
this.parse(env)
}
return new Config(this.config)
}
readFile(filename: string) {
const yaml = readFileSync(filename, 'utf-8')
const config = YAML.safeLoad(yaml)
if (config) {
this.mergeConfig(config, this.config)
}
}
parse(yaml: string) {
const config = YAML.safeLoad(yaml)
this.mergeConfig(config, this.config)
}
protected mergeConfig(source: any, destination: any) {
const stack = [{src: source, dest: destination}]
// let i = 0
while (stack.length) {
// i++
const {src, dest} = stack.pop()!
const keys = Object.keys(src)
keys.forEach(key => {
// if (i > 100) {
// throw new Error('overload')
// }
const value = src[key]
if (isObject(value) && !Array.isArray(value)) {
if (!dest.hasOwnProperty(key) ||
Array.isArray(dest[key]) ||
!isObject(dest[key])
) {
dest[key] = {}
}
stack.push({src: value, dest: dest[key]})
return
}
dest[key] = value
})
}
return destination
}
}

View File

@ -0,0 +1 @@
package.json

View File

@ -0,0 +1,6 @@
a: 1
b: 'test'
c:
d: value
e:
- entry

View File

@ -0,0 +1,5 @@
c:
d: value
e:
- entry 3
f: extra value

View File

@ -0,0 +1,30 @@
import {resolve, join} from 'path'
import {statSync, Stats} from 'fs'
export function findNearestDirectory(
dir: string, filename: string,
) {
let currentDir = dir
let lastDir = dir
do {
let s: Stats
try {
s = statSync(join(currentDir, filename))
} catch (err) {
lastDir = currentDir
currentDir = resolve(currentDir, '..')
continue
}
if (!s.isFile()) {
lastDir = currentDir
currentDir = resolve(currentDir, '..')
continue
}
return currentDir
} while (lastDir !== currentDir)
}
export function findPackageRoot(dir: string = __dirname) {
return findNearestDirectory(dir, 'package.json')
}