├── .babelrc ├── .gitignore ├── README.org ├── config ├── jest.config.js └── test.setup.js ├── develop.sh ├── images └── Overgear.png ├── jest.config.js ├── package.json ├── source ├── ast.generator.js ├── custom.validators.js ├── index.js └── tests │ └── converter.test.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "node8", 10 | "stage-0" 11 | ], 12 | "env": { 13 | "production": { 14 | "only": ["app"], 15 | "plugins": [ 16 | "transform-react-constant-elements", 17 | "transform-react-inline-elements" 18 | ] 19 | }, 20 | "test": { 21 | "plugins": ["transform-es2015-modules-commonjs", "dynamic-import-node"] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don't check auto-generated stuff into git 2 | coverage 3 | build 4 | node_modules 5 | stats.json 6 | 7 | # Cruft 8 | .DS_Store 9 | npm-debug.log 10 | .idea 11 | 12 | # Logs 13 | yarn-error.log 14 | dist -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | [[./images/Overgear.png]] 2 | 3 | * YUP AST to validatior generator 4 | 5 | We wanted to create a system where validations did not have to be statically compiled into the frontend sources, but rather 6 | served from the backend database, and usable by both the backend and the frontend simultaneously. 7 | 8 | To do this we created a simple [[https://en.wikipedia.org/wiki/Abstract_syntax_tree][AST]] model using [[https://github.com/jquense/yup][yup]] for validation. 9 | 10 | For many examples of the AST we can look into the [[./source/tests/converter.test.js][tests]] file, where different use cases are defined. 11 | 12 | * Installing 13 | 14 | With NPM: 15 | #+BEGIN_SRC bash 16 | npm install @overgear/yup-ast 17 | #+END_SRC 18 | 19 | With yarn: 20 | #+BEGIN_SRC bash 21 | yarn add @overgear/yup-ast 22 | #+END_SRC 23 | 24 | * Schema 25 | 26 | The schema is defined as follows: 27 | 28 | A simple array with a string name is seen as a prefix notational function lookup. 29 | 30 | #+BEGIN_SRC json 31 | ["yup.object"] 32 | #+END_SRC 33 | 34 | Is seen as a call to the function 35 | #+BEGIN_SRC javascript 36 | yup.object() 37 | #+END_SRC 38 | 39 | Functions can be chained together by surrounding them by an array: 40 | 41 | #+BEGIN_SRC json 42 | [ 43 | ["yup.object"], 44 | ["yup.required"], 45 | ] 46 | #+END_SRC 47 | 48 | Becomes 49 | #+BEGIN_SRC javascript 50 | yup 51 | .object() 52 | .required() 53 | #+END_SRC 54 | 55 | Anything else in the array after the prefix function is treated as an argument to be passed to that function: 56 | 57 | #+BEGIN_SRC json 58 | [ 59 | ["yup.number"], 60 | ["yup.required"], 61 | ["yup.min", 50], 62 | ["yup.max", 500], 63 | 64 | ] 65 | #+END_SRC 66 | Becomes 67 | #+BEGIN_SRC javascript 68 | yup 69 | .object() 70 | .required() 71 | .min(50) 72 | .max(500) 73 | #+END_SRC 74 | 75 | (Which validates that a number is required, greater than 50 and less than 500. 76 | 77 | Multiple arguments can be passed to functions 78 | 79 | #+BEGIN_SRC json 80 | [ 81 | ["yup.number"], 82 | ["yup.required"], 83 | ["yup.min", 50, "This is the error for failing this validation"], 84 | ["yup.max", 500, "Number should be less than 500"], 85 | ] 86 | #+END_SRC 87 | 88 | Becomes 89 | #+BEGIN_SRC javascript 90 | yup 91 | .object() 92 | .required() 93 | .min(50, "This is the error for failing this validation") 94 | .max(500, "Number should be less than 500") 95 | #+END_SRC 96 | 97 | and additional yup validators 98 | 99 | #+BEGIN_SRC json 100 | [ 101 | ["yup.object"], 102 | ["yup.required"], 103 | [ 104 | "yup.shape", 105 | { 106 | "game": [["yup.string"], ["yup.required", "wizard.validations.is_required"]], 107 | "locale": [["yup.string"], ["yup.required", "wizard.validations.is_required"]], 108 | "category": [["yup.string"], ["yup.required", "wizard.validations.is_required"]], 109 | "subcategory": [["yup.string"], ["yup.required", "wizard.validations.is_required"]], 110 | }, 111 | ], 112 | ] 113 | #+END_SRC 114 | 115 | Becomes 116 | #+BEGIN_SRC javascript 117 | yup 118 | .object() 119 | .required() 120 | .shape({ 121 | "game": yup.string().required("wizard.validations.is_required"), 122 | "locale": yup.string().required("wizard.validations.is_required"), 123 | "category": yup.string().required("wizard.validations.is_required"), 124 | "subcategory": yup.string().required("wizard.validations.is_required"), 125 | }) 126 | #+END_SRC 127 | 128 | * Custom validators 129 | ** ~addCustomValidator(name, validator, binding = false)~ 130 | Adds a custom validator by name: 131 | #+BEGIN_SRC javascript 132 | addCustomValidator('myCustomValidator', yup.number().min(50).max(500)) 133 | #+END_SRC 134 | Which creates a custom validator, to be used as: 135 | #+BEGIN_SRC json 136 | [ 137 | ["myCustomValidator"] 138 | ] 139 | #+END_SRC 140 | ** ~getCustomValidator(name)~ 141 | Gets the value of a custom validator 142 | ** ~delCustomValidator(name)~ 143 | Removes a validator which the user has added 144 | 145 | * Generated validator 146 | 147 | Since the result of a call to transformAll is a yup validator, please see the [[https://github.com/jquense/yup][yup documentation]] for more information about features available. 148 | -------------------------------------------------------------------------------- /config/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverageFrom: [ 3 | 'sources/**/*.{js,jsx}', 4 | '!sources/**/*.test.{js,jsx}', 5 | // prettier-no-wrap 6 | ], 7 | coverageThreshold: { 8 | global: { 9 | statements: 98, 10 | branches: 91, 11 | functions: 98, 12 | lines: 98, 13 | }, 14 | }, 15 | coverageReporters: ['json', 'lcov', 'text-summary'], 16 | moduleDirectories: ['node_modules', 'app'], 17 | setupTestFrameworkScriptFile: '/config/test.setup.js', 18 | testRegex: 'tests/.*\\.test\\.js$', 19 | }; 20 | -------------------------------------------------------------------------------- /config/test.setup.js: -------------------------------------------------------------------------------- 1 | // needed for regenerator-runtime 2 | // (ES7 generator support is required by redux-saga) 3 | import 'babel-polyfill'; 4 | -------------------------------------------------------------------------------- /develop.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | if [ $# -eq 0 ]; 4 | then 5 | echo "--------------------------------------------------" 6 | echo 7 | echo "Project directory required, use:" 8 | echo 9 | echo " $0 ../og-frontend" 10 | echo 11 | echo "--------------------------------------------------" 12 | exit 1 13 | fi 14 | 15 | PROJECT_DIRECTORY=$1 16 | 17 | nodemon --watch source/ --exec "yarn build:dev:once && cp dist/index.js $PROJECT_DIRECTORY/node_modules/\@overgear/yup-ast/" 18 | -------------------------------------------------------------------------------- /images/Overgear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WASD-Team/yup-ast/aa6a83307c7611dfe2959912b4699830f4b8f727/images/Overgear.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./config/jest.config'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.2", 3 | "main": "index.js", 4 | "author": "will@wasd.team", 5 | "module": "index.js", 6 | "name": "@overgear/yup-ast", 7 | "description": "Yup AST generator", 8 | "repository": "git@github.com:WASD-Team/yup-ast.git", 9 | "scripts": { 10 | "develop": "./develop.sh", 11 | "build": "NODE_ENV=production webpack --progress --config webpack.config.js; yarn version; cp package.json dist", 12 | "build:dev:once": "NODE_ENV=development webpack --progress --config webpack.config.js", 13 | "build:dev": "NODE_ENV=development webpack -w --progress --config webpack.config.js", 14 | "test": "cross-env NODE_ENV=test jest --coverage", 15 | "test:watch": "cross-env NODE_ENV=test jest --watchAll --bail", 16 | "test:inspect": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand --watchAll", 17 | "precommit": "lint-staged", 18 | "lint-js": "eslint --ignore-path .gitignore --ignore-pattern \"!**/.*\" .", 19 | "lint-css": "stylelint \"src/**/*.{css,less,styl,scss,sass,sss}\"", 20 | "lint": "yarn run lint-js && yarn run lint-css", 21 | "fix-js": "yarn run lint-js --fix", 22 | "fix-css": "yarn run lint-css --fix", 23 | "fix": "yarn run fix-js && yarn run fix-css" 24 | }, 25 | "lint-staged": { 26 | "ignore": [ 27 | "package.json" 28 | ], 29 | "linters": { 30 | "*.{js,jsx}": [ 31 | "eslint --no-ignore --fix", 32 | "git add --force" 33 | ], 34 | "*.{json,md}": [ 35 | "prettier --write", 36 | "git add --force" 37 | ], 38 | "*.{css,less,styl,scss,sass,sss}": [ 39 | "stylelint --fix", 40 | "git add --force" 41 | ] 42 | } 43 | }, 44 | "dependencies": { 45 | "babel-core": "^6.26.3", 46 | "babel-loader": "^7.1.5", 47 | "babel-plugin-add-module-exports": "^0.2.1", 48 | "babel-plugin-transform-runtime": "^6.23.0", 49 | "babel-plugin-webpack-alias": "^2.1.2", 50 | "babel-polyfill": "^6.26.0", 51 | "babel-preset-env": "^1.7.0", 52 | "babel-preset-es2015": "^6.24.1", 53 | "babel-preset-node8": "^1.2.0", 54 | "babel-preset-react": "^6.24.1", 55 | "babel-preset-stage-2": "^6.24.1", 56 | "flat": "^4.1.0", 57 | "idempotent-babel-polyfill": "^6.26.0", 58 | "immutable": "^4.0.0-rc.9", 59 | "lodash": "^4.17.10", 60 | "md5-jkmyers": "^0.0.1", 61 | "nano-equal": "^2.0.2", 62 | "nanoid": "^1.2.3", 63 | "object-hash": "^1.3.0", 64 | "ramda": "^0.25.0", 65 | "reselect": "^3.0.1", 66 | "uglifyjs-webpack-plugin": "^1.2.7", 67 | "webpack": "^4.16.2", 68 | "yup": "^0.26.6" 69 | }, 70 | "devDependencies": { 71 | "babel-eslint": "^8.2.6", 72 | "babel-plugin-dynamic-import-node": "^2.2.0", 73 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", 74 | "babel-plugin-transform-react-constant-elements": "^6.23.0", 75 | "babel-plugin-transform-react-inline-elements": "^6.22.0", 76 | "babel-preset-stage-0": "^6.24.1", 77 | "cross-env": "^5.2.0", 78 | "eslint": "^5.2.0", 79 | "eslint-config-airbnb": "^17.0.0", 80 | "eslint-config-with-prettier": "^4.2.0", 81 | "eslint-import-resolver-webpack": "^0.10.1", 82 | "eslint-plugin-css-modules": "^2.7.5", 83 | "eslint-plugin-flowtype": "^2.50.0", 84 | "eslint-plugin-import": "^2.13.0", 85 | "eslint-plugin-jsx-a11y": "^6.1.1", 86 | "eslint-plugin-prettier": "^2.6.2", 87 | "ignore-styles": "^5.0.1", 88 | "jest": "^23.6.0", 89 | "jsdom": "^11.11.0", 90 | "jsdom-global": "^3.0.2", 91 | "lint-staged": "^7.2.0", 92 | "prop-types": "^15.6.2", 93 | "webpack-command": "^0.4.1" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /source/ast.generator.js: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | 3 | import { getCustomValidator } from './custom.validators'; 4 | 5 | let DEBUG = false; 6 | 7 | class ValidationError { 8 | constructor(message) { 9 | this.message = message; 10 | } 11 | } 12 | 13 | /** 14 | * Allows setting of debugger for certain tests. 15 | * @param {boolean} newValue - True or false to set the DEBUG var. 16 | */ 17 | export function setDebug(newValue) { 18 | DEBUG = newValue; 19 | } 20 | 21 | /** 22 | * Searches for {substring} in {string}. 23 | * If found, returns the {string}, sliced after substring. 24 | * 25 | * @param {string} string - String to be sliced. 26 | * @param {string} substring - String to search for. 27 | * @returns {string|null} Null if no match found. 28 | */ 29 | function getSubString(string, substring) { 30 | if (!string) return null; 31 | 32 | const testedIndex = string.indexOf(substring); 33 | 34 | if (testedIndex > -1) { 35 | return string.slice(testedIndex + substring.length); 36 | } 37 | 38 | return null; 39 | } 40 | 41 | /** 42 | * Returns a function from yup, by passing in a function name from our schema. 43 | * @param {string} functionName - The string to search for a function. 44 | * @param {object} previousInstance - Object from previous validator result or yup itself. 45 | * @returns {function} Either the found yup function or the default validator. 46 | */ 47 | function getYupFunction(functionName, previousInstance = yup) { 48 | // Make sure we're dealing with a string 49 | if (functionName instanceof Array) { 50 | functionName = functionName[0]; 51 | } 52 | 53 | // Attempt to retrieve any custom validators first 54 | const customValidator = getCustomValidator(functionName); 55 | if (customValidator) { 56 | return customValidator; 57 | } 58 | 59 | const yupName = getSubString(functionName, 'yup.'); 60 | if (yupName && previousInstance[yupName] instanceof Function) { 61 | return previousInstance[yupName].bind(previousInstance); 62 | } 63 | 64 | if (yupName && yup[yupName] instanceof Function) { 65 | return yup[yupName].bind(yup); 66 | } 67 | 68 | throw new ValidationError('Could not find validator ' + functionName); 69 | } 70 | 71 | /** 72 | * Here we check to see if a passed array could be a prefix notation function. 73 | * @param {array} item - Item to be checked. 74 | * @param {any} item.functionName - We'll check this, and perhaps it's a prefix function name. 75 | * @returns {boolean} True if we are actually looking at prefix notation. 76 | */ 77 | function isPrefixNotation([functionName]) { 78 | if (functionName instanceof Array) { 79 | if (isPrefixNotation(functionName)) return true; 80 | } 81 | 82 | if (typeof functionName !== 'string') return false; 83 | if (functionName.indexOf('yup.') < 0) return false; 84 | 85 | return true; 86 | } 87 | 88 | /** 89 | * Ensure that the argument passed is an array. 90 | * @param {string|array} maybeArray - To be checked. 91 | * @returns {array} forced to array. 92 | */ 93 | function ensureArray(maybeArray) { 94 | if (maybeArray instanceof Array === false) { 95 | return [maybeArray]; 96 | } 97 | 98 | return maybeArray; 99 | } 100 | 101 | /** 102 | * Converts an array of ['yup.number'] to yup.number(). 103 | * @param {[Any]} jsonArray - The validation array. 104 | * @param {object} previousInstance - The result of a call to yup.number() 105 | * i.e. an object schema validation set 106 | * @returns {function} generated yup validator 107 | */ 108 | function convertArray(arrayArgument, previousInstance = yup) { 109 | const [functionName, ...argsToPass] = ensureArray(arrayArgument); 110 | 111 | // Handle the case when we have a previous instance 112 | // but we don't want to use it for this transformation 113 | // [['yup.array'], ['yup.of', [['yup.object'], ['yup.shape'] ...]]] 114 | if (functionName instanceof Array) { 115 | return transformArray(arrayArgument); 116 | } 117 | 118 | const gotFunc = getYupFunction(functionName, previousInstance, arrayArgument); 119 | 120 | // Here we'll actually call the function 121 | // This might be something like yup.number().min(5) 122 | // We could be passing different types of arguments here 123 | // so we'll try to transform them before calling the function 124 | // yup.object().shape({ test: yup.string()}) should also be transformed 125 | const convertedArguments = transformAll(argsToPass, previousInstance); 126 | 127 | // Handle the case when we've got an array of empty elements 128 | if (convertedArguments instanceof Array) { 129 | if (convertedArguments.filter(i => i).length < 1) { 130 | return gotFunc(); 131 | } 132 | 133 | // Spread the array over the function 134 | return gotFunc(...convertedArguments); 135 | } 136 | 137 | // Handle the case when we're passing another validator 138 | return gotFunc(convertedArguments); 139 | } 140 | 141 | /** 142 | * Transforms an array JSON schema to yup array schema. 143 | * 144 | * @param {array} jsonArray - array in JSON to be transformed. 145 | * @returns {array} Array with same keys, but values as yup validators. 146 | */ 147 | function transformArray(jsonArray, previousInstance = yup) { 148 | let toReturn = convertArray(jsonArray[0]); 149 | 150 | jsonArray.slice(1).forEach(item => { 151 | // Found an array, move to prefix extraction 152 | if (item instanceof Array) { 153 | toReturn = convertArray(item, toReturn); 154 | return; 155 | } 156 | 157 | // Found an object, move to object extraction 158 | if (item instanceof Object) { 159 | toReturn = transformObject(item, previousInstance); 160 | return; 161 | } 162 | 163 | // Handle an edge case where we have something like 164 | // [['yup.ref', 'linkName'], 'custom error'], and we don't want 165 | // to consume 'custom error as a variable yet' 166 | if (toReturn instanceof Array) { 167 | toReturn = toReturn.concat(item); 168 | return; 169 | } 170 | 171 | toReturn = [toReturn, item]; 172 | }); 173 | 174 | return toReturn; 175 | } 176 | 177 | /** 178 | * Transforms an object JSON schema to yup object schema. 179 | * 180 | * @param {object} jsonObject - Object in JSON to be transformed. 181 | * @returns {object} Object with same keys, but values as yup validators. 182 | */ 183 | export function transformObject(jsonObject, previousInstance = yup) { 184 | const toReturn = {}; 185 | 186 | Object.entries(jsonObject).forEach(([key, value]) => { 187 | // Found an array move to array extraction 188 | if (value instanceof Array) { 189 | toReturn[key] = transformArray(value, previousInstance); 190 | return; 191 | } 192 | 193 | // Found an object recursive extraction 194 | if (value instanceof Object) { 195 | toReturn[key] = transformObject(value, previousInstance); 196 | return; 197 | } 198 | 199 | toReturn[key] = value; 200 | }); 201 | 202 | return toReturn; 203 | } 204 | 205 | /** 206 | * Steps into arrays and objects and resolve the items inside to yup validators. 207 | * @param {object|array} jsonObjectOrArray - Object to be transformed. 208 | * @returns {yup.Validator} 209 | */ 210 | export function transformAll(jsonObjectOrArray, previousInstance = yup) { 211 | // We're dealing with an array 212 | // This could be a prefix notation function 213 | // If so, we'll call the converter 214 | if (jsonObjectOrArray instanceof Array) { 215 | if (isPrefixNotation(jsonObjectOrArray)) { 216 | return transformArray(jsonObjectOrArray, previousInstance); 217 | } 218 | 219 | return jsonObjectOrArray.map(i => transformAll(i, previousInstance)); 220 | } 221 | 222 | // If we're dealing with an object 223 | // we should check each of the values for that object. 224 | // Some of them may also be prefix notation functiosn 225 | if (jsonObjectOrArray instanceof Object) { 226 | return transformObject(jsonObjectOrArray, previousInstance); 227 | } 228 | 229 | // No case here, just return anything else 230 | return jsonObjectOrArray; 231 | } 232 | 233 | /** 234 | * Can transform arrays or an object into a single validator. 235 | * This should be your initial entrypoint. 236 | * 237 | * @param {object|array} jsonObjectOrArray - Object to be transformed. 238 | * @returns {yup.Validator} 239 | */ 240 | export function transform(jsonObjectOrArray) { 241 | try { 242 | if (jsonObjectOrArray instanceof Object) { 243 | return transformAll([ 244 | // build a custom validator which takes an object as parameter 245 | // If we don't do this, we'll get back an object of validators 246 | ['yup.object'], 247 | ['yup.required'], 248 | ['yup.shape', jsonObjectOrArray], 249 | ]); 250 | } 251 | 252 | // No case here, just return anything else 253 | return transformAll(jsonObjectOrArray); 254 | } catch (error) { 255 | if (error instanceof ValidationError) { 256 | throw new Error( 257 | 'Could not validate ' + 258 | JSON.stringify(jsonObjectOrArray, null, 4) + 259 | `\n${error.message}`, 260 | ); 261 | } 262 | 263 | throw error; 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /source/custom.validators.js: -------------------------------------------------------------------------------- 1 | import { object as yupObject } from 'yup'; 2 | 3 | const CUSTOM_VALIDATORS = {}; 4 | 5 | export function addCustomValidator(name, validator, binding = false) { 6 | if (binding !== false) { 7 | validator = validator.bind(binding); 8 | } 9 | 10 | CUSTOM_VALIDATORS[name] = validator; 11 | } 12 | 13 | export function delCustomValidator(name) { 14 | delete CUSTOM_VALIDATORS[name]; 15 | } 16 | 17 | export function getCustomValidator(name) { 18 | return CUSTOM_VALIDATORS[name]; 19 | } 20 | 21 | // Handle the case when we have an array of objects 22 | // but the previous instance of yup.shape is the yup.array 23 | addCustomValidator('yup.shape', yupObject().shape, yupObject()); 24 | -------------------------------------------------------------------------------- /source/index.js: -------------------------------------------------------------------------------- 1 | // Make yup available 2 | import * as yupImport from 'yup'; 3 | export const yup = yupImport; 4 | 5 | export { 6 | // Allows user to create their own custom validation sets 7 | addCustomValidator, 8 | getCustomValidator, 9 | delCustomValidator, 10 | } from './custom.validators'; 11 | 12 | export { 13 | // Allows the user to parse JSON AST to Yup 14 | setDebug, 15 | transform, 16 | transformAll, 17 | transformObject, 18 | } from './ast.generator'; 19 | -------------------------------------------------------------------------------- /source/tests/converter.test.js: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | import yupPrinter from 'yup/lib/util/printValue'; 3 | 4 | import { transformAll, transformObject, setDebug } from '..'; 5 | 6 | describe('correctly walks a schema object', () => { 7 | it('walks arrays', () => { 8 | const result = transformAll([['yup.object'], ['yup.shape']]); 9 | 10 | expect(result).toBeInstanceOf(yup.object); 11 | expect(result.isValidSync({})).toEqual(true); 12 | expect(result.isValidSync(5)).toEqual(false); 13 | }); 14 | 15 | it('walks arrays with objects', () => { 16 | const result = transformAll([ 17 | ['yup.object'], 18 | ['yup.shape', { test: [['yup.number'], ['yup.required'], ['yup.max', 500]] }], 19 | ['yup.required'], 20 | ]); 21 | 22 | expect(result.isValidSync({})).toEqual(false); 23 | expect(result.isValidSync({ test: 5 })).toEqual(true); 24 | expect(result.isValidSync({ test: 501 })).toEqual(false); 25 | }); 26 | 27 | it('walks arrays with objects containing multiple items', () => { 28 | const result = transformAll([ 29 | ['yup.object'], 30 | [ 31 | 'yup.shape', 32 | { 33 | test: [['yup.number'], ['yup.required'], ['yup.max', 500]], 34 | name: [['yup.string'], ['yup.required'], ['yup.min', 4], ['yup.max', 12]], 35 | }, 36 | ], 37 | ['yup.required'], 38 | ]); 39 | 40 | expect(result.isValidSync({})).toEqual(false); 41 | expect(result.isValidSync({ test: 5 })).toEqual(false); 42 | expect(result.isValidSync({ test: 501 })).toEqual(false); 43 | 44 | expect(result.isValidSync({ test: 500, name: '1234' })).toEqual(true); 45 | expect(result.isValidSync({ test: 500, name: '123' })).toEqual(false); 46 | expect(result.isValidSync({ test: 500, name: '123456789012' })).toEqual(true); 47 | expect(result.isValidSync({ test: 500, name: '1234567890123' })).toEqual(false); 48 | }); 49 | }); 50 | 51 | const numberTests = [ 52 | { 53 | name: 'Converts simple yup type (Number)', 54 | input: [['yup.number']], 55 | output: yup.number(), 56 | validates: { 57 | // prettier-no-wrap 58 | success: [1], 59 | failure: ['A'], 60 | }, 61 | }, 62 | { 63 | name: 'Converts simple type with required (Number)', 64 | input: [['yup.number'], ['yup.required']], 65 | output: yup.number().required(), 66 | validates: { 67 | // prettier-no-wrap 68 | success: [1], 69 | failure: [], 70 | }, 71 | }, 72 | { 73 | name: 'Converts simple type with required chains (Number) (1/2)', 74 | input: [['yup.number'], ['yup.required'], ['yup.min', 50]], 75 | validates: { 76 | // prettier-no-wrap 77 | success: [50], 78 | failure: [], 79 | }, 80 | }, 81 | { 82 | name: 'Converts simple type with required chains (Number) (2/2)', 83 | input: [['yup.number'], ['yup.required'], ['yup.min', 50], ['yup.max', 500]], 84 | validates: { 85 | // prettier-no-wrap 86 | success: [50, 51, 499, 500], 87 | failure: [1, 2, 3, 4, 501, 502, 503], 88 | }, 89 | }, 90 | ]; 91 | 92 | const stringTests = [ 93 | { 94 | name: 'Converts simple yup type (String)', 95 | input: [['yup.string']], 96 | validates: { 97 | // prettier-no-wrap 98 | success: ['A', 'ABC', 'ABC123', 1], 99 | failure: [null], 100 | }, 101 | }, 102 | { 103 | name: 'Converts simple type with required (String)', 104 | input: [['yup.string'], ['yup.required']], 105 | validates: { 106 | // prettier-no-wrap 107 | success: ['A', 'ABC', 'ABC123'], 108 | failure: [null], 109 | }, 110 | }, 111 | { 112 | name: 'Converts simple type with required chains (String) (1/2)', 113 | input: [['yup.string'], ['yup.required'], ['yup.min', 10]], 114 | validates: { 115 | // prettier-no-wrap 116 | success: ['1234567890', '123456789000000'], 117 | failure: ['123', 'abc'], 118 | }, 119 | }, 120 | { 121 | name: 'Converts simple type with required chains (String) (2/2)', 122 | input: [['yup.string'], ['yup.required'], ['yup.min', 10], ['yup.max', 12]], 123 | validates: { 124 | // prettier-no-wrap 125 | success: ['1234567890', '12345678901', '123456789012'], 126 | failure: ['1234567890123', '12345678901234'], 127 | }, 128 | }, 129 | ]; 130 | 131 | const objectTests = [ 132 | { 133 | name: 'Creates a simple object', 134 | input: [['yup.object']], 135 | validates: { 136 | // prettier-no-wrap 137 | success: [{}], 138 | failure: ['123', 'abc'], 139 | }, 140 | }, 141 | { 142 | name: 'Creates a simple object shape', 143 | input: [['yup.object'], ['yup.shape', {}]], 144 | validates: { 145 | // prettier-no-wrap 146 | success: [{}], 147 | failure: ['123', 'abc'], 148 | }, 149 | }, 150 | { 151 | name: 'Creates a nested object shape', 152 | input: [ 153 | ['yup.object'], 154 | ['yup.shape', { test: [['yup.object'], ['yup.shape', {}], ['yup.required']] }], 155 | ], 156 | validates: { 157 | // prettier-no-wrap 158 | success: [{ test: {} }], 159 | failure: [{}, { test: [['yup.object'], ['yup.shape', {}]] }], 160 | }, 161 | }, 162 | { 163 | name: 'Allows non-required object fields', 164 | input: [['yup.object'], ['yup.shape', { test: [['yup.object'], ['yup.shape', {}]] }]], 165 | validates: { 166 | // prettier-no-wrap 167 | success: [{ test: {} }, {}], 168 | failure: [{ test: [['yup.object'], ['yup.shape', {}]] }], 169 | }, 170 | }, 171 | ]; 172 | 173 | describe('correctly transforms data from JSON to YUP', () => { 174 | describe('number tests', () => { 175 | numberTests.forEach(({ name, input, validates: { success = [], failure = [] } }) => { 176 | it(name, () => { 177 | const generatedValidator = transformAll(input); 178 | 179 | success.forEach(item => expect(generatedValidator.isValidSync(item)).toEqual(true)); 180 | failure.forEach(item => 181 | expect(generatedValidator.isValidSync(item)).toEqual(false), 182 | ); 183 | }); 184 | }); 185 | }); 186 | 187 | describe('string tests', () => { 188 | stringTests.forEach(({ name, input, validates: { success = [], failure = [] } }) => { 189 | it(name, () => { 190 | const generatedValidator = transformAll(input); 191 | 192 | success.forEach(item => expect(generatedValidator.isValidSync(item)).toEqual(true)); 193 | failure.forEach(item => 194 | expect(generatedValidator.isValidSync(item)).toEqual(false), 195 | ); 196 | }); 197 | }); 198 | }); 199 | 200 | describe('object tests', () => { 201 | objectTests.forEach(({ name, input, validates: { success = [], failure = [] } }) => { 202 | it(name, () => { 203 | const generatedValidator = transformAll(input); 204 | 205 | success.forEach(item => expect(generatedValidator.isValidSync(item)).toEqual(true)); 206 | failure.forEach(item => 207 | expect(generatedValidator.isValidSync(item)).toEqual(false), 208 | ); 209 | }); 210 | }); 211 | }); 212 | }); 213 | 214 | describe('transform object', () => { 215 | it('correctly transforms a basic object', () => { 216 | const validator = transformObject({}); 217 | 218 | Object.values(validator).forEach(validator => { 219 | expect(validator.isValidSync({})).toEqual(true); 220 | expect(validator.isValidSync(5)).toEqual(false); 221 | expect(validator.isValidSync('A')).toEqual(false); 222 | expect(validator.isValidSync(false)).toEqual(false); 223 | expect(validator.isValidSync(new Map())).toEqual(true); 224 | }); 225 | }); 226 | 227 | it('correctly transforms a more complex object', () => { 228 | const validator = transformObject({ 229 | test: [ 230 | // prettier-no-wrap 231 | ['yup.number'], 232 | ['yup.required'], 233 | ['yup.min', 20], 234 | ['yup.max', 50], 235 | ], 236 | }); 237 | 238 | Object.values(validator).forEach(validator => { 239 | expect(validator.isValidSync({})).toEqual(false); 240 | expect(validator.isValidSync('1')).toEqual(false); 241 | expect(validator.isValidSync('A')).toEqual(false); 242 | expect(validator.isValidSync(null)).toEqual(false); 243 | expect(validator.isValidSync(20)).toEqual(true); 244 | expect(validator.isValidSync(50)).toEqual(true); 245 | expect(validator.isValidSync(51)).toEqual(false); 246 | expect(validator.isValidSync(19)).toEqual(false); 247 | }); 248 | }); 249 | 250 | it('correctly transforms an object inside another object', () => { 251 | const validator = transformObject({ 252 | test: [ 253 | // prettier-no-wrap 254 | ['yup.object'], 255 | [ 256 | 'yup.shape', 257 | { 258 | number: [ 259 | // prettier-no-wrap 260 | ['yup.number'], 261 | ['yup.required'], 262 | ['yup.min', 20], 263 | ['yup.max', 50], 264 | ], 265 | }, 266 | ], 267 | ['yup.required'], 268 | ], 269 | }); 270 | 271 | Object.values(validator).forEach(validator => { 272 | expect(validator.isValidSync({ number: {} })).toEqual(false); 273 | expect(validator.isValidSync({ number: '1' })).toEqual(false); 274 | expect(validator.isValidSync({ number: 'A' })).toEqual(false); 275 | expect(validator.isValidSync({ number: null })).toEqual(false); 276 | expect(validator.isValidSync({ number: 20 })).toEqual(true); 277 | expect(validator.isValidSync({ number: 50 })).toEqual(true); 278 | expect(validator.isValidSync({ number: 51 })).toEqual(false); 279 | expect(validator.isValidSync({ number: 19 })).toEqual(false); 280 | }); 281 | }); 282 | 283 | it('handles more complex object schema', () => { 284 | const validator = transformAll([ 285 | ['yup.object'], 286 | ['yup.required'], 287 | [ 288 | 'yup.shape', 289 | { 290 | title: [ 291 | ['yup.object'], 292 | ['yup.required'], 293 | [ 294 | 'yup.shape', 295 | { 296 | en: [ 297 | ['yup.string'], 298 | ['yup.required'], 299 | ['yup.min', 5, 'String must be at least 5 characters'], 300 | ['yup.max', 50, 'String must be at most 50 characters'], 301 | ], 302 | ru: [ 303 | ['yup.string'], 304 | ['yup.required'], 305 | ['yup.min', 5, 'String must be at least 5 characters'], 306 | ['yup.max', 50, 'String must be at most 50 characters'], 307 | ], 308 | }, 309 | ], 310 | ], 311 | value: [['yup.number'], ['yup.required'], ['yup.min', 5]], 312 | }, 313 | ], 314 | ]); 315 | 316 | expect(validator.isValidSync({ title: { en: '12345', ru: '12345' }, value: 5 })).toEqual( 317 | true, 318 | ); 319 | expect(validator.isValidSync({ title: { en: '12345', ru: '12345' } })).toEqual(false); 320 | expect(validator.isValidSync({ title: { ru: '12345' }, value: 5 })).toEqual(false); 321 | expect(validator.isValidSync({ title: { en: '12345' }, value: 5 })).toEqual(false); 322 | expect(validator.isValidSync({ title: { en: '12345', ru: '12345' } })).toEqual(false); 323 | expect(validator.isValidSync()).toEqual(false); 324 | }); 325 | 326 | it('handles objects of objects', () => { 327 | const validator = transformAll([ 328 | ['yup.object'], 329 | ['yup.required'], 330 | [ 331 | 'yup.shape', 332 | { 333 | test: [ 334 | ['yup.object'], 335 | ['yup.required'], 336 | [ 337 | 'yup.shape', 338 | { 339 | number: [ 340 | // prettier-no-wrap 341 | ['yup.number'], 342 | ['yup.required'], 343 | ['yup.min', 20], 344 | ['yup.max', 50], 345 | ], 346 | }, 347 | ], 348 | ], 349 | }, 350 | ], 351 | ['yup.required'], 352 | ]); 353 | 354 | expect(validator.isValidSync({})).toEqual(false); 355 | expect( 356 | validator.isValidSync({ 357 | test: { 358 | number: 'A', 359 | }, 360 | }), 361 | ).toEqual(false); 362 | 363 | expect( 364 | validator.isValidSync({ 365 | test: {}, 366 | }), 367 | ).toEqual(false); 368 | }); 369 | 370 | it('handles arrays of objects', () => { 371 | const validator = transformAll([ 372 | ['yup.array'], 373 | ['yup.required'], 374 | [ 375 | 'yup.of', 376 | [ 377 | ['yup.object'], 378 | ['yup.required'], 379 | [ 380 | 'yup.shape', 381 | { 382 | number: [ 383 | // prettier-no-wrap 384 | ['yup.number'], 385 | ['yup.required'], 386 | ['yup.min', 20], 387 | ['yup.max', 50], 388 | ], 389 | }, 390 | ], 391 | ], 392 | ], 393 | ]); 394 | 395 | expect(validator.isValidSync([])).toEqual(false); 396 | expect( 397 | validator.isValidSync([ 398 | { 399 | number: 'A', 400 | }, 401 | ]), 402 | ).toEqual(false); 403 | 404 | expect( 405 | validator.isValidSync([ 406 | { 407 | number: {}, 408 | }, 409 | ]), 410 | ).toEqual(false); 411 | 412 | expect( 413 | validator.isValidSync([ 414 | { 415 | number: 20, 416 | }, 417 | ]), 418 | ).toEqual(true); 419 | 420 | expect( 421 | validator.isValidSync([ 422 | { 423 | number: 50, 424 | }, 425 | ]), 426 | ).toEqual(true); 427 | 428 | expect( 429 | validator.isValidSync([ 430 | { 431 | number: 51, 432 | }, 433 | ]), 434 | ).toEqual(false); 435 | 436 | expect( 437 | validator.isValidSync([ 438 | { 439 | number: 19, 440 | }, 441 | ]), 442 | ).toEqual(false); 443 | }); 444 | 445 | it('handles promise edge case test', async () => { 446 | const validator = transformAll([ 447 | ['yup.object'], 448 | ['yup.required'], 449 | [ 450 | 'yup.shape', 451 | { 452 | game: [['yup.string'], ['yup.required', 'wizard.validations.is_required']], 453 | locale: [['yup.string'], ['yup.required', 'wizard.validations.is_required']], 454 | category: [['yup.string'], ['yup.required', 'wizard.validations.is_required']], 455 | subcategory: [ 456 | ['yup.string'], 457 | ['yup.required', 'wizard.validations.is_required'], 458 | ], 459 | }, 460 | ], 461 | ]); 462 | 463 | expect(validator.isValidSync({ game: 'test' })).toEqual(false); 464 | expect(validator.isValidSync({ game: 'test', locale: 'test' })).toEqual(false); 465 | expect(validator.isValidSync({ game: 'test', locale: 'test', category: 'test' })).toEqual( 466 | false, 467 | ); 468 | expect( 469 | validator.isValidSync({ 470 | game: 'test', 471 | locale: 'test', 472 | category: 'test', 473 | subcategory: 'test', 474 | }), 475 | ).toEqual(true); 476 | 477 | try { 478 | await validator.validate({ 479 | game: 'test', 480 | locale: 'test', 481 | category: 'test', 482 | }); 483 | } catch (error) { 484 | expect(error.message).toEqual('wizard.validations.is_required'); 485 | } 486 | }); 487 | 488 | it('handles linked form validation', async () => { 489 | // We'll create a custom binding which checks if a number is > than another 490 | // number which already exists somewhere else in the form 491 | function greaterThan(ref, msg) { 492 | return this.test({ 493 | exclusive: false, 494 | name: 'greaterThan', 495 | params: { reference: ref.path }, 496 | // This must be a function 497 | // If not we lose some binding 498 | test: function(value) { 499 | return value > this.resolve(ref); 500 | }, 501 | message: msg || '${path} must be the greater than ${reference}', 502 | }); 503 | } 504 | 505 | // Here we'll bind this new function to the yup.number generator 506 | yup.addMethod(yup.number, 'linkedGreaterThan', greaterThan); 507 | 508 | const validator = transformAll([ 509 | ['yup.object'], 510 | ['yup.required'], 511 | [ 512 | 'yup.shape', 513 | { 514 | testValueSimple: [['yup.number'], ['yup.required']], 515 | linkedTest: [ 516 | ['yup.number'], 517 | ['yup.required'], 518 | ['yup.linkedGreaterThan', ['yup.ref', 'testValueSimple']], 519 | ], 520 | }, 521 | ], 522 | ]); 523 | 524 | expect(validator.isValidSync({ testValueSimple: 1 })).toEqual(false); 525 | expect(validator.isValidSync({ testValueSimple: 100, linkedTest: 1 })).toEqual(false); 526 | expect(validator.isValidSync({ testValueSimple: 100, linkedTest: 101 })).toEqual(true); 527 | }); 528 | }); 529 | 530 | describe('Complex edge cases', () => { 531 | function equalTo(ref, msg) { 532 | console.log(msg); 533 | return this.test({ 534 | name: 'equalTo', 535 | exclusive: false, 536 | message: msg || 'DEFAULT error', 537 | params: { 538 | reference: ref.path, 539 | }, 540 | test: function(value) { 541 | return value === this.resolve(ref); 542 | }, 543 | }); 544 | } 545 | 546 | yup.addMethod(yup.string, 'equalTo', equalTo); 547 | 548 | it('our yup ast validations can extract custom error', () => { 549 | const schema = transformAll([ 550 | ['yup.object'], 551 | ['yup.required'], 552 | [ 553 | 'yup.shape', 554 | { 555 | testValue1: [ 556 | ['yup.string'], 557 | ['yup.equalTo', ['yup.ref', 'linkedValue'], 'CUSTOM error'], 558 | ['yup.required'], 559 | ], 560 | testValue2: [ 561 | ['yup.string'], 562 | ['yup.equalTo', ['yup.ref', 'linkedValue']], 563 | ['yup.required'], 564 | ], 565 | testValue3: [ 566 | ['yup.string'], 567 | ['yup.equalTo', ['yup.ref', 'linkedValue'], 'CUSTOM error2'], 568 | ['yup.required'], 569 | ], 570 | linkedValue: [['yup.string']], 571 | }, 572 | ], 573 | ]); 574 | 575 | try { 576 | schema.validateSync({ 577 | testValue1: 'one', 578 | testValue2: 'two', 579 | testValue3: 'sdasd', 580 | linkedValue: 'three', 581 | }); 582 | throw new Error('should.not.reach.here'); 583 | } catch (err) { 584 | expect(err.message).toEqual('CUSTOM error2'); 585 | } 586 | }); 587 | }); 588 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | function resolve(newPath) { 5 | return path.resolve(__dirname, newPath); 6 | } 7 | 8 | const { NODE_ENV = 'production' } = process.env; 9 | 10 | module.exports = { 11 | mode: NODE_ENV, 12 | entry: './source/index.js', 13 | output: { 14 | path: resolve('dist'), 15 | libraryTarget: 'umd', 16 | filename: 'index.js', 17 | globalObject: 'this', 18 | library: 'providers-frontend', 19 | }, 20 | resolve: { 21 | extensions: ['.js'], 22 | alias: {}, 23 | }, 24 | externals: { 25 | lodash: 'lodash', 26 | immutable: 'immutable', 27 | }, 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.js$/, 32 | loader: 'babel-loader', 33 | include: resolve('./source'), 34 | options: { 35 | cacheDirectory: true, 36 | presets: ['es2015', 'react', 'stage-2'], 37 | }, 38 | }, 39 | ], 40 | }, 41 | node: { 42 | fs: 'empty', 43 | net: 'empty', 44 | tls: 'empty', 45 | dgram: 'empty', 46 | child_process: 'empty', 47 | }, 48 | }; 49 | --------------------------------------------------------------------------------