tests are working!

This commit is contained in:
Yanick Champoux 2019-10-19 13:11:30 -04:00
parent a934133cc1
commit f24464a590
6 changed files with 366 additions and 47 deletions

233
README.md
View File

@ -17,7 +17,238 @@ All that to say, I had some fun yesterday and hacked a proto-lovechild
of `Rematch` and `Updeep`, with a dash of [VueX][] inspiration. of `Rematch` and `Updeep`, with a dash of [VueX][] inspiration.
I call it... `Updux`. I call it... `Updux`.
## Example # Synopsis
```
import updux from 'updux';
import otherUpdux from './otherUpdux';
const {
initial,
reducer,
actions,
middleware,
createStore,
} = updux({
initial: {},
subduxes: {
otherUpdux,
},
mutations: {
},
effects: {
},
})
```
# Description
`Updux` exports one function, `updux`, both as a named export and as
its default export.
## helpers = updux(config);
`updux` is a way to minimize and simplify the boilerplate associated with the
creation of a `Redux` store. It takes a shorthand configuration
object, and generates the appropriate reducer, actions, middleware, etc.
In true `Redux`-like fashion, just like reducers can be composed
of sub-reducers, upduxs can be made of sub-upduxs.
### config
The config object recognize following properties.
#### initial
The default initial state of the reducer. Can be anything your
heart desires.
#### subduxes
Object mapping slices of the state to sub-upduxs.
For example, if in plain Redux you would do
```
import { combineReducers } from 'redux';
import todosReducer from './todos';
import statisticsReducer from './statistics';
const rootReducer = combineReducers({
todos: todosReducer,
stats: statisticsReducer,
});
```
then with Updux you'd do
```
import { updux } from 'updux';
import todos from './todos';
import statistics from './statistics';
const rootUpdux = updux({
subduxes: {
todos, statistics
}
});
```
#### mutations
Object mapping actions to the associated state mutation.
For example, in `Redux` you'd do
```
function todosReducer(state=[],action) {
switch(action.type) {
case 'ADD': return [ ...state, action.payload ];
case 'DONE': return state.map( todo => todo.id === action.payload
? { ...todo, done: true } : todo )
default: return state;
}
}
```
With Updux:
```
const todosUpdux = updux({
mutations: {
add: todo => state => [ ...state, todo ],
done: done_id => u.map( u.if( ({id} => id === done_id), {done: true} ) )
}
});
```
The signature of the mutations is `(payload,action) => state => newState`.
It is designed to play well with `Updeep`. This way, instead of doing
```
mutation: {
renameTodo: newName => state => { ...state, name: newName }
}
```
we can do
```
mutation: {
renameTodo: newName => u({ name: newName })
}
```
Also, the special key `*` can be used to match any
action not explicitly matched by other mutations.
```
const todosUpdux = updux({
mutations: {
add: todo => state => [ ...state, todo ],
done: done_id => u.map( u.if( ({id} => id === done_id), {done: true} ) ),
'*' (payload,action) => state => {
console.warn( "unexpected action ", action.type );
return state;
},
}
});
```
#### effects
Plain object defining asynchronous actions and side-effects triggered by actions.
The effects themselves are Redux middleware, expect with the `dispatch`
property of the first argument augmented with all the available actions.
```
updux({
effects: {
fetch: ({dispatch}) => next => async (action) => {
next(action);
let result = await fetch(action.payload.url).then( result => result.json() );
dispatch.fetchSuccess(result);
}
}
});
```
## return value
`updux` returns an object with the following properties:
### initial
Default initial state of the reducer. If applicable, merge
the initial states of `config` and `subduxes`, with
`config` having precedence over `subduxes`.
If nothing was given, defaults to an empty object.
### reducer
A Redux reducer generated using the computed initial state and
mutations.
### mutations
Merge of the config and subduxes mutations. If an action trigger
mutations in both the main updux and its subduxes, the subduxes
mutations will be performed first.
### actions
Action creators for all actions used in the mutations, effects and subdox
of the updox config.
The action creators have the signature `(payload={},meta={}) => ({type,
payload,meta})` (with the extra sugar that if `meta` or `payload` are not
specified, the key is not present in the produced action).
### middleware
A middleware aggregating all the effects defined in the
updox and its subduxes. Effects of the updox itself are
done before the subdoxes effects.
### createStore
Same as doing
```
import { createStore, applyMiddleware } from 'redux';
const { initial, reducer, middleware, actions } = updox(...);
const store = createStore( initial, reducer, applyMiddleware(middleware) );
for ( let type in actions ) {
store.dispatch[type] = (...args) => {
store.dispatch(actions[type](...args))
};
}
```
So that later on you can do
```
store.dispatch.addTodo(...);
// still work
store.dispatch( actions.addTodo(...) );
```
# Example
#### battle.js #### battle.js

