Skip to content

Commit 51c9d85

Browse files
author
Augustin Le Fèvre
committed
Stronger typings
1 parent e8c7239 commit 51c9d85

9 files changed

+1586
-1382
lines changed

.eslintrc

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
22
"extends": "airbnb",
3+
"plugins": ["flowtype"],
34
"rules": {
4-
"indent": [
5-
0
6-
],
5+
"indent": [0],
76
"no-confusing-arrow": 0,
8-
"no-useless-escape": 0
7+
"no-useless-escape": 0,
8+
"flowtype/define-flow-type": 1
99
},
1010
"parser": "babel-eslint"
1111
}

package.json

+65-61
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,67 @@
11
{
2-
"name": "@kilix/functional-validation",
3-
"version": "1.3.1",
4-
"description": "Lightweight and customisable validation, built through composition",
5-
"main": "lib/index.js",
6-
"scripts": {
7-
"prebuild": "rimraf lib/",
8-
"build": "npm-run-all --parallel build:*",
9-
"build:main": "babel src --out-dir lib --ignore '**/__tests__/**'",
10-
"build:umd": "webpack --output-filename index.umd.js -p",
11-
"flow": "flow check",
12-
"test": "jest --watch",
13-
"test:ci": "jest --coverage && codecov",
14-
"lint": "eslint ./src",
15-
"precommit": "lint-staged",
16-
"prepublish": "npm run build",
17-
"format": "prettier --trailing-comma all --single-quote --tab-width 4 --print-width 100 --write 'src/**/*.js'"
18-
},
19-
"repository": {
20-
"type": "git",
21-
"url": "git+https://github.com/kilix/functional-validation.git"
22-
},
23-
"keywords": [],
24-
"files": [
25-
"lib"
26-
],
27-
"author": "[email protected]",
28-
"license": "MIT",
29-
"devDependencies": {
30-
"babel-cli": "^6.23.0",
31-
"babel-core": "^6.23.1",
32-
"babel-eslint": "^7.2.0",
33-
"babel-loader": "^6.3.2",
34-
"babel-plugin-transform-flow-strip-types": "^6.22.0",
35-
"babel-plugin-transform-object-rest-spread": "^6.23.0",
36-
"babel-preset-env": "^1.1.8",
37-
"codecov": "^1.0.1",
38-
"cross-env": "^3.1.4",
39-
"eslint": "^3.16.1",
40-
"eslint-config-airbnb": "^14.1.0",
41-
"eslint-plugin-import": "^2.2.0",
42-
"eslint-plugin-jsx-a11y": "^4.0.0",
43-
"eslint-plugin-react": "^6.10.0",
44-
"flow-bin": "^0.42.0",
45-
"husky": "^0.13.2",
46-
"jest": "^19.0.1",
47-
"lint-staged": "^3.3.1",
48-
"npm-run-all": "^4.0.2",
49-
"prettier": "^0.21.0",
50-
"rimraf": "^2.6.1",
51-
"webpack": "^2.2.1"
52-
},
53-
"dependencies": {
54-
"ramda": "^0.23.0"
55-
},
56-
"peerDependencies": {},
57-
"lint-staged": {
58-
"*.js": [
59-
"prettier --trailing-comma all --single-quote --tab-width 4 --print-width 100 --write",
60-
"git add"
61-
]
62-
}
2+
"name": "@kilix/functional-validation",
3+
"version": "1.3.1",
4+
"description": "Lightweight and customisable validation, built through composition",
5+
"main": "lib/index.js",
6+
"scripts": {
7+
"prebuild": "rimraf lib/",
8+
"build": "npm-run-all --parallel build:*",
9+
"build:main": "babel src --out-dir lib --ignore '**/__tests__/**'",
10+
"build:umd": "webpack --output-filename index.umd.js -p",
11+
"build:flow": "babel-flow src --out-dir lib",
12+
"flow": "flow check",
13+
"test": "jest --watch",
14+
"test:ci": "jest --coverage && codecov",
15+
"lint": "eslint ./src",
16+
"precommit": "lint-staged",
17+
"prepublishOnly": "npm run build",
18+
"format": "prettier --trailing-comma all --single-quote --tab-width 4 --print-width 100 --write 'src/**/*.js'"
19+
},
20+
"repository": {
21+
"type": "git",
22+
"url": "git+https://github.com/kilix/functional-validation.git"
23+
},
24+
"keywords": [],
25+
"files": ["lib"],
26+
"jest": {
27+
"testPathIgnorePatterns": ["<rootDir>/src/__tests__/typingTests.js"]
28+
},
29+
"author": "[email protected]",
30+
"license": "MIT",
31+
"devDependencies": {
32+
"babel-cli": "^6.23.0",
33+
"babel-cli-flow": "^1.0.0",
34+
"babel-core": "^6.23.1",
35+
"babel-eslint": "^7.2.0",
36+
"babel-loader": "^6.3.2",
37+
"babel-plugin-transform-flow-strip-types": "^6.22.0",
38+
"babel-plugin-transform-object-rest-spread": "^6.23.0",
39+
"babel-preset-env": "^1.1.8",
40+
"codecov": "^2.3.0",
41+
"cross-env": "^5.0.5",
42+
"eslint": "^4.4.1",
43+
"eslint-config-airbnb": "^15.1.0",
44+
"eslint-plugin-flowtype": "^2.35.0",
45+
"eslint-plugin-import": "2.7",
46+
"eslint-plugin-jsx-a11y": "5.1.1",
47+
"flow-bin": "^0.52.0",
48+
"husky": "^0.14.3",
49+
"jest": "^20.0.4",
50+
"lint-staged": "^4.0.3",
51+
"npm-run-all": "^4.0.2",
52+
"prettier": "^1.5.3",
53+
"rimraf": "^2.6.1",
54+
"webpack": "^2.2.1"
55+
},
56+
"dependencies": {
57+
"eslint-plugin-react": "7.2",
58+
"ramda": "^0.23.0"
59+
},
60+
"peerDependencies": {},
61+
"lint-staged": {
62+
"*.js": [
63+
"prettier --trailing-comma all --single-quote --tab-width 4 --print-width 100 --write",
64+
"git add"
65+
]
66+
}
6367
}

