Skip to content

Commit

Permalink
Merge pull request #12 from openscript/develop
Browse files Browse the repository at this point in the history
Implement transformer feature
  • Loading branch information
openscript authored May 24, 2020
2 parents e2b60ce + 3775590 commit 5c50408
Show file tree
Hide file tree
Showing 12 changed files with 542 additions and 335 deletions.
19 changes: 10 additions & 9 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
module.exports = {
parser: "@typescript-eslint/parser",
parser: '@typescript-eslint/parser',
extends: [
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
"plugin:prettier/recommended"
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
'prettier/@typescript-eslint',
'plugin:prettier/recommended'
],
parserOptions: {
ecmaFeatures: {
jsx: true
},
ecmaVersion: 2018,
sourceType: "module"
sourceType: 'module'
},
rules: {
"@typescript-eslint/explicit-function-return-type": "off",
"react/prop-types": "off"
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'react/prop-types': 'off'
},
settings: {
react: {
version: "detect"
version: 'detect'
}
}
};
37 changes: 37 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Jest All",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest/bin/jest",
},
"runtimeExecutable": "~/.asdf/shims/node"
},
{
"type": "node",
"request": "launch",
"name": "Jest Current File",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": [
"${fileBasenameNoExtension}",
"--config",
"jest.config.js"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest/bin/jest",
},
"runtimeExecutable": "~/.asdf/shims/node"
}
]
}
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,39 @@ const columns = [
</DSVImport>
```

## API
The `<DSVImport<T>>` components has the following API:

| Property | Type | Description |
|:---------------|:-------------------------------|:-----------------------------------------------------------|
| `columns` | [ColumnType](#columntype) | Description of the expected columns |
| `transformers?`| [Transformer](#transformer)`[]`| Globally applied transformers |
| `onChange?` | `(value: T[]) => void` | Callback which is called after parsing the input |
| `onValidation?`| `(errors: Error<T>[]) => void` | Callback which is called if there are validation errors |

### Types
Within this section additional types are explained.

#### ColumnType
| Property | Type | Description |
|:----------------|:-----------------------------|:------------------------------------------------------------|
| `key` | `string` | Key of the current column |
| `label` | `string` | Label of the current column, which can be shown to the user |
| `rules?` | [Rule](#rule)`[]` | Validation rules which are applied to this column |
| `transformers?` | [Transformer](#transformer)`[]`| Transformers which are applied to this column |

#### Rule
| Property | Type | Description |
|:----------------|:-----------------------------|:------------------------------------------------------------|
| `message` | `string` | Error message |
| `contraint` | `{ unique: boolean } | { constraint: `[Constraint](#constraint)`}` | Constraint for this rule |

#### Constraint
`(value: string) => boolean`

#### Transformer
`(value: string) => string`

## Project
This section describes the status of the project.

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

✅ means the feature is implemented and released. ❌ indicates that a feature is planned.
Expand Down Expand Up @@ -91,3 +124,4 @@ The most important features of this component are:
- [Article: Using ESLint and Prettier in a TypeScript Project](https://www.robertcooper.me/using-eslint-and-prettier-in-a-typescript-project)
- [Template: Rollup Starter Lib (TypeScript)](https://github.com/rollup/rollup-starter-lib/tree/typescript)
- [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
- [Template: Debugging tests in VS Code](https://github.com/microsoft/vscode-recipes/tree/master/debugging-jest-tests)
28 changes: 14 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"react"
],
"homepage": "https://openscript.github.io/react-dsv-import/",
"version": "0.2.3",
"version": "0.3.0",
"main": "dist/index.js",
"module": "dist/es/index.js",
"types": "dist/index.d.ts",
Expand All @@ -16,35 +16,35 @@
"@babel/core": "^7.9.6",
"@emotion/core": "^10.0.28",
"@emotion/styled": "^10.0.27",
"@rollup/plugin-typescript": "^4.1.1",
"@storybook/addon-actions": "^5.3.18",
"@storybook/addon-docs": "^5.3.18",
"@storybook/addon-info": "^5.3.18",
"@storybook/addon-links": "^5.3.18",
"@storybook/addon-storysource": "^5.3.18",
"@storybook/addons": "^5.3.18",
"@rollup/plugin-typescript": "^4.1.2",
"@storybook/addon-actions": "^5.3.19",
"@storybook/addon-docs": "^5.3.19",
"@storybook/addon-info": "^5.3.19",
"@storybook/addon-links": "^5.3.19",
"@storybook/addon-storysource": "^5.3.19",
"@storybook/addons": "^5.3.19",
"@storybook/preset-typescript": "^3.0.0",
"@storybook/react": "^5.3.18",
"@storybook/react": "^5.3.19",
"@testing-library/jest-dom": "^5.8.0",
"@testing-library/react": "^10.0.4",
"@testing-library/react-hooks": "^3.2.1",
"@types/jest": "^25.2.3",
"@types/node": "^14.0.4",
"@types/node": "^14.0.5",
"@types/react": "^16.9.35",
"@types/react-dom": "^16.9.8",
"@typescript-eslint/eslint-plugin": "^2.34.0",
"@typescript-eslint/parser": "^2.34.0",
"@typescript-eslint/eslint-plugin": "^3.0.0",
"@typescript-eslint/parser": "^3.0.0",
"babel-loader": "^8.1.0",
"babel-preset-react-app": "^9.1.2",
"eslint": "^7.0.0",
"eslint": "^7.1.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.20.0",
"jest": "^26.0.1",
"prettier": "^2.0.5",
"react-is": "^16.13.1",
"react-test-renderer": "^16.13.1",
"rollup": "^2.10.5",
"rollup": "^2.10.9",
"ts-jest": "^26.0.0",
"ts-node": "^8.10.1",
"tslib": "^2.0.0",
Expand Down
2 changes: 1 addition & 1 deletion src/DSVImport.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DSVImport, ColumnsType } from './';
import { action } from '@storybook/addon-actions';
import styled from '@emotion/styled';

export default { title: 'Usage|API' };
export default { title: 'Usage|Examples' };

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

Expand Down
15 changes: 12 additions & 3 deletions src/DSVImport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { State } from './models/state';
import { applyMiddlewares } from './middlewares/middleware';
import { createValidatorMiddleware } from './middlewares/validatorMiddleware';
import { ValidationError } from './models/validation';
import { createTransformerMiddleware } from './middlewares/transformerMiddleware';
import { Transformer } from './models/transformer';

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

export interface Props<T> {
columns: ColumnsType<T>;
onChange?: (value: T[]) => void;
onValidation?: (errors: ValidationError<T>[]) => void;
columns: ColumnsType<T>;
transformers?: Transformer[];
}

export const DSVImport = <T extends { [key: string]: string }>(props: PropsWithChildren<Props<T>>) => {
const DSVImportContext = getDSVImportContext<T>();
const initialValues: State<T> = { columns: props.columns };
const initialValues: State<T> = { columns: props.columns, transformers: props.transformers };
const [state, dispatch] = useReducer(createReducer<T>(), initialValues);
const enhancedDispatch = applyMiddlewares(state, dispatch, createParserMiddleware(), createValidatorMiddleware());
const enhancedDispatch = applyMiddlewares(
state,
dispatch,
createParserMiddleware(),
createTransformerMiddleware(),
createValidatorMiddleware()
);

return (
<DSVImportContext.Provider value={[state, enhancedDispatch]}>
Expand Down
66 changes: 66 additions & 0 deletions src/middlewares/transformerMiddleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { ColumnsType } from '../models/column';
import { State } from '../models/state';
import { createTransformerMiddleware } from './transformerMiddleware';

describe('validatorMiddleware', () => {
type TestType = { forename: string; surname: string; email: string };
const defaultColumns: ColumnsType<TestType> = [
{ key: 'forename', label: 'Forename' },
{ key: 'surname', label: 'Surname' },
{ key: 'email', label: 'Email' }
];
const middleware = createTransformerMiddleware<TestType>();
const parsed: TestType[] = [
{ forename: 'Hans', surname: 'Muster', email: '[email protected]' },
{ forename: 'Heidi', surname: ' Muster', email: '[email protected]' },
{ forename: 'Joe', surname: 'Doe', email: ' [email protected] ' }
];
const trimTransformer = (value: string) => value.trim();
const markTransformer = (value: string) => `${value}!`;

it('should not dispatch if there are no transformers', () => {
const state: State<TestType> = { columns: defaultColumns };
const dispatchMock = jest.fn();

middleware(state, dispatchMock, { type: 'setParsed', parsed });
expect(dispatchMock).toBeCalledTimes(0);
});

it('should run a transformer on all values', () => {
const state: State<TestType> = { columns: defaultColumns, transformers: [trimTransformer] };
const dispatchMock = jest.fn();

middleware(state, dispatchMock, { type: 'setParsed', parsed });

expect(dispatchMock).toBeCalledWith({
type: 'setParsed',
parsed: [
{ forename: 'Hans', surname: 'Muster', email: '[email protected]' },
{ forename: 'Heidi', surname: 'Muster', email: '[email protected]' },
{ forename: 'Joe', surname: 'Doe', email: '[email protected]' }
]
});
});

it('should run transformers on values of a certain column', () => {
const state: State<TestType> = {
columns: [
{ key: 'forename', label: 'Forename' },
{ key: 'surname', label: 'Surname', transformers: [trimTransformer, markTransformer] },
{ key: 'email', label: 'Email' }
]
};
const dispatchMock = jest.fn();

middleware(state, dispatchMock, { type: 'setParsed', parsed });

expect(dispatchMock).toBeCalledWith({
type: 'setParsed',
parsed: [
{ forename: 'Hans', surname: 'Muster!', email: '[email protected]' },
{ forename: 'Heidi', surname: 'Muster!', email: '[email protected]' },
{ forename: 'Joe', surname: 'Doe!', email: ' [email protected] ' }
]
});
});
});
50 changes: 50 additions & 0 deletions src/middlewares/transformerMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { State } from '../models/state';
import { Dispatch } from 'react';
import { Actions } from '../models/actions';
import { ColumnsType } from '../models/column';
import { Transformer } from '../models/transformer';

const executeGlobalTransformers = <T>(values: T[], transformer: Transformer, columns: ColumnsType<T>) => {
return values.map<T>((r) => {
const transformed = { ...r };
columns.forEach((c) => {
transformed[c.key] = (transformer(new String(r[c.key]).toString()) as unknown) as T[keyof T];
});
return transformed;
});
};

const executeColumnTransformers = <T>(values: T[], columns: ColumnsType<T>) => {
return values.map<T>((r) => {
const transformed = { ...r };
columns.forEach((c) => {
if (c.transformers) {
transformed[c.key] = (c.transformers.reduce(
(acc, t) => t(acc),
new String(r[c.key]).toString()
) as unknown) as T[keyof T];
}
});
return transformed;
});
};

export const createTransformerMiddleware = <T>() => {
return (state: State<T>, next: Dispatch<Actions<T>>, action: Actions<T>) => {
if (action.type === 'setParsed') {
let parsed = action.parsed;
if (state.transformers) {
parsed = state.transformers.reduce<T[]>((acc, t) => executeGlobalTransformers(acc, t, state.columns), parsed);
}

const hasColumnTransformers = state.columns.find((c) => c.transformers) ? true : false;
if (hasColumnTransformers) {
parsed = executeColumnTransformers(parsed, state.columns);
}

if (state.transformers || hasColumnTransformers) {
next({ type: 'setParsed', parsed });
}
}
};
};
3 changes: 2 additions & 1 deletion src/models/column.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Rule } from './rule';
import { Transformer } from './transformer';

export type ColumnsType<T> = { key: keyof T; label: string; rules?: Rule[] }[];
export type ColumnsType<T> = { key: keyof T; label: string; rules?: Rule[]; transformers?: Transformer[] }[];
2 changes: 2 additions & 0 deletions src/models/state.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { ColumnsType } from './column';
import { ValidationError } from './validation';
import { Transformer } from './transformer';

export interface State<T> {
raw?: string;
parsed?: T[];
validation?: ValidationError<T>[];
transformers?: Transformer[];
columns: ColumnsType<T>;
}

Expand Down
1 change: 1 addition & 0 deletions src/models/transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Transformer = (value: string) => string;
Loading

0 comments on commit 5c50408

Please sign in to comment.