diff --git a/Changes.md b/Changes.md index 21e7874..d5fb0e7 100644 --- a/Changes.md +++ b/Changes.md @@ -1,3 +1,10 @@ +## 1.0.17 - 2018-12-19 + +- refactor ./config.js as an es6 class +- update README syntax and improve formatting +- use path.resolve instead of ./dir/file (2x) +- watch: recursive=true +- permit retrieval of fully qualified path ## 1.0.16 - 2018-11-02 diff --git a/README.md b/README.md index e94e7f5..ca2bb43 100644 --- a/README.md +++ b/README.md @@ -27,16 +27,19 @@ See the [File Formats](#file_formats) section below for a more detailed explanation of each of the formats. # Usage + ```js - // From within a plugin: - var cfg = this.config.get(name, [type], [callback], [options]); +// From within a plugin: +const cfg = this.config.get(name, [type], [callback], [options]); ``` This will load the file config/rambling.paths in the Haraka directory. `name` is not a full path, but a filename in the config/ directory. For example: + ```js - var cfg = this.config.get('rambling.paths', 'list'); +const cfg = this.config.get('rambling.paths', 'list'); ``` + `type` can be any of the types listed above. If the file name has an `.ini`, `.json` or `.yaml` suffix, @@ -49,19 +52,19 @@ variables within your plugin. Example: ```js exports.register = function () { - var plugin = this; - plugin.loginfo('register function called'); - plugin.load_my_plugin_ini(); + const plugin = this + plugin.loginfo('register called') + plugin.load_my_plugin_ini() } exports.load_my_plugin_ini = function () { - var plugin = this; + const plugin = this plugin.cfg = plugin.config.get('my_plugin.ini', function onCfgChange () { // This closure is run a few seconds after my_plugin.ini changes // Re-run the outer function again - plugin.load_my_plugin_ini(); - }); - plugin.loginfo('cfg=' + JSON.stringify(plugin.cfg)); + plugin.load_my_plugin_ini() + }) + plugin.loginfo(`cfg=${JSON.stringify(plugin.cfg)}`) } exports.hook_connect = function (next, connection) { @@ -152,17 +155,17 @@ Entries are a simple format of key=value pairs, with optional [sections]. Here is a typical example: ```ini - first_name=Matt - last_name=Sergeant +first_name=Matt +last_name=Sergeant - [job] - title=Senior Principal Software Engineer - role=Architect +[job] +title=Senior Principal Software Engineer +role=Architect - [projects] - haraka - qpsmtpd - spamassassin +[projects] +haraka +qpsmtpd +spamassassin ``` That produces the following Javascript object: @@ -195,36 +198,42 @@ backslash "\" character. The `options` object allows you to specify which keys are boolean: ```js - { booleans: ['reject','some_true_value'] } +{ booleans: ['reject','some_true_value'] } ``` On the options declarations, key names are formatted as section.key. If the key name does not specify a section, it is presumed to be [main]. -This ensures these values are converted to true Javascript booleans when parsed, -and supports the following options for boolean values: +This ensures these values are converted to true Javascript booleans when parsed, and supports the following options for boolean values: + ``` - true, yes, ok, enabled, on, 1 +true, yes, ok, enabled, on, 1 ``` + Anything else is treated as false. To default a boolean as true (when the key is undefined or the config file is missing), prefix the key with +: + ```js - { booleans: [ '+reject' ] } +{ booleans: [ '+reject' ] } ``` For completeness the inverse is also allowed: + ```js - { booleans: [ '-reject' ] } +{ booleans: [ '-reject' ] } ``` Lists are supported using this syntax: + ```ini - hosts[] = first_host - hosts[] = second_host - hosts[] = third_host +hosts[] = first_host +hosts[] = second_host +hosts[] = third_host ``` + which produces this javascript array: + ```js - ['first_host', 'second_host', 'third_host'] +['first_host', 'second_host', 'third_host'] ``` Flat Files @@ -246,8 +255,7 @@ If a requested .json or .hjson file does not exist then the same file will be ch for with a .yaml extension and that will be loaded instead. This is done because YAML files are far easier for a human to write. -You can use JSON, HJSON or YAML files to override any other file by prefixing the -outer variable name with a `!` e.g. +You can use JSON, HJSON or YAML files to override any other file by prefixing the outer variable name with a `!` e.g. ```js { @@ -276,7 +284,7 @@ Main features: Example syntax -```js +```hjson { # specify rate in requests/second (because comments are helpful!) rate: 1000 diff --git a/appveyor.yml b/appveyor.yml index 1bd6789..4742ab5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,7 @@ version: 1.0.{build} environment: - nodejs_version: "6" + nodejs_version: "8" # Install scripts. (runs after repo cloning) install: diff --git a/config.js b/config.js index da95ee5..c4cd9cd 100644 --- a/config.js +++ b/config.js @@ -4,66 +4,125 @@ const path = require('path'); const cfreader = require('./configfile'); -module.exports = new Config(); +class Config { + constructor (root_path, no_overrides) { + this.root_path = root_path || cfreader.config_path; -function Config (root_path, no_overrides) { - this.root_path = root_path || cfreader.config_path; - this.module_config = function (defaults_path, overrides_path) { - const cfg = new Config(path.join(defaults_path, 'config'), true); - if (overrides_path) { - cfg.overrides_path = path.join(overrides_path, 'config'); + if (process.env.HARAKA_TEST_DIR) { + this.root_path = path.join(process.env.HARAKA_TEST_DIR, 'config'); + return; + } + if (process.env.HARAKA && !no_overrides) { + this.overrides_path = root_path || cfreader.config_path; + this.root_path = path.join(process.env.HARAKA, 'config'); } - return cfg; - } - if (process.env.HARAKA_TEST_DIR) { - this.root_path = path.join(process.env.HARAKA_TEST_DIR, 'config'); - return; - } - if (process.env.HARAKA && !no_overrides) { - this.overrides_path = root_path || cfreader.config_path; - this.root_path = path.join(process.env.HARAKA, 'config'); } -} -Config.prototype.get = function (name, type, cb, options) { - const a = this.arrange_args([name, type, cb, options]); - if (!a[1]) a[1] = 'value'; + get (name, type, cb, options) { + const a = this.arrange_args([name, type, cb, options]); + if (!a[1]) a[1] = 'value'; - const full_path = path.resolve(this.root_path, a[0]); + const full_path = path.isAbsolute(name) ? name : path.resolve(this.root_path, a[0]); - let results = cfreader.read_config(full_path, a[1], a[2], a[3]); + let results = cfreader.read_config(full_path, a[1], a[2], a[3]); - if (this.overrides_path) { - const overrides_path = path.resolve(this.overrides_path, a[0]); + if (this.overrides_path) { + const overrides_path = path.resolve(this.overrides_path, a[0]); + + const overrides = cfreader.read_config(overrides_path, a[1], a[2], a[3]); + + results = merge_config(results, overrides, a[1]); + } - const overrides = cfreader.read_config(overrides_path, a[1], a[2], a[3]); + // Pass arrays by value to prevent config being modified accidentally. + if (Array.isArray(results)) return results.slice(); - results = merge_config(results, overrides, a[1]); + return results; } - // Pass arrays by value to prevent config being modified accidentally. - if (Array.isArray(results)) { - return results.slice(); + getInt (filename, default_value) { + + if (!filename) return NaN; + + const full_path = path.resolve(this.root_path, filename); + const r = parseInt(cfreader.read_config(full_path, 'value', null, null), 10); + + if (!isNaN(r)) return r; + return parseInt(default_value, 10); } - return results; -} + getDir (name, opts, done) { + cfreader.read_dir(path.resolve(this.root_path, name), opts, done); + } -Config.prototype.getInt = function (filename, default_value) { + arrange_args (args) { + + /* ways get() can be called: + config.get('thing'); + config.get('thing', type); + config.get('thing', cb); + config.get('thing', cb, options); + config.get('thing', options); + config.get('thing', type, cb); + config.get('thing', type, options); + config.get('thing', type, cb, options); + */ + const fs_name = args.shift(); + let fs_type = null; + let cb; + let options; + + for (let i=0; i < args.length; i++) { + if (args[i] === undefined) continue; + switch (typeof args[i]) { // what is it? + case 'function': + cb = args[i]; + break; + case 'object': + options = args[i]; + break; + case 'string': + if (/^(ini|value|list|data|h?json|yaml|binary)$/.test(args[i])) { + fs_type = args[i]; + break; + } + console.log(`unknown string: ${args[i]}`); + break; + } + // console.log(`unknown arg: ${args[i]}, typeof: ${typeof args[i]}`); + } - if (!filename) return NaN; + if (!fs_type) { + const fs_ext = path.extname(fs_name).substring(1); - const full_path = path.resolve(this.root_path, filename); - const r = parseInt(cfreader.read_config(full_path, 'value', null, null), 10); + switch (fs_ext) { + case 'hjson': + case 'json': + case 'yaml': + case 'ini': + fs_type = fs_ext; + break; - if (!isNaN(r)) return r; - return parseInt(default_value, 10); -} + default: + fs_type = 'value'; + break; + } + } -Config.prototype.getDir = function (name, opts, done) { - cfreader.read_dir(path.resolve(this.root_path, name), opts, done); + return [fs_name, fs_type, cb, options]; + } + + module_config (defaults_path, overrides_path) { + const cfg = new Config(path.join(defaults_path, 'config'), true); + if (overrides_path) { + cfg.overrides_path = path.join(overrides_path, 'config'); + } + return cfg; + } } +module.exports = new Config(); + function merge_config (defaults, overrides, type) { switch (type) { case 'ini': @@ -86,8 +145,7 @@ function merge_config (defaults, overrides, type) { function merge_struct (defaults, overrides) { for (const k in overrides) { if (k in defaults) { - if (typeof overrides[k] === 'object' && - typeof defaults[k] === 'object') { + if (typeof overrides[k] === 'object' && typeof defaults[k] === 'object') { defaults[k] = merge_struct(defaults[k], overrides[k]); } else { @@ -100,60 +158,3 @@ function merge_struct (defaults, overrides) { } return defaults; } - -/* ways get() can be called: -config.get('thing'); -config.get('thing', type); -config.get('thing', cb); -config.get('thing', cb, options); -config.get('thing', options); -config.get('thing', type, cb); -config.get('thing', type, options); -config.get('thing', type, cb, options); -*/ - -Config.prototype.arrange_args = function (args) { - const fs_name = args.shift(); - let fs_type = null; - let cb; - let options; - - for (let i=0; i < args.length; i++) { - if (args[i] === undefined) continue; - switch (typeof args[i]) { // what is it? - case 'function': - cb = args[i]; - break; - case 'object': - options = args[i]; - break; - case 'string': - if (/^(ini|value|list|data|h?json|yaml|binary)$/.test(args[i])) { - fs_type = args[i]; - break; - } - console.log(`unknown string: ${args[i]}`); - break; - } - // console.log(`unknown arg: ${args[i]}, typeof: ${typeof args[i]}`); - } - - if (!fs_type) { - const fs_ext = path.extname(fs_name).substring(1); - - switch (fs_ext) { - case 'hjson': - case 'json': - case 'yaml': - case 'ini': - fs_type = fs_ext; - break; - - default: - fs_type = 'value'; - break; - } - } - - return [fs_name, fs_type, cb, options]; -}; diff --git a/configfile.js b/configfile.js index 1eccfc3..f7e61be 100644 --- a/configfile.js +++ b/configfile.js @@ -245,7 +245,7 @@ function fsWatchDir (dirPath) { if (cfreader._watchers[dirPath]) return; - cfreader._watchers[dirPath] = fs.watch(dirPath, { persistent: false }, function (fse, filename) { + cfreader._watchers[dirPath] = fs.watch(dirPath, { persistent: false, recursive: true }, function (fse, filename) { // console.log(`event: ${fse}, ${filename}`); if (!filename) return; const full_path = path.join(dirPath, filename); @@ -271,7 +271,7 @@ cfreader.read_dir = function (name, opts, done) { return fsReadDir(name); }) .then((fileList) => { - const reader = require(`./readers/${type}`); + const reader = require(path.resolve(__dirname, 'readers', type)); const promises = []; fileList.forEach((file) => { promises.push(reader.loadPromise(path.resolve(name, file))) @@ -322,9 +322,9 @@ cfreader.get_filetype_reader = function (type) { case 'value': case 'data': case '': - return require('./readers/flat'); + return require(path.resolve(__dirname, 'readers', 'flat')); } - return require(`./readers/${type}`); + return require(path.resolve(__dirname, 'readers', type)); } cfreader.load_config = function (name, type, options) { diff --git a/package.json b/package.json index b94f1fa..fff3e23 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "name": "haraka-config", "license": "MIT", "description": "Haraka's config file loader", - "version": "1.0.16", + "version": "1.0.17", "homepage": "http://haraka.github.io", "repository": { "type": "git", diff --git a/test/config.js b/test/config.js index 9c2f3a5..843a031 100644 --- a/test/config.js +++ b/test/config.js @@ -3,7 +3,7 @@ const fs = require('fs'); const path = require('path'); -const cb = function () { return false; }; +function cb () { return false; } const opts = { booleans: ['arg1'] }; function clearRequireCache () { @@ -359,7 +359,6 @@ exports.get = { _test_get(test, 'missing.json', 'json', null, null, {"matt": "waz here"}); }, - // config.get('test.bin', 'binary'); 'test.bin, type=binary' : function (test) { test.expect(2); const res = this.config.get('test.binary', 'binary'); @@ -367,6 +366,19 @@ exports.get = { test.ok(Buffer.isBuffer(res)); test.done(); }, + + 'fully qualified path: /etc/services' : function (test) { + test.expect(1); + let res; + if (/^win/.test(process.platform)) { + res = this.config.get('c:\\windows\\win.ini', 'list'); + } + else { + res = this.config.get('/etc/services', 'list'); + } + test.ok(res.length); + test.done(); + } } exports.merged = { @@ -451,7 +463,7 @@ exports.getDir = { }, 'loads all files in dir' : function (test) { test.expect(4); - this.config.getDir('dir', { type: 'binary' }, function (err, files) { + this.config.getDir('dir', { type: 'binary' }, (err, files) => { // console.log(files); test.equal(err, null); test.equal(files.length, 3); @@ -462,7 +474,7 @@ exports.getDir = { }, 'errs on invalid dir' : function (test) { test.expect(1); - this.config.getDir('dirInvalid', { type: 'binary' }, function (err, files) { + this.config.getDir('dirInvalid', { type: 'binary' }, (err, files) => { // console.log(arguments); test.equal(err.code, 'ENOENT'); test.done(); @@ -470,8 +482,7 @@ exports.getDir = { }, 'reloads when file in dir is touched' : function (test) { if (/darwin/.test(process.platform)) { - // due to differences in fs.watch, this test is not reliable on - // Mac OS X + // due to differences in fs.watch, this test is not reliable on Mac OS X test.done(); return; }