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

Commit eeface3

Browse files
authored
Validate reducer cases and support error and meta in actions (#13)
* Validate reducer cases are always valid Also improve types and tests * Fix types * Document new features
1 parent b2a7b07 commit eeface3

File tree

3 files changed

+154
-37
lines changed

3 files changed

+154
-37
lines changed

README.md

+6-4
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,15 @@ const ACTION_TYPE = myDuck.defineType("ACTION_TYPE");
4545
### Create action creators
4646

4747
```ts
48-
const actionType = myDuck.createAction(ACTION_TYPE);
48+
const actionType = myDuck.createAction(ACTION_TYPE, false);
4949
```
5050

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

5658
### Create reducer
5759

src/index.ts

+40-13
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
export type FSA = {
2-
type: ActionType;
3-
payload?: any;
4-
};
5-
61
type AppName = string;
72
type DuckName = string;
83
type ActionName = string;
94
type ActionType = string;
105

6+
export type FSA<Payload = undefined, Meta = undefined> = {
7+
type: ActionType;
8+
payload?: Payload;
9+
meta?: Meta;
10+
error?: boolean;
11+
};
12+
1113
type CaseFn<State> = (state: State, action?: FSA) => State;
1214

1315
type Case<State> = {
@@ -16,6 +18,28 @@ type Case<State> = {
1618

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

21+
function validateCases(cases: ActionType[]): void {
22+
if (cases.length === 0) {
23+
throw new Error(
24+
'You should pass at least one case name when creating a reducer.'
25+
);
26+
}
27+
28+
const validCases = cases.filter(caseName => caseName !== 'undefined');
29+
30+
if (validCases.length === 0) {
31+
throw new Error('All of your action types are undefined.');
32+
}
33+
34+
if (validCases.length !== Object.keys(cases).length) {
35+
throw new Error(
36+
`One or more of your action types are undefined. Valid cases are: ${validCases.join(
37+
', '
38+
)}.`
39+
);
40+
}
41+
}
42+
1943
export function createDuck(name: DuckName, app?: AppName) {
2044
function defineType(type: ActionName): ActionType {
2145
if (app) {
@@ -25,6 +49,8 @@ export function createDuck(name: DuckName, app?: AppName) {
2549
}
2650

2751
function createReducer<State>(cases: Case<State>, defaultState: State) {
52+
validateCases(Object.keys(cases));
53+
2854
return function reducer(state = defaultState, action = defaultAction) {
2955
for (const caseName in cases) {
3056
if (action.type === caseName) return cases[caseName](state, action);
@@ -33,14 +59,15 @@ export function createDuck(name: DuckName, app?: AppName) {
3359
};
3460
}
3561

36-
function createAction(type: ActionType) {
37-
return function actionCreator<Payload>(payload?: Payload): FSA {
38-
const action: FSA = {
39-
type,
40-
payload,
41-
};
42-
43-
return action;
62+
function createAction<Payload, Meta = undefined>(
63+
type: ActionType,
64+
isError = false
65+
) {
66+
return function actionCreator(
67+
payload?: Payload,
68+
meta?: Meta
69+
): FSA<Payload, Meta> {
70+
return { type, payload, error: isError, meta };
4471
};
4572
}
4673

test/index.test.ts

+108-20
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,85 @@
11
import { createDuck } from '../src';
22

3+
type CountState = { count: number };
4+
35
describe('Redux Duck', () => {
4-
test('define type without app name', () => {
5-
const duck = createDuck('duck-name');
6-
expect(duck.defineType('action-name')).toBe('duck-name/action-name');
7-
});
6+
describe('Define Type', () => {
7+
test('Without App Name', () => {
8+
const duck = createDuck('duck-name');
9+
expect(duck.defineType('action-name')).toBe('duck-name/action-name');
10+
});
811

9-
test('define type with app name', () => {
10-
const duck = createDuck('duck-name', 'app-name');
11-
expect(duck.defineType('action-name')).toBe(
12-
'app-name/duck-name/action-name'
13-
);
12+
test('With App Name', () => {
13+
const duck = createDuck('duck-name', 'app-name');
14+
expect(duck.defineType('action-name')).toBe(
15+
'app-name/duck-name/action-name'
16+
);
17+
});
1418
});
1519

16-
test('create action creator', () => {
17-
const duck = createDuck('duck-name', 'app-name');
18-
const type = duck.defineType('action-name');
20+
describe('Action Creator', () => {
21+
test('No Error', () => {
22+
const duck = createDuck('duck-name', 'app-name');
23+
const type = duck.defineType('action-name');
24+
25+
const action = duck.createAction<{ id: number }, { analytics: string }>(
26+
type
27+
);
28+
expect(typeof action).toBe('function');
29+
expect(action()).toEqual({
30+
type,
31+
error: false,
32+
meta: undefined,
33+
payload: undefined,
34+
});
35+
expect(action({ id: 1 })).toEqual({
36+
type,
37+
payload: { id: 1 },
38+
meta: undefined,
39+
error: false,
40+
});
41+
expect(action({ id: 1 }, { analytics: 'random' })).toEqual({
42+
type,
43+
payload: { id: 1 },
44+
meta: { analytics: 'random' },
45+
error: false,
46+
});
47+
});
1948

20-
const action = duck.createAction(type);
21-
expect(typeof action).toBe('function');
22-
expect(action()).toEqual({ type });
23-
expect(action({ id: 1 })).toEqual({ type, payload: { id: 1 } });
49+
test('Error', () => {
50+
const duck = createDuck('duck-name', 'app-name');
51+
const type = duck.defineType('action-name');
52+
53+
const action = duck.createAction<{ id: number }, { analytics: string }>(
54+
type,
55+
true
56+
);
57+
expect(typeof action).toBe('function');
58+
expect(action()).toEqual({
59+
type,
60+
error: true,
61+
meta: undefined,
62+
payload: undefined,
63+
});
64+
expect(action({ id: 1 })).toEqual({
65+
type,
66+
payload: { id: 1 },
67+
meta: undefined,
68+
error: true,
69+
});
70+
expect(action({ id: 1 }, { analytics: 'random' })).toEqual({
71+
type,
72+
payload: { id: 1 },
73+
meta: { analytics: 'random' },
74+
error: true,
75+
});
76+
});
2477
});
2578

26-
test('reducer', () => {
79+
test('Create Reducer', () => {
2780
const duck = createDuck('duck-name', 'app-name');
2881
const type = duck.defineType('action-name');
29-
const action = duck.createAction(type);
30-
31-
type CountState = { count: number };
82+
const action = duck.createAction<undefined, undefined>(type);
3283

3384
const reducer = duck.createReducer<CountState>(
3485
{
@@ -45,4 +96,41 @@ describe('Redux Duck', () => {
4596
expect(reducer(undefined, action())).toEqual({ count: 1 });
4697
expect(reducer({ count: 2 })).toEqual({ count: 2 });
4798
});
99+
100+
describe('Errors', () => {
101+
test('No Cases', () => {
102+
const duck = createDuck('duck-name', 'app-name');
103+
expect(() => duck.createReducer({}, '')).toThrowError(
104+
'You should pass at least one case name when creating a reducer.'
105+
);
106+
});
107+
108+
test('Zero Valid Cases', () => {
109+
const duck = createDuck('duck-name', 'app-name');
110+
expect(() => duck.createReducer({ undefined: s => s }, '')).toThrowError(
111+
'All of your action types are undefined.'
112+
);
113+
});
114+
115+
test('Only One Valid', () => {
116+
const duck = createDuck('duck-name', 'app-name');
117+
expect(() =>
118+
duck.createReducer({ valid: s => s, undefined: s => s }, '')
119+
).toThrowError(
120+
'One or more of your action types are undefined. Valid cases are: valid.'
121+
);
122+
});
123+
124+
test('More Than One Valid', () => {
125+
const duck = createDuck('duck-name', 'app-name');
126+
expect(() =>
127+
duck.createReducer(
128+
{ valid: s => s, undefined: s => s, anotherValid: s => s },
129+
''
130+
)
131+
).toThrowError(
132+
'One or more of your action types are undefined. Valid cases are: valid, anotherValid.'
133+
);
134+
});
135+
});
48136
});

0 commit comments

Comments
 (0)