Skip to content

Commit

Permalink
feat(core): skeleZip (#129)
Browse files Browse the repository at this point in the history
A simplified zipper over the Skele tree structure. It is meant to repalce
`elementZipper` which is now deprecated.

`skeleZip` does not take any configuration and **requires** explicite annotation of children  properties (using `@@skele/children`) in the tree.
  • Loading branch information
ognen committed Dec 26, 2018
1 parent 948afed commit 85d91c8
Show file tree
Hide file tree
Showing 5 changed files with 362 additions and 1 deletion.
2 changes: 2 additions & 0 deletions packages/core/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import * as data from './data'
import * as registry from './registry'
import * as zip from './zip'
import skeleZip from './zip/skele'
import * as log from './log'
import * as propNames from './propNames'
import Cursor from './vendor/cursor'
Expand All @@ -16,6 +17,7 @@ export default {
data,
registry,
zip,
skeleZip,
log,
propNames,
internal,
Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/zip/elementZipper.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

import { makeZipper } from '../zip'
import deprecated from '../log/deprecated'
import { Iterable, List, Map } from 'immutable'
import * as R from 'ramda'
import {
Expand Down Expand Up @@ -77,7 +78,7 @@ const singleChild = childColl =>
*
* @param config, configuration for the object, currently supports only the `defaultChildPositions` property
*/
export default function elementZipper(config) {
function elementZipper(config) {
const { defaultChildPositions, makeZipperOverride } = config
const dcp = asList(defaultChildPositions)

Expand All @@ -90,3 +91,8 @@ export default function elementZipper(config) {

return ElementZipperType.from.bind(ElementZipperType)
}

export default deprecated(
'elementZipper is deprecated, use `skeleZip` instead',
elementZipper
)
280 changes: 280 additions & 0 deletions packages/core/src/zip/skele/__tests__/zipperImpl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
'use strict'

import R from 'ramda'
import { fromJS } from 'immutable'
import * as zip from '../..'
import skeleZip from '..'
import * as data from '../../../data'
import * as propNames from '../../../propNames'

const childCollectionKind = '@@skele/child-collection'

describe('Skele Zipper', () => {
const singleChild = {
kind: 'parent',
[propNames.children]: 'children',
children: [
{
kind: 'lvl1',
[propNames.children]: 'children',
children: [
{
kind: 'lvl2',
},
],
},
],
}

it('zipper should correctly navigate up and down', () => {
const zipper = skeleZip(fromJS(singleChild))

expect(zip.node(zipper).get('kind')).toEqual('parent')
expect(data.isOfKind(childCollectionKind, zip.node(zip.down(zipper)))).toBe(
true
)
expect(
R.pipe(
zip.down,
zip.down,
zip.node
)(zipper).get('kind')
).toEqual('lvl1')

expect(
data.isOfKind(
childCollectionKind,

R.pipe(
zip.down,
zip.down,
zip.down,
zip.node
)(zipper)
)
).toBe(true)
expect(
R.pipe(
zip.down,
zip.down,
zip.down,
zip.down,
zip.node
)(zipper).get('kind')
).toEqual('lvl2')
expect(
R.pipe(
zip.down,
zip.down,
zip.down,
zip.down,
zip.down
)(zipper)
).toBeNull()
expect(
R.pipe(
zip.down,
zip.up,
zip.node
)(zipper).get('kind')
).toEqual('parent')
expect(
R.pipe(
zip.down,
zip.down,
zip.down,
zip.up,
zip.node
)(zipper).get('kind')
).toEqual('lvl1')
})

const multipleChildren = {
id: 1,
kind: 't',
[propNames.children]: 'children',
children: [
{
id: 2,
kind: 't',
[propNames.children]: 'children',
children: [
{
id: 3,
kind: 't',
},
{
kind: 't',
id: 4,
},
],
},
{
id: 5,
kind: 't',
[propNames.children]: 'children',
children: [
{
kind: 't',
id: 6,
},
{
kind: 't',
id: 7,
},
],
},
{
id: 8,
kind: 't',
[propNames.children]: 'children',
children: [
{
kind: 't',
id: 9,
},
{
kind: 't',
id: 10,
},
],
},
],
}

it('zipper should correctly navigate up down left and right', () => {
const zipper = skeleZip(fromJS(multipleChildren))

expect(zip.node(zipper).get('id')).toEqual(1)
expect(data.isOfKind(childCollectionKind, zip.node(zip.down(zipper)))).toBe(
true
)
expect(
R.pipe(
zip.down,
zip.down,
zip.node
)(zipper).get('id')
).toEqual(2)
expect(
R.pipe(
zip.down,
zip.down,
zip.right,
zip.node
)(zipper).get('id')
).toEqual(5)
expect(
R.pipe(
zip.down,
zip.down,
zip.right,
zip.right,
zip.node
)(zipper).get('id')
).toEqual(8)
expect(
R.pipe(
zip.down,
zip.down,
zip.right,
zip.right,
zip.left,
zip.node
)(zipper).get('id')
).toEqual(5)
expect(
data.isOfKind(
childCollectionKind,
R.pipe(
zip.down,
zip.down,
zip.right,
zip.right,
zip.left,
zip.up,
zip.node
)(zipper)
)
).toBe(true)
expect(
R.pipe(
zip.down,
zip.down,
zip.right,
zip.right,
zip.left,
zip.up,
zip.up,
zip.node
)(zipper).get('id')
).toEqual(1)
})

const multipleChildrenElements = {
id: 1,
kind: 't',
[propNames.children]: ['left', 'right'],
left: [
{
kind: 't',
id: 2,
},
],
right: [
{
kind: 't',
id: 3,
},
{
kind: 't',
id: 4,
},
],
}

it('zipper multiple children elements', () => {
const zipper = skeleZip(fromJS(multipleChildrenElements))

expect(zip.node(zipper).get('id')).toEqual(1)
expect(
R.pipe(
zip.down,
zip.node
)(zipper).get('propertyName')
).toEqual('left')

expect(
R.pipe(
zip.down,
zip.right,
zip.node
)(zipper).get('propertyName')
).toEqual('right')

expect(
R.pipe(
zip.down,
zip.down,
zip.node
)(zipper).get('id')
).toEqual(2)
expect(
R.pipe(
zip.down,
zip.right,
zip.down,
zip.node
)(zipper).get('id')
).toEqual(3)
expect(
R.pipe(
zip.down,
zip.right,
zip.down,
zip.right,
zip.node
)(zipper).get('id')
).toEqual(4)
})
})
4 changes: 4 additions & 0 deletions packages/core/src/zip/skele/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
'use strict'
import skeleZip from './zipperImpl'

export default skeleZip
69 changes: 69 additions & 0 deletions packages/core/src/zip/skele/zipperImpl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'use strict'

import { zipper } from '../../zip'
import { Iterable, List, Map } from 'immutable'
import { isOfKind, asList, childPositions } from '../../data'

const isBranch = element => {
if (isOfKind('@@skele/child-collection', element)) {
return true
}

const positions = childPositions(element)

return positions != null && !positions.isEmpty()
}

const getChildren = element => {
if (isOfKind('@@skele/child-collection', element)) {
return element.get('children').toArray()
}
// at a children collection level
const positions = childPositions(element)

const children = positions
.reduce(
(children, p) =>
element.get(p)
? children.push(makeChildCollection(p, element.get(p)))
: children,
List()
)
.toArray()

return children
}

const makeChildCollection = (p, children) =>
Map({
kind: '@@skele/child-collection',
propertyName: p,
isSingle: !Iterable.isIndexed(children),
children: asList(children),
})

const makeNode = (element, children) => {
if (isOfKind('@@skele/child-collection', element)) {
return element.set('children', List(children))
}
return children.reduce(
(el, childColl) =>
el.set(
childColl.get('propertyName'),
singleChild(childColl)
? childColl.getIn(['children', 0])
: childColl.get('children')
),
element
)
}

const singleChild = childColl =>
childColl.get('isSingle') && childColl.get('children').count() === 1

/**
* Creates a zipper over a Skele state tree.
*
* @param root the root node of the state tree
*/
export default zipper.bind(undefined, isBranch, getChildren, makeNode)

0 comments on commit 85d91c8

Please sign in to comment.