Skip to content

Commit

Permalink
Merge pull request #358 from microstates/cl/abstract-relationships
Browse files Browse the repository at this point in the history
Abstract relationships.
  • Loading branch information
cowboyd authored Oct 8, 2019
2 parents bd8529c + aabfdf3 commit 2ce0c29
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 11 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
31 changes: 20 additions & 11 deletions src/microstates.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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));

Expand Down Expand Up @@ -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) {
Expand Down
56 changes: 56 additions & 0 deletions src/relationship.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
3 changes: 3 additions & 0 deletions tests/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
73 changes: 73 additions & 0 deletions tests/relationship.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
});

0 comments on commit 2ce0c29

Please sign in to comment.