| 
									
										
										
										
											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; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } |