Skip to content

Commit 5c50408

Browse files
authored
Merge pull request #12 from openscript/develop
Implement transformer feature
2 parents e2b60ce + 3775590 commit 5c50408

File tree

12 files changed

+542
-335
lines changed

12 files changed

+542
-335
lines changed

.eslintrc.js

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
module.exports = {
2-
parser: "@typescript-eslint/parser",
2+
parser: '@typescript-eslint/parser',
33
extends: [
4-
"plugin:react/recommended",
5-
"plugin:@typescript-eslint/recommended",
6-
"prettier/@typescript-eslint",
7-
"plugin:prettier/recommended"
4+
'plugin:react/recommended',
5+
'plugin:@typescript-eslint/recommended',
6+
'prettier/@typescript-eslint',
7+
'plugin:prettier/recommended'
88
],
99
parserOptions: {
1010
ecmaFeatures: {
1111
jsx: true
1212
},
1313
ecmaVersion: 2018,
14-
sourceType: "module"
14+
sourceType: 'module'
1515
},
1616
rules: {
17-
"@typescript-eslint/explicit-function-return-type": "off",
18-
"react/prop-types": "off"
17+
'@typescript-eslint/explicit-function-return-type': 'off',
18+
'@typescript-eslint/explicit-module-boundary-types': 'off',
19+
'react/prop-types': 'off'
1920
},
2021
settings: {
2122
react: {
22-
version: "detect"
23+
version: 'detect'
2324
}
2425
}
2526
};

