// import { combineReducers } from 'redux'; const REDUX_PATH_SEPARATOR = '/'; function normalizeNamespace(name) { return name.replace(/_(.)/, match => match[1].toUpperCase()); } 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); // Validate params, they should all have a validator function as a value if (params) { const keysWithNonFunctionValidator = Object.keys(params) .filter(paramName => typeof (params[paramName]) !== 'function') .map(v => JSON.stringify(v)); if (keysWithNonFunctionValidator.length > 0) { throw new Error(`Following params of ${JSON.stringify(id)} have a non-function value as validator: ${keysWithNonFunctionValidator.join(', ')}`); } } 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 === undefined || validator === null) { 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': // validator returns either true on success or false on error if (!result) { result = new Error(`${JSON.stringify(key)}: invalid value: ${JSON.stringify(value)}`); } else { result = null; } break; default: // error or string (or something else?) if (result instanceof Error) { // modify message to include key result.message = `${JSON.stringify(key)}: ${result.message}`; } else { // use value as part of error reaosn 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 .map(normalizeNamespace) .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; } }