From 6168720c47458392c20a3b264396b9cbd1f8e28b Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Mon, 21 Oct 2019 17:17:56 -0400 Subject: [PATCH] tests are passing --- src/buildActions/index.ts | 30 +++++++ src/buildCreateStore.ts | 17 ++++ src/buildInitial/index.ts | 11 +++ src/buildMutations/index.ts | 55 +++++++++++++ src/buildUpreducer/index.ts | 18 +++++ src/index.ts | 151 +++++------------------------------- src/types.ts | 7 ++ src/updux.ts | 78 +++++++++++++++++++ tsconfig.json | 4 +- 9 files changed, 236 insertions(+), 135 deletions(-) create mode 100644 src/buildActions/index.ts create mode 100644 src/buildCreateStore.ts create mode 100644 src/buildInitial/index.ts create mode 100644 src/buildMutations/index.ts create mode 100644 src/buildUpreducer/index.ts create mode 100644 src/types.ts create mode 100644 src/updux.ts diff --git a/src/buildActions/index.ts b/src/buildActions/index.ts new file mode 100644 index 0000000..f7a39af --- /dev/null +++ b/src/buildActions/index.ts @@ -0,0 +1,30 @@ +import fp from 'lodash/fp'; + +function actionFor(type) { + return (payload = null, meta = null) => { + return fp.pickBy(v => v !== null)({type, payload, meta}); + }; +} + +export default function buildActions( + mutations = {}, + effects = {}, + subActions = {}, +) { + + let actions = { ...subActions }; + + Object.keys(mutations).forEach(type => { + if (!actions[type]) { + actions[type] = actionFor(type); + } + }); + + Object.keys(effects).forEach(type => { + if (!actions[type]) { + actions[type] = actionFor(type); + } + }); + + return actions; +} diff --git a/src/buildCreateStore.ts b/src/buildCreateStore.ts new file mode 100644 index 0000000..f2d6034 --- /dev/null +++ b/src/buildCreateStore.ts @@ -0,0 +1,17 @@ +import { createStore as reduxCreateStore, applyMiddleware } from 'redux'; + +export default function buildCreateStore( reducer, initial, middleware, + actions ) { + return () => { + const store = reduxCreateStore( reducer, initial, + applyMiddleware( middleware) + ); + for ( let a in actions ) { + store.dispatch[a] = (...args) => { + store.dispatch(actions[a](...args)) + }; + } + + return store as any; + } +}; diff --git a/src/buildInitial/index.ts b/src/buildInitial/index.ts new file mode 100644 index 0000000..3c8afaa --- /dev/null +++ b/src/buildInitial/index.ts @@ -0,0 +1,11 @@ +import fp from 'lodash/fp'; + +export default function buildInitial(initial : any = {}, subduxes = {}) { + let state = initial; + + if (fp.isPlainObject(initial)) { + initial = fp.mergeAll([ subduxes, initial, ]); + } + + return initial; +} diff --git a/src/buildMutations/index.ts b/src/buildMutations/index.ts new file mode 100644 index 0000000..3e26f1e --- /dev/null +++ b/src/buildMutations/index.ts @@ -0,0 +1,55 @@ +import fp from 'lodash/fp'; +import u from 'updeep'; + +import { Mutation, Mutations } from '../types'; + +const composeMutations = (mutations:Mutation[]) => + mutations.reduce( (m1,m2) => + (payload=null,action={}) => state => m2(payload,action)( + m1(payload,action)(state) )); + +export default function buildMutations(mutations = {}, subduxes= {}) :Mutations{ + // 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) ) + ) ); + + let mergedMutations = {}; + + let [ globby, nonGlobby ] = fp.partition( + ([_,{mutations={}}]:any) => mutations['*'], + Object.entries(subduxes) + ); + + globby = + fp.flow([ + fp.fromPairs, + fp.mapValues( + ({reducer}) => (_,action={}) => state => + reducer(state,action) ), + ])(globby); + + const globbyMutation = (payload,action) => u( + fp.mapValues( (mut:any) => mut(payload,action) )(globby) + ); + + actions.forEach( action => { + mergedMutations[action] = [ globbyMutation ] + }); + + nonGlobby.forEach( ([slice, {mutations={},reducer={}}]:any) => { + Object.entries(mutations).forEach(([type,mutation]) => { + const localized = (payload=null,action={}) => u.updateIn( slice )( (mutation as any)(payload,action) ); + + mergedMutations[type].push(localized); + }) + }); + + Object.entries(mutations).forEach(([type,mutation]) => { + mergedMutations[type].push(mutation); + }); + + return fp.mapValues( composeMutations )(mergedMutations) as Mutations; +} diff --git a/src/buildUpreducer/index.ts b/src/buildUpreducer/index.ts new file mode 100644 index 0000000..1c121ab --- /dev/null +++ b/src/buildUpreducer/index.ts @@ -0,0 +1,18 @@ +import fp from 'lodash/fp'; +import {Mutations} from '../types'; + +type Upreducer = (action:any) => (state:S) => S; + +export default function buildUpreducer(initial: S, mutations: Mutations): Upreducer { + return (action = {}) => (state:any) => { + if (state === null) state = initial; + + const a = + mutations[(action as any).type] || + mutations['*']; + + if(!a) return state; + + return a((action as any).payload, action)(state) as S; + }; +} diff --git a/src/index.ts b/src/index.ts index 0754b29..15520d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,149 +1,34 @@ import fp from 'lodash/fp'; import u from 'updeep'; -import { createStore as reduxCreateStore, applyMiddleware } from 'redux'; +import Updux from './updux'; -import buildMiddleware from './buildMiddleware'; -function actionFor(type) { - return (payload = null, meta = null) => { - return fp.pickBy(v => v !== null)({type, payload, meta}); - }; -} -function buildInitial({initial = {}, subduxes = {}}) { - let state = initial; - - if (fp.isPlainObject(initial)) { - initial = fp.mergeAll([ - fp.mapValues(fp.getOr({}, 'initial'), subduxes), - initial, - ]); - } - - return initial; -} - -function buildActions({mutations = {}, effects = {}, subduxes = {}}) { - let actions = fp.mergeAll(fp.map(fp.getOr({}, 'actions'), subduxes)) || {}; - - Object.keys(mutations).forEach(type => { - if (!actions[type]) { - actions[type] = actionFor(type); - } - }); - - Object.keys(effects).forEach(type => { - if (!actions[type]) { - actions[type] = actionFor(type); - } - }); - - return actions; -} - -const composeMutations = mutations => - mutations.reduce( (m1,m2) => - (payload=null,action={}) => state => m2(payload,action)( - m1(payload,action)(state) )); - -function buildMutations({mutations = {}, subduxes= {}}: any) { - // 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) ) - ) ); - - let mergedMutations = {}; - - let [ globby, nonGlobby ] = fp.partition( - ([_,{mutations={}}]:any) => mutations['*'], - Object.entries(subduxes) - ); - - globby = - fp.flow([ - fp.fromPairs, - fp.mapValues( - ({reducer}) => (_,action={}) => state => - reducer(state,action) ), - ])(globby); - - const globbyMutation = (payload,action) => u( - fp.mapValues( (mut:any) => mut(payload,action) )(globby) - ); - - actions.forEach( action => { - mergedMutations[action] = [ globbyMutation ] - }); - - nonGlobby.forEach( ([slice, {mutations={},reducer={}}]:any) => { - Object.entries(mutations).forEach(([type,mutation]) => { - const localized = (payload=null,action={}) => u.updateIn( slice )( (mutation as any)(payload,action) ); - - mergedMutations[type].push(localized); - }) - }); - - Object.entries(mutations).forEach(([type,mutation]) => { - mergedMutations[type].push(mutation); - }); - - return fp.mapValues( composeMutations )(mergedMutations); - -} function updux(config) { - const actions = buildActions(config); + return new Updux(config); + // const actions = buildActions( + // config.mutations, + // config.effects, + // fp.flatten( ( config.subduxes||{}).map( ({ actions }) => actions ) ) + // ); - const initial = buildInitial(config); + // const initial = buildInitial(config); - const mutations = buildMutations(config); + // const mutations = buildMutations(config.mutations,config.subduxes); - const upreducer = (action={}) => state => { - if (state === null) state = initial; - const a = - mutations[(action as any).type] || - mutations['*'] || - (() => state => state); - return a((action as any).payload, action)(state); - }; - - const reducer = (state, action) => { - return upreducer(action)(state); - }; - - const middleware = buildMiddleware( - config.effects, - actions, - config.subduxes, - ); - - const createStore = () => { - const store = reduxCreateStore( reducer, initial, - applyMiddleware( middleware) - ); - for ( let a in actions ) { - store.dispatch[a] = (...args) => { - store.dispatch(actions[a](...args)) - }; - } - - return store; - } - - return { - reducer, - upreducer, - middleware, - createStore, - actions, - mutations, - initial, - }; + // return { + // reducer, + // upreducer, + // middleware, + // createStore, + // actions: ( actions as any ), + // mutations, + // initial, + // }; } export default updux; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..c8dfa36 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,7 @@ + +export type Dictionary = { [key: string]: T }; + +export type Mutation = (payload: any, action: any) => (state:any) => any; + +export type Mutations = Dictionary; + diff --git a/src/updux.ts b/src/updux.ts new file mode 100644 index 0000000..7e6c378 --- /dev/null +++ b/src/updux.ts @@ -0,0 +1,78 @@ +import fp from 'lodash/fp'; +import buildActions from './buildActions'; +import buildInitial from './buildInitial'; +import buildMutations from './buildMutations'; + +import { Dictionary, Mutation } from './types'; +import buildCreateStore from './buildCreateStore'; +import buildMiddleware from './buildMiddleware'; +import buildUpreducer from './buildUpreducer'; + +type UpduxConfig = { + initial?: any, + mutations?: any, + effects?: any, + subduxes?: { + [ slice: string ]: UpduxConfig | Updux + } +}; + +export class Updux { + actions: any; + + subduxes: Dictionary; + + initial: any; + + mutations: Dictionary; + + createStore: Function; + + upreducer: (action:any)=>(state:any)=>any; + + reducer: (state:S,action:any) => S; + + middleware: (api:any) => (next: Function) => (action: any) => any; + + constructor(config: UpduxConfig) { + + this.subduxes = fp.mapValues( + value => fp.isPlainObject(value) ? new Updux(value as UpduxConfig) : value )(fp.getOr({},'subduxes',config) + ) as Dictionary; + + + this.actions = buildActions( + config.mutations, + config.effects, + fp.mergeAll( Object.values( this.subduxes ).map( ({ actions }) => + actions ) ) + ) + + this.initial = buildInitial( + config.initial, fp.mapValues( ({initial}) => initial )(this.subduxes) + ); + + this.mutations = buildMutations( + config.mutations, this.subduxes + ); + + this.upreducer = buildUpreducer( + this.initial, this.mutations + ); + + this.reducer = (state,action) => { + return this.upreducer(action)(state); + } + + this.middleware = buildMiddleware( + config.effects, + this.actions, + config.subduxes, + ); + + this.createStore = buildCreateStore(this.reducer,this.initial,this.middleware,this.actions); + } + +} + +export default Updux; diff --git a/tsconfig.json b/tsconfig.json index be4c882..f2613cb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "incremental": true, /* Enable incremental compilation */ "target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - "lib": [ "dom", "es2016" ], /* Specify library files to be included in the compilation. */ + "lib": [ "dom", "es2017" ], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ @@ -12,7 +12,7 @@ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ - // "outDir": "./", /* Redirect output structure to the directory. */ + "outDir": "./dist", /* Redirect output structure to the directory. */ // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */