eldewrito-menu/packages/frontend/src/website/redux/base.js

203 lines
5.8 KiB
JavaScript
Raw Normal View History

2018-04-24 10:13:48 +00:00
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;
}
}