Skip to content
This repository has been archived by the owner on May 25, 2022. It is now read-only.

Commit

Permalink
Improve bakcwards compatibility
Browse files Browse the repository at this point in the history
  • Loading branch information
cyberixae committed Nov 20, 2019
1 parent e33e16a commit ecc8227
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 57 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
"homepage": "https://github.com/sergiodxa/redux-duck/",
"devDependencies": {
"@types/jest": "^24.0.23",
"flux-standard-action": "^2.1.1",
"husky": "^3.1.0",
"redux": "^4.0.4",
"tsdx": "^0.11.0",
"tslib": "^1.10.0",
"typescript": "^3.7.2"
Expand Down
109 changes: 64 additions & 45 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,44 @@
type AppName = string;
type DuckName = string;
type ActionName = string;
type ActionType = string;
import { Reducer, Action, AnyAction } from 'redux';
import {
FSA,
FSAWithPayload,
FSAWithPayloadAndMeta,
} from 'flux-standard-action';

export type FSA<Payload = undefined, Meta = undefined> = {
type: ActionType;
payload?: Payload;
meta?: Meta;
error?: boolean;
};
export type AppName = string;
export type DuckName = string;
export type ActionName = string;

export type ActionType = string; // AppName/DuckName/ActionName or just DuckName/ActionName

type CaseFn<State> = (state: State, action?: FSA) => State;
// Ducks define FSA actions
export type ActionCreator<
A extends FSA<any, any, any> = FSA<any, any, any>
> = A extends FSAWithPayloadAndMeta<any, any, any>
? (a: A['payload'], b: A['meta']) => A
: A extends FSAWithPayload<any, any, any>
? (a: A['payload']) => A
: A extends FSA
? (a?: A['payload'], b?: A['meta']) => A
: never;

type Case<State> = {
[type: string]: CaseFn<State>;
// Ducks can listen for any actions (FSA or not)
export type ActionHandlers<S = any, A extends Action = AnyAction> = {
[T in A['type']]?: (x: S, y: Extract<A, { type: T }>) => S;
};

export interface DuckBuilder<AppAction extends Action = AnyAction> {
defineType: (a: ActionName) => ActionType;
createAction: <T extends string & AppAction['type']>(
a: T,
b?: boolean
) => ActionCreator<Extract<AppAction, FSA<T, any, any>>>;
createReducer: <S>(
a: ActionHandlers<S, AppAction>,
b: S
) => Reducer<S, AppAction>;
}

const defaultAction: FSA = { type: '@@INVALID' };

function validateCases(cases: ActionType[]): void {
Expand All @@ -40,40 +63,36 @@ function validateCases(cases: ActionType[]): void {
}
}

export function createDuck(name: DuckName, app?: AppName) {
function defineType(type: ActionName): ActionType {
if (app) {
return `${app}/${name}/${type}`;
}
return `${name}/${type}`;
}

function createReducer<State>(cases: Case<State>, defaultState: State) {
validateCases(Object.keys(cases));

return function reducer(state = defaultState, action = defaultAction) {
for (const caseName in cases) {
if (action.type === caseName) return cases[caseName](state, action);
export function createDuck(name: DuckName, app?: AppName): DuckBuilder {
return {
defineType: function(type) {
if (app) {
return `${app}/${name}/${type}`;
}
return state;
};
}
return `${name}/${type}`;
},

function createAction<Payload, Meta = undefined>(
type: ActionType,
isError = false
) {
return function actionCreator(
payload?: Payload,
meta?: Meta
): FSA<Payload, Meta> {
return { type, payload, error: isError, meta };
};
}
createReducer: function(cases, defaultState) {
validateCases(Object.keys(cases));

return {
defineType,
createReducer,
createAction,
return function reducer(state = defaultState, action = defaultAction) {
for (const caseName in cases) {
if (action.type === caseName) {
const handler = cases[caseName];
if (typeof handler === 'undefined') {
throw new Error(`missing handler for ${caseName}`);
}
return handler(state, action);
}
}
return state;
};
},

createAction: function(type, isError = false) {
return function actionCreator(payload?, meta?) {
return { type, payload, error: isError, meta };
};
},
};
}
20 changes: 8 additions & 12 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { AnyAction } from 'redux';
import { createDuck } from '../src';

type CountState = { count: number };

describe('Redux Duck', () => {
describe('Define Type', () => {
test('Without App Name', () => {
Expand All @@ -22,9 +21,7 @@ describe('Redux Duck', () => {
const duck = createDuck('duck-name', 'app-name');
const type = duck.defineType('action-name');

const action = duck.createAction<{ id: number }, { analytics: string }>(
type
);
const action = duck.createAction(type);
expect(typeof action).toBe('function');
expect(action()).toEqual({
type,
Expand All @@ -50,10 +47,7 @@ describe('Redux Duck', () => {
const duck = createDuck('duck-name', 'app-name');
const type = duck.defineType('action-name');

const action = duck.createAction<{ id: number }, { analytics: string }>(
type,
true
);
const action = duck.createAction(type, true);
expect(typeof action).toBe('function');
expect(action()).toEqual({
type,
Expand All @@ -79,9 +73,9 @@ describe('Redux Duck', () => {
test('Create Reducer', () => {
const duck = createDuck('duck-name', 'app-name');
const type = duck.defineType('action-name');
const action = duck.createAction<undefined, undefined>(type);
const action = duck.createAction(type);

const reducer = duck.createReducer<CountState>(
const reducer = duck.createReducer(
{
[type]: state => {
return {
Expand All @@ -94,7 +88,9 @@ describe('Redux Duck', () => {

expect(typeof reducer).toBe('function');
expect(reducer(undefined, action())).toEqual({ count: 1 });
expect(reducer({ count: 2 })).toEqual({ count: 2 });
expect(reducer({ count: 2 }, (undefined as unknown) as AnyAction)).toEqual({
count: 2,
});
});

describe('Errors', () => {
Expand Down
135 changes: 135 additions & 0 deletions test/types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// This example code explains how to setup app "Pond" with two ducks, "Alfred" and "Winnie"

import { FSAAuto as FSA } from 'flux-standard-action';

import { createDuck, DuckBuilder } from '../src';

type Food = string;

// State Interfaces

export interface AlfredState {
readonly belly: Food[];
readonly happy: boolean;
}

export interface WinnieState {
readonly home: boolean;
}

// Action Types

enum AlfredActions {
EAT = 'pond/alfred/EAT',
SLEEP = 'pond/alfred/SLEEP',
}
enum WinnieActions {
GO_TO_WORK = 'pond/winnie/GO_TO_WORK',
RETURN_HOME = 'pond/winnie/RETURN_HOME',
}

// Action Structures

type AlfredAction =
| FSA<
AlfredActions.EAT,
{
readonly food: Food;
},
{
comment: string;
}
>
| FSA<AlfredActions.SLEEP>;

type WinnieAction =
| FSA<WinnieActions.GO_TO_WORK>
| FSA<WinnieActions.RETURN_HOME>;

// Duck Initialization

const initialAlfred: AlfredState = {
belly: [],
happy: true,
};
const initialWinnie: WinnieState = {
home: true,
};

// Global Configuration

type PondAction = AlfredAction | WinnieAction;

type PondDuckBuilder = DuckBuilder<PondAction>;

// Duck Builders

const alfredBuilder: PondDuckBuilder = createDuck('alfred', 'pond');
const winnieBuilder: PondDuckBuilder = createDuck('winnie', 'pond');

// Ducks

// In practice Alfred and Winnie should be their on modules.
// Here we just define an object for the respective exports.
//
// The module exports should match the "Redux Reducer Bundles"
// specification https://github.com/erikras/ducks-modular-redux
//
// TypeScript modules can not currently implement interfaces
// https://github.com/microsoft/TypeScript/issues/420

const winnieExports = {
WinnieActions,
default: winnieBuilder.createReducer<WinnieState>(
{
[WinnieActions.GO_TO_WORK]: state => ({
...state,
home: false,
}),
[WinnieActions.RETURN_HOME]: state => ({
...state,
home: true,
}),
// Alfred doesn't export any action types so
// Winnie's state can not change based on them.
},
initialWinnie
),
goToWork: winnieBuilder.createAction(WinnieActions.GO_TO_WORK),
returnHome: winnieBuilder.createAction(WinnieActions.RETURN_HOME),
};

const alfredExports = {
default: alfredBuilder.createReducer<AlfredState>(
{
[AlfredActions.EAT]: (state, { payload: { food } }) => ({
...state,
belly: [...state.belly, food],
}),
[AlfredActions.SLEEP]: state => ({
...state,
belly: [],
}),
// Winnie's action types are exported and
// can therefore cause changes in Alfred.
[winnieExports.WinnieActions.GO_TO_WORK]: state => ({
...state,
happy: false,
}),
[winnieExports.WinnieActions.RETURN_HOME]: state => ({
...state,
happy: true,
}),
},
initialAlfred
),
eat: alfredBuilder.createAction(AlfredActions.EAT),
sleep: alfredBuilder.createAction(AlfredActions.SLEEP),
};

describe('Redux Duck', () => {
test('Type Definitions', () => {
expect(winnieExports).toBeDefined();
expect(alfredExports).toBeDefined();
});
});
20 changes: 20 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2493,6 +2493,13 @@ flatted@^2.0.0:
resolved "https://registry.able.co/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08"
integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==

flux-standard-action@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/flux-standard-action/-/flux-standard-action-2.1.1.tgz#b2e204145ea8e4294d6b0f178d8e8c416802948f"
integrity sha512-W86GzmXmIiTVq/dpYVd2HtTIUX9c35Iq3ao3xR6qcKtuXgbu+BDEj72op5VnEIe/kpuSbhl+I8kT1iS2hpcusw==
dependencies:
lodash "^4.17.15"

for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.able.co/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
Expand Down Expand Up @@ -4713,6 +4720,14 @@ rechoir@^0.6.2:
dependencies:
resolve "^1.1.6"

redux@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796"
integrity sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q==
dependencies:
loose-envify "^1.4.0"
symbol-observable "^1.2.0"

regenerate-unicode-properties@^8.1.0:
version "8.1.0"
resolved "https://registry.able.co/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e"
Expand Down Expand Up @@ -5486,6 +5501,11 @@ supports-color@^6.1.0:
dependencies:
has-flag "^3.0.0"

symbol-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==

symbol-tree@^3.2.2:
version "3.2.4"
resolved "https://registry.able.co/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
Expand Down

0 comments on commit ecc8227

Please sign in to comment.