<repo_name>cuarti/zenox-config<file_sep>/test/specs/util/loadDirectory.ts

import {join} from 'path';
import {loadDirectory} from '../../../src/util';


test('loadDirectory', () => {

	const path = join('test', 'config');

	expect(loadDirectory(path)).toEqual({
		db: {domain: 'localhost', port: 1418},
		http: {port: 3000, domain: 'domain.com'}
	});

	expect(loadDirectory(join(path, 'development'))).toEqual({
		http: {port: 3001, subdomains: ['blog', 'admin']},
		log: {apiKey: '<KEY>'}
	});

	expect(loadDirectory(join(path, 'production'))).toEqual({
		http: {port: 3002}
	});

	expect(loadDirectory('config')).toEqual({});

});
<file_sep>/test/specs/util/listFiles.ts

import {join} from 'path';

import {listFiles} from '../../../src/util';


test('listFiles', () => {

	const path = join('test', 'config');

	expect(listFiles(path)).toEqual(['db.yml', 'http.yml']);
	expect(listFiles(join(path, 'development'))).toEqual(['http.yml', 'log.yml']);
	expect(listFiles(join(path, 'production'))).toEqual(['http.yml']);
	expect(listFiles('config')).toEqual([]);

});
<file_sep>/src/util/index.ts


export * from './listFiles';
export * from './loadDirectory';
export * from './merge';
<file_sep>/src/util/listFiles.ts

import {readdirSync} from 'fs';


/**
 * List YAML files of directory
 *
 * @param	path	Directory path
 * @return			YAML files
 */
export function listFiles(path: string): string[] {

	try {
		return readdirSync(path).filter(f => f.endsWith('.yml'));

	} catch(err) {
		return [];
	}

}
<file_sep>/test/specs/ConfigService.ts

import {join} from 'path';

import {ConfigService} from '../..';


const config = new ConfigService({
	http: {port: 3000, domain: 'domain.com', plugins: {session: true}},
	db: {port: 1418, domain: 'localhost'}
});


describe('ConfigService', () => {

	test('#get', () => {

		expect(config.get('http')).toEqual({port: 3000, domain: 'domain.com', plugins: {session: true}});
		expect(config.get('log')).not.toBeDefined();
		expect(config.get('log', true)).toBe(true);
		expect(config.get('http.port')).toBe(3000);
		expect(config.get('http.port', 80)).toBe(3000);
		expect(config.get('http.sessionId')).not.toBeDefined();
		expect(config.get('http.sessionId', 'secret')).toBe('secret');
		expect(config.get('http.plugins')).toEqual({session: true});
		expect(config.get('http.plugins', {})).toEqual({session: true});
		expect(config.get('http.plugins.session')).toBe(true);
		expect(config.get('http.plugins.session', false)).toBe(true);

	});

	test('#getAll()', () => {

		expect(config.getAll()).toEqual({
			http: {port: 3000, domain: 'domain.com', plugins: {session: true}},
			db: {port: 1418, domain: 'localhost'}
		});

	});

	test('fromYaml', () => {

		const path = join('test', 'config');

		expect(ConfigService.fromYaml(path).getAll()).toEqual({
			db: {domain: 'localhost', port: 1418},
			http: {port: 3000, domain: 'domain.com'}
		});

		expect(ConfigService.fromYaml(path, 'development').getAll()).toEqual({
			db: {domain: 'localhost', port: 1418},
			http: {port: 3001, domain: 'domain.com', subdomains: ['blog', 'admin']},
			log: {apiKey: '<KEY>'}
		});

		expect(ConfigService.fromYaml(path, 'production').getAll()).toEqual({
			db: {domain: 'localhost', port: 1418},
			http: {port: 3002, domain: 'domain.com'}
		});

		expect(ConfigService.fromYaml('config').getAll()).toEqual({});

	});

});
<file_sep>/src/Config.ts

import {join} from 'path';

import {ConfigService} from './ConfigService';


export interface Config {

	/**
	 * Get config from namespace if exists
	 *
	 * @param	namespace		Namespace config
	 * @return					Config value
	 */
	get<T>(namespace: string): T | undefined;

	/**
	 * Get config from namespace if exists or default value otherwise
	 *
	 * @param	namespace		Namespace config
	 * @param	defaultValue	Default value for config
	 * @return					Config value
	 */
	get<T>(namespace: string, defaultValue: T): T;

