Comparing version
{ | ||
"name": "lentil", | ||
"version": "0.0.1", | ||
"description": "Utility library for working with Immutable.js and Zelkova", | ||
"version": "0.0.2", | ||
"description": "Partial lenses for TypeScript and JavaScript", | ||
"repository": { | ||
@@ -28,7 +28,3 @@ "type": "git", | ||
"vinyl-map": "^1.0.1" | ||
}, | ||
"dependencies": { | ||
"immutable": "^3.4.1", | ||
"zelkova": "^0.0.5" | ||
} | ||
} |
@@ -1,92 +0,127 @@ | ||
///<reference path="../node_modules/zelkova/dist/zelkova.d.ts"/> | ||
///<reference path="../node_modules/immutable/dist/immutable.d.ts"/> | ||
import Z = require("zelkova"); | ||
import I = require("immutable"); | ||
module lentil { | ||
"use strict"; | ||
export interface Collection<K, V> extends I.Collection<K, V> { | ||
setIn(keyPath: Array<any>, value: V): Collection<K, V>; | ||
updateIn(keyPath: Array<any>, updater: (value: any) => any): Collection<K, V>; | ||
export class Store<A, B> { | ||
set: (value: A) => B; | ||
value: A; | ||
constructor(set: (value: A) => B, value: A) { | ||
this.set = set; | ||
this.value = value; | ||
} | ||
} | ||
export class Selection { | ||
/** | ||
* A partial lens. | ||
* | ||
* `A` is the type of the value and `B` is the type of the "field" the lens is | ||
* referring to - "field" does not necessarily refer to the property of an | ||
* object, it may also be an array index, or infact anything that there | ||
* is a bijection for. | ||
*/ | ||
export class PLens<A, B> { | ||
value: any; | ||
_data: Collection<any, any>; | ||
_path: any; | ||
_update: any; | ||
private run: (a: A) => Store<B, A>; | ||
constructor(data: Collection<any, any>, path: Array<any>, update) { | ||
this._data = data; | ||
this._path = path; | ||
this._update = update; | ||
this.value = path.length === 0 ? data : data.getIn(path); | ||
constructor(run: (a: A) => Store<B, A>) { | ||
this.run = run; | ||
} | ||
set(value) { | ||
this._update(this._data.setIn(this._path, value)); | ||
compose<C>(that: PLens<C, A>): PLens<C, B> { | ||
return new PLens<C, B>(c => { | ||
var x = that.run(c); | ||
if (x.value == null) return null; | ||
var y = this.run(x.value); | ||
return new Store<B, C>(b => x.set(y.set(b)), y.value); | ||
}); | ||
} | ||
modify(fn) { | ||
this._update(this._data.updateIn(this._path, fn)); | ||
then<C>(that: PLens<B, C>): PLens<A, C> { | ||
return that.compose(this); | ||
} | ||
select(...args) { | ||
return new Selection(this._data, this._path.concat(args), this._update); | ||
get(a: A, or?: B): B { | ||
var s = this.run(a); | ||
return s ? s.value : or; | ||
} | ||
set(a: A, b: B, or?: A): A { | ||
var s = this.run(a); | ||
return s ? s.set(b) : or; | ||
} | ||
modify(a: A, map: (b: B) => B, or?: A): A { | ||
var s = this.run(a); | ||
return s ? s.set(map(s.value)) : or; | ||
} | ||
} | ||
export class Lentil { | ||
/** | ||
* Create a `PLens` from a getter and setter function. The setter should not | ||
* mutate the original object, but instead return a new value with the | ||
* updated field. | ||
*/ | ||
export function plens<A, B>(get: (value: A) => B, set: (value: A, b: B) => A): PLens<A, B> { | ||
return new PLens<A, B>(a => { | ||
return new Store<B, A>(b => set(a, b), get(a)); | ||
}); | ||
} | ||
signal: Z.Signal<Selection>; | ||
_data: Z.Signal<Collection<any, any>>; | ||
_path: any; | ||
_update: any; | ||
/** | ||
* Interface for things that look and (hopefully) behave like an anonymous | ||
* object. | ||
*/ | ||
export interface ObjLike { | ||
[key: string]: any; | ||
} | ||
constructor(data, path, update) { | ||
this._data = data; | ||
this._path = path; | ||
this._update = update; | ||
this.signal = data | ||
.dropRepeats((curr, next) => I.is(curr.getIn(path), next.getIn(path))) | ||
.map(data => new Selection(data, path, update)); | ||
/** | ||
* Shallow-copy an object, used when setting with a `prop`-created `PLens`. | ||
*/ | ||
function clone(obj: ObjLike): ObjLike { | ||
var result: ObjLike = {}; | ||
for (var k in obj) { | ||
if (obj.hasOwnProperty(k)) { | ||
result[k] = obj[k]; | ||
} | ||
} | ||
return result; | ||
} | ||
select(...args) { | ||
return new Lentil(this._data, this._path.concat(args), this._update); | ||
} | ||
/** | ||
* Create a `PLens` for a property of an anonymous object. | ||
*/ | ||
export function prop<A>(name: string): PLens<ObjLike, A> { | ||
return new PLens<ObjLike, A>(obj => { | ||
return new Store<A, ObjLike>(value => { | ||
var result = clone(obj); | ||
result[name] = value; | ||
return result; | ||
}, obj[name]); | ||
}); | ||
} | ||
map(fn) { | ||
var values = []; | ||
var results = []; | ||
return this._data.map(data => { | ||
return data.map((item, i) => { | ||
if (I.is(values[i], data.getIn(this._path.concat([i])))) { | ||
return results[i]; | ||
} else { | ||
values[i] = data.getIn(this._path.concat([i])); | ||
return results[i] = fn(new Selection(data, this._path.concat([i]), this._update), i); | ||
} | ||
}); | ||
}); | ||
} | ||
/** | ||
* Interface for things that look and (hopefully) behave like an array. | ||
*/ | ||
export interface ArrayLike { | ||
[key: number]: any; | ||
slice(): ArrayLike; | ||
} | ||
join(input: Z.Signal<Collection<any, any>>): void { | ||
Z.subscribeN(this._data, input, (data, input) => { | ||
if (!I.is(data.getIn(this._path), input)) { | ||
this._update(data.setIn(this._path, input)); | ||
} | ||
}); | ||
} | ||
/** | ||
* Create a `PLens` for an index of an array. | ||
*/ | ||
export function index<A>(i: number): PLens<ArrayLike, A> { | ||
return new PLens<ArrayLike, A>(arr => { | ||
return new Store<A, ArrayLike>(value => { | ||
var result = arr.slice(); | ||
result[i] = value; | ||
return result; | ||
}, arr[i]); | ||
}); | ||
} | ||
export function create(data, update) { | ||
return new Lentil(data, [], update); | ||
}; | ||
} | ||
export = lentil; |
"use strict"; | ||
var Z = require("zelkova"); | ||
var I = require("immutable"); | ||
var Lentil = require("../dist/lentil"); | ||
@@ -9,73 +7,92 @@ | ||
"The signal": { | ||
"should contain the initial data": function (test) { | ||
test.expect(1); | ||
var data = I.fromJS({ a: "foo", b: "bar" }); | ||
var src = Z.constant(data); | ||
var l = Lentil.create(src, function () {}); | ||
l.signal.subscribe(function (selection) { | ||
test.strictEqual(selection.value, data); | ||
}); | ||
test.done(); | ||
}, | ||
"should not emit equivalent repeat values": function (test) { | ||
test.expect(1); | ||
var data1 = I.fromJS({ a: "foo", b: "bar" }); | ||
var data2 = I.fromJS({ a: "foo", b: "bar" }); | ||
var chan = Z.channel(data1); | ||
var src = chan.signal; | ||
var l = Lentil.create(src, function () {}); | ||
l.signal.subscribe(function (selection) { | ||
test.strictEqual(selection.value, data1); | ||
}); | ||
chan.send(data2); | ||
test.done(); | ||
} | ||
"plens should create a PLens from a getter and setter": function (test) { | ||
test.expect(1); | ||
var lens = Lentil.plens( | ||
function (x) { return x; }, | ||
function (x, y) { return y; } | ||
); | ||
test.ok(lens instanceof Lentil.PLens); | ||
test.done(); | ||
}, | ||
"selecting": { | ||
"should produce a new Lentil for a more specific part of the data": function (test) { | ||
test.expect(2); | ||
var src = Z.constant(I.fromJS({ a: "foo", b: "bar" })); | ||
var l = Lentil.create(src, function () {}); | ||
l.select("a").signal.subscribe(function (selection) { test.equal(selection.value, "foo") }); | ||
l.select("b").signal.subscribe(function (selection) { test.equal(selection.value, "bar") }); | ||
test.done(); | ||
}, | ||
"should allow multi-part paths": function (test) { | ||
test.expect(1); | ||
var src = Z.constant(I.fromJS({ a: "foo", b: { test: ["bar"] } })); | ||
var l = Lentil.create(src, function () {}); | ||
l.select("b", "test", 0).signal.subscribe(function (selection) { | ||
test.equal(selection.value, "bar"); | ||
}); | ||
test.done(); | ||
}, | ||
"should be chainable": function (test) { | ||
test.expect(1); | ||
var src = Z.constant(I.fromJS({ a: "foo", b: { test: ["bar"] } })); | ||
var l = Lentil.create(src, function () {}); | ||
l.select("b").select("test").select(0).signal.subscribe(function (selection) { | ||
test.equal(selection.value, "bar"); | ||
}); | ||
test.done(); | ||
} | ||
"plens(x => x, (x, y) => y) should produce an identity lens": function (test) { | ||
test.expect(2); | ||
var lens = Lentil.plens( | ||
function (x) { return x; }, | ||
function (x, y) { return y; } | ||
); | ||
test.ok(lens.get(true)); | ||
test.ok(lens.set({}, true)); | ||
test.done(); | ||
}, | ||
"joining": { | ||
"should run the update function with new data after patching the original value": function (test) { | ||
test.expect(1); | ||
var data1 = I.fromJS({ a: "foo", b: ["bar"] }); | ||
var data2 = I.fromJS({ a: "foo", b: ["bar", "baz", "fizz"] }); | ||
var src = Z.constant(data1); | ||
var chan = Z.channel(data1.get("b")); | ||
var l = Lentil.create(src, function (value) { | ||
test.ok(I.is(value, data2)) | ||
}); | ||
l.select("b").join(chan.signal); | ||
chan.send(I.fromJS(["bar", "baz", "fizz"])); | ||
test.done(); | ||
} | ||
"lens.modify should accept the old value and return a new value": function (test) { | ||
test.expect(1); | ||
var lens = Lentil.plens( | ||
function (x) { return x; }, | ||
function (x, y) { return y; } | ||
); | ||
var init = {}; | ||
test.ok(lens.modify(init, function (x) { return x === init; })); | ||
test.done(); | ||
}, | ||
"prop should create a PLens for a property of an anonymous object": function (test) { | ||
test.expect(3); | ||
var _x = Lentil.prop("x"); | ||
test.ok(_x.get({ x: true })); | ||
var obj1 = { x: false }; | ||
var obj2 = _x.set(obj1, true); | ||
test.ok(obj1 !== obj2); | ||
test.ok(obj2.x); | ||
test.done(); | ||
}, | ||
"index should create a PLens for an index of an array": function (test) { | ||
test.expect(3); | ||
var _1 = Lentil.index(1); | ||
test.ok(_1.get([false, true, false])); | ||
var arr1 = [false, false, false]; | ||
var arr2 = _1.set(arr1, true); | ||
test.ok(arr1 !== arr2); | ||
test.ok(arr2[1]); | ||
test.done(); | ||
}, | ||
"pl1.compose(pl2) should compose pl1 and pl2": function (test) { | ||
test.expect(2); | ||
var _x = Lentil.prop("x"); | ||
var _y = Lentil.prop("y"); | ||
var _xy = _y.compose(_x); | ||
test.ok(_xy.get({ x: { y: true }})); | ||
test.ok(_xy.set({ x: { y: false }}, true).x.y); | ||
test.done(); | ||
}, | ||
"pl1.then(pl2) should compose pl2 and pl1": function (test) { | ||
test.expect(2); | ||
var _x = Lentil.prop("x"); | ||
var _y = Lentil.prop("y"); | ||
var _xy = _x.then(_y); | ||
test.ok(_xy.get({ x: { y: true }})); | ||
test.ok(_xy.set({ x: { y: false }}, true).x.y); | ||
test.done(); | ||
}, | ||
"set should alter the value of the current lens when the current value is null": function (test) { | ||
test.expect(1); | ||
var _x = Lentil.prop("x"); | ||
test.deepEqual(_x.set({ x: null }, true), { x: true }); | ||
test.done(); | ||
}, | ||
"a composed set should not alter the value of the current lens when the 'previous' value is null": function (test) { | ||
test.expect(1); | ||
var _x = Lentil.prop("x"); | ||
var _y = Lentil.prop("y"); | ||
var _xy = _y.compose(_x); | ||
test.deepEqual(_xy.set({ x: null }, true), undefined); | ||
test.done(); | ||
} | ||
}; |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
0
-100%11478
-20.26%8
-11.11%323
-17.39%1
Infinity%- Removed
- Removed
- Removed
- Removed