src/__tests__/typingTests.js

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// @flow
2+
/* eslint-disable */
3+
import {
4+
validateModel,
5+
createValidation,
6+
createSimpleValidation,
7+
type ErrorT,
8+
getFieldError,
9+
getFieldSpecificErrors,
10+
fieldHasSpecificErrors,
11+
runConditionalValidation,
12+
validateRequired,
13+
type ModelValidatorT,
14+
createNestedValidation,
15+
} from '../index';
16+
17+
type UserModel = { id: number, name?: string, value: ?string, person: { name: string } };
18+
const userModel: UserModel = {
19+
id: 2,
20+
name: 'John',
21+
value: null,
22+
person: { name: 'Plop' },
23+
};
24+
25+
// $FlowFixMe
26+
validateModel();
27+
28+
const validateName = createValidation('name', user => user.name, name => (name ? 'missing' : null));
29+
// $FlowFixMe
30+
const validateAge = createValidation('age', user => user.age, age => (age ? 'missing' : null));
31+
const validateCheckPropTyping = createValidation(
32+
'value',
33+
user => user.value,
34+
// $FlowFixMe
35+
value => (value.length ? null : 'missing'),
36+
);
37+
38+
const validateSimpleName = createSimpleValidation(name => (name ? null : 'missing'), 'name');
39+
40+
// $FlowFixMe
41+
const validateSimpleAge = createSimpleValidation(age => (age ? null : 'missing'), 'age');
42+
43+
const correctConditional = runConditionalValidation(({ name }) => !!name && name.length > 0, [
44+
validateName,
45+
]);
46+
// $FlowFixMe
47+
const wrongConditional = runConditionalValidation(({ value }) => value.length > 0, [validateName]);
48+
49+
const conditionalWithGoodValidations = runConditionalValidation(() => true, [validateName]);
50+
51+
const validateCorrectRequired = validateRequired('name');
52+
// $FlowFixMe
53+
const validateWrongRequired = validateRequired('age');
54+
55+
const isNameMissing = name => (name.length > 0 ? null : 'missing');
56+
const nestedValidation = createNestedValidation(isNameMissing, ['person', 'name']);
57+
58+
// $FlowFixMe
59+
const wrongNestedValidation = createNestedValidation(isNameMissing, 'person.name');
60+
61+
const validateUser: ModelValidatorT<UserModel> = validateModel([
62+
validateName,
63+
validateAge,
64+
validateSimpleName,
65+
validateSimpleAge,
66+
validateCheckPropTyping,
67+
correctConditional,
68+
wrongConditional,
69+
conditionalWithGoodValidations,
70+
validateCorrectRequired,
71+
validateWrongRequired,
72+
nestedValidation,
73+
wrongNestedValidation,
74+
]);
75+
76+
validateUser(userModel);
77+
78+
const errors = [{ field: 'id', error: 'missing' }];
79+
// $FlowFixMe
80+
const fieldErrors: $ReadOnlyArray<number> = getFieldError(errors)('id');
81+
// $FlowFixMe
82+
const wrongTypeSpecificErrors: $ReadOnlyArray<number> = getFieldSpecificErrors(errors, 'id');
83+
const specificErrors: $ReadOnlyArray<ErrorT> = getFieldSpecificErrors(errors, 'id');
84+
// $FlowFixMe
85+
const wrongHasError: number = fieldHasSpecificErrors(errors, 'id');
86+
const hasError: boolean = fieldHasSpecificErrors(errors, 'id');
87+
88+
// TODO
89+
// Mismatch between ValidationT & ModelValidator?
90+
// const conditionalWithWrongValidations: ModelValidatorT<UserModel> = runConditionalValidation(
91+
// () => true,
92+
// [validateAge],
93+
// );
94+
95+
// Not sure there's a way to retrieve the type of value
96+
// const simpleValidationWithTypedValidator = createSimpleValidation(
97+
// value => value.length > 0 ? null : 'missing',
98+
// 'value',
99+
// );

