From e78568b3cbe4c4addf020ae42f87f5fc47786ba5 Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Thu, 2 Jan 2020 13:53:24 -0500 Subject: [PATCH] pass mutations in constructor as a map --- Changes | 3 ++ src/addMutations.test.ts | 29 +++++------ src/buildActions/index.ts | 96 ++++++++++++++++++++-------------- src/buildMutations/index.ts | 60 ++++++++++----------- src/index.ts | 10 ++-- src/mutations.test.ts | 25 +++++++++ src/types.ts | 39 ++++++++------ src/updux.ts | 101 ++++++++++++++++++------------------ 8 files changed, 203 insertions(+), 160 deletions(-) create mode 100644 src/mutations.test.ts diff --git a/Changes b/Changes index efc262f..788bfd1 100644 --- a/Changes +++ b/Changes @@ -1,5 +1,8 @@ # Revision history for Updux +NEXT + - Mutations passed to the constructor can be arrays of arrays. + 1.2.0 2019-11-06 - The middleware's 'getState' returns the local state of its updux, instead of the root state. Plus we add `getRootState` to get diff --git a/src/addMutations.test.ts b/src/addMutations.test.ts index 7718bc2..7de8960 100644 --- a/src/addMutations.test.ts +++ b/src/addMutations.test.ts @@ -1,23 +1,20 @@ -import Updux, { actionCreator } from './updux'; +import Updux, { actionCreator } from "./updux"; type MyState = { - sum: number -} + sum: number; +}; -test( 'added mutation is present', () => { - const updux = new Updux({ - initial: { sum: 0 }, - }); +test("added mutation is present", () => { + const updux = new Updux({ + initial: { sum: 0 } + }); - const add = actionCreator('add', (n : number) => ({n}) ) + const add = actionCreator("add", (n: number) => ({ n })); - // must add 'add' in the actions 9.9 - updux.addMutation( - add, ({n},action) => ({sum}) => ({sum: sum + n}) - ); - updux.mutations; - const store = updux.createStore(); - store.dispatch.add(3); + updux.addMutation(add, ({ n }, action) => ({ sum }) => ({ sum: sum + n })); - expect(store.getState()).toEqual({ sum: 3 }); + const store = updux.createStore(); + store.dispatch.add(3); + + expect(store.getState()).toEqual({ sum: 3 }); }); diff --git a/src/buildActions/index.ts b/src/buildActions/index.ts index 6da0738..8548b75 100644 --- a/src/buildActions/index.ts +++ b/src/buildActions/index.ts @@ -1,59 +1,75 @@ -import fp from 'lodash/fp'; -import { Action, ActionCreator, ActionPayloadGenerator, Dictionary } from '../types'; +import fp from "lodash/fp"; +import { + Action, + ActionCreator, + ActionPayloadGenerator, + Dictionary +} from "../types"; -export function actionCreator( type: T, transform: (...args: any[]) => P ): ActionCreator -export function actionCreator( type: T, transform: never ): ActionCreator -export function actionCreator( type: T, transform: null ): ActionCreator -export function actionCreator(type:any, transform:any ) { +export function actionCreator( + type: T, + transform: (...args: any[]) => P +): ActionCreator; +export function actionCreator( + type: T, + transform: null +): ActionCreator; +export function actionCreator( + type: T +): ActionCreator; +export function actionCreator(type: any, transform?: any) { + if (transform) { + return Object.assign( + (...args: any[]) => ({ type, payload: transform(...args) }), + { type } + ); + } - if( transform ) { - return Object.assign( - (...args: any[]) => ({ type, payload: transform(...args) }), - { type } ) - } + if (transform === null) { + return Object.assign(() => ({ type }), { type }); + } - if( transform === null ) { - return Object.assign( () => ({ type }), { type } ) - } - - return Object.assign( (payload: unknown) => ({type, payload}) ); + return Object.assign((payload: unknown) => ({ type, payload }), { type }); } -function actionFor(type:string): ActionCreator { - const f = ( (payload = undefined, meta = undefined) => - fp.pickBy(v => v !== undefined)({type, payload, meta}) as Action - ); +export function actionFor(type: string): ActionCreator { + const f = (payload = undefined, meta = undefined) => + fp.pickBy(v => v !== undefined)({ type, payload, meta }) as Action; return Object.assign(f, { - _genericAction: true, - type + _genericAction: true, + type }); } -type ActionPair = [ string, ActionCreator ]; +type ActionPair = [string, ActionCreator]; function buildActions( - generators : Dictionary = {}, + generators: Dictionary = {}, actionNames: string[] = [], - subActions : ActionPair[] = [], -):Dictionary { + subActions: ActionPair[] = [] +): Dictionary { + // priority => generics => generic subs => craft subs => creators - // priority => generics => generic subs => craft subs => creators + const [crafted, generic] = fp.partition(([type, f]) => !f._genericAction)( + subActions + ); - const [ crafted, generic ] = fp.partition( - ([type,f]) => !f._genericAction - )( subActions ); + const actions: any = [ + ...actionNames.map(type => [type, actionFor(type)]), + ...generic, + ...crafted, + ...Object.entries( + generators + ).map(([type, payload]: [string, Function]): any => [ + type, + (payload as any).type + ? payload + : (...args: any) => ({ type, payload: payload(...args) }) + ]) + ]; - const actions : any = [ - ...(actionNames.map( type => [ type, actionFor(type) ] )), - ...generic, - ...crafted, - ...Object.entries(generators).map( - ([type, payload]: [ string, Function ]): any => [type, (payload as any).type ? payload : (...args: any) => ({ type, payload: payload(...args) })] - ), - ]; - - return fp.fromPairs(actions); + return fp.fromPairs(actions); } export default buildActions; diff --git a/src/buildMutations/index.ts b/src/buildMutations/index.ts index d20f77a..41265b8 100644 --- a/src/buildMutations/index.ts +++ b/src/buildMutations/index.ts @@ -1,72 +1,68 @@ -import fp from 'lodash/fp'; -import u from 'updeep'; -import {Mutation, Action, Dictionary} from '../types'; +import fp from "lodash/fp"; +import u from "updeep"; +import { Mutation, Action, Dictionary, MutationEntry } from "../types"; const composeMutations = (mutations: Mutation[]) => mutations.reduce((m1, m2) => (payload: any = null, action: Action) => state => - m2(payload, action)(m1(payload, action)(state)), + m2(payload, action)(m1(payload, action)(state)) ); type SubMutations = { - [ slice: string ]: Dictionary -} + [slice: string]: Dictionary; +}; function buildMutations( - mutations :Dictionary = {}, - subduxes = {} + mutations: Dictionary = {}, + subduxes = {} ) { // we have to differentiate the subduxes with '*' than those // without, as the root '*' is not the same as any sub-'*' const actions = fp.uniq( Object.keys(mutations).concat( - ...Object.values(subduxes).map(({mutations = {}}:any) => - Object.keys(mutations), - ), - ), + ...Object.values(subduxes).map(({ mutations = {} }: any) => + Object.keys(mutations) + ) + ) ); - let mergedMutations :Dictionary = {}; + let mergedMutations: Dictionary = {}; let [globby, nonGlobby] = fp.partition( - ([_, {mutations = {}}]:any) => mutations['*'], - Object.entries(subduxes), + ([_, { mutations = {} }]: any) => mutations["*"], + Object.entries(subduxes) ); globby = fp.flow([ fp.fromPairs, - fp.mapValues(({reducer}) => (_:any, action :Action) => ( state: any ) => - reducer(state, action), - ), + fp.mapValues(({ reducer }) => (_: any, action: Action) => (state: any) => + reducer(state, action) + ) ])(globby); - const globbyMutation = (payload:any, action:Action) => - u(fp.mapValues((mut:any) => mut(payload, action))(globby)); + const globbyMutation = (payload: any, action: Action) => + u(fp.mapValues((mut: any) => mut(payload, action))(globby)); actions.forEach(action => { mergedMutations[action] = [globbyMutation]; }); - nonGlobby.forEach(([slice, {mutations = {}, reducer = {}}]:any[]) => { + nonGlobby.forEach(([slice, { mutations = {}, reducer = {} }]: any[]) => { Object.entries(mutations).forEach(([type, mutation]) => { - const localized = (payload = null, action :Action) => { + const localized = (payload = null, action: Action) => { return u.updateIn(slice)((mutation as Mutation)(payload, action)); - } + }; mergedMutations[type].push(localized); }); }); Object.entries(mutations).forEach(([type, mutation]) => { - if ( Array.isArray(mutation) ) { - if( mutation[1] ) { - mergedMutations[type] = [ - mutation[0] - ] - } - else mergedMutations[type].push( mutation[0] ); - } - else mergedMutations[type].push(mutation); + if (Array.isArray(mutation)) { + if (mutation[1]) { + mergedMutations[type] = [mutation[0]]; + } else mergedMutations[type].push(mutation[0]); + } else mergedMutations[type].push(mutation); }); return fp.mapValues(composeMutations)(mergedMutations); diff --git a/src/index.ts b/src/index.ts index 9b1c97c..fae4d22 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,8 @@ -import Updux from './updux'; +import Updux from "./updux"; -export { default as Updux } from './updux'; -export { - UpduxConfig -} from './types'; +export { default as Updux } from "./updux"; +export { UpduxConfig } from "./types"; + +export { actionCreator } from "./buildActions"; export default Updux; diff --git a/src/mutations.test.ts b/src/mutations.test.ts new file mode 100644 index 0000000..1909b06 --- /dev/null +++ b/src/mutations.test.ts @@ -0,0 +1,25 @@ +import Updux, { actionCreator } from "./updux"; + +describe("as array of arrays", () => { + const doIt = actionCreator("doIt"); + + const updux = new Updux({ + initial: "", + mutations: [ + [doIt, () => () => "bingo"], + ["thisToo", () => () => "straight type"] + ] + }); + + const store = updux.createStore(); + + test("doIt", () => { + store.dispatch.doIt(); + expect(store.getState()).toEqual("bingo"); + }); + + test("straight type", () => { + store.dispatch.thisToo(); + expect(store.getState()).toEqual("straight type"); + }); +}); diff --git a/src/types.ts b/src/types.ts index b33cac4..54c876c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,24 +1,30 @@ -import {Dispatch, Middleware} from 'redux'; +import { Dispatch, Middleware } from "redux"; type MaybePayload

