├── .flowconfig ├── lib ├── utils.ts ├── contracts.ts ├── schemas │ ├── valid-schema.ts │ ├── bool-schema.ts │ ├── enumeration-schema.ts │ ├── union-schema.ts │ ├── string-schema.ts │ ├── array-schema.ts │ ├── number-schema.ts │ ├── object-schema.ts │ ├── base-schema.ts │ └── date-schema.ts ├── errors.ts └── is.ts ├── .nycrc ├── .travis.yml ├── init-hooks.sh ├── .gitignore ├── .npmignore ├── tsconfig.json ├── tsconfig.es5.json ├── tslint.json ├── git-hooks └── pre-commit.sh ├── docs ├── schemas │ ├── BOOL.md │ ├── README.md │ ├── UNION.md │ ├── STRING.md │ ├── ENUMERATION.md │ ├── OBJECT.md │ ├── NUMBER.md │ └── ARRAY.md ├── QUICKSTART.md └── README.md ├── test ├── helpers │ └── index.js └── tests │ ├── bool.spec.js │ ├── enumeration.spec.js │ ├── union.spec.js │ ├── strings.spec.js │ ├── array.spec.js │ ├── object.spec.js │ ├── numbers.spec.js │ └── date.spec.js ├── LICENSE ├── flow-typed ├── test_fluent-schemer_v3.x.x.js └── flow_v0.60.x- │ └── fluent-schemer_v3.x.x.js ├── package.json ├── webpack.config.js ├── CHANGELOG.md ├── README.md └── index.ts /.flowconfig: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | export const id = (x: T) => x; 2 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extension": [".ts"], 3 | "sourceMap": true 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8.11.3" 4 | install: yarn 5 | script: yarn && yarn lint && yarn build && yarn test 6 | after_success: yarn run coveralls 7 | -------------------------------------------------------------------------------- /init-hooks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | HOOKS="pre-commit" 4 | HOOKS_DIR=".git/hooks" 5 | 6 | for HOOK in $HOOKS; do 7 | rm "${HOOKS_DIR}/${HOOK}" 8 | cp "./git-hooks/${HOOK}.sh" "${HOOKS_DIR}/${HOOK}" 9 | done -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | npm-debug.log 5 | .vscode 6 | jsconfig.json 7 | typings.json 8 | typings 9 | dist 10 | *.d.ts 11 | *.map 12 | index.js 13 | index.es.min.js 14 | .*.swp 15 | -------------------------------------------------------------------------------- /lib/contracts.ts: -------------------------------------------------------------------------------- 1 | export interface IValidationError { 2 | type: string; 3 | message: string; 4 | path: string; 5 | } 6 | 7 | export interface IErrorFeedback { 8 | corrected: TValidated; 9 | errors: IValidationError[]; 10 | errorsCount: number; 11 | } 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | typings 3 | node_modules 4 | .nyc_output 5 | coverage 6 | .travis.yml 7 | typings.json 8 | ./test 9 | .travis.yml 10 | ./lib 11 | tsconfig.json 12 | webpack.config.js 13 | tslint.json 14 | git-hooks/ 15 | init-hooks.sh 16 | flow-demo.js 17 | .babelrc 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "declaration": true, 5 | "lib": ["es2015", "es2016", "es2017"], 6 | "target": "es6", 7 | "module": "commonjs", 8 | "noImplicitAny": true, 9 | "noUnusedLocals": true, 10 | "strict": true, 11 | "pretty": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "declaration": true, 5 | "target": "es5", 6 | "lib": ["es2015", "es2016", "es2017"], 7 | "module": "commonjs", 8 | "noImplicitAny": true, 9 | "noUnusedLocals": true, 10 | "strict": true, 11 | "pretty": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/schemas/valid-schema.ts: -------------------------------------------------------------------------------- 1 | import BaseSchema from './base-schema'; 2 | 3 | export class ValidSchema extends BaseSchema { 4 | get type() { 5 | return 'valid'; 6 | } 7 | 8 | public validateType(value: any): value is any { 9 | return true; 10 | } 11 | 12 | public validate(value: any, path: string) { 13 | return { errors: [], errorsCount: 0, corrected: value }; 14 | } 15 | } 16 | 17 | export default new ValidSchema; 18 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "browser": true, 5 | "es6": true 6 | }, 7 | "globals": { 8 | "describe": true, 9 | "it": true 10 | }, 11 | "extends": "tslint:recommended", 12 | "rules": { 13 | "indent": [ 14 | true 15 | ], 16 | "quotemark": [ 17 | true, 18 | "single" 19 | ], 20 | "new-parens": false, 21 | "arrow-parens": [false, "ban-single-arg-parens"], 22 | "interface-over-type-literal": [false], 23 | "max-classes-per-file": false, 24 | "variable-name": [ 25 | "allow-leading-underscore" 26 | ], 27 | "use-isnan": true 28 | } 29 | } -------------------------------------------------------------------------------- /git-hooks/pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep "\.ts$") 4 | 5 | echo $STAGED_FILES; 6 | if [[ "$STAGED_FILES" == "" ]]; then 7 | exit 0 8 | fi 9 | 10 | PASSING=true 11 | 12 | echo "\nRunning TSlint:\n"; 13 | 14 | for FILE_NAME in $STAGED_FILES 15 | do 16 | yarn tslint "$FILE_NAME" 17 | 18 | if [[ "$?" == 0 ]]; then 19 | echo "tslint PASSED: $FILE_NAME" 20 | else 21 | echo "tslint FAILED: $FILE_NAME" 22 | PASSING=false 23 | fi 24 | done 25 | 26 | if ! $PASSING; then 27 | echo "Failed TSlint checks" 28 | exit 1 29 | else 30 | echo "TSlint checks passed" 31 | fi 32 | 33 | exit $? 34 | -------------------------------------------------------------------------------- /docs/schemas/BOOL.md: -------------------------------------------------------------------------------- 1 | # `BoolSchema` **extends** `BaseSchema` 2 | 3 | `BoolSchema` is exported by the default name of `bool`. Currently offers the only functionality of validating whether a value is strictly boolean. Any validation errors are returned as an array of error objects. 4 | 5 | | schema-specific methods | explanation | 6 | |:-----------------------:|:-----------:| 7 | | - | - | 8 | 9 | ```js 10 | const { bool } = require('fluent-schemer'); 11 | 12 | // input values should be either true or false 13 | const boolSchema = bool().required(); 14 | 15 | const values = [true, false, '', 'true', null]; 16 | 17 | for (const val of values) { 18 | console.log(boolSchema.validate(val)); 19 | } 20 | ``` 21 | -------------------------------------------------------------------------------- /test/helpers/index.js: -------------------------------------------------------------------------------- 1 | export function dfs(root, cb) { 2 | if (Array.isArray(root)) { 3 | for (const value of root) { 4 | cb(value); 5 | } 6 | } else { 7 | for (const key in root) { 8 | dfs(root[key], cb); 9 | } 10 | } 11 | } 12 | 13 | export const shouldReturnErrors = (assert, schema, values, options = {}) => { 14 | const expectedType = options.type; 15 | const root = options.root || 'root'; 16 | const expectedPath = options.path || root; 17 | 18 | for (const val of values) { 19 | const errorsArray = []; 20 | dfs(schema.validate(val, root).errors, err => errorsArray.push(err)); 21 | assert.is(errorsArray.length, 1); 22 | const [err] = errorsArray; 23 | 24 | assert.is(err.path, expectedPath); 25 | assert.is(err.type, expectedType); 26 | } 27 | } 28 | 29 | export function shouldNotReturnErrors(assert, schema, values) { 30 | values.forEach(val => assert.is(schema.validate(val).errorsCount, 0)); 31 | } 32 | -------------------------------------------------------------------------------- /docs/schemas/README.md: -------------------------------------------------------------------------------- 1 | # Schemas 2 | 3 | Every schema that extends `BaseSchema` supports `.not()`, `.required()`, `.predicate()`, `.validate()`, `.validateWithCorrectType()`. 4 | 5 | | method | explanation | 6 | |:-------------------------- |:--------------------------------------------------------------------------------------- | 7 | | not(v1, v2, v3, ...) | values must not be equal to v1, v2, v3, ... | 8 | | required() | values must be of the type of the schema - *Schema.validateType(value) must return true | 9 | | predicate((value) => bool) | values must return true for the specified predicate function | 10 | 11 | ## List of available schemas 12 | - [number](./NUMBER.md) 13 | - [bool](./BOOL.md) 14 | - [string](./STRING.md) 15 | - [array](./ARRAY.md) 16 | - [enumeration](./ENUMERATION.md) 17 | - [object](./OBJECT.md) 18 | - [union](./UNION.md) 19 | -------------------------------------------------------------------------------- /lib/errors.ts: -------------------------------------------------------------------------------- 1 | export class ValidationError { 2 | public type: string; 3 | public message: string; 4 | public path: string; 5 | 6 | constructor(type: string, message: string, path: string) { 7 | this.type = type; 8 | this.message = message; 9 | this.path = path; 10 | } 11 | } 12 | 13 | export const ERROR_TYPES = Object.freeze({ 14 | ARGUMENT: 'argument', 15 | COMPOSITE: 'composite', 16 | PREDICATE: 'predicate', 17 | RANGE: 'range', 18 | TYPE: 'type', 19 | }); 20 | 21 | export class CompositeError extends ValidationError { 22 | public errors: ValidationError[]; 23 | 24 | constructor(path: string, errors: ValidationError[]) { 25 | super(ERROR_TYPES.COMPOSITE, 'More than one error could have occurred for the provided path', path); 26 | this.errors = errors; 27 | } 28 | } 29 | 30 | export const createError = (type: string, message: string, path: string) => new ValidationError(type, message, path); 31 | export const createCompositeError = (path: string, errors: ValidationError[]) => new CompositeError(path, errors); 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Konstantin Simeonov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /lib/is.ts: -------------------------------------------------------------------------------- 1 | function createIs(type: string): (value: any) => value is T { 2 | const typeName = type[0].toUpperCase() + type.substr(1); 3 | 4 | return (value: any): value is T => Object.prototype.toString.call(value) === `[object ${typeName}]`; 5 | } 6 | 7 | export const Obj = createIs('object'); 8 | export const String = createIs('string'); 9 | export const Numeric = createIs('number'); 10 | export const Bool = createIs('boolean'); 11 | export const Null = createIs('number'); 12 | export const Undefined = createIs('undefined'); 13 | export const RegExp = createIs('regExp'); 14 | export const Date = createIs('date'); 15 | export const Array = createIs('array'); 16 | /* tslint:disable-next-line ban-types*/ 17 | export const Function = createIs('function'); 18 | /* tslint:enable ban-types*/ 19 | export const NullOrUndefined = (value: any): value is (null | undefined) => Null(value) || Undefined(value); 20 | export const StrictNumber = (value: any): value is number => Numeric(value) && !isNaN(value); 21 | export const ValidLength = (value: any): boolean => Numeric(value) && 0 <= value; 22 | -------------------------------------------------------------------------------- /lib/schemas/bool-schema.ts: -------------------------------------------------------------------------------- 1 | import * as is from '../is'; 2 | import BaseSchema from './base-schema'; 3 | 4 | export const name = 'bool'; 5 | 6 | const typeName = 'bool'; 7 | 8 | /** 9 | * Provide validation for boolean values. 10 | * Pretty much provides only type checking. 11 | * @export 12 | * @class BoolSchema 13 | * @extends {BaseSchema} 14 | */ 15 | export default class BoolSchema extends BaseSchema { 16 | 17 | /** 18 | * Returns 'bool'. 19 | * @readonly 20 | * @type {string} 21 | * @memberof BoolSchema 22 | */ 23 | public get type(): string { 24 | return typeName; 25 | } 26 | 27 | /** 28 | * Returns true when a value is either true, false or a Boolean object. 29 | * Otherwise returns false. 30 | * @param {*} value The value to be validated. 31 | * @returns {value is boolean} Whether the value is a valid bool. 32 | * @memberof BoolSchema 33 | * 34 | * @example 35 | * bool().validateType(true); // true 36 | * bool().validateType(false); // true 37 | * bool().validateType(new Boolean(5)); // true 38 | * bool().validateType(nul;); // false 39 | */ 40 | public validateType(value: any): value is boolean { 41 | return is.Bool(value); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/schemas/UNION.md: -------------------------------------------------------------------------------- 1 | # `UnionSchema` **extends** `BaseSchema` 2 | 3 | `UnionSchema` is aliased as `union`. Allows creation of union type schemas - for example, a union is a value that is either a string or a number value. A union validation is considered successful and will not return errors when at least one of the union's subschemas matches the value that is being validated. Any validation errors are returned as an array of error objects. 4 | 5 | | schema-specific methods | explanation | 6 | |:-----------------------:|:-----------:| 7 | | - | - | 8 | 9 | ## Sample union: 10 | 11 | ```js 12 | import { string } from 'fluent-schemer'; 13 | 14 | const schema = union( 15 | string().minlength(5), 16 | object({ name: string().minlength(10) }) 17 | ) 18 | .required(); 19 | 20 | const values = ['ivancho', { name: 'ivancho' }, 'dsf', null, { name: 'kyci' }]; 21 | 22 | for(const v of values) { 23 | console.log(schema.validate(v)); 24 | } 25 | ``` 26 | 27 | - Sample union: 28 | 29 | ```js 30 | import { string } from 'fluent-schemer'; 31 | 32 | const schema = union( 33 | number().min(0), 34 | bool(), 35 | array().minlength(3) 36 | ).required(); 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/QUICKSTART.md: -------------------------------------------------------------------------------- 1 | # How to get going in a minute 2 | 3 | ## Download via npm/bower/yarn 4 | `npm install fluent-schemer` 5 | 6 | `bower install fluent-schemer` 7 | 8 | `yarn add fluent-schemer` 9 | 10 | ## Set it up in your code 11 | 12 | ### Node.js 13 | 14 | - ES2015 15 | ```js 16 | import { number } from 'fluent-schemer'; 17 | 18 | const ageSchema = number().min(0).integer().required(); 19 | 20 | console.log(ageSchema.validate(-1.5)); 21 | ``` 22 | 23 | - ES5 24 | ```js 25 | const { string } = require('fluent-schemer'); 26 | 27 | var ageSchema = number().min(0).integer().required(); 28 | 29 | console.log(ageSchema.validate(-1.5)); 30 | ``` 31 | 32 | ### Browser 33 | ```html 34 | 35 | ``` 36 | 37 | ```js 38 | const { number } = window.FluentSchemer; 39 | 40 | const ageSchema = number().min(0).integer().required(); 41 | 42 | console.log(ageSchema.validate(-1.5)); 43 | ``` 44 | 45 | ### ES2015 (Harmony) modules 46 | ```js 47 | import { number, string, bool, object, array } from 'fluent-schemer'; 48 | 49 | const ageSchema = number().min(0).integer().required(); 50 | 51 | console.log(ageSchema.validate(-1.5)); 52 | ``` 53 | 54 | ## Start declaring and validating - [docs & examples](./schemas) 55 | -------------------------------------------------------------------------------- /flow-typed/test_fluent-schemer_v3.x.x.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { array, date, object, number, string, union } from 'fluent-schemer'; 4 | 5 | date() 6 | .after(new Date().setMonth(1)) 7 | .before(new Date()) 8 | .optional(); 9 | 10 | array(date()).minlength(6).distinct().maxlength(33).optional(); 11 | array(); 12 | 13 | number().predicate((x: number) => x % 2 === 1); 14 | 15 | object({ 16 | name: string().pattern(/whatever_who_cares/), 17 | age: number().allowNaN().allowInfinity().integer().optional() // just for type checks 18 | }); 19 | 20 | date().after('1/1/1111', '10/10/1111'); 21 | 22 | // $ExpectError 23 | date().before('1/1/1111', '10/10/1111'); 24 | 25 | // $ExpectError 26 | date().weekdayBetween(5); 27 | 28 | // $ExpectError 29 | number().predicate((id: string) => id); 30 | 31 | // $ExpectError 32 | number().precision('5'); 33 | 34 | // $ExpectError 35 | number().max({}); 36 | 37 | // $ExpectError 38 | number().min(); 39 | 40 | // $ExpectError 41 | string().minlength('10'); 42 | 43 | // $ExpectError 44 | string().pattern('3424'); 45 | 46 | // $ExpectError 47 | union(string(), 'asdf'); 48 | 49 | // $ExpectError 50 | object({ 51 | name: string(), 52 | age: {} 53 | }); 54 | 55 | // $ExpectError 56 | object([string()]); 57 | // $ExpectError 58 | object(string()); 59 | 60 | // $ExpectError 61 | array({ 62 | name: string() 63 | }); 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fluent-schemer", 3 | "version": "2.1.2", 4 | "repository": "https://github.com/KonstantinSimeonov/fluent-schemer", 5 | "description": "Small and intuitive umd validation library that provides an elegant way to express validation logic.", 6 | "main": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "keywords": [ 9 | "validation", 10 | "validate", 11 | "schema", 12 | "fluent", 13 | "declarative", 14 | "es2015", 15 | "schemer" 16 | ], 17 | "scripts": { 18 | "flow": "flow", 19 | "tslint": "tslint", 20 | "lint": "yarn tslint ./index.ts ./lib/**/*.ts", 21 | "test": "nyc ava", 22 | "coverage": "nyc report --reporter=html", 23 | "coveralls": "nyc report --reporter=text-lcov | coveralls", 24 | "build": "webpack --progress --colors --profile", 25 | "watch": "webpack --watch --progress --colors --profile" 26 | }, 27 | "author": "Konstantin Simeonov", 28 | "license": "MIT", 29 | "devDependencies": { 30 | "ava": "^0.25.0", 31 | "coveralls": "^3.0.2", 32 | "flow-bin": "^0.60.1", 33 | "nyc": "^13.0.1", 34 | "ts-loader": "^5.2.1", 35 | "ts-node": "^7.0.1", 36 | "tslint": "^5.11.0", 37 | "typescript": "^3.1.1", 38 | "webpack": "^4.20.2", 39 | "webpack-cli": "^3.1.1" 40 | }, 41 | "dependencies": {}, 42 | "ava": { 43 | "require": [ 44 | "ts-node/register" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | const BUILD_DIR = path.resolve(__dirname, 'dist'); 5 | 6 | const baseConfig = { 7 | mode: 'production', 8 | devtool: 'source-map', 9 | output: { 10 | path: BUILD_DIR, 11 | library: 'fluentSchemer', 12 | libraryTarget: 'umd', 13 | globalObject: `typeof window !== 'undefined' ? window : (typeof self !== 'undefined' ? self : this)` 14 | }, 15 | entry: path.resolve(__dirname, 'index.ts'), 16 | resolve: { 17 | extensions: ['.ts', '.js'] 18 | }, 19 | }; 20 | 21 | const es6Config = { 22 | ...baseConfig, 23 | output: { 24 | ...baseConfig.output, 25 | filename: 'index.js' 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.ts$/, 31 | use: 'ts-loader' 32 | } 33 | ] 34 | }, 35 | plugins: [ 36 | new webpack.WatchIgnorePlugin([BUILD_DIR]) 37 | ] 38 | }; 39 | 40 | const es5MinConfig = { 41 | ...baseConfig, 42 | output: { 43 | ...baseConfig.output, 44 | filename: 'index.es5.min.js', 45 | sourceMapFilename: 'index.es5.min.js.map' 46 | }, 47 | module: { 48 | rules: [ 49 | { 50 | test: /\.ts$/, 51 | loader: 'ts-loader', 52 | options: { 53 | configFile: 'tsconfig.es5.json' 54 | } 55 | } 56 | ] 57 | }, 58 | plugins: [ 59 | new webpack.WatchIgnorePlugin([BUILD_DIR]) 60 | ] 61 | }; 62 | 63 | module.exports = [es5MinConfig, es6Config]; 64 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 05.15.2017 - flow libdefs, `.keys().values()` methods for object, generic typescript goodness 4 | 5 | ## 04.15.2017 - `.required()` api method on BaseSchema is deprecated, using `ava` as test runner. 6 | - _Notes_: 7 | - all tests rewritten with `ava` 8 | - `.required()` has been removed 9 | - all schemas will now be considered `required` by default 10 | - `.optional()` method has been introduces, which will mark the schema as optional. Calling this has the same behaviour as not calling `.required()` previously did. 11 | 12 | ## 25.08.2017 - Throw errors on rubbish arguments, few internal changes 13 | - _Notes_: 14 | - schema methods like `.min`, `.max`, `.minlength`, `.maxlength`, `.before`, `.hourBetween` and other will throw `TypeError`s, when their arguments do not make sense 15 | - i.e. `NaN` is passed where a valid number is required, negative numbers are passed where positive are expected 16 | - removed eslint, introduced basic tslint config 17 | - made tslint pre-commit a git hook 18 | 19 | ## 08.08.2017 - Typescript, public API simplified 20 | - _Notes_: 21 | - entire codebase rewritten in Typescript 22 | - users can no longer plug in their own base schema 23 | - users can no longer select which schemas will be available runtime on the FluentSchemer object 24 | - users no longer need to call FluentSchemer.createInstance().andsoon, all schemas are part of the default exports 25 | - exports of the `error` module are also exported in index.js 26 | - Typescript type definitions are available 27 | - DateSchema is mostly behaving correctly 28 | 29 | ## 31.03.2017 - UMD compliance 30 | - _Notes:_ 31 | - codebase is re-written using ES2015 harmony modules (`import`, `export`) 32 | - the codebase is built into a bundle that complies with UMD 33 | - bundling is now done via webpack instead of gulp 34 | - tests now run on the built codebase 35 | - nothing has been removed from the public API 36 | -------------------------------------------------------------------------------- /docs/schemas/STRING.md: -------------------------------------------------------------------------------- 1 | # `StringSchema` **extends** `BaseSchema` 2 | 3 | `StringSchema` is aliased as `string`. Only primitive string values and `String` objects are considered valid strings. Provides validation for string length and validation by a given regular expression. Any validation errors are returned as an array of error objects. 4 | 5 | | schema-specific methods | explanation | 6 | |:---------------------------------- |:------------------------------------------------------------ | 7 | | validateType(value) | returns `true` if value is primitive string or object string | 8 | | minlength(number) | sets a minimum length to the schema | 9 | | maxlength(number) | sets a maximum length to the schema | 10 | | pattern(Regexp) | sets a regexp to test values against | 11 | 12 | ```js 13 | const { string } = require('fluent-schemer'); 14 | 15 | const testSchema = string() // create a blank StringSchema 16 | .required() // the value must be a string 17 | .minlength(5) // validate wether the length of an input string is at least 5 18 | .maxlength(10) // validate wether the length of an input string is at most 10 19 | .pattern(/^[a-z]+$/i) // validate wether the input string matches a regular expression 20 | .predicate(str => str !== 'javascript') // use a custom function to validate an input string 21 | .not('c#', 'java', 'c++'); // the input value shouldn't be one of the passed values 22 | 23 | const someString = 'testtest42'; 24 | 25 | const { errors, errorsCount } = testSchema.validate(someString); 26 | 27 | console.log(errorsCount); // 1 28 | console.log(errors); 29 | /* [ ValidationError { 30 | type: 'argument', 31 | message: 'Expected testtest42 to match pattern but it did not', 32 | path: '' 33 | } ] 34 | */ 35 | ``` 36 | -------------------------------------------------------------------------------- /lib/schemas/enumeration-schema.ts: -------------------------------------------------------------------------------- 1 | import { createError, ERROR_TYPES } from '../errors'; 2 | import BaseSchema from './base-schema'; 3 | 4 | export const name = 'enumeration'; 5 | 6 | const typeName = 'enumeration'; 7 | 8 | /** 9 | * Provides validation whether a specific value belongs to a set of whitelisted values. 10 | * 11 | * @export 12 | * @class EnumerationSchema 13 | * @extends {BaseSchema} 14 | */ 15 | export default class EnumerationSchema extends BaseSchema { 16 | /** 17 | * Creates an instance of EnumerationSchema by a collection of whitelisted values. 18 | * Whitelisted values will be kept in a set, which would prevent automatic garbage collection. 19 | * @param {...any[]} args Either comma-separated whitelist values or an object, whose values will be whitelisted. 20 | * @memberof EnumerationSchema 21 | * 22 | * @example 23 | * // both declarations below are equivalent: 24 | * 25 | * const triState = enumeration(null, true, false); 26 | * const triStateFromMap = enumeration({ 27 | * unknown: null, 28 | * true: true, 29 | * false: false 30 | * }); 31 | */ 32 | public constructor(...args: any[]) { 33 | super(); 34 | 35 | const isMapEnum = args.length === 1 && typeof args[0] === 'object'; 36 | const allowedValues = new Set(isMapEnum ? Object.values(args[0]) : args); 37 | 38 | this.pushValidationFn((value, path) => 39 | allowedValues.has(value) 40 | ? undefined 41 | : createError( 42 | ERROR_TYPES.ARGUMENT, 43 | `Expected one of ${allowedValues} but got ${value}`, 44 | path, 45 | ), 46 | ); 47 | } 48 | 49 | /** 50 | * Returns 'enumeration'. 51 | * 52 | * @readonly 53 | * @type {string} 54 | * @memberof EnumerationSchema 55 | */ 56 | public get type(): string { 57 | return typeName; 58 | } 59 | 60 | /** 61 | * Always returns true. 62 | * 63 | * @returns {boolean} 64 | * @memberof EnumerationSchema 65 | */ 66 | public validateType(value: any): value is any { 67 | return true; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/tests/bool.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { bool, ERROR_TYPES } from '../../'; 3 | import { shouldReturnErrors, shouldNotReturnErrors } from '../helpers'; 4 | 5 | const ROOT = 'boolvalue'; 6 | 7 | test('bool.type :', assert => assert.is(bool().type, 'bool')); 8 | 9 | test('bool.validateType() returns "true" for true and false', assert => { 10 | const schema = bool(); 11 | 12 | assert.true(schema.validateType(true)); 13 | assert.true(schema.validateType(false)); 14 | }); 15 | 16 | test('bool.validateType() returns false for values of various other types', assert => { 17 | const schema = bool(); 18 | const values = [{}, [], [1], null, undefined, NaN, Infinity, 1, 0, '', 'plamyche', Function, Symbol, 'true', 'false']; 19 | 20 | values.map(v => schema.validateType(v)).forEach(isValid => assert.false(isValid)); 21 | }); 22 | 23 | test('bool.validate() returns errors for values not of strict boolean type', assert => { 24 | const schema = bool(); 25 | const values = [{}, [], [1], null, undefined, NaN, Infinity, 1, 0, '', 'plamyche', Function, Symbol, 'true', 'false']; 26 | 27 | shouldReturnErrors(assert, schema, values, { type: ERROR_TYPES.TYPE }); 28 | }); 29 | 30 | test('bool.validate() does not return errors for values of other types when optional', assert => { 31 | const schema = bool().optional(); 32 | const values = [{}, [], [1], null, undefined, NaN, Infinity, 1, 0, '', 'plamyche', Function, Symbol, 'true', 'false']; 33 | 34 | shouldNotReturnErrors(assert, schema, values); 35 | }); 36 | 37 | test('bool.validate() returns predicate errors when predicate is not satisfied', assert => { 38 | const schema = bool().predicate(x => x === false); 39 | const { errors: [predicateError] } = schema.validate(true, ROOT); 40 | 41 | assert.is(predicateError.type, ERROR_TYPES.PREDICATE); 42 | assert.is(predicateError.path, ROOT); 43 | }); 44 | 45 | test('All methods should enable chaining', assert => { 46 | const schema = bool().optional().not(false).predicate(x => x); 47 | 48 | assert.is(typeof schema.validate, 'function'); 49 | }); 50 | -------------------------------------------------------------------------------- /docs/schemas/ENUMERATION.md: -------------------------------------------------------------------------------- 1 | # `EnumerationSchema` **extends** `BaseSchema` 2 | 3 | `EnumerationSchema` is exported under the default name `enumeration`. Provides validation whether a value is contained in a predefined set of values. Work with everything that is a javascript value. Supports creating enumerations from parameters, from an array or from an object's values. Any validation errors are returned as an array of error objects. 4 | 5 | | schema-specific methods | explanation | 6 | |:-----------------------:|:-----------:| 7 | | - | - | 8 | 9 | - Passing the allowed values as parameters: 10 | 11 | ```js 12 | const schemerInstance = require('./fluent-schemer').createInstance(), 13 | { enumeration } = schemerInstance.schemas; 14 | 15 | const schema = enumeration(1, 2, 4, 8, 16); 16 | 17 | const values = [1, 2, 5, 10, null, undefined, 33]; 18 | 19 | for(const v of values) { 20 | const { errors } = schema.validate(v); 21 | console.log(`Errors for ${v}:`); 22 | console.log(JSON.stringify(v, null, 4)); 23 | console.log('\n\n'); 24 | } 25 | ``` 26 | 27 | - Create an enumeration schema from an array of values: 28 | 29 | ```js 30 | const schemerInstance = require('./fluent-schemer').createInstance(), 31 | { enumeration } = schemerInstance.schemas; 32 | 33 | const namesEnum = ['Penka', 'John', 'Travolta', 'ShiShi'], 34 | schema = enumeration(...namesEnum); 35 | 36 | console.log(schema.validate('Hodor')); 37 | console.log(schema.validate('Travolta')); 38 | ``` 39 | 40 | - Passing the allowed values from the keys of an object: 41 | 42 | ```js 43 | const { enumeration } = require('./fluent-schemer')().schemas; 44 | 45 | const someEnum = { 46 | FLAG1: 1, 47 | FLAG2: 2, 48 | FLAG10: 1024 49 | } 50 | 51 | const schema = enumeration(someEnum); // is the same as calling enumeration(1, 2, 1024) 52 | 53 | const values = [1, 2, 5, 42]; 54 | 55 | for(const v of values) { 56 | const { errors } = schema.validate(v); 57 | console.log(`Errors for ${v}:`); 58 | console.log(JSON.stringify(errors, null, 4)); 59 | console.log('\n\n'); 60 | } 61 | ``` 62 | -------------------------------------------------------------------------------- /test/tests/enumeration.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { enumeration, ERROR_TYPES } from '../../'; 3 | import { shouldReturnErrors, shouldNotReturnErrors } from '../helpers'; 4 | 5 | test('.type returns "enumeration"', assert => { 6 | assert.is(enumeration().type, 'enumeration'); 7 | }); 8 | 9 | test('.validate() returns errors for values that are part of the schema enumeration', assert => { 10 | const someEducationLevels = ['none', 'primary school', 'secondary school', 'bachelor']; 11 | const educationSchema = enumeration(...someEducationLevels); 12 | 13 | shouldNotReturnErrors(assert, educationSchema, someEducationLevels); 14 | }); 15 | 16 | test('.validate() returns errors for values that are not a part of the schema enumeration', assert => { 17 | const enumerationSchema = enumeration(1, 4, 10, 33); 18 | const notLevels = [-5, 11, 15, 78]; 19 | 20 | shouldReturnErrors(assert, enumerationSchema, notLevels, { type: ERROR_TYPES.ARGUMENT, root: 'val' }); 21 | }); 22 | 23 | test('.validate() returns errors for enumerations with values of different types', assert => { 24 | const weirdoEnum = [1, true, 'podlena', null]; 25 | const schema = enumeration(...weirdoEnum); 26 | 27 | shouldNotReturnErrors(assert, schema, weirdoEnum); 28 | }); 29 | 30 | test('.validate() returns errors correctly when object map has been passed to the constructor', assert => { 31 | const errorTypes = { 32 | engine: 'EngineExecutionError', 33 | application: 'ApplicationError', 34 | database: 'DatabaseError' 35 | }; 36 | const schema = enumeration(errorTypes); 37 | 38 | shouldReturnErrors(assert, schema, ['Engine', 'gosho', 1, 2, true, null, {}, [], 'podlqrkova'], { type: ERROR_TYPES.ARGUMENT }); 39 | }); 40 | 41 | test('.validate() does not return errors when object map has been passed to the constructor when values are part of the enumeration', assert => { 42 | const errorTypes = { 43 | engine: 'EngineExecutionError', 44 | application: 'ApplicationError', 45 | database: 'DatabaseError' 46 | }; 47 | const schema = enumeration(errorTypes); 48 | 49 | shouldNotReturnErrors(assert, schema, Object.keys(errorTypes).map(k => errorTypes[k])); 50 | }); 51 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # How does the whole thing work? 2 | 3 | Every schema allows a validation of a concrete type - current supported types are `number`, `string`, `bool`, `array`, `object`, `enumeration` and `union`. 4 | Each schema defines validation rules that can be added to the schema by calling a method. For readable and concise syntax, those methods provide chaining. 5 | Schemas can be used individually or in conjunction - for an example, one could write `number().min(10)` or `array(number().min(10))`. The former will provide a 6 | validate whether a number is larger than or equal to 10, while the latter will validate whether an array contains only numbers larger or equal to than 10. The 7 | validation feedback is returned in the form of a plain old javascript object that contains two keys: 8 | 9 | - **errorsCount** - how many errors have occurred during the validation process 10 | - **errors** - plain javascript object, the keys of which are errors reports 11 | - error reports can be arrays of errors, or javascript object for errors from an object schema 12 | 13 | ```js 14 | { 15 | errorsCount: 4, 16 | errors: { 17 | name: [ 18 | ValidationError { 19 | type: 'type', 20 | message: 'Expected type string but got object', 21 | path: 'name' 22 | } 23 | ], 24 | age: [ 25 | ValidationError { 26 | type: 'argument', 27 | message: 'Expected integer number but got -10.5', 28 | path: 'age' 29 | }, 30 | ValidationError { 31 | type: 'range', 32 | message: 'Expected value greater than or equal to 0 but got -10.5', 33 | path: 'age' 34 | } 35 | ], 36 | skill: { 37 | title: [ 38 | ValidationError { 39 | type: 'type', 40 | message: 'Expected type string but got number', 41 | path: 'skill.title' 42 | } 43 | ] 44 | } 45 | } 46 | } 47 | ``` 48 | 49 | If the feedback array is empty, that means no errors occurred and the passed value is valid. 50 | -------------------------------------------------------------------------------- /docs/schemas/OBJECT.md: -------------------------------------------------------------------------------- 1 | # `ObjectSchema` **extends** `BaseSchema` 2 | 3 | `ObjectSchema` is aliased as `object`. Valid objects satisfy the rule `Object.prototype.toString.call(value) === '[object Object]'` - that way `null`, arrays and functions are not considered valid objects out of the box. Provides recursive validation for objects - the object schema accepts a map of subschemas. When a value is being validated, it will have it's values validated by the schemas at the respective keys in the object schema. Object schemas can be nested as desired. Validation errors are returned in a map - each key hold the errors that occurred for it's value. 4 | 5 | - **NOTES** 6 | - recursive types are not yet supported, but are a planned feature 7 | 8 | | schema-specific methods | explanation | 9 | |:-----------------------:|:--------------------------------:| 10 | | .allowArrays() | arrays are considered objects | 11 | | .allowFunctions() | functions are considered objects | 12 | 13 | ## Validate an object that represents a student's info: 14 | 15 | ```js 16 | import { string, number, bool, object, array } from ('fluent-schemer'); 17 | 18 | const studentSchema = object({ 19 | /** 20 | * the object schema can be used with other schemas 21 | * the object() function accepts an object whose keys have other schemas as values 22 | * the values on the same keys will be validated on the input objects 23 | */ 24 | name: string() 25 | .required() 26 | .pattern(/^[A-Z][a-z]{1,20}$/), 27 | age: number() 28 | .required() 29 | .min(0) 30 | .max(120) 31 | .integer(), 32 | skills: array(string().minlength(3).maxlength(30)) 33 | .required() 34 | .maxlength(20), 35 | cat: object({ 36 | breed: string().required(), 37 | name: string().required() 38 | }) 39 | .required() 40 | }); 41 | 42 | const student = { 43 | name: 'Penka', 44 | age: 133, 45 | skills: ['studying', 'programming', 5], 46 | cat: { 47 | name: 'tom' 48 | } 49 | }; 50 | 51 | /** 52 | * leverage destructuring statements in order 53 | * to easily extract errors from the error report 54 | */ 55 | const { errorsCount, errors } = studentSchema.validate(student); 56 | const { 57 | age, 58 | skills, 59 | cat: { breed } 60 | } = errors; 61 | 62 | console.log(age); 63 | console.log(skills); 64 | console.log(breed); 65 | ``` 66 | -------------------------------------------------------------------------------- /test/tests/union.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { union, string, number, bool, array, ERROR_TYPES } from '../../'; 3 | import { shouldReturnErrors, shouldNotReturnErrors } from '../helpers'; 4 | 5 | test('UnionSchema.type: should return concatenation of all possible types', assert => { 6 | const schema = union(number(), string(), bool()); 7 | 8 | assert.is(schema.type, [number().type, string().type, bool().type].join('|')); 9 | }); 10 | 11 | test('UnionSchema.validateType(): should return false for values that are not of one of the listed types', assert => { 12 | const schema = union(string(), number()); 13 | const values = [null, undefined, true, {}, [], () => 1]; 14 | 15 | for (const v of values) { 16 | assert.false(schema.validateType(v)); 17 | } 18 | }); 19 | 20 | test('UnionSchema.validateType(): should return true for values that are of at least one of the listed types', assert => { 21 | const schema = union(number(), bool()); 22 | const values = [1, true, false, 0, new Number(10), new Boolean(true)]; 23 | 24 | const allAreTrue = values.every(v => schema.validateType(v)); 25 | 26 | assert.true(allAreTrue); 27 | }); 28 | 29 | test('.validate() does not return errors when no type of the union matches when .optional() has been called', assert => { 30 | const schema = union(number(), bool()).optional(); 31 | const notNumbersOrBools = [NaN, Infinity, {}, [], () => true, null, undefined]; 32 | 33 | shouldNotReturnErrors(assert, schema, notNumbersOrBools); 34 | }); 35 | 36 | test('.validate() returns errors when .optional() has NOT been called', assert => { 37 | const schema = union(number(), bool()); 38 | const notNumbersOrBools = [NaN, Infinity, {}, [], () => true, null, undefined]; 39 | 40 | shouldReturnErrors(assert, schema, notNumbersOrBools, { type: ERROR_TYPES.TYPE }); 41 | }); 42 | 43 | test('.validate() does not return errors when at least one of the schema types match the value', assert => { 44 | const schema = union(string(), array(number().integer())); 45 | const values = ['zdrkp', new String('jesuisstring'), [], [1, 2], [new Number(0), 3], new Array()]; 46 | 47 | shouldNotReturnErrors(assert, schema, values); 48 | }); 49 | 50 | test('.validate() returns errors when the values satisfies the conditions of no schema', assert => { 51 | const schema = union(string().minlength(5), number().integer()); 52 | // expected errors are of type range error for too short strings and argument errors for floating point numbers 53 | const shortStrings = ['js', '', new String('1')]; 54 | const floats = [1.5, -0.5, new Number(2.5)]; 55 | 56 | shortStrings.concat(floats) 57 | .map(v => schema.validate(v)) 58 | .forEach(({ errors, errorsCount }) => { 59 | assert.is(errorsCount, 2); 60 | 61 | assert.not( 62 | errors.find(e => e.type === ERROR_TYPES.TYPE), undefined 63 | ); 64 | 65 | assert.not( 66 | errors.find(e => [ERROR_TYPES.ARGUMENT, ERROR_TYPES.RANGE].includes(e.type)), undefined 67 | ); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fluent-schemer 2 | 3 | Coverage Status Coverage Status 4 | 5 | ```js 6 | const librarySchema = object({ 7 | dependenciesCount: number().min(0).max(10).integer().optional(), 8 | name: string().minlength(2).maxlength(10), 9 | testCoverage: number().min(0).max(100).optional(), 10 | lastCommitDate: date().after(new Date(new Date().setMonth(new Date().getMonth() - 1))), 11 | contributors: array( 12 | object({ 13 | username: string().minlength(5), 14 | email: string().pattern(/\S+@\S+\.\S+/) 15 | }) 16 | ), 17 | issues: array(string()), 18 | activelyMaintained: bool(), 19 | license: enumeration('MIT', 'BSD', 'GPL') 20 | }); 21 | 22 | const { errorCounts, errors } = librarySchema.validate(someLibraryRecord); 23 | ``` 24 | 25 | ## Incoming: 26 | - **default values**, something like 27 | 28 | ```ts 29 | const { corrected: pageSize } = number().min(10).max(100).integer().default(10).validate(-5); 30 | console.log(corrected); // 10 31 | 32 | const { corrected: username } = string() 33 | .minlength(2) 34 | .maxlength(10) 35 | .defaultExpression(value => ('_________' + value).slice(0, 10)) 36 | .validate('1'); 37 | console.log(username); // _________1 38 | ``` 39 | 40 | Aims to provide declarative, expressive and elegant approach to validation, while providing an intuitive, easy-to-use api. 41 | 42 | ## It's cool, because 43 | - it **fully embraces ES2015** features such as classes, fat arrow functions, mixins, destructuring statements, modules 44 | - has **typescript type definitions** - v2.0 comes with typings for enhanced development experience 45 | - has **flow libdefs**, which will soon be available on flow-typed 46 | - **easy to use and pick up**, write a little code for a lot of common validation logic 47 | - has a **fluent, readable and declarative** api 48 | - **umd compliant** - use in node/browser, with commonjs, umd, script tags, harmony modules, whatever 49 | - **no production dependencies**, small codebase 50 | - helps developers **get rid of imperative code, long if-else's** and writing boring validations all over again 51 | - **promotes code reuse** - easily share code between modules, between clients, servers and across projects 52 | - easy to extends with custom schemas 53 | - **statically type checked** with latest typescript, checked for correctness with a bunch of **unit tests** 54 | - **throws errors when rubbish arguments are provided** to schema methods, instead of failing silently 55 | 56 | ### Running the tests 57 | 58 | ``` 59 | yarn build && yarn lint && yarn test 60 | ``` 61 | 62 | ### Examples 63 | 64 | Examples can be found in the [docs](./docs), in the source code and in the tests. 65 | 66 | ## [Documentation](./docs/QUICKSTART.md) 67 | -------------------------------------------------------------------------------- /lib/schemas/union-schema.ts: -------------------------------------------------------------------------------- 1 | import { IValidationError } from '../contracts'; 2 | import BaseSchema from './base-schema'; 3 | 4 | export const name = 'union'; 5 | 6 | /** 7 | * Provides validation for union types. 8 | * 9 | * @export 10 | * @class UnionSchema 11 | * @extends {BaseSchema} 12 | */ 13 | export default class UnionSchema extends BaseSchema { 14 | private _state: { 15 | subschemas: Array>; 16 | typestring?: string; 17 | }; 18 | 19 | /** 20 | * Creates an instance of UnionSchema. 21 | * @param {...BaseSchema[]} subschemas Schemas that will be used to validate for the individual types of the union. 22 | * @memberof UnionSchema 23 | * 24 | * @example 25 | * // values must be either numbers or numerical strings 26 | * const numerical = union(number(), string().pattern(/^\d+$/)); 27 | * 28 | * const canDoMathsWith = union(bool(), number(), string().pattern(/^\d+$/)); 29 | * canDoMathsWith.validate(false); // fine 30 | * canDoMathsWith.validate(5); // fine 31 | * canDoMathsWith.validate('5'); // fine 32 | * canDoMathsWith.validate('42asd'); // error 33 | */ 34 | public constructor(...subschemas: Array>) { 35 | super(); 36 | 37 | this._state = { subschemas }; 38 | } 39 | 40 | /** 41 | * Returns 'union' 42 | * 43 | * @readonly 44 | * @memberof UnionSchema 45 | */ 46 | public get type() { 47 | return this._state.typestring 48 | || (this._state.typestring = this._state.subschemas.map(schema => schema.type).join('|')); 49 | } 50 | 51 | /** 52 | * Validates whether the passed value is of the union type defined by the current subschemas. 53 | * If the value passes the type checks for ANY of the subschemas, it is considered a part of the union. 54 | * 55 | * @param {*} value The value that will be type checked. 56 | * @returns {this} 57 | * @memberof UnionSchema 58 | * 59 | * @example 60 | * const canDoMathsWith = union(bool(), number(), string()); 61 | * canDoMathsWith.validate(false); // fine 62 | * canDoMathsWith.validate(5); // fine 63 | * canDoMathsWith.validate('5'); // fine 64 | * canDoMathsWith.validate({}); // error 65 | */ 66 | public validateType(value: any): value is any { 67 | return this._state.subschemas.findIndex(schema => schema.validateType(value)) !== -1; 68 | } 69 | 70 | protected validateValueWithCorrectType(value: any, path?: string) { 71 | const errors: IValidationError[] = []; 72 | 73 | for (const schema of this._state.subschemas) { 74 | const { errors: schemaErrors, errorsCount } = schema.validate(value, path); 75 | 76 | if (!errorsCount) { 77 | return { errors: [], errorsCount: 0, corrected: value }; 78 | } 79 | 80 | if (Array.isArray(schemaErrors)) { 81 | errors.push(...schemaErrors); 82 | } else { 83 | errors.push(schemaErrors); 84 | } 85 | } 86 | 87 | return { 88 | corrected: errors.length ? this._defaultExpr(value) : value, 89 | errors, 90 | errorsCount: errors.length, 91 | }; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /docs/schemas/NUMBER.md: -------------------------------------------------------------------------------- 1 | # `NumberSchema` **extends** `BaseSchema` 2 | 3 | `NumberSchema` is aliased under the name `number`. Primitive number values or `Number` objects are the only values that are considered valid numbers. `NaN` and `Infinity` are considered invalid by default. This can be overwritten. Provides validations for ranges(min, max), whether a number is an integer. Also provides validations for safe integers. Any validation errors are returned as an array of error objects. 4 | 5 | | schema-specific methods | explanation | 6 | |:----------------------------------------- |:----------------------------------------------------------- | 7 | | validateType(value) | `true` for primitive/boxed numbers that are not `NaN` or `Infinity`.
Configurable using .allowNaN() and .allowInfinity() | 8 | | min(number) | set a minimal possible value for the schema | 9 | | max(number) | set a maximal possible value for the schema | 10 | | integer() | value should be an integer | 11 | | precision(number) | set a maximal difference value which is used to compare floating point numbers | 12 | | safeInteger() | value must be a between -(253 - 1) inclusive to 253 - 1 | 13 | | allowNaN() | `NaN` will be considered a valid number in .validateType() | 14 | | allowInfinity() | `Infinity` will be considered a valid number in .validateType() | 15 | 16 | ## Validate a number value that should represent a person's age: 17 | 18 | ```js 19 | const { number } = require('fluent-schemer'); 20 | 21 | const ageSchema = number() // blank number schema 22 | .required() // the input value must be a number, excluding NaN and Infinity 23 | .min(0) // the input value must be at least 0 24 | .max(100) // the input value must be at most 100 25 | .integer(); // the input value must be an integer 26 | 27 | const ages = [1, 20, 200, -5, 3.4, NaN]; 28 | 29 | for(let a of ages) { 30 | const { errors } = ageSchema.validate(a); 31 | console.log(errors); 32 | } 33 | ``` 34 | 35 | - Output: 36 | 37 | ```js 38 | // 1 39 | [] 40 | // 20 41 | [] 42 | // 200 is too large 43 | [ValidationError { 44 | type: 'range', 45 | message: 'Expected value less than or equal to 100 but got 200', 46 | path: '' 47 | } ] 48 | // -5 is too small 49 | [ValidationError { 50 | type: 'range', 51 | message: 'Expected value greater than or equal to 0 but got -5', 52 | path: '' 53 | } ] 54 | // 3.4 is not an integer 55 | [ValidationError { 56 | type: 'argument', 57 | message: 'Expected integer number but got 3.4', 58 | path: '' 59 | } ] 60 | // NaN is not valid number unless .allowNaN() has been called 61 | [ValidationError { 62 | type: 'type', 63 | message: 'Expected type number but got number', 64 | path: '' 65 | }] 66 | ``` 67 | 68 | ## Using `.allowNaN()` and `.allowInfinity()`: 69 | 70 | ```js 71 | import { number } from 'fluent-schemer'; 72 | 73 | const num = number().required().allowNaN().allowInfinity(); 74 | 75 | console.log(num.validate(5).errors); 76 | console.log(num.validate('5').errors); 77 | console.log(num.validate(NaN).errors); 78 | console.log(num.validate(Infinity).errors); 79 | ``` 80 | 81 | - Output: 82 | 83 | ```js 84 | [] 85 | [ ValidationError { 86 | type: 'type', 87 | message: 'Expected type number but got string', 88 | path: '' 89 | } ] 90 | [] 91 | [] 92 | ``` 93 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import ArraySchema from './lib/schemas/array-schema'; 2 | import BaseSchema from './lib/schemas/base-schema'; 3 | import BoolSchema from './lib/schemas/bool-schema'; 4 | import DateSchema from './lib/schemas/date-schema'; 5 | import EnumerationSchema from './lib/schemas/enumeration-schema'; 6 | import NumberSchema from './lib/schemas/number-schema'; 7 | import ObjectSchema from './lib/schemas/object-schema'; 8 | import StringSchema from './lib/schemas/string-schema'; 9 | import UnionSchema from './lib/schemas/union-schema'; 10 | 11 | /** 12 | * Creates an instance of StringSchema. 13 | * @returns {StringSchema} 14 | * 15 | * @example 16 | * string() 17 | * .minlength(2) 18 | * .maxlength(20) 19 | * .pattern(/\.js$/) 20 | * .required(); 21 | */ 22 | export const string = () => new StringSchema; 23 | 24 | /** 25 | * Creates an instance of NumberSchema. 26 | * @returns {NumberSchema} 27 | * 28 | * @example 29 | * number() 30 | * .min(-5) 31 | * .max(40) 32 | * .integer(); 33 | * 34 | * number() 35 | * .integer() 36 | * .safeInteger() 37 | * .allowNaN() 38 | * .required(); 39 | * 40 | * number() 41 | * .precision(0.0001) 42 | * .not(-1, 0, 1); 43 | */ 44 | export const number = () => new NumberSchema; 45 | 46 | /** 47 | * Creates an instance of BoolSchema. 48 | * @returns {BoolSchema} 49 | * 50 | * @example 51 | * bool(); 52 | */ 53 | export const bool = () => new BoolSchema; 54 | 55 | /** 56 | * Creates an instance of DateSchema. 57 | * @returns {DateSchema} 58 | * 59 | * @example 60 | * date() 61 | * .before('5/1/2005') 62 | * .after('1/1/1999') 63 | * .required(); 64 | * .dateBetween(0, 15) 65 | * .monthBetween(2, 6) 66 | * .weekdayBetween(5, 3) // outside of [3, 5] 67 | * .hourBetween(12, 2) // outside if [2, 12] 68 | * .minuteBetween(20, 40) 69 | * .secondBetween(0, 10); 70 | */ 71 | export const date = () => new DateSchema; 72 | 73 | /** 74 | * Creates an instance of ArraySchema. 75 | * @param {BaseSchema} [subschema] - Specify a schema which is used to validate the elements of an array. 76 | * 77 | * @example 78 | * // array of positive numbers 79 | * array(number().min(0)) 80 | * 81 | * @example 82 | * // untyped array 83 | * array().minlength(5) 84 | */ 85 | export const array = (subschema?: BaseSchema) => new ArraySchema(subschema); 86 | 87 | /** 88 | * Creates an instance of EnumerationSchema by a collection of whitelisted values. 89 | * Whitelisted values will be kept in a set, which would prevent automatic garbage collection. 90 | * @param {...any[]} args Either comma-separated whitelist values or an object, whose values will be whitelisted. 91 | * 92 | * @example 93 | * // both declarations below are equivalent: 94 | * 95 | * const triState = enumeration(null, true, false); 96 | * const triStateFromMap = enumeration({ 97 | * unknown: null, 98 | * true: true, 99 | * false: false 100 | * }); 101 | */ 102 | export const enumeration = (...values: any[]) => new EnumerationSchema(...values); 103 | 104 | /** 105 | * Creates an instance of ObjectSchema. 106 | * Accepts an object, whose keys are schemas themselves. 107 | * The schemas on those keys will be used to validate values on the same 108 | * keys in validated values. 109 | * @param {{ [id: string]: BaseSchema }} subschema Object schema whose keys have schemas as their values. 110 | * 111 | * @example 112 | * object({ 113 | * name: string().minlength(3).required(), 114 | * age: number().min(0).integer().required() 115 | * }).required(); 116 | */ 117 | export const object = (subschema: { [id: string]: BaseSchema }) => new ObjectSchema(subschema); 118 | 119 | /** 120 | * Creates an instance of UnionSchema. 121 | * @param {...BaseSchema[]} subschemas Schemas that will be used to validate for the individual types of the union. 122 | * 123 | * @example 124 | * // values must be either numbers or numerical strings 125 | * const numerical = union(number(), string().pattern(/^\d+$/)); 126 | * 127 | * const canDoMathsWith = union(bool(), number(), string().pattern(/^\d+$/)); 128 | * canDoMathsWith.validate(false); // fine 129 | * canDoMathsWith.validate(5); // fine 130 | * canDoMathsWith.validate('5'); // fine 131 | * canDoMathsWith.validate('42asd'); // error 132 | */ 133 | export const union = (...subschemas: Array>) => new UnionSchema(...subschemas); 134 | 135 | export * from './lib/errors'; 136 | -------------------------------------------------------------------------------- /flow-typed/flow_v0.60.x-/fluent-schemer_v3.x.x.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module 'fluent-schemer' { 4 | declare type ObjectMap = { 5 | [id: string]: TValue; 6 | }; 7 | 8 | declare export var ERROR_TYPES: { 9 | ARGUMENT: 'argument', 10 | PREDICATE: 'predicate', 11 | RANGE: 'range', 12 | TYPE: 'type', 13 | }; 14 | 15 | declare export type IValidationError = { 16 | type: string; 17 | message: string; 18 | path: string; 19 | }; 20 | 21 | declare export type ICompositeValidationError = IValidationError & { errors: IValidationError }; 22 | 23 | declare export type IErrorFeedback = { 24 | errors: IValidationError[]; 25 | errorsCount: number; 26 | }; 27 | 28 | declare export class BaseSchema { 29 | validationFunctions(value: TValidated, path: string): IValidationError[]; 30 | validateType(value: any): boolean; 31 | optional(): this; 32 | not(...values: any[]): this; 33 | validate(value: any, path?: string, currentErrors?: IValidationError[]): IErrorFeedback; 34 | areEqual(firstValue: any, secondValue: any): boolean; 35 | validateValueWithCorrectType(value: any, path: string, currentErrors?: IValidationError[]): IErrorFeedback; 36 | } 37 | 38 | declare export class BoolSchema extends BaseSchema { 39 | type: 'bool'; 40 | } 41 | 42 | declare export class ArraySchema extends BaseSchema { 43 | type: string; 44 | minlength(length: number): this; 45 | maxlength(length: number): this; 46 | withLength(length: number): this; 47 | distinct(): this; 48 | predicate(predicateFn: (value: TInner[]) => boolean): this; 49 | } 50 | 51 | declare export class DateSchema extends BaseSchema { 52 | type: 'date'; 53 | predicate(predicateFn: (value: Date) => boolean): this; 54 | before(dateString: string): this; 55 | before(date: Date): this; 56 | before(year: number, month?: number, day?: number, hours?: number, minutes?: number, seconds?: number, milliseconds?: number): this; 57 | after(dateString: string): this; 58 | after(date: Date): this; 59 | after(year: number, month?: number, day?: number, hours?: number, minutes?: number, seconds?: number, milliseconds?: number): this; 60 | predicate(predicateFn: (value: Date) => boolean): this; 61 | dateBetween(start: number, end: number): this; 62 | monthBetween(start: number, end: number): this; 63 | hourBetween(start: number, end: number): this; 64 | weekdayBetween(start: number, end: number): this; 65 | minutesBetween(start: number, end: number): this; 66 | secondsBetween(start: number, end: number): this; 67 | } 68 | 69 | declare export class EnumerationSchema extends BaseSchema { 70 | type: 'enumeration'; 71 | validateType(value: any): boolean; 72 | predicate(predicateFn: (value: any) => boolean): this; 73 | } 74 | 75 | declare export class NumberSchema extends BaseSchema { 76 | type: 'number'; 77 | precision(allowedDiff: number): this; 78 | allowNaN(): this; 79 | allowInfinity(): this; 80 | integer(): this; 81 | min(value: number): this; 82 | max(value: number): this; 83 | predicate(predicateFn: (value: number) => boolean): this; 84 | } 85 | 86 | declare export class ObjectSchema extends BaseSchema { 87 | type: 'object'; 88 | allowArrays(): this; 89 | allowFunctions(): this; 90 | keys(keysSchema: StringSchema): this; 91 | values(valuesSchema: BaseSchema): this; 92 | predicate(predicateFn: (value: Object) => boolean): this; 93 | } 94 | 95 | declare export class StringSchema extends BaseSchema { 96 | type: 'string'; 97 | minlength(length: number): this; 98 | maxlength(length: number): this; 99 | pattern(regexp: RegExp): this; 100 | predicate(predicateFn: (value: string) => boolean): this; 101 | } 102 | 103 | declare export class UnionSchema extends BaseSchema { 104 | type: string; 105 | predicate(predicateFn: (value: boolean) => boolean): this; 106 | } 107 | 108 | declare export function enumeration(...args: any[]): EnumerationSchema; 109 | declare export function enumeration(enumMap: Object): EnumerationSchema; 110 | declare export function union(...subschemas: BaseSchema[]): UnionSchema; 111 | declare export function string(): StringSchema; 112 | declare export function number(): NumberSchema; 113 | declare export function object(subschemasMap: ObjectMap>): ObjectSchema; 114 | declare export function bool(): BoolSchema; 115 | declare export function array(subschema?: BaseSchema): ArraySchema; 116 | declare export function date(): DateSchema; 117 | } 118 | -------------------------------------------------------------------------------- /lib/schemas/string-schema.ts: -------------------------------------------------------------------------------- 1 | import { createError, ERROR_TYPES } from '../errors'; 2 | import * as is from '../is'; 3 | import BaseSchema from './base-schema'; 4 | 5 | export const name = 'string'; 6 | 7 | const typeName = 'string'; 8 | 9 | type TStringSchemaState = { 10 | minlength?: number; 11 | maxlength?: number; 12 | pattern?: RegExp 13 | }; 14 | 15 | /** 16 | * Provides type checking for strings and validations for min/max string length and regexp matching. 17 | * 18 | * @export 19 | * @class StringSchema 20 | * @extends {BaseSchema} 21 | */ 22 | export default class StringSchema extends BaseSchema { 23 | private _state: TStringSchemaState; 24 | 25 | /** 26 | * Creates an instance of StringSchema. 27 | * @memberof StringSchema 28 | */ 29 | constructor() { 30 | super(); 31 | this._state = {}; 32 | } 33 | 34 | /** 35 | * Returns 'string' 36 | * @readonly 37 | * @memberof StringSchema 38 | */ 39 | public get type() { 40 | return typeName; 41 | } 42 | 43 | /** 44 | * Every primitive string and String object is considered of type string. 45 | * 46 | * @param {*} value The value that will be type checked. 47 | * @returns {boolean} 48 | * @memberof StringSchema 49 | */ 50 | public validateType(value: any): value is string { 51 | return is.String(value); 52 | } 53 | 54 | /** 55 | * Specify a minimal string length. 56 | * 57 | * @throws {TypeError} If the provided value is not numerical or is NaN, negative of Infinite. 58 | * @param {number} length The minimal allowed length. 59 | * @returns {this} 60 | * @memberof StringSchema 61 | */ 62 | public minlength(length: number) { 63 | if (!is.ValidLength(length)) { 64 | throw new TypeError(`Expected finite positive number as minimal string length, but got ${length}`); 65 | } 66 | 67 | if (!is.Undefined(this._state.minlength)) { 68 | throw new Error('Cannot set minlength twice for a number schema instance'); 69 | } 70 | 71 | this._state.minlength = length; 72 | 73 | return this.pushValidationFn((value: string, path: string) => { 74 | if (!is.Undefined(this._state.minlength) && this._state.minlength > value.length) { 75 | return createError( 76 | ERROR_TYPES.RANGE, 77 | `Expected string with length at least ${this._state.minlength} but got ${value.length}`, 78 | path, 79 | ); 80 | } 81 | }); 82 | } 83 | 84 | /** 85 | * Specify a maximal string length. 86 | * 87 | * @throws {TypeError} If the provided value is not numerical or is NaN, negative of Infinite. 88 | * @param {number} length The maximal allowed length. 89 | * @returns {this} 90 | * @memberof StringSchema 91 | */ 92 | public maxlength(length: number) { 93 | if (!is.ValidLength(length)) { 94 | throw new TypeError(`Expected finite positive number as minimal string length, but got ${length}`); 95 | } 96 | 97 | if (!is.Undefined(this._state.maxlength)) { 98 | throw new Error('Cannot set maxlength twice for a number schema instance'); 99 | } 100 | 101 | this._state.maxlength = length; 102 | 103 | return this.pushValidationFn((value: string, path: string) => { 104 | if (!is.Undefined(this._state.maxlength) && this._state.maxlength < value.length) { 105 | return createError( 106 | ERROR_TYPES.RANGE, 107 | `Expected string with length at most ${this._state.minlength} but got ${value.length}`, 108 | path, 109 | ); 110 | } 111 | }); 112 | } 113 | 114 | /** 115 | * Specify a javascript regular expression to match against values. 116 | * If regexp.test(value) succeeds, it is assumed that the value matches the pattern. 117 | * String literals cannot be passed as patterns. 118 | * 119 | * @throws {TypeError} If the provided value is not of type regular expressions. 120 | * @param {RegExp} regexp The regular expression that will be used to test the values. 121 | * @returns {this} 122 | * @memberof StringSchema 123 | */ 124 | public pattern(regexp: RegExp) { 125 | if (!is.RegExp(regexp)) { 126 | throw new TypeError(`Expected regular expression as pattern, but got value of type ${typeof regexp}`); 127 | } 128 | 129 | if (!is.Undefined(this._state.pattern)) { 130 | throw new Error('Cannot set maxlength twice for a number schema instance'); 131 | } 132 | 133 | this._state.pattern = regexp; 134 | 135 | return this.pushValidationFn((value: string, path: string) => { 136 | if (!is.Undefined(this._state.pattern) && !this._state.pattern.test(value)) { 137 | return createError( 138 | ERROR_TYPES.ARGUMENT, 139 | `Expected ${value} to match pattern but it did not`, 140 | path, 141 | ); 142 | } 143 | }); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /docs/schemas/ARRAY.md: -------------------------------------------------------------------------------- 1 | # `ArraySchema` **extends** `BaseSchema` 2 | 3 | `ArraySchema` is exported under the default name `array`. It is used to validate native array values. 4 | Array-like objects are currently not considered valid arrays. Validation for generic arrays is also available. 5 | Below are examples of validation schemas for array, array> and array. Any validation errors are returned as an array of error objects. 6 | 7 | | schema-specific methods | explanation | 8 | |:--------------------------------------- |:--------------------------------------------- | 9 | | minlength(number) | set a minimum array length | 10 | | maxlength(number) | set a maximum array length | 11 | | withLength(number) | array should have an exact length | 12 | | district(qualify?) | values in the array should be unique with respect to the qualify function | 13 | 14 | ## Require an array of user records, distinct by username: 15 | ```ts 16 | const users = array( 17 | object({ username: string() }) 18 | ).distinct(record => record.username); 19 | 20 | users.validate([{ username: 'pesho', age: 3 }, { username: 'gosho', isCool: true }); // ok 21 | users.validate([ 22 | { username: 'pesho', age: 3 }, 23 | { username: 'gosho', isCool: true }, 24 | { username: 'pesho', salary: 666 } 25 | ]); // returns error because values returned by qualifier are not unique 26 | ``` 27 | 28 | ## Validate an array of names: 29 | 30 | ```js 31 | const { string, array } = require('fluent-schemer'); 32 | 33 | // input array should contain strictly strings that match the regular expression 34 | const nameArraySchema = array(string().pattern(/^[a-z]{2,20}$/i)).required(); 35 | const names = ['george', 'ivan', 'todor', '', 'tom1', null, 10]; 36 | 37 | const errors = nameArraySchema.validate(names); 38 | 39 | if(errors.errorsCount > 0) { 40 | console.log('Errors occurred:'); 41 | console.log(errors); 42 | } 43 | ``` 44 | 45 | - This will output the following error object: 46 | ```js 47 | { 48 | errorsCount: 1, 49 | errors: [ 50 | { 51 | type: 'type', 52 | message: 'Expected type array but got object', 53 | path: '' 54 | } 55 | ] 56 | } 57 | ``` 58 | 59 | ## Integer matrix validation: 60 | 61 | ```js 62 | const matrixSchema = array( 63 | array(number().integer()) 64 | ).required(); 65 | 66 | const matrix = [ 67 | [1, 2, 3], 68 | [4, 5, 6], 69 | [7, 8, 9] 70 | ]; 71 | const matrix2 = null; 72 | const matrix3 = [ 73 | [1, 2, 3], 74 | ['not number'], 75 | [3, 7, 8] 76 | ]; 77 | 78 | console.log(matrixSchema.validate(matrix)); 79 | console.log(matrixSchema.validate(matrix2)); 80 | console.log(matrixSchema.validate(matrix3)); 81 | ``` 82 | 83 | - This will output: 84 | ```js 85 | // matrix 1 86 | { errors: [], errorsCount: 0 } 87 | 88 | // matrix 2 89 | { 90 | errorsCount: 1, 91 | errors: [ ValidationError { 92 | type: 'type', 93 | message: 'Expected type array> but got object', 94 | path: '' 95 | } ] 96 | } 97 | 98 | // matrix 3 99 | { 100 | errorsCount: 1, 101 | errors: [ ValidationError { 102 | type: 'type', 103 | message: 'Expected type array> but got object', 104 | path: '' 105 | } ] 106 | } 107 | ``` 108 | 109 | ## Untyped arrays: 110 | 111 | - The types of the values in the array are not validated. 112 | - This could be useful for an arrays of mixed values, or when their types doesn't matter. 113 | 114 | ```js 115 | const { array } = require('fluent-schemer'); 116 | const untypedArraySchema = array().minlength(2).maxlength(10).required(); 117 | 118 | console.log(untypedArraySchema.validate([1, null, 10])); 119 | console.log(untypedArraySchema.validate(['hello'])); 120 | ``` 121 | 122 | - Output: 123 | ```js 124 | // the first array has a valid length 125 | { errors: [], errorsCount: 0 } 126 | // but the second does not 127 | { errors: 128 | errorsCount: 1, 129 | [ ValidationError { 130 | type: 'range', 131 | message: 'Expected an array with length at least 2 but got length 1', 132 | path: '' 133 | } ] 134 | } 135 | ``` 136 | 137 | ## Arrays of distinct elements: 138 | 139 | ```js 140 | const { array, number } = require('fluent-schemer'); 141 | const arrayOfUniqInts = array(number().integer()).distinct().required(); 142 | 143 | console.log(arrayOfUniqInts.validate([1, 2, 3, 44, 99])); 144 | console.log(arrayOfUniqInts.validate([1, 2, 3, 3, 44, 99])); 145 | ``` 146 | 147 | - Output: 148 | 149 | ```js 150 | { errors: [], errorsCount: 0 } 151 | { 152 | errorsCount: 1, 153 | errors: [ ValidationError { 154 | type: 'argument', 155 | message: 'Expected values in 1,2,3,3,44,99 to be distinct', 156 | path: '' 157 | } ] 158 | } 159 | ``` 160 | -------------------------------------------------------------------------------- /lib/schemas/array-schema.ts: -------------------------------------------------------------------------------- 1 | import { IErrorFeedback } from '../contracts'; 2 | import { createError, ERROR_TYPES } from '../errors'; 3 | import * as is from '../is'; 4 | import BaseSchema from './base-schema'; 5 | 6 | export const name = 'array'; 7 | 8 | /** 9 | * Provides array validations for min/max array length, distinct elements. 10 | * Elements can also be validated with a subschema. 11 | * @export 12 | * @class ArraySchema 13 | * @extends {BaseSchema} 14 | */ 15 | export default class ArraySchema extends BaseSchema { 16 | private _state: { 17 | typestring?: string; 18 | subschema?: BaseSchema; 19 | minlength: number; 20 | maxlength: number; 21 | hasMinLength?: boolean; 22 | hasMaxLength?: boolean; 23 | }; 24 | 25 | /** 26 | * Creates an instance of ArraySchema. 27 | * @param {BaseSchema} [subschema] - Specify a schema which is used to validate the elements of an array. 28 | * @memberof ArraySchema 29 | * 30 | * @example 31 | * // array of positive numbers 32 | * array(number().min(0)) 33 | * 34 | * @example 35 | * // untyped array 36 | * array().minlength(5) 37 | */ 38 | public constructor(subschema?: BaseSchema) { 39 | super(); 40 | this._state = { subschema, minlength: 0, maxlength: Infinity }; 41 | } 42 | 43 | public get type() { 44 | if (!this._state.typestring) { 45 | this._state.typestring = this._state.subschema ? `array<${this._state.subschema.type}>` : `array`; 46 | } 47 | 48 | return this._state.typestring; 49 | } 50 | 51 | public validateType(value: any): value is TInner[] { 52 | return is.Array(value) 53 | && (is.NullOrUndefined(this._state.subschema) || value.every((x: any) => this.validateElementsType(x))); 54 | } 55 | 56 | public minlength(length: number) { 57 | if (!is.ValidLength(length)) { 58 | throw new TypeError(`Expected a finite number larger than 0 as an array length but got ${length}`); 59 | } 60 | 61 | this._state.minlength = length; 62 | 63 | if (this._state.hasMinLength) { 64 | return this; 65 | } 66 | 67 | this._state.hasMinLength = true; 68 | 69 | this.validationFunctions.push((value: TInner[], path: string) => { 70 | if (this._state.minlength > value.length) { 71 | return createError( 72 | ERROR_TYPES.RANGE, 73 | `Expected an ${this.type} with length at least ${this._state.minlength} but got length ${value.length}`, 74 | path, 75 | ); 76 | } 77 | }); 78 | 79 | return this; 80 | } 81 | 82 | public maxlength(length: number) { 83 | if (!is.ValidLength(length)) { 84 | throw new TypeError(`Expected a finite number larger than 0 as an array length but got ${length}`); 85 | } 86 | 87 | this._state.maxlength = length; 88 | if (this._state.hasMaxLength) { 89 | return this; 90 | } 91 | 92 | this._state.hasMaxLength = true; 93 | 94 | this.validationFunctions.push((value: TInner[], path: string) => { 95 | if (this._state.maxlength < value.length) { 96 | return createError( 97 | ERROR_TYPES.RANGE, 98 | `Expected an ${this.type} with length at most ${this._state.minlength} but got length ${value.length}`, 99 | path, 100 | ); 101 | } 102 | }); 103 | 104 | return this; 105 | } 106 | 107 | public withLength(length: number) { 108 | return this.minlength(length).maxlength(length); 109 | } 110 | 111 | public distinct(qualify?: (inner: TInner) => Q) { 112 | return this.pushValidationFn((value: TInner[], path: string) => { 113 | const { size } = new Set(qualify ? value.map(qualify) : value as any); 114 | if (value.length !== size) { 115 | return createError(ERROR_TYPES.ARGUMENT, `Expected values in ${value} to be distinct`, path); 116 | } 117 | }); 118 | } 119 | 120 | protected validateValueWithCorrectType(value: TInner[], path: string): IErrorFeedback { 121 | const { errors } = super.validateValueWithCorrectType(value, path); 122 | const feedback = () => ({ 123 | corrected: errors.length ? this._defaultExpr(value) : value, 124 | errors, 125 | errorsCount: errors.length, 126 | }); 127 | const { subschema } = this._state; 128 | 129 | if (!subschema || errors.length) { 130 | return feedback(); 131 | } 132 | 133 | for (let i = 0, len = value.length; i < len; i += 1) { 134 | const { errors: subErrors, errorsCount } = subschema.validate(value[i], `${path}[${i}]`); 135 | 136 | if (errorsCount === 0) { 137 | continue; 138 | } 139 | 140 | if (Array.isArray(subErrors)) { 141 | errors.push(...subErrors); 142 | } else { 143 | errors.push(subErrors); 144 | } 145 | 146 | return feedback(); 147 | } 148 | 149 | return feedback(); 150 | } 151 | 152 | private validateElementsType(value: any): value is TInner { 153 | if (!this._state.subschema) { 154 | return true; 155 | } 156 | 157 | return this._state.subschema.validateType(value); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /test/tests/strings.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { string, ERROR_TYPES } from '../../'; 3 | import { shouldReturnErrors, shouldNotReturnErrors } from '../helpers'; 4 | 5 | const ROOT = 'root'; 6 | 7 | test('StringSchema.type should return string', assert => { 8 | assert.is(string().type, 'string'); 9 | }); 10 | 11 | test('StringSchema.validateType(): should return true for primitive strings and string objects', assert => { 12 | const primitives = ['1', '', 'sdfsdf', '324jn']; 13 | const stringObjects = primitives.map(str => new String(str)); 14 | const schema = string(); 15 | 16 | const allAreStrings = primitives.concat(stringObjects).every(str => schema.validateType(str)); 17 | 18 | assert.true(allAreStrings); 19 | }); 20 | 21 | test('.validate() returns errors with invalid types when .optional() has NOT been called', assert => { 22 | 23 | const schema = string(); 24 | const values = [true, {}, 10, [], null, undefined]; 25 | 26 | shouldReturnErrors(assert, schema, values, { type: ERROR_TYPES.TYPE }); 27 | }); 28 | 29 | test('.validate() does NOT return errors with invalid types when .optional() has been called', assert => { 30 | const schema = string().optional(); 31 | const invalidTypeValues = [true, {}, 10, null, [], undefined]; 32 | 33 | shouldNotReturnErrors(assert, schema, invalidTypeValues); 34 | }); 35 | 36 | test('.validate() does NOT return errors with valid strings when .optional() has NOT been called', assert => { 37 | const schema = string(); 38 | const stringValues = ['', 'gosho', new String('10'), new String(10)]; 39 | 40 | shouldNotReturnErrors(assert, schema, stringValues); 41 | }); 42 | 43 | test('.minlength().validate() returns range error for too short string', assert => { 44 | const schema = string().minlength(5); 45 | const tooShortStrings = ['a', '', 'b', 'ivan', 'pe']; 46 | 47 | shouldReturnErrors(assert, schema, tooShortStrings, { type: ERROR_TYPES.RANGE }); 48 | }); 49 | 50 | test('.minlength().validate() does not return errors for strings with at least minlength length', assert => { 51 | const schema = string().minlength(10); 52 | const validStrings = ['kalata shte hodi na fitnes', 'petya e iskreno i nepodkupno parche', 'tedi lyje, mami i obicha da tancuva']; 53 | 54 | shouldNotReturnErrors(assert, schema, validStrings); 55 | }); 56 | 57 | test('.maxlength().validate() return serrors for strings with more than allowed length', assert => { 58 | const schema = string().maxlength(8); 59 | const tooLongStrings = [ 60 | 'iskam si shala, inache shte te obesya', 61 | '6al 6al 6al 6al', 62 | '4ao 4ao 4ao 4ao s tvoya 6al 6al 6al 6al 6al', 63 | 'here iz a test', 64 | 'gosho tosho pesho shosho rosho' 65 | ]; 66 | 67 | shouldReturnErrors(assert, schema, tooLongStrings, { type: ERROR_TYPES.RANGE }); 68 | }); 69 | 70 | test('.maxlength().validate() does not return errors for strings with less than or equal to allowed length', assert => { 71 | const schema = string().maxlength(5); 72 | const validStrings = ['', '1', 'gg', 'ooo', 'four', 'gosho']; 73 | 74 | shouldNotReturnErrors(assert, schema, validStrings); 75 | }); 76 | 77 | test('.pattern().validate() returns errors for strings that do not match the provided regexp', assert => { 78 | const schema = string().pattern(/^[a-z]{5,10}$/i); 79 | const invalidStrings = ['abc', 'gg', 'kot', 'tedi pish-e i krad-e i lyj-e i mam-i i zaplashv-a i gled-a lo6o', 'testtesttest']; 80 | 81 | shouldReturnErrors(assert, schema, invalidStrings, { type: ERROR_TYPES.ARGUMENT }); 82 | }); 83 | 84 | test('.pattern().validate() does not return errors for strings that match the provided regexp', assert => { 85 | const schema = string().pattern(/^[a-z]{5,10}$/i); 86 | const validStrings = ['Goshko', 'TEODORA', 'petya', 'chieftain', 'viktor', 'cykuchev']; 87 | 88 | shouldNotReturnErrors(assert, schema, validStrings); 89 | }); 90 | 91 | test('All methods should enable chaining', assert => { 92 | const schema = string() 93 | .optional() 94 | .minlength(10) 95 | .maxlength(20) 96 | .pattern(/^[a-z]{5}$/i) 97 | .predicate(x => x !== 'test'); 98 | 99 | assert.is(typeof schema.validate, 'function'); 100 | }); 101 | 102 | test('.minlength(), .maxlength(), .optional() should return errors together with invalid strings', assert => { 103 | const schema = string() 104 | .minlength(7) 105 | .maxlength(14) 106 | .predicate(value => value.startsWith('cyki')); 107 | 108 | const invalidValues = ['tedi', 'gosho', new String('spica'), 'konsko pecheno sys shal', new String('konsko pecheno bez shal'), 'horsehorsehorsehorse']; 109 | 110 | const validationErrors = invalidValues 111 | .forEach(val => { 112 | const errorsArray = schema.validate(val, ROOT).errors; 113 | assert.is(errorsArray.filter(err => (err.type === ERROR_TYPES.RANGE) && (err.path === ROOT)).length, 1); 114 | assert.is(errorsArray.filter(err => (err.type === ERROR_TYPES.PREDICATE) && (err.path === ROOT)).length, 1); 115 | }); 116 | }); 117 | 118 | test('methods should return type error when validating value of incorrect type', assert => { 119 | const schema = string() 120 | .minlength(10) 121 | .maxlength(20) 122 | .pattern(/^[0-9]+$/i) 123 | .predicate(v => v); 124 | 125 | const root = 'arrayValue'; 126 | const notStrings = [null, undefined, { prop: 'somevalue' }, ['lol'], 10, new Number(3), () => null, /testregexp/g]; 127 | 128 | notStrings 129 | .map(v => schema.validate(v, root).errors) 130 | .forEach(errorsArray => { 131 | assert.is(errorsArray.length, 1); 132 | 133 | const [err] = errorsArray; 134 | 135 | assert.is(err.path, root); 136 | assert.is(err.type, ERROR_TYPES.TYPE); 137 | 138 | return err; 139 | }); 140 | }); 141 | 142 | test('methods should not return errors when .optional() has been called', assert => { 143 | const schema = string() 144 | .minlength(10) 145 | .maxlength(20) 146 | .pattern(/^[0-9]+$/i) 147 | .predicate(v => v) 148 | .optional(); 149 | 150 | const notStrings = [null, undefined, false, {}, [], String]; 151 | 152 | shouldNotReturnErrors(assert, schema, notStrings); 153 | }); 154 | 155 | test('methods should not return errors for valid strings', assert => { 156 | const schema = string() 157 | .minlength(2) 158 | .maxlength(6) 159 | .pattern(/^[0-9]+$/i) 160 | .predicate(v => v[0] === '0'); 161 | 162 | const validValues = ['012', '00', '001122', new String('01283')]; 163 | 164 | shouldNotReturnErrors(assert, schema, validValues); 165 | }); 166 | -------------------------------------------------------------------------------- /lib/schemas/number-schema.ts: -------------------------------------------------------------------------------- 1 | import { createError, ERROR_TYPES } from '../errors'; 2 | import BaseSchema from './base-schema'; 3 | 4 | import * as is from '../is'; 5 | 6 | export const name = 'number'; 7 | 8 | const typeName = 'number'; 9 | 10 | /** 11 | * Provides type checking for numbers, min/max, integer and safe integer validations. 12 | * 13 | * @export 14 | * @class NumberSchema 15 | * @extends {BaseSchema} 16 | */ 17 | export default class NumberSchema extends BaseSchema { 18 | private _precision: number; 19 | // @ts-ignore 20 | private _minvalue: number; 21 | // @ts-ignore 22 | private _maxvalue: number; 23 | private _nanAllowed: boolean = false; 24 | private _infinityAllowed: boolean = false; 25 | 26 | /** 27 | * Creates an instance of NumberSchema. 28 | * @memberof NumberSchema 29 | */ 30 | public constructor() { 31 | super(); 32 | this._precision = 0; 33 | } 34 | 35 | /** 36 | * Returns 'number'. 37 | * 38 | * @readonly 39 | * @memberof NumberSchema 40 | */ 41 | public get type() { 42 | return typeName; 43 | } 44 | 45 | /** 46 | * Validate whether a value is a number. 47 | * Primitive numbers and Number objects are considered of 48 | * type number, unless their value is NaN, Infinity or -Infinity. 49 | * Strings are NOT considered valid numbers, even if they are numerical. 50 | * 51 | * This behaviour can be changed by invoking the .allowNaN() and 52 | * allowInfinity() methods. 53 | * @see NumberSchema.allowNaN() 54 | * @see NumberSchema.allowInfinity() 55 | * 56 | * @param {*} value The value to be type checked. 57 | * @returns {value is number} Whether the value is a number. 58 | * @memberof NumberSchema 59 | */ 60 | public validateType(value: any): value is number { 61 | return is.Numeric(value) 62 | && (this._nanAllowed || !isNaN(value)) 63 | && (this._infinityAllowed || isFinite(value) || isNaN(value)); 64 | } 65 | 66 | /** 67 | * Set a precision that will be used for comparison between numbers by the schema, 68 | * for an example by .not(). 69 | * 70 | * @param {number} allowedDiff The largest possible difference between two numbers that would be considered equal. 71 | * @returns {this} 72 | * @memberof NumberSchema 73 | * 74 | * @example 75 | * number().not(1, 2, 3).validate(2.0001); // no error 76 | * number().precision(0.0000001).not(1, 2, 3).validate(2.0001); // error 77 | */ 78 | public precision(allowedDiff: number) { 79 | if (Number.isNaN(+allowedDiff) || allowedDiff < 0) { 80 | throw new TypeError(`Expected allowedDiff to be a valid positive number but got ${allowedDiff}`); 81 | } 82 | 83 | this._precision = allowedDiff; 84 | 85 | return this; 86 | } 87 | 88 | /** 89 | * NaN is considered of type number. 90 | * 91 | * @returns {this} 92 | * @memberof NumberSchema 93 | * 94 | * @example 95 | * number().allowNaN().validateType(NaN); // true 96 | */ 97 | public allowNaN() { 98 | this._nanAllowed = true; 99 | 100 | return this; 101 | } 102 | 103 | /** 104 | * +/- Infinity is considered of type number. 105 | * 106 | * @returns {this} 107 | * @memberof NumberSchema 108 | * 109 | * @example 110 | * number().allowInfinity().validateType(Infinity); // true 111 | */ 112 | public allowInfinity() { 113 | this._infinityAllowed = true; 114 | 115 | return this; 116 | } 117 | 118 | /** 119 | * Validated numbers must be in the range of [-Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER 120 | * 121 | * @returns {this} 122 | * @memberof NumberSchema 123 | * 124 | * @example 125 | * number().safeInteger().validate(Number.MAX_SAFE_INTEGER + 10); // error 126 | */ 127 | public safeInteger() { 128 | const newMin = Math.max(this._minvalue || -Number.MAX_SAFE_INTEGER, -Number.MAX_SAFE_INTEGER); 129 | const newMax = Math.min(this._maxvalue || Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER); 130 | 131 | return this.min(newMin).max(newMax); 132 | } 133 | 134 | /** 135 | * Specify a minimal allowed numeric value for the validated values. 136 | * 137 | * @throws {TypeError} If the provided minimal value is not of type number or NaN. 138 | * @param {number} minvalue Minimal value for the validated values. 139 | * @returns {this} 140 | * @memberof NumberSchema 141 | * 142 | * @example 143 | * number().min('asfsdf'); // throws 144 | * number().min(5).validate(2); // error 145 | */ 146 | public min(minvalue: number) { 147 | if (!is.StrictNumber(minvalue)) { 148 | throw new TypeError(`Expected a valid number as minimal value, but got ${minvalue}`); 149 | } 150 | 151 | if (is.Undefined(this._minvalue)) { 152 | this.pushValidationFn((value: number, path: string) => { 153 | if (value < this._minvalue) { 154 | return createError( 155 | ERROR_TYPES.RANGE, 156 | `Expected value greater than or equal to ${minvalue} but got ${value}`, 157 | path, 158 | ); 159 | } 160 | }); 161 | } 162 | 163 | this._minvalue = minvalue; 164 | 165 | return this; 166 | } 167 | 168 | /** 169 | * Specify a maximal value for the validated values. 170 | * 171 | * @throws {TypeError} If the provided maximum is not of type number or is NaN. 172 | * @param {number} maxvalue The maximal allowed value. 173 | * @returns {this} 174 | * @memberof NumberSchema 175 | * 176 | * @example 177 | * number().max('faceroll'); // throws 178 | * number().max(100).validate(300); // error 179 | */ 180 | public max(maxvalue: number) { 181 | if (!is.StrictNumber(maxvalue)) { 182 | throw new TypeError(`Expected a valid number as minimal value, but got ${maxvalue}`); 183 | } 184 | 185 | if (is.Undefined(this._maxvalue)) { 186 | this.pushValidationFn((value, path) => { 187 | if (value > maxvalue) { 188 | return createError( 189 | ERROR_TYPES.RANGE, 190 | `Expected value less than or equal to ${maxvalue} but got ${value}`, 191 | path, 192 | ); 193 | } 194 | }); 195 | } 196 | 197 | this._maxvalue = maxvalue; 198 | 199 | return this; 200 | } 201 | 202 | /** 203 | * Validated values must be integers. 204 | * 205 | * @returns {this} 206 | * @memberof NumberSchema 207 | * 208 | * @example 209 | * number().integer().validate(5.05); // error 210 | * number().integer().validate(5); // fine 211 | */ 212 | public integer() { 213 | return this.pushValidationFn((value: number, path: string) => 214 | Number.isInteger(value + 0) 215 | ? undefined 216 | : createError( 217 | ERROR_TYPES.ARGUMENT, 218 | `Expected integer number but got ${value}`, 219 | path, 220 | ), 221 | ); 222 | } 223 | 224 | protected areEqual(firstValue: number, secondValue: number) { 225 | const diff = Math.abs(firstValue - secondValue); 226 | 227 | return diff <= this._precision; 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /lib/schemas/object-schema.ts: -------------------------------------------------------------------------------- 1 | import { IValidationError } from '../contracts'; 2 | import { createCompositeError, createError, ERROR_TYPES } from '../errors'; 3 | import * as is from '../is'; 4 | import BaseSchema from './base-schema'; 5 | import EnumerationSchema from './enumeration-schema'; 6 | import StringSchema from './string-schema'; 7 | import valid from './valid-schema'; 8 | 9 | export const name = 'object'; 10 | 11 | const typeName = 'object'; 12 | 13 | /** 14 | * Provides validation for objects. Can be used to 15 | * create validation schemas for arbitarily deeply nested 16 | * objects. 17 | * 18 | * @export 19 | * @class ObjectSchema 20 | * @extends {BaseSchema} 21 | */ 22 | export default class ObjectSchema extends BaseSchema { 23 | private static _allowedSchemas = [StringSchema, EnumerationSchema]; 24 | private _state: { 25 | subschema: { [id: string]: BaseSchema }; 26 | allowFunctions: boolean; 27 | allowArrays: boolean; 28 | keysSchema: BaseSchema; 29 | valuesSchema: BaseSchema; 30 | }; 31 | 32 | /** 33 | * Creates an instance of ObjectSchema. 34 | * Accepts an object, whose keys are schemas themselves. 35 | * The schemas on those keys will be used to validate values on the same 36 | * keys in validated values. 37 | * @param {{ [id: string]: BaseSchema }} subschema Object schema whose keys have schemas as their values. 38 | * @memberof ObjectSchema 39 | * 40 | * @example 41 | * object({ 42 | * name: string().minlength(3).required(), 43 | * age: number().min(0).integer().required() 44 | * }).required(); 45 | */ 46 | public constructor(subschema: { [id: string]: BaseSchema }) { 47 | super(); 48 | this._state = { 49 | allowArrays: false, 50 | allowFunctions: false, 51 | keysSchema: valid, 52 | subschema: subschema || {}, 53 | valuesSchema: valid, 54 | }; 55 | } 56 | 57 | /** 58 | * Returns 'object'. 59 | * 60 | * @readonly 61 | * @memberof ObjectSchema 62 | */ 63 | public get type() { 64 | return typeName; 65 | } 66 | 67 | /** 68 | * Returns true for everything that is an object, except for Arrays and Functions. 69 | * {null} is not considered of type object. 70 | * This behaviour can be changed through the allowArrays() and allowFunctions() methods. 71 | * @see ObjectSchema.allowArray() 72 | * @see ObjectSchema.allowFunctions() 73 | * 74 | * @param {*} value The value that will be type checked. 75 | * @returns {boolean} 76 | * @memberof ObjectSchema 77 | * 78 | * @example 79 | * object().validateType({}); // fine 80 | * object().validateType([]); // error 81 | * object().validateType(() => {}); // error 82 | * object().validateType(new String('5')); // error 83 | * object().validateType(new Number(5)); // error 84 | */ 85 | public validateType(value: any): value is object { 86 | const valueIsArray = is.Array(value); 87 | const valueIsFunction = is.Function(value); 88 | 89 | return is.Obj(value) 90 | || (this._state.allowArrays && valueIsArray) 91 | || (this._state.allowFunctions && valueIsFunction); 92 | } 93 | 94 | /** 95 | * Array will be considered of type object. 96 | * 97 | * @returns {this} 98 | * @memberof ObjectSchema 99 | * 100 | * @example 101 | * object().allowArrays().required().validate([1, 2, 3]); // fine 102 | */ 103 | public allowArrays() { 104 | this._state.allowArrays = true; 105 | 106 | return this; 107 | } 108 | 109 | /** 110 | * Functions will be considered of type object. 111 | * 112 | * @returns {this} 113 | * @memberof ObjectSchema 114 | * 115 | * @example 116 | * object().allowFunctions.required().validate(() => {}); // fine 117 | */ 118 | public allowFunctions() { 119 | this._state.allowFunctions = true; 120 | 121 | return this; 122 | } 123 | 124 | public keys(keysSchema: StringSchema) { 125 | const { _allowedSchemas } = ObjectSchema; 126 | if (_allowedSchemas.every(Proto => !(keysSchema instanceof Proto))) { 127 | throw new TypeError( 128 | `Schema for object keys must be a ${_allowedSchemas.map(Proto => Proto.name).join(' or ')}`, 129 | ); 130 | } 131 | 132 | this._state.keysSchema = keysSchema; 133 | 134 | return this; 135 | } 136 | 137 | public values(valuesSchema: BaseSchema) { 138 | if (!(valuesSchema instanceof BaseSchema)) { 139 | throw new TypeError( 140 | `Schema for object values must be a ${ObjectSchema._allowedSchemas.map(x => x.name).join('or')}`, 141 | ); 142 | } 143 | 144 | this._state.valuesSchema = valuesSchema; 145 | 146 | return this; 147 | } 148 | 149 | public validate(value: any, path = '', errors: IValidationError[] = []) { 150 | if (!this.validateType(value)) { 151 | if (this._required) { 152 | errors.push(createError(ERROR_TYPES.TYPE, `Expected type ${this.type} but got ${typeof value}`, path)); 153 | } 154 | 155 | return { 156 | corrected: errors.length ? this.validateValueWithCorrectType({}, '') : value, 157 | errors, 158 | errorsCount: errors.length, 159 | }; 160 | } 161 | 162 | return this.validateValueWithCorrectType(value, path); 163 | } 164 | 165 | protected validateValueWithCorrectType(value: any, path?: string) { 166 | const errorsMap = Object.create(null); 167 | const correctedObj: any = {}; 168 | let currentErrorsCount = 0; 169 | 170 | /* tslint:disable forin */ 171 | for (const key in this._state.subschema) { 172 | const { 173 | errors, 174 | errorsCount, 175 | corrected: correctedValue, 176 | } = this._state.subschema[key].validate(value[key], path ? path + '.' + key : key); 177 | currentErrorsCount += errorsCount; 178 | errorsMap[key] = errors; 179 | correctedObj[key] = correctedValue; 180 | } 181 | 182 | const correctedKeys: string[] = []; 183 | const { valuesSchema, keysSchema } = this._state; 184 | for (const key in value) { 185 | const errorsForPath: IValidationError[] = []; 186 | 187 | const correctedKey = keysSchema.validate(key, key, errorsForPath).corrected; 188 | const correctedValue = valuesSchema.validate(value[key], key, errorsForPath).corrected; 189 | 190 | if (!errorsForPath.length) { 191 | continue; 192 | } 193 | 194 | correctedObj[correctedKey] = correctedValue; 195 | if (correctedKey !== key) { 196 | correctedKeys.push(key); 197 | } 198 | 199 | currentErrorsCount += errorsForPath.length; 200 | if (errorsMap[key]) { 201 | errorsForPath.push(errorsMap[key]); 202 | } 203 | 204 | errorsMap[key] = createCompositeError(`${path}.${key}`, errorsForPath); 205 | } 206 | /* tslint:enable forin */ 207 | 208 | const corrected = { ...value, ...correctedObj }; 209 | correctedKeys.forEach(key => delete corrected[key]); 210 | 211 | return { 212 | corrected, 213 | errors: errorsMap, 214 | errorsCount: currentErrorsCount, 215 | }; 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /lib/schemas/base-schema.ts: -------------------------------------------------------------------------------- 1 | import { IErrorFeedback, IValidationError } from '../contracts'; 2 | import { createError, ERROR_TYPES } from '../errors'; 3 | import * as is from '../is'; 4 | import { id } from '../utils'; 5 | 6 | export const name = 'base'; 7 | 8 | /** 9 | * Base class for all schemas. Implements common functionality such as 10 | * marking a schema as required, attachment of predicates to schemas, 11 | * blacklisting values and validation steps. Meant to be extended, not 12 | * instantiated directly. 13 | * @exports 14 | */ 15 | export default abstract class BaseSchema { 16 | protected validationFunctions: Array<(value: TValidated, path: string) => IValidationError | undefined>; 17 | protected _defaultExpr: (value: TValidated) => TValidated; 18 | protected _required: boolean; 19 | 20 | /** 21 | * Creates an instance of BaseSchema. 22 | * @memberof BaseSchema 23 | */ 24 | public constructor() { 25 | this._required = true; 26 | this.validationFunctions = []; 27 | this._defaultExpr = id; 28 | } 29 | 30 | /** 31 | * Perform a type validation on the value. 32 | * Used internally by the schemas, but could also be useful as individual method. 33 | * Every schema provides an implementation. 34 | * @abstract 35 | * @param {*} value - The value that will be type checked. 36 | * @returns {boolean} - Whether the value passed the type check. 37 | * @memberof BaseSchema 38 | * 39 | * @example 40 | * number().validateType('356'); // false 41 | * string().validateType('356'); // true 42 | * array(bool()).validateType([true, true, false, 0]); // false 43 | */ 44 | public abstract validateType(value: any): value is TValidated; 45 | 46 | /** 47 | * Return a string representation of the schemas's type. 48 | * @readonly 49 | * @abstract 50 | * @type {string} 51 | * @memberof BaseSchema 52 | * 53 | * @example 54 | * string().type 55 | * array().type 56 | */ 57 | public abstract get type(): string; 58 | 59 | /** 60 | * If the value fails type validation, do not emit errors. 61 | * 62 | * @returns {this} 63 | * @memberof BaseSchema 64 | * 65 | * @example 66 | * const maybeNaturalNumber = number().min(1).optional(); 67 | * maybeNaturalNumber.validate(-5); // error, type is number, but min validation fails 68 | * maybeNaturalNnumber.validate('-5'); // no errors, type validation does not pass 69 | */ 70 | public optional() { 71 | this._required = false; 72 | 73 | return this; 74 | } 75 | 76 | /** 77 | * Specify a predicate that will be used to validate the values. 78 | * @param {function} predicateFn 79 | * @returns {this} - The current instance of the BaseSchema. 80 | * @memberof BaseSchema 81 | * 82 | * @example 83 | * // predicate for odd numbers 84 | * number().predicate(n => n % 2 !== 0); 85 | */ 86 | public predicate(predicateFn: (value: TValidated) => boolean) { 87 | if (!is.Function(predicateFn)) { 88 | throw new TypeError(`Expected function as predicate but got value of type ${typeof predicateFn}`); 89 | } 90 | 91 | return this.pushValidationFn( 92 | (value, path) => predicateFn(value) ? undefined : createError(ERROR_TYPES.PREDICATE, 'Value failed predicate', path), 93 | ); 94 | } 95 | 96 | /** 97 | * Specify blacklisted values. 98 | * @param {...any[]} values - The blacklisted values. 99 | * @returns {this} 100 | * @memberof BaseSchema 101 | * 102 | * @example 103 | * 104 | * // error, because 'ts' is blacklisted 105 | * string().not('ts', 'c#', 'java').validate('ts'); 106 | */ 107 | public not(...values: any[]) { 108 | return this.pushValidationFn((value, path) => { 109 | const index = values.findIndex(element => this.areEqual(value, element)); 110 | 111 | if (index !== -1) { 112 | return createError(ERROR_TYPES.ARGUMENT, `Expected value to not equal ${values[index]} but it did`, path); 113 | } 114 | }); 115 | } 116 | 117 | public default(defaultValue: TValidated) { 118 | return this.defaultExpression(_ => defaultValue); 119 | } 120 | 121 | public defaultExpression(expr: (value: TValidated) => TValidated) { 122 | this._defaultExpr = expr; 123 | 124 | return this; 125 | } 126 | 127 | /** 128 | * Validate whether the provided value matches the type and the set of rules specified 129 | * by the current schema instance. For non-require'd schemas, failure of type validations will 130 | * result in no errors, because the schema rules will not be evaluated. Require'd schemas 131 | * will simply return type errors. 132 | * @param {*} value - The value to validate. 133 | * @param {string} [path=''] 134 | * @param {IValidationError[]} [currentErrors] 135 | * @returns {IErrorFeedback} 136 | * @memberof BaseSchema 137 | */ 138 | public validate(value: any, path = '', errors: IValidationError[] = []): IErrorFeedback { 139 | if (!this.validateType(value)) { 140 | if (this._required) { 141 | errors.push(createError(ERROR_TYPES.TYPE, `Expected type ${this.type} but got ${typeof value}`, path)); 142 | } 143 | 144 | return { 145 | corrected: errors.length ? this._defaultExpr(value) : value, 146 | errors, 147 | errorsCount: errors.length, 148 | }; 149 | } 150 | 151 | return this.validateValueWithCorrectType(value, path, errors); 152 | } 153 | 154 | /** 155 | * Adds a validation callback to the validations to be performed on a value passed to .validate() 156 | * @param {function} validationFn 157 | */ 158 | protected pushValidationFn(validationFn: (value: any, path: string) => IValidationError | undefined) { 159 | this.validationFunctions.push(validationFn); 160 | 161 | return this; 162 | } 163 | 164 | /** 165 | * Virtual method that is used to compare two values for equality in .not(). Can be overridden in child classes. 166 | * @returns {Boolean} - Returns true if the two values are equal, otherwise returns false. 167 | */ 168 | protected areEqual(firstValue: any, secondValue: any) { 169 | return firstValue === secondValue; 170 | } 171 | 172 | /** 173 | * Virtual method that synchronously validates whether a value, 174 | * which is known to be of a type matching the current schema's type, 175 | * satisfies the validation rules in the schema. Can be overridden in child classes. 176 | * @param {TValidated} value - The value of matching type to validate. 177 | * @param {string} path - The key of the value to validate. 178 | * @param {?[]} errors - Options error array to push possible validation errors to. 179 | */ 180 | protected validateValueWithCorrectType( 181 | value: TValidated, 182 | path: string, 183 | errors: IValidationError[] = [], 184 | ): IErrorFeedback { 185 | for (const validate of this.validationFunctions) { 186 | const err = validate(value, path); 187 | 188 | if (err) { 189 | errors.push(err); 190 | } 191 | } 192 | 193 | return { 194 | corrected: errors.length ? this._defaultExpr(value) : value, 195 | errors, 196 | errorsCount: errors.length, 197 | }; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /lib/schemas/date-schema.ts: -------------------------------------------------------------------------------- 1 | import { IValidationError } from '../contracts'; 2 | import { createError, ERROR_TYPES } from '../errors'; 3 | import * as is from '../is'; 4 | import BaseSchema from './base-schema'; 5 | 6 | export const name = 'date'; 7 | 8 | const typeName = 'date'; 9 | 10 | const validatePositiveInteger = (bound: any): bound is number => Number.isInteger(bound) && (0 <= bound); 11 | 12 | const isInRange = ( 13 | start: number, 14 | end: number, 15 | value: number, 16 | ) => start < end 17 | ? start <= value && value <= end 18 | : value <= end || start <= value; 19 | 20 | /** 21 | * This function is here because typescript is stupid and doesn't understand 22 | * javascript types and has not compile time templating. 23 | * @param {keyof Date} componentName 24 | * @param {Date} dateInstance 25 | * @returns {number} 26 | */ 27 | function getDateComponent(componentName: keyof Date, dateInstance: Date): number { 28 | switch (componentName) { 29 | case 'getSeconds': 30 | case 'getMinutes': 31 | case 'getHours': 32 | case 'getDay': 33 | case 'getMonth': 34 | case 'getDate': 35 | case 'getFullYear': 36 | return dateInstance[componentName](); 37 | default: 38 | throw new Error('Should never happen in production.'); 39 | } 40 | } 41 | 42 | const betweenValidation = ( 43 | start: number, 44 | end: number, 45 | ranges: { [key: string]: number }, 46 | dateGetFnKey: keyof Date, 47 | ) => { 48 | const dateComponentName = dateGetFnKey.replace(/get/, ''); 49 | if (!is.Undefined(ranges['_start' + dateComponentName] && ranges['_end' + dateComponentName])) { 50 | throw new Error(`Cannot set start and end for ${dateComponentName} twice on a single DateSchema instance`); 51 | } 52 | 53 | if (!validatePositiveInteger(start) || !validatePositiveInteger(end)) { 54 | throw new TypeError( 55 | `Expected integer numbers for start and end of ${dateComponentName}, but got ${start} and ${end}`, 56 | ); 57 | } 58 | 59 | ranges['_start' + dateComponentName] = start; 60 | ranges['_end' + dateComponentName] = end; 61 | 62 | return (value: Date, path: string) => { 63 | const rstart = ranges['_start' + dateComponentName]; 64 | const rend = ranges['_end' + dateComponentName]; 65 | const valueNumber = getDateComponent(dateGetFnKey, value); 66 | 67 | return isInRange(rstart, rend, valueNumber) 68 | ? undefined 69 | : createError( 70 | ERROR_TYPES.RANGE, 71 | `Expected ${dateComponentName} to be in range ${start}:${end} but got ${value}`, 72 | path, 73 | ); 74 | }; 75 | }; 76 | 77 | type TDateSchemaState = { 78 | _before?: Date; 79 | _after?: Date; 80 | ranges: { [key: string]: number }; 81 | }; 82 | 83 | export default class DateSchema extends BaseSchema { 84 | protected validationFunctions: Array<((value: Date, path: string) => IValidationError)> = []; 85 | private _state: TDateSchemaState; 86 | 87 | constructor() { 88 | super(); 89 | this._state = { ranges: {} }; 90 | } 91 | 92 | public get type() { 93 | return typeName; 94 | } 95 | 96 | /** 97 | * Validate whether the provided value is a Date object. Only date objects with valid time are considered valid dates. 98 | * @param {any} value - The value to be checked for type Date. 99 | * @returns {Boolean} 100 | */ 101 | public validateType(value: any): value is Date { 102 | return is.Date(value) && !Number.isNaN(value.getTime()); 103 | } 104 | 105 | /** 106 | * Introduce a before validation to the schema instance: 107 | * every date equal to or after the provided will be considered invalid. 108 | * @param {any} dateConstructorArgs - Arguments that you will typically pass to the Date constructor. 109 | * @returns {DateSchema} - Returns the current DateSchema instance to enable chaining. 110 | */ 111 | public before(...dateConstructorArgs: T[]) { 112 | if (!is.Undefined(this._state._before)) { 113 | throw new Error('Cannot set before date twice for a date schema instance'); 114 | } 115 | 116 | // @ts-ignore 117 | const beforeDate = new Date(...dateConstructorArgs); 118 | 119 | if (!this.validateType(beforeDate)) { 120 | throw new TypeError(`The value provided to .before() is not a valid date string or object ${dateConstructorArgs}`); 121 | } 122 | 123 | const { _state } = this; 124 | 125 | _state._before = beforeDate; 126 | 127 | this.pushValidationFn((value: Date, path: string) => { 128 | if (!is.NullOrUndefined(_state._before) && value >= _state._before) { 129 | return createError(ERROR_TYPES.RANGE, `Expected date before ${_state._before} but got ${value}`, path); 130 | } 131 | }); 132 | 133 | return this; 134 | } 135 | 136 | /** 137 | * Introduce an after validation to the schema instance: 138 | * every date equal to or before the provided will be considered invalid. 139 | * @param {any} dateConstructorArgs - Arguments that you will typically pass to the Date constructor. 140 | * @returns {DateSchema} - Returns the current DateSchema instance to enable chaining. 141 | */ 142 | public after(...dateConstructorArgs: T[]) { 143 | if (!is.Undefined(this._state._after)) { 144 | throw new Error('Cannot set after date twice for a date schema instance'); 145 | } 146 | 147 | // @ts-ignore 148 | const afterDate = new Date(...dateConstructorArgs); 149 | 150 | if (!this.validateType(afterDate)) { 151 | throw new TypeError(`The value provided to .after() is not a valid date string or object ${dateConstructorArgs}`); 152 | } 153 | 154 | const { _state } = this; 155 | 156 | _state._after = afterDate; 157 | 158 | this.pushValidationFn((value, path) => { 159 | if (!is.NullOrUndefined(_state._after) && value <= _state._after) { 160 | return createError(ERROR_TYPES.RANGE, `Expected date after ${_state._after} but got ${value}`, path); 161 | } 162 | }); 163 | 164 | return this; 165 | } 166 | 167 | /** 168 | * Set validation for range on date in month. 169 | * If start < end, value will be validated against the range [start, end] 170 | * If start > end, value will be validated against the ranges [0, start] and [end, 31] 171 | */ 172 | public dateBetween(start: number, end: number) { 173 | return this.pushValidationFn( 174 | betweenValidation(start, end, this._state.ranges, 'getDate'), 175 | ); 176 | } 177 | 178 | public monthBetween(start: number, end: number) { 179 | return this.pushValidationFn( 180 | betweenValidation(start, end, this._state.ranges, 'getMonth'), 181 | ); 182 | } 183 | 184 | public hourBetween(start: number, end: number) { 185 | return this.pushValidationFn( 186 | betweenValidation(start, end, this._state.ranges, 'getHours'), 187 | ); 188 | } 189 | 190 | public weekdayBetween(start: number, end: number) { 191 | return this.pushValidationFn( 192 | betweenValidation(start, end, this._state.ranges, 'getDay'), 193 | ); 194 | } 195 | 196 | public minutesBetween(start: number, end: number) { 197 | return this.pushValidationFn( 198 | betweenValidation(start, end, this._state.ranges, 'getMinutes'), 199 | ); 200 | } 201 | 202 | public secondsBetween(start: number, end: number) { 203 | return this.pushValidationFn( 204 | betweenValidation(start, end, this._state.ranges, 'getSeconds'), 205 | ); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /test/tests/array.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import * as FluentSchemer from '../../'; 3 | import { shouldReturnErrors, shouldNotReturnErrors } from '../helpers'; 4 | 5 | const { array, string, number, object, bool, ERROR_TYPES } = FluentSchemer; 6 | 7 | const validateTypeTest = (subschemaType, arrayToValidate, valid = true) => test( 8 | `array().validateType(): returns "${valid}" for array<${subschemaType}> with input ${arrayToValidate}`, 9 | assert => { 10 | const typedArraySchema = array(FluentSchemer[subschemaType]()); 11 | const isValidArray = typedArraySchema.validateType(arrayToValidate); 12 | 13 | assert.is(isValidArray, valid); 14 | } 15 | ); 16 | 17 | for (const type of ['bool', 'string', 'number', 'object']) { 18 | test(`array().type: returns array<${type}> type for subschema of type ${type}`, assert => { 19 | const schema = array(FluentSchemer[type]()); 20 | 21 | assert.is(schema.type, `array<${type}>`); 22 | }); 23 | } 24 | 25 | test('array().type: returns array for untyped array', assert => { 26 | assert.is(array().type, 'array'); 27 | }); 28 | 29 | test('array().validateType(): returns "true" for untyped array with values of distinct types', assert => { 30 | const schema = array(); 31 | const values = [[1, 2, 3], ['adf', true, {}], [null, undefined, []]]; 32 | const allValid = values.every(untyped => schema.validateType(untyped)); 33 | 34 | assert.true(allValid); 35 | }); 36 | 37 | test('array().validateType(): returns "true" for empty array', assert => { 38 | assert.true(array(bool()).validateType([])); 39 | }); 40 | 41 | validateTypeTest('string', ['a', 'spica', 'huffman', 'beer', new String('fire')]); 42 | validateTypeTest('string', [null, 1, NaN, {}, [], () => 1, true, undefined], false); 43 | 44 | validateTypeTest('bool', [true, false, new Boolean(true)]); 45 | validateTypeTest('bool', [null, undefined, 1, 0, '', 'sdf', {}, [], () => true], false); 46 | 47 | validateTypeTest('number', [1, 2, 3, 4, 10, -20, new Number(10), new Number(0)]); 48 | validateTypeTest('number', [null, undefined, NaN, Infinity, '', '10', [], {}, () => 10, false], false); 49 | 50 | validateTypeTest('object', [{}, { zdr: 'kp' }]); 51 | validateTypeTest('object', [1, 2, [], () => { }, 'dsf', true], false); 52 | 53 | test('array().validate() returns error when value is not an array', assert => { 54 | const schema = array(); 55 | const notArrays = [1, 2, 0, {}, { length: 1 }, () => 1, 'dsfsdf']; 56 | 57 | shouldReturnErrors(assert, schema, notArrays, { type: ERROR_TYPES.TYPE }); 58 | }); 59 | 60 | test('array().minlength() throws with negative numbers', assert => { 61 | assert.throws((() => array().minlength(-5)), TypeError); 62 | }); 63 | 64 | test('array().minlength() throws with NaN', assert => { 65 | assert.throws(() => array().minlength(NaN), TypeError); 66 | }) 67 | 68 | test(`array().minlength() throws with strings, even if they're numeric`, assert => { 69 | assert.throws(() => array().minlength('6'), TypeError); 70 | assert.throws(() => array().minlength('not even close brah'), TypeError); 71 | }); 72 | 73 | test('array().minlength().validate() returns range error for too arrays with (length < minlength)', assert => { 74 | const schema = array().minlength(5); 75 | const values = [[], ['a', 'tedi'], ['steven', 'kon', 'beer', 'coding'], ['kyci the mermaid']]; 76 | 77 | shouldReturnErrors(assert, schema, values, { type: ERROR_TYPES.RANGE }); 78 | }); 79 | 80 | test('array().maxlength() throws with negative numbers', assert => { 81 | assert.throws(() => array().maxlength(-5), TypeError); 82 | }); 83 | 84 | test('array().maxlength() throws with strings that are numeric', assert => { 85 | assert.throws(() => array().maxlength('6'), TypeError); 86 | }); 87 | 88 | test('array().maxlength() throws with NaN', assert => { 89 | assert.throws(() => array().maxlength(NaN), TypeError); 90 | }); 91 | 92 | test('array().maxlength() validation returns range error for arrays with length > maxlength', assert => { 93 | const schema = array().maxlength(2); 94 | const values = [[1, 2, 3], ['sdgfds', 'sdgfdg', 'errere', null], [true, false, true, false, true], [[], [], []]]; 95 | 96 | shouldReturnErrors(assert, schema, values, { type: ERROR_TYPES.RANGE }); 97 | }); 98 | 99 | test('array().withLength(): throws with negative numbers', assert => { 100 | assert.throws(() => array().withLength(-5), TypeError); 101 | }); 102 | 103 | test('array().withLength(): throws with strings that are numeric', assert => { 104 | assert.throws(() => array().withLength('6'), TypeError); 105 | }); 106 | 107 | test('array().withLength(): throws with NaN', assert => { 108 | assert.throws(() => array().withLength(NaN), TypeError); 109 | }); 110 | 111 | test('array().withLength(): validation returns errors for arrays with different length', assert => { 112 | const schema = array().withLength(3); 113 | const values = [[1, 2], [], [1, 2, 3, 4]]; 114 | 115 | shouldReturnErrors(assert, schema, values, { type: ERROR_TYPES.RANGE }); 116 | }); 117 | 118 | test('array().withLength(): validation doesn`t return errors for arrays with same length', assert => { 119 | const schema = array().withLength(3); 120 | const values = [[1, 2, 6], ['a', 'b', 'gosho'], [null, undefined, 'stamat']]; 121 | 122 | shouldNotReturnErrors(assert, schema, values); 123 | }); 124 | 125 | test('array().distinct(): validation returns error for arrays with duplicate values', assert => { 126 | const schema = array().distinct(); 127 | const object = {}; 128 | const values = [[1, 1, 2, 3, 4, 5], ['a', 'b', 'c', 'gosho', 'd', 'gosho'], [true, true], [object, 1, 2, object]]; 129 | 130 | shouldReturnErrors(assert, schema, values, { type: ERROR_TYPES.ARGUMENT }); 131 | }); 132 | 133 | test('array().distinct(): validation doesn`t return error for arrays with distinct values', assert => { 134 | const schema = array().distinct(); 135 | const values = [[1, 2, 3], ['a', 'b', 'gosho'], [{}, {}]]; 136 | 137 | shouldNotReturnErrors(assert, schema, values); 138 | }); 139 | 140 | test('array().validate(): returns error for first invalid value only', assert => { 141 | const schema = array(number().integer()); 142 | const values = [1, 2, 0, new Number(5), new Number(1.10), new Number(2.5)]; 143 | 144 | const { errors } = schema.validate(values, 'nums'); 145 | assert.is(errors.length, 1); 146 | 147 | const [err] = errors; 148 | 149 | assert.is(err.path, 'nums[4]'); 150 | assert.is(err.type, ERROR_TYPES.ARGUMENT); 151 | }); 152 | 153 | test('All methods should enable chaining', assert => { 154 | const schema = array(number()).withLength(5).distinct().predicate(x => true); 155 | 156 | assert.is(typeof schema.validate, 'function'); 157 | }); 158 | 159 | test('array().validate(): returns errors when nested schema doesnt match values', assert => { 160 | const schema = array(array()); 161 | const values = [[1, 2], ['dfdf'], [{ length: 1 }]]; 162 | 163 | shouldReturnErrors(assert, schema, values, { type: ERROR_TYPES.TYPE }); 164 | }); 165 | 166 | test('array().validate(): returns errors for multiple levels of nesting', assert => { 167 | const schema = array(array(array(number()))); 168 | const values = [ 169 | [[['asadd', 23]]], 170 | [[1, 2, 3]], 171 | [9, 10, -5] 172 | ]; 173 | 174 | shouldReturnErrors(assert, schema, values, { type: ERROR_TYPES.TYPE }); 175 | }); 176 | 177 | test('array().validate(): returns type errors for valid level of nesting but invalid type of values', assert => { 178 | const schema = array(array(array(bool()))); 179 | const values = [ 180 | [[[0, 1]]], 181 | [[['true']]], 182 | [[[null]]], 183 | [[[undefined]]] 184 | ]; 185 | 186 | shouldReturnErrors(assert, schema, values, { type: ERROR_TYPES.TYPE }); 187 | }); 188 | 189 | test('array().validate(): returns errors for array of objects', assert => { 190 | const schema = array(object({ name: string() })); 191 | const values = [ 192 | null, 193 | undefined, 194 | {}, 195 | { name: 1 }, 196 | { name: null } 197 | ]; 198 | 199 | shouldReturnErrors(assert, schema, values, { type: ERROR_TYPES.TYPE }); 200 | }); 201 | 202 | test('array().validate(): doesn`t return errors for an array of valid objects', assert => { 203 | const schema = array(object({ name: string() })); 204 | const values = [ 205 | [{ name: 'gosho' }], 206 | [{ name: 'tedi' }], 207 | [{ name: 'kyci' }], 208 | [{ name: 'bychveto' }] 209 | ]; 210 | 211 | shouldNotReturnErrors(assert, schema, values); 212 | }); 213 | -------------------------------------------------------------------------------- /test/tests/object.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { string, bool, number, object, enumeration, ERROR_TYPES } from '../../'; 3 | import { shouldNotReturnErrors } from '../helpers'; 4 | 5 | test('ObjectSchema.type should return "object"', assert => { 6 | assert.is(object().type, 'object'); 7 | }); 8 | 9 | test('ObjectSchema.validateType(): should return true for objects that are not arrays and functions', assert => { 10 | assert.is(object().validateType({}), true); 11 | assert.is(object().validateType(Object.create(null)), true); 12 | }); 13 | 14 | test('ObjectSchema.validateType(): should return false for arrays and functions', assert => { 15 | assert.is(object().validateType([]), false); 16 | assert.is(object().validateType(() => { }), false); 17 | }); 18 | 19 | test('ObjectSchema.validateType(): should return true for arrays when .allowArrays() has been called', assert => { 20 | assert.is(object().allowArrays().validateType([]), true); 21 | }); 22 | 23 | test('ObjectSchema.validate(): should return true for functions when .allowFunctions() has been called', assert => { 24 | assert.is(object().allowFunctions().validateType(() => { }), true); 25 | }); 26 | 27 | test('Should return errors for invalid keys of expected type', assert => { 28 | const personSchema = object({ 29 | name: string().minlength(3).maxlength(10).optional(), 30 | age: number().min(0).max(100).optional(), 31 | isStudent: bool() 32 | }); 33 | const invalidPerson = { 34 | name: '1', 35 | age: -1 36 | }; 37 | 38 | const { 39 | errors: { 40 | name: [nameError], 41 | age: [ageError], 42 | isStudent: [isStudentError] 43 | } 44 | } = personSchema.validate(invalidPerson, 'person'); 45 | 46 | assert.is(nameError.type, ERROR_TYPES.RANGE); 47 | assert.is(ageError.type, ERROR_TYPES.RANGE); 48 | assert.is(isStudentError.type, ERROR_TYPES.TYPE); 49 | }); 50 | 51 | test('Should return errors for keys with invalid values when .optional() has NOT been called on the subschemas', assert => { 52 | const softwareProjectSchema = object({ 53 | mainLang: string(), 54 | loc: number(), 55 | size: number(), 56 | isPrivate: bool() 57 | }); 58 | 59 | const invalidProject = { 60 | mainLang: null, 61 | loc: NaN, 62 | size: '10' 63 | }; 64 | 65 | const { 66 | errors: { 67 | mainLang: [mainLangError], 68 | loc: [locError], 69 | size: [sizeError], 70 | isPrivate: [isPrivateError] 71 | } 72 | } = softwareProjectSchema.validate(invalidProject, 'gosho'); 73 | 74 | assert.is(isPrivateError.type, ERROR_TYPES.TYPE); 75 | assert.is(locError.type, ERROR_TYPES.TYPE); 76 | assert.is(mainLangError.type, ERROR_TYPES.TYPE); 77 | assert.is(sizeError.type, ERROR_TYPES.TYPE); 78 | }); 79 | 80 | test('Should return errors for keys with invalid values but of correct type when .optional() has been called', assert => { 81 | const postSchema = object({ 82 | id: string().pattern(/^[0-9]{10}$/i).optional(), 83 | likesCount: number().integer().optional(), 84 | content: string().minlength(5).optional() 85 | }); 86 | 87 | const invalidPost = { 88 | id: 'aaaaaaaaa', 89 | likesCount: -1.5, 90 | content: 'aa' 91 | }; 92 | 93 | const { 94 | errors: { 95 | content: [contentError], 96 | id: [idError], 97 | likesCount: [likesCountError] 98 | } 99 | } = postSchema.validate(invalidPost, 'post'); 100 | 101 | assert.is(contentError.path, 'post.content'); 102 | assert.is(contentError.type, ERROR_TYPES.RANGE); 103 | 104 | assert.is(idError.path, 'post.id'); 105 | assert.is(idError.type, ERROR_TYPES.ARGUMENT); 106 | 107 | assert.is(likesCountError.path, 'post.likesCount'); 108 | assert.is(likesCountError.type, ERROR_TYPES.ARGUMENT); 109 | }); 110 | 111 | test('Should not return errors for valid keys', assert => { 112 | const personSchema = object({ 113 | name: string().minlength(3).maxlength(10), 114 | age: number().min(0).max(100), 115 | isStudent: bool() 116 | }); 117 | const pencho = { 118 | name: 'pencho', 119 | age: 5, 120 | isStudent: false 121 | }; 122 | 123 | shouldNotReturnErrors(assert, personSchema, [pencho]); 124 | }); 125 | 126 | test('Should not return errors for keys that have value of invalid type but are optional', assert => { 127 | const animalSchema = object({ 128 | breed: string().minlength(2).maxlength(10).optional(), 129 | weightKg: number().min(0).optional(), 130 | carnivore: bool().optional() 131 | }); 132 | 133 | const funkyAnimals = [ 134 | { breed: 101, weightKg: 'doge', carnivore: 'nahman' }, 135 | { breed: null, weightKg: 'tosho' }, 136 | ]; 137 | 138 | shouldNotReturnErrors(assert, animalSchema, funkyAnimals); 139 | }); 140 | 141 | test('Nested object schemas should also return errors for invalid values', assert => { 142 | const compilerSchema = object({ 143 | lang: object({ 144 | name: string().minlength(1), 145 | crossPlatform: bool(), 146 | version: number().min(0) 147 | }), 148 | name: string().minlength(1) 149 | }); 150 | 151 | const invalidCompiler = { 152 | name: 'Pesho', 153 | lang: { 154 | name: '', 155 | crossPlatform: 10, 156 | version: '39393' 157 | } 158 | }; 159 | 160 | const { 161 | errors: { 162 | lang: { 163 | name: [nameError], 164 | crossPlatform: [crossPlatformError], 165 | version: [versionError] 166 | } 167 | } 168 | } = compilerSchema.validate(invalidCompiler, 'compiler'); 169 | 170 | assert.is(crossPlatformError.path, 'compiler.lang.crossPlatform'); 171 | assert.is(crossPlatformError.type, ERROR_TYPES.TYPE); 172 | 173 | assert.is(nameError.path, 'compiler.lang.name'); 174 | assert.is(nameError.type, ERROR_TYPES.RANGE); 175 | 176 | assert.is(versionError.path, 'compiler.lang.version'); 177 | assert.is(versionError.type, ERROR_TYPES.TYPE); 178 | }); 179 | 180 | test('Nesting should return error and not crash when the nested object is not present on the actual value', assert => { 181 | const schema = object({ 182 | options: object({ opt: bool() }) 183 | }); 184 | 185 | const invalidObj = {}; 186 | const { errors: { options: errors } } = schema.validate(invalidObj, 'obj'); 187 | 188 | assert.is(errors.length, 1); 189 | 190 | const [error] = errors; 191 | 192 | assert.is(error.type, ERROR_TYPES.TYPE); 193 | assert.is(error.path, 'obj.options'); 194 | }); 195 | 196 | test('Nesting should not return errors when the nested schema is optional', assert => { 197 | const schema = object({ 198 | options: object({ opts: string() }).optional() 199 | }); 200 | 201 | shouldNotReturnErrors(assert, schema, [{}]); 202 | }); 203 | 204 | test('object().keys() throws when argument is not a string schema', assert => { 205 | assert.throws(() => object().keys({}), TypeError); 206 | assert.throws(() => object.keys(number().min(0)), TypeError); 207 | }); 208 | 209 | [ 210 | [string().pattern(/^[a-z]$/), { h: 4, o: 10, r: 1, s: 7, e: 9 }], 211 | [enumeration('gosho', 'pesho'), { gosho: 1, pesho: [] }] 212 | ].forEach( 213 | ([keySchema, input]) => test(`object.keys() returns no errors when object keys matches ${keySchema.type} schema`, assert => { 214 | const { errorsCount } = object().keys(keySchema).validate(input); 215 | assert.is(errorsCount, 0); 216 | }) 217 | ); 218 | 219 | [ 220 | [ 221 | string().maxlength(5), 222 | { 223 | ivan: true, 224 | penka: false, 225 | dmitriy: true, 226 | kostya: false 227 | }, 228 | 2 229 | ], 230 | [ 231 | enumeration('a', 'b', 'c'), 232 | { a: 1, b: 2, x: 3, c: 4, z: 3 }, 233 | 2 234 | ] 235 | ].forEach( 236 | ([keySchema, erroneousInput, expectedErrorsCount]) => test(`object.keys() returns errors when object keys do not match the ${keySchema.type} schema`, assert => { 237 | const { errorsCount } = object().keys(keySchema).validate(erroneousInput); 238 | assert.is(errorsCount, expectedErrorsCount); 239 | }) 240 | ); 241 | 242 | test('object().values() throws when argument is not a string schema', assert => { 243 | assert.throws(() => object().values({}), TypeError); 244 | }); 245 | 246 | test('object.values() returns no errors when object values match provided schema', assert => { 247 | const alphabetMapSchema = object().values(number().integer()); 248 | const letterOccurrences = { 249 | h: 4, 250 | o: 10, 251 | r: 1, 252 | s: 7, 253 | e: 9 254 | }; 255 | 256 | const { errorsCount, errors } = alphabetMapSchema.validate(letterOccurrences); 257 | 258 | assert.is(errorsCount, 0); 259 | }); 260 | 261 | test('object.values() returns errors when object values do not match provided schema', assert => { 262 | const idontcare = object().values(number().integer()); 263 | 264 | const somethingMap = { 265 | penka: false, 266 | ivan: 1.5, 267 | dmitriy: 'pesho', 268 | kostya: 5 269 | }; 270 | 271 | const { errorsCount, errors } = idontcare.validate(somethingMap); 272 | assert.is(errorsCount, 3); 273 | }); 274 | -------------------------------------------------------------------------------- /test/tests/numbers.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { number, ERROR_TYPES } from '../../'; 3 | import { shouldReturnErrors, shouldNotReturnErrors } from '../helpers'; 4 | 5 | test('.type returns "number"', assert => assert.is(number().type, 'number')); 6 | 7 | test('.validateType(): returns false for all NaN values, Infinity and values that are not of type "number"', assert => { 8 | const nans = [NaN, '1', '', {}, [], Infinity, null, undefined]; 9 | const schema = number(); 10 | const somePassedValidation = nans.some(nan => schema.validateType(nan)); 11 | 12 | assert.false(somePassedValidation); 13 | }); 14 | test('.validateType(): returns true for NaN when .allowNaN() is called', assert => { 15 | assert.true(number().allowNaN().validateType(NaN)); 16 | }); 17 | 18 | test('.validateType(): returns true for Infinity when .allowInfinity() is called', assert => { 19 | assert.true(number().allowInfinity().validateType(Infinity)); 20 | }); 21 | 22 | test('.validateType(): returns false for values of other type even when .allowNaN() has been called', assert => { 23 | const notNumbers = ['1', {}, Function, [], true, null, undefined]; 24 | const schema = number().allowNaN(); 25 | const allAreNotNumbers = notNumbers.every(nan => !schema.validateType(nan)); 26 | 27 | assert.true(allAreNotNumbers); 28 | }); 29 | 30 | test('.validateType(): returns true for primitive numbers and Number objects', assert => { 31 | const primitiveNumbers = [0, 1, -5, Number.MAX_SAFE_INTEGER, Number.MAX_VALUE, 0x12A, 0b110, 0o127]; 32 | const numberObjects = primitiveNumbers.map(n => new Number(n)); 33 | const schema = number(); 34 | 35 | const allAreNumbers = primitiveNumbers.concat(numberObjects).every(n => schema.validateType(n)); 36 | assert.true(allAreNumbers); 37 | }); 38 | 39 | test('number().validate() returns errors with invalid types when .optional() has NOT been called', assert => { 40 | const schema = number(); 41 | const NaNValues = [true, NaN, {}, [], 'kalata', null, undefined, Infinity]; 42 | 43 | shouldReturnErrors(assert, schema, NaNValues, { type: ERROR_TYPES.TYPE }); 44 | }); 45 | 46 | test('number().validate() does not return errors when .optional() has been called', assert => { 47 | const schema = number().optional(); 48 | const NaNValues = [true, NaN, {}, [], 'kalata', null, undefined, Infinity]; 49 | 50 | shouldNotReturnErrors(assert, schema, NaNValues); 51 | }); 52 | 53 | test('number().validate() does not return errors with valid numbers and .optional() has NOT been called', assert => { 54 | const schema = number(); 55 | const validNumbers = [1, new Number(2), new Number(0), -3, 0, -23929229, new Number('-2')]; 56 | 57 | shouldNotReturnErrors(assert, schema, validNumbers); 58 | }); 59 | 60 | test('number().not(): returns errors regardless of wether a value is primitive or wrapped in object', assert => { 61 | const schema = number().not(1, 5, -10); 62 | const invalidValues = [1, 5, -10].map(n => new Number(n)); 63 | 64 | shouldReturnErrors(assert, schema, invalidValues, { type: ERROR_TYPES.ARGUMENT }); 65 | }); 66 | 67 | test('.min() throws with strings that are numeric', assert => { 68 | assert.throws(() => number().min('6'), TypeError); 69 | }); 70 | 71 | test('.min() throws with NaN', assert => { 72 | assert.throws(() => number().min(NaN), TypeError); 73 | }); 74 | 75 | test('.min().validate() returns errors for values below the passed minimal value', assert => { 76 | const schema = number().min(10); 77 | const smallNumbers = [1, 2, -5, 0, new Number(9), new Number(-3), 9]; 78 | 79 | shouldReturnErrors(assert, schema, smallNumbers, { type: ERROR_TYPES.RANGE }); 80 | }); 81 | 82 | test('.min().validate() does not return errors for values in correct range', assert => { 83 | const schema = number().min(-10); 84 | const numbers = [-10, -9, -5, new Number(-4), 0, new Number(0), 20]; 85 | 86 | shouldNotReturnErrors(assert, schema, numbers); 87 | }); 88 | 89 | test('.max() throws with strings that are numeric', assert => { 90 | assert.throws(() => number().max('6'), TypeError); 91 | }); 92 | 93 | test('.max() throws with NaN', assert => { 94 | assert.throws(() => number().max(NaN), TypeError); 95 | }); 96 | 97 | test('.max().validate() returns errors for values greater than the maximum', assert => { 98 | const schema = number().max(30); 99 | const greaterNumbers = [new Number(40), 40, 44, 100, Number.MAX_SAFE_INTEGER]; 100 | 101 | shouldReturnErrors(assert, schema, greaterNumbers, { type: ERROR_TYPES.RANGE }); 102 | }); 103 | 104 | test('.max().validate() does not return errors for values lesser than or equal to the maximum', assert => { 105 | const schema = number().max(5); 106 | const validNumbers = [5, new Number(5), new Number(-3), new Number(0), 4, -11, 0, -4]; 107 | 108 | shouldNotReturnErrors(assert, schema, validNumbers); 109 | }); 110 | 111 | test('.integer().validate() returns errors for floating point numbers', assert => { 112 | const schema = number().integer(); 113 | const floats = [5.1, 3.4, -1.5, new Number(5.1), new Number(-3.14), new Number(0.5)]; 114 | 115 | shouldReturnErrors(assert, schema, floats, { type: ERROR_TYPES.ARGUMENT }); 116 | }); 117 | 118 | test('.integer().validate() does not return errors for integer numbers', assert => { 119 | const schema = number().integer(); 120 | const integers = [-10, 10, Number.MAX_SAFE_INTEGER, new Number(5), new Number(1.0), new Number(-2.0)]; 121 | 122 | shouldNotReturnErrors(assert, schema, integers); 123 | }); 124 | 125 | test('.safeInteger().validate() returns errors for unsafe integers', assert => { 126 | const schema = number().safeInteger(); 127 | const unsafeInts = [4, 6, 8] 128 | .map(x => x + Number.MAX_SAFE_INTEGER) 129 | .concat([-4, -6, -8].map(x => x - Number.MAX_SAFE_INTEGER)); 130 | 131 | shouldReturnErrors(assert, schema, unsafeInts, { type: ERROR_TYPES.RANGE }); 132 | }); 133 | 134 | test('.safeInteger().validate() keeps values from .min() and .max() if they are safe', assert => { 135 | const schema = number().min(-33).max(33).safeInteger(); 136 | const values = [-35, 35]; 137 | 138 | shouldReturnErrors(assert, schema, values, { type: ERROR_TYPES.RANGE }); 139 | }); 140 | 141 | test('All methods should enable chaining', assert => { 142 | const schema = number() 143 | .optional() 144 | .min(5) 145 | .max(10) 146 | .allowNaN() 147 | .allowInfinity() 148 | .not(5.2); 149 | 150 | assert.is(typeof schema.validate, 'function'); 151 | }); 152 | 153 | test('.min() should not return errors for NaN when .allowNaN() has been called', assert => { 154 | const schema = number().allowNaN().min(5); 155 | const { errorsCount } = schema.validate(NaN, 'tears'); 156 | 157 | assert.is(errorsCount, 0); 158 | }); 159 | 160 | test('.max() should not return errors for NaN when .allowNaN() has been called', assert => { 161 | const schema = number().allowNaN().max(5); 162 | const { errorsCount } = schema.validate(NaN, 'tears'); 163 | 164 | assert.is(errorsCount, 0); 165 | }); 166 | 167 | test(' .min() .max() .optional() .integer() should return errors for invalid numbers', assert => { 168 | const schema = number() 169 | .optional() 170 | .min(-5) 171 | .max(5) 172 | .integer(); 173 | 174 | // test with value greater than max 175 | const { errors: errorsWithGreaterThanMax } = schema.validate(10.5, 'value'); 176 | 177 | const rangeErrorGreater = errorsWithGreaterThanMax.find(err => err.type === ERROR_TYPES.RANGE), 178 | argumentErrorsGreater = errorsWithGreaterThanMax.find(err => err.type === ERROR_TYPES.ARGUMENT); 179 | 180 | assert.is(rangeErrorGreater.path, 'value'); 181 | assert.is(argumentErrorsGreater.path, 'value'); 182 | 183 | // test with value lesser than min 184 | const { errors: errorsWithLessThanMin } = schema.validate(-5.2, 'value'); 185 | 186 | const rangeErrorLesser = errorsWithLessThanMin.find(err => err.type === ERROR_TYPES.RANGE), 187 | argumentErrorsLesser = errorsWithLessThanMin.find(err => err.type === ERROR_TYPES.ARGUMENT); 188 | 189 | assert.is(rangeErrorLesser.path, 'value'); 190 | assert.is(argumentErrorsLesser.path, 'value'); 191 | }); 192 | 193 | test('.not() should return errors when diff is less than specified precision via .precision()', assert => { 194 | const schema = number() 195 | .not(1.0001, 2.5111, 3.0002) 196 | .precision(0.0001); 197 | 198 | const numbers = [1.00005, 2.51108, 3.00021]; 199 | const numberObjects = numbers.map(n => new Number(n)); 200 | 201 | shouldReturnErrors(assert, schema, numbers.concat(numberObjects), { type: ERROR_TYPES.ARGUMENT }); 202 | }); 203 | 204 | test('.not() should not return errors when diff is more than specified precision via .precision()', assert => { 205 | const schema = number() 206 | .not(1.0001, 2.5111, 3.0002) 207 | .precision(0.000001); 208 | 209 | const numbers = [1.00005, 2.51108, 3.00021], 210 | numberObjects = numbers.map(n => new Number(n)); 211 | 212 | shouldNotReturnErrors(assert, schema, numbers.concat(numberObjects)); 213 | }); 214 | 215 | test('.min() .max() .not() should return errors for invalid values', assert => { 216 | const schema = number() 217 | .min(-10) 218 | .max(0) 219 | .not(-5, -3, -1); 220 | 221 | shouldReturnErrors(assert, schema, [-15, -16, 1], { type: ERROR_TYPES.RANGE }); 222 | shouldReturnErrors(assert, schema, [-5, -3, -1], { type: ERROR_TYPES.ARGUMENT }); 223 | shouldReturnErrors(assert, schema, [null, {}, '', NaN], { type: ERROR_TYPES.TYPE }); 224 | }); 225 | 226 | test('.min() .max() .optional() .integer() should not return errors for valid values', assert => { 227 | const schema = number() 228 | .optional() 229 | .min(-30) 230 | .max(30) 231 | .integer(); 232 | const validPrimitiveNumbers = [-30, 15, 0, 1, 5, 30]; 233 | const validNumberObjects = validPrimitiveNumbers.map(n => new Number(n)); 234 | 235 | shouldNotReturnErrors(assert, schema, validPrimitiveNumbers.concat(validNumberObjects)); 236 | }); 237 | -------------------------------------------------------------------------------- /test/tests/date.spec.js: -------------------------------------------------------------------------------- 1 | // import { expect } from 'chai'; 2 | import test from 'ava'; 3 | import { date, ERROR_TYPES } from '../../'; 4 | import { shouldReturnErrors, shouldNotReturnErrors } from '../helpers'; 5 | 6 | const toDate = id => new Date(id); 7 | 8 | test('returns "date"', assert => { 9 | assert.is(date().type, 'date'); 10 | }); 11 | 12 | test('date().validateType() returns "true" for date objects with valid date value', assert => { 13 | const dates = ['01/31/1000', '01/01/1000', '05/16/1994']; 14 | const allAreValid = dates.every(validDate => date().validateType(new Date(validDate))); 15 | 16 | assert.true(allAreValid); 17 | }); 18 | 19 | test('returns "false" for date objects with invalid inner date value', assert => { 20 | const invalidDates = ['date', 'gosho', '13/13/1333', NaN, undefined].map(d => new Date(d)); 21 | const hasSomeValid = invalidDates.some(invDate => date().validateType(invDate)); 22 | 23 | assert.false(hasSomeValid); 24 | }); 25 | 26 | test('date().validate() returns errors when .optional() has NOT been called and values are not date objects or date objects with valid dates', assert => { 27 | const schema = date(); 28 | const notDateObjects = ['hi im string', 123, null, undefined, NaN, {}, [], Date]; 29 | const invalidDates = ['13/13/1333', 'sdfsd', undefined].map(dstr => new Date(dstr)); 30 | 31 | shouldReturnErrors(assert, schema, notDateObjects.concat(invalidDates), { type: ERROR_TYPES.TYPE }); 32 | }); 33 | 34 | test('date().validate() doesn`t return errors when .optional() has been called and values are not date objects or date objects with valid dates', assert => { 35 | const schema = date().optional(); 36 | const notDateObjects = ['hi im string', 123, null, undefined, NaN, {}, [], Date]; 37 | const invalidDates = ['13/13/1333', 'sdfsd', undefined].map(dstr => new Date(dstr)); 38 | 39 | shouldNotReturnErrors(assert, schema, notDateObjects.concat(invalidDates)); 40 | }); 41 | 42 | test('.before() throws TypeError when value that is not of type date object or has invalid date', assert => { 43 | const notDates = ['azsymgosho', new Date('abv'), {}, [], Date]; 44 | 45 | for (const notDate of notDates) { 46 | assert.throws(() => date().before(notDate), TypeError); 47 | } 48 | }); 49 | 50 | test('.before() throws error when called more than once even with valid parameters', assert => { 51 | assert.throws(() => date().before('11/11/2016').before('05/05/2000'), Error); 52 | }); 53 | 54 | test('.before() returns errors for dates after the provided date value', assert => { 55 | const schema = date().before(new Date('05/16/1994')); 56 | const dates = [new Date('05/17/1994'), new Date(1994, 5, 16, 0, 0, 1), new Date('12/12/2017')]; 57 | 58 | shouldReturnErrors(assert, schema, dates, { type: ERROR_TYPES.RANGE }); 59 | }); 60 | 61 | test('.before() doesn`t return errors for dates before or equal to the provided date value when date object is provided', assert => { 62 | const schema = date().before('02/02/2016'); 63 | const dates = [new Date('02/01/2016'), new Date('10/10/1999')]; 64 | 65 | shouldNotReturnErrors(assert, schema, dates); 66 | }); 67 | 68 | test('.before() doesn`t return errors for dates before or equal to the provided date value when string date is provided', assert => { 69 | const schema = date().before('02/02/2016'); 70 | const dates = ['02/01/2016', '10/10/1999'].map(toDate); 71 | 72 | shouldNotReturnErrors(assert, schema, dates); 73 | }); 74 | 75 | test('.after() throws TypeError when value that is not of type date object or has invalid date', assert => { 76 | const notDates = ['azsymgosho', new Date('abv'), {}, [], Date]; 77 | 78 | for (const notDate of notDates) { 79 | assert.throws(() => date().after(notDate), TypeError); 80 | } 81 | }); 82 | 83 | test('.after() throws error when called more than once even with valid parameters', assert => { 84 | assert.throws(() => date().after('11/11/2016').after('05/05/2000'), Error); 85 | }); 86 | 87 | test('.after() returns errors for values before the provided date value', assert => { 88 | const schema = date().after(new Date('11/16/2000')); 89 | const dates = ['10/29/2000', '11/14/2000', '11/17/1999'].map(toDate); 90 | 91 | shouldReturnErrors(assert, schema, dates, { type: ERROR_TYPES.RANGE }); 92 | }); 93 | 94 | test('.after() doesn`t return errors for values after than or equal to the provided date value when date objects are provided', assert => { 95 | const schema = date().after(new Date('01/14/2017')); 96 | const laterDates = ['02/14/2017', '02/15/2017', new Date(2017, 1, 14, 23, 59, 59)].map(toDate); 97 | 98 | shouldNotReturnErrors(assert, schema, laterDates); 99 | }); 100 | 101 | test('.after() doesn`t return errors for values after than or equal to the provided date value when date strings are provided', assert => { 102 | const schema = date().after(new Date('01/14/2017')); 103 | const laterDates = ['02/14/2017', '02/15/2017'].map(toDate); 104 | 105 | shouldNotReturnErrors(assert, schema, laterDates); 106 | }); 107 | 108 | test('.monthBetween() throws TypeError when at least one of the provided arguments is an invalid number', assert => { 109 | const nanPairs = [[1, null], [true, false], ['sdf', {}], ['11', 11]]; 110 | 111 | for (const np of nanPairs) { 112 | assert.throws(() => date().monthBetween(...np), TypeError); 113 | } 114 | }); 115 | 116 | test('.monthBetween() throws when called more than once, even with valid arguments', assert => { 117 | assert.throws(() => date().monthBetween(1, 5).monthBetween(2, 6), Error); 118 | }); 119 | 120 | test('.monthBetween() validation returns errors for values whose month value is not in the provided range when start is LESS than end', assert => { 121 | const schema = date().monthBetween(3, 7); 122 | const dates = ['01/01/1000', '02/02/1000', '03/10/2016', '09/02/2016'].map(toDate); 123 | 124 | shouldReturnErrors(assert, schema, dates, { type: ERROR_TYPES.RANGE }); 125 | }); 126 | 127 | test('.monthBetween() validation returns errors for values whose month value is not in the provided range when start is GREATER than end', assert => { 128 | const schema = date().monthBetween(10, 1); 129 | const dates = [3, 4, 5, 6, 7, 8, 9].map((m, i) => new Date(`0${m}/10/2010`)).concat([new Date('10/12/2017')]); 130 | 131 | shouldReturnErrors(assert, schema, dates, { type: ERROR_TYPES.RANGE }); 132 | }); 133 | 134 | test('.monthBetween() validation doesn`t return errors for months in the provided range when start is LESS than end', assert => { 135 | const schema = date().monthBetween(0, 5); 136 | const dates = [1, 2, 3, 4, 5, 6].map((m, i) => new Date(`0${m}/0${i + 1}/2001`)); 137 | 138 | shouldNotReturnErrors(assert, schema, dates); 139 | }); 140 | 141 | test('.monthBetween() validation doesn`t return errors for values whose month value is in the provided range when start is GREATER than end', assert => { 142 | const schema = date().monthBetween(8, 1); 143 | const dates = ['10', '11', '12', '01'].map(monthStr => new Date(`${monthStr}/25/1898`)); 144 | 145 | shouldNotReturnErrors(assert, schema, dates); 146 | }); 147 | 148 | test('.dateBetween() throws TypeError when at least one of the provided arguments is an invalid number', assert => { 149 | const nanPairs = [[1, null], [true, false], ['sdf', {}], ['11', 11]]; 150 | 151 | for (const np of nanPairs) { 152 | assert.throws(() => date().dateBetween(...np), TypeError); 153 | } 154 | }); 155 | 156 | test('.dateBetween() throws Error when called more than once, even with valid arguments', assert => { 157 | assert.throws(() => date().dateBetween(1, 30).dateBetween(2, 10), Error); 158 | }); 159 | 160 | test('.dateBetween() validation returns errors for dates whose day of month value is not in the provided range when start is LESS than end', assert => { 161 | const schema = date().dateBetween(5, 15); 162 | const dates = [0, 1, 2, 3, 4, 16, 17, 18, 19, 30].map(day => new Date(2017, 3, day)); 163 | 164 | shouldReturnErrors(assert, schema, dates, { type: ERROR_TYPES.RANGE }); 165 | }); 166 | 167 | test('.dateBetween() validation returns errors for dates whose values are not in the provided range when start is GREATER than end', assert => { 168 | const schema = date().dateBetween(15, 5); 169 | const dates = Array.from({ length: 15 - 6 }).map((_, i) => i + 6).map(day => new Date(2010, 5, day)); 170 | 171 | shouldReturnErrors(assert, schema, dates, { type: ERROR_TYPES.RANGE }); 172 | }); 173 | 174 | test('.dateBetween() validation doesn`t return errors with date values with valid days when start is LESS than end', assert => { 175 | const schema = date().dateBetween(20, 30); 176 | const dates = Array.from({ length: 30 - 20 }).map((_, i) => new Date(1777, 2, i + 20)); 177 | 178 | shouldNotReturnErrors(assert, schema, dates); 179 | }); 180 | 181 | test('.dateBetween() validation doesn`t return errors with date values with valid days when start is GREATER than end', assert => { 182 | const schema = date().dateBetween(20, 10); 183 | const dates = [20, 25, 30, 0, 1, 5, 10].map(d => new Date(1999, 10, d)); 184 | 185 | shouldNotReturnErrors(assert, schema, dates); 186 | }); 187 | 188 | test('.hourBetween() throws TypeError when at least one of the provided arguments is an invalid number', assert => { 189 | const nanPairs = [[1, null], [true, false], ['sdf', {}], ['11', 11]]; 190 | 191 | for (const np of nanPairs) { 192 | assert.throws(() => date().hourBetween(...np), TypeError); 193 | } 194 | }); 195 | 196 | test('.hourBetween() throws Error when called more than once, even with valid arguments', assert => { 197 | assert.throws(() => date().hourBetween(1, 23).hourBetween(0, 10), Error); 198 | }); 199 | 200 | test('.hourBetween() validation returns errors for dates whose hour of day value is not in the provided range when start is LESS than end', assert => { 201 | const schema = date().hourBetween(5, 21); 202 | const dates = [0, 1, 2, 3, 4, 22, 23].map(hour => new Date(2017, 3, 3, hour)); 203 | 204 | shouldReturnErrors(assert, schema, dates, { type: ERROR_TYPES.RANGE }); 205 | }); 206 | 207 | test('.hourBetween() validation returns errors for dates whose values are not in the provided range when start is GREATER than end', assert => { 208 | const schema = date().hourBetween(15, 5); 209 | const dates = Array.from({ length: 9 }).map((_, i) => i + 6).map(hour => new Date(2010, 5, 7, hour)); 210 | 211 | shouldReturnErrors(assert, schema, dates, { type: ERROR_TYPES.RANGE }); 212 | }); 213 | 214 | test('.hourBetween() validation doesn`t return errors with date values with valid days when start is LESS than end', assert => { 215 | const schema = date().hourBetween(11, 18); 216 | const dates = Array.from({ length: 6 }).map((_, i) => new Date(1777, 2, 20, i + 12)); 217 | 218 | shouldNotReturnErrors(assert, schema, dates); 219 | }); 220 | 221 | test('.hourBetween() validation doesn`t return errors with date values with valid days when start is GREATER than end', assert => { 222 | const schema = date().hourBetween(20, 10); 223 | const dates = [21, 22, 23, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(hour => new Date(1999, 10, 7, hour)); 224 | 225 | shouldNotReturnErrors(assert, schema, dates); 226 | }); 227 | 228 | test('weekdayBetween() throws TypeError when at least one of the provided arguments is an invalid number', assert => { 229 | const nanPairs = [[1, null], [true, false], ['sdf', {}], ['11', 11]]; 230 | 231 | for (const np of nanPairs) { 232 | assert.throws(() => date().weekdayBetween(...np), TypeError); 233 | } 234 | }); 235 | 236 | test('weekdayBetween() throws Error when called more than once, even with valid arguments', assert => { 237 | assert.throws(() => date().weekdayBetween(1, 4).weekdayBetween(3, 5), Error); 238 | }); 239 | 240 | test('weekdayBetween() validation returns errors for dates whose hour of day value is not in the provided range when start is LESS than end', assert => { 241 | const schema = date().weekdayBetween(3, 6); 242 | const dates = ['04/23/2017', '04/24/2017', '04/25/2017', '04/30/2017', '05/01/2017'].map(d => new Date(d)); 243 | shouldReturnErrors(assert, schema, dates, { type: ERROR_TYPES.RANGE }); 244 | }); 245 | 246 | test('weekdayBetween() validation returns errors for dates whose values are not in the provided range when start is GREATER than end', assert => { 247 | const schema = date().weekdayBetween(4, 1); 248 | const dates = ['05/23/2017', '05/24/2017'].map(d => new Date(d)); 249 | 250 | shouldReturnErrors(assert, schema, dates, { type: ERROR_TYPES.RANGE }); 251 | }); 252 | 253 | test('weekdayBetween() validation doesn`t return errors with date values with valid days when start is LESS than end', assert => { 254 | const schema = date().weekdayBetween(2, 6); 255 | const dates = [26, 27, 28, 29].map(date => new Date(`04/${date}/2017`)); 256 | 257 | shouldNotReturnErrors(assert, schema, dates); 258 | }); 259 | 260 | test('weekdayBetween() validation doesn`t return errors with date values with valid days when start is GREATER than end', assert => { 261 | const schema = date().weekdayBetween(5, 2); 262 | const dates = [20, 21, 22].map(date => new Date(`05/${date}/2017`)); 263 | 264 | shouldNotReturnErrors(assert, schema, dates); 265 | }); 266 | 267 | test('minutesBetween() throws TypeError when at least one of the provided arguments is an invalid number', assert => { 268 | const nanPairs = [[1, null], [true, false], ['sdf', {}], ['11', 11]]; 269 | 270 | for (const np of nanPairs) { 271 | assert.throws(() => date().minutesBetween(...np), TypeError); 272 | } 273 | }); 274 | 275 | test('minutesBetween() throws Error when called more than once, even with valid arguments', assert => { 276 | assert.throws(() => date().minutesBetween(5, 20).minutesBetween(30, 50), Error); 277 | }); 278 | 279 | test('minutesBetween() validation returns errors for dates whose minute value is not in the provided range when start is LESS than end', assert => { 280 | const schema = date().minutesBetween(33, 40); 281 | const dates = [new Date(2017, 1, 1, 1, 23), new Date(1999, 11, 11, 11, 1), new Date(2020, 11, 11, 1, 50)]; 282 | shouldReturnErrors(assert, schema, dates, { type: ERROR_TYPES.RANGE }); 283 | }); 284 | 285 | test('minutesBetween() validation returns errors for dates whose minute values are not in the provided range when start is GREATER than end', assert => { 286 | const schema = date().minutesBetween(20, 10); 287 | const dates = [new Date(2017, 1, 1, 1, 11), new Date(1999, 11, 11, 11, 15), new Date(2020, 11, 11, 1, 19)].map(d => new Date(d)); 288 | 289 | shouldReturnErrors(assert, schema, dates, { type: ERROR_TYPES.RANGE }); 290 | }); 291 | 292 | test('minutesBetween() validation doesn`t return errors with date values with valid days when start is LESS than end', assert => { 293 | const schema = date().minutesBetween(0, 20); 294 | const dates = [10, 11, 15, 3, 6, 2, 1].map(minutes => new Date(2017, 4, 5, 6, minutes)); 295 | 296 | shouldNotReturnErrors(assert, schema, dates); 297 | }); 298 | 299 | test('minutesBetween() validation doesn`t return errors with date values with valid minutes when start is GREATER than end', assert => { 300 | const schema = date().minutesBetween(55, 30); 301 | const dates = [10, 15, 0, 58].map(minutes => new Date(2017, 4, 5, 6, minutes)); 302 | 303 | shouldNotReturnErrors(assert, schema, dates); 304 | }); 305 | 306 | test('secondsBetween() throws TypeError when at least one of the provided arguments is an invalid number', assert => { 307 | const nanPairs = [[1, null], [true, false], ['sdf', {}], ['11', 11]]; 308 | 309 | for (const np of nanPairs) { 310 | assert.throws(() => date().secondsBetween(...np), TypeError); 311 | } 312 | }); 313 | 314 | test('secondsBetween() throws Error when called more than once, even with valid arguments', assert => { 315 | assert.throws(() => date().secondsBetween(5, 20).secondsBetween(30, 50), Error); 316 | }); 317 | 318 | test('secondsBetween() validation returns errors for dates whose seconds value is not in the provided range when start is LESS than end', assert => { 319 | const schema = date().secondsBetween(33, 40); 320 | const dates = [new Date(2017, 1, 1, 1, 1, 23), new Date(1999, 11, 11, 11, 11, 1), new Date(2020, 11, 11, 1, 11, 50)]; 321 | shouldReturnErrors(assert, schema, dates, { type: ERROR_TYPES.RANGE }); 322 | }); 323 | 324 | test('secondsBetween() validation returns errors for dates whose seconds value are not in the provided range when start is GREATER than end', assert => { 325 | const schema = date().secondsBetween(20, 10); 326 | const dates = [new Date(2017, 1, 1, 1, 1, 11), new Date(1999, 11, 11, 11, 1, 15), new Date(2020, 11, 11, 1, 1, 19)].map(d => new Date(d)); 327 | 328 | shouldReturnErrors(assert, schema, dates, { type: ERROR_TYPES.RANGE }); 329 | }); 330 | 331 | test('secondsBetween() validation doesn`t return errors with date values with valid days when start is LESS than end', assert => { 332 | const schema = date().secondsBetween(0, 20); 333 | const dates = [10, 11, 15, 3, 6, 2, 1].map(seconds => new Date(2017, 4, 5, 6, 1, seconds)); 334 | 335 | shouldNotReturnErrors(assert, schema, dates); 336 | }); 337 | 338 | test('secondsBetween() validation doesn`t return errors with date values with valid seconds when start is GREATER than end', assert => { 339 | const schema = date().secondsBetween(55, 30); 340 | const dates = [10, 15, 0, 58].map(seconds => new Date(2017, 4, 5, 6, 1, seconds)); 341 | 342 | shouldNotReturnErrors(assert, schema, dates); 343 | }); 344 | 345 | --------------------------------------------------------------------------------