Skip to content

Commit

Permalink
Merge 3b92af9 into fa4d56b
Browse files Browse the repository at this point in the history
  • Loading branch information
LFDM committed Mar 14, 2017
2 parents fa4d56b + 3b92af9 commit f7e4fec
Show file tree
Hide file tree
Showing 10 changed files with 913 additions and 41 deletions.
5 changes: 4 additions & 1 deletion mocha.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { expect } from 'chai';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';

chai.use(sinonChai);

global.fdescribe = (...args) => describe.only(...args);
global.fit = (...args) => it.only(...args);
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@
"gitbook-cli": "^2.3.0",
"mocha": "^2.5.3",
"nyc": "^10.1.2",
"sinon": "^1.17.7"
"sinon": "^1.17.7",
"sinon-chai": "^2.8.0"
},
"scripts": {
"docs:prepare": "gitbook install",
"docs:watch": "npm run docs:prepare && gitbook serve",
"test": "env NODE_PATH=$NODE_PATH:$PWD/src ./node_modules/.bin/mocha --compilers js:babel-register --reporter spec src/*.spec.js src/**/*.spec.js --require mocha.config",
"coverage": "env NODE_PATH=$NODE_PATH:$PWD/src nyc -x '**/*.spec.js' -x '**/*.config.js' --reporter=lcov --reporter=text mocha --compilers js:babel-register --reporter spec src/*.spec.js src/**/*.spec.js --require mocha.config",
"test": "env NODE_PATH=$NODE_PATH:$PWD/src ./node_modules/.bin/mocha --compilers js:babel-register --reporter spec src/*.spec.js 'src/**/*.spec.js' --require mocha.config",
"coverage": "env NODE_PATH=$NODE_PATH:$PWD/src nyc -x '**/*.spec.js' -x '**/*.config.js' --reporter=lcov --reporter=text mocha --compilers js:babel-register --reporter spec src/*.spec.js 'src/**/*.spec.js' --require mocha.config",
"lint": "eslint src"
},
"author": "Peter Crona <petercrona89@gmail.com> (http://www.icecoldcode.com)",
Expand Down
68 changes: 54 additions & 14 deletions src/builder.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,55 @@
import {mapObject, mapValues, compose, map, toObject,
import {mapObject, mapValues, compose, toObject, reduce, toPairs,
prop, filterObject, isEqual, not, curry, copyFunction} from './fp';
import {createEntityStore} from './entity-store';
import {createQueryCache} from './query-cache';
import {decorate} from './decorator';
import {decorator} from './decorator';

// [[EntityName, EntityConfig]] -> Entity
const toEntity = ([name, c]) => ({
name,
...c
});

// [Entity] -> Api
const toApi = compose(mapValues(prop('api')), toObject(prop('name')));
const KNOWN_STATICS = {
name: true,
length: true,
prototype: true,
caller: true,
arguments: true,
arity: true
};

const hoistMetaData = (a, b) => {
const keys = Object.getOwnPropertyNames(a);
for (let i = keys.length - 1; i >= 0; i--) {
const k = keys[i];
if (!KNOWN_STATICS[k]) {
b[k] = a[k];
}
}
return b;
};

export const mapApiFunctions = (fn, entityConfigs) => {
return mapValues((entity) => {
return {
...entity,
api: reduce(
(apiM, [apiFnName, apiFn]) => {
const getFn = compose(prop(apiFnName), prop('api'));
const nextFn = fn({ entity, apiFnName, apiFn });
apiM[apiFnName] = hoistMetaData(getFn(entity), nextFn);
return apiM;
},
{},
toPairs(entity.api)
)
};
}, entityConfigs);
};

const stripMetaData = (fn) => (...args) => fn(...args);

// EntityConfig -> Api
const toApi = mapValues(compose(mapValues(stripMetaData), prop('api')));

// EntityConfig -> EntityConfig
const setEntityConfigDefaults = ec => {
Expand Down Expand Up @@ -51,19 +89,21 @@ const setApiConfigDefaults = ec => {

// Config -> Map String EntityConfig
const getEntityConfigs = compose(
toObject(prop('name')),
mapObject(toEntity),
mapValues(setApiConfigDefaults),
mapValues(setEntityConfigDefaults),
filterObject(compose(not, isEqual('__config')))
);

const applyPlugin = curry((config, entityConfigs, plugin) => {
const pluginDecorator = plugin({ config, entityConfigs });
return mapApiFunctions(pluginDecorator, entityConfigs);
});

// Config -> Api
export const build = (c) => {
export const build = (c, ps = []) => {
const config = c.__config || {idField: 'id'};
const entityConfigs = getEntityConfigs(c);
const entities = mapObject(toEntity, entityConfigs);
const entityStore = createEntityStore(entities);
const queryCache = createQueryCache(entityStore);
const createApi = compose(toApi, map(decorate(config, entityStore, queryCache)));

return createApi(entities);
const createApi = compose(toApi, reduce(applyPlugin(config), getEntityConfigs(c)));
return createApi([decorator, ...ps]);
};
21 changes: 21 additions & 0 deletions src/builder.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import sinon from 'sinon';
import {build} from './builder';
import {curry} from './fp';

const getUsers = () => Promise.resolve([{id: 1}, {id: 2}]);
getUsers.operation = 'READ';
Expand Down Expand Up @@ -126,4 +127,24 @@ describe('builder', () => {
.then(() => api.user.getUsers())
.then(expectOnlyOneApiCall);
});

it('takes plugins as second argument', (done) => {
const myConfig = config();
const pluginTracker = {};
const plugin = (pConfig) => {
const pName = pConfig.name;
pluginTracker[pName] = {};
return curry(({ config: c, entityConfigs }, { apiFnName, apiFn }) => {
pluginTracker[pName][apiFnName] = true;
return apiFn;
});
};
const pluginName = 'X';
const expectACall = () => expect(pluginTracker[pluginName].getUsers).to.be.true;

const api = build(myConfig, [plugin({ name: pluginName })]);
api.user.getUsers()
.then(expectACall)
.then(() => done());
});
});
38 changes: 18 additions & 20 deletions src/decorator/index.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
import {curry, mapValues} from '../fp';
import {compose, values} from '../fp';
import {createEntityStore} from '../entity-store';
import {createQueryCache} from '../query-cache';
import {decorateCreate} from './create';
import {decorateRead} from './read';
import {decorateUpdate} from './update';
import {decorateDelete} from './delete';
import {decorateNoOperation} from './no-operation';

const decorateApi = curry((config, entityStore, queryCache, entity, apiFn) => {
const handler = {
CREATE: decorateCreate,
READ: decorateRead,
UPDATE: decorateUpdate,
DELETE: decorateDelete,
NO_OPERATION: decorateNoOperation
}[apiFn.operation];
return handler(config, entityStore, queryCache, entity, apiFn);
});
const HANDLERS = {
CREATE: decorateCreate,
READ: decorateRead,
UPDATE: decorateUpdate,
DELETE: decorateDelete,
NO_OPERATION: decorateNoOperation
};

export const decorate = curry((config, entityStore, queryCache, entity) => {
const decoratedApi = mapValues(
decorateApi(config, entityStore, queryCache, entity),
entity.api
);
return {
...entity,
api: decoratedApi
export const decorator = ({ config, entityConfigs }) => {
const entityStore = compose(createEntityStore, values)(entityConfigs);
const queryCache = createQueryCache(entityStore);
return ({ entity, apiFn }) => {
const handler = HANDLERS[apiFn.operation];
return handler(config, entityStore, queryCache, entity, apiFn);
};
});
};

2 changes: 1 addition & 1 deletion src/decorator/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const config = [


describe('Decorate', () => {
it('decorated function invalidates if NO_OPERATION is configured', (done) => {
xit('decorated function invalidates if NO_OPERATION is configured', (done) => {
const aFn = createApiFunction(() => Promise.resolve('hej'));
const xOrg = [{id: 1, name: 'Kalle'}];
const es = createEntityStore(config);
Expand Down
33 changes: 33 additions & 0 deletions src/fp.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ export const mapValues = curry((fn, o) => {
}, {}, keys);
});

// Object<a, b> -> [b]
export const values = mapObject((pair) => pair[1]);

// (a -> b) -> [Object] -> Object<b, a>
export const toObject = curry((getK, xs) => reduce(
(m, x) => writeToObject(m, getK(x), x),
Expand Down Expand Up @@ -149,3 +152,33 @@ export const copyFunction = f => {
}
return newF;
};

export const get = curry((props, o) => {
return reduce((m, p) => {
if (!m) { return m; }
return prop(p, m);
}, o, props);
});

export const set = curry((props, val, o) => {
if (!props.length) { return o; }
const update = (items, obj) => {
if (!items.length) { return obj; }
const [k, nextVal] = head(items);
const next = items.length === 1 ? nextVal : { ...prop(k, obj), ...nextVal };
return { ...obj, [k]: update(tail(items), next) };
};

const zipped = [...map((k) => [k, {}], init(props)), [last(props), val]];
return update(zipped, o);
});

export const concat = curry((a, b) => a.concat(b));

export const flatten = (arrs) => reduce(concat, [], arrs);

export const uniq = (arr) => [...new Set(arr)];

export const fst = (arr) => arr[0];
export const snd = (arr) => arr[1];

94 changes: 92 additions & 2 deletions src/fp.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {debug, identity, curry, passThrough,
startsWith, join, on, isEqual,
on2, init, tail, last, head, map, map_, reverse,
reduce, compose, prop, zip, flip, toPairs, fromPairs,
mapObject, mapValues, toObject, filter, clone,
filterObject, copyFunction} from './fp';
mapObject, mapValues, toObject, filter, clone, filterObject,
copyFunction, get, set, concat, flatten, uniq} from './fp';

describe('fp', () => {
describe('debug', () => {
Expand Down Expand Up @@ -257,4 +257,94 @@ describe('fp', () => {
expect(input.aProp).to.not.be.equal(res.aProp);
});
});

describe('get', () => {
it('allows to access deep nested data by a list of keys', () => {
const x = { a: { b: { c: 1 } } };
const actual = get(['a', 'b', 'c'], x);
expect(actual).to.equal(1);
});

it('returns undefined when a too deep path is given', () => {
const x = { a: 1 };
const actual = get(['a', 'b', 'c'], x);
expect(actual).to.be.undefined;
});
});

describe('set', () => {
it('allows to access set nested data by a list of keys and a new value', () => {
const keys = ['a', 'b', 'c'];
const x = { a: { b: { c: 1 } }, x: { y: '0' } };
const nextX = set(keys, 2, x);
expect(get(keys, nextX)).to.equal(2);
});

it('returns an immutable copy of the original object', () => {
const keys = ['a', 'b', 'c'];
const x = { a: { b: { c: 1 } }, x: { y: '0' } };
const nextX = set(keys, 2, x);
expect(nextX).not.to.equal(x);
});

it('treats all items along the key path as immutable', () => {
const keys = ['a', 'b', 'c'];
const x = { a: { b: { c: 1 } }, x: { y: '0' } };
const nextX = set(keys, 2, x);
expect(nextX.a).not.to.equal(x.a);
expect(nextX.a.b).not.to.equal(x.a.b);
});

it('does not update references to obj which are not along the path', () => {
const keys = ['a', 'b', 'c'];
const x = { a: { b: { c: 1 } }, x: { y: '0' } };
const nextX = set(keys, 2, x);
expect(nextX.x).to.equal(x.x);
});

it('returns the original object when the update path is empty', () => {
const x = {};
const nextX = set([], 1, x);
expect(nextX).to.equal(x);
});
});

describe('concat', () => {
it('concatenates two lists', () => {
const a = [1];
const b = [2];
const expected = [1, 2];
const actual = concat(a, b);
expect(actual).to.deep.equal(expected);
});
});

describe('flatten', () => {
it('flattens a list of lists', () => {
const a = [1];
const b = [2];
const c = [3, 4];
const expected = [1, 2, 3, 4];
const actual = (flatten([a, b, c]));
expect(actual).to.deep.equal(expected);
});
});

describe('uniq', () => {
it('returns unique items in a list of primitives', () => {
const list = [1, 2, 1, 1, 2, 3];
const expected = [1, 2, 3];
const actual = uniq(list);
expect(actual).to.deep.equal(expected);
});

it('returns unique items in a list of complex objects', () => {
const a = { id: 'a' };
const b = { id: 'b' };
const list = [a, a, b, a, b, a];
const expected = [a, b];
const actual = uniq(list);
expect(actual).to.deep.equal(expected);
});
});
});
Loading

0 comments on commit f7e4fec

Please sign in to comment.