2018-04-24 15:11:09 +00:00
|
|
|
// import { combineReducers } from 'redux';
|
2018-04-24 10:13:48 +00:00
|
|
|
|
|
|
|
const REDUX_PATH_SEPARATOR = '/';
|
|
|
|
|
2018-04-24 15:11:09 +00:00
|
|
|
function normalizeNamespace(name) {
|
|
|
|
return name.replace(/_(.)/, match => match[1].toUpperCase());
|
|
|
|
}
|
|
|
|
|
2018-04-24 10:13:48 +00:00
|
|
|
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,
|
|
|
|
{
|
2018-04-24 15:11:09 +00:00
|
|
|
params = {},
|
2018-04-24 10:13:48 +00:00
|
|
|
reducer,
|
|
|
|
},
|
|
|
|
]) => {
|
|
|
|
const id = this.actionType(name);
|
2018-04-24 15:11:09 +00:00
|
|
|
|
|
|
|
// 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(', ')}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-24 10:13:48 +00:00
|
|
|
const target = {
|
|
|
|
...current,
|
|
|
|
actions: {
|
|
|
|
...current.actions,
|
|
|
|
[name]: (payload = {}) => {
|
|
|
|
// Validate all payload values
|
|
|
|
const err = Object.entries(payload)
|
|
|
|
.map(([key, value]) => {
|
|
|
|
const validator = params[key];
|
2018-04-24 15:11:09 +00:00
|
|
|
if (validator === undefined || validator === null) {
|
2018-04-24 10:13:48 +00:00
|
|
|
console.warn(`${id}: extra argument ${JSON.stringify(key)}, ignoring`);
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
let result = validator(value);
|
2018-04-24 15:11:09 +00:00
|
|
|
// console.debug(`${id}: verifying ${JSON.stringify(key)} result:`, result);
|
2018-04-24 10:13:48 +00:00
|
|
|
switch (typeof result) {
|
2018-04-24 15:11:09 +00:00
|
|
|
case 'boolean': // validator returns either true on success or false on error
|
2018-04-24 10:13:48 +00:00
|
|
|
if (!result) {
|
|
|
|
result = new Error(`${JSON.stringify(key)}: invalid value: ${JSON.stringify(value)}`);
|
|
|
|
} else {
|
|
|
|
result = null;
|
|
|
|
}
|
|
|
|
break;
|
2018-04-24 15:11:09 +00:00
|
|
|
default: // error or string (or something else?)
|
|
|
|
if (result instanceof Error) { // modify message to include key
|
2018-04-24 10:13:48 +00:00
|
|
|
result.message = `${JSON.stringify(key)}: ${result.message}`;
|
2018-04-24 15:11:09 +00:00
|
|
|
} else { // use value as part of error reaosn
|
2018-04-24 10:13:48 +00:00
|
|
|
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) {
|
2018-04-24 15:11:09 +00:00
|
|
|
// console.debug(`${type}: skipping action-mismatching reducer`, { state, payload });
|
2018-04-24 10:13:48 +00:00
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Call original reducer with our payload
|
2018-04-24 15:11:09 +00:00
|
|
|
// console.debug(`${type}: action-matched reducer called`, { state, payload });
|
2018-04-24 10:13:48 +00:00
|
|
|
const result = reducer(state, payload);
|
2018-04-24 15:11:09 +00:00
|
|
|
// console.debug(' =>', result);
|
2018-04-24 10:13:48 +00:00
|
|
|
return result;
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return target;
|
|
|
|
}, {
|
|
|
|
actions: {},
|
|
|
|
actionTypes: {},
|
|
|
|
reducers: Object.entries(reducers)
|
|
|
|
.reduce((current, [name, reducer]) => ({
|
|
|
|
[name]: (state, ...args) => {
|
2018-04-24 15:11:09 +00:00
|
|
|
// console.debug(`${this._id.join('/', `reducer:${name}`)}: called`, { state, args });
|
2018-04-24 10:13:48 +00:00
|
|
|
const result = reducer(state, ...args);
|
2018-04-24 15:11:09 +00:00
|
|
|
// console.debug(' =>', result);
|
2018-04-24 10:13:48 +00:00
|
|
|
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
|
2018-04-24 15:11:09 +00:00
|
|
|
.map(normalizeNamespace)
|
2018-04-24 10:13:48 +00:00
|
|
|
.reduce(
|
|
|
|
(currentState, namespaceName) =>
|
|
|
|
currentState[namespaceName], state);
|
2018-04-24 15:11:09 +00:00
|
|
|
// console.debug(`${[...this._id, `selector:${name}`].join('/')}: called`, { state: namespaceState, args });
|
2018-04-24 10:13:48 +00:00
|
|
|
const result = selector(namespaceState, ...args);
|
2018-04-24 15:11:09 +00:00
|
|
|
// console.debug(' =>', result);
|
2018-04-24 10:13:48 +00:00
|
|
|
return result;
|
|
|
|
},
|
|
|
|
}), selectors);
|
|
|
|
|
|
|
|
retval.reducer = (state, ...args) => {
|
2018-04-24 15:11:09 +00:00
|
|
|
// console.debug(`${this._id.join('/')}: combined reducer called`, { state, args });
|
2018-04-24 10:13:48 +00:00
|
|
|
|
|
|
|
if (state === undefined) {
|
2018-04-24 15:11:09 +00:00
|
|
|
// console.debug(`${this._id.join('/')}: undefined state passed to reducer`, { state, args });
|
2018-04-24 10:13:48 +00:00
|
|
|
const result = initialState;
|
2018-04-24 15:11:09 +00:00
|
|
|
// console.debug(' =>', result);
|
2018-04-24 10:13:48 +00:00
|
|
|
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,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-04-24 15:11:09 +00:00
|
|
|
// console.debug(`${this._id.join('/')}: no reducers for this namespace, skipping`);
|
2018-04-24 10:13:48 +00:00
|
|
|
return state;
|
|
|
|
};
|
|
|
|
|
|
|
|
// if (initialState) {
|
|
|
|
// retval.initialState = initialState;
|
|
|
|
// }
|
|
|
|
|
2018-04-24 15:11:09 +00:00
|
|
|
// console.debug('generated redux module:', retval);
|
2018-04-24 10:13:48 +00:00
|
|
|
return retval;
|
|
|
|
}
|
|
|
|
}
|