203 lines
5.8 KiB
JavaScript
203 lines
5.8 KiB
JavaScript
|
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;
|
||
|
}
|
||
|
}
|