Skip to content

Commit

Permalink
Version 2.0.0! (#7)
Browse files Browse the repository at this point in the history
* Fixes #6. Properties on targets can now be deleted declaratively.
* API is now P, S, PS, D (was patch, scope, ps). Terse capital initials are more consistently identifiable, at a skim, as directives in arbitrary code structures.
  • Loading branch information
barneycarroll authored Jan 2, 2018
1 parent 0b0e889 commit c077856
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 66 deletions.
41 changes: 21 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,31 @@ Throw your rose-tinted [lenses](https://medium.com/javascript-inside/an-introduc

# What

Patchinko exposes 3 functions: `patch`, `scope`, & `ps`.
Patchinko exposes 3 functions: `P`, `S`, & `PS`.

`patch` is like `Object.assign` - given `patch(target, input1, input2, etc)`, it consumes inputs left to right and copies their properties onto the supplied target.
`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*
*except that*

If any target properties are instances of `scope(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 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 `patch` & `scope`, for when you need to patch recursively. If you supply a `target`, the original value will be left untouched (useful for immutable patching).
`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:

```js
const {patch : p, scope : s, ps} = require('patchinko')
const {P, S, PS, D} = require('patchinko')

// Some arbitrary structure
const thing = {
foo: 'bar',

fizz: 'buzz',

bish: 'bash',

utils: {
mean: (...set) =>
set.reduce((a, b) => a + b) / set.length,
Expand All @@ -42,16 +44,18 @@ const thing = {
deep: {
structure: ['lol']
},
with: ['an', 'array', 'tacked']
with: ['a', 'list', 'tacked', 'on']
}
}

// A deep patch
p(thing, {
foo: 'baz', // Change the value of foo
P(thing, {
foo: 'baz', // Change the value of `foo`

bish: D, // Delete property `bish`

utils: ps({ // We want to patch a level deeper
fibonacci: s(function closure(definition){ // Memoize fibonacci
utils: PS({ // We want to patch a level deeper
fibonacci: S(function closure(definition){ // Memoize `fibonacci`
var cache = {}

return function override(x){
Expand All @@ -64,16 +68,16 @@ p(thing, {
})
}),

stupidly: ps({
deep: ps({
structure: s(structure =>
stupidly: PS({
deep: PS({
structure: S(structure =>
structure.concat('roflmao') // Why not
)
}),
with: ps(
with: PS(
[],
{1: 'copy'}
) // ['a', 'copy', 'tacked'], the original array is left untouched
) // ['a', 'copy', 'tacked', 'on'] - the original array is left untouched
})
})
```
Expand All @@ -84,11 +88,8 @@ Observe that:
* `utils.fibonacci` can safely be decorated (again, the rest of `utils` is unaffected)
* `stupidly.deep.structure` can be modified, keeping its identity

`stupidly.deep.stucture` & `utils.fibonacci` show that any kind of structure can be modified or replaced at any kind of depth: `patch` is geared towards the common case of objects, but `scope` 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.

```JS
`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.

````

# Why

Expand Down
37 changes: 21 additions & 16 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,40 @@
function patch(target){
function P(target){
for(var i = 1; i < arguments.length; i++)
for(var key in arguments[i])
if(arguments[i].hasOwnProperty(key))
target[key] = (
arguments[i][key] instanceof scope
arguments[i][key] == D
? delete target[key]
: target[key] =
arguments[i][key] instanceof S
? arguments[i][key].apply(target[key])
: arguments[i][key]
)

return target
}

function scope(closure){
if(!(this instanceof scope))
return new scope(closure)
function S(closure){
if(!(this instanceof S))
return new S(closure)

this.apply = function(definition){
return closure(definition)
}
}

function ps(target, input){
return arguments.length === 2
? new scope(function(definition){
return patch(target, definition, input)
})
: new scope(function(definition){
return patch(definition, target) // `target` really is `input` in that case
})
function PS(target, input){
return new S(
arguments.length === 2
? function(definition){
return P(target, definition, input)
}
: function(definition){
return P(definition, target)
}
)
}

function D(){}

try {
module.exports = {patch: patch, scope: scope, ps: ps}
module.exports = {P: P, S: S, PS: PS, D: D}
} catch(e) {}
99 changes: 69 additions & 30 deletions tests/index.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
const o = require('ospec')

const {patch, scope, ps} = require('../index.js')
const {P, S, PS, D} = require('../index.js')

const I = x => x
const A = f => x => f(x)

o('`scope`', () => {
o('`S`', () => {
const unique = Symbol('unicum')

o(
scope(I).apply(unique)
S(I).apply(unique)
).equals(
A(I)(unique)
)
('is equivalent to an applicative combinator')

o(
scope(I) instanceof scope
S(I) instanceof S
).equals(
true
)
('whose partial application is identifiable as a `scope`')
('whose partial application is identifiable as a `S`')
})

o.spec('`patch`', () => {
o.spec('`P`', () => {
o('consumes a target object & an input object', () => {
o(patch({}, {}))
o(P({}, {}))
})

o('is equivalent to `Object.assign` in the absence of `scope`', () => {
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'}),
Expand All @@ -37,31 +37,33 @@ o.spec('`patch`', () => {
const [a, b] = [factoryA(), factoryB()]

o(
patch(a, b)
P(a, b)
).equals(
a
)
('preserves target identity')

o(
patch(factoryA(), factoryB())
P(factoryA(), factoryB())
).deepEquals(
Object.assign(factoryA(), factoryB())
)
('copies properties of input onto target')
})

o.spec('with `scope`', () => {
o.spec('with `S`', () => {
o('supplies the target\'s property value to the scoped function', () => {
const unique = Symbol('unicum')

let interception

patch(
{a: unique},
{a: scope(received => {
interception = received
})}
P(
{ a: unique },
{
a: S(received => {
interception = received
})
}
)

o(interception).equals(unique)
Expand All @@ -72,43 +74,80 @@ o.spec('`patch`', () => {
const unique2 = Symbol('unicum2')

o(
patch(
{a: unique1},
{a: scope(I)}
P(
{ a: unique1 },
{ a: S(I) }
).a
).equals(
unique1
)
)

o(
patch(
{a: unique1},
{a: scope(() => unique2)}
P(
{ a: unique1 },
{ a: S(() => unique2) }
).a
).equals(
unique2
)
})
})

o.spec('with `D`', () => {
o('deletes the target property with the same key', () => {
o(
P(
{ 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
)
})
})
})

o.spec('`ps`', () => {
o('composes `patch` with `scope`, allowing recursion', () => {
o.spec('`PS`', () => {
o('composes `P` with `S`, allowing recursion', () => {
const one = Symbol('one')
const two = Symbol('two')

let interception

o(
patch(
P(
{
a: {
b: one
}
},

{
a: ps({
b: scope(received => {
a: PS({
b: S(received => {
interception = received

return two
Expand All @@ -129,12 +168,12 @@ o.spec('`ps`', () => {
})
o('accepts a custom target', () => {
o(
patch(
P(
{
a: [1, 2]
},
{
a: ps(
a: PS(
[],
{
1: 3
Expand Down

0 comments on commit c077856

Please sign in to comment.