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 }
+ )
+ })
+ })
+})