diff --git a/README.md b/README.md index 15c2c4f..3b98773 100644 --- a/README.md +++ b/README.md @@ -6,23 +6,56 @@ Throw your rose-tinted [lenses](https://medium.com/javascript-inside/an-introduc # What -Patchinko exposes 3 functions: `P`, `S`, & `PS`. +Patchinko exposes 4 granular APIs: `P`, `S`, `PS`, & `D`. `P` is like `Object.assign`: given `P(target, input1, input2, etc)`, it consumes inputs left to right and copies their properties onto the supplied target *…except that…* -If any target properties are instances of `S(function)`, it will supply the scoped function with the target property for that key, and assign the result back to the target; if any target properties are `D`, it will delete the property of the same key on the target. +If any target properties are instances of `S(function)`, it will supply the scoped function with the target property for that key, and assign the result back to the target. + +If any target properties are `D`, it will delete the property of the same key on the target. `PS([ target, ] input)` is a composition of `P` & `S`, for when you need to patch recursively. If you supply a `target`, the original value will be left untouched (useful for immutable patching). -# How +*** -The kitchen sink example: +Patchinko also comes with a don't-make-me-think single-reference overloaded API - useful when the essential patching operations are intuitive but the different API invocations are cognitively overbearing to determine or noisy to read. + +`O` is an overloaded API that subsumes the above (with the exception of the n-ary immutable `PS` overload): + +1. No arguments stands in for `D`. +2. A function argument stands in for `S`. +3. A non-function single argument stands in for `PS`. +4. …otherwise, `P`. + +# Where + +Supplied in CommonJS module format & as unscoped top-level references. Available on [npm](https://npmjs.org/package/patchinko) & [UNPKG cdn](https://unpkg.com/patchinko). + +In Node: ```js const {P, S, PS, D} = require('patchinko') +// or +const O = require('patchinko/overloaded') +``` + +In the browser: +```html + + + + + +``` + +# How + +The kitchen sink example: + +```js // Some arbitrary structure const thing = { foo: 'bar', @@ -35,7 +68,7 @@ const thing = { mean: (...set) => set.reduce((a, b) => a + b) / set.length, - fibonacci: function(x) { + fibonacci(x){ return x <= 1 ? x : this.fibonacci(x - 1) + this.fibonacci(x - 2) }, }, @@ -55,14 +88,14 @@ P(thing, { bish: D, // Delete property `bish` utils: PS({ // We want to patch a level deeper - fibonacci: S(function closure(definition){ // Memoize `fibonacci` - var cache = {} + fibonacci: S(fibonacci => { // Memoize `fibonacci` + const cache = {} - return function override(x){ + return function(x){ return ( x in cache ? cache[x] - : cache[x] = definition.apply(this, arguments) + : cache[x] = fibonacci.call(this, x) ) } }) @@ -90,6 +123,46 @@ Observe that: `stupidly.deep.stucture` & `utils.fibonacci` show that any kind of structure can be modified or replaced at any kind of depth: `P` is geared towards the common case of objects, but `S` can deal with any type in whatever way necessary. You get closures for free so gnarly patch logic can be isolated at the point where it makes the most sense. +*** + +Using the overloaded API, the same results are achieved as follows: + +```js +const O = require('patchinko/overloaded') + +O(thing, { + foo: 'baz', + + bish: O, + + utils: O({ + fibonacci: O(fibonacci => { + const cache = {} + + return function(x){ + return ( + x in cache + ? cache[x] + : cache[x] = fibonacci.call(this, x) + ) + } + }) + }), + + stupidly: O({ + deep: O({ + structure: O(structure => + structure.concat('roflmao') + ) + }), + with: O(structure => + O([], structure, {1: 'copy'}) // [1] + ) + }) +}) +``` + +[1️] The single-API overload forbids the immutable `PS` overload because more than 1 argument will necessarily fork to `P`. Thus immutable nested structure patching with `O` requires 2 invocations, 1 forking to `S` and the 2nd to `P`. # Why diff --git a/index.js b/index.js index e64e7be..6eaff99 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,7 @@ function P(target){ for(var i = 1; i < arguments.length; i++) for(var key in arguments[i]) if(arguments[i].hasOwnProperty(key)) - arguments[i][key] == D + arguments[i][key] === D ? delete target[key] : target[key] = arguments[i][key] instanceof S @@ -35,6 +35,16 @@ function PS(target, input){ function D(){} +function O(x){ + return arguments.length + ? 1 < arguments.length + ? P.apply(arguments) + : typeof x === 'function' + ? new S(x) + : PS(x) + : D +} + try { - module.exports = {P: P, S: S, PS: PS, D: D} + module.exports = {P: P, S: S, PS: PS, D: D, O: O} } catch(e) {} diff --git a/overloaded.js b/overloaded.js new file mode 100644 index 0000000..0451d75 --- /dev/null +++ b/overloaded.js @@ -0,0 +1,34 @@ +function O(a, b){ + if(arguments.length == 1) + if(typeof a == 'function') + if(!(this instanceof O)) + return new O(a) + + else + this.apply = function(c){ + return a(c) + } + + else + return new O(function(c){ + return O(c, a) + }) + + else { + for(var i = 1; i < arguments.length; i++, b = arguments[i]) + for(var key in b) + if(b.hasOwnProperty(key)) + b[key] == O + ? delete a[key] + : a[key] = + b[key] instanceof O + ? b[key].apply(a[key]) + : b[key] + + return a + } +} + +try { + module.exports = O +} catch(e) {} diff --git a/package.json b/package.json index 293eb79..21ff06f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "patchinko", - "version": "2.0.0", + "version": "2.1.0", "description": "Concise monkey-patching utility", "main": "index.js", "repository": "git@github.com:barneycarroll/patchinko.git", diff --git a/tests/index.js b/tests/index.js index 5419652..981c0d5 100644 --- a/tests/index.js +++ b/tests/index.js @@ -30,8 +30,8 @@ o.spec('`P`', () => { o('is equivalent to `Object.assign` in the absence of `S`', () => { const [factoryA, factoryB] = [ - () => ({a:'foo', b:2, d: {bar: 'z'}, f: [3, 4]}), - () => ({a:'baz', c:3, d: {fizz: 'z'}, f: 'buzz'}), + () => ({a: 'foo', b: 2, d: {bar: 'z'}, f: [3, 4]}), + () => ({a: 'baz', c: 3, d: {fizz: 'z'}, f: 'buzz'}), ] const [a, b] = [factoryA(), factoryB()] @@ -52,7 +52,7 @@ o.spec('`P`', () => { }) o.spec('with `S`', () => { - o('supplies the target\'s property value to the scoped function', () => { + o("supplies the target's property value to the scoped function", () => { const unique = Symbol('unicum') let interception @@ -75,21 +75,21 @@ o.spec('`P`', () => { o( P( - { a: unique1 }, + { a: unique1 }, { a: S(I) } ).a ).equals( unique1 - ) + ) o( P( - { a: unique1 }, + { a: unique1 }, { a: S(() => unique2) } ).a ).equals( unique2 - ) + ) }) }) @@ -97,36 +97,13 @@ o.spec('`P`', () => { o('deletes the target property with the same key', () => { o( P( - { a: 1, b: 2 }, + { a: 1, b: 2 }, { a: D } ) ).deepEquals( { b: 2 } ) }) - - o('assigns the product of any scoped closures to the target properties', () => { - const unique1 = Symbol('unicum1') - const unique2 = Symbol('unicum2') - - o( - P( - { a: unique1 }, - { a: S(I) } - ).a - ).equals( - unique1 - ) - - o( - P( - { a: unique1 }, - { a: S(() => unique2) } - ).a - ).equals( - unique2 - ) - }) }) }) @@ -155,17 +132,17 @@ o.spec('`PS`', () => { }) } ) - ) - .deepEquals( - { - a: { - b: two - } + ).deepEquals( + { + a: { + b: two } - ) + } + ) o(interception).equals(one) }) + o('accepts a custom target', () => { o( P( @@ -174,7 +151,7 @@ o.spec('`PS`', () => { }, { a: PS( - [], + [], { 1: 3 } diff --git a/tests/overloaded.js b/tests/overloaded.js new file mode 100644 index 0000000..ca4737a --- /dev/null +++ b/tests/overloaded.js @@ -0,0 +1,108 @@ +const o = require('ospec') + +const O = require('../overloaded.js') + +const I = x => x +const A = f => x => f(x) + +o('`O` (with a single function argument)', () => { + const unique = Symbol('unicum') + + o( + O(I).apply(unique) + ).equals( + A(I)(unique) + ) + ('is equivalent to an applicative combinator') + + o( + O(I) instanceof O + ).equals( + true + ) + ('whose partial application is identifiable as an `O`') +}) + +o.spec('`O` (with 2 or more arguments)', () => { + o('consumes a target object & an input object', () => { + o(O({}, {})) + }) + + o('is equivalent to `Object.assign` in the absence of any sub-properties', () => { + const [factoryA, factoryB] = [ + () => ({a: 'foo', b: 2, d: {bar: 'z'}, f: [3, 4]}), + () => ({a: 'baz', c: 3, d: {fizz: 'z'}, f: 'buzz'}), + ] + + const [a, b] = [factoryA(), factoryB()] + + o( + O(a, b) + ).equals( + a + ) + ('preserves target identity') + + o( + O(factoryA(), factoryB()) + ).deepEquals( + Object.assign(factoryA(), factoryB()) + ) + ('copies properties of input onto target') + }) + + o.spec('with nested (single-function-consuming) `O` properties', () => { + o("supplies the target's property value to the scoped function", () => { + const unique = Symbol('unicum') + + let interception + + O( + { a: unique }, + { + a: O(received => { + interception = received + }) + } + ) + + o(interception).equals(unique); + }); + + o('assigns the product of any scoped closures to the target properties', () => { + const unique1 = Symbol('unicum1') + const unique2 = Symbol('unicum2') + + o( + O( + { a: unique1 }, + { a: O(I) } + ).a + ).equals( + unique1 + ) + + o( + O( + { a: unique1 }, + { a: O(() => unique2) } + ).a + ).equals( + unique2 + ) + }) + }) + + o.spec('with `O` (supplied as a property value)', () => { + o('deletes the target property with the same key', () => { + o( + O( + { a: 1, b: 2 }, + { a: O } + ) + ).deepEquals( + { b: 2 } + ) + }) + }) +})