import { combineReducers } from 'redux'; const REDUX_PATH_SEPARATOR = '/'; export default class ReduxGenerator { constructor(...id) { this._id = id; } sub(name) { return new ReduxGenerator(...this._id, name); } actionType(name) { return [...this._id, name] .join(REDUX_PATH_SEPARATOR); } actionObject(name, payload) { return { type: this.actionType(name), payload, }; } selector(selector) { return (state, ...params) => { const selectedState = this._id .slice(1) // ignore root id .reduce((current, id) => current[id], state); return selector(selectedState, ...params); }; } all({ actions = {}, selectors = {}, reducers = {}, initialState = null, }) { /* Descriptor = { someName: { params: { param1: (v) => typeof(value) === "string", }, reducer(currentState, payload) { // ... do stuff return { ...currentState, ...newStateDiff, }; }, }, // ... } */ const retval = Object.entries(actions) .reduce((current, [ name, { params, reducer, }, ]) => { const id = this.actionType(name); const target = { ...current, actions: { ...current.actions, [name]: (payload = {}) => { // Validate all payload values const err = Object.entries(payload) .map(([key, value]) => { const validator = params[key]; if (!validator) { console.warn(`${id}: extra argument ${JSON.stringify(key)}, ignoring`); return undefined; } let result = validator(value); console.debug(`${id}: verifying ${JSON.stringify(key)} result:`, result); switch (typeof result) { case 'boolean': if (!result) { result = new Error(`${JSON.stringify(key)}: invalid value: ${JSON.stringify(value)}`); } else { result = null; } break; default: if (result instanceof Error) { result.message = `${JSON.stringify(key)}: ${result.message}`; } else { result = new Error(`${JSON.stringify(key)}: ${result}`); } } Object.freeze(result); return result; }) .find(v => !!v); if (err) { throw err; } // Create action object return { type: id, payload, }; }, }, actionTypes: { ...current.actionTypes, [name]: id, }, }; if (reducer) { target.reducers = { ...current.reducers, [name]: (state, { type, payload }) => { // Ignore non-matching actions if (type !== id) { console.debug(`${type}: skipping action-mismatching reducer`, { state, payload }); return state; } // Call original reducer with our payload console.debug(`${type}: action-matched reducer called`, { state, payload }); const result = reducer(state, payload); console.debug(' =>', result); return result; }, }; } return target; }, { actions: {}, actionTypes: {}, reducers: Object.entries(reducers) .reduce((current, [name, reducer]) => ({ [name]: (state, ...args) => { console.debug(`${this._id.join('/', `reducer:${name}`)}: called`, { state, args }); const result = reducer(state, ...args); console.debug(' =>', result); return result; }, }), {}), selectors: {}, }); retval.selectors = Object.entries(selectors) .reduce((current, [name, selector]) => ({ ...current, [name]: (state, ...args) => { const namespaceState = this._id .slice(1) // ignore root id .reduce( (currentState, namespaceName) => currentState[namespaceName], state); console.debug(`${[...this._id, `selector:${name}`].join('/')}: called`, { state: namespaceState, args }); const result = selector(namespaceState, ...args); console.debug(' =>', result); return result; }, }), selectors); retval.reducer = (state, ...args) => { console.debug(`${this._id.join('/')}: combined reducer called`, { state, args }); if (state === undefined) { console.debug(`${this._id.join('/')}: undefined state passed to reducer`, { state, args }); const result = initialState; console.debug(' =>', result); return result; } // not using combineReducers here as we want all reducers to act on the same namespace if (retval.reducers) { return Object.values(retval.reducers) .filter(value => typeof (value) === 'function') .reduce( (current, reducer) => reducer(current, ...args), state, ); } console.debug(`${this._id.join('/')}: no reducers for this namespace, skipping`); return state; }; // if (initialState) { // retval.initialState = initialState; // } console.debug('generated redux module:', retval); return retval; } }