From 1d07280a3827ff223ecf0abe6a628caa6af7a423 Mon Sep 17 00:00:00 2001 From: nlepage <19571875+nlepage@users.noreply.github.com> Date: Tue, 29 May 2018 10:28:02 +0200 Subject: [PATCH 01/15] :construction: PoC nav --- packages/immutadot/src/nav/consts.js | 1 + packages/immutadot/src/nav/nav.js | 18 +++++++++++++ packages/immutadot/src/nav/nav.spec.js | 37 ++++++++++++++++++++++++++ packages/immutadot/src/nav/prop.js | 13 +++++++++ 4 files changed, 69 insertions(+) create mode 100644 packages/immutadot/src/nav/consts.js create mode 100644 packages/immutadot/src/nav/nav.js create mode 100644 packages/immutadot/src/nav/nav.spec.js create mode 100644 packages/immutadot/src/nav/prop.js diff --git a/packages/immutadot/src/nav/consts.js b/packages/immutadot/src/nav/consts.js new file mode 100644 index 00000000..2aa4aea7 --- /dev/null +++ b/packages/immutadot/src/nav/consts.js @@ -0,0 +1 @@ +export const NONE = Symbol() diff --git a/packages/immutadot/src/nav/nav.js b/packages/immutadot/src/nav/nav.js new file mode 100644 index 00000000..cf3cc9b0 --- /dev/null +++ b/packages/immutadot/src/nav/nav.js @@ -0,0 +1,18 @@ +import * as types from '@immutadot/parser/consts' +import { NONE } from './consts' +import { prop } from './prop' + +export function nav(path) { + return path.map(toNav).reduceRight((next, nav) => nav(next), finalNav) +} + +function toNav([type, value]) { + switch (type) { + case types.prop: return prop(value) + default: throw TypeError(type) + } +} + +function finalNav(value) { + return (updater = NONE) => updater === NONE ? value : updater(value) +} diff --git a/packages/immutadot/src/nav/nav.spec.js b/packages/immutadot/src/nav/nav.spec.js new file mode 100644 index 00000000..2679e470 --- /dev/null +++ b/packages/immutadot/src/nav/nav.spec.js @@ -0,0 +1,37 @@ +/* eslint-env jest */ +import { immutaTest } from 'test.utils' +import { nav } from './nav' +import { toPath } from '@immutadot/parser' + +describe('nav.nav', () => { + const obj = { nested: { prop: 'foo' } } + const path = 'nested.prop' + + it('should allow to get a nested prop', () => { + expect(nav(toPath(path))(obj)()).toBe('foo') + }) + + it('should allow to set a nested prop', () => { + immutaTest( + obj, + [path], + input => { + const output = nav(toPath(path))(input)(() => 'bar') + expect(output).toEqual({ nested: { prop: 'bar' } }) + return output + }, + ) + }) + + it('should allow to update a nested prop', () => { + immutaTest( + obj, + [path], + input => { + const output = nav(toPath(path))(input)(value => value.toUpperCase()) + expect(output).toEqual({ nested: { prop: 'FOO' } }) + return output + }, + ) + }) +}) diff --git a/packages/immutadot/src/nav/prop.js b/packages/immutadot/src/nav/prop.js new file mode 100644 index 00000000..520ac5ca --- /dev/null +++ b/packages/immutadot/src/nav/prop.js @@ -0,0 +1,13 @@ +import { NONE } from './consts' +import { isNil } from 'util/lang' + +export function prop(key) { + return next => obj => (updater = NONE) => { + const nextValue = isNil(obj) ? next(undefined) : next(obj[key]) + + if (updater === NONE) return nextValue() + + const copy = isNil(obj) ? {} : { ...obj } + return Object.assign(copy, { [key]: nextValue(updater) }) + } +} From 6203a823772f6207340be8df00af1d0912380ff9 Mon Sep 17 00:00:00 2001 From: nlepage <19571875+nlepage@users.noreply.github.com> Date: Tue, 29 May 2018 11:59:47 +0200 Subject: [PATCH 02/15] :construction: Add index navigator --- packages/immutadot/src/nav/_index.js | 19 +++++++++++++++++++ packages/immutadot/src/nav/nav.js | 2 ++ packages/immutadot/src/nav/nav.spec.js | 12 ++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 packages/immutadot/src/nav/_index.js diff --git a/packages/immutadot/src/nav/_index.js b/packages/immutadot/src/nav/_index.js new file mode 100644 index 00000000..c1291946 --- /dev/null +++ b/packages/immutadot/src/nav/_index.js @@ -0,0 +1,19 @@ +import { NONE } from './consts' +import { isNil } from 'util/lang' + +function makeCopy(obj) { + if (isNil(obj)) return [] + return Array.isArray() +} + +export function index(key) { + return next => obj => (updater = NONE) => { + const nextValue = isNil(obj) ? next(undefined) : next(obj[key]) + + if (updater === NONE) return nextValue() + + const copy = makeCopy(obj) + copy[key] = nextValue(updater) + return copy + } +} diff --git a/packages/immutadot/src/nav/nav.js b/packages/immutadot/src/nav/nav.js index cf3cc9b0..0bddf51f 100644 --- a/packages/immutadot/src/nav/nav.js +++ b/packages/immutadot/src/nav/nav.js @@ -1,5 +1,6 @@ import * as types from '@immutadot/parser/consts' import { NONE } from './consts' +import { index } from './_index' import { prop } from './prop' export function nav(path) { @@ -9,6 +10,7 @@ export function nav(path) { function toNav([type, value]) { switch (type) { case types.prop: return prop(value) + case types.index: return index(value) default: throw TypeError(type) } } diff --git a/packages/immutadot/src/nav/nav.spec.js b/packages/immutadot/src/nav/nav.spec.js index 2679e470..8724a394 100644 --- a/packages/immutadot/src/nav/nav.spec.js +++ b/packages/immutadot/src/nav/nav.spec.js @@ -34,4 +34,16 @@ describe('nav.nav', () => { }, ) }) + + it('should create unknown path', () => { + immutaTest( + {}, + ['nested.prop.0', 'nested.prop.1'], + input => { + const output = nav(toPath('nested.prop[1]'))(input)(() => 'foo') + expect(output).toEqual({ nested: { prop: [undefined, 'foo'] } }) + return output + }, + ) + }) }) From 9592a003ac30de8deb6696cd838554e0293e04c3 Mon Sep 17 00:00:00 2001 From: nlepage <19571875+nlepage@users.noreply.github.com> Date: Tue, 29 May 2018 15:04:48 +0200 Subject: [PATCH 03/15] :construction: Add slice navigator --- packages/immutadot/src/nav/_index.js | 14 +++--- packages/immutadot/src/nav/nav.js | 2 + packages/immutadot/src/nav/nav.spec.js | 60 ++++++++++++++++++++------ packages/immutadot/src/nav/prop.js | 10 +++-- packages/immutadot/src/nav/slice.js | 49 +++++++++++++++++++++ 5 files changed, 112 insertions(+), 23 deletions(-) create mode 100644 packages/immutadot/src/nav/slice.js diff --git a/packages/immutadot/src/nav/_index.js b/packages/immutadot/src/nav/_index.js index c1291946..ea1b596a 100644 --- a/packages/immutadot/src/nav/_index.js +++ b/packages/immutadot/src/nav/_index.js @@ -3,17 +3,19 @@ import { isNil } from 'util/lang' function makeCopy(obj) { if (isNil(obj)) return [] - return Array.isArray() + return Array.isArray(obj) ? [...obj] : { ...obj } } export function index(key) { - return next => obj => (updater = NONE) => { + return next => obj => { const nextValue = isNil(obj) ? next(undefined) : next(obj[key]) - if (updater === NONE) return nextValue() + return (updater = NONE) => { + if (updater === NONE) return nextValue() - const copy = makeCopy(obj) - copy[key] = nextValue(updater) - return copy + const copy = makeCopy(obj) + copy[key] = nextValue(updater) + return copy + } } } diff --git a/packages/immutadot/src/nav/nav.js b/packages/immutadot/src/nav/nav.js index 0bddf51f..47eed426 100644 --- a/packages/immutadot/src/nav/nav.js +++ b/packages/immutadot/src/nav/nav.js @@ -2,6 +2,7 @@ import * as types from '@immutadot/parser/consts' import { NONE } from './consts' import { index } from './_index' import { prop } from './prop' +import { slice } from './slice' export function nav(path) { return path.map(toNav).reduceRight((next, nav) => nav(next), finalNav) @@ -11,6 +12,7 @@ function toNav([type, value]) { switch (type) { case types.prop: return prop(value) case types.index: return index(value) + case types.slice: return slice(value) default: throw TypeError(type) } } diff --git a/packages/immutadot/src/nav/nav.spec.js b/packages/immutadot/src/nav/nav.spec.js index 8724a394..1518f41a 100644 --- a/packages/immutadot/src/nav/nav.spec.js +++ b/packages/immutadot/src/nav/nav.spec.js @@ -4,18 +4,15 @@ import { nav } from './nav' import { toPath } from '@immutadot/parser' describe('nav.nav', () => { - const obj = { nested: { prop: 'foo' } } - const path = 'nested.prop' - - it('should allow to get a nested prop', () => { - expect(nav(toPath(path))(obj)()).toBe('foo') + it('should get a nested prop', () => { + expect(nav(toPath('nested.prop'))({ nested: { prop: 'foo' } })()).toBe('foo') }) - it('should allow to set a nested prop', () => { + it('should set a nested prop', () => { immutaTest( - obj, - [path], - input => { + { nested: { prop: 'foo' } }, + ['nested.prop'], + (input, [path]) => { const output = nav(toPath(path))(input)(() => 'bar') expect(output).toEqual({ nested: { prop: 'bar' } }) return output @@ -23,11 +20,11 @@ describe('nav.nav', () => { ) }) - it('should allow to update a nested prop', () => { + it('should update a nested prop', () => { immutaTest( - obj, - [path], - input => { + { nested: { prop: 'foo' } }, + ['nested.prop'], + (input, [path]) => { const output = nav(toPath(path))(input)(value => value.toUpperCase()) expect(output).toEqual({ nested: { prop: 'FOO' } }) return output @@ -46,4 +43,41 @@ describe('nav.nav', () => { }, ) }) + + it('should get a slice', () => { + expect(nav(toPath('nested.prop[:].val'))({ + nested: { + prop: [ + { val: 'foo' }, + { val: 'bar' }, + ], + }, + })()).toEqual(['foo', 'bar']) + }) + + it('should update a slice', () => immutaTest( + { + nested: { + prop: [ + { val: 'foo' }, + { val: 'bar' }, + { val: 'baz' }, + ], + }, + }, + ['nested.prop.1.val', 'nested.prop.2.val'], + input => { + const output = nav(toPath('nested.prop[-2:].val'))(input)(value => value.toUpperCase()) + expect(output).toEqual({ + nested: { + prop: [ + { val: 'foo' }, + { val: 'BAR' }, + { val: 'BAZ' }, + ], + }, + }) + return output + }, + )) }) diff --git a/packages/immutadot/src/nav/prop.js b/packages/immutadot/src/nav/prop.js index 520ac5ca..24aa7b95 100644 --- a/packages/immutadot/src/nav/prop.js +++ b/packages/immutadot/src/nav/prop.js @@ -2,12 +2,14 @@ import { NONE } from './consts' import { isNil } from 'util/lang' export function prop(key) { - return next => obj => (updater = NONE) => { + return next => obj => { const nextValue = isNil(obj) ? next(undefined) : next(obj[key]) - if (updater === NONE) return nextValue() + return (updater = NONE) => { + if (updater === NONE) return nextValue() - const copy = isNil(obj) ? {} : { ...obj } - return Object.assign(copy, { [key]: nextValue(updater) }) + const copy = isNil(obj) ? {} : { ...obj } + return Object.assign(copy, { [key]: nextValue(updater) }) + } } } diff --git a/packages/immutadot/src/nav/slice.js b/packages/immutadot/src/nav/slice.js new file mode 100644 index 00000000..a1e0ae6a --- /dev/null +++ b/packages/immutadot/src/nav/slice.js @@ -0,0 +1,49 @@ +import { + isNil, + length, +} from 'util/lang' +import { NONE } from './consts' + +function getSliceBound(value, length) { + if (value < 0) return Math.max(length + value, 0) + return value +} + +function getSliceBounds([start, end], length) { + return [ + getSliceBound(start, length), + getSliceBound(end === undefined ? length : end, length), + ] +} + +// FIXME mutualize with index (same file array.js ?) +function makeCopy(obj) { + if (isNil(obj)) return [] + return Array.isArray(obj) ? [...obj] : { ...obj } +} + +export function slice(bounds) { + return next => obj => { + let nextValue + + if (isNil(obj)) { + nextValue = () => [] + } else { + const [start, end] = getSliceBounds(bounds, length(obj)) + + const nextValues = Array.from(function* () { + for (let i = start; i < end; i++) yield [i, next(obj[i])] + }()) + + nextValue = updater => nextValues.map(([i, nextIndex]) => [i, nextIndex(updater)]) + } + + return (updater = NONE) => { + if (updater === NONE) return nextValue().map(([, value]) => value) + + const copy = makeCopy(obj) + for (const [i, value] of nextValue(updater)) copy[i] = value + return copy + } + } +} From 52f80e85d9776288f26fda81b2efa38a2503c002 Mon Sep 17 00:00:00 2001 From: nlepage <19571875+nlepage@users.noreply.github.com> Date: Tue, 29 May 2018 15:15:05 +0200 Subject: [PATCH 04/15] :recycle: Reorganize code --- packages/immutadot/src/nav/_index.js | 21 ------------- .../immutadot/src/nav/{slice.js => array.js} | 30 ++++++++++++------- packages/immutadot/src/nav/nav.js | 5 ++-- .../immutadot/src/nav/{prop.js => object.js} | 0 4 files changed, 22 insertions(+), 34 deletions(-) delete mode 100644 packages/immutadot/src/nav/_index.js rename packages/immutadot/src/nav/{slice.js => array.js} (75%) rename packages/immutadot/src/nav/{prop.js => object.js} (100%) diff --git a/packages/immutadot/src/nav/_index.js b/packages/immutadot/src/nav/_index.js deleted file mode 100644 index ea1b596a..00000000 --- a/packages/immutadot/src/nav/_index.js +++ /dev/null @@ -1,21 +0,0 @@ -import { NONE } from './consts' -import { isNil } from 'util/lang' - -function makeCopy(obj) { - if (isNil(obj)) return [] - return Array.isArray(obj) ? [...obj] : { ...obj } -} - -export function index(key) { - return next => obj => { - const nextValue = isNil(obj) ? next(undefined) : next(obj[key]) - - return (updater = NONE) => { - if (updater === NONE) return nextValue() - - const copy = makeCopy(obj) - copy[key] = nextValue(updater) - return copy - } - } -} diff --git a/packages/immutadot/src/nav/slice.js b/packages/immutadot/src/nav/array.js similarity index 75% rename from packages/immutadot/src/nav/slice.js rename to packages/immutadot/src/nav/array.js index a1e0ae6a..9988afea 100644 --- a/packages/immutadot/src/nav/slice.js +++ b/packages/immutadot/src/nav/array.js @@ -1,9 +1,25 @@ -import { - isNil, - length, -} from 'util/lang' +import { isNil, length } from 'util/lang' import { NONE } from './consts' +function makeCopy(obj) { + if (isNil(obj)) return [] + return Array.isArray(obj) ? [...obj] : { ...obj } +} + +export function index(key) { + return next => obj => { + const nextValue = isNil(obj) ? next(undefined) : next(obj[key]) + + return (updater = NONE) => { + if (updater === NONE) return nextValue() + + const copy = makeCopy(obj) + copy[key] = nextValue(updater) + return copy + } + } +} + function getSliceBound(value, length) { if (value < 0) return Math.max(length + value, 0) return value @@ -16,12 +32,6 @@ function getSliceBounds([start, end], length) { ] } -// FIXME mutualize with index (same file array.js ?) -function makeCopy(obj) { - if (isNil(obj)) return [] - return Array.isArray(obj) ? [...obj] : { ...obj } -} - export function slice(bounds) { return next => obj => { let nextValue diff --git a/packages/immutadot/src/nav/nav.js b/packages/immutadot/src/nav/nav.js index 47eed426..ddb4eabe 100644 --- a/packages/immutadot/src/nav/nav.js +++ b/packages/immutadot/src/nav/nav.js @@ -1,8 +1,7 @@ import * as types from '@immutadot/parser/consts' +import { index, slice } from './array' import { NONE } from './consts' -import { index } from './_index' -import { prop } from './prop' -import { slice } from './slice' +import { prop } from './object' export function nav(path) { return path.map(toNav).reduceRight((next, nav) => nav(next), finalNav) diff --git a/packages/immutadot/src/nav/prop.js b/packages/immutadot/src/nav/object.js similarity index 100% rename from packages/immutadot/src/nav/prop.js rename to packages/immutadot/src/nav/object.js From 2e406869c4bbab772cbfd17ae5973092b1eecc02 Mon Sep 17 00:00:00 2001 From: nlepage <19571875+nlepage@users.noreply.github.com> Date: Tue, 29 May 2018 16:25:00 +0200 Subject: [PATCH 05/15] :recycle: Move apart get and update logic and use ES6 classes --- packages/immutadot/src/nav/array.js | 109 ++++++++++++++++--------- packages/immutadot/src/nav/consts.js | 1 - packages/immutadot/src/nav/nav.js | 29 +++++-- packages/immutadot/src/nav/nav.spec.js | 12 +-- packages/immutadot/src/nav/object.js | 34 ++++++-- 5 files changed, 121 insertions(+), 64 deletions(-) delete mode 100644 packages/immutadot/src/nav/consts.js diff --git a/packages/immutadot/src/nav/array.js b/packages/immutadot/src/nav/array.js index 9988afea..fa0005e8 100644 --- a/packages/immutadot/src/nav/array.js +++ b/packages/immutadot/src/nav/array.js @@ -1,59 +1,88 @@ import { isNil, length } from 'util/lang' -import { NONE } from './consts' -function makeCopy(obj) { - if (isNil(obj)) return [] - return Array.isArray(obj) ? [...obj] : { ...obj } +class ArrayNav { + constructor(obj, next) { + this.obj = obj + this.next = next + } + + copy() { + if (isNil(this.obj)) return [] + return Array.isArray(this.obj) ? [...this.obj] : { ...this.obj } + } } -export function index(key) { - return next => obj => { - const nextValue = isNil(obj) ? next(undefined) : next(obj[key]) +class IndexNav extends ArrayNav { + constructor(obj, index, next) { + super(obj, next) + this.index = index + } - return (updater = NONE) => { - if (updater === NONE) return nextValue() + get nextValue() { + return isNil(this.obj) ? this.next(undefined) : this.next(this.obj[this.index]) + } - const copy = makeCopy(obj) - copy[key] = nextValue(updater) - return copy - } + get() { + return this.nextValue.get() } -} -function getSliceBound(value, length) { - if (value < 0) return Math.max(length + value, 0) - return value + update(updater) { + const copy = this.copy() + copy[this.index] = this.nextValue.update(updater) + return copy + } } -function getSliceBounds([start, end], length) { - return [ - getSliceBound(start, length), - getSliceBound(end === undefined ? length : end, length), - ] +export function indexNav(index) { + return next => obj => new IndexNav(obj, index, next) } -export function slice(bounds) { - return next => obj => { - let nextValue +class SliceNav extends ArrayNav { + constructor(obj, bounds, next) { + super(obj, next) + this.bounds = bounds + } - if (isNil(obj)) { - nextValue = () => [] - } else { - const [start, end] = getSliceBounds(bounds, length(obj)) + get length() { + if (this._length === undefined) this._length = length(this.obj) + return this._length + } - const nextValues = Array.from(function* () { - for (let i = start; i < end; i++) yield [i, next(obj[i])] - }()) + bound(index) { + if (index < 0) return Math.max(this.length + index, 0) + return index + } - nextValue = updater => nextValues.map(([i, nextIndex]) => [i, nextIndex(updater)]) - } + get start() { + return this.bound(this.bounds[0]) + } - return (updater = NONE) => { - if (updater === NONE) return nextValue().map(([, value]) => value) + get end() { + const [, end] = this.bounds + return this.bound(end === undefined ? this.length : end) + } - const copy = makeCopy(obj) - for (const [i, value] of nextValue(updater)) copy[i] = value - return copy - } + get range() { + const { start, end } = this + return (function*() { + for (let i = start; i < end; i++) yield i + }()) } + + get() { + if (isNil(this.obj)) return [] + return Array.from(this.range, index => this.next(this.obj[index]).get()) + } + + update(updater) { + if (isNil(this.obj)) return [] + + const copy = this.copy() + for (const index of this.range) copy[index] = this.next(this.obj[index]).update(updater) + return copy + } +} + +export function sliceNav(bounds) { + return next => obj => new SliceNav(obj, bounds, next) } diff --git a/packages/immutadot/src/nav/consts.js b/packages/immutadot/src/nav/consts.js deleted file mode 100644 index 2aa4aea7..00000000 --- a/packages/immutadot/src/nav/consts.js +++ /dev/null @@ -1 +0,0 @@ -export const NONE = Symbol() diff --git a/packages/immutadot/src/nav/nav.js b/packages/immutadot/src/nav/nav.js index ddb4eabe..784f05d6 100644 --- a/packages/immutadot/src/nav/nav.js +++ b/packages/immutadot/src/nav/nav.js @@ -1,7 +1,6 @@ -import * as types from '@immutadot/parser/consts' -import { index, slice } from './array' -import { NONE } from './consts' -import { prop } from './object' +import { index, prop, slice } from '@immutadot/parser/consts' +import { indexNav, sliceNav } from './array' +import { propNav } from './object' export function nav(path) { return path.map(toNav).reduceRight((next, nav) => nav(next), finalNav) @@ -9,13 +8,27 @@ export function nav(path) { function toNav([type, value]) { switch (type) { - case types.prop: return prop(value) - case types.index: return index(value) - case types.slice: return slice(value) + case prop: return propNav(value) + case index: return indexNav(value) + case slice: return sliceNav(value) default: throw TypeError(type) } } +class FinalNav { + constructor(value) { + this.value = value + } + + get() { + return this.value + } + + update(updater) { + return updater(this.value) + } +} + function finalNav(value) { - return (updater = NONE) => updater === NONE ? value : updater(value) + return new FinalNav(value) } diff --git a/packages/immutadot/src/nav/nav.spec.js b/packages/immutadot/src/nav/nav.spec.js index 1518f41a..33125f16 100644 --- a/packages/immutadot/src/nav/nav.spec.js +++ b/packages/immutadot/src/nav/nav.spec.js @@ -5,7 +5,7 @@ import { toPath } from '@immutadot/parser' describe('nav.nav', () => { it('should get a nested prop', () => { - expect(nav(toPath('nested.prop'))({ nested: { prop: 'foo' } })()).toBe('foo') + expect(nav(toPath('nested.prop'))({ nested: { prop: 'foo' } }).get()).toBe('foo') }) it('should set a nested prop', () => { @@ -13,7 +13,7 @@ describe('nav.nav', () => { { nested: { prop: 'foo' } }, ['nested.prop'], (input, [path]) => { - const output = nav(toPath(path))(input)(() => 'bar') + const output = nav(toPath(path))(input).update(() => 'bar') expect(output).toEqual({ nested: { prop: 'bar' } }) return output }, @@ -25,7 +25,7 @@ describe('nav.nav', () => { { nested: { prop: 'foo' } }, ['nested.prop'], (input, [path]) => { - const output = nav(toPath(path))(input)(value => value.toUpperCase()) + const output = nav(toPath(path))(input).update(value => value.toUpperCase()) expect(output).toEqual({ nested: { prop: 'FOO' } }) return output }, @@ -37,7 +37,7 @@ describe('nav.nav', () => { {}, ['nested.prop.0', 'nested.prop.1'], input => { - const output = nav(toPath('nested.prop[1]'))(input)(() => 'foo') + const output = nav(toPath('nested.prop[1]'))(input).update(() => 'foo') expect(output).toEqual({ nested: { prop: [undefined, 'foo'] } }) return output }, @@ -52,7 +52,7 @@ describe('nav.nav', () => { { val: 'bar' }, ], }, - })()).toEqual(['foo', 'bar']) + }).get()).toEqual(['foo', 'bar']) }) it('should update a slice', () => immutaTest( @@ -67,7 +67,7 @@ describe('nav.nav', () => { }, ['nested.prop.1.val', 'nested.prop.2.val'], input => { - const output = nav(toPath('nested.prop[-2:].val'))(input)(value => value.toUpperCase()) + const output = nav(toPath('nested.prop[-2:].val'))(input).update(value => value.toUpperCase()) expect(output).toEqual({ nested: { prop: [ diff --git a/packages/immutadot/src/nav/object.js b/packages/immutadot/src/nav/object.js index 24aa7b95..ebb1829c 100644 --- a/packages/immutadot/src/nav/object.js +++ b/packages/immutadot/src/nav/object.js @@ -1,15 +1,31 @@ -import { NONE } from './consts' import { isNil } from 'util/lang' -export function prop(key) { - return next => obj => { - const nextValue = isNil(obj) ? next(undefined) : next(obj[key]) +class PropNav { + constructor(obj, key, next) { + this.obj = obj + this.key = key + this.next = next + } + + get nextValue() { + return isNil(this.obj) ? this.next(undefined) : this.next(this.obj[this.key]) + } + + get() { + return this.nextValue.get() + } - return (updater = NONE) => { - if (updater === NONE) return nextValue() + copy() { + return isNil(this.obj) ? {} : { ...this.obj } + } - const copy = isNil(obj) ? {} : { ...obj } - return Object.assign(copy, { [key]: nextValue(updater) }) - } + update(updater) { + const copy = this.copy() + copy[this.key] = this.nextValue.update(updater) + return copy } } + +export function propNav(key) { + return next => obj => new PropNav(obj, key, next) +} From 0960f54786216ac812efdf107e7426c34a5c7b48 Mon Sep 17 00:00:00 2001 From: nlepage <19571875+nlepage@users.noreply.github.com> Date: Tue, 29 May 2018 16:29:36 +0200 Subject: [PATCH 06/15] :fire: Remove unused currification level --- packages/immutadot/src/nav/array.js | 8 ++++---- packages/immutadot/src/nav/nav.js | 10 +++++----- packages/immutadot/src/nav/object.js | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/immutadot/src/nav/array.js b/packages/immutadot/src/nav/array.js index fa0005e8..4f45d25c 100644 --- a/packages/immutadot/src/nav/array.js +++ b/packages/immutadot/src/nav/array.js @@ -33,8 +33,8 @@ class IndexNav extends ArrayNav { } } -export function indexNav(index) { - return next => obj => new IndexNav(obj, index, next) +export function indexNav(index, next) { + return obj => new IndexNav(obj, index, next) } class SliceNav extends ArrayNav { @@ -83,6 +83,6 @@ class SliceNav extends ArrayNav { } } -export function sliceNav(bounds) { - return next => obj => new SliceNav(obj, bounds, next) +export function sliceNav(bounds, next) { + return obj => new SliceNav(obj, bounds, next) } diff --git a/packages/immutadot/src/nav/nav.js b/packages/immutadot/src/nav/nav.js index 784f05d6..9e3a5588 100644 --- a/packages/immutadot/src/nav/nav.js +++ b/packages/immutadot/src/nav/nav.js @@ -3,14 +3,14 @@ import { indexNav, sliceNav } from './array' import { propNav } from './object' export function nav(path) { - return path.map(toNav).reduceRight((next, nav) => nav(next), finalNav) + return path.reduceRight((next, [type, value]) => toNav(type)(value, next), finalNav) } -function toNav([type, value]) { +function toNav(type) { switch (type) { - case prop: return propNav(value) - case index: return indexNav(value) - case slice: return sliceNav(value) + case prop: return propNav + case index: return indexNav + case slice: return sliceNav default: throw TypeError(type) } } diff --git a/packages/immutadot/src/nav/object.js b/packages/immutadot/src/nav/object.js index ebb1829c..e4c968b3 100644 --- a/packages/immutadot/src/nav/object.js +++ b/packages/immutadot/src/nav/object.js @@ -26,6 +26,6 @@ class PropNav { } } -export function propNav(key) { - return next => obj => new PropNav(obj, key, next) +export function propNav(key, next) { + return obj => new PropNav(obj, key, next) } From 4a8578aabf7ec96b5e3b72592f4f69ff026df416 Mon Sep 17 00:00:00 2001 From: nlepage <19571875+nlepage@users.noreply.github.com> Date: Tue, 29 May 2018 22:19:39 +0200 Subject: [PATCH 07/15] :zap: Use nav in set and validate performance improvement with benchmark --- packages/immutadot-benchmark/package.json | 2 +- .../src/updateTodos.spec.js | 26 +++---------------- packages/immutadot/src/core/set.js | 10 +++---- yarn.lock | 6 ----- 4 files changed, 9 insertions(+), 35 deletions(-) diff --git a/packages/immutadot-benchmark/package.json b/packages/immutadot-benchmark/package.json index 815c80dc..7956a8f7 100644 --- a/packages/immutadot-benchmark/package.json +++ b/packages/immutadot-benchmark/package.json @@ -7,7 +7,7 @@ "cross-env": "~5.1.6", "immer": "~1.3.1", "immutable": "~3.8.2", - "immutadot": "~1.0.0", + "immutadot": "~2.0.0", "jest": "~21.2.1", "lerna": "~2.11.0", "qim": "~0.0.52" diff --git a/packages/immutadot-benchmark/src/updateTodos.spec.js b/packages/immutadot-benchmark/src/updateTodos.spec.js index ae010cb1..212062d4 100644 --- a/packages/immutadot-benchmark/src/updateTodos.spec.js +++ b/packages/immutadot-benchmark/src/updateTodos.spec.js @@ -3,7 +3,7 @@ import { $each, $slice, set as qimSet } from 'qim' import { List, Record } from 'immutable' -import immer, { setAutoFreeze, setUseProxies } from 'immer' +import immer, { setAutoFreeze } from 'immer' import { createBenchmark } from './benchmark' @@ -70,7 +70,7 @@ function updateTodosList(title, listSize, modifySize, maxTime, maxOperations) { }) }) - it('immutable w/o conversion', () => { + it('immutable', () => { benchmark('immutable', 'immutable 3.8.2 (w/o conversion to plain JS objects)', () => { const [start, end] = randomBounds() immutableState.withMutations(state => { @@ -79,15 +79,6 @@ function updateTodosList(title, listSize, modifySize, maxTime, maxOperations) { }) }) - it('immutable w/ conversion', () => { - benchmark('immutable-toJS', 'immutable 3.8.2 (w/ conversion to plain JS objects)', () => { - const [start, end] = randomBounds() - return immutableState.withMutations(state => { - for (let i = start; i < end; i++) state.setIn([i, 'done'], true) - }).toJS() - }) - }) - it('immer proxy', () => { benchmark('immer-proxy', 'immer 1.2.0 (proxy implementation w/o autofreeze)', () => { const [start, end] = randomBounds() @@ -97,17 +88,6 @@ function updateTodosList(title, listSize, modifySize, maxTime, maxOperations) { }) }) - it('immer ES5', () => { - setUseProxies(false) - benchmark('immer-es5', 'immer 1.2.0 (ES5 implementation w/o autofreeze)', () => { - const [start, end] = randomBounds() - return immer(baseState, draft => { - for (let i = start; i < end; i++) draft[i].done = true - }) - }) - setUseProxies(true) - }) - it('qim', () => { benchmark('qim', 'qim 0.0.52', () => { const [start, end] = randomBounds() @@ -116,7 +96,7 @@ function updateTodosList(title, listSize, modifySize, maxTime, maxOperations) { }) it('immutad●t', () => { - benchmark('immutadot', 'immutad●t 1.0.0', () => { + benchmark('immutadot', 'immutad●t 2.0.0', () => { const [start, end] = randomBounds() return set(baseState, `[${start}:${end}].done`, true) }) diff --git a/packages/immutadot/src/core/set.js b/packages/immutadot/src/core/set.js index f74ef9bc..2a1a0f4c 100644 --- a/packages/immutadot/src/core/set.js +++ b/packages/immutadot/src/core/set.js @@ -1,10 +1,8 @@ -import { apply } from 'path/apply' - -const setOperation = (obj, prop, _, value) => { obj[prop] = value } +import { nav } from 'nav/nav' +import { toPath } from '@immutadot/parser' /** * Sets the value at path of obj. - * @function * @memberof core * @param {*} obj The object to modify. * @param {string|Array} path The path of the property to set. @@ -13,6 +11,8 @@ const setOperation = (obj, prop, _, value) => { obj[prop] = value } * @example set({ nested: { prop: 'old' } }, 'nested.prop', 'new') // => { nested: { prop: 'new' } } * @since 1.0.0 */ -const set = apply(setOperation) +function set(obj, path, value) { + return nav(toPath(path))(obj).update(() => value) +} export { set } diff --git a/yarn.lock b/yarn.lock index 68f8bb21..028d61d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2557,12 +2557,6 @@ immutable@~3.8.2: version "3.8.2" resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" -immutadot@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/immutadot/-/immutadot-1.0.0.tgz#9d99bffde37666755aff0b6f9cb7df8e5a4b4231" - dependencies: - babel-runtime "^6.26.0" - imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" From b8e0c6e90a048468fee16b813a96225acd2ea2e8 Mon Sep 17 00:00:00 2001 From: nlepage <19571875+nlepage@users.noreply.github.com> Date: Wed, 30 May 2018 12:17:35 +0200 Subject: [PATCH 08/15] :sparkles: Put back negative array index support --- packages/immutadot/src/nav/array.js | 22 +++++++++++++++------- packages/immutadot/src/nav/nav.spec.js | 4 ++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/immutadot/src/nav/array.js b/packages/immutadot/src/nav/array.js index 4f45d25c..3c0a74b3 100644 --- a/packages/immutadot/src/nav/array.js +++ b/packages/immutadot/src/nav/array.js @@ -6,6 +6,11 @@ class ArrayNav { this.next = next } + get length() { + if (this._length === undefined) this._length = length(this.obj) + return this._length + } + copy() { if (isNil(this.obj)) return [] return Array.isArray(this.obj) ? [...this.obj] : { ...this.obj } @@ -15,11 +20,19 @@ class ArrayNav { class IndexNav extends ArrayNav { constructor(obj, index, next) { super(obj, next) - this.index = index + this._index = index + } + + get index() { + const { _index, length } = this + if (_index >= 0) return _index + if (-_index > length) return undefined + return Math.max(length + _index, 0) } get nextValue() { - return isNil(this.obj) ? this.next(undefined) : this.next(this.obj[this.index]) + const { index, obj } = this + return (isNil(obj) || index === undefined) ? this.next(undefined) : this.next(obj[index]) } get() { @@ -43,11 +56,6 @@ class SliceNav extends ArrayNav { this.bounds = bounds } - get length() { - if (this._length === undefined) this._length = length(this.obj) - return this._length - } - bound(index) { if (index < 0) return Math.max(this.length + index, 0) return index diff --git a/packages/immutadot/src/nav/nav.spec.js b/packages/immutadot/src/nav/nav.spec.js index 33125f16..0530966f 100644 --- a/packages/immutadot/src/nav/nav.spec.js +++ b/packages/immutadot/src/nav/nav.spec.js @@ -55,6 +55,10 @@ describe('nav.nav', () => { }).get()).toEqual(['foo', 'bar']) }) + it('should get a negative array index', () => { + expect(nav(toPath('nested.prop[-3]'))({ nested: { prop: [0, 1, 2, 3, 4] } }).get()).toBe(2) + }) + it('should update a slice', () => immutaTest( { nested: { From 719fd70d8335ca94cbf30289da92439eea383fce Mon Sep 17 00:00:00 2001 From: nlepage <19571875+nlepage@users.noreply.github.com> Date: Wed, 30 May 2018 12:36:35 +0200 Subject: [PATCH 09/15] :white_check_mark: Report apply tests to nav --- packages/immutadot/src/nav/nav.spec.js | 340 +++++++++++++++++++++++++ 1 file changed, 340 insertions(+) diff --git a/packages/immutadot/src/nav/nav.spec.js b/packages/immutadot/src/nav/nav.spec.js index 0530966f..ba65e484 100644 --- a/packages/immutadot/src/nav/nav.spec.js +++ b/packages/immutadot/src/nav/nav.spec.js @@ -1,9 +1,349 @@ /* eslint-env jest */ +import { get } from 'core' import { immutaTest } from 'test.utils' +import { isString } from 'util/lang' import { nav } from './nav' import { toPath } from '@immutadot/parser' describe('nav.nav', () => { + function incV(v, i = 1) { + let r = Number(v) + if (Number.isNaN(r)) + r = 0 + return r + i + } + + function uncurriedInc(obj, path, ...args) { + return nav(toPath(path))(obj).update(v => incV(v, ...args)) + } + + function curriedInc(path, ...args) { + return function(obj) { + return uncurriedInc(obj, path, ...args) + } + } + + function inc(...args) { + const [firstArg, ...argsRest] = args + if (isString(firstArg)) return curriedInc(...args) + return uncurriedInc(firstArg, ...argsRest) + } + + it('should inc element at negative position in array', () => { + immutaTest({ nested: { prop: [0, 1, 2, 3] } }, + ['nested.prop.3'], + input => { + const output = inc(input, 'nested.prop[-1]', 1) + expect(output).toEqual({ nested: { prop: [0, 1, 2, 4] } }) + return output + }) + }) + + it.skip('should do nothing for out of bounds negative array index', () => { + immutaTest({ nested: { prop: [0, 1, 2, 3] } }, + [], + input => { + const output = inc(input, 'nested.prop[-5]', 1) + expect(output).toEqual({ nested: { prop: [0, 1, 2, 3] } }) + return output + }) + }) + + it('should inc in an array slice', () => { + immutaTest({ + nested: { + prop: [{ + val: 4, + other: {}, + }, + { val: -8 }, + { val: 'a' }, + {}, + ], + }, + }, [ + 'nested.prop.0.val', + 'nested.prop.1.val', + 'nested.prop.2.val', + 'nested.prop.3.val', + ], input => { + const output = inc(input, 'nested.prop[:].val', 2) + expect(output).toEqual({ + nested: { + prop: [{ + val: 6, + other: {}, + }, + { val: -6 }, + { val: 2 }, + { val: 2 }, + ], + }, + }) + return output + }) + immutaTest({ + nested: { + prop: [{ val: 0 }, + { + val: 1, + other: {}, + }, + { val: 2 }, + { val: 3 }, + ], + }, + other: {}, + }, [ + 'nested.prop.1.val', + 'nested.prop.2.val', + ], input => { + const output = inc(input, 'nested.prop[1:3].val') + expect(output).toEqual({ + nested: { + prop: [{ val: 0 }, + { + val: 2, + other: {}, + }, + { val: 3 }, + { val: 3 }, + ], + }, + other: {}, + }) + return output + }) + immutaTest({ + nested: { + prop: [{ val: 0 }, + { + val: 1, + other: {}, + }, + { val: 2 }, + { val: 3 }, + ], + }, + other: {}, + }, [ + 'nested.prop.1.val', + 'nested.prop.2.val', + ], input => { + const output = inc(input, 'nested.prop[-3:-1].val') + expect(output).toEqual({ + nested: { + prop: [{ val: 0 }, + { + val: 2, + other: {}, + }, + { val: 3 }, + { val: 3 }, + ], + }, + other: {}, + }) + return output + }) + immutaTest({ + nested: { + prop: [{ val: 0 }, + { val: 1 }, + ], + }, + other: {}, + }, [ + 'nested.prop.2', + 'nested.prop.3.val', + 'nested.prop.4.val', + ], input => { + const output = inc(input, 'nested.prop[3:5].val', 6) + expect(output).toEqual({ + nested: { + prop: [{ val: 0 }, + { val: 1 }, + undefined, + { val: 6 }, + { val: 6 }, + ], + }, + other: {}, + }) + return output + }) + }) + + it.skip('should avoid unnecessary copies with slice operator', () => { + immutaTest({ + nested: { + prop: [{ val: 0 }, + { val: 1 }, + ], + }, + other: {}, + }, [], input => inc(input, 'nested.prop[0:0].val', 6)) + immutaTest({ + nested: { + prop: [{ + arr: [{ val: 0 }, + { val: 1 }, + ], + }, + { arr: [{ val: 2 }] }, + ], + }, + other: {}, + }, [], input => inc(input, 'nested.prop[:].arr[0:0].val', 6)) + immutaTest({ + nested: { + prop: [{ + arr: [{ val: 0 }, + { val: 1 }, + ], + }, + { arr: [{ val: 2 }] }, + ], + }, + other: {}, + }, ['nested.prop.0.arr.1.val'], input => { + const output = inc(input, 'nested.prop[:].arr[1:].val', 6) + expect(output).toEqual({ + nested: { + prop: [{ + arr: [{ val: 0 }, + { val: 7 }, + ], + }, + { arr: [{ val: 2 }] }, + ], + }, + other: {}, + }) + return output + }) + }) + + it('should inc in a list of props', () => { + immutaTest({ + nested: { + 'prop1': { val: 0 }, + 'prop2': { val: 5 }, + 'prop{3}': { val: 5 }, + '"prop4"': { val: 3 }, + 'prop5': { val: 5 }, + }, + other: {}, + }, [ + 'nested.prop1.val', + 'nested.prop2.val', + 'nested.prop{3}.val', + 'nested."prop4".val', + ], input => { + const output = inc(input, 'nested.{prop1,prop2,"prop{3}",\'"prop4"\'}.val') + expect(output).toEqual({ + nested: { + 'prop1': { val: 1 }, + 'prop2': { val: 6 }, + 'prop{3}': { val: 6 }, + '"prop4"': { val: 4 }, + 'prop5': { val: 5 }, + }, + other: {}, + }) + return output + }) + }) + + it('should inc in all props', () => { + immutaTest({ + nested: { + 'prop1': { val: 0 }, + 'prop2': { val: 5 }, + 'prop{3}': { val: 5 }, + '"prop4"': { val: 3 }, + }, + other: {}, + }, [ + 'nested.prop1.val', + 'nested.prop2.val', + 'nested.prop{3}.val', + 'nested."prop4".val', + ], input => { + const output = inc(input, 'nested.{*}.val') + expect(output).toEqual({ + nested: { + 'prop1': { val: 1 }, + 'prop2': { val: 6 }, + 'prop{3}': { val: 6 }, + '"prop4"': { val: 4 }, + }, + other: {}, + }) + return output + }) + }) + + it('should throw an explicit error when en empty path is given as parameter', () => { + expect(() => inc({}, '')).toThrowError('path should not be empty') + }) + + it('should support curried first arg', () => { + immutaTest({ + nested: { prop: 5 }, + other: {}, + }, ['nested.prop'], (input, [path]) => { + const output = inc(path)(input, { shouldBeDiscarded: true }) + expect(output).toEqual({ + nested: { prop: 6 }, + other: {}, + }) + return output + }) + }) + + it('should initialize unknown props in a list', () => { + immutaTest({ + nested: { prop1: { val: 5 } }, + other: {}, + }, [ + 'nested.prop1.val', + 'nested.prop2.val', + ], input => { + const output = inc(input, 'nested.{prop1,prop2}.val') + expect(output).toEqual({ + nested: { + prop1: { val: 6 }, + prop2: { val: 1 }, + }, + other: {}, + }) + return output + }) + }) + + it.skip('should support lazy function args', () => { + immutaTest({ + nested: { + prop1: { val: 3 }, + prop2: { val: 4 }, + }, + other: {}, + }, + ['nested.prop1.val'], + input => { + const output = inc(input, 'nested.prop1.val', get('nested.prop2.val')) + expect(output).toEqual({ + nested: { + prop1: { val: 7 }, + prop2: { val: 4 }, + }, + other: {}, + }) + return output + }) + }) + it('should get a nested prop', () => { expect(nav(toPath('nested.prop'))({ nested: { prop: 'foo' } }).get()).toBe('foo') }) From 410cec96f45a45d55c337e09df1ec759fc04280d Mon Sep 17 00:00:00 2001 From: nlepage <19571875+nlepage@users.noreply.github.com> Date: Wed, 30 May 2018 16:07:57 +0200 Subject: [PATCH 10/15] :truck: Move each navigator in its own file as suggested by @frinyvonnick --- packages/immutadot/src/nav/array.js | 96 ------------------- packages/immutadot/src/nav/arrayNav.js | 18 ++++ packages/immutadot/src/nav/indexNav.js | 35 +++++++ packages/immutadot/src/nav/nav.js | 5 +- .../src/nav/{object.js => propNav.js} | 0 packages/immutadot/src/nav/sliceNav.js | 47 +++++++++ 6 files changed, 103 insertions(+), 98 deletions(-) delete mode 100644 packages/immutadot/src/nav/array.js create mode 100644 packages/immutadot/src/nav/arrayNav.js create mode 100644 packages/immutadot/src/nav/indexNav.js rename packages/immutadot/src/nav/{object.js => propNav.js} (100%) create mode 100644 packages/immutadot/src/nav/sliceNav.js diff --git a/packages/immutadot/src/nav/array.js b/packages/immutadot/src/nav/array.js deleted file mode 100644 index 3c0a74b3..00000000 --- a/packages/immutadot/src/nav/array.js +++ /dev/null @@ -1,96 +0,0 @@ -import { isNil, length } from 'util/lang' - -class ArrayNav { - constructor(obj, next) { - this.obj = obj - this.next = next - } - - get length() { - if (this._length === undefined) this._length = length(this.obj) - return this._length - } - - copy() { - if (isNil(this.obj)) return [] - return Array.isArray(this.obj) ? [...this.obj] : { ...this.obj } - } -} - -class IndexNav extends ArrayNav { - constructor(obj, index, next) { - super(obj, next) - this._index = index - } - - get index() { - const { _index, length } = this - if (_index >= 0) return _index - if (-_index > length) return undefined - return Math.max(length + _index, 0) - } - - get nextValue() { - const { index, obj } = this - return (isNil(obj) || index === undefined) ? this.next(undefined) : this.next(obj[index]) - } - - get() { - return this.nextValue.get() - } - - update(updater) { - const copy = this.copy() - copy[this.index] = this.nextValue.update(updater) - return copy - } -} - -export function indexNav(index, next) { - return obj => new IndexNav(obj, index, next) -} - -class SliceNav extends ArrayNav { - constructor(obj, bounds, next) { - super(obj, next) - this.bounds = bounds - } - - bound(index) { - if (index < 0) return Math.max(this.length + index, 0) - return index - } - - get start() { - return this.bound(this.bounds[0]) - } - - get end() { - const [, end] = this.bounds - return this.bound(end === undefined ? this.length : end) - } - - get range() { - const { start, end } = this - return (function*() { - for (let i = start; i < end; i++) yield i - }()) - } - - get() { - if (isNil(this.obj)) return [] - return Array.from(this.range, index => this.next(this.obj[index]).get()) - } - - update(updater) { - if (isNil(this.obj)) return [] - - const copy = this.copy() - for (const index of this.range) copy[index] = this.next(this.obj[index]).update(updater) - return copy - } -} - -export function sliceNav(bounds, next) { - return obj => new SliceNav(obj, bounds, next) -} diff --git a/packages/immutadot/src/nav/arrayNav.js b/packages/immutadot/src/nav/arrayNav.js new file mode 100644 index 00000000..d7be147d --- /dev/null +++ b/packages/immutadot/src/nav/arrayNav.js @@ -0,0 +1,18 @@ +import { isNil, length } from 'util/lang' + +export class ArrayNav { + constructor(obj, next) { + this.obj = obj + this.next = next + } + + get length() { + if (this._length === undefined) this._length = length(this.obj) + return this._length + } + + copy() { + if (isNil(this.obj)) return [] + return Array.isArray(this.obj) ? [...this.obj] : { ...this.obj } + } +} diff --git a/packages/immutadot/src/nav/indexNav.js b/packages/immutadot/src/nav/indexNav.js new file mode 100644 index 00000000..eeee2ee7 --- /dev/null +++ b/packages/immutadot/src/nav/indexNav.js @@ -0,0 +1,35 @@ +import { ArrayNav } from './arrayNav' +import { isNil } from 'util/lang' + +class IndexNav extends ArrayNav { + constructor(obj, index, next) { + super(obj, next) + this._index = index + } + + get index() { + const { _index, length } = this + if (_index >= 0) return _index + if (-_index > length) return undefined + return Math.max(length + _index, 0) + } + + get nextValue() { + const { index, obj } = this + return (isNil(obj) || index === undefined) ? this.next(undefined) : this.next(obj[index]) + } + + get() { + return this.nextValue.get() + } + + update(updater) { + const copy = this.copy() + copy[this.index] = this.nextValue.update(updater) + return copy + } +} + +export function indexNav(index, next) { + return obj => new IndexNav(obj, index, next) +} diff --git a/packages/immutadot/src/nav/nav.js b/packages/immutadot/src/nav/nav.js index 9e3a5588..d235350f 100644 --- a/packages/immutadot/src/nav/nav.js +++ b/packages/immutadot/src/nav/nav.js @@ -1,6 +1,7 @@ import { index, prop, slice } from '@immutadot/parser/consts' -import { indexNav, sliceNav } from './array' -import { propNav } from './object' +import { indexNav } from './indexNav' +import { propNav } from './propNav' +import { sliceNav } from './sliceNav' export function nav(path) { return path.reduceRight((next, [type, value]) => toNav(type)(value, next), finalNav) diff --git a/packages/immutadot/src/nav/object.js b/packages/immutadot/src/nav/propNav.js similarity index 100% rename from packages/immutadot/src/nav/object.js rename to packages/immutadot/src/nav/propNav.js diff --git a/packages/immutadot/src/nav/sliceNav.js b/packages/immutadot/src/nav/sliceNav.js new file mode 100644 index 00000000..cf6aa4c4 --- /dev/null +++ b/packages/immutadot/src/nav/sliceNav.js @@ -0,0 +1,47 @@ +import { ArrayNav } from './arrayNav' +import { isNil } from 'util/lang' + +class SliceNav extends ArrayNav { + constructor(obj, bounds, next) { + super(obj, next) + this.bounds = bounds + } + + bound(index) { + if (index < 0) return Math.max(this.length + index, 0) + return index + } + + get start() { + return this.bound(this.bounds[0]) + } + + get end() { + const [, end] = this.bounds + return this.bound(end === undefined ? this.length : end) + } + + get range() { + const { start, end } = this + return (function*() { + for (let i = start; i < end; i++) yield i + }()) + } + + get() { + if (isNil(this.obj)) return [] + return Array.from(this.range, index => this.next(this.obj[index]).get()) + } + + update(updater) { + if (isNil(this.obj)) return [] + + const copy = this.copy() + for (const index of this.range) copy[index] = this.next(this.obj[index]).update(updater) + return copy + } +} + +export function sliceNav(bounds, next) { + return obj => new SliceNav(obj, bounds, next) +} From c74587b4955f52ab2fe33c3452468bd9b13849b7 Mon Sep 17 00:00:00 2001 From: nlepage <19571875+nlepage@users.noreply.github.com> Date: Wed, 30 May 2018 16:17:35 +0200 Subject: [PATCH 11/15] :refactor: Optional currying on set --- packages/immutadot/src/core/set.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/immutadot/src/core/set.js b/packages/immutadot/src/core/set.js index 2a1a0f4c..7c30a35b 100644 --- a/packages/immutadot/src/core/set.js +++ b/packages/immutadot/src/core/set.js @@ -1,3 +1,4 @@ +import { isString } from 'util/lang' import { nav } from 'nav/nav' import { toPath } from '@immutadot/parser' @@ -15,4 +16,10 @@ function set(obj, path, value) { return nav(toPath(path))(obj).update(() => value) } -export { set } +const curried = (path, value) => obj => set(obj, path, value) + +function optionallyCurried(...args) { + return isString(args[0]) ? curried(...args) : set(...args) +} + +export { optionallyCurried as set } From 481f3d5ecb9d14d0270802cc9fad09066d16a746 Mon Sep 17 00:00:00 2001 From: nlepage <19571875+nlepage@users.noreply.github.com> Date: Wed, 30 May 2018 17:20:21 +0200 Subject: [PATCH 12/15] :sparkles: Add all props navigator --- packages/immutadot/src/nav/allPropsNav.js | 24 ++++++++++++++ packages/immutadot/src/nav/arrayNav.js | 15 ++++----- packages/immutadot/src/nav/baseNav.js | 6 ++++ packages/immutadot/src/nav/indexNav.js | 16 +++++----- packages/immutadot/src/nav/nav.js | 6 ++-- packages/immutadot/src/nav/objectNav.js | 9 ++++++ packages/immutadot/src/nav/propNav.js | 23 ++++++------- packages/immutadot/src/nav/sliceNav.js | 17 ++++++---- packages/immutadot/src/util/lang.js | 23 ------------- packages/immutadot/src/util/lang.spec.js | 39 +---------------------- 10 files changed, 78 insertions(+), 100 deletions(-) create mode 100644 packages/immutadot/src/nav/allPropsNav.js create mode 100644 packages/immutadot/src/nav/baseNav.js create mode 100644 packages/immutadot/src/nav/objectNav.js diff --git a/packages/immutadot/src/nav/allPropsNav.js b/packages/immutadot/src/nav/allPropsNav.js new file mode 100644 index 00000000..1f0b552a --- /dev/null +++ b/packages/immutadot/src/nav/allPropsNav.js @@ -0,0 +1,24 @@ +import { ObjectNav } from './objectNav' +import { isNil } from 'util/lang' + +class AllPropsNav extends ObjectNav { + get() { + const { _next, value } = this + + if (isNil(value)) return [] + + return Object.keys(value).map(key => _next(value[key])) + } + + update(updater) { + const { _next, value } = this + + const copy = this.copy() + for (const key of Object.keys(copy)) copy[key] = _next(value[key]).update(updater) + return copy + } +} + +export function allPropsNav(_, next) { + return value => new AllPropsNav(value, next) +} diff --git a/packages/immutadot/src/nav/arrayNav.js b/packages/immutadot/src/nav/arrayNav.js index d7be147d..d79299ae 100644 --- a/packages/immutadot/src/nav/arrayNav.js +++ b/packages/immutadot/src/nav/arrayNav.js @@ -1,18 +1,15 @@ import { isNil, length } from 'util/lang' +import { BaseNav } from './baseNav' -export class ArrayNav { - constructor(obj, next) { - this.obj = obj - this.next = next - } - +export class ArrayNav extends BaseNav { get length() { - if (this._length === undefined) this._length = length(this.obj) + if (this._length === undefined) this._length = length(this.value) return this._length } copy() { - if (isNil(this.obj)) return [] - return Array.isArray(this.obj) ? [...this.obj] : { ...this.obj } + const { value } = this + if (isNil(value)) return [] + return Array.isArray(value) ? [...value] : { ...value } } } diff --git a/packages/immutadot/src/nav/baseNav.js b/packages/immutadot/src/nav/baseNav.js new file mode 100644 index 00000000..01bd297d --- /dev/null +++ b/packages/immutadot/src/nav/baseNav.js @@ -0,0 +1,6 @@ +export class BaseNav { + constructor(value, next) { + this.value = value + this._next = next + } +} diff --git a/packages/immutadot/src/nav/indexNav.js b/packages/immutadot/src/nav/indexNav.js index eeee2ee7..576245d3 100644 --- a/packages/immutadot/src/nav/indexNav.js +++ b/packages/immutadot/src/nav/indexNav.js @@ -2,8 +2,8 @@ import { ArrayNav } from './arrayNav' import { isNil } from 'util/lang' class IndexNav extends ArrayNav { - constructor(obj, index, next) { - super(obj, next) + constructor(value, index, next) { + super(value, next) this._index = index } @@ -14,22 +14,22 @@ class IndexNav extends ArrayNav { return Math.max(length + _index, 0) } - get nextValue() { - const { index, obj } = this - return (isNil(obj) || index === undefined) ? this.next(undefined) : this.next(obj[index]) + get next() { + const { _next, index, value } = this + return (isNil(value) || index === undefined) ? _next(undefined) : _next(value[index]) } get() { - return this.nextValue.get() + return this.next.get() } update(updater) { const copy = this.copy() - copy[this.index] = this.nextValue.update(updater) + copy[this.index] = this.next.update(updater) return copy } } export function indexNav(index, next) { - return obj => new IndexNav(obj, index, next) + return value => new IndexNav(value, index, next) } diff --git a/packages/immutadot/src/nav/nav.js b/packages/immutadot/src/nav/nav.js index d235350f..c9460c55 100644 --- a/packages/immutadot/src/nav/nav.js +++ b/packages/immutadot/src/nav/nav.js @@ -1,4 +1,5 @@ -import { index, prop, slice } from '@immutadot/parser/consts' +import { allProps, index, prop, slice } from '@immutadot/parser/consts' +import { allPropsNav } from './allPropsNav' import { indexNav } from './indexNav' import { propNav } from './propNav' import { sliceNav } from './sliceNav' @@ -9,8 +10,9 @@ export function nav(path) { function toNav(type) { switch (type) { - case prop: return propNav + case allProps: return allPropsNav case index: return indexNav + case prop: return propNav case slice: return sliceNav default: throw TypeError(type) } diff --git a/packages/immutadot/src/nav/objectNav.js b/packages/immutadot/src/nav/objectNav.js new file mode 100644 index 00000000..c2e7464a --- /dev/null +++ b/packages/immutadot/src/nav/objectNav.js @@ -0,0 +1,9 @@ +import { BaseNav } from './baseNav' +import { isNil } from 'util/lang' + +export class ObjectNav extends BaseNav { + copy() { + const { value } = this + return isNil(value) ? {} : { ...value } + } +} diff --git a/packages/immutadot/src/nav/propNav.js b/packages/immutadot/src/nav/propNav.js index e4c968b3..44bd52a7 100644 --- a/packages/immutadot/src/nav/propNav.js +++ b/packages/immutadot/src/nav/propNav.js @@ -1,31 +1,28 @@ +import { ObjectNav } from './objectNav' import { isNil } from 'util/lang' -class PropNav { - constructor(obj, key, next) { - this.obj = obj +class PropNav extends ObjectNav { + constructor(value, key, next) { + super(value, next) this.key = key - this.next = next } - get nextValue() { - return isNil(this.obj) ? this.next(undefined) : this.next(this.obj[this.key]) + get next() { + const { _next, key, value } = this + return isNil(value) ? _next(undefined) : _next(value[key]) } get() { - return this.nextValue.get() - } - - copy() { - return isNil(this.obj) ? {} : { ...this.obj } + return this.next.get() } update(updater) { const copy = this.copy() - copy[this.key] = this.nextValue.update(updater) + copy[this.key] = this.next.update(updater) return copy } } export function propNav(key, next) { - return obj => new PropNav(obj, key, next) + return value => new PropNav(value, key, next) } diff --git a/packages/immutadot/src/nav/sliceNav.js b/packages/immutadot/src/nav/sliceNav.js index cf6aa4c4..3ce7b3a1 100644 --- a/packages/immutadot/src/nav/sliceNav.js +++ b/packages/immutadot/src/nav/sliceNav.js @@ -2,8 +2,8 @@ import { ArrayNav } from './arrayNav' import { isNil } from 'util/lang' class SliceNav extends ArrayNav { - constructor(obj, bounds, next) { - super(obj, next) + constructor(value, bounds, next) { + super(value, next) this.bounds = bounds } @@ -29,19 +29,22 @@ class SliceNav extends ArrayNav { } get() { - if (isNil(this.obj)) return [] - return Array.from(this.range, index => this.next(this.obj[index]).get()) + const { _next, value, range } = this + + if (isNil(value)) return [] + + return Array.from(range, index => _next(value[index]).get()) } update(updater) { - if (isNil(this.obj)) return [] + const { _next, value, range } = this const copy = this.copy() - for (const index of this.range) copy[index] = this.next(this.obj[index]).update(updater) + for (const index of range) copy[index] = _next(value[index]).update(updater) return copy } } export function sliceNav(bounds, next) { - return obj => new SliceNav(obj, bounds, next) + return value => new SliceNav(value, bounds, next) } diff --git a/packages/immutadot/src/util/lang.js b/packages/immutadot/src/util/lang.js index e00993bf..fd08a576 100644 --- a/packages/immutadot/src/util/lang.js +++ b/packages/immutadot/src/util/lang.js @@ -42,17 +42,6 @@ const isNil = arg => arg === undefined || arg === null */ const isString = arg => typeof arg === 'string' -/** - * Tests whether arg is a Symbol. - * @param {*} arg The value to test - * @return {boolean} True if arg is a Symbol, false otherwise - * @memberof util - * @private - * @since 1.0.0 - * @see {@link https://mdn.io/Symbol|Symbol} for more information. - */ -const isSymbol = arg => typeof arg === 'symbol' - /** * Returns the length of arg. * @function @@ -77,23 +66,11 @@ const length = arg => { */ const toString = arg => typeof arg === 'string' ? arg : `${arg}` -/** - * Tests whether arg is a object. - * @param {*} arg The value to test - * @return {boolean} True if arg is an Object, false otherwise - * @memberof util - * @private - * @since 1.0.0 - */ -const isObject = arg => arg instanceof Object - export { isFunction, isNaturalInteger, isNil, - isObject, isString, - isSymbol, length, toString, } diff --git a/packages/immutadot/src/util/lang.spec.js b/packages/immutadot/src/util/lang.spec.js index 4d630340..7b6bd31d 100644 --- a/packages/immutadot/src/util/lang.spec.js +++ b/packages/immutadot/src/util/lang.spec.js @@ -3,12 +3,11 @@ import { isFunction, isNaturalInteger, isNil, - isObject, isString, - isSymbol, length, toString, } from './lang' + describe('Lang utils', () => { describe('util.isFunction', () => { it('should return true for functions', () => { @@ -77,18 +76,6 @@ describe('Lang utils', () => { expect(isString(null)).toBe(false) }) }) - describe('util.isSymbol', () => { - it('should return true for symbols', () => { - expect(isSymbol(Symbol())).toBe(true) - expect(isSymbol(Symbol('\uD83C\uDF7A'))).toBe(true) - expect(isSymbol(Symbol.for('\uD83C\uDF7A'))).toBe(true) - }) - it('should return false for non symbols', () => { - expect(isSymbol('\uD83C\uDF7A')).toBe(false) - expect(isSymbol(666)).toBe(false) - expect(isSymbol({})).toBe(false) - }) - }) describe('util.length', () => { it('should return length of array', () => { expect(length(Array(666))).toBe(666) @@ -114,28 +101,4 @@ describe('Lang utils', () => { expect(toString(666)).toBe('666') }) }) - describe('util.isObject', () => { - it('should return true for object', () => { - expect(isObject({})).toBe(true) - }) - it('should return true for array', () => { - expect(isObject([])).toBe(true) - }) - it('should return true for function', () => { - const func = () => 1 - expect(isObject(func)).toBe(true) - }) - it('should return true for string', () => { - expect(isObject('')).toBe(false) - }) - it('should return true for number', () => { - expect(isObject(1)).toBe(false) - }) - it('should return true for instance of wrappers', () => { - /* eslint-disable no-new-wrappers */ - expect(isObject(new Number(1))).toBe(true) - expect(isObject(new String(''))).toBe(true) - expect(isObject(new Boolean(true))).toBe(true) /* eslint-enable no-new-wrappers */ - }) - }) }) From 53fc488fd18ef81d9a4ee2538c88dffddb9ee59a Mon Sep 17 00:00:00 2001 From: nlepage <19571875+nlepage@users.noreply.github.com> Date: Wed, 30 May 2018 21:36:21 +0200 Subject: [PATCH 13/15] :sparkles: Prop list navigator --- packages/immutadot/src/nav/allPropsNav.js | 24 ---------------- packages/immutadot/src/nav/nav.js | 8 ++++-- packages/immutadot/src/nav/propsNav.js | 35 +++++++++++++++++++++++ 3 files changed, 40 insertions(+), 27 deletions(-) delete mode 100644 packages/immutadot/src/nav/allPropsNav.js create mode 100644 packages/immutadot/src/nav/propsNav.js diff --git a/packages/immutadot/src/nav/allPropsNav.js b/packages/immutadot/src/nav/allPropsNav.js deleted file mode 100644 index 1f0b552a..00000000 --- a/packages/immutadot/src/nav/allPropsNav.js +++ /dev/null @@ -1,24 +0,0 @@ -import { ObjectNav } from './objectNav' -import { isNil } from 'util/lang' - -class AllPropsNav extends ObjectNav { - get() { - const { _next, value } = this - - if (isNil(value)) return [] - - return Object.keys(value).map(key => _next(value[key])) - } - - update(updater) { - const { _next, value } = this - - const copy = this.copy() - for (const key of Object.keys(copy)) copy[key] = _next(value[key]).update(updater) - return copy - } -} - -export function allPropsNav(_, next) { - return value => new AllPropsNav(value, next) -} diff --git a/packages/immutadot/src/nav/nav.js b/packages/immutadot/src/nav/nav.js index c9460c55..6e6e2bae 100644 --- a/packages/immutadot/src/nav/nav.js +++ b/packages/immutadot/src/nav/nav.js @@ -1,7 +1,7 @@ -import { allProps, index, prop, slice } from '@immutadot/parser/consts' -import { allPropsNav } from './allPropsNav' +import { allProps, index, list, prop, slice } from '@immutadot/parser/consts' import { indexNav } from './indexNav' import { propNav } from './propNav' +import { propsNav } from './propsNav' import { sliceNav } from './sliceNav' export function nav(path) { @@ -10,7 +10,9 @@ export function nav(path) { function toNav(type) { switch (type) { - case allProps: return allPropsNav + case allProps: + case list: + return propsNav case index: return indexNav case prop: return propNav case slice: return sliceNav diff --git a/packages/immutadot/src/nav/propsNav.js b/packages/immutadot/src/nav/propsNav.js new file mode 100644 index 00000000..6272f7bb --- /dev/null +++ b/packages/immutadot/src/nav/propsNav.js @@ -0,0 +1,35 @@ +import { ObjectNav } from './objectNav' +import { isNil } from 'util/lang' + +class PropsNav extends ObjectNav { + constructor(value, keys, next) { + super(value, next) + this._keys = keys + } + + get keys() { + const { _keys, value } = this + + if (_keys !== undefined) return _keys + + return isNil(value) ? [] : Object.keys(value) + } + + get() { + const { _next, keys, value } = this + + return keys.map(key => _next(value[key])) + } + + update(updater) { + const { _next, keys, value } = this + + const copy = this.copy() + for (const key of keys) copy[key] = _next(value[key]).update(updater) + return copy + } +} + +export function propsNav(keys, next) { + return value => new PropsNav(value, keys, next) +} From 3fa0bd203d8a09722e75fd355b611d27ee72ec85 Mon Sep 17 00:00:00 2001 From: nlepage <19571875+nlepage@users.noreply.github.com> Date: Wed, 30 May 2018 22:16:37 +0200 Subject: [PATCH 14/15] :recycle: Put TypeError for empty path in nav --- packages/immutadot/src/nav/nav.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/immutadot/src/nav/nav.js b/packages/immutadot/src/nav/nav.js index 6e6e2bae..8bac4154 100644 --- a/packages/immutadot/src/nav/nav.js +++ b/packages/immutadot/src/nav/nav.js @@ -5,6 +5,8 @@ import { propsNav } from './propsNav' import { sliceNav } from './sliceNav' export function nav(path) { + if (path.length === 0) throw new TypeError('path should not be empty') + return path.reduceRight((next, [type, value]) => toNav(type)(value, next), finalNav) } From 77f98573ff51f046de86c936acab8cca30d15726 Mon Sep 17 00:00:00 2001 From: nlepage <19571875+nlepage@users.noreply.github.com> Date: Wed, 6 Jun 2018 19:24:38 +0200 Subject: [PATCH 15/15] :ok_hand: @frinyvonnick's review --- packages/immutadot/src/nav/arrayNav.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/immutadot/src/nav/arrayNav.js b/packages/immutadot/src/nav/arrayNav.js index d79299ae..2e7d7224 100644 --- a/packages/immutadot/src/nav/arrayNav.js +++ b/packages/immutadot/src/nav/arrayNav.js @@ -3,8 +3,7 @@ import { BaseNav } from './baseNav' export class ArrayNav extends BaseNav { get length() { - if (this._length === undefined) this._length = length(this.value) - return this._length + return length(this.value) } copy() {