diff --git a/.codeclimate.yml b/.codeclimate.yml index dfe3bb6..a8ccd03 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,10 +1,10 @@ engines: eslint: enabled: true - channel: "eslint-6" + channel: 'eslint-6' config: - config: ".eslintrc.yaml" + config: '.eslintrc.yaml' ratings: - paths: - - "**.js" + paths: + - '**.js' diff --git a/.eslintrc.yaml b/.eslintrc.yaml index bda2f84..035a400 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -2,15 +2,6 @@ env: node: true es6: true mocha: true + es2022: true -plugins: - - haraka - -extends: [ "eslint:recommended", "plugin:haraka/recommended" ] - -root: true - -rules: - indent: [2, 4, {"SwitchCase": 1}] - no-console: 0 - no-unused-vars: 1 +extends: ['@haraka'] diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8abca40..fd04e8f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,7 @@ version: 2 updates: - - package-ecosystem: "npm" # See documentation for possible values - directory: "/" # Location of package manifests + - package-ecosystem: 'npm' # See documentation for possible values + directory: '/' # Location of package manifests schedule: - interval: "daily" + interval: 'daily' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74e7a36..1d07c8c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,7 @@ name: CI on: push: + pull_request: env: CI: true @@ -14,28 +15,10 @@ jobs: uses: haraka/.github/.github/workflows/coverage.yml@master secrets: inherit - test: - needs: lint - runs-on: ${{ matrix.os }} - # services: - # redis: - # image: redis - # ports: - # - 6379:6379 - strategy: - matrix: - os: [ ubuntu-latest, windows-latest ] - node-version: [ 14, 16, 18 ] - fail-fast: false + ubuntu: + needs: [lint] + uses: haraka/.github/.github/workflows/ubuntu.yml@master - steps: - - uses: actions/checkout@v3 - - - uses: actions/setup-node@v3 - name: Node ${{ matrix.node-version }} on ${{ matrix.os }} - with: - node-version: ${{ matrix.node-version }} - - - run: npm install - - - run: npm test + windows: + needs: [lint] + uses: haraka/.github/.github/workflows/windows.yml@master diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3627451..8314a66 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,10 +2,10 @@ name: CodeQL on: push: - branches: [ master ] + branches: [master] pull_request: # The branches below must be a subset of the branches above - branches: [ master ] + branches: [master] schedule: - cron: '18 7 * * 4' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d489fbd..e81c15f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,4 +13,4 @@ env: jobs: publish: uses: haraka/.github/.github/workflows/publish.yml@master - secrets: inherit \ No newline at end of file + secrets: inherit diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a8e94cb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule ".release"] + path = .release + url = git@github.com:msimerson/.release.git diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 0000000..8ded5e0 --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,2 @@ +singleQuote: true +semi: false diff --git a/.release b/.release new file mode 160000 index 0000000..36bb27a --- /dev/null +++ b/.release @@ -0,0 +1 @@ +Subproject commit 36bb27a93862517943e04f24fd67b0df2da6cbbe diff --git a/Changes.md b/CHANGELOG.md similarity index 65% rename from Changes.md rename to CHANGELOG.md index f1abd6b..77a0ece 100644 --- a/Changes.md +++ b/CHANGELOG.md @@ -1,19 +1,37 @@ - -## NEXT - 202Y-MM-DD - - -### 1.1.0 - 2022-05-27 +### Unreleased + +### [1.2.0] - 2024-04-14 + +- feat: getDir can parse different types of files in a dir +- feat: all file readers now have load and loadPromise, so that + a feature like getDir can safely use the nicer promise API +- chore: require syntax, prefix node builtins with `node:` +- moved configfile -> lib/reader +- moved readers/ -> lib/readers +- refactored regex lib to lib/regex +- refactored watch functions into lib/watch +- es6 + - add several uses of `...` (spread operator / param collection) + - replace `for i` with `for ... of` + - consolidate all cases of type detection into configfile.getType + - replace `new Promise` with async/await + - use shortened array function syntax +- ci: update to shared haraka/.github +- dep: eslint-plugin-haraka -> @haraka/eslint-config +- lint: remove duplicate / stale rules from .eslintrc +- package.json: populate [files] +- deps: version bumps + +### [1.1.0] - 2022-05-27 - chore(ci): depend on shared GHA workflows - chore(dep): eslint 6 -> 8 - chore(dep): mocha 8 -> 9 - ### 1.0.20 - 2021-09-01 - chore(dep): update YAML 3.13 -> 4.1 (#65) - ### 1.0.19 - 2021-06-10 - configfile: disable watch dir when platform not mac or win @@ -22,12 +40,10 @@ - configfile: use simpler es6 `for..in` and `for..of` - getDir tests, use os.EOL for comparison - ### 1.0.18 - 2019-10-11 - add support for loading `.js` configurations - ### 1.0.17 - 2018-12-19 - refactor ./config.js as an es6 class @@ -36,13 +52,11 @@ - watch: recursive=true - permit retrieval of fully qualified path - ### 1.0.16 - 2018-11-02 - remove trailing ; from function declarations - add config.getInt(filename, default_value) - ### 1.0.15 - 2017-09-21 - additional test for 'missing json loads yaml' @@ -51,35 +65,29 @@ - configs w/o .ext or declared type default to flat - add test for json/yaml !filename overloads - ### 1.0.14 - 2017-09-19 -- add __dirname/../../config to config_dir_candidates for haraka/Haraka/tests/* +- add \_\_dirname/../../config to config_dir_candidates for haraka/Haraka/tests/\* - sync process.env.HARAKA_TEST_DIR from haraka/Haraka/config - eslint no-var updates #25 - ### 1.0.13 - 2017-06-16 - lint updates for eslint 4 - ### 1.0.12 - 2017-05-21 - unref() the setInterval so that Haraka can gracefully exit - ### 1.0.11 - 2017-03-04 - add config.getDir, loads all files in a directory - ### 1.0.10 - 2017-02-05 - log error vs throw on bad YAML - fix appveyor badge URL - ### 1.0.9 - 2017-01-27 - config cache fix (see haraka/Haraka#1738) @@ -89,29 +97,27 @@ - use haraka-eslint plugin (vs local copy of .eslintrc) - lint updates - ### 1.0.8 - 2017-01-02 - version bump, lint updates & sync - lint fixes - ### 1.0.7 - 2016-11-17 - update tests for appveyor (Windows) compatibility #9 - ### 1.0.6 - 2016-11-10 - handle invalid .ini lines properly (skip them) - ### 1.0.5 - 2016-10-25 - do not leave behind a `*` section in config (due to wildcard boolean) - ### 1.0.3 - added wildcard boolean support - reduce node required 4.3 -> 0.10.43 + +[1.1.0]: https://github.com/haraka/haraka-config/releases/tag/1.1.0 +[1.2.0]: https://github.com/haraka/haraka-config/releases/tag/v1.2.0 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..a7ac9c8 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,8 @@ +# Contributors + +This handcrafted artisinal software is brought to you by: + +|
msimerson (52) |
PSSGCSim (7) |
baudehlo (1) |
Wesitos (1) |
oreoluwa (1) | +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | + +this file is maintained by [.release](https://github.com/msimerson/.release) diff --git a/README.md b/README.md index 47eb135..7c6b61b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ - # haraka-config Haraka config file loader, parser, and watcher. @@ -9,14 +8,14 @@ Haraka config file loader, parser, and watcher. Haraka's config loader can load several types of configuration files. -* 'value' - load a flat file containing a single value (default) -* 'ini' - load an ini file -* 'json' - load a json file -* 'hjson' - load a hjson file -* 'yaml' - load a yaml file -* 'list' - load a flat file containing a list of values -* 'data' - load a flat file containing a list of values, keeping comments and whitespace. -* 'binary' - load a binary file into a Buffer +- value - load a flat file containing a single value (default) +- ini - load an ini file +- json - load a json file +- hjson - load a hjson file +- yaml - load a yaml file +- list - load a flat file containing a list of values +- data - load a flat file containing a list, keeping comments and whitespace. +- binary - load a binary file into a Buffer See the [File Formats](#file_formats) section below for a more detailed explanation of each of the formats. @@ -25,14 +24,15 @@ explanation of each of the formats. ```js // From within a plugin: -const cfg = this.config.get(name, [type], [callback], [options]); +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 -const cfg = this.config.get('rambling.paths', 'list'); +const cfg = this.config.get('rambling.paths', 'list') ``` `type` can be any of the types listed above. @@ -42,40 +42,34 @@ the `type` parameter can be omitted. `callback` is an optional callback function that will be called when an update is detected on the file after the configuration cache has been -updated by re-reading the file. Use this to refresh configuration +updated by re-reading the file. Use this to refresh configuration variables within your plugin. Example: ```js exports.register = function () { - const plugin = this - plugin.loginfo('register called') - plugin.load_my_plugin_ini() + this.loginfo('register called') + this.load_my_plugin_ini() } exports.load_my_plugin_ini = function () { - 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)}`) + this.cfg = this.config.get('my_plugin.ini', () => { + // This closure is run a few seconds after my_plugin.ini changes + // Re-run the outer function again + this.load_my_plugin_ini() + }) + this.loginfo(`cfg=${JSON.stringify(this.cfg)}`) } exports.hook_connect = function (next, connection) { - // plugin.cfg here will be kept updated + // this.cfg here will be kept updated } ``` The `options` object can accepts the following keys: -* `no_watch` (default: false) - prevents Haraka from watching for updates. -* `no_cache` (default: false) - prevents Haraka from caching the file. This -means that the file will be re-read on every call to `config.get`. This is -not recommended as config files are read syncronously, will block the event -loop, and will slow down Haraka. -* `booleans` (default: none) - for .ini files, this allows specifying -boolean type keys. Default true or false can be specified. +- `no_watch` (default: false) - prevents Haraka from watching for updates. +- `no_cache` (default: false) - prevents Haraka from caching the file. The file will be re-read on every call to `config.get`. This is not recommended as config files are read syncronously and will slow down Haraka. +- `booleans` (default: none) - for .ini files, this allows specifying boolean type keys. Default true or false can be specified. ## Default Config and Overrides @@ -84,7 +78,7 @@ and another user installed file containing overrides. The default configs reside: - Haraka: within the config directory in the Haraka install (where `npm i` -installed Haraka) + installed Haraka) - NPM plugins - inside the module/config directory Config files with overrides are **always** installed in the Haraka config @@ -92,21 +86,21 @@ directory, which you specified when you ran `haraka -i`. Overrides work in the following manner: -* For `json`, `ini` and `yaml` config, values are overridden on a deep -key by key basis. -* For every other config format, an override file replaces the entire -config. +- For `json`, `ini` and `yaml` config, values are overridden on a deep + key by key basis. +- For every other config format, an override file replaces the entire + config. ## Examples 1. a plugin installed as a module (or a core Haraka plugin) -loads a `list` config from their own `config/plugin_name` file. That list -can be completely overridden by a file called `config/plugin_name` in the -Haraka local install directory. + loads a `list` config from their own `config/plugin_name` file. That list + can be completely overridden by a file called `config/plugin_name` in the + Haraka local install directory. 2. a plugin using default config from `config/plugin_name.ini` -can be overridden on a key-by-key basis. A default -`plugin_name.ini` might contain: + can be overridden on a key-by-key basis. A default + `plugin_name.ini` might contain: ```ini toplevel1=foo @@ -136,19 +130,15 @@ sub1=something sub2=otherthing ``` -This allows plugins to provide a default config, and allow users to override +This allows plugins to ship a default config and users can override values on a key-by-key basis. -File Formats -============ +# File Formats -Ini Files ---------- +## Ini Files -INI files have their heritage in early versions of Microsoft Windows. -Entries are a simple format of key=value pairs, with optional [sections]. +[INI files](https://en.wikipedia.org/wiki/INI_file) are key=value pairs, with optional [sections]. A typical example: -Here is a typical example: ```ini first_name=Matt last_name=Sergeant @@ -162,6 +152,7 @@ haraka qpsmtpd spamassassin ``` + That produces the following Javascript object: ```js @@ -182,23 +173,27 @@ That produces the following Javascript object: } ``` -Items before any [section] marker are in the implicit [main] section. +Items before any `[section]` marker are in the implicit `[main]` section. -There is some auto-conversion of values on the right hand side of -the equals: integers are converted to integers, floats are converted to -floats. +Some values on the right hand side of the equals are converted: -The key=value pairs support continuation lines using the -backslash "\" character. +- integers are converted to integers +- floats are converted to floats. + +The key=value pairs support continuation lines using the 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]. +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: +Declaring booleans ensures that values are converted as boolean when parsed, and supports the following options for boolean values: ``` true, yes, ok, enabled, on, 1 @@ -210,13 +205,19 @@ 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 @@ -231,24 +232,15 @@ which produces this javascript array: ['first_host', 'second_host', 'third_host'] ``` -Flat Files ----------- +## Flat Files -Flat files are simply either lists of values separated by \n or a single -value in a file on its own. Those who have used qmail or qpsmtpd will be -familiar with this format. -Lines starting with '#' and blank lines will be ignored unless the type is -specified as 'data', however even then line endings will be stripped. -See plugins/dnsbl.js for an example. +Flat files are simply either lists of values separated by \n or a single value in a file on its own. Qmail or qpsmtpd users will be familiar with this format. Lines starting with '#' and blank lines will be ignored unless the type is specified as 'data', however even then line endings will be stripped. -JSON Files ----------- +## JSON Files These are as you would expect, and returns an object as given in the file. -If a requested .json or .hjson file does not exist then the same file will be checked -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. +If a requested .json or .hjson file does not exist then the same file will be checked 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. @@ -258,20 +250,18 @@ You can use JSON, HJSON or YAML files to override any other file by prefixing th } ``` -If the config/smtpgreeting file did not exist, then this value would replace -it. +If the config/smtpgreeting file did not exist, then this value would replace it. -NOTE: You must ensure that the data type (e.g. Object, Array or String) for -the replaced value is correct. This cannot be done automatically. +NOTE: You must ensure that the data type (e.g. Object, Array or String) for the replaced value is correct. This cannot be done automatically. -Hjson Files ----------- +## Hjson Files Hjson is a syntax extension to JSON. It is intended to be used like a user interface for humans, to read and edit before passing the JSON data to the machine. That means you can use it to parse JSON files but it is not intended as a replacement. You can check [Hjson's homepage](https://hjson.org) to get familiar with it and you can [try out its syntax](https://hjson.org/try.html). Main features: + - Comments - Optional quotes - Optional commas @@ -281,51 +271,35 @@ Example syntax ```hjson { - # specify rate in requests/second (because comments are helpful!) - rate: 1000 + # specify rate in requests/second (because comments are helpful!) + rate: 1000 - // prefer c-style comments? - /* feeling old fashioned? */ + // prefer c-style comments? + /* feeling old fashioned? */ - # did you notice that rate does not need quotes? - hey: look ma, no quotes for strings either! + # did you notice that rate does not need quotes? + hey: look ma, no quotes for strings either! - # best of all - notice: [] - anything: ? + # best of all + notice: [] + anything: ? - # yes, commas are optional! + # yes, commas are optional! } ``` -NOTE: Hjson can be also replaced by YAML configuration file. You can find more on this issue under JSON section. - +NOTE: Hjson can be also replaced by a YAML configuration file. You can find more on this issue under JSON section. -YAML Files ----------- +## YAML Files As per JSON files above but in YAML format. +# Reloading/Caching -Reloading/Caching -======== - -Haraka automatically reloads configuration files, but this only works if -whatever is looking at that config re-calls config.get() to retrieve the -new config. Providing a callback in the config.get() call is the most -efficient method to do this. - -Configuration files are watched for changes using filesystem events which -are inexpensive. Due to caching, calling config.get() is normally a -lightweight process. +Haraka automatically reloads configuration files, but this only works if whatever is looking at that config re-calls config.get() to retrieve the new config. Providing a callback in the config.get() call is the most efficient method to do this. -On Linux/Windows, newly created files that Haraka has tried to read in the -past will be noticed immediately and loaded. For other operating systems, -it may take up to 60 seconds to load, due to differences between in the -kernel APIs for watching files/directories. +Configuration files are watched for changes using filesystem events which are inexpensive. Due to caching, calling config.get() is normally a lightweight process. -Haraka reads a number of configuration files at startup. Any files read -in a plugins register() function are read *before* Haraka drops privileges. -Be sure that Haraka's user/group has permission to read these files else -Haraka will be unable to update them after changes. +On Linux/Windows, newly created files that Haraka has tried to read in the past will be noticed immediately and loaded. For other operating systems, it may take up to 60 seconds to load, due to differences between in the kernel APIs for watching files/directories. +Haraka reads a number of configuration files at startup. Any files read in a plugins register() function are read _before_ Haraka drops privileges. Be sure that Haraka's user/group has permission to read these files else Haraka will be unable to update them after changes. diff --git a/config.js b/config.js index 41d1dbd..bc18492 100644 --- a/config.js +++ b/config.js @@ -1,65 +1,69 @@ -'use strict'; +'use strict' -const path = require('path'); +const path = require('path') -const cfreader = require('./configfile'); +const reader = require('./lib/reader') class Config { - constructor (root_path, no_overrides) { - this.root_path = root_path || cfreader.config_path; - - 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'); - } - } - - get (name, type, cb, options) { - const a = this.arrange_args([name, type, cb, options]); - if (!a[1]) a[1] = 'value'; + constructor(root_path, no_overrides) { + this.root_path = root_path || reader.config_path - const full_path = path.isAbsolute(name) ? name : path.resolve(this.root_path, a[0]); + 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 || reader.config_path + this.root_path = path.join(process.env.HARAKA, 'config') + } + } - let results = cfreader.read_config(full_path, a[1], a[2], a[3]); + get(...args) { + /* eslint prefer-const: 0 */ + let [name, type, cb, options] = this.arrange_args(args) + if (!type) type = 'value' - if (this.overrides_path) { - const overrides_path = path.resolve(this.overrides_path, a[0]); + const full_path = path.isAbsolute(name) + ? name + : path.resolve(this.root_path, name) - const overrides = cfreader.read_config(overrides_path, a[1], a[2], a[3]); + let results = reader.read_config(full_path, type, cb, options) - results = merge_config(results, overrides, a[1]); - } + if (this.overrides_path) { + const overrides_path = path.resolve(this.overrides_path, name) - // Pass arrays by value to prevent config being modified accidentally. - if (Array.isArray(results)) return results.slice(); + const overrides = reader.read_config(overrides_path, type, cb, options) - return results; + results = merge_config(results, overrides, type) } - getInt (filename, default_value) { + // Pass arrays by value to prevent config being modified accidentally. + if (Array.isArray(results)) return results.slice() - if (!filename) return NaN; + return results + } - const full_path = path.resolve(this.root_path, filename); - const r = parseInt(cfreader.read_config(full_path, 'value', null, null), 10); + getInt(filename, default_value) { + if (!filename) return NaN - if (!isNaN(r)) return r; - return parseInt(default_value, 10); - } + const full_path = path.resolve(this.root_path, filename) + const r = parseInt(reader.read_config(full_path, 'value', null, null), 10) - getDir (name, opts, done) { - cfreader.read_dir(path.resolve(this.root_path, name), opts).then((files) => { - done(null, files) // keep the API consistent - }).catch(done) - } + if (!isNaN(r)) return r + return parseInt(default_value, 10) + } - arrange_args (args) { + getDir(name, opts, done) { + reader + .read_dir(path.resolve(this.root_path, name), opts) + .then((files) => { + done(null, files) // keep the API consistent + }) + .catch(done) + } - /* ways get() can be called: + arrange_args(args) { + /* ways get() can be called: config.get('thing'); config.get('thing', type); config.get('thing', cb); @@ -69,96 +73,81 @@ class Config { 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|js|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 'js': - case 'ini': - fs_type = fs_ext; - break; - - default: - fs_type = 'value'; - break; - } - } - - return [fs_name, fs_type, cb, options]; + const fs_name = args.shift() + let fs_type = null + let cb + let options + + for (const arg of args) { + if ([undefined, null].includes(arg)) continue + switch (typeof arg) { + case 'function': + cb = arg + continue + case 'object': + options = arg + continue + case 'string': + if (/^(ini|value|list|data|h?json|js|yaml|binary)$/.test(arg)) { + fs_type = arg + continue + } + console.log(`unknown string: ${arg}`) + continue + } + // console.log(`unknown arg: ${arg}, typeof: ${typeof arg}`); } - 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(); + if (!fs_type) fs_type = reader.getType(fs_name) -function merge_config (defaults, overrides, type) { - switch (type) { - case 'ini': - case 'hjson': - case 'json': - case 'js': - case 'yaml': - return merge_struct(JSON.parse(JSON.stringify(defaults)), overrides); - } + return [fs_name, fs_type, cb, options] + } - if (Array.isArray(overrides) && Array.isArray(defaults) && - overrides.length > 0) { - return overrides; + 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 + } +} - if (overrides != null) return overrides; - - return defaults; +module.exports = new Config() + +function merge_config(defaults, overrides, type) { + switch (type) { + case 'ini': + case 'hjson': + case 'json': + case 'js': + case 'yaml': + return merge_struct(JSON.parse(JSON.stringify(defaults)), overrides) + } + + if ( + Array.isArray(overrides) && + Array.isArray(defaults) && + overrides.length > 0 + ) { + return overrides + } + + if (overrides != null) return overrides + + return defaults } -function merge_struct (defaults, overrides) { - for (const k in overrides) { - if (k in defaults) { - if (typeof overrides[k] === 'object' && typeof defaults[k] === 'object') { - defaults[k] = merge_struct(defaults[k], overrides[k]); - } - else { - defaults[k] = overrides[k]; - } - } - else { - defaults[k] = overrides[k]; - } +function merge_struct(defaults, overrides) { + for (const k in overrides) { + if (k in defaults) { + if (typeof overrides[k] === 'object' && typeof defaults[k] === 'object') { + defaults[k] = merge_struct(defaults[k], overrides[k]) + } else { + defaults[k] = overrides[k] + } + } else { + defaults[k] = overrides[k] } - return defaults; + } + return defaults } diff --git a/configfile.js b/configfile.js deleted file mode 100644 index 8e3491a..0000000 --- a/configfile.js +++ /dev/null @@ -1,397 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); - -let config_dir_candidates = [ - // these work when this file is loaded as require('./config.js') - path.join(__dirname, 'config'), // Haraka ./config dir - __dirname, // npm packaged plugins -]; - -class cfreader { - constructor () { - this.watch_files = true; - this._config_cache = {}; - this._read_args = {}; - this._watchers = {}; - this._enoent_timer = false; - this._enoent_files = {}; - this._sedation_timers = {}; - this._overrides = {}; - - this.get_path_to_config_dir() - - // for "ini" type files - this.regex = { - section: /^\s*\[\s*([^\]]*?)\s*\]\s*$/, - param: /^\s*([\w@:._\-/[\]]+)\s*(?:=\s*(.*?)\s*)?$/, - comment: /^\s*[;#].*$/, - line: /^\s*(.*?)\s*$/, - blank: /^\s*$/, - continuation: /\\[ \t]*$/, - is_integer: /^-?\d+$/, - is_float: /^-?\d+\.\d+$/, - is_truth: /^(?:true|yes|ok|enabled|on|1)$/i, - is_array: /(.+)\[\]$/, - } - } - - get_path_to_config_dir () { - if (process.env.HARAKA) { - // console.log(`process.env.HARAKA: ${process.env.HARAKA}`); - this.config_path = path.join(process.env.HARAKA, 'config'); - return; - } - - if (process.env.NODE_ENV === 'test') { - // loaded by haraka-config/test/* - this.config_path = path.join(__dirname, 'test', 'config'); - return; - } - - // these work when this is loaded with require('haraka-config') - if (/node_modules[\\/]haraka-config$/.test(__dirname)) { - config_dir_candidates = [ - path.join(__dirname, '..', '..', 'config'), // haraka/Haraka/* - path.join(__dirname, '..', '..'), // npm packaged modules - ] - } - - for (const candidate of config_dir_candidates) { - try { - const stat = fs.statSync(candidate); - if (stat && stat.isDirectory()) { - this.config_path = candidate; - return; - } - } - catch (ignore) { - console.error(ignore.message); - } - } - } - - on_watch_event (name, type, options, cb) { - return (fse, filename) => { - if (this._sedation_timers[name]) { - clearTimeout(this._sedation_timers[name]); - } - this._sedation_timers[name] = setTimeout(() => { - console.log(`Reloading file: ${name}`); - this.load_config(name, type, options); - delete this._sedation_timers[name]; - if (typeof cb === 'function') cb(); - }, 5 * 1000); - - if (fse !== 'rename') return; - // https://github.com/joyent/node/issues/2062 - // After a rename event, re-watch the file - this._watchers[name].close(); - try { - this._watchers[name] = fs.watch(name, { persistent: false }, this.on_watch_event(...arguments)); - } - catch (e) { - if (e.code === 'ENOENT') { - this._enoent_files[name] = true; - this.ensure_enoent_timer(); - } - else { - console.error(`Error watching file: ${name} : ${e}`); - } - } - } - } - - watch_dir () { - // NOTE: Has OS platform limitations: - // https://nodejs.org/api/fs.html#fs_fs_watch_filename_options_listener - const cp = this.config_path; - if (this._watchers[cp]) return; - - try { - this._watchers[cp] = fs.watch(cp, { persistent: false }, (fse, filename) => { - if (!filename) return; - const full_path = path.join(cp, filename); - if (!this._read_args[full_path]) return; - const args = this._read_args[full_path]; - if (args.options && args.options.no_watch) return; - if (this._sedation_timers[filename]) { - clearTimeout(this._sedation_timers[filename]); - } - this._sedation_timers[filename] = setTimeout(() => { - console.log(`Reloading file: ${full_path}`); - this.load_config(full_path, args.type, args.options); - delete this._sedation_timers[filename]; - if (typeof args.cb === 'function') args.cb(); - }, 5 * 1000); - }); - } - catch (e) { - console.error(`Error watching directory ${cp}(${e})`); - } - return; - } - - watch_file (name, type, cb, options) { - // This works on all OS's, but watch_dir() above is preferred for Linux and - // Windows as it is far more efficient. - // NOTE: we need a fs.watch per file. It's impossible to watch non-existent - // files. Instead, note which files we attempted - // to watch that returned ENOENT and fs.stat each periodically - if (this._watchers[name] || (options && options.no_watch)) return; - - try { - this._watchers[name] = fs.watch(name, {persistent: false}, this.on_watch_event(name, type, options, cb)); - } - catch (e) { - if (e.code !== 'ENOENT') { // ignore error when ENOENT - console.error(`Error watching config file: ${name} : ${e}`); - } - else { - this._enoent_files[name] = true; - this.ensure_enoent_timer(); - } - } - } - - get_cache_key (name, options) { - - // Ignore options etc. if this is an overriden value - if (this._overrides[name]) return name; - - if (options) { - // ordering of objects isn't guaranteed to be consistent, but typically is. - return name + JSON.stringify(options); - } - - if (this._read_args[name] && this._read_args[name].options) { - return name + JSON.stringify(this._read_args[name].options); - } - - return name; - } - - read_config (name, type, cb, options) { - // Store arguments used so we can: - // 1. re-use them by filename later - // 2. to know which files we've read, so we can ignore - // other files written to the same directory. - - this._read_args[name] = { - type, - cb, - options - }; - - // Check cache first - if (!process.env.WITHOUT_CONFIG_CACHE) { - const cache_key = this.get_cache_key(name, options); - // console.log(`\tcache_key: ${cache_key}`); - if (this._config_cache[cache_key] !== undefined) { - // console.log(`\t${name} is cached`); - return this._config_cache[cache_key]; - } - } - - // load config file - const result = this.load_config(name, type, options); - if (!this.watch_files) return result; - - // We can watch the directory on these platforms which - // allows us to notice when files are newly created. - switch (process.platform) { - case 'win32': - case 'win64': - case 'linux': - this.watch_dir(); - break; - default: - // All other operating systems - this.watch_file(name, type, cb, options); - } - - return result; - } - - read_dir (name, opts) { - return new Promise((resolve, reject) => { - - this._read_args[name] = { opts } - const type = opts.type || 'binary'; - - isDirectory(name) - .then((result) => { - return fsReadDir(name); - }) - .then((fileList) => { - const reader = require(path.resolve(__dirname, 'readers', type)); - const promises = []; - for (const file of fileList) { - promises.push(reader.loadPromise(path.resolve(name, file))) - } - return Promise.all(promises); - }) - .then((fileList) => { - // console.log(fileList); - resolve(fileList); - }) - .catch(reject) - - if (opts.watchCb) this.fsWatchDir(name); - }) - } - - ensure_enoent_timer () { - if (this._enoent_timer) return; - // Create timer - this._enoent_timer = setInterval(() => { - const files = Object.keys(this._enoent_files); - for (const fileOuter of files) { - /* BLOCK SCOPE */ - ((file) => { - fs.stat(file, (err) => { - if (err) return; - // File now exists - delete(this._enoent_files[file]); - const args = this._read_args[file]; - this.load_config(file, args.type, args.options, args.cb); - this._watchers[file] = fs.watch(file, {persistent: false}, - this.on_watch_event(file, args.type, args.options, args.cb)); - }); - })(fileOuter); // END BLOCK SCOPE - } - }, 60 * 1000); - this._enoent_timer.unref(); // This shouldn't block exit - } - - get_filetype_reader (type) { - switch (type) { - case 'list': - case 'value': - case 'data': - case '': - return require(path.resolve(__dirname, 'readers', 'flat')); - } - return require(path.resolve(__dirname, 'readers', type)); - } - - load_config (name, type, options) { - let result; - - if (!type) { - type = path.extname(name).toLowerCase().substring(1); - } - - let cfrType = this.get_filetype_reader(type); - - if (!fs.existsSync(name)) { - if (!/\.h?json$/.test(name)) { - return cfrType.empty(options, type); - } - - const yaml_name = name.replace(/\.h?json$/, '.yaml'); - if (!fs.existsSync(yaml_name)) return cfrType.empty(options, type); - - name = yaml_name; - type = 'yaml'; - - cfrType = this.get_filetype_reader(type); - } - - const cache_key = this.get_cache_key(name, options); - try { - switch (type) { - case 'ini': - result = cfrType.load(name, options, this.regex); - break; - case 'hjson': - case 'json': - case 'yaml': - result = cfrType.load(name); - this.process_file_overrides(name, options, result); - break; - // case 'binary': - default: - result = cfrType.load(name, type, options, this.regex); - } - this._config_cache[cache_key] = result; - } - catch (err) { - console.error(err.message); - if (this._config_cache[cache_key]) { - return this._config_cache[cache_key]; - } - return cfrType.empty(options, type); - } - return result; - } - - process_file_overrides (name, options, result) { - // We might be re-loading this file: - // * build a list of cached overrides - // * remove them and add them back - const cp = this.config_path; - const cache_key = this.get_cache_key(name, options); - - if (this._config_cache[cache_key]) { - for (const ck in this._config_cache[cache_key]) { - if (ck.substr(0,1) === '!') delete this._config_cache[path.join(cp, ck.substr(1))]; - } - } - - // Allow JSON files to create or overwrite other config file data - // by prefixing the outer variable name with ! e.g. !smtp.ini - for (const key in result) { - if (key.substr(0,1) !== '!') continue; - const fn = key.substr(1); - // Overwrite the config cache for this filename - console.log(`Overriding file ${fn} with config from ${name}`); - this._config_cache[path.join(cp, fn)] = result[key]; - } - } - - fsWatchDir (dirPath) { - - if (this._watchers[dirPath]) return; - const watchOpts = { persistent: false, recursive: true } - - // recursive is only supported on Windows (win32, win64) and macOS (darwin) - if (!/win/.test(process.platform)) watchOpts.recursive = false - - this._watchers[dirPath] = fs.watch(dirPath, watchOpts, (fse, filename) => { - // console.log(`event: ${fse}, ${filename}`); - if (!filename) return; - const full_path = path.join(dirPath, filename); - const args = this._read_args[dirPath]; - // console.log(args); - if (this._sedation_timers[full_path]) { - clearTimeout(this._sedation_timers[full_path]); - } - this._sedation_timers[full_path] = setTimeout(() => { - delete this._sedation_timers[full_path]; - args.opts.watchCb(); - }, 2 * 1000); - }); - } -} - -module.exports = new cfreader(); - -function isDirectory (filepath) { - return new Promise((resolve, reject) => { - fs.stat(filepath, (err, stat) => { - if (err) return reject(err); - resolve(stat.isDirectory()); - }) - }) -} - -function fsReadDir (filepath) { - return new Promise((resolve, reject) => { - fs.readdir(filepath, (err, fileList) => { - if (err) return reject(err); - resolve(fileList); - }) - }) -} diff --git a/lib/reader.js b/lib/reader.js new file mode 100644 index 0000000..e729024 --- /dev/null +++ b/lib/reader.js @@ -0,0 +1,247 @@ +'use strict' + +const fs = require('node:fs') +const fsp = require('node:fs/promises') +const path = require('node:path') + +const watch = require('./watch') + +let config_dir_candidates = [ + path.join(__dirname, '..', 'config'), // Haraka ./config dir + path.join(__dirname, '..'), // npm packaged plugins +] + +class Reader { + constructor() { + this.watch_files = true + this._config_cache = {} + this._read_args = {} + this._overrides = {} + + this.get_path_to_config_dir() + } + + get_path_to_config_dir() { + if (process.env.HARAKA) { + // console.log(`process.env.HARAKA: ${process.env.HARAKA}`); + this.config_path = path.join(process.env.HARAKA, 'config') + return + } + + if (process.env.NODE_ENV === 'test') { + // console.log(`loaded by haraka-config/test/*`) + this.config_path = path.join(__dirname, '..', 'test', 'config') + return + } + + // these work when this is loaded with require('haraka-config') + if (/node_modules[\\/]haraka-config$/.test(__dirname)) { + config_dir_candidates = [ + path.join(__dirname, '..', '..', 'config'), // haraka/Haraka/* + path.join(__dirname, '..', '..'), // npm packaged modules + ] + } + + for (const candidate of config_dir_candidates) { + try { + const stat = fs.statSync(candidate) + if (stat?.isDirectory()) { + this.config_path = candidate + return + } + } catch (ignore) { + console.error(ignore.message) + } + } + } + + getType(fileName) { + const ext = path.extname(fileName).substring(1).toLowerCase() + switch (ext) { + case 'hjson': + case 'json': + case 'yaml': + case 'js': + case 'ini': + case 'list': + case 'data': + return ext + case 'yml': + return 'yaml' + case 'pem': + case 'bin': + case 'binary': + return 'binary' + default: + return 'value' + } + } + + get_cache_key(name, options) { + // Ignore options etc. if this is an overriden value + if (this._overrides[name]) return name + + if (options) { + // ordering of objects isn't guaranteed to be consistent, but typically is. + return name + JSON.stringify(options) + } + + if (this._read_args[name] && this._read_args[name].options) { + return name + JSON.stringify(this._read_args[name].options) + } + + return name + } + + read_config(name, type, cb, options) { + // Store arguments used so we can: + // 1. re-use them by filename later + // 2. to know which files we've read, so we can ignore + // other files written to the same directory. + + this._read_args[name] = { + type, + cb, + options, + } + + // Check cache first + if (!process.env.WITHOUT_CONFIG_CACHE) { + const cache_key = this.get_cache_key(name, options) + // console.log(`\tcache_key: ${cache_key}`); + if (this._config_cache[cache_key] !== undefined) { + // console.log(`\t${name} is cached`); + return this._config_cache[cache_key] + } + } + + // load config file + const result = this.load_config(name, type, options) + if (!this.watch_files) return result + + switch (process.platform) { + // these platforms allow us to notice when files are created. + case 'win32': + case 'win64': + case 'linux': + watch.dir(this) + break + default: + // All other operating systems + watch.file(this, name, type, cb, options) + } + + return result + } + + read_dir(name, opts = {}) { + return new Promise((resolve, reject) => { + this._read_args[name] = { opts } + + fsp + .stat(name) + .then((stat) => stat.isDirectory()) + .then(() => fsp.readdir(name)) + .then(async (fileList) => { + const contents = [] + for (const file of fileList) { + const type = opts.type ?? this.getType(file) + contents.push( + this.load_config(path.resolve(name, file), type, opts), + ) + } + return contents + }) + .then(resolve) + .catch(reject) + + if (opts.watchCb) watch.dir2(this, name) + }) + } + + get_filetype_reader(type) { + switch (type) { + case 'list': + case 'value': + case 'data': + case '': + return require(path.resolve(__dirname, 'readers', 'flat')) + } + return require(path.resolve(__dirname, 'readers', type)) + } + + load_config(name, type, options) { + let result + + if (!type) type = this.getType(name) + + let cfrType = this.get_filetype_reader(type) + + if (!fs.existsSync(name)) { + if (!/\.h?json$/.test(name)) { + return cfrType.empty(options, type) + } + + const yaml_name = name.replace(/\.h?json$/, '.yaml') + if (!fs.existsSync(yaml_name)) return cfrType.empty(options, type) + + name = yaml_name + type = 'yaml' + + cfrType = this.get_filetype_reader(type) + } + + const cache_key = this.get_cache_key(name, options) + try { + switch (type) { + case 'ini': + result = cfrType.load(name, options) + break + case 'hjson': + case 'json': + case 'yaml': + result = cfrType.load(name) + this.process_file_overrides(name, options, result) + break + // case 'binary': + default: + result = cfrType.load(name, type, options) + } + this._config_cache[cache_key] = result + } catch (err) { + console.error(err.message) + if (this._config_cache[cache_key]) { + return this._config_cache[cache_key] + } + return cfrType.empty(options, type) + } + return result + } + + process_file_overrides(name, options, result) { + // We might be re-loading this file: + // * build a list of cached overrides + // * remove them and add them back + const cp = this.config_path + const cache_key = this.get_cache_key(name, options) + + if (this._config_cache[cache_key]) { + for (const ck in this._config_cache[cache_key]) { + if (ck.substr(0, 1) === '!') + delete this._config_cache[path.join(cp, ck.substr(1))] + } + } + + // Allow JSON files to create or overwrite other config file data + // by prefixing the outer variable name with ! e.g. !smtp.ini + for (const key in result) { + if (key.substr(0, 1) !== '!') continue + const fn = key.substr(1) + // Overwrite the config cache for this filename + console.log(`Overriding file ${fn} with config from ${name}`) + this._config_cache[path.join(cp, fn)] = result[key] + } + } +} + +module.exports = new Reader() diff --git a/lib/readers/binary.js b/lib/readers/binary.js new file mode 100644 index 0000000..de22534 --- /dev/null +++ b/lib/readers/binary.js @@ -0,0 +1,16 @@ +'use strict' + +exports.load = (name) => { + return require('node:fs').readFileSync(name) +} + +exports.loadPromise = async (name) => { + return { + path: name, + data: await require('node:fs/promises').readFile(name), + } +} + +exports.empty = () => { + return null +} diff --git a/lib/readers/flat.js b/lib/readers/flat.js new file mode 100644 index 0000000..cc689c3 --- /dev/null +++ b/lib/readers/flat.js @@ -0,0 +1,81 @@ +'use strict' + +const regex = require('../regex') + +exports.load = (...args) => { + return this.parseValue( + ...args, + require('node:fs').readFileSync(args[0], 'UTF-8'), + ) +} + +exports.loadPromise = async (...args) => { + return this.parseValue( + ...args, + await require('node:fs/promises').readFile(args[0], 'UTF-8'), + ) +} + +exports.parseValue = (name, type, options, data) => { + let result = [] + + if (type === 'data') { + while (data.length > 0) { + const match = data.match(/^([^\r\n]*)\r?\n?/) + result.push(match[1]) + data = data.slice(match[0].length) + } + return result + } + + for (const line of data.split(/\r\n|\r|\n/)) { + if (regex.comment.test(line)) continue + if (regex.blank.test(line)) continue + + const line_data = regex.line.exec(line) + if (!line_data) continue + + result.push(line_data[1].trim()) + } + + if (result.length && type !== 'list' && type !== 'data') { + result = result[0] + if (options && in_array(result, options.booleans)) { + return regex.is_truth.test(result) + } + if (regex.is_integer.test(result)) { + return parseInt(result, 10) + } + if (regex.is_float.test(result)) { + return parseFloat(result) + } + return result + } + + // Return hostname for 'me' if no result + if (/\/me$/.test(name) && !(result && result.length)) { + return [require('os').hostname()] + } + + // For value types with no result + if (!(type && (type === 'list' || type === 'data'))) { + if (!(result && result.length)) return null + } + + return result +} + +exports.empty = (options, type) => { + switch (type) { + case 'flat': + case 'value': + return null + default: + return [] + } +} + +function in_array(item, array) { + if (!Array.isArray(array)) return false + return array.includes(item) +} diff --git a/lib/readers/hjson.js b/lib/readers/hjson.js new file mode 100644 index 0000000..29e53ea --- /dev/null +++ b/lib/readers/hjson.js @@ -0,0 +1,15 @@ +'use strict' + +const hjson = require('hjson') + +exports.load = (name) => { + return hjson.parse(require('node:fs').readFileSync(name, 'UTF-8')) +} + +exports.loadPromise = async (name) => { + return hjson.parse(await require('node:fs/promises').readFile(name, 'UTF-8')) +} + +exports.empty = () => { + return {} +} diff --git a/lib/readers/ini.js b/lib/readers/ini.js new file mode 100644 index 0000000..6d8f2a6 --- /dev/null +++ b/lib/readers/ini.js @@ -0,0 +1,139 @@ +'use strict' + +const regex = require('../regex') + +exports.load = (...args) => { + return this.parseIni( + ...args, + require('node:fs').readFileSync(args[0], 'UTF-8'), + ) +} + +exports.loadPromise = async (...args) => { + return this.parseIni( + ...args, + await require('node:fs/promises').readFile(args[0], 'UTF-8'), + ) +} + +exports.parseIni = (name, options = {}, data) => { + let result = { main: {} } + let current_sect = result.main + let current_sect_name = 'main' + this.bool_matches = [] + if (options?.booleans) { + this.bool_matches = options.booleans.slice() + } + + // Initialize any booleans + result = this.init_booleans(options, result) + + let pre = '' + + for (let line of data.split(/\r\n|\r|\n/)) { + if (regex.comment.test(line)) continue + if (regex.blank.test(line)) continue + + let match = regex.section.exec(line) + if (match) { + if (!result[match[1]]) result[match[1]] = {} + current_sect = result[match[1]] + current_sect_name = match[1] + continue + } + + if (regex.continuation.test(line)) { + pre += line.replace(regex.continuation, '') + continue + } + + line = `${pre}${line}` + pre = '' + + match = regex.param.exec(line) + if (!match) { + exports.logger(`Invalid line in config file '${name}': ${line}`) + continue + } + + const keyName = match[1] + const keyVal = match[2] + + const setter = this.getSetter(current_sect, regex.is_array.test(keyName)) + + if ( + exports.isDeclaredBoolean(`${current_sect_name}.${keyName}`) || + exports.isDeclaredBoolean(`*.${keyName}`) + ) { + current_sect[keyName] = regex.is_truth.test(keyVal) + } else if (regex.is_integer.test(keyVal)) { + setter(keyName, parseInt(keyVal, 10)) + } else if (regex.is_float.test(keyVal)) { + setter(keyName, parseFloat(keyVal)) + } else { + setter(keyName, keyVal) + } + } + + return result +} + +exports.empty = (options) => { + this.bool_matches = [] + return this.init_booleans(options, { main: {} }) +} + +exports.getSetter = (current_sect, isArray) => { + if (isArray) { + return (key, value) => { + key = key.replace('[]', '') + if (!current_sect[key]) current_sect[key] = [] + current_sect[key].push(value) + } + } else { + return (key, value) => { + current_sect[key] = value + } + } +} + +exports.isDeclaredBoolean = (entry) => { + if (exports.bool_matches.includes(entry)) return true + return false +} + +exports.init_booleans = (options, result) => { + if (!options) return result + if (!Array.isArray(options.booleans)) return result + + // console.log(options.booleans); + for (let i = 0; i < options.booleans.length; i++) { + const m = /^(?:([^. ]+)\.)?(.+)/.exec(options.booleans[i]) + if (!m) continue + + let section = m[1] || 'main' + let key = m[2] + + const bool_default = + section[0] === '+' ? true : key[0] === '+' ? true : false + + if (section.match(/^(-|\+)/)) section = section.substr(1) + if (key.match(/^(-|\+)/)) key = key.substr(1) + + if (section === '*') continue // wildcard, don't initialize + + // so boolean detection in the next section will match + if (options.booleans.indexOf(`${section}.${key}`) === -1) { + this.bool_matches.push(`${section}.${key}`) + } + + if (!result[section]) result[section] = {} + result[section][key] = bool_default + } + + return result +} + +exports.logger = (msg) => { + console.log(msg) +} diff --git a/lib/readers/js.js b/lib/readers/js.js new file mode 100644 index 0000000..b6463fa --- /dev/null +++ b/lib/readers/js.js @@ -0,0 +1,13 @@ +'use strict' + +exports.load = (name) => { + return require(name) +} + +exports.loadPromise = async (name) => { + return require(name) +} + +exports.empty = () => { + return {} +} diff --git a/lib/readers/json.js b/lib/readers/json.js new file mode 100644 index 0000000..64144fe --- /dev/null +++ b/lib/readers/json.js @@ -0,0 +1,13 @@ +'use strict' + +exports.load = (name) => { + return JSON.parse(require('node:fs').readFileSync(name)) +} + +exports.loadPromise = async (name) => { + return JSON.parse(await require('node:fs/promises').readFile(name)) +} + +exports.empty = () => { + return {} +} diff --git a/lib/readers/yaml.js b/lib/readers/yaml.js new file mode 100644 index 0000000..35a88dd --- /dev/null +++ b/lib/readers/yaml.js @@ -0,0 +1,15 @@ +'use strict' + +const yaml = require('js-yaml') + +exports.load = (name) => { + return yaml.load(require('node:fs').readFileSync(name, 'UTF-8')) +} + +exports.loadPromise = async (name) => { + return yaml.load(await require('node:fs/promises').readFile(name, 'UTF-8')) +} + +exports.empty = () => { + return {} +} diff --git a/lib/regex.js b/lib/regex.js new file mode 100644 index 0000000..4c61fac --- /dev/null +++ b/lib/regex.js @@ -0,0 +1,12 @@ +module.exports = { + section: /^\s*\[\s*([^\]]*?)\s*\]\s*$/, + param: /^\s*([\w@:._\-/[\]]+)\s*(?:=\s*(.*?)\s*)?$/, + comment: /^\s*[;#].*$/, + line: /^\s*(.*?)\s*$/, + blank: /^\s*$/, + continuation: /\\[ \t]*$/, + is_integer: /^-?\d+$/, + is_float: /^-?\d+\.\d+$/, + is_truth: /^(?:true|yes|ok|enabled|on|1)$/i, + is_array: /(.+)\[\]$/, +} diff --git a/lib/watch.js b/lib/watch.js new file mode 100644 index 0000000..1277764 --- /dev/null +++ b/lib/watch.js @@ -0,0 +1,144 @@ +const fs = require('node:fs') +const path = require('node:path') + +const enoent = { timer: false, files: [] } +const watchers = {} +const sedation_timers = {} + +module.exports.ensure_enoent_timer = (reader) => { + if (enoent.timer) return + // Create timer + enoent.timer = setInterval(() => { + for (const file of Object.keys(enoent.files)) { + fs.stat(file, (err) => { + if (err) return + // File now exists + delete enoent.files[file] + const args = reader._read_args[file] + reader.load_config(file, args.type, args.options, args.cb) + watchers[file] = fs.watch( + file, + { persistent: false }, + this.onEvent(reader, file, args), + ) + }) + } + }, 60 * 1000) + enoent.timer.unref() // don't block process exit +} + +module.exports.file = (reader, name, type, cb, options) => { + // This works on all OS's, but watch_dir() above is preferred for Linux and + // Windows as it is far more efficient. + // NOTE: we need a fs.watch per file. It's impossible to watch non-existent + // files. Instead, note which files we attempted + // to watch that returned ENOENT and fs.stat each periodically + if (watchers[name] || (options && options.no_watch)) return + + try { + watchers[name] = fs.watch( + name, + { persistent: false }, + this.onEvent(reader, name, { type, options, cb }), + ) + } catch (e) { + if (e.code === 'ENOENT') { + // ignore error when ENOENT + enoent.files[name] = true + this.ensure_enoent_timer(reader) + } else { + console.error(`Error watching config file: ${name} : ${e}`) + } + } +} + +// used to watch main haraka config dir +module.exports.dir = (reader) => { + // NOTE: Has OS platform limitations: + // https://nodejs.org/api/fs.html#fs_fs_watch_filename_options_listener + const cp = reader.config_path + if (watchers[cp]) return + + try { + watchers[cp] = fs.watch( + cp, + { persistent: false }, + (fse, filename) => { + if (!filename) return + const full_path = path.join(cp, filename) + const args = reader._read_args[full_path] + if (!args) return + if (args.options?.no_watch) return + if (sedation_timers[filename]) { + clearTimeout(sedation_timers[filename]) + } + sedation_timers[filename] = setTimeout(() => { + console.log(`Reloading file: ${full_path}`) + reader.load_config(full_path, args.type, args.options) + delete sedation_timers[filename] + if (typeof args.cb === 'function') args.cb() + }, 5 * 1000) + }, + ) + } catch (e) { + console.error(`Error watching directory ${cp}(${e})`) + } +} + +// used by getDir +module.exports.dir2 = (reader, dirPath) => { + if (watchers[dirPath]) return + const watchOpts = { persistent: false, recursive: true } + + // recursive is only supported on Windows (win32, win64) and macOS (darwin) + if (!/win/.test(process.platform)) watchOpts.recursive = false + + watchers[dirPath] = fs.watch(dirPath, watchOpts, (fse, filename) => { + // console.log(`event: ${fse}, ${filename}`); + if (!filename) return + const full_path = path.join(dirPath, filename) + const args = reader._read_args[dirPath] + // console.log(args); + if (sedation_timers[full_path]) { + clearTimeout(sedation_timers[full_path]) + } + sedation_timers[full_path] = setTimeout(() => { + delete sedation_timers[full_path] + args.opts.watchCb() + }, 2 * 1000) + }) +} + +module.exports.onEvent = (reader, name, args) => { + return (fse) => { + if (sedation_timers[name]) { + clearTimeout(sedation_timers[name]) + } + + sedation_timers[name] = setTimeout(() => { + console.log(`Reloading file: ${name}`) + reader.load_config(name, args.type, args.options) + delete sedation_timers[name] + if (typeof args.cb === 'function') args.cb() + }, 5 * 1000) + + if (fse !== 'rename') return + // https://github.com/joyent/node/issues/2062 + // After a rename event, re-watch the file + watchers[name].close() + try { + watchers[name] = fs.watch( + name, + { persistent: false }, + this.onEvent(...arguments), + ) + } catch (e) { + if (e.code === 'ENOENT') { + enoent.files[name] = true + this.ensure_enoent_timer(reader) + } else { + console.error(`Error watching file: ${name} : ${e}`) + } + } + } +} \ No newline at end of file diff --git a/package.json b/package.json index 28cdf1c..a957203 100644 --- a/package.json +++ b/package.json @@ -3,35 +3,41 @@ "name": "haraka-config", "license": "MIT", "description": "Haraka's config file loader", - "version": "1.1.0", + "version": "1.2.0", "homepage": "http://haraka.github.io", "repository": { "type": "git", "url": "git@github.com:haraka/haraka-config.git" }, "main": "config.js", + "files": [ + "lib", + "CHANGELOG.md" + ], "engines": { - "node": ">= 12" + "node": ">=16" }, "dependencies": { "js-yaml": "^4.1.0" }, "optionalDependencies": { - "hjson": "^3.2.0" + "hjson": "^3.2.2" }, "devDependencies": { - "eslint": ">=8", - "eslint-plugin-haraka": "*", - "mocha": ">=9" + "@haraka/eslint-config": "^1.1.3" }, "bugs": { "mail": "haraka.mail@gmail.com", "url": "https://github.com/haraka/haraka-config/issues" }, "scripts": { - "test": "npx mocha test test/readers", - "lint": "npx eslint *.js readers test test/*/*.js", - "lintfix": "npx eslint --fix *.js readers test test/*/*.js", - "versions": "npx dependency-version-checker check" + "format": "npm run prettier:fix && npm run lint:fix", + "lint": "npx eslint@^8 *.js lib test test/*/*.js", + "lint:fix": "npx eslint@^8 *.js lib test test/*/*.js --fix", + "prettier": "npx prettier . --check", + "prettier:fix": "npx prettier . --write --log-level=warn", + "test": "npx mocha@10 test test/readers", + "versions": "npx dependency-version-checker check", + "versions:fix": "npx dependency-version-checker update && npm run prettier:fix" } } diff --git a/readers/binary.js b/readers/binary.js deleted file mode 100644 index 1c0296b..0000000 --- a/readers/binary.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -const fs = require('fs'); - -exports.load = (name) => { - return fs.readFileSync(name); -} - -exports.loadPromise = (name) => { - return new Promise((resolve, reject) => { - fs.readFile(name, (err, content) => { - if (err) return reject(err); - resolve({ path: name, data: content }); - }); - }); -} - -exports.empty = () => { - return null; -} diff --git a/readers/flat.js b/readers/flat.js deleted file mode 100644 index 2ff5c05..0000000 --- a/readers/flat.js +++ /dev/null @@ -1,69 +0,0 @@ -'use strict'; - -const fs = require('fs'); - -exports.load = (name, type, options, regex) => { - let result = []; - - let data = fs.readFileSync(name, 'UTF-8'); - if (type === 'data') { - while (data.length > 0) { - const match = data.match(/^([^\r\n]*)\r?\n?/); - result.push(match[1]); - data = data.slice(match[0].length); - } - return result; - } - - data.split(/\r\n|\r|\n/).forEach( (line) => { - if (regex.comment.test(line)) return; - if (regex.blank.test(line)) return; - - const line_data = regex.line.exec(line); - if (!line_data) return; - - result.push(line_data[1].trim()); - }) - - if (result.length && type !== 'list' && type !== 'data') { - result = result[0]; - if (options && in_array(result, options.booleans)) { - return regex.is_truth.test(result); - } - if (regex.is_integer.test(result)) { - return parseInt(result, 10); - } - if (regex.is_float.test(result)) { - return parseFloat(result); - } - return result; - } - - // Return hostname for 'me' if no result - if (/\/me$/.test(name) && !(result && result.length)) { - return [ require('os').hostname() ]; - } - - // For value types with no result - if (!(type && (type === 'list' || type === 'data'))) { - if (!(result && result.length)) return null; - } - - return result; -} - -exports.empty = (options, type) => { - switch (type) { - case 'flat': - case 'value': - return null; - default: - return []; - } -} - -function in_array (item, array) { - if (!array) return false; - if (!Array.isArray(array)) return false; - return (array.indexOf(item) !== -1); -} diff --git a/readers/hjson.js b/readers/hjson.js deleted file mode 100644 index e7e8a58..0000000 --- a/readers/hjson.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const hjson = require('hjson'); - -exports.load = (name) => { - return hjson.parse(fs.readFileSync(name, "utf8")); -} - -exports.empty = () => { - return {}; -} diff --git a/readers/ini.js b/readers/ini.js deleted file mode 100644 index eb013c9..0000000 --- a/readers/ini.js +++ /dev/null @@ -1,123 +0,0 @@ -'use strict'; - -const fs = require('fs'); - -exports.load = (name, options, regex) => { - let result = { main: {} }; - let current_sect = result.main; - let current_sect_name = 'main'; - this.bool_matches = []; - if (options && options.booleans) { - this.bool_matches = options.booleans.slice(); - } - - // Initialize any booleans - result = this.init_booleans(options, result); - - let match; - let setter; - let pre = ''; - - fs.readFileSync(name, 'UTF-8') - .split(/\r\n|\r|\n/) - .forEach( (line) => { - if (regex.comment.test(line)) return; - if (regex.blank.test(line) ) return; - - match = regex.section.exec(line); - if (match) { - if (!result[match[1]]) result[match[1]] = {}; - current_sect = result[match[1]]; - current_sect_name = match[1]; - return; - } - - if (regex.continuation.test(line)) { - pre += line.replace(regex.continuation, ''); - return; - } - - line = `${pre}${line}`; - pre = ''; - - match = regex.param.exec(line); - if (!match) { - exports.logger(`Invalid line in config file '${name}': ${line}`); - return; - } - - const is_array_match = regex.is_array.exec(match[1]); - if (is_array_match) { - setter = function (key, value) { - key = key.replace('[]', ''); - if (! current_sect[key]) current_sect[key] = []; - current_sect[key].push(value); - }; - } - else { - setter = function (key, value) { current_sect[key] = value; }; - } - - if (options && Array.isArray(options.booleans) && - ( - exports.bool_matches.indexOf(`${current_sect_name}.${match[1]}`) !== -1 - || - exports.bool_matches.indexOf(`*.${match[1]}`) !== -1 - )) { - current_sect[match[1]] = regex.is_truth.test(match[2]); - // exports.logger(`Using boolean ${current_sect[match[1]]} for ${current_sect_name}.${match[1]}=${match[2]}`, 'logdebug'); - } - else if (regex.is_integer.test(match[2])) { - setter(match[1], parseInt(match[2], 10)); - } - else if (regex.is_float.test(match[2])) { - setter(match[1], parseFloat(match[2])); - } - else { - setter(match[1], match[2]); - } - }); - - return result; -} - -exports.empty = (options) => { - this.bool_matches = []; - return this.init_booleans(options, { main: {} }); -} - -exports.init_booleans = (options, result) => { - if (!options) return result; - if (!Array.isArray(options.booleans)) return result; - - // console.log(options.booleans); - for (let i=0; i { - // if (!level) level = 'logwarn'; - console.log(msg); -} diff --git a/readers/js.js b/readers/js.js deleted file mode 100644 index c289ac6..0000000 --- a/readers/js.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -exports.load = (name) => { - return require(name); -} - -exports.empty = () => { - return {}; -} diff --git a/readers/json.js b/readers/json.js deleted file mode 100644 index a4d51c7..0000000 --- a/readers/json.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -const fs = require('fs'); - -exports.load = (name) => { - return JSON.parse(fs.readFileSync(name)); -} - -exports.empty = () => { - return {}; -} \ No newline at end of file diff --git a/readers/yaml.js b/readers/yaml.js deleted file mode 100644 index 657bf30..0000000 --- a/readers/yaml.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const yaml = require('js-yaml'); - -exports.load = (name) => { - return yaml.load(fs.readFileSync(name, 'utf8')); -} - -exports.empty = () => { - return {}; -} diff --git a/test/config.js b/test/config.js index 73fc324..41083cf 100644 --- a/test/config.js +++ b/test/config.js @@ -1,570 +1,561 @@ +const assert = require('node:assert') +// const { beforeEach, describe, it } = require('node:test') +const fs = require('node:fs') +const os = require('node:os') +const path = require('node:path') + +function cb() { + return false +} +const opts = { booleans: ['arg1'] } -const assert = require('assert') -const fs = require('fs') -const os = require('os') -const path = require('path') - -function cb () { return false; } -const opts = { booleans: ['arg1'] }; - -function clearRequireCache () { - // node_unit runs all the tests in the same process, so the process.env - // changes affect other tests. Icky. Work around by invalidating - // the require cache, so config and configfile re-initialize - delete require.cache[ - `${path.resolve(__dirname, '..','config')}.js` - ]; - delete require.cache[ - `${path.resolve(__dirname, '..','configfile')}.js` - ]; +function clearRequireCache() { + // the tests are run in the same process, so process.env changes affect + // other tests. Invalidate the require cache between tests + delete require.cache[`${path.resolve(__dirname, '..', 'config')}.js`] + delete require.cache[`${path.resolve(__dirname, '..', 'lib', 'reader')}.js`] } -function testSetup (done) { - process.env.NODE_ENV = 'test' - process.env.HARAKA = ''; - process.env.WITHOUT_CONFIG_CACHE = '1'; - clearRequireCache(); - this.config = require('../config'); - done(); +function testSetup(done) { + process.env.NODE_ENV = 'test' + process.env.HARAKA = '' + process.env.WITHOUT_CONFIG_CACHE = '1' + clearRequireCache() + this.config = require('../config') + done() } describe('config', function () { + beforeEach(testSetup) - beforeEach(testSetup) - - it('new', function (done) { - assert.equal(path.resolve('test','config'), this.config.root_path); - done(); - }) + it('new', function () { + assert.equal(path.resolve('test', 'config'), this.config.root_path) + }) - it('module_config', function (done) { - const c = this.config.module_config('foo', 'bar'); - assert.equal(c.root_path, path.join('foo','config')); - assert.equal(c.overrides_path, path.join('bar','config')); - done(); - }) + it('module_config', function () { + const c = this.config.module_config('foo', 'bar') + assert.equal(c.root_path, path.join('foo', 'config')) + assert.equal(c.overrides_path, path.join('bar', 'config')) + }) - describe('config_path', function () { - it('config_path process.env.HARAKA', function (done) { - process.env.HARAKA = '/tmp'; - clearRequireCache(); - const config = require('../config'); - // console.log(config); - assert.equal(config.root_path, path.join('/tmp','config')); - done(); - }) - - it('config_path process.env.NODE_ENV', function (done) { - process.env.HARAKA = ''; - process.env.NODE_ENV = 'not-test'; - clearRequireCache(); - const config = require('../config'); - // ./config doesn't exist so path will be resolved ./ - assert.ok(/haraka-config$/.test(config.root_path)); - process.env.NODE_ENV = 'test'; - done(); - }) + describe('config_path', function () { + it('config_path process.env.HARAKA', function () { + process.env.HARAKA = '/tmp' + clearRequireCache() + const config = require('../config') + assert.equal(config.root_path, path.join('/tmp', 'config')) }) - describe('arrange_args', function () { - beforeEach(testSetup) - - // config.get('name'); - it('name', function (done) { - assert.deepEqual( - this.config.arrange_args(['test.ini']), - ['test.ini', 'ini', undefined, undefined]); - done(); - }) - // config.get('name', type); - it('name, type', function (done) { - assert.deepEqual( - this.config.arrange_args(['test.ini','ini']), - ['test.ini', 'ini', undefined, undefined]); - done(); - }) - - // config.get('name', cb); - it('name, callback', function (done) { - assert.deepEqual( - this.config.arrange_args(['test.ini',cb]), - ['test.ini', 'ini', cb, undefined]); - done(); - }) - - // config.get('name', cb, options); - it('name, callback, options', function (done) { - assert.deepEqual( - this.config.arrange_args(['test.ini',cb,opts]), - ['test.ini', 'ini', cb, opts]); - done(); - }) - - // config.get('name', options); - it('name, options', function (done) { - assert.deepEqual( - this.config.arrange_args(['test.ini',opts]), - ['test.ini', 'ini', undefined, opts]); - done(); - }) - - // config.get('name', type, cb); - it('name, type, callback', function (done) { - assert.deepEqual( - this.config.arrange_args(['test.ini','ini',cb]), - ['test.ini', 'ini', cb, undefined]); - done(); - }) - - // config.get('name', type, options); - it('name, type, options', function (done) { - assert.deepEqual( - this.config.arrange_args(['test.ini','ini',opts]), - ['test.ini', 'ini', undefined, opts]); - done(); - }) - - // config.get('name', type, cb, options); - it('name, type, callback, options', function (done) { - assert.deepEqual( - this.config.arrange_args(['test.ini','ini',cb, opts]), - ['test.ini', 'ini', cb, opts]); - done(); - }) - - // config.get('name', list, cb, options); - it('name, list type, callback, options', function (done) { - assert.deepEqual( - this.config.arrange_args(['test.ini','list',cb, opts]), - ['test.ini', 'list', cb, opts]); - done(); - }) - - // config.get('name', binary, cb, options); - it('name, binary type, callback, options', function (done) { - assert.deepEqual( - this.config.arrange_args(['test.ini','binary',cb, opts]), - ['test.ini', 'binary', cb, opts]); - done(); - }) - - // config.get('name', type, cb, options); - it('name, value type, callback, options', function (done) { - assert.deepEqual( - this.config.arrange_args(['test.ini','value',cb, opts]), - ['test.ini', 'value', cb, opts]); - done(); - }) - - // config.get('name', type, cb, options); - it('name, hjson type, callback, options', function (done) { - assert.deepEqual( - this.config.arrange_args(['test.ini','hjson',cb, opts]), - ['test.ini', 'hjson', cb, opts]); - done(); - }) - - // config.get('name', type, cb, options); - it('name, json type, callback, options', function (done) { - assert.deepEqual( - this.config.arrange_args(['test.ini','json',cb, opts]), - ['test.ini', 'json', cb, opts]); - done(); - }) - - // config.get('name', type, cb, options); - it('name, data type, callback, options', function (done) { - assert.deepEqual( - this.config.arrange_args(['test.ini','data',cb, opts]), - ['test.ini', 'data', cb, opts]); - done(); - }) + it('config_path process.env.NODE_ENV', function () { + process.env.HARAKA = '' + process.env.NODE_ENV = 'not-test' + clearRequireCache() + const config = require('../config') + assert.ok(/haraka-config$/.test(config.root_path)) }) -}) - -const hjsonRes = { - matt: 'waz here and also made comments', - differentArray: [ 'has element #1', 'has element #2' ], - object: { - 'has a property one': 'with a value A', - 'has a property two': 'with a value B' - } -} - -const jsonRes = { - matt: 'waz here', - array: [ 'has an element' ], - objecty: { 'has a property': 'with a value' } -} - -const yamlRes = { - main: { - bool_true: true, - bool_false: false, - str_true: true, - str_false: false - }, - sect1: { - bool_true: true, - bool_false: false, - str_true: true, - str_false: false - }, - whitespace: { - str_no_trail: true, - str_trail: true - }, - matt: 'waz here', - array: ['has an element'], - objecty: { - 'has a property': 'with a value' - } -} - -function _test_get (done, name, type, callback, options, expected) { - const config = require('../config'); - const cfg = config.get(name, type, callback, options); - assert.deepEqual(cfg, expected); - done(); -} - -function _test_int (done, name, default_value, expected) { - const config = require('../config'); - const result = config.getInt(name, default_value); - if (result) { - assert.equal(typeof result, 'number'); - } - assert.deepEqual(result, expected); - done(); -} + }) -describe('get', function () { + describe('arrange_args', function () { beforeEach(testSetup) - // config.get('name'); - it('test (non-existing)', function (done) { - _test_get(done, 'test', null, null, null, null); + it('name', function () { + assert.deepEqual(this.config.arrange_args(['test.ini']), [ + 'test.ini', + 'ini', + undefined, + undefined, + ]) }) - it('test (non-existing, cached)', function (done) { - process.env.WITHOUT_CONFIG_CACHE= ''; - const cfg = this.config.get('test', null, null); - assert.deepEqual(cfg, null); - done(); + it('name, type', function () { + assert.deepEqual(this.config.arrange_args(['test.ini', 'ini']), [ + 'test.ini', + 'ini', + undefined, + undefined, + ]) }) - // config.get('test.ini'); - it('test.ini, no opts', function (done) { - _test_get(done, 'test.ini', null, null, null, { - main: { bool_true: 'true', bool_false: 'false', str_true: 'true', str_false: 'false' }, - sect1: { bool_true: 'true', bool_false: 'false', str_true: 'true', str_false: 'false' }, - whitespace: { str_no_trail: 'true', str_trail: 'true' }, - funnychars: { 'results.auth/auth_base.fail': 'fun' }, - empty_values: { first: undefined, second: undefined }, - has_ipv6: { '2605:ae00:329::2': undefined }, - array_test: { - hostlist: [ 'first_host', 'second_host', 'third_host' ], - intlist: [ '123', '456', '789' ], - }, - 'foo.com': { is_bool: 'true' }, - 'bar.com': { is_bool: 'false' }, - has_nums: { integer: 454, float: 10.5 }, - }) + it('name, callback', function () { + assert.deepEqual(this.config.arrange_args(['test.ini', cb]), [ + 'test.ini', + 'ini', + cb, + undefined, + ]) }) - it('test.ini, opts', function (done) { - _test_get(done, 'test.ini', 'ini', null, { - booleans: [ - '*.bool_true', - '*.bool_false', - ] - }, { - main: { bool_true: true, bool_false: false, str_true: 'true', str_false: 'false' }, - sect1: { bool_true: true, bool_false: false, str_true: 'true', str_false: 'false' }, - whitespace: { str_no_trail: 'true', str_trail: 'true' }, - funnychars: { 'results.auth/auth_base.fail': 'fun' }, - empty_values: { first: undefined, second: undefined }, - has_ipv6: { '2605:ae00:329::2': undefined }, - array_test: { - hostlist: [ 'first_host', 'second_host', 'third_host' ], - intlist: [ '123', '456', '789' ], - }, - 'foo.com': { is_bool: 'true' }, - 'bar.com': { is_bool: 'false' }, - has_nums: { integer: 454, float: 10.5 }, - }) + it('name, callback, options', function () { + assert.deepEqual(this.config.arrange_args(['test.ini', cb, opts]), [ + 'test.ini', + 'ini', + cb, + opts, + ]) }) - // config.get('test.txt'); - it('test.txt', function (done) { - _test_get(done, 'test.txt', null, null, null, null); + it('name, options', function () { + assert.deepEqual(this.config.arrange_args(['test.ini', opts]), [ + 'test.ini', + 'ini', + undefined, + opts, + ]) }) - // config.get('test.flat'); - it('test.flat, type=', function (done) { - _test_get(done, 'test.flat', null, null, null, 'line1'); + it('name, type, callback', function () { + assert.deepEqual(this.config.arrange_args(['test.ini', 'ini', cb]), [ + 'test.ini', + 'ini', + cb, + undefined, + ]) }) - // NOTE: the test.flat file had to be duplicated for these tests, to avoid - // the config cache from returning invalid results. - - // config.get('test.flat', 'value'); - it('test.flat, type=value', function (done) { - _test_get(done, 'test.value', 'value', null, null, 'line1'); + it('name, type, options', function () { + assert.deepEqual(this.config.arrange_args(['test.ini', 'ini', opts]), [ + 'test.ini', + 'ini', + undefined, + opts, + ]) }) - // config.get('test.flat', 'list'); - it('test.flat, type=list', function (done) { - _test_get(done, 'test.list', 'list', null, null, - ['line1', 'line2','line3', 'line5'] ); + it('name, type, callback, options', function () { + assert.deepEqual( + this.config.arrange_args(['test.ini', 'ini', cb, opts]), + ['test.ini', 'ini', cb, opts], + ) }) - // config.get('test.flat', 'data'); - it('test.flat, type=data', function (done) { - _test_get(done, 'test.data', 'data', null, null, - ['line1', 'line2','line3', '', 'line5'] ); + it('name, list type, callback, options', function () { + assert.deepEqual( + this.config.arrange_args(['test.ini', 'list', cb, opts]), + ['test.ini', 'list', cb, opts], + ) }) - // config.get('test.hjson'); - it('test.hjson, type=', function (done) { - _test_get(done, 'test.hjson', null, null, null, hjsonRes); + it('name, binary type, callback, options', function () { + assert.deepEqual( + this.config.arrange_args(['test.ini', 'binary', cb, opts]), + ['test.ini', 'binary', cb, opts], + ) }) - // config.get('test.hjson', 'hjson'); - it('test.hjson, type=hjson', function (done) { - _test_get(done, 'test.hjson', 'hjson', null, null, hjsonRes); + it('name, value type, callback, options', function () { + assert.deepEqual( + this.config.arrange_args(['test.ini', 'value', cb, opts]), + ['test.ini', 'value', cb, opts], + ) }) - // config.get('test.json'); - it('test.json, type=', function (done) { - _test_get(done, 'test.json', null, null, null, jsonRes); + it('name, hjson type, callback, options', function () { + assert.deepEqual( + this.config.arrange_args(['test.ini', 'hjson', cb, opts]), + ['test.ini', 'hjson', cb, opts], + ) }) - // config.get('test.json', 'json'); - it('test.json, type=json', function (done) { - _test_get(done, 'test.json', 'json', null, null, jsonRes); + // config.get('name', type, cb, options); + it('name, json type, callback, options', function () { + assert.deepEqual( + this.config.arrange_args(['test.ini', 'json', cb, opts]), + ['test.ini', 'json', cb, opts], + ) }) - // config.get('test.yaml'); - it('test.yaml, type=', function (done) { - _test_get(done, 'test.yaml', null, null, null, yamlRes); - }) - // config.get('test.yaml', 'yaml'); - it('test.yaml, type=yaml', function (done) { - _test_get(done, 'test.yaml', 'yaml', null, null, yamlRes); - }) - // config.get('missing2.hjson'); - it('missing2.yaml, asked for hjson', function (done) { - _test_get(done, 'missing2.hjson', 'hjson', null, null, {"matt": "waz here - hjson type"}); - }) - // config.get('missing.json'); - it('missing.yaml, asked for json', function (done) { - _test_get(done, 'missing.json', 'json', null, null, {"matt": "waz here"}); - }) - - it('test.bin, type=binary', function (done) { - const res = this.config.get('test.binary', 'binary'); - assert.equal(res.length, 120); - assert.ok(Buffer.isBuffer(res)); - done(); - }) - - it('fully qualified path: /etc/services', function (done) { - let res; - if (/^win/.test(process.platform)) { - res = this.config.get('c:\\windows\\win.ini', 'list'); - } - else { - res = this.config.get('/etc/services', 'list'); - } - assert.ok(res.length); - done(); + // config.get('name', type, cb, options); + it('name, data type, callback, options', function () { + assert.deepEqual( + this.config.arrange_args(['test.ini', 'data', cb, opts]), + ['test.ini', 'data', cb, opts], + ) }) + }) }) -describe('merged', function () { - beforeEach(testSetup) - - it('before_merge', function (done) { - const lc = this.config.module_config( - path.join('test','default') - ); - assert.deepEqual(lc.get('test.ini'), - { main: {}, defaults: { one: 'one', two: 'two' } } - ); - done(); - }) - - it('after_merge', function (done) { - const lc = this.config.module_config( - path.join('test','default'), - path.join('test','override') - ); - assert.deepEqual(lc.get('test.ini'), - { main: {}, defaults: { one: 'three', two: 'four' } } - ); - done(); - }) +const hjsonRes = { + matt: 'waz here and also made comments', + differentArray: ['has element #1', 'has element #2'], + object: { + 'has a property one': 'with a value A', + 'has a property two': 'with a value B', + }, +} - it('flat overridden', function (done) { - const lc = this.config.module_config( - path.join('test','default'), - path.join('test','override') - ); - assert.equal(lc.get('test.flat'), 'flatoverrode'); - done(); - }) -}) +const jsonRes = { + matt: 'waz here', + array: ['has an element'], + objecty: { 'has a property': 'with a value' }, +} -describe('getInt', function () { - beforeEach(testSetup) +const yamlRes = { + main: { + bool_true: true, + bool_false: false, + str_true: true, + str_false: false, + }, + sect1: { + bool_true: true, + bool_false: false, + str_true: true, + str_false: false, + }, + whitespace: { + str_no_trail: true, + str_trail: true, + }, + matt: 'waz here', + array: ['has an element'], + objecty: { + 'has a property': 'with a value', + }, +} - // config.get('name'); - it('empty filename is NaN', function (done) { - const result = this.config.getInt(); - assert.equal(typeof result, 'number'); - assert.ok(isNaN(result)); - done(); - }) +function _test_get(name, type, callback, options, expected) { + const config = require('../config') + const cfg = config.get(name, type, callback, options) + assert.deepEqual(cfg, expected) +} - it('empty/missing file contents is NaN', function (done) { - const result = this.config.getInt('test-non-exist'); - assert.equal(typeof result, 'number'); - assert.ok(isNaN(result)); - done(); - }) +function _test_int(name, default_value, expected) { + const config = require('../config') + const result = config.getInt(name, default_value) + if (result) assert.equal(typeof result, 'number') + assert.deepEqual(result, expected) +} - it('non-existing file returns default', function (done) { - _test_int(done, 'test-non-exist', 5, 5); - }) +describe('get', function () { + beforeEach(testSetup) + + // config.get('name'); + it('test (non-existing)', function () { + _test_get('test', null, null, null, null) + }) + + it('test (non-existing, cached)', function (done) { + process.env.WITHOUT_CONFIG_CACHE = '' + const cfg = this.config.get('test', null, null) + assert.deepEqual(cfg, null) + done() + }) + + it('test.ini, no opts', function () { + _test_get('test.ini', null, null, null, { + main: { + bool_true: 'true', + bool_false: 'false', + str_true: 'true', + str_false: 'false', + }, + sect1: { + bool_true: 'true', + bool_false: 'false', + str_true: 'true', + str_false: 'false', + }, + whitespace: { str_no_trail: 'true', str_trail: 'true' }, + funnychars: { 'results.auth/auth_base.fail': 'fun' }, + empty_values: { first: undefined, second: undefined }, + has_ipv6: { '2605:ae00:329::2': undefined }, + array_test: { + hostlist: ['first_host', 'second_host', 'third_host'], + intlist: ['123', '456', '789'], + }, + 'foo.com': { is_bool: 'true' }, + 'bar.com': { is_bool: 'false' }, + has_nums: { integer: 454, float: 10.5 }, + }) + }) + + it('test.ini, opts', function () { + _test_get( + 'test.ini', + 'ini', + null, + { + booleans: ['*.bool_true', '*.bool_false'], + }, + { + main: { + bool_true: true, + bool_false: false, + str_true: 'true', + str_false: 'false', + }, + sect1: { + bool_true: true, + bool_false: false, + str_true: 'true', + str_false: 'false', + }, + whitespace: { str_no_trail: 'true', str_trail: 'true' }, + funnychars: { 'results.auth/auth_base.fail': 'fun' }, + empty_values: { first: undefined, second: undefined }, + has_ipv6: { '2605:ae00:329::2': undefined }, + array_test: { + hostlist: ['first_host', 'second_host', 'third_host'], + intlist: ['123', '456', '789'], + }, + 'foo.com': { is_bool: 'true' }, + 'bar.com': { is_bool: 'false' }, + has_nums: { integer: 454, float: 10.5 }, + }, + ) + }) + + it('test.txt', function () { + _test_get('test.txt', null, null, null, null) + }) + + it('test.flat, type=', function () { + _test_get('test.flat', null, null, null, 'line1') + }) + + // NOTE: the test.flat file had to be duplicated for these tests, to avoid + // the config cache from returning invalid results. + + it('test.flat, type=value', function () { + _test_get('test.value', 'value', null, null, 'line1') + }) + + it('test.flat, type=list', function () { + _test_get('test.list', 'list', null, null, [ + 'line1', + 'line2', + 'line3', + 'line5', + ]) + }) + + it('test.flat, type=data', function () { + _test_get('test.data', 'data', null, null, [ + 'line1', + 'line2', + 'line3', + '', + 'line5', + ]) + }) + + it('test.hjson, type=', function () { + _test_get('test.hjson', null, null, null, hjsonRes) + }) + + it('test.hjson, type=hjson', function () { + _test_get('test.hjson', 'hjson', null, null, hjsonRes) + }) + + it('test.json, type=', function () { + _test_get('test.json', null, null, null, jsonRes) + }) + + it('test.json, type=json', function () { + _test_get('test.json', 'json', null, null, jsonRes) + }) + + it('test.yaml, type=', function () { + _test_get('test.yaml', null, null, null, yamlRes) + }) + + it('test.yaml, type=yaml', function () { + _test_get('test.yaml', 'yaml', null, null, yamlRes) + }) + + it('missing2.yaml, asked for hjson', function () { + _test_get('missing2.hjson', 'hjson', null, null, { + matt: 'waz here - hjson type', + }) + }) + + it('missing.yaml, asked for json', function () { + _test_get('missing.json', 'json', null, null, { matt: 'waz here' }) + }) + + it('test.bin, type=binary', function () { + const res = this.config.get('test.binary', 'binary') + assert.equal(res.length, 120) + assert.ok(Buffer.isBuffer(res)) + }) + + it('fully qualified path: /etc/services', function () { + let res + if (/^win/.test(process.platform)) { + res = this.config.get('c:\\windows\\win.ini', 'list') + } else { + res = this.config.get('/etc/services', 'list') + } + assert.ok(res.length) + }) +}) - it('test.int equals 6', function (done) { - _test_int(done, 'test.int', undefined, 6); - }) +describe('merged', function () { + beforeEach(testSetup) + + it('before_merge', function () { + const lc = this.config.module_config(path.join('test', 'default')) + assert.deepEqual(lc.get('test.ini'), { + main: {}, + defaults: { one: 'one', two: 'two' }, + }) + }) + + it('after_merge', function () { + const lc = this.config.module_config( + path.join('test', 'default'), + path.join('test', 'override'), + ) + assert.deepEqual(lc.get('test.ini'), { + main: {}, + defaults: { one: 'three', two: 'four' }, + }) + }) + + it('flat overridden', function () { + const lc = this.config.module_config( + path.join('test', 'default'), + path.join('test', 'override'), + ) + assert.equal(lc.get('test.flat'), 'flatoverrode') + }) +}) - it('test.int equals 6 (with default 7)', function (done) { - _test_int(done, 'test.int', 7, 6); - }) +describe('getInt', function () { + beforeEach(testSetup) + + // config.get('name'); + it('empty filename is NaN', function () { + const result = this.config.getInt() + assert.equal(typeof result, 'number') + assert.ok(isNaN(result)) + }) + + it('empty/missing file contents is NaN', function () { + const result = this.config.getInt('test-non-exist') + assert.equal(typeof result, 'number') + assert.ok(isNaN(result)) + }) + + it('non-existing file returns default', function () { + _test_int('test-non-exist', 5, 5) + }) + + it('test.int equals 6', function () { + _test_int('test.int', undefined, 6) + }) + + it('test.int equals 6 (with default 7)', function () { + _test_int('test.int', 7, 6) + }) }) -const tmpFile = path.resolve('test', 'config', 'dir', '4.ext'); +const tmpFile = path.resolve('test', 'config', 'dir', '4.ext') -function cleanup (done) { - fs.unlink(tmpFile, () => { - done(); - }) +function cleanup(done) { + fs.unlink(tmpFile, () => { + done() + }) } describe('getDir', function () { - beforeEach(function (done) { - process.env.HARAKA = ''; - clearRequireCache(); - this.config = require('../config'); - cleanup(done); - }) - - it('loads all files in dir', function (done) { - this.config.getDir('dir', { type: 'binary' }, (err, files) => { - assert.ifError(err); - // console.log(files); - assert.equal(err, null); - assert.equal(files.length, 3); - assert.equal(files[0].data, `contents1${os.EOL}`); - assert.equal(files[2].data, `contents3${os.EOL}`); - done(); - }) - }) - - it('errs on invalid dir', function (done) { - this.config.getDir('dirInvalid', { type: 'binary' }, (err, files) => { - // console.log(arguments); - assert.equal(err.code, 'ENOENT'); - done(); - }) - }) + beforeEach(function (done) { + process.env.NODE_ENV = 'test' + process.env.HARAKA = '' + process.env.WITHOUT_CONFIG_CACHE = '1' + clearRequireCache() + this.config = require('../config') + cleanup(done) + }) + + it('loads all files in dir', function (done) { + this.config.getDir('dir', { type: 'binary' }, (err, files) => { + if (err) console.error(err) + assert.ifError(err) + assert.equal(err, null) + assert.equal(files.length, 3) + assert.equal(files[0], `contents1${os.EOL}`) + assert.equal(files[2], `contents3${os.EOL}`) + done() + }) + }) + + it('errs on invalid dir', function (done) { + this.config.getDir('dirInvalid', { type: 'binary' }, (err) => { + assert.equal(err.code, 'ENOENT') + done() + }) + }) + + it('reloads when file in dir is touched', function (done) { + this.timeout(3500) + if (/darwin/.test(process.platform)) { + // due to differences in fs.watch, this test is not reliable on Mac OS X + done() + return + } - it('reloads when file in dir is touched', function (done) { - this.timeout(3500); - if (/darwin/.test(process.platform)) { - // due to differences in fs.watch, this test is not reliable on Mac OS X - done(); - return; + const self = this + let callCount = 0 + + function getDir() { + const opts2 = { type: 'binary', watchCb: getDir } + self.config.getDir('dir', opts2, (err, files) => { + // console.log('Loading: test/config/dir'); + if (err) console.error(err) + callCount++ + if (callCount === 1) { + // console.log(files); + assert.equal(err, null) + assert.equal(files.length, 3) + assert.equal(files[0], `contents1${os.EOL}`) + assert.equal(files[2], `contents3${os.EOL}`) + fs.writeFile(tmpFile, 'contents4\n', (err2) => { + assert.equal(err2, null) + // console.log('file touched, waiting for callback'); + }) } - const self = this; - let callCount = 0; - - function getDir () { - const opts2 = { type: 'binary', watchCb: getDir }; - self.config.getDir('dir', opts2, (err, files) => { - // console.log('Loading: test/config/dir'); - if (err) console.error(err); - callCount++; - if (callCount === 1) { - // console.log(files); - assert.equal(err, null); - assert.equal(files.length, 3); - assert.equal(files[0].data, `contents1${os.EOL}`); - assert.equal(files[2].data, `contents3${os.EOL}`); - fs.writeFile(tmpFile, 'contents4\n', (err2, res) => { - assert.equal(err2, null); - // console.log('file touched, waiting for callback'); - // console.log(res); - }); - } - if (callCount === 2) { - assert.equal(files[3].data, 'contents4\n'); - done(); - } - }); + if (callCount === 2) { + assert.equal(files[3], 'contents4\n') + fs.unlink(tmpFile, () => {}) + done() } - getDir(); - }) + }) + } + getDir() + }) }) describe('hjsonOverrides', function () { - beforeEach(testSetup) - - it('no override for smtpgreeting', function (done) { - // console.log(this.config); - assert.deepEqual( - this.config.get('smtpgreeting', 'list'), - [] - ); - done(); - }) - - it('with smtpgreeting override', function (done) { - process.env.WITHOUT_CONFIG_CACHE=''; - const main = this.config.get('main.hjson'); - console.log(main); - assert.deepEqual( - this.config.get('smtpgreeting', 'list'), - [ 'this is line one for hjson', 'this is line two for hjson' ] - ); - done(); - }) + beforeEach(testSetup) + + it('no override for smtpgreeting', function () { + assert.deepEqual(this.config.get('smtpgreeting', 'list'), []) + }) + + it('with smtpgreeting override', function () { + process.env.WITHOUT_CONFIG_CACHE = '' + this.config.get('main.hjson') + assert.deepEqual(this.config.get('smtpgreeting', 'list'), [ + 'this is line one for hjson', + 'this is line two for hjson', + ]) + }) }) describe('jsonOverrides', function () { - beforeEach(testSetup) - - it('no override for smtpgreeting', function (done) { - // console.log(this.config); - assert.deepEqual( - this.config.get('smtpgreeting', 'list'), - [] - ); - done(); - }) - - it('with smtpgreeting override', function (done) { - process.env.WITHOUT_CONFIG_CACHE=''; - const main = this.config.get('main.json'); - console.log(main); - assert.deepEqual( - this.config.get('smtpgreeting', 'list'), - [ 'this is line one', 'this is line two' ] - ); - done(); - }) + beforeEach(testSetup) + + it('no override for smtpgreeting', function () { + assert.deepEqual(this.config.get('smtpgreeting', 'list'), []) + }) + + it('with smtpgreeting override', function () { + process.env.WITHOUT_CONFIG_CACHE = '' + this.config.get('main.json') + assert.deepEqual(this.config.get('smtpgreeting', 'list'), [ + 'this is line one', + 'this is line two', + ]) + }) }) diff --git a/test/config/main.json b/test/config/main.json index c990f72..c787dee 100644 --- a/test/config/main.json +++ b/test/config/main.json @@ -1,3 +1,3 @@ { - "!smtpgreeting": [ "this is line one", "this is line two" ] -} \ No newline at end of file + "!smtpgreeting": ["this is line one", "this is line two"] +} diff --git a/test/config/missing.yaml b/test/config/missing.yaml index d5392a0..ab2d2dd 100644 --- a/test/config/missing.yaml +++ b/test/config/missing.yaml @@ -1,3 +1,2 @@ --- -matt: "waz here" -... \ No newline at end of file +matt: 'waz here' diff --git a/test/config/missing2.yaml b/test/config/missing2.yaml index a088f48..a5044cf 100644 --- a/test/config/missing2.yaml +++ b/test/config/missing2.yaml @@ -1,3 +1,2 @@ --- -matt: "waz here - hjson type" -... +matt: 'waz here - hjson type' diff --git a/test/config/mixed/1.ini b/test/config/mixed/1.ini new file mode 100644 index 0000000..a71a2af --- /dev/null +++ b/test/config/mixed/1.ini @@ -0,0 +1,2 @@ +[sect] +one=true diff --git a/test/config/mixed/2.yml b/test/config/mixed/2.yml new file mode 100644 index 0000000..40272c9 --- /dev/null +++ b/test/config/mixed/2.yml @@ -0,0 +1,2 @@ +main: + two: false diff --git a/test/config/override.yaml b/test/config/override.yaml index 50f4ef3..63163fa 100644 --- a/test/config/override.yaml +++ b/test/config/override.yaml @@ -1,2 +1,2 @@ has: - value: true \ No newline at end of file + value: true diff --git a/test/config/test.js b/test/config/test.js index 620db27..d86fc9a 100644 --- a/test/config/test.js +++ b/test/config/test.js @@ -1,9 +1,7 @@ module.exports = { - matthrew: "waz here", - marray: [ - "has an element" - ], - objective: { - "has a property": "with a value" - } + matthrew: 'waz here', + marray: ['has an element'], + objective: { + 'has a property': 'with a value', + }, } diff --git a/test/config/test.json b/test/config/test.json index e8e5edc..2526ff7 100644 --- a/test/config/test.json +++ b/test/config/test.json @@ -1,9 +1,7 @@ { - "matt": "waz here", - "array": [ - "has an element" - ], - "objecty": { - "has a property": "with a value" - } -} \ No newline at end of file + "matt": "waz here", + "array": ["has an element"], + "objecty": { + "has a property": "with a value" + } +} diff --git a/test/config/test.yaml b/test/config/test.yaml index e7da16d..d9a5f0a 100644 --- a/test/config/test.yaml +++ b/test/config/test.yaml @@ -1,23 +1,22 @@ --- main: - bool_true: true - bool_false: false - str_true: true - str_false: false + bool_true: true + bool_false: false + str_true: true + str_false: false sect1: - bool_true: true - bool_false: false - str_true: true - str_false: false + bool_true: true + bool_false: false + str_true: true + str_false: false whitespace: - str_no_trail: true - str_trail: true + str_no_trail: true + str_trail: true -matt: "waz here" +matt: 'waz here' array: - - "has an element" + - 'has an element' objecty: - "has a property": "with a value" -... \ No newline at end of file + 'has a property': 'with a value' diff --git a/test/configfile.js b/test/configfile.js index 1b648b2..fd7e4dd 100644 --- a/test/configfile.js +++ b/test/configfile.js @@ -1,445 +1,362 @@ -'use strict'; - -const assert = require('assert') -// const path = require('path'); - -function _setUp (done) { - process.env.NODE_ENV === 'test'; - this.cfreader = require('../configfile'); - this.opts = { booleans: ['main.bool_true','main.bool_false'] }; - done(); -} - -describe('configfile', function () { - beforeEach(_setUp) - - describe('load_config', function () { - - describe('non-exist.ini', function () { - - it('empty', function (done) { - assert.deepEqual( - this.cfreader.load_config('non-exist.ini','ini'), - { main: { } } - ); - done() - }) - - it('boolean', function (done) { - assert.deepEqual( - this.cfreader.load_config('non-exist.ini', 'ini', - { booleans: ['reject']}), - { main: { reject: false } } - ); - done() - }) - - it('boolean true default', function (done) { - assert.deepEqual( - this.cfreader.load_config('non-exist.ini', 'ini', - { booleans: ['+reject']}), - { main: { reject: true } } - ); - assert.deepEqual( - this.cfreader.load_config('non-exist.ini', 'ini', - { booleans: ['+main.reject']}), - { main: { reject: true } } - ); - assert.deepEqual( - this.cfreader.load_config('non-exist.ini', 'ini', - { booleans: ['main.+reject']}), - { main: { reject: true } } - ); - done() - }) - - it('boolean false default', function (done) { - assert.deepEqual( - this.cfreader.load_config('non-exist.ini', 'ini', - { booleans: ['-reject']}), - { main: { reject: false } } - ); - assert.deepEqual( - this.cfreader.load_config('non-exist.ini', 'ini', - { booleans: ['-main.reject']}), - { main: { reject: false } } - ); - assert.deepEqual( - this.cfreader.load_config('non-exist.ini', 'ini', - { booleans: ['main.-reject']}), - { main: { reject: false } } - ); - done() - }) - - it('boolean false default, section', function (done) { - assert.deepEqual( - this.cfreader.load_config('non-exist.ini', 'ini', - { booleans: ['-reject.boolf']}), - { main: { }, reject: {boolf: false} } - ); - assert.deepEqual( - this.cfreader.load_config('non-exist.ini', 'ini', - { booleans: ['+reject.boolt']}), - { main: { }, reject: {boolt: true} } - ); - done() - }) - }) - - describe('test.ini', function () { - - it('no opts', function (done) { - const r = this.cfreader.load_config('test/config/test.ini','ini'); - assert.strictEqual(r.main.bool_true, 'true'); - assert.strictEqual(r.main.bool_false, 'false'); - assert.strictEqual(r.main.str_true, 'true'); - assert.strictEqual(r.main.str_false, 'false'); - done() - }) - - it('opts', function (done) { - const r = this.cfreader.load_config('test/config/test.ini', 'ini', this.opts); - assert.strictEqual(r.main.bool_true, true); - assert.strictEqual(r.main.bool_false, false); - assert.strictEqual(r.main.str_true, 'true'); - assert.strictEqual(r.main.str_false, 'false'); - done() - }) - - it('sect1, opts', function (done) { - const r = this.cfreader.load_config('test/config/test.ini', 'ini', { - booleans: ['sect1.bool_true','sect1.bool_false'] - }); - assert.strictEqual(r.sect1.bool_true, true); - assert.strictEqual(r.sect1.bool_false, false); - assert.strictEqual(r.sect1.str_true, 'true'); - assert.strictEqual(r.sect1.str_false, 'false'); - done() - }) - - it('sect1, opts, w/defaults', function (done) { - const r = this.cfreader.load_config('test/config/test.ini', 'ini', { - booleans: [ - '+sect1.bool_true','-sect1.bool_false', - '+sect1.bool_true_default', 'sect1.-bool_false_default' - ] - }); - assert.strictEqual(r.sect1.bool_true, true); - assert.strictEqual(r.sect1.bool_false, false); - assert.strictEqual(r.sect1.str_true, 'true'); - assert.strictEqual(r.sect1.str_false, 'false'); - assert.strictEqual(r.sect1.bool_true_default, true); - assert.strictEqual(r.sect1.bool_false_default, false); - done() - }) - - it('funnychars, /', function (done) { - const r = this.cfreader.load_config('test/config/test.ini'); - assert.strictEqual(r.funnychars['results.auth/auth_base.fail'], 'fun'); - done() - }) - - it('funnychars, _', function (done) { - const r = this.cfreader.load_config('test/config/test.ini'); - assert.strictEqual(r.funnychars['results.auth/auth_base.fail'], 'fun'); - done() - }) - - it('ipv6 addr, :', function (done) { - const r = this.cfreader.load_config('test/config/test.ini'); - assert.ok( '2605:ae00:329::2' in r.has_ipv6 ); - done() - }) - - it('empty value', function (done) { - const r = this.cfreader.load_config('test/config/test.ini'); - assert.deepEqual({ first: undefined, second: undefined}, r.empty_values); - done() - }) - - it('array', function (done) { - const r = this.cfreader.load_config('test/config/test.ini'); - assert.deepEqual(['first_host', 'second_host', 'third_host'], r.array_test.hostlist); - assert.deepEqual([123, 456, 789], r.array_test.intlist); - done() - }) - }) +'use strict' + +const assert = require('node:assert') +const path = require('node:path') + +describe('reader', function () { + beforeEach(function (done) { + process.env.NODE_ENV === 'test' + this.cfreader = require('../lib/reader') + this.opts = { booleans: ['main.bool_true', 'main.bool_false'] } + done() + }) + + describe('load_config', function () { + describe('non-exist.ini', function () { + it('empty', function () { + assert.deepEqual(this.cfreader.load_config('non-exist.ini', 'ini'), { + main: {}, + }) + }) + + it('boolean', function () { + assert.deepEqual( + this.cfreader.load_config('non-exist.ini', 'ini', { + booleans: ['reject'], + }), + { main: { reject: false } }, + ) + }) + + it('boolean true default', function () { + assert.deepEqual( + this.cfreader.load_config('non-exist.ini', 'ini', { + booleans: ['+reject'], + }), + { main: { reject: true } }, + ) + assert.deepEqual( + this.cfreader.load_config('non-exist.ini', 'ini', { + booleans: ['+main.reject'], + }), + { main: { reject: true } }, + ) + assert.deepEqual( + this.cfreader.load_config('non-exist.ini', 'ini', { + booleans: ['main.+reject'], + }), + { main: { reject: true } }, + ) + }) + + it('boolean false default', function () { + assert.deepEqual( + this.cfreader.load_config('non-exist.ini', 'ini', { + booleans: ['-reject'], + }), + { main: { reject: false } }, + ) + assert.deepEqual( + this.cfreader.load_config('non-exist.ini', 'ini', { + booleans: ['-main.reject'], + }), + { main: { reject: false } }, + ) + assert.deepEqual( + this.cfreader.load_config('non-exist.ini', 'ini', { + booleans: ['main.-reject'], + }), + { main: { reject: false } }, + ) + }) + + it('boolean false default, section', function () { + assert.deepEqual( + this.cfreader.load_config('non-exist.ini', 'ini', { + booleans: ['-reject.boolf'], + }), + { main: {}, reject: { boolf: false } }, + ) + assert.deepEqual( + this.cfreader.load_config('non-exist.ini', 'ini', { + booleans: ['+reject.boolt'], + }), + { main: {}, reject: { boolt: true } }, + ) + }) }) - describe('get_filetype_reader', function () { - - it('binary', function (done) { - const reader = this.cfreader.get_filetype_reader('binary'); - assert.equal(typeof reader.load, 'function'); - assert.equal(typeof reader.empty, 'function'); - done() - }) - - it('flat', function (done) { - const reader = this.cfreader.get_filetype_reader('flat'); - assert.equal(typeof reader.load, 'function'); - assert.equal(typeof reader.empty, 'function'); - done() - }) - - it('hjson', function (done) { - const reader = this.cfreader.get_filetype_reader('hjson'); - assert.equal(typeof reader.load, 'function'); - assert.equal(typeof reader.empty, 'function'); - done() - }) - - it('json', function (done) { - const reader = this.cfreader.get_filetype_reader('json'); - assert.equal(typeof reader.load, 'function'); - assert.equal(typeof reader.empty, 'function'); - done() - }) - - it('ini', function (done) { - const reader = this.cfreader.get_filetype_reader('ini'); - assert.equal(typeof reader.load, 'function'); - assert.equal(typeof reader.empty, 'function'); - done() - }) - - it('yaml', function (done) { - const reader = this.cfreader.get_filetype_reader('yaml'); - assert.equal(typeof reader.load, 'function'); - assert.equal(typeof reader.empty, 'function'); - done() - }) - - it('value', function (done) { - const reader = this.cfreader.get_filetype_reader('value'); - assert.equal(typeof reader.load, 'function'); - assert.equal(typeof reader.empty, 'function'); - done() - }) - - it('list', function (done) { - const reader = this.cfreader.get_filetype_reader('list'); - assert.equal(typeof reader.load, 'function'); - assert.equal(typeof reader.empty, 'function'); - done() - }) - - it('data', function (done) { - const reader = this.cfreader.get_filetype_reader('data'); - assert.equal(typeof reader.load, 'function'); - assert.equal(typeof reader.empty, 'function'); - done() - }) + describe('test.ini', function () { + it('no opts', function () { + const r = this.cfreader.load_config('test/config/test.ini', 'ini') + assert.strictEqual(r.main.bool_true, 'true') + assert.strictEqual(r.main.bool_false, 'false') + assert.strictEqual(r.main.str_true, 'true') + assert.strictEqual(r.main.str_false, 'false') + }) + + it('opts', function () { + const r = this.cfreader.load_config( + 'test/config/test.ini', + 'ini', + this.opts, + ) + assert.strictEqual(r.main.bool_true, true) + assert.strictEqual(r.main.bool_false, false) + assert.strictEqual(r.main.str_true, 'true') + assert.strictEqual(r.main.str_false, 'false') + }) + + it('sect1, opts', function () { + const r = this.cfreader.load_config('test/config/test.ini', 'ini', { + booleans: ['sect1.bool_true', 'sect1.bool_false'], + }) + assert.strictEqual(r.sect1.bool_true, true) + assert.strictEqual(r.sect1.bool_false, false) + assert.strictEqual(r.sect1.str_true, 'true') + assert.strictEqual(r.sect1.str_false, 'false') + }) + + it('sect1, opts, w/defaults', function () { + const r = this.cfreader.load_config('test/config/test.ini', 'ini', { + booleans: [ + '+sect1.bool_true', + '-sect1.bool_false', + '+sect1.bool_true_default', + 'sect1.-bool_false_default', + ], + }) + assert.strictEqual(r.sect1.bool_true, true) + assert.strictEqual(r.sect1.bool_false, false) + assert.strictEqual(r.sect1.str_true, 'true') + assert.strictEqual(r.sect1.str_false, 'false') + assert.strictEqual(r.sect1.bool_true_default, true) + assert.strictEqual(r.sect1.bool_false_default, false) + }) + + it('funnychars, /', function () { + const r = this.cfreader.load_config('test/config/test.ini') + assert.strictEqual(r.funnychars['results.auth/auth_base.fail'], 'fun') + }) + + it('funnychars, _', function () { + const r = this.cfreader.load_config('test/config/test.ini') + assert.strictEqual(r.funnychars['results.auth/auth_base.fail'], 'fun') + }) + + it('ipv6 addr, :', function () { + const r = this.cfreader.load_config('test/config/test.ini') + assert.ok('2605:ae00:329::2' in r.has_ipv6) + }) + + it('empty value', function () { + const r = this.cfreader.load_config('test/config/test.ini') + assert.deepEqual( + { first: undefined, second: undefined }, + r.empty_values, + ) + }) + + it('array', function () { + const r = this.cfreader.load_config('test/config/test.ini') + assert.deepEqual( + ['first_host', 'second_host', 'third_host'], + r.array_test.hostlist, + ) + assert.deepEqual([123, 456, 789], r.array_test.intlist) + }) + }) + }) + + describe('read_dir', function () { + it.skip('returns dir contents', async function () { + // may have race collission with config.getDir test + const result = await this.cfreader.read_dir( + path.resolve('test/config/dir'), + ) + assert.deepEqual(result, ['contents1', 'contents2', 'contents3']) }) - describe('empty', function () { - it('empty object for HJSON files', function (done) { - const result = this.cfreader.load_config( - 'test/config/non-existent.hjson' - ); - assert.deepEqual(result, {}); - done() - }) - - it('empty object for JSON files', function (done) { - const result = this.cfreader.load_config( - 'test/config/non-existent.json' - ); - assert.deepEqual(result, {}); - done() - }) - - it('empty object for YAML files', function (done) { - const result = this.cfreader.load_config( - 'test/config/non-existent.yaml' - ); - assert.deepEqual(result, {}); - done() - }) - - it('null for binary file', function (done) { - const result = this.cfreader.load_config( - 'test/config/non-existent.bin', - 'binary' - ); - assert.equal(result, null); - done() - }) + it('returns dir with mixed types', async function () { + const result = await this.cfreader.read_dir('test/config/mixed') + assert.deepEqual(result, [ + { main: {}, sect: { one: 'true' } }, + { main: { two: false } }, + ]) + }) + }) - it('null for flat file', function (done) { - const result = this.cfreader.load_config( - 'test/config/non-existent.flat' - ); - assert.deepEqual(result, null); - done() - }) + describe('get_filetype_reader', function () { + it('binary', function () { + const reader = this.cfreader.get_filetype_reader('binary') + assert.equal(typeof reader.load, 'function') + assert.equal(typeof reader.empty, 'function') + }) - it('null for value file', function (done) { - const result = this.cfreader.load_config( - 'test/config/non-existent.value' - ); - assert.deepEqual(result, null); - done() - }) + it('flat', function () { + const reader = this.cfreader.get_filetype_reader('flat') + assert.equal(typeof reader.load, 'function') + assert.equal(typeof reader.empty, 'function') + }) - it('empty array for list file', function (done) { - const result = this.cfreader.load_config( - 'test/config/non-existent.list' - ); - assert.deepEqual(result, []); - done() - }) + it('hjson', function () { + const reader = this.cfreader.get_filetype_reader('hjson') + assert.equal(typeof reader.load, 'function') + assert.equal(typeof reader.empty, 'function') + }) - it('template ini for INI file', function (done) { - const result = this.cfreader.load_config( - 'test/config/non-existent.ini' - ); - assert.deepEqual(result, { main: {} }); - done() - }) + it('json', function () { + const reader = this.cfreader.get_filetype_reader('json') + assert.equal(typeof reader.load, 'function') + assert.equal(typeof reader.empty, 'function') }) - describe('get_cache_key', function () { - it('no options is the name', function (done) { - assert.equal(this.cfreader.get_cache_key('test'), - 'test'); - done() - }) + it('ini', function () { + const reader = this.cfreader.get_filetype_reader('ini') + assert.equal(typeof reader.load, 'function') + assert.equal(typeof reader.empty, 'function') + }) - it('one option is name + serialized opts', function (done) { - assert.equal(this.cfreader.get_cache_key('test', {foo: 'bar'}), - 'test{"foo":"bar"}'); - done() - }) + it('yaml', function () { + const reader = this.cfreader.get_filetype_reader('yaml') + assert.equal(typeof reader.load, 'function') + assert.equal(typeof reader.empty, 'function') + }) - it('two options are returned predictably', function (done) { - assert.equal( - this.cfreader.get_cache_key('test', {opt1: 'foo', opt2: 'bar'}), - 'test{"opt1":"foo","opt2":"bar"}'); - done() - }) + it('value', function () { + const reader = this.cfreader.get_filetype_reader('value') + assert.equal(typeof reader.load, 'function') + assert.equal(typeof reader.empty, 'function') }) - describe('regex', function () { - it('section', function (done) { - assert.equal(this.cfreader.regex.section.test('[foo]'), true); - assert.equal(this.cfreader.regex.section.test('bar'), false); - assert.equal(this.cfreader.regex.section.test('[bar'), false); - assert.equal(this.cfreader.regex.section.test('bar]'), false); - done() - }) + it('list', function () { + const reader = this.cfreader.get_filetype_reader('list') + assert.equal(typeof reader.load, 'function') + assert.equal(typeof reader.empty, 'function') + }) - it('param', function (done) { - assert.equal(this.cfreader.regex.param.exec('foo=true')[1], 'foo'); - assert.equal(this.cfreader.regex.param.exec(';foo=true'), undefined); - done() - }) + it('data', function () { + const reader = this.cfreader.get_filetype_reader('data') + assert.equal(typeof reader.load, 'function') + assert.equal(typeof reader.empty, 'function') + }) + }) - it('comment', function (done) { - assert.equal(this.cfreader.regex.comment.test('; true'), true); - assert.equal(this.cfreader.regex.comment.test('false'), false); - done() - }) + describe('empty', function () { + it('empty object for HJSON files', function () { + const result = this.cfreader.load_config('test/config/non-existent.hjson') + assert.deepEqual(result, {}) + }) - it('line', function (done) { - assert.equal(this.cfreader.regex.line.test(' boo '), true); - assert.equal(this.cfreader.regex.line.test('foo'), true); - done() - }) + it('empty object for JSON files', function () { + const result = this.cfreader.load_config('test/config/non-existent.json') + assert.deepEqual(result, {}) + }) - it('blank', function (done) { - assert.equal(this.cfreader.regex.blank.test('foo'), false); - assert.equal(this.cfreader.regex.blank.test(' '), true); - done() - }) + it('empty object for YAML files', function () { + const result = this.cfreader.load_config('test/config/non-existent.yaml') + assert.deepEqual(result, {}) + }) - // 'continuation', function (done) { - // done() - // }) + it('null for binary file', function () { + const result = this.cfreader.load_config( + 'test/config/non-existent.bin', + 'binary', + ) + assert.equal(result, null) + }) - it('is_integer', function (done) { - assert.equal(this.cfreader.regex.is_integer.test(1), true); - assert.equal(this.cfreader.regex.is_integer.test(''), false); - assert.equal(this.cfreader.regex.is_integer.test('a'), false); - done() - }) + it('null for flat file', function () { + const result = this.cfreader.load_config('test/config/non-existent.flat') + assert.deepEqual(result, null) + }) - it('is_float', function (done) { - assert.equal(this.cfreader.regex.is_float.test('1.0'), true); - assert.equal(this.cfreader.regex.is_float.test(''), false); - assert.equal(this.cfreader.regex.is_float.test('45'), false); - done() - }) + it('null for value file', function () { + const result = this.cfreader.load_config('test/config/non-existent.value') + assert.deepEqual(result, null) + }) - it('is_truth', function (done) { - assert.equal(this.cfreader.regex.is_truth.test('no'), false); - assert.equal(this.cfreader.regex.is_truth.test('nope'), false); - assert.equal(this.cfreader.regex.is_truth.test('nuh uh'), false); - assert.equal(this.cfreader.regex.is_truth.test('yes'), true); - assert.equal(this.cfreader.regex.is_truth.test('true'), true); - assert.equal(this.cfreader.regex.is_truth.test(true), true); - done() - }) + it('empty array for list file', function () { + const result = this.cfreader.load_config('test/config/non-existent.list') + assert.deepEqual(result, []) + }) - it('is_array', function (done) { - assert.equal(this.cfreader.regex.is_array.test('foo=bar'), false); - assert.equal(this.cfreader.regex.is_array.test('foo'), false); - assert.equal(this.cfreader.regex.is_array.test('foo[]'), true); - done() - }) + it('template ini for INI file', function () { + const result = this.cfreader.load_config('test/config/non-existent.ini') + assert.deepEqual(result, { main: {} }) }) + }) - describe('bad_config', function () { - it('bad.yaml returns empty', function (done) { - assert.deepEqual( - this.cfreader.load_config('test/config/bad.yaml'), - {} - ); - done() - }) + describe('get_cache_key', function () { + it('no options is the name', function () { + assert.equal(this.cfreader.get_cache_key('test'), 'test') }) - describe('overrides', function () { + it('one option is name + serialized opts', function () { + assert.equal( + this.cfreader.get_cache_key('test', { foo: 'bar' }), + 'test{"foo":"bar"}', + ) + }) - it('missing hjson loads yaml instead', function (done) { - assert.deepEqual( - this.cfreader.load_config('test/config/override2.hjson'), - { hasDifferent: { value: false } }); - done() - }) + it('two options are returned predictably', function () { + assert.equal( + this.cfreader.get_cache_key('test', { opt1: 'foo', opt2: 'bar' }), + 'test{"opt1":"foo","opt2":"bar"}', + ) + }) + }) - it('missing json loads yaml instead', function (done) { - assert.deepEqual( - this.cfreader.load_config('test/config/override.json'), - { has: { value: true } }); - done() - }) + describe('bad_config', function () { + it('bad.yaml returns empty', function () { + assert.deepEqual(this.cfreader.load_config('test/config/bad.yaml'), {}) + }) + }) + + describe('overrides', function () { + it('missing hjson loads yaml instead', function () { + assert.deepEqual( + this.cfreader.load_config('test/config/override2.hjson'), + { hasDifferent: { value: false } }, + ) }) - describe('get_path_to_config_dir', function () { - it('Haraka runtime (env.HARAKA=*)', function (done) { - process.env.HARAKA = '/etc/'; - this.cfreader.get_path_to_config_dir(); - assert.ok(/etc.config$/.test(this.cfreader.config_path), this.cfreader.config_path); - delete process.env.HARAKA; - done() - }) + it('missing json loads yaml instead', function () { + assert.deepEqual(this.cfreader.load_config('test/config/override.json'), { + has: { value: true }, + }) + }) + }) + + describe('get_path_to_config_dir', function () { + it('Haraka runtime (env.HARAKA=*)', function () { + process.env.HARAKA = '/etc/' + this.cfreader.get_path_to_config_dir() + assert.ok( + /etc.config$/.test(this.cfreader.config_path), + this.cfreader.config_path, + ) + delete process.env.HARAKA + }) - it('NODE_ENV=test', function (done) { - process.env.NODE_ENV = 'test'; - this.cfreader.get_path_to_config_dir(); - assert.ok(/haraka-config.test.config$/.test(this.cfreader.config_path), this.cfreader.config_path); - delete process.env.NODE_ENV; - done() - }) + it('NODE_ENV=test', function () { + process.env.NODE_ENV = 'test' + this.cfreader.get_path_to_config_dir() + assert.ok( + /haraka-config.test.config$/.test(this.cfreader.config_path), + this.cfreader.config_path, + ) + delete process.env.NODE_ENV + }) - it('no $ENV defaults to ./config (if present) or ./', function (done) { - delete process.env.HARAKA; - delete process.env.NODE_ENV; - this.cfreader.get_path_to_config_dir(); - assert.ok(/haraka-config$/.test(this.cfreader.config_path), this.cfreader.config_path); - done() - }) + it('no $ENV defaults to ./config (if present) or ./', function () { + delete process.env.HARAKA + delete process.env.NODE_ENV + this.cfreader.get_path_to_config_dir() + assert.ok( + /haraka-config$/.test(this.cfreader.config_path), + this.cfreader.config_path, + ) }) + }) }) diff --git a/test/readers/binary.js b/test/readers/binary.js index 9f4d8db..aff676e 100644 --- a/test/readers/binary.js +++ b/test/readers/binary.js @@ -1,30 +1,26 @@ - const assert = require('assert') -const fs = require('fs') -const path = require('path') +const fs = require('fs') +const path = require('path') beforeEach(function (done) { - this.bin = require('../../readers/binary') - done() + this.bin = require('../../lib/readers/binary') + done() }) describe('binary', function () { - it('module is required', function (done) { - assert.ok(this.bin) - done() - }) + it('module is required', function () { + assert.ok(this.bin) + }) - it('has a load function', function (done) { - assert.ok(typeof this.bin.load === 'function') - done() - }) + it('has a load function', function () { + assert.ok(typeof this.bin.load === 'function') + }) - it('loads the test binary file', function (done) { - const testBin = path.join('test','config','test.binary') - const result = this.bin.load(testBin) - assert.ok(Buffer.isBuffer(result)) - assert.equal(result.length, 120) - assert.deepEqual(result, fs.readFileSync(testBin)) - done() - }) -}) \ No newline at end of file + it('loads the test binary file', function () { + const testBin = path.join('test', 'config', 'test.binary') + const result = this.bin.load(testBin) + assert.ok(Buffer.isBuffer(result)) + assert.equal(result.length, 120) + assert.deepEqual(result, fs.readFileSync(testBin)) + }) +}) diff --git a/test/readers/flat.js b/test/readers/flat.js index 384dd3f..78f8d2f 100644 --- a/test/readers/flat.js +++ b/test/readers/flat.js @@ -1,41 +1,31 @@ - const assert = require('assert') -const regex = require('../../configfile').regex - beforeEach(function (done) { - this.flat = require('../../readers/flat') - done() + this.flat = require('../../lib/readers/flat') + done() }) describe('flat', function () { - - it('module is required', function (done) { - assert.ok(this.flat); - done(); - }) - - it('has a load function', function (done) { - assert.ok(typeof this.flat.load === 'function'); - done(); - }) - - it('loads the test flat file, as list', function (done) { - const result = this.flat.load('test/config/test.flat', 'list', null, regex); - assert.deepEqual(result, [ 'line1', 'line2', 'line3', 'line5' ]); - done(); - }) - - it('loads the test flat file, unspecified type', function (done) { - const result = this.flat.load('test/config/test.flat', null, null, regex); - assert.deepEqual(result, 'line1'); - done(); - }) - - it('returns hostname for empty "me"', function (done) { - const result = this.flat.load( 'test/config/me', null, null, regex); - console.log(result); - assert.ok(result); - done(); - }) + it('module is required', function () { + assert.ok(this.flat) + }) + + it('has a load function', function () { + assert.ok(typeof this.flat.load === 'function') + }) + + it('loads the test flat file, as list', function () { + const result = this.flat.load('test/config/test.flat', 'list', null) + assert.deepEqual(result, ['line1', 'line2', 'line3', 'line5']) + }) + + it('loads the test flat file, unspecified type', function () { + const result = this.flat.load('test/config/test.flat', null, null) + assert.deepEqual(result, 'line1') + }) + + it('returns hostname for empty "me"', function () { + const result = this.flat.load('test/config/me', null, null) + assert.ok(result) + }) }) diff --git a/test/readers/hjson.js b/test/readers/hjson.js index 5f9ffc1..4682428 100644 --- a/test/readers/hjson.js +++ b/test/readers/hjson.js @@ -1,30 +1,26 @@ - const assert = require('assert') -const path = require('path') +const path = require('path') beforeEach(function (done) { - this.hjson = require('../../readers/hjson') - done() + this.hjson = require('../../lib/readers/hjson') + done() }) describe('hjson', function () { - it('module is required', function (done) { - assert.ok(this.hjson) - done() - }) + it('module is required', function () { + assert.ok(this.hjson) + }) - it('has a load function', function (done) { - assert.ok(typeof this.hjson.load === 'function') - done() - }) + it('has a load function', function () { + assert.ok(typeof this.hjson.load === 'function') + }) - it('loads the test HJSON file', function (done) { - const result = this.hjson.load(path.join('test','config','test.hjson')) - // console.log(result) - assert.equal(result.matt, 'waz here and also made comments') - assert.ok(result.differentArray.length) - assert.ok(result.object['has a property one']) - assert.ok(result.object['has a property two']) - done() - }) + it('loads the test HJSON file', function () { + const result = this.hjson.load(path.join('test', 'config', 'test.hjson')) + // console.log(result) + assert.equal(result.matt, 'waz here and also made comments') + assert.ok(result.differentArray.length) + assert.ok(result.object['has a property one']) + assert.ok(result.object['has a property two']) + }) }) diff --git a/test/readers/ini.js b/test/readers/ini.js index ed6e331..e31e057 100644 --- a/test/readers/ini.js +++ b/test/readers/ini.js @@ -1,164 +1,140 @@ - const assert = require('assert') -const regex = require('../../configfile').regex - beforeEach(function (done) { - this.ini = require('../../readers/ini'); - this.opts = { - booleans: [ 'main.bool_true', 'main.bool_false' ] - }; - done(); + this.ini = require('../../lib/readers/ini') + this.opts = { + booleans: ['main.bool_true', 'main.bool_false'], + } + done() }) describe('ini', function () { - - it('requires', function (done) { - assert.ok(this.ini); - done() + it('requires', function () { + assert.ok(this.ini) + }) + + it('has a load function', function () { + assert.ok(typeof this.ini.load === 'function') + }) + + it('loads the test ini file', function () { + const result = this.ini.load('test/config/test.ini', {}) + // console.log(result); + assert.deepEqual(result.main, { + bool_true: 'true', + bool_false: 'false', + str_true: 'true', + str_false: 'false', }) - - it('has a load function', function (done) { - assert.ok(typeof this.ini.load === 'function'); - done() + }) + + describe('test.ini', function () { + it('no opts', function () { + const r = this.ini.load('test/config/test.ini', {}) + assert.strictEqual(r.main.bool_true, 'true') + assert.strictEqual(r.main.bool_false, 'false') + assert.strictEqual(r.main.str_true, 'true') + assert.strictEqual(r.main.str_false, 'false') }) - it('loads the test ini file', function (done) { - const result = this.ini.load('test/config/test.ini', {}, regex); - // console.log(result); - assert.deepEqual(result.main, { - bool_true: 'true', bool_false: 'false', - str_true: 'true', str_false: 'false' - }); - done() + it('opts', function () { + const r = this.ini.load('test/config/test.ini', this.opts).main + assert.strictEqual(r.bool_true, true) + assert.strictEqual(r.bool_false, false) + assert.strictEqual(r.str_true, 'true') + assert.strictEqual(r.str_false, 'false') }) - describe('test.ini', function () { - it('no opts', function (done) { - const r = this.ini.load('test/config/test.ini', {}, regex); - assert.strictEqual(r.main.bool_true, 'true'); - assert.strictEqual(r.main.bool_false, 'false'); - assert.strictEqual(r.main.str_true, 'true'); - assert.strictEqual(r.main.str_false, 'false'); - done() - }) - - it('opts', function (done) { - const r = this.ini.load('test/config/test.ini', this.opts, regex).main; - assert.strictEqual(r.bool_true, true); - assert.strictEqual(r.bool_false, false); - assert.strictEqual(r.str_true, 'true'); - assert.strictEqual(r.str_false, 'false'); - done() - }) - - it('sect1, opts', function (done) { - const r = this.ini.load('test/config/test.ini', { - booleans: ['sect1.bool_true','sect1.bool_false'] - }, regex); - assert.strictEqual(r.sect1.bool_true, true); - assert.strictEqual(r.sect1.bool_false, false); - assert.strictEqual(r.sect1.str_true, 'true'); - assert.strictEqual(r.sect1.str_false, 'false'); - done() - }) - - it('sect1, opts, w/defaults', function (done) { - const r = this.ini.load('test/config/test.ini', { - booleans: [ - '+sect1.bool_true', - '-sect1.bool_false', - '+sect1.bool_true_default', - 'sect1.-bool_false_default' - ] - }, regex); - assert.strictEqual(r.sect1.bool_true, true); - assert.strictEqual(r.sect1.bool_false, false); - assert.strictEqual(r.sect1.str_true, 'true'); - assert.strictEqual(r.sect1.str_false, 'false'); - assert.strictEqual(r.sect1.bool_true_default, true); - assert.strictEqual(r.sect1.bool_false_default, false); - done() - }) + it('sect1, opts', function () { + const r = this.ini.load('test/config/test.ini', { + booleans: ['sect1.bool_true', 'sect1.bool_false'], + }) + assert.strictEqual(r.sect1.bool_true, true) + assert.strictEqual(r.sect1.bool_false, false) + assert.strictEqual(r.sect1.str_true, 'true') + assert.strictEqual(r.sect1.str_false, 'false') + }) - it('wildcard boolean', function (done) { - const r = this.ini.load('test/config/test.ini', { - booleans: [ '+main.bool_true', '*.is_bool' ] - }, regex); - assert.strictEqual(r['*'], undefined); - assert.strictEqual(r.main.bool_true, true); - assert.strictEqual(r.main.is_bool, undefined); - assert.strictEqual(r['foo.com'].is_bool, true); - assert.strictEqual(r['bar.com'].is_bool, false); - done() - }) + it('sect1, opts, w/defaults', function () { + const r = this.ini.load('test/config/test.ini', { + booleans: [ + '+sect1.bool_true', + '-sect1.bool_false', + '+sect1.bool_true_default', + 'sect1.-bool_false_default', + ], + }) + assert.strictEqual(r.sect1.bool_true, true) + assert.strictEqual(r.sect1.bool_false, false) + assert.strictEqual(r.sect1.str_true, 'true') + assert.strictEqual(r.sect1.str_false, 'false') + assert.strictEqual(r.sect1.bool_true_default, true) + assert.strictEqual(r.sect1.bool_false_default, false) }) - describe('non-exist.ini (empty)', function () { + it('wildcard boolean', function () { + const r = this.ini.load('test/config/test.ini', { + booleans: ['+main.bool_true', '*.is_bool'], + }) + assert.strictEqual(r['*'], undefined) + assert.strictEqual(r.main.bool_true, true) + assert.strictEqual(r.main.is_bool, undefined) + assert.strictEqual(r['foo.com'].is_bool, true) + assert.strictEqual(r['bar.com'].is_bool, false) + }) + }) - it('is template', function (done) { - assert.deepEqual(this.ini.empty(), { main: { } } ); - done() - }) + describe('non-exist.ini (empty)', function () { + it('is template', function () { + assert.deepEqual(this.ini.empty(), { main: {} }) + }) - it('boolean', function (done) { - assert.deepEqual( - this.ini.empty({ booleans: ['reject']}), - { main: { reject: false } } - ); - done() - }) + it('boolean', function () { + assert.deepEqual(this.ini.empty({ booleans: ['reject'] }), { + main: { reject: false }, + }) + }) - it('boolean true default', function (done) { - assert.deepEqual( - this.ini.empty({ booleans: ['+reject']}), - { main: { reject: true } } - ); - assert.deepEqual( - this.ini.empty({ booleans: ['+main.reject']}), - { main: { reject: true } } - ); - assert.deepEqual( - this.ini.empty({ booleans: ['main.+reject']}), - { main: { reject: true } } - ); - done() - }) + it('boolean true default', function () { + assert.deepEqual(this.ini.empty({ booleans: ['+reject'] }), { + main: { reject: true }, + }) + assert.deepEqual(this.ini.empty({ booleans: ['+main.reject'] }), { + main: { reject: true }, + }) + assert.deepEqual(this.ini.empty({ booleans: ['main.+reject'] }), { + main: { reject: true }, + }) + }) - it('boolean false default', function (done) { - assert.deepEqual( - this.ini.empty({ booleans: ['-reject']}), - { main: { reject: false } } - ); - assert.deepEqual( - this.ini.empty({ booleans: ['-main.reject']}), - { main: { reject: false } } - ); - assert.deepEqual( - this.ini.empty({ booleans: ['main.-reject']}), - { main: { reject: false } } - ); - done() - }) + it('boolean false default', function () { + assert.deepEqual(this.ini.empty({ booleans: ['-reject'] }), { + main: { reject: false }, + }) + assert.deepEqual(this.ini.empty({ booleans: ['-main.reject'] }), { + main: { reject: false }, + }) + assert.deepEqual(this.ini.empty({ booleans: ['main.-reject'] }), { + main: { reject: false }, + }) + }) - it('boolean false default, section', function (done) { - assert.deepEqual( - this.ini.empty({ booleans: ['-reject.boolf']}), - { main: { }, reject: {boolf: false} } - ); - assert.deepEqual( - this.ini.empty({ booleans: ['+reject.boolt']}), - { main: { }, reject: {boolt: true} } - ); - done() - }) + it('boolean false default, section', function () { + assert.deepEqual(this.ini.empty({ booleans: ['-reject.boolf'] }), { + main: {}, + reject: { boolf: false }, + }) + assert.deepEqual(this.ini.empty({ booleans: ['+reject.boolt'] }), { + main: {}, + reject: { boolt: true }, + }) }) + }) - describe('goobers.ini', function () { - it('goobers.ini has invalid entry', function (done) { - const result = this.ini.load('test/config/goobers.ini', {}, regex); - assert.deepEqual(result, { main: { } } ); - done() - }) + describe('goobers.ini', function () { + it('goobers.ini has invalid entry', function () { + const result = this.ini.load('test/config/goobers.ini', {}) + assert.deepEqual(result, { main: {} }) }) + }) }) diff --git a/test/readers/json.js b/test/readers/json.js index 6eea5d4..988fd1e 100644 --- a/test/readers/json.js +++ b/test/readers/json.js @@ -1,29 +1,24 @@ - const assert = require('assert') beforeEach(function (done) { - this.json = require('../../readers/json'); - done(); + this.json = require('../../lib/readers/json') + done() }) describe('json', function () { + it('module is required', function () { + assert.ok(this.json) + }) - it('module is required', function (done) { - assert.ok(this.json); - done(); - }) - - it('has a load function', function (done) { - assert.ok(typeof this.json.load === 'function'); - done(); - }) + it('has a load function', function () { + assert.ok(typeof this.json.load === 'function') + }) - it('loads the test JSON file', function (done) { - const result = this.json.load('test/config/test.json'); - // console.log(result); - assert.equal(result.matt, 'waz here'); - assert.ok(result.array.length); - assert.ok(result.objecty['has a property']); - done(); - }) + it('loads the test JSON file', function () { + const result = this.json.load('test/config/test.json') + // console.log(result); + assert.equal(result.matt, 'waz here') + assert.ok(result.array.length) + assert.ok(result.objecty['has a property']) + }) }) diff --git a/test/readers/yaml.js b/test/readers/yaml.js index d77e6b8..f2a2d83 100644 --- a/test/readers/yaml.js +++ b/test/readers/yaml.js @@ -1,29 +1,24 @@ - const assert = require('assert') beforeEach(function (done) { - this.yaml = require('../../readers/yaml'); - done(); + this.yaml = require('../../lib/readers/yaml') + done() }) describe('yaml', function () { + it('module is required', function () { + assert.ok(this.yaml) + }) - it('module is required', function (done) { - assert.ok(this.yaml); - done(); - }) - - it('has a load function', function (done) { - assert.ok(typeof this.yaml.load === 'function'); - done(); - }) + it('has a load function', function () { + assert.ok(typeof this.yaml.load === 'function') + }) - it('loads the test yaml file', function (done) { - const result = this.yaml.load('test/config/test.yaml'); - assert.strictEqual(result.main.bool_true, true); - assert.equal(result.matt, 'waz here'); - assert.ok(result.array.length); - assert.ok(result.objecty['has a property']); - done(); - }) + it('loads the test yaml file', function () { + const result = this.yaml.load('test/config/test.yaml') + assert.strictEqual(result.main.bool_true, true) + assert.equal(result.matt, 'waz here') + assert.ok(result.array.length) + assert.ok(result.objecty['has a property']) + }) }) diff --git a/test/regex.js b/test/regex.js new file mode 100644 index 0000000..f095af0 --- /dev/null +++ b/test/regex.js @@ -0,0 +1,59 @@ +const assert = require('node:assert') + +const regex = require('../lib/regex') + +describe('regex', function () { + it('section', function () { + assert.equal(regex.section.test('[foo]'), true) + assert.equal(regex.section.test('bar'), false) + assert.equal(regex.section.test('[bar'), false) + assert.equal(regex.section.test('bar]'), false) + }) + + it('param', function () { + assert.equal(regex.param.exec('foo=true')[1], 'foo') + assert.equal(regex.param.exec(';foo=true'), undefined) + }) + + it('comment', function () { + assert.equal(regex.comment.test('; true'), true) + assert.equal(regex.comment.test('false'), false) + }) + + it('line', function () { + assert.equal(regex.line.test(' boo '), true) + assert.equal(regex.line.test('foo'), true) + }) + + it('blank', function () { + assert.equal(regex.blank.test('foo'), false) + assert.equal(regex.blank.test(' '), true) + }) + + it('is_integer', function () { + assert.equal(regex.is_integer.test(1), true) + assert.equal(regex.is_integer.test(''), false) + assert.equal(regex.is_integer.test('a'), false) + }) + + it('is_float', function () { + assert.equal(regex.is_float.test('1.0'), true) + assert.equal(regex.is_float.test(''), false) + assert.equal(regex.is_float.test('45'), false) + }) + + it('is_truth', function () { + assert.equal(regex.is_truth.test('no'), false) + assert.equal(regex.is_truth.test('nope'), false) + assert.equal(regex.is_truth.test('nuh uh'), false) + assert.equal(regex.is_truth.test('yes'), true) + assert.equal(regex.is_truth.test('true'), true) + assert.equal(regex.is_truth.test(true), true) + }) + + it('is_array', function () { + assert.equal(regex.is_array.test('foo=bar'), false) + assert.equal(regex.is_array.test('foo'), false) + assert.equal(regex.is_array.test('foo[]'), true) + }) +})