From 17c1c275823402931c4f67728c4d023f14f2d1aa Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Fri, 3 May 2024 09:58:54 -0700 Subject: [PATCH] Release v1.1.0 (#21) - feat(set): add 'default' mode. --- .codeclimate.yml | 15 +++++ .github/workflows/ci.yml | 5 ++ .release | 2 +- CHANGELOG.md | 5 ++ CONTRIBUTORS.md | 8 +++ README.md | 18 ++++- index.js | 76 +++++++++++---------- package.json | 6 +- test/index.js | 140 +++++++++++++++++++-------------------- 9 files changed, 162 insertions(+), 113 deletions(-) create mode 100644 .codeclimate.yml create mode 100644 CONTRIBUTORS.md diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..d657c7a --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,15 @@ +engines: + eslint: + enabled: true + channel: 'eslint-8' + config: + config: '.eslintrc.yaml' + +checks: + method-complexity: + config: + threshold: 10 + +ratings: + paths: + - '**.js' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 435be73..9198c99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,7 @@ name: CI on: push: + pull_request: env: CI: true @@ -21,3 +22,7 @@ jobs: windows: needs: [lint] uses: haraka/.github/.github/workflows/windows.yml@master + + macos: + needs: [lint] + uses: haraka/.github/.github/workflows/macos.yml@master diff --git a/.release b/.release index 7cd5707..0fa4e69 160000 --- a/.release +++ b/.release @@ -1 +1 @@ -Subproject commit 7cd5707f7d69f8d4dca1ec407ada911890e59d0a +Subproject commit 0fa4e690ffabb0157e46d56f18e4f7cfe49ce291 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2227e4c..6ef84ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ### Unreleased +### [1.1.0] - 2024-05-03 + +- set: added 'default' mode. See docs. # + ### [1.0.7] - 2024-04-08 - use `[files]` in package.json. Delete .npmignore. @@ -42,3 +46,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). [1.0.4]: https://github.com/haraka/haraka-notes/releases/tag/1.0.4 [1.0.6]: https://github.com/haraka/haraka-notes/releases/tag/1.0.6 [1.0.7]: https://github.com/haraka/haraka-notes/releases/tag/v1.0.7 +[1.1.0]: https://github.com/haraka/haraka-notes/releases/tag/v1.1.0 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..0a6d322 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,8 @@ +# Contributors + +This handcrafted artisinal software is brought to you by: + +|
msimerson (23)| +| :---: | + +this file is maintained by [.release](https://github.com/msimerson/.release) diff --git a/README.md b/README.md index ac35779..7436d63 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,23 @@ Notes are objects that exist on Haraka connections and transactions. Prior to the release of [haraka-notes](https://github.com/haraka/haraka-notes), notes was just an empty object. Now notes is an empty object with two functions: -### set (path, value) +## Usage -Sets a note at a dot delimited path to the specified value. The path can be any number of levels deep and any missing objects in the path are [autovivified](https://en.wikipedia.org/wiki/Autovivification). Perl refugees, contain yourselves. +```js +const Notes = require('haraka-notes') +const myNote = new Notes() + +myNote.set('some.path', 'a value') // { some: {path: 'a value'}} +myNote.get('some.path') // 'a value' +``` + +### set (path, value, [onlyIfUndefined]) + +Sets a note at a dot delimited path to the specified value. The path can be any number of levels deep and any missing objects in the path are [autovivified](https://en.wikipedia.org/wiki/Autovivification). Perl afficianados, contain yourselves. + +#### set default + +If the third set argument is any truthy value, then the property is only set if the current value is undefined. This is useful for applying default values. ```js connection.transaction.notes.set('queue.wants', 'smtp_forward') diff --git a/index.js b/index.js index 8ceacfc..b7b1bdc 100644 --- a/index.js +++ b/index.js @@ -1,54 +1,58 @@ class Notes { - constructor(notes) { - if (notes && typeof notes === 'object') { - Object.assign(this, notes) - } - - Object.defineProperty(this, 'set', { - configurable: false, - enumerable: false, - writable: false, - value: assignPathValue.bind(this), - }) - - Object.defineProperty(this, 'get', { - configurable: false, - enumerable: false, - writable: false, - value: getPathValue.bind(this), - }) + constructor(notes) { + if (notes && typeof notes === 'object') { + Object.assign(this, notes) } + + Object.defineProperty(this, 'set', { + configurable: false, + enumerable: false, + writable: false, + value: assignPathValue.bind(this), + }) + + Object.defineProperty(this, 'get', { + configurable: false, + enumerable: false, + writable: false, + value: getPathValue.bind(this), + }) + } } module.exports = Notes function getSegments(path) { - // a dot.delimited.path - if (typeof path === 'string') return path.split('.') + // a dot.delimited.path + if (typeof path === 'string') return path.split('.') - // ['one', 'two', 'thr.ee'] - if (Array.isArray(path)) return path + // ['one', 'two', 'thr.ee'] + if (Array.isArray(path)) return path } -function assignPathValue(path, value) { - if (path === undefined || value === undefined) return +function assignPathValue(path, value, onlyWhenUndefined) { + if (path === undefined || value === undefined) return - const segments = getSegments(path) - let dest = this + const segments = getSegments(path) + let dest = this - while (segments.length > 1) { + while (segments.length > 1) { // create any missing paths - if (!dest[segments[0]]) dest[segments[0]] = {} - // set dest one path segment deeper - dest = dest[segments.shift()] - } + if (!dest[segments[0]]) dest[segments[0]] = {} + // set dest one path segment deeper + dest = dest[segments.shift()] + } + if (onlyWhenUndefined) { + if (dest[segments[0]] === undefined) dest[segments[0]] = value + } else { dest[segments[0]] = value + } } function getPathValue(path) { - if (!path) return - const segments = getSegments(path) - return segments.reduce((prev, curr) => { - return prev ? prev[curr] : undefined - }, this) + if (!path) return + const segments = getSegments(path) + return segments.reduce((prev, curr) => { + return prev ? prev[curr] : undefined + }, this) } diff --git a/package.json b/package.json index 7189b26..5418450 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "haraka-notes", - "version": "1.0.7", + "version": "1.1.0", "description": "Haraka Notes", "main": "index.js", "files": [ @@ -13,8 +13,8 @@ "prettier": "npx prettier . --check", "prettier:fix": "npx prettier . --write --log-level=warn", "test": "npx mocha@10", - "versions": "npx @msimerson/dependency-version-checker check", - "versions:fix": "npx @msimerson/dependency-version-checker update && npm run prettier:fix" + "versions": "npx dependency-version-checker check", + "versions:fix": "npx dependency-version-checker update && npm run prettier:fix" }, "repository": { "type": "git", diff --git a/test/index.js b/test/index.js index ebab3da..16f565a 100644 --- a/test/index.js +++ b/test/index.js @@ -3,91 +3,89 @@ const assert = require('assert') const Notes = require('../index') describe('notes', () => { - beforeEach((done) => { - this.notes = new Notes() - done() - }) + beforeEach((done) => { + this.notes = new Notes() + done() + }) - it('exports an object', (done) => { - // console.log(this.notes) - assert.ok(typeof this.notes === 'object') - done() - }) + it('exports an object', () => { + assert.ok(typeof this.notes === 'object') + }) - const functionList = ['get', 'set'] + const functionList = ['get', 'set'] - functionList.forEach((fn) => { - it(`has ${fn}()`, (done) => { - assert.equal(typeof this.notes[fn], 'function') - done() - }) + for (const fn of functionList) { + it(`has ${fn}()`, () => { + assert.equal(typeof this.notes[fn], 'function') }) + } - functionList.forEach((fn) => { - it(`ignores attempts to redefine ${fn}`, (done) => { - this.notes[fn] = 'turd' - this.notes[fn]('turd') - done() - }) + for (const fn of functionList) { + it(`ignores attempts to redefine ${fn}`, () => { + this.notes[fn] = 'turd' + this.notes[fn]('turd') }) + } - it('sets a top level value', (done) => { - this.notes.set('foo', 'bar') - // console.log(this.notes) - assert.equal(this.notes.foo, 'bar') - done() - }) + it('sets a top level value', () => { + this.notes.set('foo', 'bar') + assert.equal(this.notes.foo, 'bar') + }) - it('can set a false value', (done) => { - this.notes.set('boolean', false) - assert.equal(this.notes.boolean, false) - done() - }) + it('can set a false value', () => { + this.notes.set('boolean', false) + assert.equal(this.notes.boolean, false) + }) - it('gets a top level value', (done) => { - this.notes.set('foo', 'bar') - assert.equal(this.notes.get('foo'), 'bar') - done() - }) + it('gets a top level value', () => { + this.notes.set('foo', 'bar') + assert.equal(this.notes.get('foo'), 'bar') + }) - it('sets/gets a second level value', (done) => { - this.notes.set('seg1.seg2', 'bar') - assert.equal(this.notes.seg1.seg2, 'bar') - assert.equal(this.notes.get('seg1.seg2'), 'bar') - done() - }) + it('sets/gets a second level value', () => { + this.notes.set('seg1.seg2', 'bar') + assert.equal(this.notes.seg1.seg2, 'bar') + assert.equal(this.notes.get('seg1.seg2'), 'bar') + }) - it('sets/gets a three level value', (done) => { - this.notes.set('one.two.three', 'floor') - assert.equal(this.notes.one.two.three, 'floor') - assert.equal(this.notes.get('one.two.three'), 'floor') - done() - }) + it('sets/gets a three level value', () => { + this.notes.set('one.two.three', 'floor') + assert.equal(this.notes.one.two.three, 'floor') + assert.equal(this.notes.get('one.two.three'), 'floor') + }) - it('supports array syntax', (done) => { - this.notes.set(['one', 'two', 'three'], 'floor') - assert.equal(this.notes.one.two.three, 'floor') - assert.equal(this.notes.get(['one', 'two', 'three']), 'floor') - done() - }) + it('supports array syntax', () => { + this.notes.set(['one', 'two', 'three'], 'floor') + assert.equal(this.notes.one.two.three, 'floor') + assert.equal(this.notes.get(['one', 'two', 'three']), 'floor') + }) - it('array syntax tolerates dots', (done) => { - this.notes.set(['one', 'two', 'three.four'], 'floor') - assert.equal(this.notes.one.two['three.four'], 'floor') - assert.equal(this.notes.get(['one', 'two', 'three.four']), 'floor') - done() - }) + it('array syntax tolerates dots', () => { + this.notes.set(['one', 'two', 'three.four'], 'floor') + assert.equal(this.notes.one.two['three.four'], 'floor') + assert.equal(this.notes.get(['one', 'two', 'three.four']), 'floor') + }) + + it('sets default sets a property', () => { + this.notes.set(['one', 'two'], 'tree', true) + assert.equal(this.notes.one.two, 'tree') + }) + + it('set default does NOT change defined property', () => { + this.notes.set('one.two', 'tree', true) + this.notes.set('one.two', 'three', true) + assert.equal(this.notes.one.two, 'tree') + }) }) describe('notes + object', () => { - it('assigns instantiation object', (done) => { - const passIn = { - one: true, - two: 'false', - three: 'floor', - } - this.notes = this.notes = new Notes(passIn) - assert.deepEqual(this.notes, passIn) - done() - }) + it('assigns instantiation object', () => { + const passIn = { + one: true, + two: 'false', + three: 'floor', + } + this.notes = this.notes = new Notes(passIn) + assert.deepEqual(this.notes, passIn) + }) })