diff --git a/.gitignore b/.gitignore index dcc8f16..cc6afae 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,6 @@ src/**/index.js node_modules # typescript output -bin/delta-processor.js +bin/*.js tsconfig.tsbuildinfo diff --git a/bin/server.ts b/bin/server.ts index ec93fb7..0e8445a 100644 --- a/bin/server.ts +++ b/bin/server.ts @@ -1,7 +1,7 @@ /** * Start the Liza Server * - * Copyright (C) 2017 R-T Specialty, LLC. + * Copyright (C) 2010-2019 R-T Specialty, LLC. * * This file is part of the Liza Data Collection Framework. * @@ -19,19 +19,12 @@ * along with this program. If not, see . */ -'use strict'; +import fs = require( 'fs' ); +import path = require( 'path' ); -const fs = require( 'fs' ); -const path = require( 'path' ); - -const { - conf: { - ConfLoader, - ConfStore, - }, - server, - version, -} = require( '../' ); +import { ConfLoader } from "../src/conf/ConfLoader"; +import { ConfStore } from "../src/conf/ConfStore"; +import * as version from "../src/version"; // kluge for now const conf_path = ( @@ -42,7 +35,7 @@ const conf_path = ( const conf_dir = path.dirname( conf_path ); -ConfLoader( fs, ConfStore ) +new ConfLoader( fs, ConfStore ) .fromFile( conf_path ) .then( conf => Promise.all( [ conf.get( 'name' ), @@ -70,12 +63,12 @@ ConfLoader( fs, ConfStore ) * Produce an absolute path if `path` is absolute, otherwise a path relative * to the configuration directory * - * @param {string} conf_path configuration path (for relative `path`) - * @param {string} path path to resolve + * @param conf_path - configuration path (for relative `path`) + * @param path - path to resolve * * @return resolved path */ -function _resolvePath( conf_path, path ) +function _resolvePath( conf_path: string, path: string ): string { return ( path[ 0 ] === '/' ) ? path @@ -83,7 +76,12 @@ function _resolvePath( conf_path, path ) } -function writePidFile( pid_path ) +/** + * Write process id (PID) file + * + * @param pid_path - path to pid file + */ +function writePidFile( pid_path: string ): void { fs.writeFile( pid_path, process.pid ); @@ -91,7 +89,16 @@ function writePidFile( pid_path ) } -function greet( name, pid_path ) +/** + * Output greeting + * + * The greeting contains the program name, version, configuration path, + * and PID file path. + * + * @param name - program name + * @param pid_path - path to PID file + */ +function greet( name: string, pid_path: string ): void { console.log( `${name} (liza-${version})`); console.log( `Server configuration: ${conf_path}` ); diff --git a/src/conf/ConfLoader.js b/src/conf/ConfLoader.ts similarity index 74% rename from src/conf/ConfLoader.js rename to src/conf/ConfLoader.ts index 17f26ed..7d556e5 100644 --- a/src/conf/ConfLoader.js +++ b/src/conf/ConfLoader.ts @@ -19,9 +19,8 @@ * along with this program. If not, see . */ -'use strict'; - -const { Class } = require( 'easejs' ); +import { readFile } from "fs"; +import { Store } from "../store/Store"; /** @@ -35,21 +34,8 @@ const { Class } = require( 'easejs' ); * TODO: Merging multiple configuration files would be convenient for * modular configuration. */ -module.exports = Class( 'ConfLoader', +export 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 * @@ -57,14 +43,13 @@ module.exports = Class( 'ConfLoader', * 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 + * @param fs - filesystem module + * @param store_ctor - Store object constructor */ - constructor( fs, store_ctor ) - { - this._fs = fs; - this._storeCtor = store_ctor; - }, + constructor( + private _fs: { readFile: typeof readFile }, + private _storeCtor: () => Store, + ) {} /** @@ -72,11 +57,11 @@ module.exports = Class( 'ConfLoader', * * A Store will be produced, populated with the configuration data. * - * @param {string} filename path to configuration JSON + * @param filename - path to configuration JSON * - * @return {Promise.} a promise of a populated Store + * @return a promise of a populated Store */ - 'public fromFile'( filename ) + fromFile( filename: string ): Promise { return new Promise( ( resolve, reject ) => { @@ -104,7 +89,7 @@ module.exports = Class( 'ConfLoader', } } ); } ); - }, + } /** @@ -112,12 +97,12 @@ module.exports = Class( 'ConfLoader', * * Parses configuration string as JSON. * - * @param {string} data raw configuration data + * @param data raw configuration data * - * @return {Promise.} `data` parsed as JSON + * @return `data` parsed as JSON */ - 'virtual protected parseConfData'( data ) + protected parseConfData( data: string ): Promise { return Promise.resolve( JSON.parse( data ) ); - }, -} ); + } +} diff --git a/src/conf/ConfStore.d.ts b/src/conf/ConfStore.d.ts new file mode 100644 index 0000000..8140736 --- /dev/null +++ b/src/conf/ConfStore.d.ts @@ -0,0 +1,32 @@ +/** + * Ideal Store for system configuration + * + * Copyright (C) 2010-2019 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 . + */ + +import { Store } from "../store/Store"; + +/** + * A store that recursively instantiates itself + * + * This store is ideal for nested configurations, and handles cases where + * configuration might be asynchronously retrieved. Nested values may be + * retrieved by delimiting the key with `.` (e.g. `foo.bar.baz`); see + * trait `DelimitedKey` for more information and examples. + */ +export declare function ConfStore(): Store; diff --git a/src/conf/ConfStore.js b/src/conf/ConfStore.js index 0c0569f..d51ad38 100644 --- a/src/conf/ConfStore.js +++ b/src/conf/ConfStore.js @@ -36,7 +36,7 @@ const { * retrieved by delimiting the key with `.` (e.g. `foo.bar.baz`); see * trait `DelimitedKey` for more information and examples. */ -module.exports = function ConfStore() +exports.ConfStore = function ConfStore() { return MemoryStore .use( AutoObjectStore( ConfStore ) ) diff --git a/src/store/Store.d.ts b/src/store/Store.d.ts new file mode 100644 index 0000000..0b62417 --- /dev/null +++ b/src/store/Store.d.ts @@ -0,0 +1,114 @@ +/** + * Generic key/value store + * + * Copyright (C) 2010-2019 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 . + */ + +/** Store key type */ +type K = string; + + +/** + * Generic key/value store with bulk clear + * + * @todo There's a lot of overlap between this concept and that of the + * Bucket. Maybe have the Bucket layer atop of simple Store + * interface as a step toward a new, simpler Bucket + * implementation. This was not implemented atop of the Bucket + * interface because its haphazard implementation would + * overcomplicate this. + */ +export interface Store +{ + /** + * Add item to store under `key` with value `value` + * + * The promise will be fulfilled with an object containing the + * `key` and `value` added to the store; this is convenient for + * promises. + * + * @param key - store key + * @param value - value for key + * + * @return promise to add item to store, resolving to self (for + * chaining) + */ + add( key: K, value: T ): Promise; + + + /** + * Populate store with each element in object `obj` + * + * This is simply a convenient way to call `#add` for each element in an + * object. This does directly call `#add`, so overriding that method + * will also affect this one. + * + * If the intent is to change the behavior of what happens when an item + * is added to the store, override the `#add` method instead of this one + * so that it affects _all_ adds, not just calls to this method. + * + * @param obj - object with which to populate store + * + * @return array of #add promises + */ + populate( obj: Record ): Promise[]; + + + /** + * Retrieve item from store under `key` + * + * The promise will be rejected if the key is unavailable. + * + * @param key - store key + * + * @return promise for the key value + */ + get( key: K ): Promise; + + + /** + * Clear all items in store + * + * @return promise to clear store, resolving to self (for chaining) + */ + clear(): Promise; + + + /** + * Fold (reduce) all stored values + * + * This provides a way to iterate through all stored values and + * their keys while providing a useful functional result (folding). + * + * The order of folding is undefined. + * + * The ternary function `callback` is of the same form as + * {@link Array#fold}: the first argument is the value of the + * accumulator (initialized to the value of `initial`; the second + * is the stored item; and the third is the key of that item. + * + * @param callback - folding function + * @param initial - initial value for accumulator + * + * @return promise of a folded value (final accumulator value) + */ + reduce( + callback: ( accum: T, value: T, key: K ) => T, + initial: T, + ): Promise; +} diff --git a/src/version.d.ts b/src/version.d.ts new file mode 100644 index 0000000..f7a4b0e --- /dev/null +++ b/src/version.d.ts @@ -0,0 +1,39 @@ +/** + * Version information + * + * Copyright (C) 2010-2019 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 . + */ + +/** Format of version array */ +type VersionTuple = [ number, number, number, string ]; + +/** Version information */ +declare interface Version extends VersionTuple +{ + major: number; + minor: number; + rev: number; + suffix: string; + + toString(): string; +} + +/** Exported version data */ +declare const version: Version; + +export = version; diff --git a/test/conf/ConfLoaderTest.js b/test/conf/ConfLoaderTest.ts similarity index 55% rename from test/conf/ConfLoaderTest.js rename to test/conf/ConfLoaderTest.ts index b942216..4d71301 100644 --- a/test/conf/ConfLoaderTest.js +++ b/test/conf/ConfLoaderTest.ts @@ -1,15 +1,34 @@ /** * Tests ConfLoader + * + * Copyright (C) 2010-2019 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 chai = require( 'chai' ); +const expect = chai.expect; + +import { readFile } from "fs"; + +import { ConfLoader as Sut } from "../../src/conf/ConfLoader"; + +type FsLike = { readFile: typeof readFile }; -const chai = require( 'chai' ); -const expect = chai.expect; const { - conf: { - ConfLoader: Sut, - }, store: { MemoryStore: Store, }, @@ -25,8 +44,8 @@ describe( 'ConfLoader', () => const expected_path = "/foo/bar/baz.json"; const expected_data = '{ "foo": "bar" }'; - const fs = { - readFile( path, encoding, callback ) + const fs = { + readFile( path: string, encoding: string, callback: any ) { expect( path ).to.equal( expected_path ); expect( encoding ).to.equal( 'utf8' ); @@ -36,7 +55,7 @@ describe( 'ConfLoader', () => }; return expect( - Sut( fs, Store ) + new Sut( fs, Store ) .fromFile( expected_path ) .then( conf => conf.get( 'foo' ) ) ).to.eventually.deep.equal( JSON.parse( expected_data ).foo ); @@ -47,14 +66,14 @@ describe( 'ConfLoader', () => { const expected_err = Error( 'rejected' ); - const fs = { - readFile( _, __, callback ) + const fs = { + readFile( _: any, __: any, callback: any ) { callback( expected_err, null ); }, }; - return expect( Sut( fs ).fromFile( '' ) ) + return expect( new Sut( fs, Store ).fromFile( '' ) ) .to.eventually.be.rejectedWith( expected_err ); } ); @@ -64,21 +83,21 @@ describe( 'ConfLoader', () => const result = { foo: {} }; const input = "foo"; - const fs = { - readFile( _, __, callback ) + const fs = { + readFile( _: any, __: any, callback: any ) { callback( null, input ); }, }; - const sut = Sut.extend( + const sut = new class extends Sut { - 'override parseConfData'( given_input ) + parseConfData( given_input: string ) { expect( given_input ).to.equal( input ); return Promise.resolve( result ); - }, - } )( fs, Store ); + } + }( fs, Store ); return expect( sut.fromFile( '' ) @@ -91,8 +110,8 @@ describe( 'ConfLoader', () => { const expected_err = SyntaxError( 'test parsing error' ); - const fs = { - readFile( _, __, callback ) + const fs = { + readFile( _: any, __: any, callback: any ) { // make async so that we clear the stack, and therefore // try/catch @@ -100,13 +119,13 @@ describe( 'ConfLoader', () => }, }; - const sut = Sut.extend( + const sut = new class extends Sut { - 'override parseConfData'( given_input ) + parseConfData( _given_input: string ): never { throw expected_err; - }, - } )( fs, Store ); + } + }( fs, Store ); return expect( sut.fromFile( '' ) ) .to.eventually.be.rejectedWith( expected_err ); @@ -117,20 +136,21 @@ describe( 'ConfLoader', () => { const expected_err = Error( 'test Store ctor error' ); - const fs = { - readFile: ( _, __, callback ) => callback( null, '' ), + const fs = { + readFile: ( _: any, __: any, callback: any ) => + callback( null, '' ), }; const badstore = () => { throw expected_err }; - return expect( Sut( fs, badstore ).fromFile( '' ) ) + return expect( new Sut( fs, badstore ).fromFile( '' ) ) .to.eventually.be.rejectedWith( expected_err ); } ); it( "rejects promise on bad fs call", () => { - return expect( Sut( {}, Store ).fromFile( '' ) ) + return expect( new Sut( {}, Store ).fromFile( '' ) ) .to.eventually.be.rejected; } ); } );