From c857dcb056429616c581059c4a2ba50ac0cab3e8 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Fri, 25 Aug 2017 11:55:40 -0400 Subject: [PATCH] Add ConfLoader * src/conf/ConfLoader.js: Add class. * test/conf/ConfLoaderTest.js: Respective test case. --- src/conf/ConfLoader.js | 123 ++++++++++++++++++++++++++++++++ test/conf/ConfLoaderTest.js | 136 ++++++++++++++++++++++++++++++++++++ 2 files changed, 259 insertions(+) create mode 100644 src/conf/ConfLoader.js create mode 100644 test/conf/ConfLoaderTest.js diff --git a/src/conf/ConfLoader.js b/src/conf/ConfLoader.js new file mode 100644 index 0000000..fd06e4b --- /dev/null +++ b/src/conf/ConfLoader.js @@ -0,0 +1,123 @@ +/** + * Configuration loader + * + * Copyright (C) 2017 R-T Specialty, LLC. + * + * This file is part of the Liza Data Collection Framework. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +'use strict'; + +const { Class } = require( 'easejs' ); + + +/** + * Load system configuration from JSON + * + * @example + * ConfLoader( require( 'fs' ), SomeStore ) + * .fromFile( 'conf/vanilla-server.json' ) + * .then( conf => conf.get( 'foo' ) ); + * + * TODO: Merging multiple configuration files would be convenient for + * modular configuration. + */ +module.exports = Class( 'ConfLoader', +{ + /** + * Filesystem module + * @type {fs} + */ + 'private _fs': null, + + /** + * Store object constructor + * @type {function():Store} + */ + 'private _storeCtor': null, + + + /** + * Initialize with provided filesystem module and Store constructor + * + * The module should implement `#readFile` compatible with + * Node.js'. The Store constructor `store_ctor` is used to instantiate + * new stores to be populated with configuration data. + * + * @param {fs} fs filesystem module + * @param {function():Store} store_ctor Store object constructor + */ + constructor( fs, store_ctor ) + { + this._fs = fs; + this._storeCtor = store_ctor; + }, + + + /** + * Produce configuration from file + * + * A Store will be produced, populated with the configuration data. + * + * @param {string} filename path to configuration JSON + * + * @return {Promise.} a promise of a populated Store + */ + 'public fromFile'( filename ) + { + return new Promise( ( resolve, reject ) => + { + this._fs.readFile( filename, 'utf8', ( err, data ) => + { + if ( err ) + { + reject( err ); + return; + } + + try + { + const store = this._storeCtor(); + + resolve( + this.parseConfData( data ) + .then( parsed => store.populate( parsed ) ) + .then( _ => store ) + ); + } + catch ( e ) + { + reject( e ); + } + } ); + } ); + }, + + + /** + * Parse raw configuration string + * + * Parses configuration string as JSON. + * + * @param {string} data raw configuration data + * + * @return {Promise.} `data` parsed as JSON + */ + 'virtual protected parseConfData'( data ) + { + return Promise.resolve( JSON.parse( data ) ); + }, +} ); diff --git a/test/conf/ConfLoaderTest.js b/test/conf/ConfLoaderTest.js new file mode 100644 index 0000000..b942216 --- /dev/null +++ b/test/conf/ConfLoaderTest.js @@ -0,0 +1,136 @@ +/** + * Tests ConfLoader + */ + +'use strict'; + +const chai = require( 'chai' ); +const expect = chai.expect; +const { + conf: { + ConfLoader: Sut, + }, + store: { + MemoryStore: Store, + }, +} = require( '../../' ); + +chai.use( require( 'chai-as-promised' ) ); + + +describe( 'ConfLoader', () => +{ + it( "loads Store'd configuration from file", () => + { + const expected_path = "/foo/bar/baz.json"; + const expected_data = '{ "foo": "bar" }'; + + const fs = { + readFile( path, encoding, callback ) + { + expect( path ).to.equal( expected_path ); + expect( encoding ).to.equal( 'utf8' ); + + callback( null, expected_data ); + }, + }; + + return expect( + Sut( fs, Store ) + .fromFile( expected_path ) + .then( conf => conf.get( 'foo' ) ) + ).to.eventually.deep.equal( JSON.parse( expected_data ).foo ); + } ); + + + it( "fails on read error", () => + { + const expected_err = Error( 'rejected' ); + + const fs = { + readFile( _, __, callback ) + { + callback( expected_err, null ); + }, + }; + + return expect( Sut( fs ).fromFile( '' ) ) + .to.eventually.be.rejectedWith( expected_err ); + } ); + + + it( "can override #parseConfData for custom parser", () => + { + const result = { foo: {} }; + const input = "foo"; + + const fs = { + readFile( _, __, callback ) + { + callback( null, input ); + }, + }; + + const sut = Sut.extend( + { + 'override parseConfData'( given_input ) + { + expect( given_input ).to.equal( input ); + return Promise.resolve( result ); + }, + } )( fs, Store ); + + return expect( + sut.fromFile( '' ) + .then( conf => conf.get( 'foo' ) ) + ).to.eventually.equal( result.foo ); + } ); + + + it( 'rejects promise on parsing error', () => + { + const expected_err = SyntaxError( 'test parsing error' ); + + const fs = { + readFile( _, __, callback ) + { + // make async so that we clear the stack, and therefore + // try/catch + process.nextTick( () => callback( null, '' ) ); + }, + }; + + const sut = Sut.extend( + { + 'override parseConfData'( given_input ) + { + throw expected_err; + }, + } )( fs, Store ); + + return expect( sut.fromFile( '' ) ) + .to.eventually.be.rejectedWith( expected_err ); + } ); + + + it( "rejects promise on Store ctor error", () => + { + const expected_err = Error( 'test Store ctor error' ); + + const fs = { + readFile: ( _, __, callback ) => callback( null, '' ), + }; + + const badstore = () => { throw expected_err }; + + return expect( Sut( fs, badstore ).fromFile( '' ) ) + .to.eventually.be.rejectedWith( expected_err ); + } ); + + + it( "rejects promise on bad fs call", () => + { + return expect( Sut( {}, Store ).fromFile( '' ) ) + .to.eventually.be.rejected; + } ); +} );