= P extends object | string | boolean | number ? { payload: P; } - : {payload?: P}; + : { payload?: P }; export type Action = { type: T; } & MaybePayload

; -export type Dictionary = {[key: string]: T}; +export type Dictionary = { [key: string]: T }; export type Mutation = ( - payload: A['payload'], - action: A, + payload: A["payload"], + action: A ) => (state: S) => S; export type ActionPayloadGenerator = (...args: any[]) => any; +export type MutationEntry = [ + ActionCreator | string, + Mutation>, + boolean? +]; + export type ActionCreator = { type: T; _genericAction?: boolean; @@ -26,12 +32,11 @@ export type ActionCreator = { export type UpduxDispatch = Dispatch & Dictionary; - /** - * Configuration object given to Updux's constructor. - * @typeparam S Type of the Updux's state. Defaults to `any`. - */ -export type UpduxConfig = { + * Configuration object given to Updux's constructor. + * @typeparam S Type of the Updux's state. Defaults to `any`. + */ +export type UpduxConfig = { /** * The default initial state of the reducer. Can be anything your * heart desires. @@ -155,7 +160,7 @@ export type UpduxConfig = { * } * }); */ - mutations?: any; + mutations?: { [actionType: string]: Mutation } | MutationEntry[]; groomMutations?: (m: Mutation) => Mutation; @@ -184,10 +189,10 @@ export type UpduxConfig = { export type Upreducer = (action: Action) => (state: S) => S; export interface UpduxMiddlewareAPI { - dispatch: UpduxDispatch, - getState(): any, - getRootState(): S - + dispatch: UpduxDispatch; + getState(): any; + getRootState(): S; } -export type UpduxMiddleware = (api: UpduxMiddlewareAPI ) => ( next: UpduxDispatch ) => ( action: Action ) => any; - +export type UpduxMiddleware = ( + api: UpduxMiddlewareAPI +) => (next: UpduxDispatch) => (action: Action) => any; diff --git a/src/updux.ts b/src/updux.ts index 772162c..69a4431 100644 --- a/src/updux.ts +++ b/src/updux.ts @@ -1,14 +1,14 @@ -import fp from 'lodash/fp'; -import u from 'updeep'; -import {observable, computed, toJS} from 'mobx'; +import fp from "lodash/fp"; +import u from "updeep"; +import { observable, computed, toJS } from "mobx"; -import buildActions from './buildActions'; -import buildInitial from './buildInitial'; -import buildMutations from './buildMutations'; +import buildActions, { actionFor } from "./buildActions"; +import buildInitial from "./buildInitial"; +import buildMutations from "./buildMutations"; -import buildCreateStore from './buildCreateStore'; -import buildMiddleware from './buildMiddleware'; -import buildUpreducer from './buildUpreducer'; +import buildCreateStore from "./buildCreateStore"; +import buildMiddleware from "./buildMiddleware"; +import buildUpreducer from "./buildUpreducer"; import { UpduxConfig, Dictionary, @@ -17,29 +17,30 @@ import { Mutation, Upreducer, UpduxDispatch, - UpduxMiddleware -} from './types'; + UpduxMiddleware, + MutationEntry +} from "./types"; -import {Middleware, Store} from 'redux'; -export {actionCreator} from './buildActions'; +import { Middleware, Store } from "redux"; +export { actionCreator } from "./buildActions"; type StoreWithDispatchActions< S = any, - Actions = {[action: string]: (...args: any) => Action} + Actions = { [action: string]: (...args: any) => Action } > = Store & { - dispatch: {[type in keyof Actions]: (...args: any) => void}; + dispatch: { [type in keyof Actions]: (...args: any) => void }; }; export type Dux = Pick< Updux, - | 'subduxes' - | 'actions' - | 'initial' - | 'mutations' - | 'reducer' - | 'middleware' - | 'createStore' - | 'upreducer' + | "subduxes" + | "actions" + | "initial" + | "mutations" + | "reducer" + | "middleware" + | "createStore" + | "upreducer" >; /** @@ -98,35 +99,36 @@ export class Updux { */ groomMutations: (mutation: Mutation) => Mutation; - @observable private localEffects: Dictionary< - UpduxMiddleware - >; + @observable private localEffects: Dictionary>; @observable private localActions: Dictionary; @observable private localMutations: Dictionary< Mutation | [Mutation, boolean | undefined] - >; + > = {}; constructor(config: UpduxConfig = {}) { this.groomMutations = config.groomMutations || ((x: Mutation) => x); this.subduxes = fp.mapValues((value: UpduxConfig | Updux) => - fp.isPlainObject(value) ? new Updux(value) : value, - )(fp.getOr({}, 'subduxes', config)) as Dictionary; + fp.isPlainObject(value) ? new Updux(value) : value + )(fp.getOr({}, "subduxes", config)) as Dictionary; - this.localActions = fp.getOr({}, 'actions', config); + this.localActions = fp.getOr({}, "actions", config); - this.localEffects = fp.getOr({}, 'effects', config); + this.localEffects = fp.getOr({}, "effects", config); this.initial = buildInitial( config.initial, - fp.mapValues(({initial}) => initial)(this.subduxes), + fp.mapValues(({ initial }) => initial)(this.subduxes) ); - this.localMutations = fp.mapValues((m: Mutation) => - this.groomMutations(m), - )(fp.getOr({}, 'mutations', config)); + let mutations = fp.getOr([], "mutations", config); + if (!Array.isArray(mutations)) { + mutations = fp.toPairs(mutations); + } + + mutations.forEach(args => (this.addMutation as any)(...args)); } /** @@ -138,11 +140,7 @@ export class Updux { * alongside `getState` to get the root state. */ @computed get middleware(): UpduxMiddleware { - return buildMiddleware( - this.localEffects, - this.actions, - this.subduxes, - ); + return buildMiddleware(this.localEffects, this.actions, this.subduxes); } /** @@ -164,10 +162,10 @@ export class Updux { this.localActions, [...Object.keys(this.localMutations), ...Object.keys(this.localEffects)], fp.flatten( - Object.values(this.subduxes).map(({actions}: Updux) => - Object.entries(actions), - ), - ), + Object.values(this.subduxes).map(({ actions }: Updux) => + Object.entries(actions) + ) + ) ); } @@ -245,7 +243,7 @@ export class Updux { this.reducer, this.initial, this.middleware as Middleware, - this.actions, + this.actions ) as () => StoreWithDispatchActions; } @@ -264,7 +262,7 @@ export class Updux { actions: this.actions, reducer: this.reducer, mutations: this.mutations, - initial: this.initial, + initial: this.initial }; } @@ -283,12 +281,15 @@ export class Updux { addMutation( creator: A, mutation: Mutation infer R ? R : never>, - isSink?: boolean, + isSink?: boolean ) { - this.localActions[creator.type] = creator; - this.localMutations[creator.type] = [ + let c = fp.isFunction(creator) ? creator : actionFor(creator); + + this.localActions[c.type] = c; + + this.localMutations[c.type] = [ this.groomMutations(mutation as any) as Mutation, - isSink, + isSink ]; } }