redux-pathspace
Version:
> Quickly & easily create path-based namespaces to add actions that map to reducers
319 lines (277 loc) • 10.2 kB
JavaScript
import set from 'ramda/src/set';
import view from 'ramda/src/view';
import lensPath from 'ramda/src/lensPath';
import lensProp from 'ramda/src/lensProp';
import lensIndex from 'ramda/src/lensIndex';
import isPlainObject from 'lodash.isplainobject';
function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg;
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce((a, b) => (...args) => a(b(...args)));
}
function createPathspace() {
const PATH_JOINER = '.';
const PREFIX_SEPERATOR = ':';
const pathStringSymbol = Symbol('@@Pathspace->createNamespace->path[pathString]');
const pathLensSymbol = Symbol('@@Pathspace->createNamespace->path[pathLens]');
const _namespaces = new Map();
let _store;
let _actionCreators;
function getPathPrefix(path) {
if (!Array.isArray(path)) return path;
return path.reduce((stringified, location) => (
typeof location === 'number'
? `${stringified.slice(0, -1)}[${location}].`
: `${stringified}${location}.`
), '').slice(0, -1);
}
function checkPathArray(arr) {
const isValid = arr.reduce((bool, val) => {
if (!bool) return false;
if (typeof val === 'string' || typeof val === 'number') {
if (typeof val === 'string') return val.split(PATH_JOINER).length === 1;
return true;
}
return false;
}, true);
return isValid;
}
function getNamespace(path) {
return _namespaces.get(getPathPrefix(path));
}
function getNamespaceName(actionType) {
const split = actionType.split(PREFIX_SEPERATOR)[0].split(PATH_JOINER);
return split.length > 1
? split
: split[0];
}
function reducerWrapper(lens, reducer, getPipeline) {
const getter = view(lens);
const setter = set(lens);
return function wrappedReducer(state, payload) {
const pipe = [
x => setter(reducer(getter(x), payload, x), x),
...getPipeline().map(funcOrObj => (
typeof funcOrObj === 'function'
? x => funcOrObj(x, payload)
: x => getNamespace(getNamespaceName(funcOrObj.type))
.get(funcOrObj.type)(x, funcOrObj.payload)
)),
];
return compose(...pipe)(state);
};
}
function createActionContainer(lens) {
const _actions = new Map();
return {
set(actionName, reducer, getPipeline) {
if (_actions.has(actionName)) throw new Error(`The action "${actionName}" already exists for this path`);
return _actions.set(actionName, reducerWrapper(lens, reducer, getPipeline));
},
get(actionName) {
return _actions.get(actionName);
},
has(actionName) {
return _actions.has(actionName);
},
};
}
function getActionName(path, actionName) {
return !path.length
? actionName
: `${getPathPrefix(path)}${PREFIX_SEPERATOR}${actionName}`;
}
function defaultReducer(state, payload) {
return payload;
}
function createNoSideEffect() {
return payload => payload;
}
function validateAddActionArgs(actionType, reducer, meta) {
if (typeof reducer !== 'function') throw new Error('The "reducer" property passed to "addAction" must be a function');
if (typeof meta !== 'object') throw new Error('The "meta" property passed to "addAction" must be a plain object');
if (Array.isArray(meta)) throw new Error('The "meta" property passed to "addAction" must be a plain object');
if (typeof actionType !== 'string') throw new Error('The "actionType" property passed to "addAction" must be a string');
}
function getSubPath(path, subPath) {
if (Array.isArray(path)) {
if (Array.isArray(subPath)) {
return [...path, ...subPath];
}
if (typeof subPath === 'string') {
return [...path, ...subPath.split(PATH_JOINER)];
}
return [...path, subPath];
}
if (Array.isArray(subPath)) {
return `${path}.${subPath.join(PATH_JOINER)}`;
}
if (typeof subPath === 'number') {
return [...path.split(PATH_JOINER), subPath];
}
return `${path}.${subPath}`;
}
function validatePath(path, parentPath) {
if (typeof path !== 'number' && !path) throw new Error('No path was provided to "createNamespace" function, which is required');
if (typeof path !== 'string' && !Array.isArray(path) && typeof path !== 'number') throw new Error('The path provided to "createNamespace" function must be a string or array');
if (parentPath && !(parentPath[pathStringSymbol] && parentPath[pathLensSymbol])) throw new Error('When creating a sub path, the parent path must be a valid "path" function returned from "createNamespace"');
if (Array.isArray(path) && !checkPathArray(path)) throw new Error('When using an array to "createNamespace", only strings and numbers are permitted');
}
function ensurePath(path) {
if (!Array.isArray(path) && typeof path !== 'number') {
const split = path.split(PATH_JOINER);
if (split.length > 1) return split;
return split[0];
}
return path;
}
function createLens(path, parentPath) {
let lens;
if (Array.isArray(path)) lens = lensPath(path);
if (typeof path === 'number') lens = lensIndex(path);
if (typeof path === 'string') lens = lensProp(path);
return parentPath
? x => parentPath[pathLensSymbol](lens(x))
: lens;
}
function setNamespace(path, parentPath) {
validatePath(path, parentPath);
const lens = createLens(ensurePath(path), parentPath);
const pathString = parentPath ? getSubPath(parentPath[pathStringSymbol], path) : path;
const prefix = getPathPrefix(pathString);
if (_namespaces.has(prefix)) throw new Error(`The path "${prefix}" already exists`);
_namespaces.set(prefix, createActionContainer(lens));
return { lens, prefix };
}
function createNamespace(p, parentPath) {
const { lens, prefix } = setNamespace(p, parentPath);
function mapActionToReducer(actionType, reducer = defaultReducer, meta = {}) {
validateAddActionArgs(actionType, reducer, meta);
const _pipeline = [];
let _createSideEffect = createNoSideEffect;
const type = getActionName(prefix, actionType);
function getPipeline() {
return _pipeline;
}
getNamespace(prefix).set(type, reducer, getPipeline);
function actionCreator(...args) {
return {
type,
payload: _createSideEffect(_store, _actionCreators)(...args),
meta,
};
}
function withSideEffect(createSideEffect) {
if (typeof createSideEffect !== 'function') throw new Error('Value supplied to "withSideEffect" must be a function');
_createSideEffect = createSideEffect;
return actionCreator;
}
function withPipeline(...args) {
_pipeline.push(...args);
return actionCreator;
}
actionCreator.withSideEffect = withSideEffect;
actionCreator.withPipeline = withPipeline;
return actionCreator;
}
function wrapReducer(func) {
return (state, payload) => set(lens, func(view(lens, state), payload, state), state);
}
return {
[pathStringSymbol]: prefix,
[pathLensSymbol]: lens,
examine: view(lens),
mapActionToReducer,
wrapReducer,
lens,
};
}
function createReducer(initialState = {}) {
const initState = typeof initialState === 'function' ? initialState() : initialState;
return function reducer(state, { type, payload }) {
const actions = getNamespace(getNamespaceName(type));
if (actions && actions.has(type)) {
const result = actions.get(type)(state, payload);
return result;
}
return state === undefined
? initState
: state;
};
}
function setStore(store, actionCreators) {
_store = store;
_actionCreators = actionCreators;
return _store;
}
function createArrayNamespace(path, nested, mapper, isString) {
const namespace = createNamespace(path);
const _indexNamespaces = [];
function arrayNamespace(index) {
if (!_indexNamespaces[index]) {
_indexNamespaces[index] = mapper(nested, [...path, index]);
}
return _indexNamespaces[index];
}
Object.keys(namespace).forEach((key) => {
arrayNamespace[key] = namespace[key];
});
if (isString) {
arrayNamespace.examine = data => (
Object.keys(data).reduce((str, key) => `${str}${data[key]}`, '')
);
}
return arrayNamespace;
}
function mapNamespacesToTarget(target, prevKey = []) {
if (Array.isArray(target) || typeof target === 'string') {
let nested;
let isString;
if (Array.isArray(target)) {
nested = target.find(val => Array.isArray(val) || isPlainObject(val));
}
if (typeof target === 'string') isString = true;
return createArrayNamespace(prevKey, nested, mapNamespacesToTarget, isString);
}
if (isPlainObject(target)) {
return Object.keys(target).reduce((cloned, key) => {
const path = [...prevKey, key];
if (isPlainObject(target[key])) {
return {
...cloned,
[key]: mapNamespacesToTarget(target[key], path),
};
}
if (Array.isArray(target[key])) {
return { ...cloned, [key]: mapNamespacesToTarget(target[key], path) };
}
return { ...cloned, [key]: createNamespace(path) };
}, createNamespace(prevKey));
}
return createNamespace(prevKey);
}
function mapNamespaces(target) {
if (!Array.isArray(target) && !isPlainObject(target) && typeof target !== 'string') {
throw new TypeError(`mapNamespaces only maps namespaces to arrays and objects. Instead you provided ${target}, which is of type ${typeof target}`);
}
return mapNamespacesToTarget(target);
}
return {
createNamespace,
createReducer,
setStore,
mapNamespaces,
};
}
const { createNamespace, createReducer, setStore, mapNamespaces } = createPathspace();
export {
createNamespace,
createReducer,
setStore,
mapNamespaces,
createPathspace,
};