diff --git a/CHANGELOG.md b/CHANGELOG.md index 609b7952..a918a318 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + +- Fine grained control of how related Microstates are instantiated + via abstract relationships + https://github.com/microstates/microstates.js/pull/358 + ## [0.14.0] - 2019-03-27 ### Breaking diff --git a/index.js b/index.js index 95711c7d..6b9c72c7 100644 --- a/index.js +++ b/index.js @@ -3,4 +3,5 @@ export { default as from } from './src/literal'; export { map, filter, reduce, find } from './src/query'; export { default as Store } from './src/identity'; export { metaOf, valueOf } from './src/meta'; +export { relationship } from './src/relationship.js'; export * from './src/types'; diff --git a/src/microstates.js b/src/microstates.js index 14409b6c..1fb8727c 100644 --- a/src/microstates.js +++ b/src/microstates.js @@ -1,8 +1,9 @@ import { append, stable, map } from 'funcadelic'; import { set } from './lens'; -import { Meta, mount, metaOf, valueOf, sourceOf } from './meta'; +import { Meta, mount, metaOf, sourceOf, valueOf } from './meta'; import { methodsOf } from './reflection'; import dsl from './dsl'; +import { Relationship, Edge, relationship } from './relationship'; import Any from './types/any'; import CachedProperty from './cached-property'; import Observable from './observable'; @@ -29,12 +30,11 @@ const MicrostateType = stable(function MicrostateType(Type) { constructor(value) { super(value); Object.defineProperties(this, map((slot, key) => { + let relationship = slot instanceof Relationship ? slot : legacy(slot); + return CachedProperty(key, self => { - let value = valueOf(self); - let expanded = expandProperty(slot); - let substate = value != null && value[key] != null ? expanded.set(value[key]) : expanded; - let mounted = mount(self, substate, key); - return mounted; + let { Type, value } = relationship.traverse(new Edge(self, [key])); + return mount(self, create(Type, value), key); }); }, this)); @@ -62,14 +62,23 @@ const MicrostateType = stable(function MicrostateType(Type) { return Microstate; }); -function expandProperty(property) { - let meta = metaOf(property); +/** + * Implement the legacy DSL as a relationship. + * + * Consider emitting a deprecation warning, as this will likely be + * removed before microstates 1.0 + */ + +function legacy(object) { + let cell; + let meta = metaOf(object); if (meta != null) { - return property; + cell = { Type: object.constructor.Type, value: valueOf(object) }; } else { - let { Type, value } = dsl.expand(property); - return create(Type, value); + cell = dsl.expand(object); } + let { Type } = cell; + return relationship(cell.value).map(({ value }) => ({ Type, value })); } function hasOwnProperty(target, propertyName) { diff --git a/src/relationship.js b/src/relationship.js new file mode 100644 index 00000000..3d9736d3 --- /dev/null +++ b/src/relationship.js @@ -0,0 +1,56 @@ +import { Any } from './types'; +import { view, Path } from './lens'; +import { valueOf } from './meta'; + +export class Relationship { + + constructor(traverse) { + this.traverse = traverse; + } + + flatMap(sequence) { + return new Relationship(edge => { + let cell = this.traverse(edge); + let next = sequence(cell); + return next.traverse(edge); + }); + } + + map(fn) { + return this.flatMap(cell => new Relationship(() => fn(cell))); + } +} + +export function relationship(definition) { + if (typeof definition === 'function') { + return new Relationship(definition); + } else { + return relationship(({ value }) => { + if (value != null) { + return { Type: Any, value }; + } else { + return { Type: Any, value: definition }; + } + }); + } +} + +export class Edge { + constructor(parent, path) { + this.parent = parent; + this.path = path; + } + + get name() { + let [ name ] = this.path.slice(-1); + return name; + } + + get value() { + return view(Path(this.path), this.parentValue); + } + + get parentValue() { + return valueOf(this.parent); + } +} diff --git a/tests/index.test.js b/tests/index.test.js index 27237bc2..64203e3d 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -23,4 +23,7 @@ describe('index of module exports', () => { expect(index.metaOf).toBeInstanceOf(Function); expect(index.valueOf).toBeInstanceOf(Function); }); + it('has the relationship function', () => { + expect(index.relationship).toBeInstanceOf(Function); + }); }); diff --git a/tests/relationship.test.js b/tests/relationship.test.js new file mode 100644 index 00000000..e0cc049f --- /dev/null +++ b/tests/relationship.test.js @@ -0,0 +1,73 @@ +/* global describe, it, beforeEach */ + +import expect from 'expect'; + +import { Any } from '../src/types'; +import { relationship, Edge } from '../src/relationship'; + +describe('relationships', () => { + let rel; + let e = (value) => new Edge(value, []); + + describe('with absolutely nothing specified', () => { + beforeEach(() => { + rel = relationship(10); + }); + + it('reifies to using Any for the type and the passed value, }', () => { + expect(rel.traverse(e(5))).toEqual({ Type: Any, value: 5 }); + }); + + it('uses the supplied value if non is given', () => { + expect(rel.traverse(e())).toEqual({ Type: Any, value: 10 }); + }); + }); + + describe('depending on the parent microstate that they are a part of', () => { + beforeEach(() => { + rel = relationship((value, parent) => { + if (typeof parent === 'number') { + return { Type: Number, value: Number(value) }; + } else if (typeof parent === 'string') { + return { Type: String, value: String(value) }; + } else { + return { Type: Object, value: Object(value) }; + } + }); + }); + it('generates numbers when the parent is a number', () => { + expect(rel.traverse(5, 'parent')).toEqual({ Type: String, value: '5'}); + }); + + it('generates strings when the parent is a string', () => { + expect(rel.traverse(5, 0)).toEqual({ Type: Number, value: 5}); + }); + }); + + describe('that are mapped', () => { + beforeEach(() => { + rel = relationship() + .map(({ value }) => ({Type: Number, value: value * 2 })); + }); + + it('executes the mapping as part of the traversal', () => { + expect(rel.traverse(e(3))).toEqual({Type: Number, value: 6}); + }); + }); + + describe('Edges', () => { + let edge; + beforeEach(() => { + edge = new Edge({one: {two: 2}}, ['one', 'two']); + }); + it('knows its value', () => { + expect(edge.value).toEqual(2); + }); + it('knows its parent value', () => { + expect(edge.parentValue).toEqual({one: {two: 2}}); + }); + it('has a name provided it is not anonymous', () => { + expect(edge.name).toEqual('two'); + }); + }); +});