├── .eslintignore ├── .prettierrc.yml ├── .eslintrc ├── test ├── fixtures │ └── apps │ │ ├── y-validator-test │ │ ├── app │ │ │ ├── schemas │ │ │ │ ├── login │ │ │ │ │ └── login.yaml │ │ │ │ ├── heihei │ │ │ │ │ └── index.json │ │ │ │ └── haha │ │ │ │ │ ├── aa.yml │ │ │ │ │ └── index.js │ │ │ ├── router.js │ │ │ └── controller │ │ │ │ └── home.js │ │ ├── config │ │ │ ├── plugin.js │ │ │ └── config.default.js │ │ └── package.json │ │ └── superstruct-test │ │ ├── config │ │ ├── plugin.js │ │ └── config.default.js │ │ ├── app │ │ ├── schemas │ │ │ ├── heihei │ │ │ │ └── index.json │ │ │ └── haha │ │ │ │ └── index.js │ │ ├── router.js │ │ └── controller │ │ │ └── home.js │ │ └── package.json └── y-validator.test.js ├── app.js ├── .gitignore ├── .editorconfig ├── .travis.yml ├── appveyor.yml ├── config └── config.default.js ├── .autod.conf.js ├── app ├── middleware │ └── validator.js └── extend │ └── context.js ├── .github └── PULL_REQUEST_TEMPLATE.md ├── LICENSE ├── package.json ├── README.zh_CN.md └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | test -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-egg" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/y-validator-test/app/schemas/login/login.yaml: -------------------------------------------------------------------------------- 1 | name: 2 | type: 'string' 3 | required: true -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | app.config.coreMiddlewares.push('validator'); 5 | }; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | npm-debug.log 3 | node_modules/ 4 | coverage/ 5 | .idea/ 6 | run/ 7 | .DS_Store 8 | *.swp 9 | package-lock.json -------------------------------------------------------------------------------- /test/fixtures/apps/y-validator-test/app/schemas/heihei/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "required": true, 4 | "type": "string" 5 | } 6 | } -------------------------------------------------------------------------------- /test/fixtures/apps/superstruct-test/config/plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.validator = { 4 | enable: true, 5 | package: 'egg-y-validator', 6 | }; 7 | -------------------------------------------------------------------------------- /test/fixtures/apps/y-validator-test/config/plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.validator = { 4 | enable: true, 5 | package: 'egg-y-validator', 6 | }; 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = false 9 | insert_final_newline = false -------------------------------------------------------------------------------- /test/fixtures/apps/y-validator-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "y-validator-test", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "egg-y-validator": "file:../../../.." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/apps/superstruct-test/app/schemas/heihei/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "string", 3 | "title": "string", 4 | "is_published": "boolean?", 5 | "tags": "string", 6 | "author": "string" 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/apps/superstruct-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "y-validator-test", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "egg-y-validator": "file:../../../..", 6 | "superstruct": "^0.5.1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/apps/superstruct-test/app/schemas/haha/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'string', 3 | email: 'email', 4 | types: { 5 | email: v => { 6 | return Boolean(v.indexOf('@') != -1); 7 | } 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '8' 5 | - '9' 6 | before_install: 7 | - npm i npminstall -g 8 | install: 9 | - npminstall 10 | script: 11 | - npm run ci 12 | after_script: 13 | - npminstall codecov && codecov 14 | -------------------------------------------------------------------------------- /test/fixtures/apps/y-validator-test/app/schemas/haha/aa.yml: -------------------------------------------------------------------------------- 1 | name: 2 | - 3 | required: true 4 | - 5 | validator: !!js/function > 6 | function validator(ctx) { 7 | return async function (rule, value, callback, source, options) { 8 | throw [{field:'name', message:'错误'}] 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: '8' 4 | - nodejs_version: '9' 5 | 6 | install: 7 | - ps: Install-Product node $env:nodejs_version 8 | - npm i npminstall && node_modules\.bin\npminstall 9 | 10 | test_script: 11 | - node --version 12 | - npm --version 13 | - npm run test 14 | 15 | build: off 16 | -------------------------------------------------------------------------------- /test/fixtures/apps/superstruct-test/app/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const { router, controller } = app; 5 | 6 | router.get('/', controller.home.index); 7 | router.get('/a', controller.home.a); 8 | router.get('/b', controller.home.b); 9 | router.get('/d', controller.home.d); 10 | router.get('/f', controller.home.f); 11 | }; 12 | -------------------------------------------------------------------------------- /test/fixtures/apps/y-validator-test/app/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const { router, controller } = app; 5 | 6 | router.get('/', controller.home.index); 7 | router.get('/a', controller.home.a); 8 | router.get('/b', controller.home.b); 9 | router.get('/d', controller.home.d); 10 | router.get('/f', controller.home.f); 11 | }; 12 | -------------------------------------------------------------------------------- /test/fixtures/apps/y-validator-test/config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = '123456'; 4 | exports.validator = { 5 | open: 'zh-CN', 6 | languages: { 7 | 'zh-CN': { 8 | required: '%s 必填', 9 | }, 10 | }, 11 | async formatter(ctx, error) { 12 | ctx.type = 'json'; 13 | ctx.status = 400; 14 | ctx.body = error; 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /test/fixtures/apps/superstruct-test/config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = '123456'; 4 | exports.validator = { 5 | superstruct: true, 6 | types(ctx) { 7 | return { 8 | email: v => true 9 | }; 10 | }, 11 | async formatter(ctx, error) { 12 | const { data, path, value } = error; 13 | ctx.type = 'json'; 14 | ctx.status = 400; 15 | ctx.body = { field: path[0], message: '无效的值' }; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * egg-y-validator default config 5 | * @member Config#validator 6 | * @property {String} SOME_KEY - some description 7 | */ 8 | exports.validator = { 9 | open: 'zh-CN', 10 | languages: { 11 | 'zh-CN': { 12 | required: '%s 必填', 13 | }, 14 | }, 15 | superstruct: false, 16 | types() {}, 17 | async formatter(ctx, error) { 18 | ctx.status = 400; 19 | ctx.body = error[0].message; 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /.autod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | write: true, 5 | prefix: '^', 6 | plugin: 'autod-egg', 7 | test: [ 8 | 'test', 9 | 'benchmark', 10 | ], 11 | devdep: [ 12 | 'egg', 13 | 'egg-ci', 14 | 'egg-bin', 15 | 'autod', 16 | 'autod-egg', 17 | 'eslint', 18 | 'eslint-config-egg', 19 | 'webstorm-disable-index', 20 | ], 21 | exclude: [ 22 | './test/fixtures', 23 | './docs', 24 | './coverage', 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /app/middleware/validator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const depd = require('depd')('egg-y-validator'); 3 | 4 | module.exports = options => { 5 | return async (ctx, next) => { 6 | if (options.formatter || options.formate) { 7 | let fn = options.formatter; 8 | if (options.formate) { 9 | depd( 10 | 'config.formate is deprecated. now you can use config.formatter replace.' 11 | ); 12 | fn = options.formate; 13 | } 14 | try { 15 | return await next(); 16 | } catch (e) { 17 | return await fn(ctx, e); 18 | } 19 | } 20 | return next(); 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | ##### Checklist 12 | 13 | 14 | - [ ] `npm test` passes 15 | - [ ] tests and/or benchmarks are included 16 | - [ ] documentation is changed or added 17 | - [ ] commit message follows commit guidelines 18 | 19 | ##### Affected core subsystem(s) 20 | 21 | 22 | 23 | ##### Description of change 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 ChenMingMing Holding Limited and other contributors. 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. 22 | -------------------------------------------------------------------------------- /test/fixtures/apps/superstruct-test/app/controller/home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | const assert = require('power-assert'); 5 | 6 | class HomeController extends Controller { 7 | async index() { 8 | await this.ctx.verify('login.login', 'query'); 9 | this.ctx.body = 'hi, ' + this.app.plugins.validator.name; 10 | } 11 | async a() { 12 | await this.ctx.verify('heihei', this.ctx.query); 13 | this.ctx.body = 'hi, ' + this.app.plugins.validator.name; 14 | } 15 | async b() { 16 | const ret = await this.ctx.verify( 17 | { 18 | name: 'string' 19 | }, 20 | async () => { 21 | return this.ctx.query; 22 | } 23 | ); 24 | this.ctx.body = 'hi, ' + this.app.plugins.validator.name; 25 | } 26 | async d() { 27 | await this.ctx.verify('haha', async () => { 28 | return { name: '123', email: 'ck123.com' }; 29 | }); 30 | this.ctx.body = 'hi, ' + this.app.plugins.validator.name; 31 | } 32 | async f() { 33 | const ret = await this.ctx.verify('haha.aa', async () => { 34 | return this.ctx.query; 35 | }); 36 | console.log(ret); 37 | this.ctx.body = 'hi, ' + this.app.plugins.validator.name; 38 | } 39 | } 40 | 41 | module.exports = HomeController; 42 | -------------------------------------------------------------------------------- /test/fixtures/apps/y-validator-test/app/controller/home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | const assert = require('power-assert'); 5 | 6 | class HomeController extends Controller { 7 | async index() { 8 | await this.ctx.verify('login.login', 'query'); 9 | this.ctx.body = 'hi, ' + this.app.plugins.validator.name; 10 | } 11 | async a() { 12 | await this.ctx.verify('heihei', this.ctx.query); 13 | console.log(this.ctx.docs); 14 | this.ctx.body = 'hi, ' + this.app.plugins.validator.name; 15 | } 16 | async b() { 17 | const ret = await this.ctx.verify( 18 | { 19 | name: { 20 | required: true, 21 | }, 22 | }, 23 | async () => { 24 | return this.ctx.query; 25 | } 26 | ); 27 | assert.deepEqual(ret, { name: 'some' }); 28 | this.ctx.body = 'hi, ' + this.app.plugins.validator.name; 29 | } 30 | async d() { 31 | await this.ctx.verify('haha', async () => { 32 | return this.ctx.query; 33 | }); 34 | this.ctx.body = 'hi, ' + this.app.plugins.validator.name; 35 | } 36 | async f() { 37 | const ret = await this.ctx.verify('haha.aa', async () => { 38 | return this.ctx.query; 39 | }); 40 | console.log(ret); 41 | this.ctx.body = 'hi, ' + this.app.plugins.validator.name; 42 | } 43 | } 44 | 45 | module.exports = HomeController; -------------------------------------------------------------------------------- /test/fixtures/apps/y-validator-test/app/schemas/haha/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict'; 3 | 4 | module.exports = { 5 | name: [ 6 | { 7 | required: true, 8 | }, { 9 | validator: ctx => async (rule, value, callback, source, options) => { 10 | // console.log(ctx); 11 | // console.log(rule); 12 | // { validator: [Function], 13 | // field: 'name', 14 | // fullField: 'name', 15 | // type: 'string' } 16 | // console.log(value); 17 | // some 18 | // console.log(source); 19 | // { name: 'some' } 20 | // console.log(options); 21 | // { messages: 22 | // { default: 'Validation error on field %s', 23 | // required: '%s 必填', 24 | // enum: '%s must be one of %s', 25 | // whitespace: '%s cannot be empty', 26 | // date: 27 | // { format: '%s date %s is invalid for format %s', 28 | // parse: '%s date could not be parsed, %s is invalid ', 29 | // invalid: '%s date %s is invalid' }, 30 | // types: 31 | // { string: '%s is not a %s', 32 | // method: '%s is not a %s (function)', 33 | // array: '%s is not an %s', 34 | // object: '%s is not an %s', 35 | // number: '%s is not a %s', 36 | // date: '%s is not a %s', 37 | // boolean: '%s is not a %s', 38 | // integer: '%s is not an %s', 39 | // float: '%s is not a %s', 40 | // regexp: '%s is not a valid %s', 41 | // email: '%s is not a valid %s', 42 | // url: '%s is not a valid %s', 43 | // hex: '%s is not a valid %s' }, 44 | // string: 45 | // { len: '%s must be exactly %s characters', 46 | // min: '%s must be at least %s characters', 47 | // max: '%s cannot be longer than %s characters', 48 | // range: '%s must be between %s and %s characters' }, 49 | // number: 50 | // { len: '%s must equal %s', 51 | // min: '%s cannot be less than %s', 52 | // max: '%s cannot be greater than %s', 53 | // range: '%s must be between %s and %s' }, 54 | // array: 55 | // { len: '%s must be exactly %s in length', 56 | // min: '%s cannot be less than %s in length', 57 | // max: '%s cannot be greater than %s in length', 58 | // range: '%s must be between %s and %s in length' }, 59 | // pattern: { mismatch: '%s value %s does not match pattern %s' }, 60 | // clone: [Function: clone] } } 61 | throw [{field:'name', message:'错误'}] 62 | }, 63 | }], 64 | }; 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "egg-y-validator", 3 | "version": "1.4.0", 4 | "description": "auto validator", 5 | "eggPlugin": { 6 | "name": "validator" 7 | }, 8 | "publishConfig": { 9 | "access": "public" 10 | }, 11 | "keywords": [ 12 | "egg", 13 | "eggPlugin", 14 | "egg-plugin" 15 | ], 16 | "dependencies": { 17 | "async-validator": "^2.0.1", 18 | "camelcase": "^5.0.0", 19 | "clone": "^2.1.2", 20 | "debug": "^4.1.0", 21 | "depd": "^2.0.0", 22 | "fast-glob": "^3.0.4", 23 | "m-import": "^1.0.2", 24 | "slash": "^3.0.0", 25 | "superstruct": "^0.6.0" 26 | }, 27 | "devDependencies": { 28 | "autod": "^3.0.0", 29 | "autod-egg": "^1.0.0", 30 | "conventional-changelog-eslint": "^3.0.0", 31 | "egg": "^2.8.1", 32 | "egg-bin": "^4.7.0", 33 | "egg-ci": "^1.8.0", 34 | "egg-mock": "^3.17.2", 35 | "eslint": "^6.1.0", 36 | "eslint-config-egg": "^7.0.0", 37 | "np": "^5.0.3" 38 | }, 39 | "engines": { 40 | "node": ">=8.0.0" 41 | }, 42 | "scripts": { 43 | "test": "npm run lint-fix && egg-bin pkgfiles && npm run test-local", 44 | "test-local": "egg-bin test", 45 | "cov": "egg-bin cov", 46 | "lint": "eslint .", 47 | "lint-fix": "eslint . --fix", 48 | "ci": "egg-bin pkgfiles --check && npm run lint && npm run cov", 49 | "pkgfiles": "egg-bin pkgfiles", 50 | "autod": "autod", 51 | "release": "np" 52 | }, 53 | "files": [ 54 | ".autod.conf.js", 55 | "app", 56 | "config", 57 | "app.js" 58 | ], 59 | "ci": { 60 | "version": "8, 9" 61 | }, 62 | "release": { 63 | "analyzeCommits": { 64 | "preset": "angular", 65 | "releaseRules": [ 66 | { 67 | "type": "docs", 68 | "scope": "README", 69 | "release": "patch" 70 | }, 71 | { 72 | "type": "refactor", 73 | "release": "patch" 74 | }, 75 | { 76 | "type": "style", 77 | "release": "patch" 78 | } 79 | ], 80 | "parserOpts": { 81 | "noteKeywords": [ 82 | "BREAKING CHANGE", 83 | "BREAKING CHANGES", 84 | "BREAKING" 85 | ] 86 | } 87 | } 88 | }, 89 | "repository": { 90 | "type": "git", 91 | "url": "https://github.com/MiYogurt/egg-y-validator.git" 92 | }, 93 | "bugs": { 94 | "url": "https://github.com/MiYogurt/egg-y-validator/issues" 95 | }, 96 | "homepage": "https://github.com/MiYogurt/egg-y-validator.git#readme", 97 | "author": "miyogurt", 98 | "license": "MIT", 99 | "main": ".autod.conf.js", 100 | "directories": { 101 | "test": "test" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test/y-validator.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mock = require('egg-mock'); 4 | 5 | describe('async validator test', () => { 6 | let app; 7 | before(() => { 8 | app = mock.app({ 9 | baseDir: 'apps/y-validator-test' 10 | }); 11 | return app.ready(); 12 | }); 13 | 14 | after(() => app.close()); 15 | afterEach(mock.restore); 16 | 17 | it('test type string', () => { 18 | return app 19 | .httpRequest() 20 | .get('/?ss=some') 21 | .expect('[{"message":"name 必填","field":"name"}]') 22 | .expect(400); 23 | }); 24 | 25 | it('test type object', () => { 26 | return app 27 | .httpRequest() 28 | .get('/a?ss=some') 29 | .expect('[{"message":"name 必填","field":"name"}]') 30 | .expect(400); 31 | }); 32 | 33 | it('test type async function', () => { 34 | return app 35 | .httpRequest() 36 | .get('/b?name=some') 37 | .expect('hi, validator') 38 | .expect(200); 39 | }); 40 | 41 | it('test type rules function js', () => { 42 | return app 43 | .httpRequest() 44 | .get('/d?name=some') 45 | .expect('[{"field":"name","message":"错误"}]') 46 | .expect(400); 47 | }); 48 | 49 | it('test type rules function by yaml', () => { 50 | return app 51 | .httpRequest() 52 | .get('/f?name=some') 53 | .expect('[{"field":"name","message":"错误"}]') 54 | .expect(400); 55 | }); 56 | }); 57 | 58 | describe('superstruct', () => { 59 | let app; 60 | before(() => { 61 | app = mock.app({ baseDir: 'apps/superstruct-test' }); 62 | return app.ready(); 63 | }); 64 | 65 | after(() => app.close()); 66 | afterEach(mock.restore); 67 | 68 | it('test type object', () => { 69 | return ( 70 | app 71 | .httpRequest() 72 | .get('/a') 73 | .query({ 74 | id: 123, 75 | title: 'Hello World', 76 | tags: 'news', 77 | author: 12 78 | }) 79 | // .expect('[{"message":"name 必填","field":"name"}]') 80 | .expect(200) 81 | ); 82 | }); 83 | 84 | it('test type async function', () => { 85 | return app 86 | .httpRequest() 87 | .get('/b?name=coco') 88 | .expect('hi, validator') 89 | .expect(200); 90 | }); 91 | 92 | it('test type rules function js', () => { 93 | return app 94 | .httpRequest() 95 | .get('/d?name=some') 96 | .expect('{"field":"email","message":"无效的值"}') 97 | .expect(400); 98 | }); 99 | 100 | // it('test type rules function by yaml', () => { 101 | // return app 102 | // .httpRequest() 103 | // .get('/f?name=some') 104 | // .expect('[{"field":"name","message":"错误"}]') 105 | // .expect(400); 106 | // }); 107 | }); 108 | -------------------------------------------------------------------------------- /app/extend/context.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Validate = require('async-validator').default; 3 | const { resolve, sep: s } = require('path'); 4 | const glob = require('fast-glob'); 5 | const mi = require('m-import').default; 6 | const R = require('ramda'); 7 | const camelCase = require('camelcase'); 8 | const clone = require('clone'); 9 | const slash = require('slash'); 10 | 11 | const debug = require('debug')('egg-y-validator'); 12 | 13 | const CACHE = Symbol.for('egg-y-validator'); 14 | const VALIDATOR = Symbol.for('egg-y-validator:getValidator'); 15 | const GETFIELD = Symbol.for('egg-y-validator:getField'); 16 | 17 | const { assocPath, compose, curry } = R; 18 | 19 | const delStr = curry((delStr, souStr) => { 20 | if (Array.isArray(delStr)) { 21 | return delStr.reduce((prev, current) => prev.replace(current, ''), souStr); 22 | } 23 | return souStr.replace(delStr, ''); 24 | }); 25 | //* 对所有的函数传递 ctx 26 | const invokeFn = (obj, ctx) => { 27 | const forEach = (value, index) => { 28 | if (R.type(value) === 'Array') { 29 | invokeFn(value, ctx); 30 | } 31 | if (R.type(value) === 'Object' && R.has('validator', value)) { 32 | value.validator = value.validator(ctx); 33 | } 34 | if (index === 'validator') { 35 | obj[index] = obj[index](ctx); 36 | } 37 | }; 38 | if (R.type(obj) === 'Array') { 39 | R.forEach(forEach, obj); 40 | } 41 | if (R.type(obj) === 'Object') { 42 | R.forEachObjIndexed(forEach, obj); 43 | } 44 | }; 45 | 46 | module.exports = { 47 | loadDocs(reload) { 48 | if (!reload && this[CACHE]) { 49 | return this[CACHE]; 50 | } 51 | const { app } = this; 52 | let schemas = {}; 53 | const matchPath = resolve(app.config.baseDir, 'app', 'schemas', '**', '*'); 54 | const paths = glob.sync(slash(matchPath)); 55 | 56 | const delAllStr = compose( 57 | delStr([ '.json', '.js', '.toml', '.tml', '.yaml', '.yml' ]), 58 | v => { 59 | return v.replace(/.*(\/|\\)schemas(\/|\\)/ig, ''); 60 | } 61 | // delStr(app.config.baseDir + `${s}app${s}schemas${s}`) 62 | ); 63 | 64 | const ForEach = R.tryCatch(path => { 65 | const content = mi(path); 66 | path = delAllStr(path); 67 | if (path.indexOf(s) === -1 && s !== '/') { 68 | path = path.split('/'); 69 | } else { 70 | path = path.split(s); 71 | } 72 | this[CACHE] = schemas = assocPath( 73 | path.map(p => camelCase(p)), 74 | content, 75 | schemas 76 | ); 77 | }, console.error); 78 | 79 | paths.forEach(ForEach); 80 | return schemas; 81 | }, 82 | 83 | get docs() { 84 | return this.loadDocs(false); 85 | }, 86 | //* 需要验证的对象 87 | async [GETFIELD](type) { 88 | if (compose(R.equals('AsyncFunction'), R.type)(type)) { 89 | return await type(); 90 | } 91 | return R.cond([ 92 | [ compose(R.equals('Object'), R.type), R.always(type) ], 93 | [ compose(R.equals('Function'), R.type), type ], 94 | [ R.equals('query'), R.always(this.request.query) ], 95 | [ R.equals('body'), R.always(this.request.body) ], 96 | [ R.equals('params'), R.always(this.params) ], 97 | [ 98 | R.T, 99 | R.always(R.merge(this.params, this.request.query, this.request.body)), 100 | ], 101 | ])(type); 102 | }, 103 | //* 拿到验证规则 104 | getValidatorRules(path) { 105 | let rules; 106 | if (R.type(path) === 'Object') { 107 | rules = path; 108 | } else { 109 | path = path.split('.'); 110 | rules = R.path(path, this.docs); 111 | rules = R.defaultTo(rules, R.prop('index', rules)); 112 | rules = clone(rules); 113 | invokeFn(rules, this); 114 | } 115 | return rules; 116 | }, 117 | //* 拿到验证器 118 | async [VALIDATOR](config) { 119 | if (this.app.config.validator.superstruct) { 120 | const { superstruct } = require('superstruct'); 121 | const types = R.defaultTo({}, this.app.config.validator.types(this)); 122 | const struct = superstruct({ 123 | types: R.merge(types, config.types), 124 | }); 125 | delete config.types; 126 | const validator = struct(config); 127 | // 保证跟 async-validator 相同的 API 128 | return { 129 | validate: (fields, fn) => { 130 | try { 131 | fn(null, validator(fields)); 132 | } catch (e) { 133 | fn(e); 134 | } 135 | }, 136 | }; 137 | } 138 | let open = this.app.config.validator.open; 139 | if (R.type(open) === 'Function' || R.type(open) === 'AsyncFunction') { 140 | open = await open(this); 141 | } 142 | const messages = this.app.config.validator.languages[open] || {}; 143 | const validator = new Validate(config); 144 | validator.messages(messages); 145 | return validator; 146 | }, 147 | async verify(path, type) { 148 | const rules = this.getValidatorRules(path); 149 | const validator = await this[VALIDATOR](rules); 150 | const fields = await this[GETFIELD](type); 151 | debug('rules %j', rules); 152 | debug('fields %o', fields); 153 | return new Promise((resolve, reject) => { 154 | validator.validate(fields, errors => { 155 | if (errors) { 156 | reject(errors); 157 | } 158 | resolve(fields); 159 | }); 160 | }); 161 | }, 162 | }; 163 | -------------------------------------------------------------------------------- /README.zh_CN.md: -------------------------------------------------------------------------------- 1 | # egg-y-validator 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![build status][travis-image]][travis-url] 5 | [![Test coverage][codecov-image]][codecov-url] 6 | [![David deps][david-image]][david-url] 7 | [![Known Vulnerabilities][snyk-image]][snyk-url] 8 | [![npm download][download-image]][download-url] 9 | ![](https://img.shields.io/badge/license-MIT-000000.svg) 10 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FMiYogurt%2Fegg-y-validator.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FMiYogurt%2Fegg-y-validator?ref=badge_shield) 11 | 12 | [npm-image]: https://img.shields.io/npm/v/egg-y-validator.svg?style=flat-square 13 | [npm-url]: https://npmjs.org/package/egg-y-validator 14 | [travis-image]: https://img.shields.io/travis/MiYogurt/egg-y-validator.svg?style=flat-square 15 | [travis-url]: https://travis-ci.org/MiYogurt/egg-y-validator 16 | [codecov-image]: https://img.shields.io/codecov/c/github/MiYogurt/egg-y-validator.svg?style=flat-square 17 | [codecov-url]: https://codecov.io/github/MiYogurt/egg-y-validator?branch=master 18 | [david-image]: https://img.shields.io/david/MiYogurt/egg-y-validator.svg?style=flat-square 19 | [david-url]: https://david-dm.org/MiYogurt/egg-y-validator 20 | [snyk-image]: https://snyk.io/test/npm/egg-y-validator/badge.svg?style=flat-square 21 | [snyk-url]: https://snyk.io/test/npm/egg-y-validator 22 | [download-image]: https://img.shields.io/npm/dm/egg-y-validator.svg?style=flat-square 23 | [download-url]: https://npmjs.org/package/egg-y-validator 24 | 25 | 28 | 29 | ## 安装 30 | 31 | ```bash 32 | $ npm i egg-y-validator --save 33 | ``` 34 | 35 | ## 使用 36 | 37 | ```js 38 | // {app_root}/config/plugin.js 39 | exports.validator = { 40 | enable: true, 41 | package: 'egg-y-validator' 42 | }; 43 | ``` 44 | 45 | ## 配置 46 | 47 | ```js 48 | // {app_root}/config/config.default.js 49 | exports.validator = { 50 | open: async ctx => 'zh-CN', 51 | // or 52 |  // open: 'zh-CN',  它表示开启的语言 53 |  languages: { 54 | 'zh-CN': { 55 | required: '%s 必填' 56 | } 57 | }, 58 | async formatter(ctx, error) { 59 | ctx.type = 'json'; 60 | ctx.status = 400; 61 | ctx.body = error; 62 | } 63 | }; 64 | ``` 65 | 66 | 看 [config/config.default.js](config/config.default.js) 可以看到更多配置. 67 | 68 | ## 创建 69 | 70 | `app/schemas/login/login.yml` 71 | 72 | Suport json、js、yaml、toml file. 73 | 74 | 所有的规则你可以在这里找到 [async-validator](https://github.com/yiminghe/async-validator/blob/e782748f0345b462d84e96a582c0dd38db2de666/__tests__/deep.spec.js) 75 | 76 | ```yaml 77 | name: 78 | type: 'string' 79 | required: true 80 | ``` 81 | 82 | 假如你想自定义,写高阶函数,获取到 context,仅支持 yaml 和 js 格式。 83 | 84 | ```js 85 | /* eslint-disable */ 86 | 'use strict'; 87 | 88 | module.exports = { 89 | name: [ 90 | { 91 | required: true 92 | }, 93 | { 94 | validator: ctx => async (rule, value, callback, source, options) => { 95 | // console.log(ctx); 96 | // console.log(rule); 97 | // { validator: [Function], 98 | // field: 'name', 99 | // fullField: 'name', 100 | // type: 'string' } 101 | // console.log(value); 102 | // some 103 | // console.log(source); 104 | // { name: 'some' } 105 | // console.log(options); 106 | // { messages: 107 | // { default: 'Validation error on field %s', 108 | // required: '%s 必填', 109 | // enum: '%s must be one of %s', 110 | // whitespace: '%s cannot be empty', 111 | // date: 112 | // { format: '%s date %s is invalid for format %s', 113 | // parse: '%s date could not be parsed, %s is invalid ', 114 | // invalid: '%s date %s is invalid' }, 115 | // types: 116 | // { string: '%s is not a %s', 117 | // method: '%s is not a %s (function)', 118 | // array: '%s is not an %s', 119 | // object: '%s is not an %s', 120 | // number: '%s is not a %s', 121 | // date: '%s is not a %s', 122 | // boolean: '%s is not a %s', 123 | // integer: '%s is not an %s', 124 | // float: '%s is not a %s', 125 | // regexp: '%s is not a valid %s', 126 | // email: '%s is not a valid %s', 127 | // url: '%s is not a valid %s', 128 | // hex: '%s is not a valid %s' }, 129 | // string: 130 | // { len: '%s must be exactly %s characters', 131 | // min: '%s must be at least %s characters', 132 | // max: '%s cannot be longer than %s characters', 133 | // range: '%s must be between %s and %s characters' }, 134 | // number: 135 | // { len: '%s must equal %s', 136 | // min: '%s cannot be less than %s', 137 | // max: '%s cannot be greater than %s', 138 | // range: '%s must be between %s and %s' }, 139 | // array: 140 | // { len: '%s must be exactly %s in length', 141 | // min: '%s cannot be less than %s in length', 142 | // max: '%s cannot be greater than %s in length', 143 | // range: '%s must be between %s and %s in length' }, 144 | // pattern: { mismatch: '%s value %s does not match pattern %s' }, 145 | // clone: [Function: clone] } } 146 | throw [{ field: 'name', message: '错误' }]; 147 | } 148 | } 149 | ] 150 | }; 151 | ``` 152 | 153 | ```yml 154 | name: 155 | - 156 | required: true 157 | - 158 | validator: !!js/function > 159 | function validator(ctx) { 160 | return async function (rule, value, callback, source, options) { 161 | throw [{field:'name', message:'错误'}] 162 | } 163 | } 164 | ``` 165 | 166 | 抛出错误使用 throw 抛出,或者 callback 方法 167 | 168 | ## 在你的控制器里面 调用 verify 验证 169 | 170 | ```js 171 | 'use strict'; 172 | 173 | const Controller = require('egg').Controller; 174 | 175 | class HomeController extends Controller { 176 | async index() { 177 | const query = await this.ctx.verify('login.login', 'query'); 178 | this.ctx.body = 'hi, ' + this.app.plugins.validator.name; 179 | } 180 | } 181 | 182 | module.exports = HomeController; 183 | ``` 184 | 185 | ##  接口 186 | 187 | ### ctx.verify(path, type) 188 | 189 | * path 验证的规则 190 | 191 | * `login.login` -> 'app/schemas/login/login.{json/js/toml/yaml}' 192 | * `login` -> 假如这个 login 下面有 index -> `login.index` -> 'app/schemas/login/index.{json/js/toml/yaml}' 193 | * 假如 path 是一个 object 会直接使用这个 object 194 | 195 | * type 验证的对象 196 | 197 | * query -> ctx.request.query 198 | * body -> ctx.request.body 199 | * params -> ctx.params 200 | * undefined -> R.merge(this.params, this.request.query, this.request.body) // 不传会自动 merge 这几个对象 201 | * object -> object 202 | * async function -> // 会调用你的 async 方法获取 203 | 204 | ### ctx.docs 205 | 206 | 所有的规则在这里 207 | 208 | ### ctx.loadDocs(reload) 209 | 210 | * reload -> boolean 里面有 cache 当 reload 为 true 才会重新加载 211 | 212 | ## Questions & Suggestions 213 | 214 | Please open an issue [here](https://github.com/MiYogurt/egg-y-validator/issues). 215 | 216 | ## 支持 superstruct 库 217 | 218 | but superstruct custom type function not support async function 219 | 220 | 但是这个库不支持 async fucction 221 | 222 | [superstruct api](https://github.com/ianstormtaylor/superstruct/blob/master/docs/reference.md#types) 223 | 224 | more info you can see the test file example. 225 | 226 | ### config.default.js 227 | 228 | ```js 229 | exports.validator = { 230 | superstruct: true, 231 | types(ctx) { 232 |    // 自定义你的类型 不支持 async function 233 |    return { 234 | email: v => true 235 | }; 236 | }, 237 | async formatter(ctx, error) { 238 | const { data, path, value } = error; 239 | console.log(error); 240 | ctx.type = 'json'; 241 | ctx.status = 400; 242 | ctx.body = { field: path[0], message: '无效的值' }; 243 | console.log(ctx.body); 244 | } 245 | }; 246 | ``` 247 | 248 | ### controller 249 | 250 | ```js 251 | async b() { 252 | const ret = await this.ctx.verify( 253 |      { // 规则 254 |        name: 'string' 255 | }, 256 |      async () => { // 数据 257 |        return this.ctx.query; 258 | } 259 | ); 260 | this.ctx.body = 'hi, ' + this.app.plugins.validator.name; 261 | } 262 | async d() { 263 | await this.ctx.verify('haha', async () => { 264 | return { name: '123', email: 'ck123.com' }; 265 | }); 266 | this.ctx.body = 'hi, ' + this.app.plugins.validator.name; 267 | } 268 | ``` 269 | 270 | ### rules 271 | 272 | 验证规则的写法 273 | 274 | ```js 275 | module.exports = { 276 | name: 'string', 277 | email: 'email', 278 | types: { 279 | email: v => { 280 | console.log('email verify'); 281 | return Boolean(v.indexOf('@') != -1); 282 | } 283 | } 284 | }; 285 | ``` 286 | 287 | ## License 288 | 289 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FMiYogurt%2Fegg-y-validator.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FMiYogurt%2Fegg-y-validator?ref=badge_large) 290 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # egg-y-validator 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![build status][travis-image]][travis-url] 5 | [![Test coverage][codecov-image]][codecov-url] 6 | [![David deps][david-image]][david-url] 7 | [![Known Vulnerabilities][snyk-image]][snyk-url] 8 | [![npm download][download-image]][download-url] 9 | ![](https://img.shields.io/badge/license-MIT-000000.svg) 10 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FMiYogurt%2Fegg-y-validator.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FMiYogurt%2Fegg-y-validator?ref=badge_shield) 11 | 12 | [npm-image]: https://img.shields.io/npm/v/egg-y-validator.svg?style=flat-square 13 | [npm-url]: https://npmjs.org/package/egg-y-validator 14 | [travis-image]: https://img.shields.io/travis/MiYogurt/egg-y-validator.svg?style=flat-square 15 | [travis-url]: https://travis-ci.org/MiYogurt/egg-y-validator 16 | [codecov-image]: https://img.shields.io/codecov/c/github/MiYogurt/egg-y-validator.svg?style=flat-square 17 | [codecov-url]: https://codecov.io/github/MiYogurt/egg-y-validator?branch=master 18 | [david-image]: https://img.shields.io/david/MiYogurt/egg-y-validator.svg?style=flat-square 19 | [david-url]: https://david-dm.org/MiYogurt/egg-y-validator 20 | [snyk-image]: https://snyk.io/test/npm/egg-y-validator/badge.svg?style=flat-square 21 | [snyk-url]: https://snyk.io/test/npm/egg-y-validator 22 | [download-image]: https://img.shields.io/npm/dm/egg-y-validator.svg?style=flat-square 23 | [download-url]: https://npmjs.org/package/egg-y-validator 24 | 25 | 28 | 29 | [中文文档](https://github.com/MiYogurt/egg-y-validator/blob/master/README.zh_CN.md) 30 | 31 | ## Install 32 | 33 | ```bash 34 | $ npm i egg-y-validator --save 35 | ``` 36 | 37 | ## Usage 38 | 39 | ```js 40 | // {app_root}/config/plugin.js 41 | exports.validator = { 42 | enable: true, 43 | package: 'egg-y-validator' 44 | }; 45 | ``` 46 | 47 | ## Configuration 48 | 49 | ```js 50 | // {app_root}/config/config.default.js 51 | exports.validator = { 52 | open: async ctx => 'zh-CN', 53 | // or 54 | // open: 'zh-CN', 55 | languages: { 56 | 'zh-CN': { 57 | required: '%s 必填' 58 | } 59 | }, 60 | async formatter(ctx, error) { 61 | ctx.type = 'json'; 62 | ctx.status = 400; 63 | ctx.body = error; 64 | } 65 | }; 66 | ``` 67 | 68 | see [config/config.default.js](config/config.default.js) for more detail. 69 | 70 | ## Create rules 71 | 72 | `app/schemas/login/login.yml` 73 | 74 | Suport json、js、yaml、toml file. 75 | 76 | all rules you can find in [async-validator](https://github.com/yiminghe/async-validator/blob/e782748f0345b462d84e96a582c0dd38db2de666/__tests__/deep.spec.js) 77 | 78 | ```yaml 79 | name: 80 | type: 'string' 81 | required: true 82 | ``` 83 | 84 | if you want custom rules,and get context ,but only support js、yal file 85 | 86 | ```js 87 | /* eslint-disable */ 88 | 'use strict'; 89 | 90 | module.exports = { 91 | name: [ 92 | { 93 | required: true 94 | }, 95 | { 96 | validator: ctx => async (rule, value, callback, source, options) => { 97 | // console.log(ctx); 98 | // console.log(rule); 99 | // { validator: [Function], 100 | // field: 'name', 101 | // fullField: 'name', 102 | // type: 'string' } 103 | // console.log(value); 104 | // some 105 | // console.log(source); 106 | // { name: 'some' } 107 | // console.log(options); 108 | // { messages: 109 | // { default: 'Validation error on field %s', 110 | // required: '%s 必填', 111 | // enum: '%s must be one of %s', 112 | // whitespace: '%s cannot be empty', 113 | // date: 114 | // { format: '%s date %s is invalid for format %s', 115 | // parse: '%s date could not be parsed, %s is invalid ', 116 | // invalid: '%s date %s is invalid' }, 117 | // types: 118 | // { string: '%s is not a %s', 119 | // method: '%s is not a %s (function)', 120 | // array: '%s is not an %s', 121 | // object: '%s is not an %s', 122 | // number: '%s is not a %s', 123 | // date: '%s is not a %s', 124 | // boolean: '%s is not a %s', 125 | // integer: '%s is not an %s', 126 | // float: '%s is not a %s', 127 | // regexp: '%s is not a valid %s', 128 | // email: '%s is not a valid %s', 129 | // url: '%s is not a valid %s', 130 | // hex: '%s is not a valid %s' }, 131 | // string: 132 | // { len: '%s must be exactly %s characters', 133 | // min: '%s must be at least %s characters', 134 | // max: '%s cannot be longer than %s characters', 135 | // range: '%s must be between %s and %s characters' }, 136 | // number: 137 | // { len: '%s must equal %s', 138 | // min: '%s cannot be less than %s', 139 | // max: '%s cannot be greater than %s', 140 | // range: '%s must be between %s and %s' }, 141 | // array: 142 | // { len: '%s must be exactly %s in length', 143 | // min: '%s cannot be less than %s in length', 144 | // max: '%s cannot be greater than %s in length', 145 | // range: '%s must be between %s and %s in length' }, 146 | // pattern: { mismatch: '%s value %s does not match pattern %s' }, 147 | // clone: [Function: clone] } } 148 | throw [{ field: 'name', message: '错误' }]; 149 | } 150 | } 151 | ] 152 | }; 153 | ``` 154 | 155 | ```yml 156 | name: 157 | - 158 | required: true 159 | - 160 | validator: !!js/function > 161 | function validator(ctx) { 162 | return async function (rule, value, callback, source, options) { 163 | throw [{field:'name', message:'错误'}] 164 | } 165 | } 166 | ``` 167 | 168 | throw error you can use throw or callback 169 | 170 | ## Verify on your controller 171 | 172 | ```js 173 | 'use strict'; 174 | 175 | const Controller = require('egg').Controller; 176 | 177 | class HomeController extends Controller { 178 | async index() { 179 | const query = await this.ctx.verify('login.login', 'query'); 180 | this.ctx.body = 'hi, ' + this.app.plugins.validator.name; 181 | } 182 | } 183 | 184 | module.exports = HomeController; 185 | ``` 186 | 187 | ## api 188 | 189 | ### ctx.verify(path, type) 190 | 191 | * path 192 | * `login.login` -> 'app/schemas/login/login.{json/js/toml/yaml}' 193 | * `login` -> if login has index propety -> `login.index` -> 'app/schemas/login/index.{json/js/toml/yaml}' 194 | * if path type is object use the object 195 | * type 196 | * query -> ctx.request.query 197 | * body -> ctx.request.body 198 | * params -> ctx.params 199 | * undefined -> R.merge(this.params, this.request.query, this.request.body) 200 | * object -> object 201 | * async function -> will be invoke 202 | 203 | ### ctx.docs 204 | 205 | all validator rules 206 | 207 | ### ctx.loadDocs(reload) 208 | 209 | * reload -> boolean true will reload rules file 210 | 211 | ## Questions & Suggestions 212 | 213 | Please open an issue [here](https://github.com/MiYogurt/egg-y-validator/issues). 214 | 215 | ## support superstruct 216 | 217 | but superstruct custom type function not support async function 218 | 219 | [superstruct api](https://github.com/ianstormtaylor/superstruct/blob/master/docs/reference.md#types) 220 | 221 | more info you can see the test file example. 222 | 223 | ### config.default.js 224 | 225 | ```js 226 | exports.validator = { 227 | superstruct: true, 228 | types(ctx) { 229 | // custom you types not support async 230 | return { 231 | email: v => true 232 | }; 233 | }, 234 | async formatter(ctx, error) { 235 | const { data, path, value } = error; 236 | console.log(error); 237 | ctx.type = 'json'; 238 | ctx.status = 400; 239 | ctx.body = { field: path[0], message: '无效的值' }; 240 | console.log(ctx.body); 241 | } 242 | }; 243 | ``` 244 | 245 | ### controller 246 | 247 | ```js 248 | async b() { 249 | const ret = await this.ctx.verify( 250 | { // rules 251 | name: 'string' 252 | }, 253 | async () => { // data 254 | return this.ctx.query; 255 | } 256 | ); 257 | this.ctx.body = 'hi, ' + this.app.plugins.validator.name; 258 | } 259 | async d() { 260 | await this.ctx.verify('haha', async () => { 261 | return { name: '123', email: 'ck123.com' }; 262 | }); 263 | this.ctx.body = 'hi, ' + this.app.plugins.validator.name; 264 | } 265 | ``` 266 | 267 | ### rules 268 | 269 | ```js 270 | module.exports = { 271 | name: 'string', 272 | email: 'email', 273 | types: { 274 | email: v => { 275 | console.log('email verify'); 276 | return Boolean(v.indexOf('@') != -1); 277 | } 278 | } 279 | }; 280 | ``` 281 | 282 | ## License 283 | 284 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FMiYogurt%2Fegg-y-validator.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FMiYogurt%2Fegg-y-validator?ref=badge_large) 285 | --------------------------------------------------------------------------------