src/createValidations.js

+12-8
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,21 @@ import R from 'ramda';
33

44
import createValidation from './createValidation';
55
import { testMissingValue } from './fieldValidators';
6+
import type { ErrorT } from './validateModel';
7+
import type { CreateSimpleValidation, CreateNestedValidation } from './types';
68

7-
const createNestedValidation = R.curry((validateField: () =>
8-
| string
9-
| null, fieldPath: Array<string>) =>
10-
createValidation(fieldPath.join('.'), R.path(fieldPath), validateField));
9+
const createNestedValidation: CreateNestedValidation = R.curry(
10+
(validateField: () => string | null, fieldPath: $ReadOnlyArray<string>) =>
11+
createValidation(fieldPath.join('.'), R.path(fieldPath), validateField),
12+
);
1113

12-
// TODO Fix typing because of curry
13-
const createSimpleValidation = R.curry((validateField: () => string | null, field: string) =>
14-
createValidation(field, R.prop(field), validateField));
14+
const createSimpleValidation: CreateSimpleValidation = R.curry(
15+
(validateField: () => string | null, field: string) =>
16+
createValidation(field, R.prop(field), validateField),
17+
);
1518

16-
const validateRequired = createSimpleValidation(testMissingValue);
19+
type ValidateRequired = <T: Object>(field: $Keys<T>) => (model: T) => ErrorT | null;
20+
const validateRequired: ValidateRequired = createSimpleValidation(testMissingValue);
1721
const validateNestedRequired = createNestedValidation(testMissingValue);
1822

1923
export { createNestedValidation, createSimpleValidation, validateRequired, validateNestedRequired };

src/fieldValidators.js

+7-5
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ const testMissingValue = (field: any): string | null =>
1717
* @param {Date|moment} limit - The reference date
1818
* @return {string} "before" if the date is before the limit, and both are specified, null if not
1919
*/
20-
type DatesT = { date: ?string, limit: ?string };
20+
type DateT = string | Date | any; // Also handle moment objects, or object with a valueOf
21+
const getDate = (date: DateT) => new Date(date.valueOf ? date.valueOf() : date);
22+
type DatesT = { date: ?DateT, limit: ?DateT };
2123
const testDateAfter = ({ date, limit }: DatesT) =>
22-
date === null || limit === null || new Date(date) - new Date(limit) > 0 ? null : 'before';
24+
!date || !limit || getDate(date) - getDate(limit) > 0 ? null : 'before';
2325

2426
/**
2527
* @desc Check if a date is before the given date
@@ -28,7 +30,7 @@ const testDateAfter = ({ date, limit }: DatesT) =>
2830
* @return {string} "after" if the date is after the limit, and both are specified, null if not
2931
*/
3032
const testDateBefore = ({ date, limit }: DatesT) =>
31-
date === null || limit === null || new Date(date) - new Date(limit) < 0 ? null : 'after';
33+
!date || !limit || getDate(date) - getDate(limit) < 0 ? null : 'after';
3234

3335
/**
3436
* @desc Test if a value is greater than another
@@ -55,7 +57,7 @@ const testOnlyLetters = (value: ?string) =>
5557
* @return {?string} 'wrongLength' if the item doesn't have the specified length, null if it has
5658
*/
5759
const testLength = R.curry(
58-
(length, value) => !value || !length || value.length === length ? null : 'wrongLength',
60+
(length, value) => (!value || !length || value.length === length ? null : 'wrongLength'),
5961
);
6062