	/**
	 * Get all configurations
	 *
	 * @return	All configuration
	 */
	getAll<T extends object = object>(): T;

}

const {ZENOX_PROJECT_DIR, ZENOX_CONFIG_DIR, NODE_ENV} = process.env;

const path = ZENOX_CONFIG_DIR || join(ZENOX_PROJECT_DIR || process.cwd(), 'config');
const env = NODE_ENV || 'development';

export const Config: Config = ConfigService.fromYaml(path, env);
<file_sep>/src/ConfigService.ts

import {join} from 'path';

import {Config} from './Config';
import {loadDirectory, merge} from './util';


/**
 * Config basic implementation
 */
export class ConfigService implements Config {

	/**
	 * Config data
	 */
	private readonly data: object;

	/**
	 * Constructor method
	 *
	 * @param	data	Config data
	 */
	public constructor(data: object) {
		this.data = data;
	}

	public get<T>(namespace: string, defaultValue?: T): T | undefined {

		let value = namespace.split('.').reduce((r, k) => r && r[k], this.data);

		return value || defaultValue;
	}

	public getAll<T extends object = object>(): T {
		return this.data as T;
	}

	/**
	 * Create config from YAML config directory and environment
	 *
	 * @param	path	Config directory
	 * @param 	env		Environment
	 * @return			Config
	 */
	public static fromYaml(path: string, env?: string): Config {

		let data = loadDirectory(path);

		if(env) {
			data = merge(data, loadDirectory(join(path, env)));
		}

		return new ConfigService(data);
	}

}
<file_sep>/src/util/merge.ts


/**
 * Merge two objects
 *
 * @param	a	First object to merge
 * @param	b	Second object to merge
 * @return		Merged object
 */
export function merge(a: object = {}, b: object = {}): object {

	let keys = Object.keys(a).concat(Object.keys(b))
		.filter((k, i, arr) => arr.indexOf(k, i + 1) < 0);

	return keys.reduce((r, k) => {

		if(!b.hasOwnProperty(k)) {
			r[k] = a[k];

		} else if(typeof b[k] === 'object' && !(b[k] instanceof Array)) {
			r[k] = merge(a[k], b[k]);

		} else {
			r[k] = b[k];
		}

		return r;
	}, {});

}
<file_sep>/README.md

# zenox-config
Library to load configuration data from YAML files depending of NODE_ENV value.


### Default config from YAML files
Default config service that loads config from YAML files with path of one of next options:

- ZENOX_CONFIG_DIR env variable.
- ZENOX_PROJECT_DIR env variable plus "config" child. 
- Current working directory plus "config" child.

###### Usage
```typescript
import {Config} from '@zenox/config';

let port = Config.get('http.port');
```


### Custom config from object literal
Custom config service that loads config from object literal. 

###### Usage
```typescript
import {ConfigService} from '@zenox/config';

let config = new ConfigService({http: {port: 3000}});
let port = config.get('http.port');
```


### Custom config from YAML files
Custom config service that loads config from path of directory with YAML files.

###### Usage
```typescript
import {ConfigService} from '@zenox/config';

let path = '<relative path to config dir>';
let config = ConfigService.fromYaml(path);
let port = config.get('http.port');
```
<file_sep>/src/util/loadDirectory.ts

import {readFileSync} from 'fs';
import {join} from 'path';
import {load} from 'js-yaml';

import {listFiles} from './listFiles';


/**
 * Load YAML configuration files from directory
 *
 * @param	path	Directory path
 * @return			Config
 */
export function loadDirectory(path: string): object {

	return listFiles(path).reduce((r, file) => {

		let name = file.substring(0, file.length - 4);
		r[name] = load(readFileSync(join(path, file), 'utf8'));

		return r;
	}, {});

}
<file_sep>/src/index.ts


export * from './Config';
export * from './ConfigService';
<file_sep>/test/specs/util/merge.ts

import {merge} from '../../../src/util';


test('merge', () => {

	expect(merge({name: 'joe'}, {age: 12})).toEqual({name: 'joe', age: 12});
	expect(merge({name: 'joe', age: 12}, {name: 'jake'})).toEqual({name: 'jake', age: 12});

});