View File

@ -1,4 +1,5 @@
module.exports = { module.exports = {
"plugins": [["@babel/plugin-proposal-pipeline-operator", { "proposal": "minimal" }]],
presets: [ presets: [
[ [
'@babel/preset-env', '@babel/preset-env',

View File

@ -11,5 +11,8 @@
"lodash": "^4.17.15", "lodash": "^4.17.15",
"redux": "^4.0.4", "redux": "^4.0.4",
"updeep": "^1.2.0" "updeep": "^1.2.0"
},
"devDependencies": {
"@babel/plugin-proposal-pipeline-operator": "^7.5.0"
} }
} }

View File

@ -9,12 +9,12 @@ function actionFor(type) {
}; };
} }
function buildInitial({initial = {}, reducers = {}}) { function buildInitial({initial = {}, subduxes = {}}) {
let state = initial; let state = initial;
if (fp.isPlainObject(initial)) { if (fp.isPlainObject(initial)) {
initial = fp.mergeAll([ initial = fp.mergeAll([
fp.mapValues(fp.getOr({}, 'initial'), reducers), fp.mapValues(fp.getOr({}, 'initial'), subduxes),
initial, initial,
]); ]);
} }
@ -22,8 +22,8 @@ function buildInitial({initial = {}, reducers = {}}) {
return initial; return initial;
} }
function buildActions({mutations = {}, reducers = {}}) { function buildActions({mutations = {}, subduxes = {}}) {
let actions = fp.mergeAll(fp.map(fp.getOr({}, 'actions'), reducers)) || {}; let actions = fp.mergeAll(fp.map(fp.getOr({}, 'actions'), subduxes)) || {};
Object.keys(mutations).forEach(type => { Object.keys(mutations).forEach(type => {
if (!actions[type]) { if (!actions[type]) {
@ -34,41 +34,71 @@ function buildActions({mutations = {}, reducers = {}}) {
return actions; return actions;
} }
function buildMutations({mutations = {}, reducers = {}}) { const composeMutations = (m1,m2) =>
let subMut = {}; (payload=null,action={}) => state => m2(payload,action)(
m1(payload,action)(state) );
for (let slice in reducers) { function buildMutations({mutations = {}, subduxes = {}}) {
for (let mutation in reducers[slice].mutations) { // we have to differentiate the subduxes with '*' than those
subMut = u( // without, as the root '*' is not the same as any sub-'*'
{
[mutation]: {
[slice]: u.constant(reducers[slice].mutations[mutation]),
},
},
subMut,
);
}
}
subMut = fp.mapValues(updates => action => const actions = fp.uniq( Object.keys(mutations).concat(
u(fp.mapValues(f => f(action))(updates)), ...Object.values( subduxes ).map( ({mutations}) => Object.keys(mutations) )
)(subMut); ) );
for (let name in mutations) { // let's seed with noops
if (subMut[name]) { let mergedMutations = {};
const pre = subMut[name];
subMut[name] = action => state => actions.forEach( action => {
mutations[name](action)(pre(action)(state)); mergedMutations[action] = () => state => state;
} else { });
subMut[name] = mutations[name];
}
} console.log(mergedMutations);
Object.entries( subduxes ).forEach( ([slice, {mutations,reducer}]) => {
if( mutations['*'] ) {
const localized = (payload=null,action={}) => u.updateIn( slice, mutations['*'](payload,action) );
console.log("b");
mergedMutations = fp.mapValues(
mutation => composeMutations(
(dummy,action) => u.updateIn(slice,
state => reducer(state,action)
), mutation )
)(mergedMutations);
return;
}
Object.entries(mutations).forEach(([type,mutation]) => {
const localized = (payload=null,action={}) => u.updateIn( slice, mutation(payload,action) );
if( type !== '*' ) {
console.log("a");
mergedMutations[type] = composeMutations(
localized, mergedMutations[type]
)
}
else {
}
})
});
console.log(mergedMutations);
Object.entries(mutations).forEach(([type,mutation]) => {
console.log(type,":",mutation,":",mergedMutations[type]);
mergedMutations[type] = composeMutations(
mergedMutations[type], mutation
)
});
return mergedMutations;
return subMut;
} }
function buildMiddleware({effects={},reducers={}},{actions}) { function buildMiddleware({effects={},subduxes={}},{actions}) {
return api => { return api => {
for ( let type in actions ) { for ( let type in actions ) {
@ -79,14 +109,12 @@ function buildMiddleware({effects={},reducers={}},{actions}) {
return [ return [
...fp.toPairs(effects).map(([type,effect])=> { ...fp.toPairs(effects).map(([type,effect])=> {
return api => next => action => { return api => next => action => {
console.log(action);
if( action.type !== type ) return next(action); if( action.type !== type ) return next(action);
return effect(api)(next)(action); return effect(api)(next)(action);
}; };
}), }),
...fp.map( 'middleware', reducers ) ...fp.map( 'middleware', subduxes )
].filter(x=>x).reduceRight( (next,mw) => mw(api)(next), original_next ) ].filter(x=>x).reduceRight( (next,mw) => mw(api)(next), original_next )
}} }}
} }
@ -100,7 +128,7 @@ function updux(config) {
dux.mutations = buildMutations(config); dux.mutations = buildMutations(config);
dux.upreducer = action => state => { dux.upreducer = (action={}) => state => {
if (state === null) state = dux.initial; if (state === null) state = dux.initial;
const a = const a =

63
src/splat.test.js Normal file
View File

@ -0,0 +1,63 @@
import updux from '.';
import u from 'updeep';
const tracer = chr => u({ tracer: s => (s||'') + chr });
test( 'mutations, simple', () => {
const dux = updux({
mutations: {
foo: () => tracer('a'),
'*': (p,a) => { console.log(a); return tracer('b') },
},
});
const store = dux.createStore();
expect(store.getState()).toEqual({ tracer: 'b'});
store.dispatch.foo();
expect(store.getState()).toEqual({ tracer: 'ba', });
store.dispatch({ type: 'bar' });
expect(store.getState()).toEqual({ tracer: 'bab', });
});
test( 'with subduxes', () => {
const dux = updux({
mutations: {
foo: () => tracer('a'),
'*': (dummy,a) => { console.log("got XXX " ,a); return tracer('b') },
bar: () => ({bar}) => ({ bar, tracer: bar.tracer })
},
subduxes: {
bar: updux({
mutations: {
foo: () => tracer('d'),
'*': (dummy,a) => { console.log( "got a ", dummy, a ); return tracer('e') },
},
}),
},
});
const store = dux.createStore();
expect(store.getState()).toEqual({
tracer: 'b',
bar: { tracer: 'e' } });
store.dispatch.foo();
expect(store.getState()).toEqual({
tracer: 'ba',
bar: { tracer: 'ed' } });
store.dispatch({type: 'bar'});
expect(store.getState()).toEqual({
tracer: 'ede',
bar: { tracer: 'ede' } });
});

View File

@ -55,7 +55,7 @@ test( 'sub reducers', () => {
}); });
const { initial, actions, reducer } = updux({ const { initial, actions, reducer } = updux({
reducers: { subduxes: {
foo, bar foo, bar
} }
}); });
@ -99,7 +99,7 @@ test('precedence between root and sub-reducers', () => {
} }
} }
}, },
reducers: { subduxes: {
foo: updux({ foo: updux({
initial: { initial: {
bar: 2, bar: 2,
@ -135,26 +135,21 @@ test( 'middleware', async () => {
mutations: { mutations: {
inc: (addition) => state => state + addition, inc: (addition) => state => state + addition,
doEeet: () => state => { doEeet: () => state => {
console.log("up", state);
return state + 'Z'; return state + 'Z';
}, },
}, },
effects: { effects: {
doEeet: api => next => async action => { doEeet: api => next => async action => {
console.log(api);
api.dispatch.inc('a'); api.dispatch.inc('a');
next(action); next(action);
await timeout(1000); await timeout(1000);
api.dispatch.inc('c'); api.dispatch.inc('c');
} }
}, },
reducers: { subduxes: {
foo: updux({ foo: updux({
effects: { effects: {
doEeet: (api) => next => action => { doEeet: (api) => next => action => {
console.log("in foo",api);
api.dispatch({ type: 'inc', payload: 'b'}); api.dispatch({ type: 'inc', payload: 'b'});
next(action); next(action);
} }
@ -165,8 +160,6 @@ test( 'middleware', async () => {
const store = createStore(); const store = createStore();
console.log(store);
store.dispatch.doEeet(); store.dispatch.doEeet();
expect(store.getState()).toEqual( 'abZ' ); expect(store.getState()).toEqual( 'abZ' );