6163
/**
@@ -88,7 +90,7 @@ const checkMaxLength = R.curry(
8890
* @return {?string} 'wrongValue' if the value is outside of the boundaries, null if not
8991
*/
9092
const isBetween = R.curry(
91-
(min: number, max: number, input) => input < min || input > max ? 'wrongValue' : null,
93+
(min: number, max: number, input) => (input < min || input > max ? 'wrongValue' : null),
9294
);
9395

9496
export {

src/getFieldError.js

+10-4
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@
22
import R from 'ramda';
33
import type { ErrorT } from './validateModel';
44

5-
const getFieldError = (errors: Array<ErrorT>) =>
6-
(field: string) => errors.filter(R.propEq('field', field)).map(R.prop('error'));
5+
const getFieldError = (errors: $ReadOnlyArray<ErrorT>) => (field: string): $ReadOnlyArray<string> =>
6+
errors.filter(R.propEq('field', field)).map(R.prop('error'));
77

8-
const getFieldSpecificErrors = (errors: Array<ErrorT>, fieldName: string) =>
8+
const getFieldSpecificErrors = (
9+
errors: $ReadOnlyArray<ErrorT>,
10+
fieldName: string,
11+
): $ReadOnlyArray<ErrorT> =>
912
errors.filter(({ field, error }) => field === fieldName && error !== 'missing');
1013

11-
const fieldHasSpecificErrors = R.pipe(getFieldSpecificErrors, R.isEmpty, R.not);
14+
const fieldHasSpecificErrors: (
15+
errors: $ReadOnlyArray<ErrorT>,
16+
fieldName: string,
17+
) => boolean = R.pipe(getFieldSpecificErrors, R.isEmpty, R.not);
1218

1319
export { fieldHasSpecificErrors, getFieldError, getFieldSpecificErrors };

src/types.js

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// @flow
2+
import type { ErrorT } from './validateModel';
3+
4+
type Validation = (value: *) => string | null;
5+
6+
type _CreateSimpleValidation1 = <M>(
7+
validation: Validation,
8+
) => (field: $Keys<M>) => M => ErrorT | null;
9+
type _CreateSimpleValidation2 = <M>(validation: Validation, field: $Keys<M>) => M => ErrorT | null;
10+
11+
export type CreateSimpleValidation = _CreateSimpleValidation1 & _CreateSimpleValidation2;
12+
13+
type _CreateNestedValidation1 = <M>(
14+
validation: Validation,
15+
) => (path: Array<string>) => M => ErrorT | null;
16+
type _CreateNestedValidation2 = <M>(
17+
validation: Validation,
18+
path: Array<string>,
19+
) => M => ErrorT | null;
20+
export type CreateNestedValidation = _CreateNestedValidation1 & _CreateNestedValidation2;

src/validateModel.js

+10-6
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ import R from 'ramda';
33

44
export type ErrorT = { field: string, error: string };
55
type MaybeErrorT = ErrorT | null;
6-
export type ValidationT<T> = (model: T) => Array<ErrorT> | Array<MaybeErrorT> | MaybeErrorT;
7-
export type ValidationsT<T> = Array<ValidationT<T>>;
8-
export type ModelValidatorT<T> = (model: T) => Array<ErrorT>;
96

10-
const validateModel = <T>(validations: ValidationsT<T>): ModelValidatorT<T> =>
11-
R.memoize((model: T): Array<ErrorT> =>
12-
R.pipe(R.map(validation => validation(model)), R.flatten, R.filter(Boolean))(validations));
7+
export type ValidationT<T> = (model: T) => $ReadOnlyArray<ErrorT | MaybeErrorT> | MaybeErrorT;
8+
9+
export type ModelValidatorT<T> = (model: T) => $ReadOnlyArray<ErrorT>;
10+
export type ValidationsT<T> = $ReadOnlyArray<ValidationT<T>>;
11+
12+
function validateModel<T>(validations: ValidationsT<T>): ModelValidatorT<T> {
13+
return R.memoize((model: T): $ReadOnlyArray<ErrorT> =>
14+
R.pipe(R.map(validation => validation(model)), R.flatten, R.filter(Boolean))(validations),
15+
);
16+
}
1317

1418
export default validateModel;

0 commit comments

Comments
 (0)