.vscode/launch.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"version": "0.2.0",
3+
"configurations": [
4+
{
5+
"type": "node",
6+
"request": "launch",
7+
"name": "Jest All",
8+
"program": "${workspaceFolder}/node_modules/.bin/jest",
9+
"args": ["--runInBand"],
10+
"console": "integratedTerminal",
11+
"internalConsoleOptions": "neverOpen",
12+
"disableOptimisticBPs": true,
13+
"windows": {
14+
"program": "${workspaceFolder}/node_modules/jest/bin/jest",
15+
},
16+
"runtimeExecutable": "~/.asdf/shims/node"
17+
},
18+
{
19+
"type": "node",
20+
"request": "launch",
21+
"name": "Jest Current File",
22+
"program": "${workspaceFolder}/node_modules/.bin/jest",
23+
"args": [
24+
"${fileBasenameNoExtension}",
25+
"--config",
26+
"jest.config.js"
27+
],
28+
"console": "integratedTerminal",
29+
"internalConsoleOptions": "neverOpen",
30+
"disableOptimisticBPs": true,
31+
"windows": {
32+
"program": "${workspaceFolder}/node_modules/jest/bin/jest",
33+
},
34+
"runtimeExecutable": "~/.asdf/shims/node"
35+
}
36+
]
37+
}

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,39 @@ const columns = [
5151
</DSVImport>
5252
```
5353

54+
## API
55+
The `<DSVImport<T>>` components has the following API:
56+
57+
| Property | Type | Description |
58+
|:---------------|:-------------------------------|:-----------------------------------------------------------|
59+
| `columns` | [ColumnType](#columntype) | Description of the expected columns |
60+
| `transformers?`| [Transformer](#transformer)`[]`| Globally applied transformers |
61+
| `onChange?` | `(value: T[]) => void` | Callback which is called after parsing the input |
62+
| `onValidation?`| `(errors: Error<T>[]) => void` | Callback which is called if there are validation errors |
63+
64+
### Types
65+
Within this section additional types are explained.
66+
67+
#### ColumnType
68+
| Property | Type | Description |
69+
|:----------------|:-----------------------------|:------------------------------------------------------------|
70+
| `key` | `string` | Key of the current column |
71+
| `label` | `string` | Label of the current column, which can be shown to the user |
72+
| `rules?` | [Rule](#rule)`[]` | Validation rules which are applied to this column |
73+
| `transformers?` | [Transformer](#transformer)`[]`| Transformers which are applied to this column |
74+
75+
#### Rule
76+
| Property | Type | Description |
77+
|:----------------|:-----------------------------|:------------------------------------------------------------|
78+
| `message` | `string` | Error message |
79+
| `contraint` | `{ unique: boolean } | { constraint: `[Constraint](#constraint)`}` | Constraint for this rule |
80+
81+
#### Constraint
82+
`(value: string) => boolean`
83+
84+
#### Transformer
85+
`(value: string) => string`
86+
5487
## Project
5588
This section describes the status of the project.
5689

@@ -63,7 +96,7 @@ The most important features of this component are:
6396
- ✅ Automatic testing with >90% coverage
6497
- ✅ Input validation
6598
-[Ant Design](https://ant.design/) integration (see storybook)
66-
- Input transformation
99+
- Input transformation (e.g. trim, ...)
67100
-[Material UI](https://material-ui.com/) integration (see storybook)
68101

69102
✅ means the feature is implemented and released. ❌ indicates that a feature is planned.
@@ -91,3 +124,4 @@ The most important features of this component are:
91124
- [Article: Using ESLint and Prettier in a TypeScript Project](https://www.robertcooper.me/using-eslint-and-prettier-in-a-typescript-project)
92125
- [Template: Rollup Starter Lib (TypeScript)](https://github.com/rollup/rollup-starter-lib/tree/typescript)
93126
- [Article: Creating a React Component library using Rollup, Typescript, Sass and Storybook](https://blog.harveydelaney.com/creating-your-own-react-component-library/) <br> Explains how to create a React component library using Rollup
127+
- [Template: Debugging tests in VS Code](https://github.com/microsoft/vscode-recipes/tree/master/debugging-jest-tests)

package.json

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"react"
88
],
99
"homepage": "https://openscript.github.io/react-dsv-import/",
10-
"version": "0.2.3",
10+
"version": "0.3.0",
1111
"main": "dist/index.js",
1212
"module": "dist/es/index.js",
1313
"types": "dist/index.d.ts",
@@ -16,35 +16,35 @@
1616
"@babel/core": "^7.9.6",
1717
"@emotion/core": "^10.0.28",
1818
"@emotion/styled": "^10.0.27",
19-
"@rollup/plugin-typescript": "^4.1.1",
20-
"@storybook/addon-actions": "^5.3.18",
21-
"@storybook/addon-docs": "^5.3.18",
22-
"@storybook/addon-info": "^5.3.18",
23-
"@storybook/addon-links": "^5.3.18",
24-
"@storybook/addon-storysource": "^5.3.18",
25-
"@storybook/addons": "^5.3.18",
19+
"@rollup/plugin-typescript": "^4.1.2",
20+
"@storybook/addon-actions": "^5.3.19",
21+
"@storybook/addon-docs": "^5.3.19",
22+
"@storybook/addon-info": "^5.3.19",
23+
"@storybook/addon-links": "^5.3.19",
24+
"@storybook/addon-storysource": "^5.3.19",
25+
"@storybook/addons": "^5.3.19",
2626
"@storybook/preset-typescript": "^3.0.0",
27-
"@storybook/react": "^5.3.18",
27+
"@storybook/react": "^5.3.19",
2828
"@testing-library/jest-dom": "^5.8.0",
2929
"@testing-library/react": "^10.0.4",
3030
"@testing-library/react-hooks": "^3.2.1",
3131
"@types/jest": "^25.2.3",
32-
"@types/node": "^14.0.4",
32+
"@types/node": "^14.0.5",
3333
"@types/react": "^16.9.35",
3434
"@types/react-dom": "^16.9.8",
35-
"@typescript-eslint/eslint-plugin": "^2.34.0",
36-
"@typescript-eslint/parser": "^2.34.0",
35+
"@typescript-eslint/eslint-plugin": "^3.0.0",
36+
"@typescript-eslint/parser": "^3.0.0",
3737
"babel-loader": "^8.1.0",
3838
"babel-preset-react-app": "^9.1.2",
39-
"eslint": "^7.0.0",
39+
"eslint": "^7.1.0",
4040
"eslint-config-prettier": "^6.11.0",
4141
"eslint-plugin-prettier": "^3.1.3",
4242
"eslint-plugin-react": "^7.20.0",
4343
"jest": "^26.0.1",
4444
"prettier": "^2.0.5",
4545
"react-is": "^16.13.1",
4646
"react-test-renderer": "^16.13.1",
47-
"rollup": "^2.10.5",
47+
"rollup": "^2.10.9",
4848
"ts-jest": "^26.0.0",
4949
"ts-node": "^8.10.1",
5050
"tslib": "^2.0.0",

src/DSVImport.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { DSVImport, ColumnsType } from './';
33
import { action } from '@storybook/addon-actions';
44
import styled from '@emotion/styled';
55

6-
export default { title: 'Usage|API' };
6+
export default { title: 'Usage|Examples' };
77

88
type BasicType = { forename: string; surname: string; email: string };
99

src/DSVImport.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { State } from './models/state';
66
import { applyMiddlewares } from './middlewares/middleware';
77
import { createValidatorMiddleware } from './middlewares/validatorMiddleware';
88
import { ValidationError } from './models/validation';
9+
import { createTransformerMiddleware } from './middlewares/transformerMiddleware';
10+
import { Transformer } from './models/transformer';
911

1012
interface EventListenerProps<T> {
1113
onChange?: (value: T[]) => void;
@@ -31,16 +33,23 @@ const EventListener = <T extends { [key: string]: string }>(props: EventListener
3133
};
3234

3335
export interface Props<T> {
36+
columns: ColumnsType<T>;
3437
onChange?: (value: T[]) => void;
3538
onValidation?: (errors: ValidationError<T>[]) => void;
36-
columns: ColumnsType<T>;
39+
transformers?: Transformer[];
3740
}
3841

3942
export const DSVImport = <T extends { [key: string]: string }>(props: PropsWithChildren<Props<T>>) => {
4043
const DSVImportContext = getDSVImportContext<T>();
41-
const initialValues: State<T> = { columns: props.columns };
44+
const initialValues: State<T> = { columns: props.columns, transformers: props.transformers };
4245
const [state, dispatch] = useReducer(createReducer<T>(), initialValues);
43-
const enhancedDispatch = applyMiddlewares(state, dispatch, createParserMiddleware(), createValidatorMiddleware());
46+
const enhancedDispatch = applyMiddlewares(
47+
state,
48+
dispatch,
49+
createParserMiddleware(),
50+
createTransformerMiddleware(),
51+
createValidatorMiddleware()
52+
);
4453

4554
return (
4655
<DSVImportContext.Provider value={[state, enhancedDispatch]}>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { ColumnsType } from '../models/column';
2+
import { State } from '../models/state';
3+
import { createTransformerMiddleware } from './transformerMiddleware';
4+
5+
describe('validatorMiddleware', () => {
6+
type TestType = { forename: string; surname: string; email: string };
7+
const defaultColumns: ColumnsType<TestType> = [
8+
{ key: 'forename', label: 'Forename' },
9+
{ key: 'surname', label: 'Surname' },
10+
{ key: 'email', label: 'Email' }
11+
];
12+
const middleware = createTransformerMiddleware<TestType>();
13+
const parsed: TestType[] = [
14+
{ forename: 'Hans', surname: 'Muster', email: '[email protected]' },
15+
{ forename: 'Heidi', surname: ' Muster', email: '[email protected]' },
16+
{ forename: 'Joe', surname: 'Doe', email: ' [email protected] ' }
17+
];
18+
const trimTransformer = (value: string) => value.trim();
19+
const markTransformer = (value: string) => `${value}!`;
20+
21+
it('should not dispatch if there are no transformers', () => {
22+
const state: State<TestType> = { columns: defaultColumns };
23+
const dispatchMock = jest.fn();
24+
25+
middleware(state, dispatchMock, { type: 'setParsed', parsed });
26+
expect(dispatchMock).toBeCalledTimes(0);
27+
});
28+
29+
it('should run a transformer on all values', () => {
30+
const state: State<TestType> = { columns: defaultColumns, transformers: [trimTransformer] };
31+
const dispatchMock = jest.fn();
32+
33+
middleware(state, dispatchMock, { type: 'setParsed', parsed });
34+
35+
expect(dispatchMock).toBeCalledWith({
36+
type: 'setParsed',
37+
parsed: [
38+
{ forename: 'Hans', surname: 'Muster', email: '[email protected]' },
39+
{ forename: 'Heidi', surname: 'Muster', email: '[email protected]' },
40+
{ forename: 'Joe', surname: 'Doe', email: '[email protected]' }
41+
]
42+
});
43+
});
44+
45+
it('should run transformers on values of a certain column', () => {
46+
const state: State<TestType> = {
47+
columns: [
48+
{ key: 'forename', label: 'Forename' },
49+
{ key: 'surname', label: 'Surname', transformers: [trimTransformer, markTransformer] },
50+
{ key: 'email', label: 'Email' }
51+
]
52+
};
53+
const dispatchMock = jest.fn();
54+
55+
middleware(state, dispatchMock, { type: 'setParsed', parsed });
56+
57+
expect(dispatchMock).toBeCalledWith({
58+
type: 'setParsed',
59+
parsed: [
60+
{ forename: 'Hans', surname: 'Muster!', email: '[email protected]' },
61+
{ forename: 'Heidi', surname: 'Muster!', email: '[email protected]' },
62+
{ forename: 'Joe', surname: 'Doe!', email: ' [email protected] ' }
63+
]
64+
});
65+
});
66+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { State } from '../models/state';
2+
import { Dispatch } from 'react';
3+
import { Actions } from '../models/actions';
4+
import { ColumnsType } from '../models/column';
5+
import { Transformer } from '../models/transformer';
6+
7+
const executeGlobalTransformers = <T>(values: T[], transformer: Transformer, columns: ColumnsType<T>) => {
8+
return values.map<T>((r) => {
9+
const transformed = { ...r };
10+
columns.forEach((c) => {
11+
transformed[c.key] = (transformer(new String(r[c.key]).toString()) as unknown) as T[keyof T];
12+
});
13+
return transformed;
14+
});
15+
};
16+
17+
const executeColumnTransformers = <T>(values: T[], columns: ColumnsType<T>) => {
18+
return values.map<T>((r) => {
19+
const transformed = { ...r };
20+
columns.forEach((c) => {
21+
if (c.transformers) {
22+
transformed[c.key] = (c.transformers.reduce(
23+
(acc, t) => t(acc),
24+
new String(r[c.key]).toString()
25+
) as unknown) as T[keyof T];
26+
}
27+
});
28+
return transformed;
29+
});
30+
};
31+
32+
export const createTransformerMiddleware = <T>() => {
33+
return (state: State<T>, next: Dispatch<Actions<T>>, action: Actions<T>) => {
34+
if (action.type === 'setParsed') {
35+
let parsed = action.parsed;
36+
if (state.transformers) {
37+
parsed = state.transformers.reduce<T[]>((acc, t) => executeGlobalTransformers(acc, t, state.columns), parsed);
38+
}
39+
40+
const hasColumnTransformers = state.columns.find((c) => c.transformers) ? true : false;
41+
if (hasColumnTransformers) {
42+
parsed = executeColumnTransformers(parsed, state.columns);
43+
}
44+
45+
if (state.transformers || hasColumnTransformers) {
46+
next({ type: 'setParsed', parsed });
47+
}
48+
}
49+
};
50+
};

src/models/column.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
import { Rule } from './rule';
2+
import { Transformer } from './transformer';
23

3-
export type ColumnsType<T> = { key: keyof T; label: string; rules?: Rule[] }[];
4+
export type ColumnsType<T> = { key: keyof T; label: string; rules?: Rule[]; transformers?: Transformer[] }[];

src/models/state.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { ColumnsType } from './column';
22
import { ValidationError } from './validation';
3+
import { Transformer } from './transformer';
34

45
export interface State<T> {
56
raw?: string;
67
parsed?: T[];
78
validation?: ValidationError<T>[];
9+
transformers?: Transformer[];
810
columns: ColumnsType<T>;
911
}
1012

0 commit comments

Comments
 (0)