From 925554832674488d563b4fc67b09e21f4da17f05 Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Sun, 28 Aug 2022 12:47:24 -0400 Subject: [PATCH] add mutations --- .eslintrc.cjs | 4 +-- TODO | 4 +++ src/Updux.js | 52 ++++++++++++++++++++++++++++++-------- src/dux-selectors.test.js | 18 ++++++------- src/mutations.test.js | 53 +++++++++++++++++++++++++++++++++++---- src/selectors.js | 26 ++++++++++--------- src/upreducer.js | 35 ++++++++++++++++++++++++++ 7 files changed, 152 insertions(+), 40 deletions(-) create mode 100644 TODO create mode 100644 src/upreducer.js diff --git a/.eslintrc.cjs b/.eslintrc.cjs index dd2ef33..ea428bc 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -8,9 +8,9 @@ module.exports = { browser: true, }, plugins: ['todo-plz', 'no-only-tests'], - overrides: [ - ], + overrides: [], rules: { + 'no-console': ['error'], 'todo-plz/ticket-ref': ['error', { pattern: 'GT[0-9]+' }], 'no-only-tests/no-only-tests': [ 'error', diff --git a/TODO b/TODO new file mode 100644 index 0000000..4523e4c --- /dev/null +++ b/TODO @@ -0,0 +1,4 @@ +- setMutation +- check that the mutations mutate +- documentation generator (mkdocs + jsdoc-to-markdown) +- createStore diff --git a/src/Updux.js b/src/Updux.js index 3fe50fe..f88334d 100644 --- a/src/Updux.js +++ b/src/Updux.js @@ -1,7 +1,9 @@ import R from 'remeda'; +import u from 'updeep'; import { createStore as reduxCreateStore } from 'redux'; import { buildSelectors } from './selectors.js'; +import { buildUpreducer } from './upreducer.js'; import { action } from './actions.js'; function isActionGen(action) { @@ -34,7 +36,11 @@ export class Updux { this.#addSubduxActions(slice, sub), ); - this.#selectors = buildSelectors( config.selectors, config.splatSelectors, this.#subduxes ); + this.#selectors = buildSelectors( + config.selectors, + config.splatSelectors, + this.#subduxes, + ); } #addSubduxActions(_slice, subdux) { @@ -73,19 +79,43 @@ export class Updux { } get upreducer() { - return (action) => (state) => { - const mutation = this.#mutations[action.type]; - - if (mutation) { - state = mutation(action.payload, action)(state); - } - - return state; - }; + return buildUpreducer(this.#mutations, this.#subduxes); } + /** + * + * @param {string | Function} action - Action triggering the mutation. If + * the action is a string, it has to have been previously declared for this + * updux, but if it's a function generator, it'll be automatically added to the + * updux if not already present (the idea being that making a typo on a string + * is easy, but passing a wrong function very more unlikely). + * @param {Function} mutation - Mutating function. + * @return {void} + */ setMutation(action, mutation) { - this.#mutations[action.type] = mutation; + // TODO option strict: false to make it okay to auto-create + // the actions as strings? + if (action.type) { + if (!this.#actions[action.type]) { + this.#actions[action.type] = action; + } else if (this.#actions[action.type] !== action) { + throw new Error( + `action '${action.type}' not defined for this updux or definition is different`, + ); + } + + action = action.type; + } + + if (!this.#actions[action]) { + throw new Error(`action '${action}' is not defined`); + } + + this.#mutations[action] = mutation; + } + + get mutations() { + return this.#mutations; } createStore(initial = undefined, enhancerGenerator = undefined) { diff --git a/src/dux-selectors.test.js b/src/dux-selectors.test.js index fdf40c7..5ddbd5c 100644 --- a/src/dux-selectors.test.js +++ b/src/dux-selectors.test.js @@ -2,25 +2,23 @@ import { test, expect } from 'vitest'; import { dux } from './Updux.js'; -test( "basic selectors", () => { - +test('basic selectors', () => { const foo = dux({ initial: { x: 1, }, selectors: { - getX: ({x}) => x, + getX: ({ x }) => x, }, subduxes: { bar: { initial: { y: 2 }, selectors: { - getY: ({y}) => y - } - } - } + getY: ({ y }) => y, + }, + }, + }, }); - expect( foo.selectors.getY({bar:{y:3}} ) ).toBe(3); - -} ); + expect(foo.selectors.getY({ bar: { y: 3 } })).toBe(3); +}); diff --git a/src/mutations.test.js b/src/mutations.test.js index f71c041..d1c532a 100644 --- a/src/mutations.test.js +++ b/src/mutations.test.js @@ -4,7 +4,7 @@ import u from 'updeep'; import { action } from './actions.js'; -import { Updux } from './Updux.js'; +import { Updux, dux } from './Updux.js'; test('set a mutation', () => { const dux = new Updux({ @@ -17,14 +17,57 @@ test('set a mutation', () => { }, }); - dux.setMutation(dux.actions.foo, (payload, action) => - u({ + dux.setMutation(dux.actions.foo, (payload, action) => { + expect(payload).toEqual({ x: 'hello ' }); + expect(action).toEqual(dux.actions.foo('hello ')); + return u({ x: payload.x + action.type, - }), - ); + }); + }); const result = dux.reducer(undefined, dux.actions.foo('hello ')); expect(result).toEqual({ x: 'hello foo', }); }); + +test('mutation of a subdux', async () => { + const bar = dux({ + actions: { + baz: null, + }, + }); + bar.setMutation('baz', () => (state) => ({ ...state, x: 1 })); + + const foo = dux({ + subduxes: { bar }, + }); + + const store = foo.createStore(); + store.dispatch.baz(); + expect(store.getState()).toMatchObject({ bar: { x: 1 } }); +}); + +test('strings and generators', async () => { + const actionA = action('a'); + + const foo = dux({ + actions: { + b: null, + a: actionA, + }, + }); + + // as a string and defined + expect(() => foo.setMutation('a', () => {})).not.toThrow(); + + // as a generator and defined + expect(() => foo.setMutation(actionA, () => {})).not.toThrow(); + + // as a string, not defined + expect(() => foo.setMutation('c', () => {})).toThrow(); + + foo.setMutation(action('d'), () => {}); + + expect(foo.actions.d).toBeTypeOf('function'); +}); diff --git a/src/selectors.js b/src/selectors.js index dce582d..00af786 100644 --- a/src/selectors.js +++ b/src/selectors.js @@ -3,17 +3,19 @@ import R from 'remeda'; export function buildSelectors( localSelectors, splatSelector = {}, - subduxes = {} + subduxes = {}, ) { - const subSelectors = Object.entries(subduxes).map(([slice,{ selectors }]) => { - if (!selectors) return {}; - if (slice === '*') return {}; + const subSelectors = Object.entries(subduxes).map( + ([slice, { selectors }]) => { + if (!selectors) return {}; + if (slice === '*') return {}; - return R.mapValues( - selectors, - (func) => (state) => func(state[slice]) - ); - }); + return R.mapValues( + selectors, + (func) => (state) => func(state[slice]), + ); + }, + ); let splat = {}; @@ -28,10 +30,10 @@ export function buildSelectors( res, mapValues( subduxes['*'].selectors, - (selector) => () => selector(value) - ) + (selector) => () => selector(value), + ), ); }; } - return R.mergeAll([ ...subSelectors, localSelectors, splat ]); + return R.mergeAll([...subSelectors, localSelectors, splat]); } diff --git a/src/upreducer.js b/src/upreducer.js new file mode 100644 index 0000000..fc09070 --- /dev/null +++ b/src/upreducer.js @@ -0,0 +1,35 @@ +import R from 'remeda'; +import u from 'updeep'; + +const localMutation = (mutations) => (action) => (state) => { + const mutation = mutations[action.type]; + + if (!mutation) return state; + + return mutation(action.payload, action)(state); +}; + +const subMutations = (subduxes) => (action) => (state) => { + const subReducers = + Object.keys(subduxes).length > 0 + ? R.mapValues(subduxes, R.prop('upreducer')) + : null; + + if (!subReducers) return state; + + if (subReducers['*']) { + return u.updateIn('*', subReducers['*'](action), state); + } + + const update = R.mapValues(subReducers, (upReducer) => upReducer(action)); + + return u(update, state); +}; + +export function buildUpreducer(mutations, subduxes) { + return (action) => (state) => { + state = subMutations(subduxes)(action)(state); + + return localMutation(mutations)(action)(state); + }; +}