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

Commit

Permalink
Validate reducer cases and support error and meta in actions (#13)
Browse files Browse the repository at this point in the history
* Validate reducer cases are always valid

Also improve types and tests

* Fix types

* Document new features
  • Loading branch information
sergiodxa authored Nov 20, 2019
1 parent b2a7b07 commit eeface3
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 37 deletions.
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ const ACTION_TYPE = myDuck.defineType("ACTION_TYPE");
### Create action creators

```ts
const actionType = myDuck.createAction(ACTION_TYPE);
const actionType = myDuck.createAction(ACTION_TYPE, false);
```

- `createAction` receive just one argument.
- `createAction` receive two arguments, the second argument is optional.
- The first argument is the action type.
- The second, and optional, argument is if the action will be an error one.
- This argument should be the defined action type string.
- It should return a function who will receive the action payload and return a valid (FSA compilant) action object.
- The action creator will receive an optional argument with the action payload.
- It will return a function who will receive the action payload and meta data and return a valid (FSA compilant) action object.
- The action creator will receive two optional arguments, one with the action payload and another with the action meta data.

### Create reducer

Expand Down
53 changes: 40 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
export type FSA = {
type: ActionType;
payload?: any;
};

type AppName = string;
type DuckName = string;
type ActionName = string;
type ActionType = string;

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

type CaseFn<State> = (state: State, action?: FSA) => State;

type Case<State> = {
Expand All @@ -16,6 +18,28 @@ type Case<State> = {

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

function validateCases(cases: ActionType[]): void {
if (cases.length === 0) {
throw new Error(
'You should pass at least one case name when creating a reducer.'
);
}

const validCases = cases.filter(caseName => caseName !== 'undefined');

if (validCases.length === 0) {
throw new Error('All of your action types are undefined.');
}

if (validCases.length !== Object.keys(cases).length) {
throw new Error(
`One or more of your action types are undefined. Valid cases are: ${validCases.join(
', '
)}.`
);
}
}

export function createDuck(name: DuckName, app?: AppName) {
function defineType(type: ActionName): ActionType {
if (app) {
Expand All @@ -25,6 +49,8 @@ export function createDuck(name: DuckName, app?: AppName) {
}

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);
Expand All @@ -33,14 +59,15 @@ export function createDuck(name: DuckName, app?: AppName) {
};
}

function createAction(type: ActionType) {
return function actionCreator<Payload>(payload?: Payload): FSA {
const action: FSA = {
type,
payload,
};

return action;
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 };
};
}

Expand Down
128 changes: 108 additions & 20 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,85 @@
import { createDuck } from '../src';

type CountState = { count: number };

describe('Redux Duck', () => {
test('define type without app name', () => {
const duck = createDuck('duck-name');
expect(duck.defineType('action-name')).toBe('duck-name/action-name');
});
describe('Define Type', () => {
test('Without App Name', () => {
const duck = createDuck('duck-name');
expect(duck.defineType('action-name')).toBe('duck-name/action-name');
});

test('define type with app name', () => {
const duck = createDuck('duck-name', 'app-name');
expect(duck.defineType('action-name')).toBe(
'app-name/duck-name/action-name'
);
test('With App Name', () => {
const duck = createDuck('duck-name', 'app-name');
expect(duck.defineType('action-name')).toBe(
'app-name/duck-name/action-name'
);
});
});

test('create action creator', () => {
const duck = createDuck('duck-name', 'app-name');
const type = duck.defineType('action-name');
describe('Action Creator', () => {
test('No Error', () => {
const duck = createDuck('duck-name', 'app-name');
const type = duck.defineType('action-name');

const action = duck.createAction<{ id: number }, { analytics: string }>(
type
);
expect(typeof action).toBe('function');
expect(action()).toEqual({
type,
error: false,
meta: undefined,
payload: undefined,
});
expect(action({ id: 1 })).toEqual({
type,
payload: { id: 1 },
meta: undefined,
error: false,
});
expect(action({ id: 1 }, { analytics: 'random' })).toEqual({
type,
payload: { id: 1 },
meta: { analytics: 'random' },
error: false,
});
});

const action = duck.createAction(type);
expect(typeof action).toBe('function');
expect(action()).toEqual({ type });
expect(action({ id: 1 })).toEqual({ type, payload: { id: 1 } });
test('Error', () => {
const duck = createDuck('duck-name', 'app-name');
const type = duck.defineType('action-name');

const action = duck.createAction<{ id: number }, { analytics: string }>(
type,
true
);
expect(typeof action).toBe('function');
expect(action()).toEqual({
type,
error: true,
meta: undefined,
payload: undefined,
});
expect(action({ id: 1 })).toEqual({
type,
payload: { id: 1 },
meta: undefined,
error: true,
});
expect(action({ id: 1 }, { analytics: 'random' })).toEqual({
type,
payload: { id: 1 },
meta: { analytics: 'random' },
error: true,
});
});
});

test('reducer', () => {
test('Create Reducer', () => {
const duck = createDuck('duck-name', 'app-name');
const type = duck.defineType('action-name');
const action = duck.createAction(type);

type CountState = { count: number };
const action = duck.createAction<undefined, undefined>(type);

const reducer = duck.createReducer<CountState>(
{
Expand All @@ -45,4 +96,41 @@ describe('Redux Duck', () => {
expect(reducer(undefined, action())).toEqual({ count: 1 });
expect(reducer({ count: 2 })).toEqual({ count: 2 });
});

describe('Errors', () => {
test('No Cases', () => {
const duck = createDuck('duck-name', 'app-name');
expect(() => duck.createReducer({}, '')).toThrowError(
'You should pass at least one case name when creating a reducer.'
);
});

test('Zero Valid Cases', () => {
const duck = createDuck('duck-name', 'app-name');
expect(() => duck.createReducer({ undefined: s => s }, '')).toThrowError(
'All of your action types are undefined.'
);
});

test('Only One Valid', () => {
const duck = createDuck('duck-name', 'app-name');
expect(() =>
duck.createReducer({ valid: s => s, undefined: s => s }, '')
).toThrowError(
'One or more of your action types are undefined. Valid cases are: valid.'
);
});

test('More Than One Valid', () => {
const duck = createDuck('duck-name', 'app-name');
expect(() =>
duck.createReducer(
{ valid: s => s, undefined: s => s, anotherValid: s => s },
''
)
).toThrowError(
'One or more of your action types are undefined. Valid cases are: valid, anotherValid.'
);
});
});
});

0 comments on commit eeface3

Please sign in to comment.