├── .github └── workflows │ └── ci-module.yml ├── .gitignore ├── .npmrc ├── API.md ├── LICENSE.md ├── README.md ├── benchmarks ├── .gitignore ├── .npmrc ├── README.md ├── bench.js ├── package.json └── suite.js ├── browser ├── .gitignore ├── .npmrc ├── karma.conf.js ├── lib │ └── version-loader.js ├── package.json ├── tests │ └── index.js ├── webpack.config.js └── webpack.mocha.js ├── eslint.config.js ├── lib ├── annotate.js ├── base.js ├── cache.js ├── common.js ├── compile.js ├── errors.js ├── extend.js ├── index.d.ts ├── index.js ├── manifest.js ├── messages.js ├── modify.js ├── ref.js ├── schemas.js ├── state.js ├── template.js ├── trace.js ├── types │ ├── alternatives.js │ ├── any.js │ ├── array.js │ ├── binary.js │ ├── boolean.js │ ├── date.js │ ├── function.js │ ├── keys.js │ ├── link.js │ ├── number.js │ ├── object.js │ ├── string.js │ └── symbol.js ├── validator.js └── values.js ├── package.json └── test ├── base.js ├── cache.js ├── common.js ├── compile.js ├── errors.js ├── extend.js ├── helper.js ├── index.js ├── index.ts ├── manifest.js ├── modify.js ├── ref.js ├── template.js ├── trace.js ├── types ├── alternatives.js ├── any.js ├── array.js ├── binary.js ├── boolean.js ├── date.js ├── function.js ├── link.js ├── number.js ├── object.js ├── string.js └── symbol.js ├── validator.js └── values.js /.github/workflows/ci-module.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - v17 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: read # for actions/checkout 13 | 14 | jobs: 15 | test: 16 | uses: hapijs/.github/.github/workflows/ci-module.yml@min-node-20-hapi-21 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/package-lock.json 3 | 4 | coverage.* 5 | 6 | **/.DS_Store 7 | **/._* 8 | 9 | **/*.pem 10 | 11 | **/.vs 12 | **/.vscode 13 | **/.idea 14 | 15 | sandbox.js 16 | 17 | dist 18 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2022, Project contributors. 2 | Copyright (c) 2012-2022, Sideway. Inc. 3 | Copyright (c) 2012-2014, Walmart. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | * The names of any contributors may not be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # joi 2 | 3 | #### The most powerful schema description language and data validator for JavaScript. 4 | 5 | ## Installation 6 | `npm install joi` 7 | 8 | ### Visit the [joi.dev](https://joi.dev) Developer Portal for tutorials, documentation, and support 9 | 10 | ## Useful resources 11 | 12 | - [Documentation and API](https://joi.dev/api/) 13 | - [Versions status](https://joi.dev/resources/status/#joi) 14 | - [Changelog](https://joi.dev/resources/changelog/) 15 | - [Project policies](https://joi.dev/policies/) 16 | -------------------------------------------------------------------------------- /benchmarks/.gitignore: -------------------------------------------------------------------------------- 1 | results.json 2 | 3 | -------------------------------------------------------------------------------- /benchmarks/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true 2 | -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # Joi benchmarks 2 | 3 | The benchmarks in this folder are there to do performance regression testing. This is not to compare joi to some other library as this is most of the time meaningless. 4 | 5 | Run it first with `npm run bench-update` to establish a baseline then run `npm run bench` or `npm test` to compare your modifications to the baseline. 6 | 7 | Significant (> 10% by default) are put in colors in the report, the rest should be fairly obvious. 8 | -------------------------------------------------------------------------------- /benchmarks/bench.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Fs = require('fs'); 4 | 5 | const { assert } = require('@hapi/hoek'); 6 | const Benchmark = require('benchmark'); 7 | const Bossy = require('@hapi/bossy'); 8 | const Chalk = require('chalk'); 9 | const CliTable = require('cli-table'); 10 | const D3 = require('d3-format'); 11 | 12 | 13 | const definition = { 14 | c: { 15 | alias: 'compare', 16 | type: 'string' 17 | }, 18 | s: { 19 | alias: 'save', 20 | type: 'string' 21 | }, 22 | t: { 23 | alias: 'threshold', 24 | type: 'number', 25 | default: 10 26 | }, 27 | j: { 28 | alias: 'joi', 29 | type: 'string', 30 | default: '..' 31 | } 32 | }; 33 | 34 | const args = Bossy.parse(definition); 35 | 36 | let compare; 37 | if (args.compare) { 38 | try { 39 | compare = JSON.parse(Fs.readFileSync(args.compare, 'utf8')); 40 | } 41 | catch { 42 | // Ignore error 43 | } 44 | } 45 | 46 | const formats = { 47 | number: D3.format(',d'), 48 | percentage: D3.format('.2f'), 49 | integer: D3.format(',') 50 | }; 51 | 52 | Benchmark.options.minSamples = 100; 53 | 54 | const Joi = require(args.joi); 55 | 56 | const Suite = new Benchmark.Suite('joi'); 57 | 58 | const versionPick = (o) => { 59 | 60 | if (typeof o === 'function') { 61 | return o; 62 | } 63 | 64 | for (const k of Object.keys(o)) { 65 | if (Joi.version.startsWith(k)) { 66 | return o[k]; 67 | } 68 | } 69 | 70 | throw new Error(`Unsupported version ${Joi.version}`); 71 | }; 72 | 73 | const test = ([name, initFn, testFn]) => { 74 | 75 | const [schema, valid, invalid] = versionPick(initFn)(); 76 | 77 | assert(valid === undefined || !testFn(schema, valid).error, 'validation must not fail for: ' + name); 78 | assert(invalid === undefined || testFn(schema, invalid).error, 'validation must fail for: ' + name); 79 | 80 | testFn = versionPick(testFn); 81 | Suite.add(name + (valid !== undefined ? ' (valid)' : ''), () => { 82 | 83 | testFn(schema, valid); 84 | }); 85 | 86 | if (invalid !== undefined) { 87 | Suite.add(name + ' (invalid)', () => { 88 | 89 | testFn(schema, invalid); 90 | }); 91 | } 92 | }; 93 | 94 | require('./suite')(Joi).forEach(test); 95 | 96 | Suite 97 | .on('complete', (benches) => { 98 | 99 | const report = benches.currentTarget.map((bench) => { 100 | 101 | const { name, hz, stats, error } = bench; 102 | return { name, hz, rme: stats.rme, size: stats.sample.length, error }; 103 | }); 104 | 105 | if (args.save) { 106 | Fs.writeFileSync(args.save, JSON.stringify(report, null, 2), 'utf8'); 107 | } 108 | 109 | const tableDefinition = { 110 | head: [Chalk.blue('Name'), '', Chalk.yellow('Ops/sec'), Chalk.yellow('MoE'), Chalk.yellow('Sample size')], 111 | colAligns: ['left', '', 'right', 'right', 'right'] 112 | }; 113 | 114 | if (compare) { 115 | tableDefinition.head.push('', Chalk.cyan('Previous ops/sec'), Chalk.cyan('Previous MoE'), Chalk.cyan('Previous sample size'), '', Chalk.whiteBright('% difference')); 116 | tableDefinition.colAligns.push('', 'right', 'right', 'right', '', 'right'); 117 | } 118 | 119 | const table = new CliTable(tableDefinition); 120 | 121 | table.push(...report.map((s) => { 122 | 123 | const row = [ 124 | s.error ? Chalk.redBright(s.name) : s.name, 125 | '', 126 | formats.number(s.hz), 127 | `± ${formats.percentage(s.rme)} %`, 128 | formats.integer(s.size) 129 | ]; 130 | 131 | if (compare) { 132 | const previousRun = compare.find((run) => run.name === s.name); 133 | if (previousRun) { 134 | const difference = s.hz - previousRun.hz; 135 | const percentage = 100 * difference / previousRun.hz; 136 | const isSignificant = Math.abs(percentage) > args.threshold; 137 | const formattedDifference = `${percentage > 0 ? '+' : ''}${formats.percentage(percentage)} %`; 138 | row.push( 139 | '', 140 | formats.number(previousRun.hz), 141 | `± ${formats.percentage(previousRun.rme)} %`, 142 | formats.integer(previousRun.size), 143 | '', 144 | isSignificant 145 | ? Chalk[difference > 0 ? 'green' : 'red'](formattedDifference) 146 | : formattedDifference 147 | ); 148 | } 149 | } 150 | 151 | return row; 152 | })); 153 | 154 | console.log(table.toString()); 155 | 156 | const errors = report.filter((s) => s.error); 157 | if (errors.length) { 158 | console.log(Chalk.redBright.underline.bold('\nErrors:')); 159 | console.log(errors.map((e) => `> ${Chalk.italic(e.name)}\n${e.error.stack}`).join('\n')); 160 | } 161 | }); 162 | 163 | Suite.run(); 164 | -------------------------------------------------------------------------------- /benchmarks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "benchmarks", 3 | "scripts": { 4 | "test": "npm run bench", 5 | "bench": "node ./bench.js --compare results.json", 6 | "bench-update": "npm run bench -- --save results.json", 7 | "full-bench": "npm run bench-update -- --joi @hapi/joi && npm test" 8 | }, 9 | "dependencies": { 10 | "@hapi/bossy": "^6.0.1", 11 | "@hapi/hoek": "^11.0.2", 12 | "@hapi/joi": "^15.1.0", 13 | "benchmark": "^2.1.4", 14 | "chalk": "^2.4.1", 15 | "cli-table": "^0.3.11", 16 | "d3-format": "^1.3.2", 17 | "joi": "^17.8.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /benchmarks/suite.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (Joi) => [ 4 | [ 5 | 'Simple object', 6 | () => [ 7 | Joi.object({ 8 | id: Joi.string().required(), 9 | level: Joi.string() 10 | .valid('debug', 'info', 'notice') 11 | .required() 12 | }).unknown(false), 13 | { id: '1', level: 'info' }, 14 | { id: '2', level: 'warning' } 15 | ], 16 | (schema, value) => schema.validate(value, { convert: false }) 17 | ], 18 | [ 19 | 'Simple object with inlined prefs', 20 | { 21 | 15: () => [ 22 | Joi.object({ 23 | id: Joi.string().required(), 24 | level: Joi.string() 25 | .valid('debug', 'info', 'notice') 26 | .required() 27 | }).unknown(false).options({ convert: false }), 28 | { id: '1', level: 'info' }, 29 | { id: '2', level: 'warning' } 30 | ], 31 | 16: () => [ 32 | Joi.object({ 33 | id: Joi.string().required(), 34 | level: Joi.string() 35 | .valid('debug', 'info', 'notice') 36 | .required() 37 | }).unknown(false).prefs({ convert: false }), 38 | { id: '1', level: 'info' }, 39 | { id: '2', level: 'warning' } 40 | ], 41 | 17: () => [ 42 | Joi.object({ 43 | id: Joi.string().required(), 44 | level: Joi.string() 45 | .valid('debug', 'info', 'notice') 46 | .required() 47 | }).unknown(false).prefs({ convert: false }), 48 | { id: '1', level: 'info' }, 49 | { id: '2', level: 'warning' } 50 | ] 51 | }, 52 | (schema, value) => schema.validate(value) 53 | ], 54 | [ 55 | 'Schema creation', 56 | () => [], 57 | { 58 | 15: () => 59 | 60 | Joi.object({ 61 | foo: Joi.array().items( 62 | Joi.boolean().required(), 63 | Joi.string().allow(''), 64 | Joi.symbol() 65 | ).single().sparse().required(), 66 | bar: Joi.number().min(12).max(353).default(56).positive(), 67 | baz: Joi.date().timestamp('unix'), 68 | qux: [Joi.func().minArity(12).strict(), Joi.binary().max(345)], 69 | quxx: Joi.string().ip({ version: ['ipv6'] }), 70 | quxxx: [554, 'azerty', true] 71 | }) 72 | .xor('foo', 'bar') 73 | .or('bar', 'baz') 74 | .pattern(/b/, Joi.when('a', { 75 | is: true, 76 | then: Joi.options({ language: { 'any.required': 'oops' } }) 77 | })) 78 | .meta('foo') 79 | .strip() 80 | .default(() => 'foo', 'Def') 81 | .optional(), 82 | 16: () => 83 | 84 | Joi.object({ 85 | foo: Joi.array().items( 86 | Joi.boolean().required(), 87 | Joi.string().allow(''), 88 | Joi.symbol() 89 | ).single().sparse().required(), 90 | bar: Joi.number().min(12).max(353).default(56).positive(), 91 | baz: Joi.date().timestamp('unix'), 92 | qux: [Joi.function().minArity(12).strict(), Joi.binary().max(345)], 93 | quxx: Joi.string().ip({ version: ['ipv6'] }), 94 | quxxx: [554, 'azerty', true] 95 | }) 96 | .xor('foo', 'bar') 97 | .or('bar', 'baz') 98 | .pattern(/b/, Joi.when('a', { 99 | is: true, 100 | then: Joi.prefs({ messages: { 'any.required': 'oops' } }) 101 | })) 102 | .meta('foo') 103 | .strip() 104 | .default(() => 'foo') 105 | .optional(), 106 | 107 | 17: () => 108 | 109 | Joi.object({ 110 | foo: Joi.array().items( 111 | Joi.boolean().required(), 112 | Joi.string().allow(''), 113 | Joi.symbol() 114 | ).single().sparse().required(), 115 | bar: Joi.number().min(12).max(353).default(56).positive(), 116 | baz: Joi.date().timestamp('unix'), 117 | qux: [Joi.function().minArity(12).strict(), Joi.binary().max(345)], 118 | quxx: Joi.string().ip({ version: ['ipv6'] }), 119 | quxxx: [554, 'azerty', true] 120 | }) 121 | .xor('foo', 'bar') 122 | .or('bar', 'baz') 123 | .pattern(/b/, Joi.when('a', { 124 | is: true, 125 | then: Joi.prefs({ messages: { 'any.required': 'oops' } }) 126 | })) 127 | .meta('foo') 128 | .strip() 129 | .default(() => 'foo') 130 | .optional() 131 | } 132 | ], 133 | [ 134 | 'Schema creation with long valid() list', 135 | () => { 136 | 137 | const list = []; 138 | for (let i = 10000; i < 50000; ++i) { 139 | list.push(i.toString()); 140 | } 141 | 142 | return [list.filter((x) => !['12345', '23456', '34567', '456789'].includes(x))]; 143 | }, 144 | (list) => Joi.object().keys({ foo: Joi.string().valid(...list) }) 145 | ], 146 | [ 147 | 'String with long valid() list', 148 | () => { 149 | 150 | const list = []; 151 | for (let i = 10000; i < 50000; ++i) { 152 | list.push(i.toString()); 153 | } 154 | 155 | const schema = Joi.string().valid(...list); 156 | 157 | let i = 0; 158 | const value = () => { 159 | 160 | return `${10000 + (++i % 40000)}`; 161 | }; 162 | 163 | return [schema, value, () => '5000']; 164 | }, 165 | (schema, value) => schema.validate(value()) 166 | ], 167 | [ 168 | 'Complex object', 169 | () => 170 | [ 171 | Joi.object({ 172 | id: Joi.number() 173 | .min(0) 174 | .max(100) 175 | .required(), 176 | 177 | level: Joi.string() 178 | .min(1) 179 | .max(100) 180 | .lowercase() 181 | .required(), 182 | 183 | tags: Joi.array() 184 | .items(Joi.boolean()) 185 | .min(2) 186 | }) 187 | .unknown(false), 188 | { id: 1, level: 'info', tags: [true, false] } 189 | ], 190 | (schema, value) => schema.validate(value) 191 | ], 192 | [ 193 | 'Dependency validation', 194 | () => [ 195 | Joi.object({ 196 | 'a': Joi.string(), 197 | 'b': Joi.string() 198 | }), 199 | { a: 'foo', b: 'bar' } 200 | ], 201 | (schema, value) => schema.validate(value) 202 | ], 203 | [ 204 | 'Parsing of exponential numbers', 205 | () => [ 206 | Joi.number(), 207 | '+001231.0133210e003', 208 | '90071992547409811e-1' 209 | ], 210 | (schema, value) => schema.validate(value) 211 | ] 212 | ]; 213 | -------------------------------------------------------------------------------- /browser/.gitignore: -------------------------------------------------------------------------------- 1 | stats.json 2 | 3 | -------------------------------------------------------------------------------- /browser/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /browser/karma.conf.js: -------------------------------------------------------------------------------- 1 | const Path = require('path'); 2 | 3 | const WebpackMocha = require('./webpack.mocha'); 4 | 5 | const internals = { 6 | libs: Path.join(__dirname, '../lib/**/*.js'), 7 | tests: Path.join(__dirname, './tests/**/*.js') 8 | }; 9 | 10 | module.exports = function (config) { 11 | config.set({ 12 | basePath: '', 13 | frameworks: ['mocha'], 14 | files: [ 15 | internals.libs, 16 | internals.tests 17 | ], 18 | preprocessors: { 19 | [internals.libs]: ['webpack', 'sourcemap'], 20 | [internals.tests]: ['webpack', 'sourcemap'] 21 | }, 22 | reporters: ['progress'], 23 | port: 9876, 24 | colors: true, 25 | logLevel: config.LOG_ERROR, 26 | autoWatch: true, 27 | browsers: ['ChromeHeadless'], 28 | singleRun: true, 29 | concurrency: Infinity, 30 | webpack: WebpackMocha, 31 | webpackMiddleware: { 32 | noInfo: true, 33 | stats: { 34 | chunks: false 35 | } 36 | }, 37 | }) 38 | }; 39 | -------------------------------------------------------------------------------- /browser/lib/version-loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Pkg = require('../../package.json'); 4 | 5 | 6 | const internals = {}; 7 | 8 | 9 | module.exports = function () { 10 | 11 | return `{ "version": "${Pkg.version}" }`; 12 | }; 13 | -------------------------------------------------------------------------------- /browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "webpack --mode production", 4 | "build-dev": "webpack --mode development", 5 | "build-analyze": "webpack --mode production --stats-optimization-bailout --profile --json > stats.json", 6 | "postbuild-analyze": "webpack-bundle-analyzer stats.json", 7 | "test": "karma start" 8 | }, 9 | "devDependencies": { 10 | "@babel/core": "^7.21.0", 11 | "@babel/plugin-proposal-class-properties": "^7.18.6", 12 | "@babel/preset-env": "^7.20.2", 13 | "@mixer/webpack-bundle-compare": "^0.1.1", 14 | "assert": "^2.0.0", 15 | "babel-loader": "^9.1.2", 16 | "karma": "^6.4.1", 17 | "karma-chrome-launcher": "^3.1.1", 18 | "karma-mocha": "^2.0.1", 19 | "karma-sourcemap-loader": "^0.4.0", 20 | "karma-webpack": "^5.0.0", 21 | "mocha": "^8.4.0", 22 | "mocha-loader": "^5.1.5", 23 | "util": "^0.12.5", 24 | "webpack": "^5.76.1", 25 | "webpack-bundle-analyzer": "^3.4.1", 26 | "webpack-cli": "^5.0.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /browser/tests/index.js: -------------------------------------------------------------------------------- 1 | const Assert = require('assert'); 2 | 3 | const Joi = require('../..'); 4 | 5 | 6 | describe('Joi', () => { 7 | 8 | it('should be able to create schemas', () => { 9 | 10 | Joi.boolean().truthy('true'); 11 | Joi.number().min(5).max(10).multiple(2); 12 | Joi.array().items(Joi.number().required()); 13 | Joi.object({ 14 | key: Joi.string().required() 15 | }); 16 | }); 17 | 18 | it('should be able to validate data', () => { 19 | 20 | const schema = Joi.string().min(5); 21 | Assert.ok(!schema.validate('123456').error); 22 | Assert.ok(schema.validate('123').error); 23 | }); 24 | 25 | it('fails using binary', () => { 26 | 27 | Assert.throws(() => Joi.binary().min(1)); 28 | Assert.strictEqual(Joi.binary, undefined); 29 | }); 30 | 31 | it('validates email', () => { 32 | 33 | const schema = Joi.string().email({ tlds: false }).required(); 34 | Assert.ok(!schema.validate('test@example.com').error); 35 | Assert.ok(schema.validate('test@example.com ').error); 36 | Assert.ok(!schema.validate('伊昭傑@郵件.商務').error); 37 | 38 | const schema2 = Joi.string().email({ tlds: { allow: false } }).required(); 39 | Assert.ok(!schema2.validate('test@example.com').error); 40 | Assert.ok(schema2.validate('test@example.com ').error); 41 | Assert.ok(!schema2.validate('伊昭傑@郵件.商務').error); 42 | }); 43 | 44 | it('validates domain', () => { 45 | 46 | const schema = Joi.string().domain().required(); 47 | Assert.ok(!schema.validate('example.com').error); 48 | Assert.ok(schema.validate('example.com ').error); 49 | Assert.ok(!schema.validate('example.商務').error); 50 | 51 | const schema2 = Joi.string().domain({ tlds: { allow: false } }).required(); 52 | Assert.ok(!schema2.validate('example.com').error); 53 | Assert.ok(schema2.validate('example.com ').error); 54 | Assert.ok(!schema2.validate('example.商務').error); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /browser/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Path = require('path'); 4 | 5 | const Webpack = require('webpack'); 6 | const { BundleComparisonPlugin } = require('@mixer/webpack-bundle-compare'); 7 | 8 | 9 | module.exports = { 10 | entry: '../lib/index.js', 11 | output: { 12 | filename: 'joi-browser.min.js', 13 | path: Path.join(__dirname, '../dist'), 14 | library: 'joi', 15 | libraryTarget: 'umd' 16 | }, 17 | plugins: [ 18 | new Webpack.DefinePlugin({ 19 | Buffer: false 20 | }), 21 | new BundleComparisonPlugin({ 22 | file: '../stats.msp.gz', 23 | format: 'msgpack', 24 | gzip: true, 25 | }) 26 | ], 27 | module: { 28 | rules: [ 29 | { 30 | use: './lib/version-loader', 31 | include: [ 32 | Path.join(__dirname, '../package.json') 33 | ] 34 | }, 35 | { 36 | test: /\.js$/, 37 | use: { 38 | loader: 'babel-loader', 39 | options: { 40 | presets: [ 41 | [ 42 | '@babel/preset-env', 43 | { 44 | 'targets': '> 1%, not IE 11, not dead' 45 | } 46 | ] 47 | ], 48 | plugins: [ 49 | '@babel/plugin-transform-class-properties', 50 | '@babel/plugin-transform-optional-chaining', 51 | '@babel/plugin-transform-nullish-coalescing-operator', 52 | ] 53 | } 54 | } 55 | }, 56 | { 57 | test: /@(hapi|sideway)\//, 58 | sideEffects: false 59 | } 60 | ] 61 | }, 62 | node: false, 63 | resolve: { 64 | alias: { 65 | [Path.join(__dirname, '../lib/annotate.js')]: false, 66 | [Path.join(__dirname, '../lib/manifest.js')]: false, 67 | [Path.join(__dirname, '../lib/trace.js')]: false, 68 | [Path.join(__dirname, '../lib/types/binary.js')]: false, 69 | [Path.join(__dirname, '../node_modules/@hapi/tlds/esm/index.js')]: false, 70 | [Path.join(__dirname, '../node_modules/@hapi/address/esm/decode.js')]: false, 71 | [Path.join(__dirname, '../node_modules/@hapi/hoek/lib/bench.js')]: false, 72 | [Path.join(__dirname, '../node_modules/@hapi/hoek/lib/block.js')]: false, 73 | [Path.join(__dirname, '../node_modules/@hapi/hoek/lib/contain.js')]: false, 74 | [Path.join(__dirname, '../node_modules/@hapi/hoek/lib/flatten.js')]: false, 75 | [Path.join(__dirname, '../node_modules/@hapi/hoek/lib/intersect.js')]: false, 76 | [Path.join(__dirname, '../node_modules/@hapi/hoek/lib/isPromise.js')]: false, 77 | [Path.join(__dirname, '../node_modules/@hapi/hoek/lib/escapeHeaderAttribute.js')]: false, 78 | [Path.join(__dirname, '../node_modules/@hapi/hoek/lib/escapeJson.js')]: false, 79 | [Path.join(__dirname, '../node_modules/@hapi/hoek/lib/once.js')]: false, 80 | [Path.join(__dirname, '../node_modules/@hapi/hoek/lib/reachTemplate.js')]: false, 81 | [Path.join(__dirname, '../node_modules/@hapi/hoek/lib/wait.js')]: false, 82 | }, 83 | fallback: { 84 | url: false, 85 | util: false, 86 | } 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /browser/webpack.mocha.js: -------------------------------------------------------------------------------- 1 | const Path = require('path'); 2 | const Webpack = require('webpack'); 3 | 4 | const WebpackConfig = require('./webpack.config'); 5 | 6 | WebpackConfig.mode = 'production'; 7 | WebpackConfig.devServer = { 8 | host: 'localhost', 9 | port: 8081 10 | }; 11 | WebpackConfig.devtool = 'inline-source-map'; 12 | WebpackConfig.entry = [ 13 | `mocha-loader!${Path.join(__dirname, 'tests')}` 14 | ]; 15 | WebpackConfig.output.publicPath = 'http://localhost:8081'; 16 | WebpackConfig.module.rules[1].use.options.presets[0][1].exclude = [ 17 | '@babel/plugin-transform-regenerator' 18 | ]; 19 | 20 | // Used in testing. 21 | WebpackConfig.plugins.push(new Webpack.DefinePlugin({ 22 | 'process.env.NODE_DEBUG': false, 23 | })); 24 | WebpackConfig.node = { 25 | global: true, 26 | }; 27 | WebpackConfig.resolve.fallback.util = require.resolve('util/'); 28 | WebpackConfig.resolve.fallback.assert = require.resolve('assert/'); 29 | 30 | module.exports = WebpackConfig; 31 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const HapiPlugin = require('@hapi/eslint-plugin'); 4 | 5 | module.exports = [ 6 | { 7 | ignores: ['browser', 'dist', 'sandbox.js'] 8 | }, 9 | ...HapiPlugin.configs.module 10 | ]; 11 | -------------------------------------------------------------------------------- /lib/annotate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { clone } = require('@hapi/hoek'); 4 | 5 | const Common = require('./common'); 6 | 7 | 8 | const internals = { 9 | annotations: Symbol('annotations') 10 | }; 11 | 12 | 13 | exports.error = function (stripColorCodes) { 14 | 15 | if (!this._original || 16 | typeof this._original !== 'object') { 17 | 18 | return this.details[0].message; 19 | } 20 | 21 | const redFgEscape = stripColorCodes ? '' : '\u001b[31m'; 22 | const redBgEscape = stripColorCodes ? '' : '\u001b[41m'; 23 | const endColor = stripColorCodes ? '' : '\u001b[0m'; 24 | 25 | const obj = clone(this._original); 26 | 27 | for (let i = this.details.length - 1; i >= 0; --i) { // Reverse order to process deepest child first 28 | const pos = i + 1; 29 | const error = this.details[i]; 30 | const path = error.path; 31 | let node = obj; 32 | for (let j = 0; ; ++j) { 33 | const seg = path[j]; 34 | 35 | if (Common.isSchema(node)) { 36 | node = node.clone(); // joi schemas are not cloned by hoek, we have to take this extra step 37 | } 38 | 39 | if (j + 1 < path.length && 40 | typeof node[seg] !== 'string') { 41 | 42 | node = node[seg]; 43 | } 44 | else { 45 | const refAnnotations = node[internals.annotations] || { errors: {}, missing: {} }; 46 | node[internals.annotations] = refAnnotations; 47 | 48 | const cacheKey = seg || error.context.key; 49 | 50 | if (node[seg] !== undefined) { 51 | refAnnotations.errors[cacheKey] = refAnnotations.errors[cacheKey] || []; 52 | refAnnotations.errors[cacheKey].push(pos); 53 | } 54 | else { 55 | refAnnotations.missing[cacheKey] = pos; 56 | } 57 | 58 | break; 59 | } 60 | } 61 | } 62 | 63 | const replacers = { 64 | key: /_\$key\$_([, \d]+)_\$end\$_"/g, 65 | missing: /"_\$miss\$_([^|]+)\|(\d+)_\$end\$_": "__missing__"/g, 66 | arrayIndex: /\s*"_\$idx\$_([, \d]+)_\$end\$_",?\n(.*)/g, 67 | specials: /"\[(NaN|Symbol.*|-?Infinity|function.*|\(.*)]"/g 68 | }; 69 | 70 | let message = internals.safeStringify(obj, 2) 71 | .replace(replacers.key, ($0, $1) => `" ${redFgEscape}[${$1}]${endColor}`) 72 | .replace(replacers.missing, ($0, $1, $2) => `${redBgEscape}"${$1}"${endColor}${redFgEscape} [${$2}]: -- missing --${endColor}`) 73 | .replace(replacers.arrayIndex, ($0, $1, $2) => `\n${$2} ${redFgEscape}[${$1}]${endColor}`) 74 | .replace(replacers.specials, ($0, $1) => $1); 75 | 76 | message = `${message}\n${redFgEscape}`; 77 | 78 | for (let i = 0; i < this.details.length; ++i) { 79 | const pos = i + 1; 80 | message = `${message}\n[${pos}] ${this.details[i].message}`; 81 | } 82 | 83 | message = message + endColor; 84 | 85 | return message; 86 | }; 87 | 88 | 89 | // Inspired by json-stringify-safe 90 | 91 | internals.safeStringify = function (obj, spaces) { 92 | 93 | return JSON.stringify(obj, internals.serializer(), spaces); 94 | }; 95 | 96 | 97 | internals.serializer = function () { 98 | 99 | const keys = []; 100 | const stack = []; 101 | 102 | const cycleReplacer = (key, value) => { 103 | 104 | if (stack[0] === value) { 105 | return '[Circular ~]'; 106 | } 107 | 108 | return '[Circular ~.' + keys.slice(0, stack.indexOf(value)).join('.') + ']'; 109 | }; 110 | 111 | return function (key, value) { 112 | 113 | if (stack.length > 0) { 114 | const thisPos = stack.indexOf(this); 115 | if (~thisPos) { 116 | stack.length = thisPos + 1; 117 | keys.length = thisPos + 1; 118 | keys[thisPos] = key; 119 | } 120 | else { 121 | stack.push(this); 122 | keys.push(key); 123 | } 124 | 125 | if (~stack.indexOf(value)) { 126 | value = cycleReplacer.call(this, key, value); 127 | } 128 | } 129 | else { 130 | stack.push(value); 131 | } 132 | 133 | if (value) { 134 | const annotations = value[internals.annotations]; 135 | if (annotations) { 136 | if (Array.isArray(value)) { 137 | const annotated = []; 138 | 139 | for (let i = 0; i < value.length; ++i) { 140 | if (annotations.errors[i]) { 141 | annotated.push(`_$idx$_${annotations.errors[i].sort().join(', ')}_$end$_`); 142 | } 143 | 144 | annotated.push(value[i]); 145 | } 146 | 147 | value = annotated; 148 | } 149 | else { 150 | for (const errorKey in annotations.errors) { 151 | value[`${errorKey}_$key$_${annotations.errors[errorKey].sort().join(', ')}_$end$_`] = value[errorKey]; 152 | value[errorKey] = undefined; 153 | } 154 | 155 | for (const missingKey in annotations.missing) { 156 | value[`_$miss$_${missingKey}|${annotations.missing[missingKey]}_$end$_`] = '__missing__'; 157 | } 158 | } 159 | 160 | return value; 161 | } 162 | } 163 | 164 | if (value === Infinity || 165 | value === -Infinity || 166 | Number.isNaN(value) || 167 | typeof value === 'function' || 168 | typeof value === 'symbol') { 169 | 170 | return '[' + value.toString() + ']'; 171 | } 172 | 173 | return value; 174 | }; 175 | }; 176 | -------------------------------------------------------------------------------- /lib/cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { assert, clone } = require('@hapi/hoek'); 4 | 5 | const Common = require('./common'); 6 | 7 | 8 | const internals = { 9 | max: 1000, 10 | supported: new Set(['undefined', 'boolean', 'number', 'string']) 11 | }; 12 | 13 | 14 | exports.provider = { 15 | 16 | provision(options) { 17 | 18 | return new internals.Cache(options); 19 | } 20 | }; 21 | 22 | 23 | // Least Recently Used (LRU) Cache 24 | 25 | internals.Cache = class { 26 | 27 | constructor(options = {}) { 28 | 29 | Common.assertOptions(options, ['max']); 30 | assert(options.max === undefined || options.max && options.max > 0 && isFinite(options.max), 'Invalid max cache size'); 31 | 32 | this._max = options.max || internals.max; 33 | 34 | this._map = new Map(); // Map of nodes by key 35 | this._list = new internals.List(); // List of nodes (most recently used in head) 36 | } 37 | 38 | get length() { 39 | 40 | return this._map.size; 41 | } 42 | 43 | set(key, value) { 44 | 45 | if (key !== null && 46 | !internals.supported.has(typeof key)) { 47 | 48 | return; 49 | } 50 | 51 | let node = this._map.get(key); 52 | if (node) { 53 | node.value = value; 54 | this._list.first(node); 55 | return; 56 | } 57 | 58 | node = this._list.unshift({ key, value }); 59 | this._map.set(key, node); 60 | this._compact(); 61 | } 62 | 63 | get(key) { 64 | 65 | const node = this._map.get(key); 66 | if (node) { 67 | this._list.first(node); 68 | return clone(node.value); 69 | } 70 | } 71 | 72 | _compact() { 73 | 74 | if (this._map.size > this._max) { 75 | const node = this._list.pop(); 76 | this._map.delete(node.key); 77 | } 78 | } 79 | }; 80 | 81 | 82 | internals.List = class { 83 | 84 | constructor() { 85 | 86 | this.tail = null; 87 | this.head = null; 88 | } 89 | 90 | unshift(node) { 91 | 92 | node.next = null; 93 | node.prev = this.head; 94 | 95 | if (this.head) { 96 | this.head.next = node; 97 | } 98 | 99 | this.head = node; 100 | 101 | if (!this.tail) { 102 | this.tail = node; 103 | } 104 | 105 | return node; 106 | } 107 | 108 | first(node) { 109 | 110 | if (node === this.head) { 111 | return; 112 | } 113 | 114 | this._remove(node); 115 | this.unshift(node); 116 | } 117 | 118 | pop() { 119 | 120 | return this._remove(this.tail); 121 | } 122 | 123 | _remove(node) { 124 | 125 | const { next, prev } = node; 126 | 127 | next.prev = prev; 128 | 129 | if (prev) { 130 | prev.next = next; 131 | } 132 | 133 | if (node === this.tail) { 134 | this.tail = next; 135 | } 136 | 137 | node.prev = null; 138 | node.next = null; 139 | 140 | return node; 141 | } 142 | }; 143 | -------------------------------------------------------------------------------- /lib/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { assert: Assert, AssertError } = require('@hapi/hoek'); 4 | 5 | const Pkg = require('../package.json'); 6 | 7 | let Messages; 8 | let Schemas; 9 | 10 | 11 | const internals = { 12 | isoDate: /^(?:[-+]\d{2})?(?:\d{4}(?!\d{2}\b))(?:(-?)(?:(?:0[1-9]|1[0-2])(?:\1(?:[12]\d|0[1-9]|3[01]))?|W(?:[0-4]\d|5[0-2])(?:-?[1-7])?|(?:00[1-9]|0[1-9]\d|[12]\d{2}|3(?:[0-5]\d|6[1-6])))(?![T]$|[T][\d]+Z$)(?:[T\s](?:(?:(?:[01]\d|2[0-3])(?:(:?)[0-5]\d)?|24\:?00)(?:[.,]\d+(?!:))?)(?:\2[0-5]\d(?:[.,]\d+)?)?(?:[Z]|(?:[+-])(?:[01]\d|2[0-3])(?::?[0-5]\d)?)?)?)?$/ 13 | }; 14 | 15 | 16 | exports.version = Pkg.version; 17 | 18 | 19 | exports.defaults = { 20 | abortEarly: true, 21 | allowUnknown: false, 22 | artifacts: false, 23 | cache: true, 24 | context: null, 25 | convert: true, 26 | dateFormat: 'iso', 27 | errors: { 28 | escapeHtml: false, 29 | label: 'path', 30 | language: null, 31 | render: true, 32 | stack: false, 33 | wrap: { 34 | label: '"', 35 | array: '[]' 36 | } 37 | }, 38 | externals: true, 39 | messages: {}, 40 | nonEnumerables: false, 41 | noDefaults: false, 42 | presence: 'optional', 43 | skipFunctions: false, 44 | stripUnknown: false, 45 | warnings: false 46 | }; 47 | 48 | 49 | exports.symbols = { 50 | any: Symbol.for('@hapi/joi/schema'), // Used to internally identify any-based types (shared with other joi versions) 51 | arraySingle: Symbol('arraySingle'), 52 | deepDefault: Symbol('deepDefault'), 53 | errors: Symbol('errors'), 54 | literal: Symbol('literal'), 55 | override: Symbol('override'), 56 | parent: Symbol('parent'), 57 | prefs: Symbol('prefs'), 58 | ref: Symbol('ref'), 59 | template: Symbol('template'), 60 | values: Symbol('values') 61 | }; 62 | 63 | 64 | exports.assertOptions = function (options, keys, name = 'Options') { 65 | 66 | Assert(options && typeof options === 'object' && !Array.isArray(options), 'Options must be of type object'); 67 | const unknownKeys = Object.keys(options).filter((k) => !keys.includes(k)); 68 | Assert(unknownKeys.length === 0, `${name} contain unknown keys: ${unknownKeys}`); 69 | }; 70 | 71 | 72 | exports.checkPreferences = function (prefs) { 73 | 74 | Schemas = Schemas || require('./schemas'); 75 | 76 | const result = Schemas.preferences.validate(prefs); 77 | 78 | if (result.error) { 79 | throw new AssertError([result.error.details[0].message]); 80 | } 81 | }; 82 | 83 | 84 | exports.compare = function (a, b, operator) { 85 | 86 | switch (operator) { 87 | case '=': return a === b; 88 | case '>': return a > b; 89 | case '<': return a < b; 90 | case '>=': return a >= b; 91 | case '<=': return a <= b; 92 | } 93 | }; 94 | 95 | 96 | exports.default = function (value, defaultValue) { 97 | 98 | return value === undefined ? defaultValue : value; 99 | }; 100 | 101 | 102 | exports.isIsoDate = function (date) { 103 | 104 | return internals.isoDate.test(date); 105 | }; 106 | 107 | 108 | exports.isNumber = function (value) { 109 | 110 | return typeof value === 'number' && !isNaN(value); 111 | }; 112 | 113 | 114 | exports.isResolvable = function (obj) { 115 | 116 | if (!obj) { 117 | return false; 118 | } 119 | 120 | return obj[exports.symbols.ref] || obj[exports.symbols.template]; 121 | }; 122 | 123 | 124 | exports.isSchema = function (schema, options = {}) { 125 | 126 | const any = schema && schema[exports.symbols.any]; 127 | if (!any) { 128 | return false; 129 | } 130 | 131 | Assert(options.legacy || any.version === exports.version, 'Cannot mix different versions of joi schemas'); 132 | return true; 133 | }; 134 | 135 | 136 | exports.isValues = function (obj) { 137 | 138 | return obj[exports.symbols.values]; 139 | }; 140 | 141 | 142 | exports.limit = function (value) { 143 | 144 | return Number.isSafeInteger(value) && value >= 0; 145 | }; 146 | 147 | 148 | exports.preferences = function (target, source) { 149 | 150 | Messages = Messages || require('./messages'); 151 | 152 | target = target || {}; 153 | source = source || {}; 154 | 155 | const merged = Object.assign({}, target, source); 156 | if (source.errors && 157 | target.errors) { 158 | 159 | merged.errors = Object.assign({}, target.errors, source.errors); 160 | merged.errors.wrap = Object.assign({}, target.errors.wrap, source.errors.wrap); 161 | } 162 | 163 | if (source.messages) { 164 | merged.messages = Messages.compile(source.messages, target.messages); 165 | } 166 | 167 | delete merged[exports.symbols.prefs]; 168 | return merged; 169 | }; 170 | 171 | 172 | exports.tryWithPath = function (fn, key, options = {}) { 173 | 174 | try { 175 | return fn(); 176 | } 177 | catch (err) { 178 | if (err.path !== undefined) { 179 | err.path = key + '.' + err.path; 180 | } 181 | else { 182 | err.path = key; 183 | } 184 | 185 | if (options.append) { 186 | err.message = `${err.message} (${err.path})`; 187 | } 188 | 189 | throw err; 190 | } 191 | }; 192 | 193 | 194 | exports.validateArg = function (value, label, { assert, message }) { 195 | 196 | if (exports.isSchema(assert)) { 197 | const result = assert.validate(value); 198 | if (!result.error) { 199 | return; 200 | } 201 | 202 | return result.error.message; 203 | } 204 | else if (!assert(value)) { 205 | return label ? `${label} ${message}` : message; 206 | } 207 | }; 208 | 209 | 210 | exports.verifyFlat = function (args, method) { 211 | 212 | for (const arg of args) { 213 | Assert(!Array.isArray(arg), 'Method no longer accepts array arguments:', method); 214 | } 215 | }; 216 | -------------------------------------------------------------------------------- /lib/compile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { assert } = require('@hapi/hoek'); 4 | 5 | const Common = require('./common'); 6 | const Ref = require('./ref'); 7 | 8 | 9 | const internals = {}; 10 | 11 | 12 | exports.schema = function (Joi, config, options = {}) { 13 | 14 | Common.assertOptions(options, ['appendPath', 'override']); 15 | 16 | try { 17 | return internals.schema(Joi, config, options); 18 | } 19 | catch (err) { 20 | if (options.appendPath && 21 | err.path !== undefined) { 22 | 23 | err.message = `${err.message} (${err.path})`; 24 | } 25 | 26 | throw err; 27 | } 28 | }; 29 | 30 | 31 | internals.schema = function (Joi, config, options) { 32 | 33 | assert(config !== undefined, 'Invalid undefined schema'); 34 | 35 | if (Array.isArray(config)) { 36 | assert(config.length, 'Invalid empty array schema'); 37 | 38 | if (config.length === 1) { 39 | config = config[0]; 40 | } 41 | } 42 | 43 | const valid = (base, ...values) => { 44 | 45 | if (options.override !== false) { 46 | return base.valid(Joi.override, ...values); 47 | } 48 | 49 | return base.valid(...values); 50 | }; 51 | 52 | if (internals.simple(config)) { 53 | return valid(Joi, config); 54 | } 55 | 56 | if (typeof config === 'function') { 57 | return Joi.custom(config); 58 | } 59 | 60 | assert(typeof config === 'object', 'Invalid schema content:', typeof config); 61 | 62 | if (Common.isResolvable(config)) { 63 | return valid(Joi, config); 64 | } 65 | 66 | if (Common.isSchema(config)) { 67 | return config; 68 | } 69 | 70 | if (Array.isArray(config)) { 71 | for (const item of config) { 72 | if (!internals.simple(item)) { 73 | return Joi.alternatives().try(...config); 74 | } 75 | } 76 | 77 | return valid(Joi, ...config); 78 | } 79 | 80 | if (config instanceof RegExp) { 81 | return Joi.string().regex(config); 82 | } 83 | 84 | if (config instanceof Date) { 85 | return valid(Joi.date(), config); 86 | } 87 | 88 | assert(Object.getPrototypeOf(config) === Object.getPrototypeOf({}), 'Schema can only contain plain objects'); 89 | 90 | return Joi.object().keys(config); 91 | }; 92 | 93 | 94 | exports.ref = function (id, options) { 95 | 96 | return Ref.isRef(id) ? id : Ref.create(id, options); 97 | }; 98 | 99 | 100 | exports.compile = function (root, schema, options = {}) { 101 | 102 | Common.assertOptions(options, ['legacy']); 103 | 104 | // Compiled by any supported version 105 | 106 | const any = schema && schema[Common.symbols.any]; 107 | if (any) { 108 | assert(options.legacy || any.version === Common.version, 'Cannot mix different versions of joi schemas:', any.version, Common.version); 109 | return schema; 110 | } 111 | 112 | // Uncompiled root 113 | 114 | if (typeof schema !== 'object' || 115 | !options.legacy) { 116 | 117 | return exports.schema(root, schema, { appendPath: true }); // Will error if schema contains other versions 118 | } 119 | 120 | // Scan schema for compiled parts 121 | 122 | const compiler = internals.walk(schema); 123 | if (!compiler) { 124 | return exports.schema(root, schema, { appendPath: true }); 125 | } 126 | 127 | return compiler.compile(compiler.root, schema); 128 | }; 129 | 130 | 131 | internals.walk = function (schema) { 132 | 133 | if (typeof schema !== 'object') { 134 | return null; 135 | } 136 | 137 | if (Array.isArray(schema)) { 138 | for (const item of schema) { 139 | const compiler = internals.walk(item); 140 | if (compiler) { 141 | return compiler; 142 | } 143 | } 144 | 145 | return null; 146 | } 147 | 148 | const any = schema[Common.symbols.any]; 149 | if (any) { 150 | return { root: schema[any.root], compile: any.compile }; 151 | } 152 | 153 | assert(Object.getPrototypeOf(schema) === Object.getPrototypeOf({}), 'Schema can only contain plain objects'); 154 | 155 | for (const key in schema) { 156 | const compiler = internals.walk(schema[key]); 157 | if (compiler) { 158 | return compiler; 159 | } 160 | } 161 | 162 | return null; 163 | }; 164 | 165 | 166 | internals.simple = function (value) { 167 | 168 | return value === null || ['boolean', 'string', 'number'].includes(typeof value); 169 | }; 170 | 171 | 172 | exports.when = function (schema, condition, options) { 173 | 174 | if (options === undefined) { 175 | assert(condition && typeof condition === 'object', 'Missing options'); 176 | 177 | options = condition; 178 | condition = Ref.create('.'); 179 | } 180 | 181 | if (Array.isArray(options)) { 182 | options = { switch: options }; 183 | } 184 | 185 | Common.assertOptions(options, ['is', 'not', 'then', 'otherwise', 'switch', 'break']); 186 | 187 | // Schema condition 188 | 189 | if (Common.isSchema(condition)) { 190 | assert(options.is === undefined, '"is" can not be used with a schema condition'); 191 | assert(options.not === undefined, '"not" can not be used with a schema condition'); 192 | assert(options.switch === undefined, '"switch" can not be used with a schema condition'); 193 | 194 | return internals.condition(schema, { is: condition, then: options.then, otherwise: options.otherwise, break: options.break }); 195 | } 196 | 197 | // Single condition 198 | 199 | assert(Ref.isRef(condition) || typeof condition === 'string', 'Invalid condition:', condition); 200 | assert(options.not === undefined || options.is === undefined, 'Cannot combine "is" with "not"'); 201 | 202 | if (options.switch === undefined) { 203 | let rule = options; 204 | if (options.not !== undefined) { 205 | rule = { is: options.not, then: options.otherwise, otherwise: options.then, break: options.break }; 206 | } 207 | 208 | let is = rule.is !== undefined ? schema.$_compile(rule.is) : schema.$_root.invalid(null, false, 0, '').required(); 209 | assert(rule.then !== undefined || rule.otherwise !== undefined, 'options must have at least one of "then", "otherwise", or "switch"'); 210 | assert(rule.break === undefined || rule.then === undefined || rule.otherwise === undefined, 'Cannot specify then, otherwise, and break all together'); 211 | 212 | if (options.is !== undefined && 213 | !Ref.isRef(options.is) && 214 | !Common.isSchema(options.is)) { 215 | 216 | is = is.required(); // Only apply required if this wasn't already a schema or a ref 217 | } 218 | 219 | return internals.condition(schema, { ref: exports.ref(condition), is, then: rule.then, otherwise: rule.otherwise, break: rule.break }); 220 | } 221 | 222 | // Switch statement 223 | 224 | assert(Array.isArray(options.switch), '"switch" must be an array'); 225 | assert(options.is === undefined, 'Cannot combine "switch" with "is"'); 226 | assert(options.not === undefined, 'Cannot combine "switch" with "not"'); 227 | assert(options.then === undefined, 'Cannot combine "switch" with "then"'); 228 | 229 | const rule = { 230 | ref: exports.ref(condition), 231 | switch: [], 232 | break: options.break 233 | }; 234 | 235 | for (let i = 0; i < options.switch.length; ++i) { 236 | const test = options.switch[i]; 237 | const last = i === options.switch.length - 1; 238 | 239 | Common.assertOptions(test, last ? ['is', 'then', 'otherwise'] : ['is', 'then']); 240 | 241 | assert(test.is !== undefined, 'Switch statement missing "is"'); 242 | assert(test.then !== undefined, 'Switch statement missing "then"'); 243 | 244 | const item = { 245 | is: schema.$_compile(test.is), 246 | then: schema.$_compile(test.then) 247 | }; 248 | 249 | if (!Ref.isRef(test.is) && 250 | !Common.isSchema(test.is)) { 251 | 252 | item.is = item.is.required(); // Only apply required if this wasn't already a schema or a ref 253 | } 254 | 255 | if (last) { 256 | assert(options.otherwise === undefined || test.otherwise === undefined, 'Cannot specify "otherwise" inside and outside a "switch"'); 257 | const otherwise = options.otherwise !== undefined ? options.otherwise : test.otherwise; 258 | if (otherwise !== undefined) { 259 | assert(rule.break === undefined, 'Cannot specify both otherwise and break'); 260 | item.otherwise = schema.$_compile(otherwise); 261 | } 262 | } 263 | 264 | rule.switch.push(item); 265 | } 266 | 267 | return rule; 268 | }; 269 | 270 | 271 | internals.condition = function (schema, condition) { 272 | 273 | for (const key of ['then', 'otherwise']) { 274 | if (condition[key] === undefined) { 275 | delete condition[key]; 276 | } 277 | else { 278 | condition[key] = schema.$_compile(condition[key]); 279 | } 280 | } 281 | 282 | return condition; 283 | }; 284 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Annotate = require('./annotate'); 4 | const Common = require('./common'); 5 | const Template = require('./template'); 6 | 7 | 8 | const internals = {}; 9 | 10 | 11 | exports.Report = class { 12 | 13 | constructor(code, value, local, flags, messages, state, prefs) { 14 | 15 | this.code = code; 16 | this.flags = flags; 17 | this.messages = messages; 18 | this.path = state.path; 19 | this.prefs = prefs; 20 | this.state = state; 21 | this.value = value; 22 | 23 | this.message = null; 24 | this.template = null; 25 | 26 | this.local = local || {}; 27 | this.local.label = exports.label(this.flags, this.state, this.prefs, this.messages); 28 | 29 | if (this.value !== undefined && 30 | !this.local.hasOwnProperty('value')) { 31 | 32 | this.local.value = this.value; 33 | } 34 | 35 | if (this.path.length) { 36 | const key = this.path[this.path.length - 1]; 37 | if (typeof key !== 'object') { 38 | this.local.key = key; 39 | } 40 | } 41 | } 42 | 43 | _setTemplate(template) { 44 | 45 | this.template = template; 46 | 47 | if (!this.flags.label && 48 | this.path.length === 0) { 49 | 50 | const localized = this._template(this.template, 'root'); 51 | if (localized) { 52 | this.local.label = localized; 53 | } 54 | } 55 | } 56 | 57 | toString() { 58 | 59 | if (this.message) { 60 | return this.message; 61 | } 62 | 63 | const code = this.code; 64 | 65 | if (!this.prefs.errors.render) { 66 | return this.code; 67 | } 68 | 69 | const template = this._template(this.template) || 70 | this._template(this.prefs.messages) || 71 | this._template(this.messages); 72 | 73 | if (template === undefined) { 74 | return `Error code "${code}" is not defined, your custom type is missing the correct messages definition`; 75 | } 76 | 77 | // Render and cache result 78 | 79 | this.message = template.render(this.value, this.state, this.prefs, this.local, { errors: this.prefs.errors, messages: [this.prefs.messages, this.messages] }); 80 | if (!this.prefs.errors.label) { 81 | this.message = this.message.replace(/^"" /, '').trim(); 82 | } 83 | 84 | return this.message; 85 | } 86 | 87 | _template(messages, code) { 88 | 89 | return exports.template(this.value, messages, code || this.code, this.state, this.prefs); 90 | } 91 | }; 92 | 93 | 94 | exports.path = function (path) { 95 | 96 | let label = ''; 97 | for (const segment of path) { 98 | if (typeof segment === 'object') { // Exclude array single path segment 99 | continue; 100 | } 101 | 102 | if (typeof segment === 'string') { 103 | if (label) { 104 | label += '.'; 105 | } 106 | 107 | label += segment; 108 | } 109 | else { 110 | label += `[${segment}]`; 111 | } 112 | } 113 | 114 | return label; 115 | }; 116 | 117 | 118 | exports.template = function (value, messages, code, state, prefs) { 119 | 120 | if (!messages) { 121 | return; 122 | } 123 | 124 | if (Template.isTemplate(messages)) { 125 | return code !== 'root' ? messages : null; 126 | } 127 | 128 | let lang = prefs.errors.language; 129 | if (Common.isResolvable(lang)) { 130 | lang = lang.resolve(value, state, prefs); 131 | } 132 | 133 | if (lang && 134 | messages[lang]) { 135 | 136 | if (messages[lang][code] !== undefined) { 137 | return messages[lang][code]; 138 | } 139 | 140 | if (messages[lang]['*'] !== undefined) { 141 | return messages[lang]['*']; 142 | } 143 | } 144 | 145 | if (!messages[code]) { 146 | return messages['*']; 147 | } 148 | 149 | return messages[code]; 150 | }; 151 | 152 | 153 | exports.label = function (flags, state, prefs, messages) { 154 | 155 | if (!prefs.errors.label) { 156 | return ''; 157 | } 158 | 159 | if (flags.label) { 160 | return flags.label; 161 | } 162 | 163 | let path = state.path; 164 | if (prefs.errors.label === 'key' && 165 | state.path.length > 1) { 166 | 167 | path = state.path.slice(-1); 168 | } 169 | 170 | const normalized = exports.path(path); 171 | if (normalized) { 172 | return normalized; 173 | } 174 | 175 | return exports.template(null, prefs.messages, 'root', state, prefs) || 176 | messages && exports.template(null, messages, 'root', state, prefs) || 177 | 'value'; 178 | }; 179 | 180 | 181 | exports.process = function (errors, original, prefs) { 182 | 183 | if (!errors) { 184 | return null; 185 | } 186 | 187 | const { override, message, details } = exports.details(errors); 188 | if (override) { 189 | return override; 190 | } 191 | 192 | if (prefs.errors.stack) { 193 | return new exports.ValidationError(message, details, original); 194 | } 195 | 196 | const limit = Error.stackTraceLimit; 197 | Error.stackTraceLimit = 0; 198 | const validationError = new exports.ValidationError(message, details, original); 199 | Error.stackTraceLimit = limit; 200 | return validationError; 201 | }; 202 | 203 | 204 | exports.details = function (errors, options = {}) { 205 | 206 | let messages = []; 207 | const details = []; 208 | 209 | for (const item of errors) { 210 | 211 | // Override 212 | 213 | if (item instanceof Error) { 214 | if (options.override !== false) { 215 | return { override: item }; 216 | } 217 | 218 | const message = item.toString(); 219 | messages.push(message); 220 | 221 | details.push({ 222 | message, 223 | type: 'override', 224 | context: { error: item } 225 | }); 226 | 227 | continue; 228 | } 229 | 230 | // Report 231 | 232 | const message = item.toString(); 233 | messages.push(message); 234 | 235 | details.push({ 236 | message, 237 | path: item.path.filter((v) => typeof v !== 'object'), 238 | type: item.code, 239 | context: item.local 240 | }); 241 | } 242 | 243 | if (messages.length > 1) { 244 | messages = [...new Set(messages)]; 245 | } 246 | 247 | return { message: messages.join('. '), details }; 248 | }; 249 | 250 | 251 | exports.ValidationError = class extends Error { 252 | 253 | constructor(message, details, original) { 254 | 255 | super(message); 256 | this._original = original; 257 | this.details = details; 258 | } 259 | 260 | static isError(err) { 261 | 262 | return err instanceof exports.ValidationError; 263 | } 264 | }; 265 | 266 | 267 | exports.ValidationError.prototype.isJoi = true; 268 | 269 | exports.ValidationError.prototype.name = 'ValidationError'; 270 | 271 | exports.ValidationError.prototype.annotate = Annotate.error; 272 | -------------------------------------------------------------------------------- /lib/extend.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { assert, clone } = require('@hapi/hoek'); 4 | 5 | const Common = require('./common'); 6 | const Messages = require('./messages'); 7 | 8 | 9 | const internals = {}; 10 | 11 | 12 | exports.type = function (from, options) { 13 | 14 | const base = Object.getPrototypeOf(from); 15 | const prototype = clone(base); 16 | const schema = from._assign(Object.create(prototype)); 17 | const def = Object.assign({}, options); // Shallow cloned 18 | delete def.base; 19 | 20 | prototype._definition = def; 21 | 22 | const parent = base._definition || {}; 23 | def.messages = Messages.merge(parent.messages, def.messages); 24 | def.properties = Object.assign({}, parent.properties, def.properties); 25 | 26 | // Type 27 | 28 | schema.type = def.type; 29 | 30 | // Flags 31 | 32 | def.flags = Object.assign({}, parent.flags, def.flags); 33 | 34 | // Terms 35 | 36 | const terms = Object.assign({}, parent.terms); 37 | if (def.terms) { 38 | for (const name in def.terms) { // Only apply own terms 39 | const term = def.terms[name]; 40 | assert(schema.$_terms[name] === undefined, 'Invalid term override for', def.type, name); 41 | schema.$_terms[name] = term.init; 42 | terms[name] = term; 43 | } 44 | } 45 | 46 | def.terms = terms; 47 | 48 | // Constructor arguments 49 | 50 | if (!def.args) { 51 | def.args = parent.args; 52 | } 53 | 54 | // Prepare 55 | 56 | def.prepare = internals.prepare(def.prepare, parent.prepare); 57 | 58 | // Coerce 59 | 60 | if (def.coerce) { 61 | if (typeof def.coerce === 'function') { 62 | def.coerce = { method: def.coerce }; 63 | } 64 | 65 | if (def.coerce.from && 66 | !Array.isArray(def.coerce.from)) { 67 | 68 | def.coerce = { method: def.coerce.method, from: [].concat(def.coerce.from) }; 69 | } 70 | } 71 | 72 | def.coerce = internals.coerce(def.coerce, parent.coerce); 73 | 74 | // Validate 75 | 76 | def.validate = internals.validate(def.validate, parent.validate); 77 | 78 | // Rules 79 | 80 | const rules = Object.assign({}, parent.rules); 81 | if (def.rules) { 82 | for (const name in def.rules) { 83 | const rule = def.rules[name]; 84 | assert(typeof rule === 'object', 'Invalid rule definition for', def.type, name); 85 | 86 | let method = rule.method; 87 | if (method === undefined) { 88 | method = function () { 89 | 90 | return this.$_addRule(name); 91 | }; 92 | } 93 | 94 | if (method) { 95 | assert(!prototype[name], 'Rule conflict in', def.type, name); 96 | prototype[name] = method; 97 | } 98 | 99 | assert(!rules[name], 'Rule conflict in', def.type, name); 100 | rules[name] = rule; 101 | 102 | if (rule.alias) { 103 | const aliases = [].concat(rule.alias); 104 | for (const alias of aliases) { 105 | prototype[alias] = rule.method; 106 | } 107 | } 108 | 109 | if (rule.args) { 110 | rule.argsByName = new Map(); 111 | rule.args = rule.args.map((arg) => { 112 | 113 | if (typeof arg === 'string') { 114 | arg = { name: arg }; 115 | } 116 | 117 | assert(!rule.argsByName.has(arg.name), 'Duplicated argument name', arg.name); 118 | 119 | if (Common.isSchema(arg.assert)) { 120 | arg.assert = arg.assert.strict().label(arg.name); 121 | } 122 | 123 | rule.argsByName.set(arg.name, arg); 124 | return arg; 125 | }); 126 | } 127 | } 128 | } 129 | 130 | def.rules = rules; 131 | 132 | // Modifiers 133 | 134 | const modifiers = Object.assign({}, parent.modifiers); 135 | if (def.modifiers) { 136 | for (const name in def.modifiers) { 137 | assert(!prototype[name], 'Rule conflict in', def.type, name); 138 | 139 | const modifier = def.modifiers[name]; 140 | assert(typeof modifier === 'function', 'Invalid modifier definition for', def.type, name); 141 | 142 | const method = function (arg) { 143 | 144 | return this.rule({ [name]: arg }); 145 | }; 146 | 147 | prototype[name] = method; 148 | modifiers[name] = modifier; 149 | } 150 | } 151 | 152 | def.modifiers = modifiers; 153 | 154 | // Overrides 155 | 156 | if (def.overrides) { 157 | prototype._super = base; 158 | schema.$_super = {}; // Backwards compatibility 159 | for (const override in def.overrides) { 160 | assert(base[override], 'Cannot override missing', override); 161 | def.overrides[override][Common.symbols.parent] = base[override]; 162 | schema.$_super[override] = base[override].bind(schema); // Backwards compatibility 163 | } 164 | 165 | Object.assign(prototype, def.overrides); 166 | } 167 | 168 | // Casts 169 | 170 | def.cast = Object.assign({}, parent.cast, def.cast); 171 | 172 | // Manifest 173 | 174 | const manifest = Object.assign({}, parent.manifest, def.manifest); 175 | manifest.build = internals.build(def.manifest && def.manifest.build, parent.manifest && parent.manifest.build); 176 | def.manifest = manifest; 177 | 178 | // Rebuild 179 | 180 | def.rebuild = internals.rebuild(def.rebuild, parent.rebuild); 181 | 182 | return schema; 183 | }; 184 | 185 | 186 | // Helpers 187 | 188 | internals.build = function (child, parent) { 189 | 190 | if (!child || 191 | !parent) { 192 | 193 | return child || parent; 194 | } 195 | 196 | return function (obj, desc) { 197 | 198 | return parent(child(obj, desc), desc); 199 | }; 200 | }; 201 | 202 | 203 | internals.coerce = function (child, parent) { 204 | 205 | if (!child || 206 | !parent) { 207 | 208 | return child || parent; 209 | } 210 | 211 | return { 212 | from: child.from && parent.from ? [...new Set([...child.from, ...parent.from])] : null, 213 | method(value, helpers) { 214 | 215 | let coerced; 216 | if (!parent.from || 217 | parent.from.includes(typeof value)) { 218 | 219 | coerced = parent.method(value, helpers); 220 | if (coerced) { 221 | if (coerced.errors || 222 | coerced.value === undefined) { 223 | 224 | return coerced; 225 | } 226 | 227 | value = coerced.value; 228 | } 229 | } 230 | 231 | if (!child.from || 232 | child.from.includes(typeof value)) { 233 | 234 | const own = child.method(value, helpers); 235 | if (own) { 236 | return own; 237 | } 238 | } 239 | 240 | return coerced; 241 | } 242 | }; 243 | }; 244 | 245 | 246 | internals.prepare = function (child, parent) { 247 | 248 | if (!child || 249 | !parent) { 250 | 251 | return child || parent; 252 | } 253 | 254 | return function (value, helpers) { 255 | 256 | const prepared = child(value, helpers); 257 | if (prepared) { 258 | if (prepared.errors || 259 | prepared.value === undefined) { 260 | 261 | return prepared; 262 | } 263 | 264 | value = prepared.value; 265 | } 266 | 267 | return parent(value, helpers) || prepared; 268 | }; 269 | }; 270 | 271 | 272 | internals.rebuild = function (child, parent) { 273 | 274 | if (!child || 275 | !parent) { 276 | 277 | return child || parent; 278 | } 279 | 280 | return function (schema) { 281 | 282 | parent(schema); 283 | child(schema); 284 | }; 285 | }; 286 | 287 | 288 | internals.validate = function (child, parent) { 289 | 290 | if (!child || 291 | !parent) { 292 | 293 | return child || parent; 294 | } 295 | 296 | return function (value, helpers) { 297 | 298 | const result = parent(value, helpers); 299 | if (result) { 300 | if (result.errors && 301 | (!Array.isArray(result.errors) || result.errors.length)) { 302 | 303 | return result; 304 | } 305 | 306 | value = result.value; 307 | } 308 | 309 | return child(value, helpers) || result; 310 | }; 311 | }; 312 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { assert, clone } = require('@hapi/hoek'); 4 | 5 | const Cache = require('./cache'); 6 | const Common = require('./common'); 7 | const Compile = require('./compile'); 8 | const Errors = require('./errors'); 9 | const Extend = require('./extend'); 10 | const Manifest = require('./manifest'); 11 | const Ref = require('./ref'); 12 | const Template = require('./template'); 13 | const Trace = require('./trace'); 14 | 15 | let Schemas; 16 | 17 | 18 | const internals = { 19 | types: { 20 | alternatives: require('./types/alternatives'), 21 | any: require('./types/any'), 22 | array: require('./types/array'), 23 | boolean: require('./types/boolean'), 24 | date: require('./types/date'), 25 | function: require('./types/function'), 26 | link: require('./types/link'), 27 | number: require('./types/number'), 28 | object: require('./types/object'), 29 | string: require('./types/string'), 30 | symbol: require('./types/symbol') 31 | }, 32 | aliases: { 33 | alt: 'alternatives', 34 | bool: 'boolean', 35 | func: 'function' 36 | } 37 | }; 38 | 39 | 40 | if (Buffer) { // $lab:coverage:ignore$ 41 | internals.types.binary = require('./types/binary'); 42 | } 43 | 44 | 45 | internals.root = function () { 46 | 47 | const root = { 48 | _types: new Set(Object.keys(internals.types)) 49 | }; 50 | 51 | // Types 52 | 53 | for (const type of root._types) { 54 | root[type] = function (...args) { 55 | 56 | assert(!args.length || ['alternatives', 'link', 'object'].includes(type), 'The', type, 'type does not allow arguments'); 57 | return internals.generate(this, internals.types[type], args); 58 | }; 59 | } 60 | 61 | // Shortcuts 62 | 63 | for (const method of ['allow', 'custom', 'disallow', 'equal', 'exist', 'forbidden', 'invalid', 'not', 'only', 'optional', 'options', 'prefs', 'preferences', 'required', 'strip', 'valid', 'when']) { 64 | root[method] = function (...args) { 65 | 66 | return this.any()[method](...args); 67 | }; 68 | } 69 | 70 | // Methods 71 | 72 | Object.assign(root, internals.methods); 73 | 74 | // Aliases 75 | 76 | for (const alias in internals.aliases) { 77 | const target = internals.aliases[alias]; 78 | root[alias] = root[target]; 79 | } 80 | 81 | root.x = root.expression; 82 | 83 | // Trace 84 | 85 | if (Trace.setup) { // $lab:coverage:ignore$ 86 | Trace.setup(root); 87 | } 88 | 89 | return root; 90 | }; 91 | 92 | 93 | internals.methods = { 94 | 95 | ValidationError: Errors.ValidationError, 96 | version: Common.version, 97 | cache: Cache.provider, 98 | 99 | assert(value, schema, ...args /* [message], [options] */) { 100 | 101 | internals.assert(value, schema, true, args); 102 | }, 103 | 104 | attempt(value, schema, ...args /* [message], [options] */) { 105 | 106 | return internals.assert(value, schema, false, args); 107 | }, 108 | 109 | build(desc) { 110 | 111 | assert(typeof Manifest.build === 'function', 'Manifest functionality disabled'); 112 | return Manifest.build(this, desc); 113 | }, 114 | 115 | checkPreferences(prefs) { 116 | 117 | Common.checkPreferences(prefs); 118 | }, 119 | 120 | compile(schema, options) { 121 | 122 | return Compile.compile(this, schema, options); 123 | }, 124 | 125 | defaults(modifier) { 126 | 127 | assert(typeof modifier === 'function', 'modifier must be a function'); 128 | 129 | const joi = Object.assign({}, this); 130 | for (const type of joi._types) { 131 | const schema = modifier(joi[type]()); 132 | assert(Common.isSchema(schema), 'modifier must return a valid schema object'); 133 | 134 | joi[type] = function (...args) { 135 | 136 | return internals.generate(this, schema, args); 137 | }; 138 | } 139 | 140 | return joi; 141 | }, 142 | 143 | expression(...args) { 144 | 145 | return new Template(...args); 146 | }, 147 | 148 | extend(...extensions) { 149 | 150 | Common.verifyFlat(extensions, 'extend'); 151 | 152 | Schemas = Schemas || require('./schemas'); 153 | 154 | assert(extensions.length, 'You need to provide at least one extension'); 155 | this.assert(extensions, Schemas.extensions); 156 | 157 | const joi = Object.assign({}, this); 158 | joi._types = new Set(joi._types); 159 | 160 | for (let extension of extensions) { 161 | if (typeof extension === 'function') { 162 | extension = extension(joi); 163 | } 164 | 165 | this.assert(extension, Schemas.extension); 166 | 167 | const expanded = internals.expandExtension(extension, joi); 168 | for (const item of expanded) { 169 | assert(joi[item.type] === undefined || joi._types.has(item.type), 'Cannot override name', item.type); 170 | 171 | const base = item.base || this.any(); 172 | const schema = Extend.type(base, item); 173 | 174 | joi._types.add(item.type); 175 | joi[item.type] = function (...args) { 176 | 177 | return internals.generate(this, schema, args); 178 | }; 179 | } 180 | } 181 | 182 | return joi; 183 | }, 184 | 185 | isError: Errors.ValidationError.isError, 186 | isExpression: Template.isTemplate, 187 | isRef: Ref.isRef, 188 | isSchema: Common.isSchema, 189 | 190 | in(...args) { 191 | 192 | return Ref.in(...args); 193 | }, 194 | 195 | override: Common.symbols.override, 196 | 197 | ref(...args) { 198 | 199 | return Ref.create(...args); 200 | }, 201 | 202 | types() { 203 | 204 | const types = {}; 205 | for (const type of this._types) { 206 | types[type] = this[type](); 207 | } 208 | 209 | for (const target in internals.aliases) { 210 | types[target] = this[target](); 211 | } 212 | 213 | return types; 214 | } 215 | }; 216 | 217 | 218 | // Helpers 219 | 220 | internals.assert = function (value, schema, annotate, args /* [message], [options] */) { 221 | 222 | const message = args[0] instanceof Error || typeof args[0] === 'string' ? args[0] : null; 223 | const options = message !== null ? args[1] : args[0]; 224 | const result = schema.validate(value, Common.preferences({ errors: { stack: true } }, options || {})); 225 | 226 | let error = result.error; 227 | if (!error) { 228 | return result.value; 229 | } 230 | 231 | if (message instanceof Error) { 232 | throw message; 233 | } 234 | 235 | const display = annotate && typeof error.annotate === 'function' ? error.annotate() : error.message; 236 | 237 | if (error instanceof Errors.ValidationError === false) { 238 | error = clone(error); 239 | } 240 | 241 | error.message = message ? `${message} ${display}` : display; 242 | throw error; 243 | }; 244 | 245 | 246 | internals.generate = function (root, schema, args) { 247 | 248 | assert(root, 'Must be invoked on a Joi instance.'); 249 | 250 | schema.$_root = root; 251 | 252 | if (!schema._definition.args || 253 | !args.length) { 254 | 255 | return schema; 256 | } 257 | 258 | return schema._definition.args(schema, ...args); 259 | }; 260 | 261 | 262 | internals.expandExtension = function (extension, joi) { 263 | 264 | if (typeof extension.type === 'string') { 265 | return [extension]; 266 | } 267 | 268 | const extended = []; 269 | for (const type of joi._types) { 270 | if (extension.type.test(type)) { 271 | const item = Object.assign({}, extension); 272 | item.type = type; 273 | item.base = joi[type](); 274 | extended.push(item); 275 | } 276 | } 277 | 278 | return extended; 279 | }; 280 | 281 | 282 | module.exports = internals.root(); 283 | -------------------------------------------------------------------------------- /lib/messages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { assert, clone } = require('@hapi/hoek'); 4 | 5 | const Template = require('./template'); 6 | 7 | 8 | const internals = {}; 9 | 10 | 11 | exports.compile = function (messages, target) { 12 | 13 | // Single value string ('plain error message', 'template {error} message') 14 | 15 | if (typeof messages === 'string') { 16 | assert(!target, 'Cannot set single message string'); 17 | return new Template(messages); 18 | } 19 | 20 | // Single value template 21 | 22 | if (Template.isTemplate(messages)) { 23 | assert(!target, 'Cannot set single message template'); 24 | return messages; 25 | } 26 | 27 | // By error code { 'number.min': } 28 | 29 | assert(typeof messages === 'object' && !Array.isArray(messages), 'Invalid message options'); 30 | 31 | target = target ? clone(target) : {}; 32 | 33 | for (let code in messages) { 34 | const message = messages[code]; 35 | 36 | if (code === 'root' || 37 | Template.isTemplate(message)) { 38 | 39 | target[code] = message; 40 | continue; 41 | } 42 | 43 | if (typeof message === 'string') { 44 | target[code] = new Template(message); 45 | continue; 46 | } 47 | 48 | // By language { english: { 'number.min': } } 49 | 50 | assert(typeof message === 'object' && !Array.isArray(message), 'Invalid message for', code); 51 | 52 | const language = code; 53 | target[language] = target[language] || {}; 54 | 55 | for (code in message) { 56 | const localized = message[code]; 57 | 58 | if (code === 'root' || 59 | Template.isTemplate(localized)) { 60 | 61 | target[language][code] = localized; 62 | continue; 63 | } 64 | 65 | assert(typeof localized === 'string', 'Invalid message for', code, 'in', language); 66 | target[language][code] = new Template(localized); 67 | } 68 | } 69 | 70 | return target; 71 | }; 72 | 73 | 74 | exports.decompile = function (messages) { 75 | 76 | // By error code { 'number.min': } 77 | 78 | const target = {}; 79 | for (let code in messages) { 80 | const message = messages[code]; 81 | 82 | if (code === 'root') { 83 | target.root = message; 84 | continue; 85 | } 86 | 87 | if (Template.isTemplate(message)) { 88 | target[code] = message.describe({ compact: true }); 89 | continue; 90 | } 91 | 92 | // By language { english: { 'number.min': } } 93 | 94 | const language = code; 95 | target[language] = {}; 96 | 97 | for (code in message) { 98 | const localized = message[code]; 99 | 100 | if (code === 'root') { 101 | target[language].root = localized; 102 | continue; 103 | } 104 | 105 | target[language][code] = localized.describe({ compact: true }); 106 | } 107 | } 108 | 109 | return target; 110 | }; 111 | 112 | 113 | exports.merge = function (base, extended) { 114 | 115 | if (!base) { 116 | return exports.compile(extended); 117 | } 118 | 119 | if (!extended) { 120 | return base; 121 | } 122 | 123 | // Single value string 124 | 125 | if (typeof extended === 'string') { 126 | return new Template(extended); 127 | } 128 | 129 | // Single value template 130 | 131 | if (Template.isTemplate(extended)) { 132 | return extended; 133 | } 134 | 135 | // By error code { 'number.min': } 136 | 137 | const target = clone(base); 138 | 139 | for (let code in extended) { 140 | const message = extended[code]; 141 | 142 | if (code === 'root' || 143 | Template.isTemplate(message)) { 144 | 145 | target[code] = message; 146 | continue; 147 | } 148 | 149 | if (typeof message === 'string') { 150 | target[code] = new Template(message); 151 | continue; 152 | } 153 | 154 | // By language { english: { 'number.min': } } 155 | 156 | assert(typeof message === 'object' && !Array.isArray(message), 'Invalid message for', code); 157 | 158 | const language = code; 159 | target[language] = target[language] || {}; 160 | 161 | for (code in message) { 162 | const localized = message[code]; 163 | 164 | if (code === 'root' || 165 | Template.isTemplate(localized)) { 166 | 167 | target[language][code] = localized; 168 | continue; 169 | } 170 | 171 | assert(typeof localized === 'string', 'Invalid message for', code, 'in', language); 172 | target[language][code] = new Template(localized); 173 | } 174 | } 175 | 176 | return target; 177 | }; 178 | -------------------------------------------------------------------------------- /lib/modify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { assert } = require('@hapi/hoek'); 4 | 5 | const Common = require('./common'); 6 | const Ref = require('./ref'); 7 | 8 | 9 | const internals = {}; 10 | 11 | 12 | 13 | exports.Ids = internals.Ids = class { 14 | 15 | constructor() { 16 | 17 | this._byId = new Map(); 18 | this._byKey = new Map(); 19 | this._schemaChain = false; 20 | } 21 | 22 | clone() { 23 | 24 | const clone = new internals.Ids(); 25 | clone._byId = new Map(this._byId); 26 | clone._byKey = new Map(this._byKey); 27 | clone._schemaChain = this._schemaChain; 28 | return clone; 29 | } 30 | 31 | concat(source) { 32 | 33 | if (source._schemaChain) { 34 | this._schemaChain = true; 35 | } 36 | 37 | for (const [id, value] of source._byId.entries()) { 38 | assert(!this._byKey.has(id), 'Schema id conflicts with existing key:', id); 39 | this._byId.set(id, value); 40 | } 41 | 42 | for (const [key, value] of source._byKey.entries()) { 43 | assert(!this._byId.has(key), 'Schema key conflicts with existing id:', key); 44 | this._byKey.set(key, value); 45 | } 46 | } 47 | 48 | fork(path, adjuster, root) { 49 | 50 | const chain = this._collect(path); 51 | chain.push({ schema: root }); 52 | const tail = chain.shift(); 53 | let adjusted = { id: tail.id, schema: adjuster(tail.schema) }; 54 | 55 | assert(Common.isSchema(adjusted.schema), 'adjuster function failed to return a joi schema type'); 56 | 57 | for (const node of chain) { 58 | adjusted = { id: node.id, schema: internals.fork(node.schema, adjusted.id, adjusted.schema) }; 59 | } 60 | 61 | return adjusted.schema; 62 | } 63 | 64 | labels(path, behind = []) { 65 | 66 | const current = path[0]; 67 | const node = this._get(current); 68 | if (!node) { 69 | return [...behind, ...path].join('.'); 70 | } 71 | 72 | const forward = path.slice(1); 73 | behind = [...behind, node.schema._flags.label || current]; 74 | if (!forward.length) { 75 | return behind.join('.'); 76 | } 77 | 78 | return node.schema._ids.labels(forward, behind); 79 | } 80 | 81 | reach(path, behind = []) { 82 | 83 | const current = path[0]; 84 | const node = this._get(current); 85 | assert(node, 'Schema does not contain path', [...behind, ...path].join('.')); 86 | 87 | const forward = path.slice(1); 88 | if (!forward.length) { 89 | return node.schema; 90 | } 91 | 92 | return node.schema._ids.reach(forward, [...behind, current]); 93 | } 94 | 95 | register(schema, { key } = {}) { 96 | 97 | if (!schema || 98 | !Common.isSchema(schema)) { 99 | 100 | return; 101 | } 102 | 103 | if (schema.$_property('schemaChain') || 104 | schema._ids._schemaChain) { 105 | 106 | this._schemaChain = true; 107 | } 108 | 109 | const id = schema._flags.id; 110 | if (id) { 111 | const existing = this._byId.get(id); 112 | assert(!existing || existing.schema === schema, 'Cannot add different schemas with the same id:', id); 113 | assert(!this._byKey.has(id), 'Schema id conflicts with existing key:', id); 114 | 115 | this._byId.set(id, { schema, id }); 116 | } 117 | 118 | if (key) { 119 | assert(!this._byKey.has(key), 'Schema already contains key:', key); 120 | assert(!this._byId.has(key), 'Schema key conflicts with existing id:', key); 121 | 122 | this._byKey.set(key, { schema, id: key }); 123 | } 124 | } 125 | 126 | reset() { 127 | 128 | this._byId = new Map(); 129 | this._byKey = new Map(); 130 | this._schemaChain = false; 131 | } 132 | 133 | _collect(path, behind = [], nodes = []) { 134 | 135 | const current = path[0]; 136 | const node = this._get(current); 137 | assert(node, 'Schema does not contain path', [...behind, ...path].join('.')); 138 | 139 | nodes = [node, ...nodes]; 140 | 141 | const forward = path.slice(1); 142 | if (!forward.length) { 143 | return nodes; 144 | } 145 | 146 | return node.schema._ids._collect(forward, [...behind, current], nodes); 147 | } 148 | 149 | _get(id) { 150 | 151 | return this._byId.get(id) || this._byKey.get(id); 152 | } 153 | }; 154 | 155 | 156 | internals.fork = function (schema, id, replacement) { 157 | 158 | const each = (item, { key }) => { 159 | 160 | if (id === (item._flags.id || key)) { 161 | return replacement; 162 | } 163 | }; 164 | 165 | const obj = exports.schema(schema, { each, ref: false }); 166 | return obj ? obj.$_mutateRebuild() : schema; 167 | }; 168 | 169 | 170 | exports.schema = function (schema, options) { 171 | 172 | let obj; 173 | 174 | for (const name in schema._flags) { 175 | if (name[0] === '_') { 176 | continue; 177 | } 178 | 179 | const result = internals.scan(schema._flags[name], { source: 'flags', name }, options); 180 | if (result !== undefined) { 181 | obj = obj || schema.clone(); 182 | obj._flags[name] = result; 183 | } 184 | } 185 | 186 | for (let i = 0; i < schema._rules.length; ++i) { 187 | const rule = schema._rules[i]; 188 | const result = internals.scan(rule.args, { source: 'rules', name: rule.name }, options); 189 | if (result !== undefined) { 190 | obj = obj || schema.clone(); 191 | const clone = Object.assign({}, rule); 192 | clone.args = result; 193 | obj._rules[i] = clone; 194 | 195 | const existingUnique = obj._singleRules.get(rule.name); 196 | if (existingUnique === rule) { 197 | obj._singleRules.set(rule.name, clone); 198 | } 199 | } 200 | } 201 | 202 | for (const name in schema.$_terms) { 203 | if (name[0] === '_') { 204 | continue; 205 | } 206 | 207 | const result = internals.scan(schema.$_terms[name], { source: 'terms', name }, options); 208 | if (result !== undefined) { 209 | obj = obj || schema.clone(); 210 | obj.$_terms[name] = result; 211 | } 212 | } 213 | 214 | return obj; 215 | }; 216 | 217 | 218 | internals.scan = function (item, source, options, _path, _key) { 219 | 220 | const path = _path || []; 221 | 222 | if (item === null || 223 | typeof item !== 'object') { 224 | 225 | return; 226 | } 227 | 228 | let clone; 229 | 230 | if (Array.isArray(item)) { 231 | for (let i = 0; i < item.length; ++i) { 232 | const key = source.source === 'terms' && source.name === 'keys' && item[i].key; 233 | const result = internals.scan(item[i], source, options, [i, ...path], key); 234 | if (result !== undefined) { 235 | clone = clone || item.slice(); 236 | clone[i] = result; 237 | } 238 | } 239 | 240 | return clone; 241 | } 242 | 243 | if (options.schema !== false && Common.isSchema(item) || 244 | options.ref !== false && Ref.isRef(item)) { 245 | 246 | const result = options.each(item, { ...source, path, key: _key }); 247 | if (result === item) { 248 | return; 249 | } 250 | 251 | return result; 252 | } 253 | 254 | for (const key in item) { 255 | if (key[0] === '_') { 256 | continue; 257 | } 258 | 259 | const result = internals.scan(item[key], source, options, [key, ...path], _key); 260 | if (result !== undefined) { 261 | clone = clone || Object.assign({}, item); 262 | clone[key] = result; 263 | } 264 | } 265 | 266 | return clone; 267 | }; 268 | -------------------------------------------------------------------------------- /lib/schemas.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Joi = require('./index'); 4 | 5 | 6 | const internals = {}; 7 | 8 | 9 | // Preferences 10 | 11 | internals.wrap = Joi.string() 12 | .min(1) 13 | .max(2) 14 | .allow(false); 15 | 16 | 17 | exports.preferences = Joi.object({ 18 | allowUnknown: Joi.boolean(), 19 | abortEarly: Joi.boolean(), 20 | artifacts: Joi.boolean(), 21 | cache: Joi.boolean(), 22 | context: Joi.object(), 23 | convert: Joi.boolean(), 24 | dateFormat: Joi.valid('date', 'iso', 'string', 'time', 'utc'), 25 | debug: Joi.boolean(), 26 | errors: { 27 | escapeHtml: Joi.boolean(), 28 | label: Joi.valid('path', 'key', false), 29 | language: [ 30 | Joi.string(), 31 | Joi.object().ref() 32 | ], 33 | render: Joi.boolean(), 34 | stack: Joi.boolean(), 35 | wrap: { 36 | label: internals.wrap, 37 | array: internals.wrap, 38 | string: internals.wrap 39 | } 40 | }, 41 | externals: Joi.boolean(), 42 | messages: Joi.object(), 43 | noDefaults: Joi.boolean(), 44 | nonEnumerables: Joi.boolean(), 45 | presence: Joi.valid('required', 'optional', 'forbidden'), 46 | skipFunctions: Joi.boolean(), 47 | stripUnknown: Joi.object({ 48 | arrays: Joi.boolean(), 49 | objects: Joi.boolean() 50 | }) 51 | .or('arrays', 'objects') 52 | .allow(true, false), 53 | warnings: Joi.boolean() 54 | }) 55 | .strict(); 56 | 57 | 58 | // Extensions 59 | 60 | internals.nameRx = /^[a-zA-Z0-9]\w*$/; 61 | 62 | 63 | internals.rule = Joi.object({ 64 | alias: Joi.array().items(Joi.string().pattern(internals.nameRx)).single(), 65 | args: Joi.array().items( 66 | Joi.string(), 67 | Joi.object({ 68 | name: Joi.string().pattern(internals.nameRx).required(), 69 | ref: Joi.boolean(), 70 | assert: Joi.alternatives([ 71 | Joi.function(), 72 | Joi.object().schema() 73 | ]) 74 | .conditional('ref', { is: true, then: Joi.required() }), 75 | normalize: Joi.function(), 76 | message: Joi.string().when('assert', { is: Joi.function(), then: Joi.required() }) 77 | }) 78 | ), 79 | convert: Joi.boolean(), 80 | manifest: Joi.boolean(), 81 | method: Joi.function().allow(false), 82 | multi: Joi.boolean(), 83 | validate: Joi.function() 84 | }); 85 | 86 | 87 | exports.extension = Joi.object({ 88 | type: Joi.alternatives([ 89 | Joi.string(), 90 | Joi.object().regex() 91 | ]) 92 | .required(), 93 | args: Joi.function(), 94 | cast: Joi.object().pattern(internals.nameRx, Joi.object({ 95 | from: Joi.function().maxArity(1).required(), 96 | to: Joi.function().minArity(1).maxArity(2).required() 97 | })), 98 | base: Joi.object().schema() 99 | .when('type', { is: Joi.object().regex(), then: Joi.forbidden() }), 100 | coerce: [ 101 | Joi.function().maxArity(3), 102 | Joi.object({ method: Joi.function().maxArity(3).required(), from: Joi.array().items(Joi.string()).single() }) 103 | ], 104 | flags: Joi.object().pattern(internals.nameRx, Joi.object({ 105 | setter: Joi.string(), 106 | default: Joi.any() 107 | })), 108 | manifest: { 109 | build: Joi.function().arity(2) 110 | }, 111 | messages: [Joi.object(), Joi.string()], 112 | modifiers: Joi.object().pattern(internals.nameRx, Joi.function().minArity(1).maxArity(2)), 113 | overrides: Joi.object().pattern(internals.nameRx, Joi.function()), 114 | prepare: Joi.function().maxArity(3), 115 | rebuild: Joi.function().arity(1), 116 | rules: Joi.object().pattern(internals.nameRx, internals.rule), 117 | terms: Joi.object().pattern(internals.nameRx, Joi.object({ 118 | init: Joi.array().allow(null).required(), 119 | manifest: Joi.object().pattern(/.+/, [ 120 | Joi.valid('schema', 'single'), 121 | Joi.object({ 122 | mapped: Joi.object({ 123 | from: Joi.string().required(), 124 | to: Joi.string().required() 125 | }) 126 | .required() 127 | }) 128 | ]) 129 | })), 130 | validate: Joi.function().maxArity(3) 131 | }) 132 | .strict(); 133 | 134 | 135 | exports.extensions = Joi.array().items(Joi.object(), Joi.function().arity(1)).strict(); 136 | 137 | 138 | // Manifest 139 | 140 | internals.desc = { 141 | 142 | buffer: Joi.object({ 143 | buffer: Joi.string() 144 | }), 145 | 146 | func: Joi.object({ 147 | function: Joi.function().required(), 148 | options: { 149 | literal: true 150 | } 151 | }), 152 | 153 | override: Joi.object({ 154 | override: true 155 | }), 156 | 157 | ref: Joi.object({ 158 | ref: Joi.object({ 159 | type: Joi.valid('value', 'global', 'local'), 160 | path: Joi.array().required(), 161 | separator: Joi.string().length(1).allow(false), 162 | ancestor: Joi.number().min(0).integer().allow('root'), 163 | map: Joi.array().items(Joi.array().length(2)).min(1), 164 | adjust: Joi.function(), 165 | iterables: Joi.boolean(), 166 | in: Joi.boolean(), 167 | render: Joi.boolean() 168 | }) 169 | .required() 170 | }), 171 | 172 | regex: Joi.object({ 173 | regex: Joi.string().min(3) 174 | }), 175 | 176 | special: Joi.object({ 177 | special: Joi.valid('deep').required() 178 | }), 179 | 180 | template: Joi.object({ 181 | template: Joi.string().required(), 182 | options: Joi.object() 183 | }), 184 | 185 | value: Joi.object({ 186 | value: Joi.alternatives([Joi.object(), Joi.array()]).required() 187 | }) 188 | }; 189 | 190 | 191 | internals.desc.entity = Joi.alternatives([ 192 | Joi.array().items(Joi.link('...')), 193 | Joi.boolean(), 194 | Joi.function(), 195 | Joi.number(), 196 | Joi.string(), 197 | internals.desc.buffer, 198 | internals.desc.func, 199 | internals.desc.ref, 200 | internals.desc.regex, 201 | internals.desc.special, 202 | internals.desc.template, 203 | internals.desc.value, 204 | Joi.link('/') 205 | ]); 206 | 207 | 208 | internals.desc.values = Joi.array() 209 | .items( 210 | null, 211 | Joi.boolean(), 212 | Joi.function(), 213 | Joi.number().allow(Infinity, -Infinity), 214 | Joi.string().allow(''), 215 | Joi.symbol(), 216 | internals.desc.buffer, 217 | internals.desc.func, 218 | internals.desc.override, 219 | internals.desc.ref, 220 | internals.desc.regex, 221 | internals.desc.template, 222 | internals.desc.value 223 | ); 224 | 225 | 226 | internals.desc.messages = Joi.object() 227 | .pattern(/.+/, [ 228 | Joi.string(), 229 | internals.desc.template, 230 | Joi.object().pattern(/.+/, [Joi.string(), internals.desc.template]) 231 | ]); 232 | 233 | 234 | exports.description = Joi.object({ 235 | type: Joi.string().required(), 236 | flags: Joi.object({ 237 | cast: Joi.string(), 238 | default: Joi.any(), 239 | description: Joi.string(), 240 | empty: Joi.link('/'), 241 | failover: internals.desc.entity, 242 | id: Joi.string(), 243 | label: Joi.string(), 244 | only: true, 245 | presence: ['optional', 'required', 'forbidden'], 246 | result: ['raw', 'strip'], 247 | strip: Joi.boolean(), 248 | unit: Joi.string() 249 | }) 250 | .unknown(), 251 | preferences: { 252 | allowUnknown: Joi.boolean(), 253 | abortEarly: Joi.boolean(), 254 | artifacts: Joi.boolean(), 255 | cache: Joi.boolean(), 256 | convert: Joi.boolean(), 257 | dateFormat: ['date', 'iso', 'string', 'time', 'utc'], 258 | errors: { 259 | escapeHtml: Joi.boolean(), 260 | label: ['path', 'key'], 261 | language: [ 262 | Joi.string(), 263 | internals.desc.ref 264 | ], 265 | wrap: { 266 | label: internals.wrap, 267 | array: internals.wrap 268 | } 269 | }, 270 | externals: Joi.boolean(), 271 | messages: internals.desc.messages, 272 | noDefaults: Joi.boolean(), 273 | nonEnumerables: Joi.boolean(), 274 | presence: ['required', 'optional', 'forbidden'], 275 | skipFunctions: Joi.boolean(), 276 | stripUnknown: Joi.object({ 277 | arrays: Joi.boolean(), 278 | objects: Joi.boolean() 279 | }) 280 | .or('arrays', 'objects') 281 | .allow(true, false), 282 | warnings: Joi.boolean() 283 | }, 284 | allow: internals.desc.values, 285 | invalid: internals.desc.values, 286 | rules: Joi.array().min(1).items({ 287 | name: Joi.string().required(), 288 | args: Joi.object().min(1), 289 | keep: Joi.boolean(), 290 | message: [ 291 | Joi.string(), 292 | internals.desc.messages 293 | ], 294 | warn: Joi.boolean() 295 | }), 296 | 297 | // Terms 298 | 299 | keys: Joi.object().pattern(/.*/, Joi.link('/')), 300 | link: internals.desc.ref 301 | }) 302 | .pattern(/^[a-z]\w*$/, Joi.any()); 303 | -------------------------------------------------------------------------------- /lib/state.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { clone, reach } = require('@hapi/hoek'); 4 | 5 | const Common = require('./common'); 6 | 7 | 8 | const internals = { 9 | value: Symbol('value') 10 | }; 11 | 12 | 13 | module.exports = internals.State = class { 14 | 15 | constructor(path, ancestors, state) { 16 | 17 | this.path = path; 18 | this.ancestors = ancestors; // [parent, ..., root] 19 | 20 | this.mainstay = state.mainstay; 21 | this.schemas = state.schemas; // [current, ..., root] 22 | this.debug = null; 23 | } 24 | 25 | localize(path, ancestors = null, schema = null) { 26 | 27 | const state = new internals.State(path, ancestors, this); 28 | 29 | if (schema && 30 | state.schemas) { 31 | 32 | state.schemas = [internals.schemas(schema), ...state.schemas]; 33 | } 34 | 35 | return state; 36 | } 37 | 38 | nest(schema, debug) { 39 | 40 | const state = new internals.State(this.path, this.ancestors, this); 41 | state.schemas = state.schemas && [internals.schemas(schema), ...state.schemas]; 42 | state.debug = debug; 43 | return state; 44 | } 45 | 46 | shadow(value, reason) { 47 | 48 | this.mainstay.shadow = this.mainstay.shadow || new internals.Shadow(); 49 | this.mainstay.shadow.set(this.path, value, reason); 50 | } 51 | 52 | snapshot() { 53 | 54 | if (this.mainstay.shadow) { 55 | this._snapshot = clone(this.mainstay.shadow.node(this.path)); 56 | } 57 | 58 | this.mainstay.snapshot(); 59 | } 60 | 61 | restore() { 62 | 63 | if (this.mainstay.shadow) { 64 | this.mainstay.shadow.override(this.path, this._snapshot); 65 | this._snapshot = undefined; 66 | } 67 | 68 | this.mainstay.restore(); 69 | } 70 | 71 | commit() { 72 | 73 | if (this.mainstay.shadow) { 74 | this.mainstay.shadow.override(this.path, this._snapshot); 75 | this._snapshot = undefined; 76 | } 77 | 78 | this.mainstay.commit(); 79 | } 80 | }; 81 | 82 | 83 | internals.schemas = function (schema) { 84 | 85 | if (Common.isSchema(schema)) { 86 | return { schema }; 87 | } 88 | 89 | return schema; 90 | }; 91 | 92 | 93 | internals.Shadow = class { 94 | 95 | constructor() { 96 | 97 | this._values = null; 98 | } 99 | 100 | set(path, value, reason) { 101 | 102 | if (!path.length) { // No need to store root value 103 | return; 104 | } 105 | 106 | if (reason === 'strip' && 107 | typeof path[path.length - 1] === 'number') { // Cannot store stripped array values (due to shift) 108 | 109 | return; 110 | } 111 | 112 | this._values = this._values || new Map(); 113 | 114 | let node = this._values; 115 | for (let i = 0; i < path.length; ++i) { 116 | const segment = path[i]; 117 | let next = node.get(segment); 118 | if (!next) { 119 | next = new Map(); 120 | node.set(segment, next); 121 | } 122 | 123 | node = next; 124 | } 125 | 126 | node[internals.value] = value; 127 | } 128 | 129 | get(path) { 130 | 131 | const node = this.node(path); 132 | if (node) { 133 | return node[internals.value]; 134 | } 135 | } 136 | 137 | node(path) { 138 | 139 | if (!this._values) { 140 | return; 141 | } 142 | 143 | return reach(this._values, path, { iterables: true }); 144 | } 145 | 146 | override(path, node) { 147 | 148 | if (!this._values) { 149 | return; 150 | } 151 | 152 | const parents = path.slice(0, -1); 153 | const own = path[path.length - 1]; 154 | const parent = reach(this._values, parents, { iterables: true }); 155 | 156 | if (node) { 157 | parent.set(own, node); 158 | return; 159 | } 160 | 161 | if (parent) { 162 | parent.delete(own); 163 | } 164 | } 165 | }; 166 | -------------------------------------------------------------------------------- /lib/trace.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { deepEqual } = require('@hapi/hoek'); 4 | const Pinpoint = require('@hapi/pinpoint'); 5 | 6 | const Errors = require('./errors'); 7 | 8 | 9 | const internals = { 10 | codes: { 11 | error: 1, 12 | pass: 2, 13 | full: 3 14 | }, 15 | labels: { 16 | 0: 'never used', 17 | 1: 'always error', 18 | 2: 'always pass' 19 | } 20 | }; 21 | 22 | 23 | exports.setup = function (root) { 24 | 25 | const trace = function () { 26 | 27 | root._tracer = root._tracer || new internals.Tracer(); 28 | return root._tracer; 29 | }; 30 | 31 | root.trace = trace; 32 | root[Symbol.for('@hapi/lab/coverage/initialize')] = trace; 33 | 34 | root.untrace = () => { 35 | 36 | root._tracer = null; 37 | }; 38 | }; 39 | 40 | 41 | exports.location = function (schema) { 42 | 43 | return schema.$_setFlag('_tracerLocation', Pinpoint.location(2)); // base.tracer(), caller 44 | }; 45 | 46 | 47 | internals.Tracer = class { 48 | 49 | constructor() { 50 | 51 | this.name = 'Joi'; 52 | this._schemas = new Map(); 53 | } 54 | 55 | _register(schema) { 56 | 57 | const existing = this._schemas.get(schema); 58 | if (existing) { 59 | return existing.store; 60 | } 61 | 62 | const store = new internals.Store(schema); 63 | const { filename, line } = schema._flags._tracerLocation || Pinpoint.location(5); // internals.tracer(), internals.entry(), exports.entry(), validate(), caller 64 | this._schemas.set(schema, { filename, line, store }); 65 | return store; 66 | } 67 | 68 | _combine(merged, sources) { 69 | 70 | for (const { store } of this._schemas.values()) { 71 | store._combine(merged, sources); 72 | } 73 | } 74 | 75 | report(file) { 76 | 77 | const coverage = []; 78 | 79 | // Process each registered schema 80 | 81 | for (const { filename, line, store } of this._schemas.values()) { 82 | if (file && 83 | file !== filename) { 84 | 85 | continue; 86 | } 87 | 88 | // Process sub schemas of the registered root 89 | 90 | const missing = []; 91 | const skipped = []; 92 | 93 | for (const [schema, log] of store._sources.entries()) { 94 | 95 | // Check if sub schema parent skipped 96 | 97 | if (internals.sub(log.paths, skipped)) { 98 | continue; 99 | } 100 | 101 | // Check if sub schema reached 102 | 103 | if (!log.entry) { 104 | missing.push({ 105 | status: 'never reached', 106 | paths: [...log.paths] 107 | }); 108 | 109 | skipped.push(...log.paths); 110 | continue; 111 | } 112 | 113 | // Check values 114 | 115 | for (const type of ['valid', 'invalid']) { 116 | const set = schema[`_${type}s`]; 117 | if (!set) { 118 | continue; 119 | } 120 | 121 | const values = new Set(set._values); 122 | const refs = new Set(set._refs); 123 | for (const { value, ref } of log[type]) { 124 | values.delete(value); 125 | refs.delete(ref); 126 | } 127 | 128 | if (values.size || 129 | refs.size) { 130 | 131 | missing.push({ 132 | status: [...values, ...[...refs].map((ref) => ref.display)], 133 | rule: `${type}s` 134 | }); 135 | } 136 | } 137 | 138 | // Check rules status 139 | 140 | const rules = schema._rules.map((rule) => rule.name); 141 | for (const type of ['default', 'failover']) { 142 | if (schema._flags[type] !== undefined) { 143 | rules.push(type); 144 | } 145 | } 146 | 147 | for (const name of rules) { 148 | const status = internals.labels[log.rule[name] || 0]; 149 | if (status) { 150 | const report = { rule: name, status }; 151 | if (log.paths.size) { 152 | report.paths = [...log.paths]; 153 | } 154 | 155 | missing.push(report); 156 | } 157 | } 158 | } 159 | 160 | if (missing.length) { 161 | coverage.push({ 162 | filename, 163 | line, 164 | missing, 165 | severity: 'error', 166 | message: `Schema missing tests for ${missing.map(internals.message).join(', ')}` 167 | }); 168 | } 169 | } 170 | 171 | return coverage.length ? coverage : null; 172 | } 173 | }; 174 | 175 | 176 | internals.Store = class { 177 | 178 | constructor(schema) { 179 | 180 | this.active = true; 181 | this._sources = new Map(); // schema -> { paths, entry, rule, valid, invalid } 182 | this._combos = new Map(); // merged -> [sources] 183 | this._scan(schema); 184 | } 185 | 186 | debug(state, source, name, result) { 187 | 188 | state.mainstay.debug && state.mainstay.debug.push({ type: source, name, result, path: state.path }); 189 | } 190 | 191 | entry(schema, state) { 192 | 193 | internals.debug(state, { type: 'entry' }); 194 | 195 | this._record(schema, (log) => { 196 | 197 | log.entry = true; 198 | }); 199 | } 200 | 201 | filter(schema, state, source, value) { 202 | 203 | internals.debug(state, { type: source, ...value }); 204 | 205 | this._record(schema, (log) => { 206 | 207 | log[source].add(value); 208 | }); 209 | } 210 | 211 | log(schema, state, source, name, result) { 212 | 213 | internals.debug(state, { type: source, name, result: result === 'full' ? 'pass' : result }); 214 | 215 | this._record(schema, (log) => { 216 | 217 | log[source][name] = log[source][name] || 0; 218 | log[source][name] |= internals.codes[result]; 219 | }); 220 | } 221 | 222 | resolve(state, ref, to) { 223 | 224 | if (!state.mainstay.debug) { 225 | return; 226 | } 227 | 228 | const log = { type: 'resolve', ref: ref.display, to, path: state.path }; 229 | state.mainstay.debug.push(log); 230 | } 231 | 232 | value(state, by, from, to, name) { 233 | 234 | if (!state.mainstay.debug || 235 | deepEqual(from, to)) { 236 | 237 | return; 238 | } 239 | 240 | const log = { type: 'value', by, from, to, path: state.path }; 241 | if (name) { 242 | log.name = name; 243 | } 244 | 245 | state.mainstay.debug.push(log); 246 | } 247 | 248 | _record(schema, each) { 249 | 250 | const log = this._sources.get(schema); 251 | if (log) { 252 | each(log); 253 | return; 254 | } 255 | 256 | const sources = this._combos.get(schema); 257 | for (const source of sources) { 258 | this._record(source, each); 259 | } 260 | } 261 | 262 | _scan(schema, _path) { 263 | 264 | const path = _path || []; 265 | 266 | let log = this._sources.get(schema); 267 | if (!log) { 268 | log = { 269 | paths: new Set(), 270 | entry: false, 271 | rule: {}, 272 | valid: new Set(), 273 | invalid: new Set() 274 | }; 275 | 276 | this._sources.set(schema, log); 277 | } 278 | 279 | if (path.length) { 280 | log.paths.add(path); 281 | } 282 | 283 | const each = (sub, source) => { 284 | 285 | const subId = internals.id(sub, source); 286 | this._scan(sub, path.concat(subId)); 287 | }; 288 | 289 | schema.$_modify({ each, ref: false }); 290 | } 291 | 292 | _combine(merged, sources) { 293 | 294 | this._combos.set(merged, sources); 295 | } 296 | }; 297 | 298 | 299 | internals.message = function (item) { 300 | 301 | const path = item.paths ? Errors.path(item.paths[0]) + (item.rule ? ':' : '') : ''; 302 | return `${path}${item.rule || ''} (${item.status})`; 303 | }; 304 | 305 | 306 | internals.id = function (schema, { source, name, path, key }) { 307 | 308 | if (schema._flags.id) { 309 | return schema._flags.id; 310 | } 311 | 312 | if (key) { 313 | return key; 314 | } 315 | 316 | name = `@${name}`; 317 | 318 | if (source === 'terms') { 319 | return [name, path[Math.min(path.length - 1, 1)]]; 320 | } 321 | 322 | return name; 323 | }; 324 | 325 | 326 | internals.sub = function (paths, skipped) { 327 | 328 | for (const path of paths) { 329 | for (const skip of skipped) { 330 | if (deepEqual(path.slice(0, skip.length), skip)) { 331 | return true; 332 | } 333 | } 334 | } 335 | 336 | return false; 337 | }; 338 | 339 | 340 | internals.debug = function (state, event) { 341 | 342 | if (state.mainstay.debug) { 343 | event.path = state.debug ? [...state.path, state.debug] : state.path; 344 | state.mainstay.debug.push(event); 345 | } 346 | }; 347 | -------------------------------------------------------------------------------- /lib/types/alternatives.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { assert, merge } = require('@hapi/hoek'); 4 | 5 | const Any = require('./any'); 6 | const Common = require('../common'); 7 | const Compile = require('../compile'); 8 | const Errors = require('../errors'); 9 | const Ref = require('../ref'); 10 | 11 | 12 | const internals = {}; 13 | 14 | 15 | module.exports = Any.extend({ 16 | 17 | type: 'alternatives', 18 | 19 | flags: { 20 | 21 | match: { default: 'any' } // 'any', 'one', 'all' 22 | }, 23 | 24 | terms: { 25 | 26 | matches: { init: [], register: Ref.toSibling } 27 | }, 28 | 29 | args(schema, ...schemas) { 30 | 31 | if (schemas.length === 1) { 32 | if (Array.isArray(schemas[0])) { 33 | return schema.try(...schemas[0]); 34 | } 35 | } 36 | 37 | return schema.try(...schemas); 38 | }, 39 | 40 | validate(value, helpers) { 41 | 42 | const { schema, error, state, prefs } = helpers; 43 | 44 | // Match all or one 45 | 46 | if (schema._flags.match) { 47 | const matched = []; 48 | const failed = []; 49 | 50 | for (let i = 0; i < schema.$_terms.matches.length; ++i) { 51 | const item = schema.$_terms.matches[i]; 52 | const localState = state.nest(item.schema, `match.${i}`); 53 | localState.snapshot(); 54 | 55 | const result = item.schema.$_validate(value, localState, prefs); 56 | if (!result.errors) { 57 | matched.push(result.value); 58 | localState.commit(); 59 | } 60 | else { 61 | failed.push(result.errors); 62 | localState.restore(); 63 | } 64 | } 65 | 66 | if (matched.length === 0) { 67 | const context = { 68 | details: failed.map((f) => Errors.details(f, { override: false })) 69 | }; 70 | 71 | return { errors: error('alternatives.any', context) }; 72 | } 73 | 74 | // Match one 75 | 76 | if (schema._flags.match === 'one') { 77 | return matched.length === 1 ? { value: matched[0] } : { errors: error('alternatives.one') }; 78 | } 79 | 80 | // Match all 81 | 82 | if (matched.length !== schema.$_terms.matches.length) { 83 | const context = { 84 | details: failed.map((f) => Errors.details(f, { override: false })) 85 | }; 86 | 87 | return { errors: error('alternatives.all', context) }; 88 | } 89 | 90 | const isAnyObj = (alternative) => { 91 | 92 | return alternative.$_terms.matches.some((v) => { 93 | 94 | return v.schema.type === 'object' || 95 | (v.schema.type === 'alternatives' && isAnyObj(v.schema)); 96 | }); 97 | }; 98 | 99 | return isAnyObj(schema) ? { value: matched.reduce((acc, v) => merge(acc, v, { mergeArrays: false })) } : { value: matched[matched.length - 1] }; 100 | } 101 | 102 | // Match any 103 | 104 | const errors = []; 105 | for (let i = 0; i < schema.$_terms.matches.length; ++i) { 106 | const item = schema.$_terms.matches[i]; 107 | 108 | // Try 109 | 110 | if (item.schema) { 111 | const localState = state.nest(item.schema, `match.${i}`); 112 | localState.snapshot(); 113 | 114 | const result = item.schema.$_validate(value, localState, prefs); 115 | if (!result.errors) { 116 | localState.commit(); 117 | return result; 118 | } 119 | 120 | localState.restore(); 121 | errors.push({ schema: item.schema, reports: result.errors }); 122 | continue; 123 | } 124 | 125 | // Conditional 126 | 127 | const input = item.ref ? item.ref.resolve(value, state, prefs) : value; 128 | const tests = item.is ? [item] : item.switch; 129 | 130 | for (let j = 0; j < tests.length; ++j) { 131 | const test = tests[j]; 132 | const { is, then, otherwise } = test; 133 | 134 | const id = `match.${i}${item.switch ? '.' + j : ''}`; 135 | if (!is.$_match(input, state.nest(is, `${id}.is`), prefs)) { 136 | if (otherwise) { 137 | return otherwise.$_validate(value, state.nest(otherwise, `${id}.otherwise`), prefs); 138 | } 139 | } 140 | else if (then) { 141 | return then.$_validate(value, state.nest(then, `${id}.then`), prefs); 142 | } 143 | } 144 | } 145 | 146 | return internals.errors(errors, helpers); 147 | }, 148 | 149 | rules: { 150 | 151 | conditional: { 152 | method(condition, options) { 153 | 154 | assert(!this._flags._endedSwitch, 'Unreachable condition'); 155 | assert(!this._flags.match, 'Cannot combine match mode', this._flags.match, 'with conditional rule'); 156 | assert(options.break === undefined, 'Cannot use break option with alternatives conditional'); 157 | 158 | const obj = this.clone(); 159 | 160 | const match = Compile.when(obj, condition, options); 161 | const conditions = match.is ? [match] : match.switch; 162 | for (const item of conditions) { 163 | if (item.then && 164 | item.otherwise) { 165 | 166 | obj.$_setFlag('_endedSwitch', true, { clone: false }); 167 | break; 168 | } 169 | } 170 | 171 | obj.$_terms.matches.push(match); 172 | return obj.$_mutateRebuild(); 173 | } 174 | }, 175 | 176 | match: { 177 | method(mode) { 178 | 179 | assert(['any', 'one', 'all'].includes(mode), 'Invalid alternatives match mode', mode); 180 | 181 | if (mode !== 'any') { 182 | for (const match of this.$_terms.matches) { 183 | assert(match.schema, 'Cannot combine match mode', mode, 'with conditional rules'); 184 | } 185 | } 186 | 187 | return this.$_setFlag('match', mode); 188 | } 189 | }, 190 | 191 | try: { 192 | method(...schemas) { 193 | 194 | assert(schemas.length, 'Missing alternative schemas'); 195 | Common.verifyFlat(schemas, 'try'); 196 | 197 | assert(!this._flags._endedSwitch, 'Unreachable condition'); 198 | 199 | const obj = this.clone(); 200 | for (const schema of schemas) { 201 | obj.$_terms.matches.push({ schema: obj.$_compile(schema) }); 202 | } 203 | 204 | return obj.$_mutateRebuild(); 205 | } 206 | } 207 | }, 208 | 209 | overrides: { 210 | 211 | label(name) { 212 | 213 | const obj = this.$_parent('label', name); 214 | const each = (item, source) => { 215 | 216 | return source.path[0] !== 'is' && typeof item._flags.label !== 'string' ? item.label(name) : undefined; 217 | }; 218 | 219 | return obj.$_modify({ each, ref: false }); 220 | } 221 | }, 222 | 223 | rebuild(schema) { 224 | 225 | // Flag when an alternative type is an array 226 | 227 | const each = (item) => { 228 | 229 | if (Common.isSchema(item) && 230 | item.type === 'array') { 231 | 232 | schema.$_setFlag('_arrayItems', true, { clone: false }); 233 | } 234 | }; 235 | 236 | schema.$_modify({ each }); 237 | }, 238 | 239 | manifest: { 240 | 241 | build(obj, desc) { 242 | 243 | if (desc.matches) { 244 | for (const match of desc.matches) { 245 | const { schema, ref, is, not, then, otherwise } = match; 246 | if (schema) { 247 | obj = obj.try(schema); 248 | } 249 | else if (ref) { 250 | obj = obj.conditional(ref, { is, then, not, otherwise, switch: match.switch }); 251 | } 252 | else { 253 | obj = obj.conditional(is, { then, otherwise }); 254 | } 255 | } 256 | } 257 | 258 | return obj; 259 | } 260 | }, 261 | 262 | messages: { 263 | 'alternatives.all': '{{#label}} does not match all of the required types', 264 | 'alternatives.any': '{{#label}} does not match any of the allowed types', 265 | 'alternatives.match': '{{#label}} does not match any of the allowed types', 266 | 'alternatives.one': '{{#label}} matches more than one allowed type', 267 | 'alternatives.types': '{{#label}} must be one of {{#types}}' 268 | } 269 | }); 270 | 271 | 272 | // Helpers 273 | 274 | internals.errors = function (failures, { error, state }) { 275 | 276 | // Nothing matched due to type criteria rules 277 | 278 | if (!failures.length) { 279 | return { errors: error('alternatives.any') }; 280 | } 281 | 282 | // Single error 283 | 284 | if (failures.length === 1) { 285 | return { errors: failures[0].reports }; 286 | } 287 | 288 | // Analyze reasons 289 | 290 | const valids = new Set(); 291 | const complex = []; 292 | 293 | for (const { reports, schema } of failures) { 294 | 295 | // Multiple errors (!abortEarly) 296 | 297 | if (reports.length > 1) { 298 | return internals.unmatched(failures, error); 299 | } 300 | 301 | // Custom error 302 | 303 | const report = reports[0]; 304 | if (report instanceof Errors.Report === false) { 305 | return internals.unmatched(failures, error); 306 | } 307 | 308 | // Internal object or array error 309 | 310 | if (report.state.path.length !== state.path.length) { 311 | complex.push({ type: schema.type, report }); 312 | continue; 313 | } 314 | 315 | // Valids 316 | 317 | if (report.code === 'any.only') { 318 | for (const valid of report.local.valids) { 319 | valids.add(valid); 320 | } 321 | 322 | continue; 323 | } 324 | 325 | // Base type 326 | 327 | const [type, code] = report.code.split('.'); 328 | if (code !== 'base') { 329 | complex.push({ type: schema.type, report }); 330 | } 331 | else if (report.code === 'object.base') { 332 | valids.add(report.local.type); 333 | } 334 | else { 335 | valids.add(type); 336 | } 337 | } 338 | 339 | // All errors are base types or valids 340 | 341 | if (!complex.length) { 342 | return { errors: error('alternatives.types', { types: [...valids] }) }; 343 | } 344 | 345 | // Single complex error 346 | 347 | if (complex.length === 1) { 348 | return { errors: complex[0].report }; 349 | } 350 | 351 | return internals.unmatched(failures, error); 352 | }; 353 | 354 | 355 | internals.unmatched = function (failures, error) { 356 | 357 | const errors = []; 358 | for (const failure of failures) { 359 | errors.push(...failure.reports); 360 | } 361 | 362 | return { errors: error('alternatives.match', Errors.details(errors, { override: false })) }; 363 | }; 364 | -------------------------------------------------------------------------------- /lib/types/any.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { assert } = require('@hapi/hoek'); 4 | 5 | const Base = require('../base'); 6 | const Common = require('../common'); 7 | const Messages = require('../messages'); 8 | 9 | 10 | const internals = {}; 11 | 12 | 13 | module.exports = Base.extend({ 14 | 15 | type: 'any', 16 | 17 | flags: { 18 | 19 | only: { default: false } 20 | }, 21 | 22 | terms: { 23 | 24 | alterations: { init: null }, 25 | examples: { init: null }, 26 | externals: { init: null }, 27 | metas: { init: [] }, 28 | notes: { init: [] }, 29 | shared: { init: null }, 30 | tags: { init: [] }, 31 | whens: { init: null } 32 | }, 33 | 34 | rules: { 35 | 36 | custom: { 37 | method(method, description) { 38 | 39 | assert(typeof method === 'function', 'Method must be a function'); 40 | assert(description === undefined || description && typeof description === 'string', 'Description must be a non-empty string'); 41 | 42 | return this.$_addRule({ name: 'custom', args: { method, description } }); 43 | }, 44 | validate(value, helpers, { method }) { 45 | 46 | try { 47 | return method(value, helpers); 48 | } 49 | catch (err) { 50 | return helpers.error('any.custom', { error: err }); 51 | } 52 | }, 53 | args: ['method', 'description'], 54 | multi: true 55 | }, 56 | 57 | messages: { 58 | method(messages) { 59 | 60 | return this.prefs({ messages }); 61 | } 62 | }, 63 | 64 | shared: { 65 | method(schema) { 66 | 67 | assert(Common.isSchema(schema) && schema._flags.id, 'Schema must be a schema with an id'); 68 | 69 | const obj = this.clone(); 70 | obj.$_terms.shared = obj.$_terms.shared || []; 71 | obj.$_terms.shared.push(schema); 72 | obj.$_mutateRegister(schema); 73 | return obj; 74 | } 75 | }, 76 | 77 | warning: { 78 | method(code, local) { 79 | 80 | assert(code && typeof code === 'string', 'Invalid warning code'); 81 | 82 | return this.$_addRule({ name: 'warning', args: { code, local }, warn: true }); 83 | }, 84 | validate(value, helpers, { code, local }) { 85 | 86 | return helpers.error(code, local); 87 | }, 88 | args: ['code', 'local'], 89 | multi: true 90 | } 91 | }, 92 | 93 | modifiers: { 94 | 95 | keep(rule, enabled = true) { 96 | 97 | rule.keep = enabled; 98 | }, 99 | 100 | message(rule, message) { 101 | 102 | rule.message = Messages.compile(message); 103 | }, 104 | 105 | warn(rule, enabled = true) { 106 | 107 | rule.warn = enabled; 108 | } 109 | }, 110 | 111 | manifest: { 112 | 113 | build(obj, desc) { 114 | 115 | for (const key in desc) { 116 | const values = desc[key]; 117 | 118 | if (['examples', 'externals', 'metas', 'notes', 'tags'].includes(key)) { 119 | for (const value of values) { 120 | obj = obj[key.slice(0, -1)](value); 121 | } 122 | 123 | continue; 124 | } 125 | 126 | if (key === 'alterations') { 127 | const alter = {}; 128 | for (const { target, adjuster } of values) { 129 | alter[target] = adjuster; 130 | } 131 | 132 | obj = obj.alter(alter); 133 | continue; 134 | } 135 | 136 | if (key === 'whens') { 137 | for (const value of values) { 138 | const { ref, is, not, then, otherwise, concat } = value; 139 | if (concat) { 140 | obj = obj.concat(concat); 141 | } 142 | else if (ref) { 143 | obj = obj.when(ref, { is, not, then, otherwise, switch: value.switch, break: value.break }); 144 | } 145 | else { 146 | obj = obj.when(is, { then, otherwise, break: value.break }); 147 | } 148 | } 149 | 150 | continue; 151 | } 152 | 153 | if (key === 'shared') { 154 | for (const value of values) { 155 | obj = obj.shared(value); 156 | } 157 | } 158 | } 159 | 160 | return obj; 161 | } 162 | }, 163 | 164 | messages: { 165 | 'any.custom': '{{#label}} failed custom validation because {{#error.message}}', 166 | 'any.default': '{{#label}} threw an error when running default method', 167 | 'any.failover': '{{#label}} threw an error when running failover method', 168 | 'any.invalid': '{{#label}} contains an invalid value', 169 | 'any.only': '{{#label}} must be {if(#valids.length == 1, "", "one of ")}{{#valids}}', 170 | 'any.ref': '{{#label}} {{#arg}} references {{:#ref}} which {{#reason}}', 171 | 'any.required': '{{#label}} is required', 172 | 'any.unknown': '{{#label}} is not allowed' 173 | } 174 | }); 175 | -------------------------------------------------------------------------------- /lib/types/binary.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { assert } = require('@hapi/hoek'); 4 | 5 | const Any = require('./any'); 6 | const Common = require('../common'); 7 | 8 | 9 | const internals = {}; 10 | 11 | 12 | module.exports = Any.extend({ 13 | 14 | type: 'binary', 15 | 16 | coerce: { 17 | from: ['string', 'object'], 18 | method(value, { schema }) { 19 | 20 | if (typeof value === 'string' || (value !== null && value.type === 'Buffer')) { 21 | try { 22 | return { value: Buffer.from(value, schema._flags.encoding) }; 23 | } 24 | catch { } 25 | } 26 | } 27 | }, 28 | 29 | validate(value, { error }) { 30 | 31 | if (!Buffer.isBuffer(value)) { 32 | return { value, errors: error('binary.base') }; 33 | } 34 | }, 35 | 36 | rules: { 37 | encoding: { 38 | method(encoding) { 39 | 40 | assert(Buffer.isEncoding(encoding), 'Invalid encoding:', encoding); 41 | 42 | return this.$_setFlag('encoding', encoding); 43 | } 44 | }, 45 | 46 | length: { 47 | method(limit) { 48 | 49 | return this.$_addRule({ name: 'length', method: 'length', args: { limit }, operator: '=' }); 50 | }, 51 | validate(value, helpers, { limit }, { name, operator, args }) { 52 | 53 | if (Common.compare(value.length, limit, operator)) { 54 | return value; 55 | } 56 | 57 | return helpers.error('binary.' + name, { limit: args.limit, value }); 58 | }, 59 | args: [ 60 | { 61 | name: 'limit', 62 | ref: true, 63 | assert: Common.limit, 64 | message: 'must be a positive integer' 65 | } 66 | ] 67 | }, 68 | 69 | max: { 70 | method(limit) { 71 | 72 | return this.$_addRule({ name: 'max', method: 'length', args: { limit }, operator: '<=' }); 73 | } 74 | }, 75 | 76 | min: { 77 | method(limit) { 78 | 79 | return this.$_addRule({ name: 'min', method: 'length', args: { limit }, operator: '>=' }); 80 | } 81 | } 82 | }, 83 | 84 | cast: { 85 | string: { 86 | from: (value) => Buffer.isBuffer(value), 87 | to(value, helpers) { 88 | 89 | return value.toString(); 90 | } 91 | } 92 | }, 93 | 94 | messages: { 95 | 'binary.base': '{{#label}} must be a buffer or a string', 96 | 'binary.length': '{{#label}} must be {{#limit}} bytes', 97 | 'binary.max': '{{#label}} must be less than or equal to {{#limit}} bytes', 98 | 'binary.min': '{{#label}} must be at least {{#limit}} bytes' 99 | } 100 | }); 101 | -------------------------------------------------------------------------------- /lib/types/boolean.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { assert } = require('@hapi/hoek'); 4 | 5 | const Any = require('./any'); 6 | const Common = require('../common'); 7 | const Values = require('../values'); 8 | 9 | 10 | const internals = {}; 11 | 12 | 13 | internals.isBool = function (value) { 14 | 15 | return typeof value === 'boolean'; 16 | }; 17 | 18 | 19 | module.exports = Any.extend({ 20 | 21 | type: 'boolean', 22 | 23 | flags: { 24 | 25 | sensitive: { default: false } 26 | }, 27 | 28 | terms: { 29 | 30 | falsy: { 31 | init: null, 32 | manifest: 'values' 33 | }, 34 | 35 | truthy: { 36 | init: null, 37 | manifest: 'values' 38 | } 39 | }, 40 | 41 | coerce(value, { schema }) { 42 | 43 | if (typeof value === 'boolean') { 44 | return; 45 | } 46 | 47 | if (typeof value === 'string') { 48 | const normalized = schema._flags.sensitive ? value : value.toLowerCase(); 49 | value = normalized === 'true' ? true : (normalized === 'false' ? false : value); 50 | } 51 | 52 | if (typeof value !== 'boolean') { 53 | value = schema.$_terms.truthy && schema.$_terms.truthy.has(value, null, null, !schema._flags.sensitive) || 54 | (schema.$_terms.falsy && schema.$_terms.falsy.has(value, null, null, !schema._flags.sensitive) ? false : value); 55 | } 56 | 57 | return { value }; 58 | }, 59 | 60 | validate(value, { error }) { 61 | 62 | if (typeof value !== 'boolean') { 63 | return { value, errors: error('boolean.base') }; 64 | } 65 | }, 66 | 67 | rules: { 68 | truthy: { 69 | method(...values) { 70 | 71 | Common.verifyFlat(values, 'truthy'); 72 | 73 | const obj = this.clone(); 74 | obj.$_terms.truthy = obj.$_terms.truthy || new Values(); 75 | 76 | for (let i = 0; i < values.length; ++i) { 77 | const value = values[i]; 78 | 79 | assert(value !== undefined, 'Cannot call truthy with undefined'); 80 | obj.$_terms.truthy.add(value); 81 | } 82 | 83 | return obj; 84 | } 85 | }, 86 | 87 | falsy: { 88 | method(...values) { 89 | 90 | Common.verifyFlat(values, 'falsy'); 91 | 92 | const obj = this.clone(); 93 | obj.$_terms.falsy = obj.$_terms.falsy || new Values(); 94 | 95 | for (let i = 0; i < values.length; ++i) { 96 | const value = values[i]; 97 | 98 | assert(value !== undefined, 'Cannot call falsy with undefined'); 99 | obj.$_terms.falsy.add(value); 100 | } 101 | 102 | return obj; 103 | } 104 | }, 105 | 106 | sensitive: { 107 | method(enabled = true) { 108 | 109 | return this.$_setFlag('sensitive', enabled); 110 | } 111 | } 112 | }, 113 | 114 | cast: { 115 | number: { 116 | from: internals.isBool, 117 | to(value, helpers) { 118 | 119 | return value ? 1 : 0; 120 | } 121 | }, 122 | string: { 123 | from: internals.isBool, 124 | to(value, helpers) { 125 | 126 | return value ? 'true' : 'false'; 127 | } 128 | } 129 | }, 130 | 131 | manifest: { 132 | 133 | build(obj, desc) { 134 | 135 | if (desc.truthy) { 136 | obj = obj.truthy(...desc.truthy); 137 | } 138 | 139 | if (desc.falsy) { 140 | obj = obj.falsy(...desc.falsy); 141 | } 142 | 143 | return obj; 144 | } 145 | }, 146 | 147 | messages: { 148 | 'boolean.base': '{{#label}} must be a boolean' 149 | } 150 | }); 151 | -------------------------------------------------------------------------------- /lib/types/date.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { assert } = require('@hapi/hoek'); 4 | 5 | const Any = require('./any'); 6 | const Common = require('../common'); 7 | const Template = require('../template'); 8 | 9 | 10 | const internals = {}; 11 | 12 | 13 | internals.isDate = function (value) { 14 | 15 | return value instanceof Date; 16 | }; 17 | 18 | 19 | module.exports = Any.extend({ 20 | 21 | type: 'date', 22 | 23 | coerce: { 24 | from: ['number', 'string'], 25 | method(value, { schema }) { 26 | 27 | return { value: internals.parse(value, schema._flags.format) || value }; 28 | } 29 | }, 30 | 31 | validate(value, { schema, error, prefs }) { 32 | 33 | if (value instanceof Date && 34 | !isNaN(value.getTime())) { 35 | 36 | return; 37 | } 38 | 39 | const format = schema._flags.format; 40 | 41 | if (!prefs.convert || 42 | !format || 43 | typeof value !== 'string') { 44 | 45 | return { value, errors: error('date.base') }; 46 | } 47 | 48 | return { value, errors: error('date.format', { format }) }; 49 | }, 50 | 51 | rules: { 52 | 53 | compare: { 54 | method: false, 55 | validate(value, helpers, { date }, { name, operator, args }) { 56 | 57 | const to = date === 'now' ? Date.now() : date.getTime(); 58 | if (Common.compare(value.getTime(), to, operator)) { 59 | return value; 60 | } 61 | 62 | return helpers.error('date.' + name, { limit: args.date, value }); 63 | }, 64 | args: [ 65 | { 66 | name: 'date', 67 | ref: true, 68 | normalize: (date) => { 69 | 70 | return date === 'now' ? date : internals.parse(date); 71 | }, 72 | assert: (date) => date !== null, 73 | message: 'must have a valid date format' 74 | } 75 | ] 76 | }, 77 | 78 | format: { 79 | method(format) { 80 | 81 | assert(['iso', 'javascript', 'unix'].includes(format), 'Unknown date format', format); 82 | 83 | return this.$_setFlag('format', format); 84 | } 85 | }, 86 | 87 | greater: { 88 | method(date) { 89 | 90 | return this.$_addRule({ name: 'greater', method: 'compare', args: { date }, operator: '>' }); 91 | } 92 | }, 93 | 94 | iso: { 95 | method() { 96 | 97 | return this.format('iso'); 98 | } 99 | }, 100 | 101 | less: { 102 | method(date) { 103 | 104 | return this.$_addRule({ name: 'less', method: 'compare', args: { date }, operator: '<' }); 105 | } 106 | }, 107 | 108 | max: { 109 | method(date) { 110 | 111 | return this.$_addRule({ name: 'max', method: 'compare', args: { date }, operator: '<=' }); 112 | } 113 | }, 114 | 115 | min: { 116 | method(date) { 117 | 118 | return this.$_addRule({ name: 'min', method: 'compare', args: { date }, operator: '>=' }); 119 | } 120 | }, 121 | 122 | timestamp: { 123 | method(type = 'javascript') { 124 | 125 | assert(['javascript', 'unix'].includes(type), '"type" must be one of "javascript, unix"'); 126 | 127 | return this.format(type); 128 | } 129 | } 130 | }, 131 | 132 | cast: { 133 | number: { 134 | from: internals.isDate, 135 | to(value, helpers) { 136 | 137 | return value.getTime(); 138 | } 139 | }, 140 | string: { 141 | from: internals.isDate, 142 | to(value, { prefs }) { 143 | 144 | return Template.date(value, prefs); 145 | } 146 | } 147 | }, 148 | 149 | messages: { 150 | 'date.base': '{{#label}} must be a valid date', 151 | 'date.format': '{{#label}} must be in {msg("date.format." + #format) || #format} format', 152 | 'date.greater': '{{#label}} must be greater than {{:#limit}}', 153 | 'date.less': '{{#label}} must be less than {{:#limit}}', 154 | 'date.max': '{{#label}} must be less than or equal to {{:#limit}}', 155 | 'date.min': '{{#label}} must be greater than or equal to {{:#limit}}', 156 | 157 | // Messages used in date.format 158 | 159 | 'date.format.iso': 'ISO 8601 date', 160 | 'date.format.javascript': 'timestamp or number of milliseconds', 161 | 'date.format.unix': 'timestamp or number of seconds' 162 | } 163 | }); 164 | 165 | 166 | // Helpers 167 | 168 | internals.parse = function (value, format) { 169 | 170 | if (value instanceof Date) { 171 | return value; 172 | } 173 | 174 | if (typeof value !== 'string' && 175 | (isNaN(value) || !isFinite(value))) { 176 | 177 | return null; 178 | } 179 | 180 | if (/^\s*$/.test(value)) { 181 | return null; 182 | } 183 | 184 | // ISO 185 | 186 | if (format === 'iso') { 187 | if (!Common.isIsoDate(value)) { 188 | return null; 189 | } 190 | 191 | return internals.date(value.toString()); 192 | } 193 | 194 | // Normalize number string 195 | 196 | const original = value; 197 | if (typeof value === 'string' && 198 | /^[+-]?\d+(\.\d+)?$/.test(value)) { 199 | 200 | value = parseFloat(value); 201 | } 202 | 203 | // Timestamp 204 | 205 | if (format) { 206 | if (format === 'javascript') { 207 | return internals.date(1 * value); // Casting to number 208 | } 209 | 210 | if (format === 'unix') { 211 | return internals.date(1000 * value); 212 | } 213 | 214 | if (typeof original === 'string') { 215 | return null; 216 | } 217 | } 218 | 219 | // Plain 220 | 221 | return internals.date(value); 222 | }; 223 | 224 | 225 | internals.date = function (value) { 226 | 227 | const date = new Date(value); 228 | if (!isNaN(date.getTime())) { 229 | return date; 230 | } 231 | 232 | return null; 233 | }; 234 | -------------------------------------------------------------------------------- /lib/types/function.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { assert } = require('@hapi/hoek'); 4 | 5 | const Keys = require('./keys'); 6 | 7 | 8 | const internals = {}; 9 | 10 | 11 | module.exports = Keys.extend({ 12 | 13 | type: 'function', 14 | 15 | properties: { 16 | typeof: 'function' 17 | }, 18 | 19 | rules: { 20 | arity: { 21 | method(n) { 22 | 23 | assert(Number.isSafeInteger(n) && n >= 0, 'n must be a positive integer'); 24 | 25 | return this.$_addRule({ name: 'arity', args: { n } }); 26 | }, 27 | validate(value, helpers, { n }) { 28 | 29 | if (value.length === n) { 30 | return value; 31 | } 32 | 33 | return helpers.error('function.arity', { n }); 34 | } 35 | }, 36 | 37 | class: { 38 | method() { 39 | 40 | return this.$_addRule('class'); 41 | }, 42 | validate(value, helpers) { 43 | 44 | if ((/^\s*class\s/).test(value.toString())) { 45 | return value; 46 | } 47 | 48 | return helpers.error('function.class', { value }); 49 | } 50 | }, 51 | 52 | minArity: { 53 | method(n) { 54 | 55 | assert(Number.isSafeInteger(n) && n > 0, 'n must be a strict positive integer'); 56 | 57 | return this.$_addRule({ name: 'minArity', args: { n } }); 58 | }, 59 | validate(value, helpers, { n }) { 60 | 61 | if (value.length >= n) { 62 | return value; 63 | } 64 | 65 | return helpers.error('function.minArity', { n }); 66 | } 67 | }, 68 | 69 | maxArity: { 70 | method(n) { 71 | 72 | assert(Number.isSafeInteger(n) && n >= 0, 'n must be a positive integer'); 73 | 74 | return this.$_addRule({ name: 'maxArity', args: { n } }); 75 | }, 76 | validate(value, helpers, { n }) { 77 | 78 | if (value.length <= n) { 79 | return value; 80 | } 81 | 82 | return helpers.error('function.maxArity', { n }); 83 | } 84 | } 85 | }, 86 | 87 | messages: { 88 | 'function.arity': '{{#label}} must have an arity of {{#n}}', 89 | 'function.class': '{{#label}} must be a class', 90 | 'function.maxArity': '{{#label}} must have an arity lesser or equal to {{#n}}', 91 | 'function.minArity': '{{#label}} must have an arity greater or equal to {{#n}}' 92 | } 93 | }); 94 | -------------------------------------------------------------------------------- /lib/types/link.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { assert } = require('@hapi/hoek'); 4 | 5 | const Any = require('./any'); 6 | const Common = require('../common'); 7 | const Compile = require('../compile'); 8 | const Errors = require('../errors'); 9 | 10 | 11 | const internals = {}; 12 | 13 | 14 | module.exports = Any.extend({ 15 | 16 | type: 'link', 17 | 18 | properties: { 19 | schemaChain: true 20 | }, 21 | 22 | terms: { 23 | 24 | link: { init: null, manifest: 'single', register: false } 25 | }, 26 | 27 | args(schema, ref) { 28 | 29 | return schema.ref(ref); 30 | }, 31 | 32 | validate(value, { schema, state, prefs }) { 33 | 34 | assert(schema.$_terms.link, 'Uninitialized link schema'); 35 | 36 | const linked = internals.generate(schema, value, state, prefs); 37 | const ref = schema.$_terms.link[0].ref; 38 | return linked.$_validate(value, state.nest(linked, `link:${ref.display}:${linked.type}`), prefs); 39 | }, 40 | 41 | generate(schema, value, state, prefs) { 42 | 43 | return internals.generate(schema, value, state, prefs); 44 | }, 45 | 46 | rules: { 47 | 48 | ref: { 49 | method(ref) { 50 | 51 | assert(!this.$_terms.link, 'Cannot reinitialize schema'); 52 | 53 | ref = Compile.ref(ref); 54 | 55 | assert(ref.type === 'value' || ref.type === 'local', 'Invalid reference type:', ref.type); 56 | assert(ref.type === 'local' || ref.ancestor === 'root' || ref.ancestor > 0, 'Link cannot reference itself'); 57 | 58 | const obj = this.clone(); 59 | obj.$_terms.link = [{ ref }]; 60 | return obj; 61 | } 62 | }, 63 | 64 | relative: { 65 | method(enabled = true) { 66 | 67 | return this.$_setFlag('relative', enabled); 68 | } 69 | } 70 | }, 71 | 72 | overrides: { 73 | 74 | concat(source) { 75 | 76 | assert(this.$_terms.link, 'Uninitialized link schema'); 77 | assert(Common.isSchema(source), 'Invalid schema object'); 78 | assert(source.type !== 'link', 'Cannot merge type link with another link'); 79 | 80 | const obj = this.clone(); 81 | 82 | if (!obj.$_terms.whens) { 83 | obj.$_terms.whens = []; 84 | } 85 | 86 | obj.$_terms.whens.push({ concat: source }); 87 | return obj.$_mutateRebuild(); 88 | } 89 | }, 90 | 91 | manifest: { 92 | 93 | build(obj, desc) { 94 | 95 | assert(desc.link, 'Invalid link description missing link'); 96 | return obj.ref(desc.link); 97 | } 98 | } 99 | }); 100 | 101 | 102 | // Helpers 103 | 104 | internals.generate = function (schema, value, state, prefs) { 105 | 106 | let linked = state.mainstay.links.get(schema); 107 | if (linked) { 108 | return linked._generate(value, state, prefs).schema; 109 | } 110 | 111 | const ref = schema.$_terms.link[0].ref; 112 | const { perspective, path } = internals.perspective(ref, state); 113 | internals.assert(perspective, 'which is outside of schema boundaries', ref, schema, state, prefs); 114 | 115 | try { 116 | linked = path.length ? perspective.$_reach(path) : perspective; 117 | } 118 | catch { 119 | internals.assert(false, 'to non-existing schema', ref, schema, state, prefs); 120 | } 121 | 122 | internals.assert(linked.type !== 'link', 'which is another link', ref, schema, state, prefs); 123 | 124 | if (!schema._flags.relative) { 125 | state.mainstay.links.set(schema, linked); 126 | } 127 | 128 | return linked._generate(value, state, prefs).schema; 129 | }; 130 | 131 | 132 | internals.perspective = function (ref, state) { 133 | 134 | if (ref.type === 'local') { 135 | for (const { schema, key } of state.schemas) { // From parent to root 136 | const id = schema._flags.id || key; 137 | if (id === ref.path[0]) { 138 | return { perspective: schema, path: ref.path.slice(1) }; 139 | } 140 | 141 | if (schema.$_terms.shared) { 142 | for (const shared of schema.$_terms.shared) { 143 | if (shared._flags.id === ref.path[0]) { 144 | return { perspective: shared, path: ref.path.slice(1) }; 145 | } 146 | } 147 | } 148 | } 149 | 150 | return { perspective: null, path: null }; 151 | } 152 | 153 | if (ref.ancestor === 'root') { 154 | return { perspective: state.schemas[state.schemas.length - 1].schema, path: ref.path }; 155 | } 156 | 157 | return { perspective: state.schemas[ref.ancestor] && state.schemas[ref.ancestor].schema, path: ref.path }; 158 | }; 159 | 160 | 161 | internals.assert = function (condition, message, ref, schema, state, prefs) { 162 | 163 | if (condition) { // Manual check to avoid generating error message on success 164 | return; 165 | } 166 | 167 | assert(false, `"${Errors.label(schema._flags, state, prefs)}" contains link reference "${ref.display}" ${message}`); 168 | }; 169 | -------------------------------------------------------------------------------- /lib/types/number.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { assert } = require('@hapi/hoek'); 4 | 5 | const Any = require('./any'); 6 | const Common = require('../common'); 7 | 8 | 9 | const internals = { 10 | numberRx: /^\s*[+-]?(?:(?:\d+(?:\.\d*)?)|(?:\.\d+))(?:e([+-]?\d+))?\s*$/i, 11 | precisionRx: /(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/, 12 | exponentialPartRegex: /[eE][+-]?\d+$/, 13 | leadingSignAndZerosRegex: /^[+-]?(0*)?/, 14 | dotRegex: /\./, 15 | trailingZerosRegex: /0+$/, 16 | decimalPlaces(value) { 17 | 18 | const str = value.toString(); 19 | const dindex = str.indexOf('.'); 20 | const eindex = str.indexOf('e'); 21 | return ( 22 | (dindex < 0 ? 0 : (eindex < 0 ? str.length : eindex) - dindex - 1) + 23 | (eindex < 0 ? 0 : Math.max(0, -parseInt(str.slice(eindex + 1)))) 24 | ); 25 | } 26 | }; 27 | 28 | 29 | module.exports = Any.extend({ 30 | 31 | type: 'number', 32 | 33 | flags: { 34 | 35 | unsafe: { default: false } 36 | }, 37 | 38 | coerce: { 39 | from: 'string', 40 | method(value, { schema, error }) { 41 | 42 | const matches = value.match(internals.numberRx); 43 | if (!matches) { 44 | return; 45 | } 46 | 47 | value = value.trim(); 48 | const result = { value: parseFloat(value) }; 49 | 50 | if (result.value === 0) { 51 | result.value = 0; // -0 52 | } 53 | 54 | if (!schema._flags.unsafe) { 55 | if (value.match(/e/i)) { 56 | if (internals.extractSignificantDigits(value) !== internals.extractSignificantDigits(String(result.value))) { 57 | result.errors = error('number.unsafe'); 58 | return result; 59 | } 60 | } 61 | else { 62 | const string = result.value.toString(); 63 | if (string.match(/e/i)) { 64 | return result; 65 | } 66 | 67 | if (string !== internals.normalizeDecimal(value)) { 68 | result.errors = error('number.unsafe'); 69 | return result; 70 | } 71 | } 72 | } 73 | 74 | return result; 75 | } 76 | }, 77 | 78 | validate(value, { schema, error, prefs }) { 79 | 80 | if (value === Infinity || 81 | value === -Infinity) { 82 | 83 | return { value, errors: error('number.infinity') }; 84 | } 85 | 86 | if (!Common.isNumber(value)) { 87 | return { value, errors: error('number.base') }; 88 | } 89 | 90 | const result = { value }; 91 | 92 | if (prefs.convert) { 93 | const rule = schema.$_getRule('precision'); 94 | if (rule) { 95 | const precision = Math.pow(10, rule.args.limit); // This is conceptually equivalent to using toFixed but it should be much faster 96 | result.value = Math.round(result.value * precision) / precision; 97 | } 98 | } 99 | 100 | if (result.value === 0) { 101 | result.value = 0; // -0 102 | } 103 | 104 | if (!schema._flags.unsafe && 105 | (value > Number.MAX_SAFE_INTEGER || value < Number.MIN_SAFE_INTEGER)) { 106 | 107 | result.errors = error('number.unsafe'); 108 | } 109 | 110 | return result; 111 | }, 112 | 113 | rules: { 114 | 115 | compare: { 116 | method: false, 117 | validate(value, helpers, { limit }, { name, operator, args }) { 118 | 119 | if (Common.compare(value, limit, operator)) { 120 | return value; 121 | } 122 | 123 | return helpers.error('number.' + name, { limit: args.limit, value }); 124 | }, 125 | args: [ 126 | { 127 | name: 'limit', 128 | ref: true, 129 | assert: Common.isNumber, 130 | message: 'must be a number' 131 | } 132 | ] 133 | }, 134 | 135 | greater: { 136 | method(limit) { 137 | 138 | return this.$_addRule({ name: 'greater', method: 'compare', args: { limit }, operator: '>' }); 139 | } 140 | }, 141 | 142 | integer: { 143 | method() { 144 | 145 | return this.$_addRule('integer'); 146 | }, 147 | validate(value, helpers) { 148 | 149 | if (Math.trunc(value) - value === 0) { 150 | return value; 151 | } 152 | 153 | return helpers.error('number.integer'); 154 | } 155 | }, 156 | 157 | less: { 158 | method(limit) { 159 | 160 | return this.$_addRule({ name: 'less', method: 'compare', args: { limit }, operator: '<' }); 161 | } 162 | }, 163 | 164 | max: { 165 | method(limit) { 166 | 167 | return this.$_addRule({ name: 'max', method: 'compare', args: { limit }, operator: '<=' }); 168 | } 169 | }, 170 | 171 | min: { 172 | method(limit) { 173 | 174 | return this.$_addRule({ name: 'min', method: 'compare', args: { limit }, operator: '>=' }); 175 | } 176 | }, 177 | 178 | multiple: { 179 | method(base) { 180 | 181 | const baseDecimalPlace = typeof base === 'number' ? internals.decimalPlaces(base) : null; 182 | const pfactor = Math.pow(10, baseDecimalPlace); 183 | 184 | return this.$_addRule({ 185 | name: 'multiple', 186 | args: { 187 | base, 188 | baseDecimalPlace, 189 | pfactor 190 | } 191 | }); 192 | }, 193 | validate(value, helpers, { base, baseDecimalPlace, pfactor }, options) { 194 | 195 | const valueDecimalPlace = internals.decimalPlaces(value); 196 | 197 | if (valueDecimalPlace > baseDecimalPlace) { 198 | // Value with higher precision than base can never be a multiple 199 | return helpers.error('number.multiple', { multiple: options.args.base, value }); 200 | } 201 | 202 | return Math.round(pfactor * value) % Math.round(pfactor * base) === 0 ? 203 | value : 204 | helpers.error('number.multiple', { multiple: options.args.base, value }); 205 | }, 206 | args: [ 207 | { 208 | name: 'base', 209 | ref: true, 210 | assert: (value) => typeof value === 'number' && isFinite(value) && value > 0, 211 | message: 'must be a positive number' 212 | }, 213 | 'baseDecimalPlace', 214 | 'pfactor' 215 | ], 216 | multi: true 217 | }, 218 | 219 | negative: { 220 | method() { 221 | 222 | return this.sign('negative'); 223 | } 224 | }, 225 | 226 | port: { 227 | method() { 228 | 229 | return this.$_addRule('port'); 230 | }, 231 | validate(value, helpers) { 232 | 233 | if (Number.isSafeInteger(value) && 234 | value >= 0 && 235 | value <= 65535) { 236 | 237 | return value; 238 | } 239 | 240 | return helpers.error('number.port'); 241 | } 242 | }, 243 | 244 | positive: { 245 | method() { 246 | 247 | return this.sign('positive'); 248 | } 249 | }, 250 | 251 | precision: { 252 | method(limit) { 253 | 254 | assert(Number.isSafeInteger(limit), 'limit must be an integer'); 255 | 256 | return this.$_addRule({ name: 'precision', args: { limit } }); 257 | }, 258 | validate(value, helpers, { limit }) { 259 | 260 | const places = value.toString().match(internals.precisionRx); 261 | const decimals = Math.max((places[1] ? places[1].length : 0) - (places[2] ? parseInt(places[2], 10) : 0), 0); 262 | if (decimals <= limit) { 263 | return value; 264 | } 265 | 266 | return helpers.error('number.precision', { limit, value }); 267 | }, 268 | convert: true 269 | }, 270 | 271 | sign: { 272 | method(sign) { 273 | 274 | assert(['negative', 'positive'].includes(sign), 'Invalid sign', sign); 275 | 276 | return this.$_addRule({ name: 'sign', args: { sign } }); 277 | }, 278 | validate(value, helpers, { sign }) { 279 | 280 | if (sign === 'negative' && value < 0 || 281 | sign === 'positive' && value > 0) { 282 | 283 | return value; 284 | } 285 | 286 | return helpers.error(`number.${sign}`); 287 | } 288 | }, 289 | 290 | unsafe: { 291 | method(enabled = true) { 292 | 293 | assert(typeof enabled === 'boolean', 'enabled must be a boolean'); 294 | 295 | return this.$_setFlag('unsafe', enabled); 296 | } 297 | } 298 | }, 299 | 300 | cast: { 301 | string: { 302 | from: (value) => typeof value === 'number', 303 | to(value, helpers) { 304 | 305 | return value.toString(); 306 | } 307 | } 308 | }, 309 | 310 | messages: { 311 | 'number.base': '{{#label}} must be a number', 312 | 'number.greater': '{{#label}} must be greater than {{#limit}}', 313 | 'number.infinity': '{{#label}} cannot be infinity', 314 | 'number.integer': '{{#label}} must be an integer', 315 | 'number.less': '{{#label}} must be less than {{#limit}}', 316 | 'number.max': '{{#label}} must be less than or equal to {{#limit}}', 317 | 'number.min': '{{#label}} must be greater than or equal to {{#limit}}', 318 | 'number.multiple': '{{#label}} must be a multiple of {{#multiple}}', 319 | 'number.negative': '{{#label}} must be a negative number', 320 | 'number.port': '{{#label}} must be a valid port', 321 | 'number.positive': '{{#label}} must be a positive number', 322 | 'number.precision': '{{#label}} must have no more than {{#limit}} decimal places', 323 | 'number.unsafe': '{{#label}} must be a safe number' 324 | } 325 | }); 326 | 327 | 328 | // Helpers 329 | 330 | internals.extractSignificantDigits = function (value) { 331 | 332 | return value 333 | .replace(internals.exponentialPartRegex, '') 334 | .replace(internals.dotRegex, '') 335 | .replace(internals.trailingZerosRegex, '') 336 | .replace(internals.leadingSignAndZerosRegex, ''); 337 | }; 338 | 339 | 340 | internals.normalizeDecimal = function (str) { 341 | 342 | str = str 343 | // Remove leading plus signs 344 | .replace(/^\+/, '') 345 | // Remove trailing zeros if there is a decimal point and unecessary decimal points 346 | .replace(/\.0*$/, '') 347 | // Add a integer 0 if the numbers starts with a decimal point 348 | .replace(/^(-?)\.([^\.]*)$/, '$10.$2') 349 | // Remove leading zeros 350 | .replace(/^(-?)0+([0-9])/, '$1$2'); 351 | 352 | if (str.includes('.') && 353 | str.endsWith('0')) { 354 | 355 | str = str.replace(/0+$/, ''); 356 | } 357 | 358 | if (str === '-0') { 359 | return '0'; 360 | } 361 | 362 | return str; 363 | }; 364 | -------------------------------------------------------------------------------- /lib/types/object.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Keys = require('./keys'); 4 | 5 | 6 | const internals = {}; 7 | 8 | 9 | module.exports = Keys.extend({ 10 | 11 | type: 'object', 12 | 13 | cast: { 14 | map: { 15 | from: (value) => value && typeof value === 'object', 16 | to(value, helpers) { 17 | 18 | return new Map(Object.entries(value)); 19 | } 20 | } 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /lib/types/symbol.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { assert } = require('@hapi/hoek'); 4 | 5 | const Any = require('./any'); 6 | 7 | 8 | const internals = {}; 9 | 10 | 11 | internals.Map = class extends Map { 12 | 13 | slice() { 14 | 15 | return new internals.Map(this); 16 | } 17 | }; 18 | 19 | 20 | module.exports = Any.extend({ 21 | 22 | type: 'symbol', 23 | 24 | terms: { 25 | 26 | map: { init: new internals.Map() } 27 | }, 28 | 29 | coerce: { 30 | method(value, { schema, error }) { 31 | 32 | const lookup = schema.$_terms.map.get(value); 33 | if (lookup) { 34 | value = lookup; 35 | } 36 | 37 | if (!schema._flags.only || 38 | typeof value === 'symbol') { 39 | 40 | return { value }; 41 | } 42 | 43 | return { value, errors: error('symbol.map', { map: schema.$_terms.map }) }; 44 | } 45 | }, 46 | 47 | validate(value, { error }) { 48 | 49 | if (typeof value !== 'symbol') { 50 | return { value, errors: error('symbol.base') }; 51 | } 52 | }, 53 | 54 | rules: { 55 | map: { 56 | method(iterable) { 57 | 58 | if (iterable && 59 | !iterable[Symbol.iterator] && 60 | typeof iterable === 'object') { 61 | 62 | iterable = Object.entries(iterable); 63 | } 64 | 65 | assert(iterable && iterable[Symbol.iterator], 'Iterable must be an iterable or object'); 66 | 67 | const obj = this.clone(); 68 | 69 | const symbols = []; 70 | for (const entry of iterable) { 71 | assert(entry && entry[Symbol.iterator], 'Entry must be an iterable'); 72 | const [key, value] = entry; 73 | 74 | assert(typeof key !== 'object' && typeof key !== 'function' && typeof key !== 'symbol', 'Key must not be of type object, function, or Symbol'); 75 | assert(typeof value === 'symbol', 'Value must be a Symbol'); 76 | 77 | obj.$_terms.map.set(key, value); 78 | symbols.push(value); 79 | } 80 | 81 | return obj.valid(...symbols); 82 | } 83 | } 84 | }, 85 | 86 | manifest: { 87 | 88 | build(obj, desc) { 89 | 90 | if (desc.map) { 91 | obj = obj.map(desc.map); 92 | } 93 | 94 | return obj; 95 | } 96 | }, 97 | 98 | messages: { 99 | 'symbol.base': '{{#label}} must be a symbol', 100 | 'symbol.map': '{{#label}} must be one of {{#map}}' 101 | } 102 | }); 103 | -------------------------------------------------------------------------------- /lib/values.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { assert, deepEqual } = require('@hapi/hoek'); 4 | 5 | const Common = require('./common'); 6 | 7 | 8 | const internals = {}; 9 | 10 | 11 | module.exports = internals.Values = class { 12 | 13 | constructor(values, refs) { 14 | 15 | this._values = new Set(values); 16 | this._refs = new Set(refs); 17 | this._lowercase = internals.lowercases(values); 18 | 19 | this._override = false; 20 | } 21 | 22 | get length() { 23 | 24 | return this._values.size + this._refs.size; 25 | } 26 | 27 | add(value, refs) { 28 | 29 | // Reference 30 | 31 | if (Common.isResolvable(value)) { 32 | if (!this._refs.has(value)) { 33 | this._refs.add(value); 34 | 35 | if (refs) { // Skipped in a merge 36 | refs.register(value); 37 | } 38 | } 39 | 40 | return; 41 | } 42 | 43 | // Value 44 | 45 | if (!this.has(value, null, null, false)) { 46 | this._values.add(value); 47 | 48 | if (typeof value === 'string') { 49 | this._lowercase.set(value.toLowerCase(), value); 50 | } 51 | } 52 | } 53 | 54 | static merge(target, source, remove) { 55 | 56 | target = target || new internals.Values(); 57 | 58 | if (source) { 59 | if (source._override) { 60 | return source.clone(); 61 | } 62 | 63 | for (const item of [...source._values, ...source._refs]) { 64 | target.add(item); 65 | } 66 | } 67 | 68 | if (remove) { 69 | for (const item of [...remove._values, ...remove._refs]) { 70 | target.remove(item); 71 | } 72 | } 73 | 74 | return target.length ? target : null; 75 | } 76 | 77 | remove(value) { 78 | 79 | // Reference 80 | 81 | if (Common.isResolvable(value)) { 82 | this._refs.delete(value); 83 | return; 84 | } 85 | 86 | // Value 87 | 88 | this._values.delete(value); 89 | 90 | if (typeof value === 'string') { 91 | this._lowercase.delete(value.toLowerCase()); 92 | } 93 | } 94 | 95 | has(value, state, prefs, insensitive) { 96 | 97 | return !!this.get(value, state, prefs, insensitive); 98 | } 99 | 100 | get(value, state, prefs, insensitive) { 101 | 102 | if (!this.length) { 103 | return false; 104 | } 105 | 106 | // Simple match 107 | 108 | if (this._values.has(value)) { 109 | return { value }; 110 | } 111 | 112 | // Case insensitive string match 113 | 114 | if (typeof value === 'string' && 115 | value && 116 | insensitive) { 117 | 118 | const found = this._lowercase.get(value.toLowerCase()); 119 | if (found) { 120 | return { value: found }; 121 | } 122 | } 123 | 124 | if (!this._refs.size && 125 | typeof value !== 'object') { 126 | 127 | return false; 128 | } 129 | 130 | // Objects 131 | 132 | if (typeof value === 'object') { 133 | for (const item of this._values) { 134 | if (deepEqual(item, value)) { 135 | return { value: item }; 136 | } 137 | } 138 | } 139 | 140 | // References 141 | 142 | if (state) { 143 | for (const ref of this._refs) { 144 | const resolved = ref.resolve(value, state, prefs, null, { in: true }); 145 | if (resolved === undefined) { 146 | continue; 147 | } 148 | 149 | const items = !ref.in || typeof resolved !== 'object' 150 | ? [resolved] 151 | : Array.isArray(resolved) ? resolved : Object.keys(resolved); 152 | 153 | for (const item of items) { 154 | if (typeof item !== typeof value) { 155 | continue; 156 | } 157 | 158 | if (insensitive && 159 | value && 160 | typeof value === 'string') { 161 | 162 | if (item.toLowerCase() === value.toLowerCase()) { 163 | return { value: item, ref }; 164 | } 165 | } 166 | else { 167 | if (deepEqual(item, value)) { 168 | return { value: item, ref }; 169 | } 170 | } 171 | } 172 | } 173 | } 174 | 175 | return false; 176 | } 177 | 178 | override() { 179 | 180 | this._override = true; 181 | } 182 | 183 | values(options) { 184 | 185 | if (options && 186 | options.display) { 187 | 188 | const values = []; 189 | 190 | for (const item of [...this._values, ...this._refs]) { 191 | if (item !== undefined) { 192 | values.push(item); 193 | } 194 | } 195 | 196 | return values; 197 | } 198 | 199 | return Array.from([...this._values, ...this._refs]); 200 | } 201 | 202 | clone() { 203 | 204 | const set = new internals.Values(this._values, this._refs); 205 | set._override = this._override; 206 | return set; 207 | } 208 | 209 | concat(source) { 210 | 211 | assert(!source._override, 'Cannot concat override set of values'); 212 | 213 | const set = new internals.Values([...this._values, ...source._values], [...this._refs, ...source._refs]); 214 | set._override = this._override; 215 | return set; 216 | } 217 | 218 | describe() { 219 | 220 | const normalized = []; 221 | 222 | if (this._override) { 223 | normalized.push({ override: true }); 224 | } 225 | 226 | for (const value of this._values.values()) { 227 | normalized.push(value && typeof value === 'object' ? { value } : value); 228 | } 229 | 230 | for (const value of this._refs.values()) { 231 | normalized.push(value.describe()); 232 | } 233 | 234 | return normalized; 235 | } 236 | }; 237 | 238 | 239 | internals.Values.prototype[Common.symbols.values] = true; 240 | 241 | 242 | // Aliases 243 | 244 | internals.Values.prototype.slice = internals.Values.prototype.clone; 245 | 246 | 247 | // Helpers 248 | 249 | internals.lowercases = function (from) { 250 | 251 | const map = new Map(); 252 | 253 | if (from) { 254 | for (const value of from) { 255 | if (typeof value === 'string') { 256 | map.set(value.toLowerCase(), value); 257 | } 258 | } 259 | } 260 | 261 | return map; 262 | }; 263 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joi", 3 | "description": "Object schema validation", 4 | "version": "17.13.3", 5 | "repository": { 6 | "url": "git://github.com/hapijs/joi", 7 | "type": "git" 8 | }, 9 | "engines": { 10 | "node": ">= 20" 11 | }, 12 | "main": "lib/index.js", 13 | "types": "lib/index.d.ts", 14 | "browser": "dist/joi-browser.min.js", 15 | "files": [ 16 | "lib/**/*", 17 | "dist/*" 18 | ], 19 | "keywords": [ 20 | "schema", 21 | "validation" 22 | ], 23 | "dependencies": { 24 | "@hapi/address": "^5.1.1", 25 | "@hapi/formula": "^3.0.2", 26 | "@hapi/hoek": "^11.0.7", 27 | "@hapi/pinpoint": "^2.0.1", 28 | "@hapi/tlds": "^1.1.1", 29 | "@hapi/topo": "^6.0.2" 30 | }, 31 | "devDependencies": { 32 | "@hapi/bourne": "^3.0.0", 33 | "@hapi/code": "^9.0.3", 34 | "@hapi/eslint-plugin": "^7.0.0", 35 | "@hapi/joi-legacy-test": "npm:@hapi/joi@15.x.x", 36 | "@hapi/lab": "^26.0.0", 37 | "@types/node": "^20.17.47", 38 | "typescript": "^5.8.3" 39 | }, 40 | "scripts": { 41 | "prepublishOnly": "cd browser && npm install && npm run build", 42 | "test": "lab -t 100 -a @hapi/code -L -Y", 43 | "test-cov-html": "lab -r html -o coverage.html -a @hapi/code" 44 | }, 45 | "license": "BSD-3-Clause" 46 | } 47 | -------------------------------------------------------------------------------- /test/cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Code = require('@hapi/code'); 4 | const Lab = require('@hapi/lab'); 5 | const Joi = require('..'); 6 | 7 | 8 | const internals = {}; 9 | 10 | 11 | const { describe, it } = exports.lab = Lab.script(); 12 | const { expect } = Code; 13 | 14 | 15 | describe('Cache', () => { 16 | 17 | describe('schema', () => { 18 | 19 | it('caches values', () => { 20 | 21 | const schema = Joi.string().pattern(/abc/).cache(); 22 | 23 | const validate = schema._definition.rules.pattern.validate; 24 | let count = 0; 25 | schema._definition.rules.pattern.validate = function (...args) { 26 | 27 | ++count; 28 | return validate(...args); 29 | }; 30 | 31 | expect(schema.validate('xabcd').error).to.not.exist(); 32 | expect(schema.validate('xabcd').error).to.not.exist(); 33 | expect(schema.validate('xabcd').error).to.not.exist(); 34 | 35 | expect(count).to.equal(1); 36 | 37 | schema._definition.rules.pattern.validate = validate; 38 | }); 39 | 40 | it('caches values (object key)', () => { 41 | 42 | const a = Joi.string().pattern(/abc/).cache(); 43 | const schema = Joi.object({ a }); 44 | 45 | const validate = a._definition.rules.pattern.validate; 46 | let count = 0; 47 | a._definition.rules.pattern.validate = function (...args) { 48 | 49 | ++count; 50 | return validate(...args); 51 | }; 52 | 53 | expect(schema.validate({ a: 'xabcd' }).error).to.not.exist(); 54 | expect(schema.validate({ a: 'xabcd' }).error).to.not.exist(); 55 | expect(schema.validate({ a: 'xabcd' }).error).to.not.exist(); 56 | 57 | expect(count).to.equal(1); 58 | 59 | a._definition.rules.pattern.validate = validate; 60 | }); 61 | 62 | it('caches errors', () => { 63 | 64 | const schema = Joi.string().pattern(/abc/).cache(); 65 | 66 | const validate = schema._definition.rules.pattern.validate; 67 | let count = 0; 68 | schema._definition.rules.pattern.validate = function (...args) { 69 | 70 | ++count; 71 | return validate(...args); 72 | }; 73 | 74 | const err = schema.validate('xbcd').error; 75 | expect(schema.validate('xbcd').error).to.equal(err); 76 | expect(schema.validate('xbcd').error).to.equal(err); 77 | expect(schema.validate('xbcd').error).to.equal(err); 78 | 79 | expect(count).to.equal(1); 80 | 81 | schema._definition.rules.pattern.validate = validate; 82 | }); 83 | 84 | it('skips caching when prefs disabled', () => { 85 | 86 | const cache = Joi.cache.provision(); 87 | const schema = Joi.string().pattern(/abc/).cache(cache); 88 | 89 | const validate = schema._definition.rules.pattern.validate; 90 | let count = 0; 91 | schema._definition.rules.pattern.validate = function (...args) { 92 | 93 | ++count; 94 | return validate(...args); 95 | }; 96 | 97 | expect(schema.validate('xabcd', { cache: false }).error).to.not.exist(); 98 | expect(schema.validate('xabcd', { cache: false }).error).to.not.exist(); 99 | expect(schema.validate('xabcd', { cache: false }).error).to.not.exist(); 100 | 101 | expect(count).to.equal(3); 102 | 103 | schema._definition.rules.pattern.validate = validate; 104 | }); 105 | 106 | it('skips caching when schema contains refs', () => { 107 | 108 | const a = Joi.string().allow(Joi.ref('b')).pattern(/abc/).cache(); 109 | const schema = Joi.object({ 110 | a, 111 | b: Joi.any() 112 | }); 113 | 114 | const validate = a._definition.rules.pattern.validate; 115 | let count = 0; 116 | a._definition.rules.pattern.validate = function (...args) { 117 | 118 | ++count; 119 | return validate(...args); 120 | }; 121 | 122 | expect(schema.validate({ a: 'xabcd' }).error).to.not.exist(); 123 | expect(schema.validate({ a: 'xabcd' }).error).to.not.exist(); 124 | expect(schema.validate({ a: 'xabcd' }).error).to.not.exist(); 125 | 126 | expect(count).to.equal(3); 127 | 128 | a._definition.rules.pattern.validate = validate; 129 | }); 130 | }); 131 | 132 | describe('provider', () => { 133 | 134 | describe('provision()', () => { 135 | 136 | it('generates cache', () => { 137 | 138 | const cache = Joi.cache.provision({ max: 5 }); 139 | 140 | cache.set(1, 'x'); 141 | expect(cache.get(1)).to.equal('x'); 142 | 143 | cache.set(2, 'y'); 144 | expect(cache.get(2)).to.equal('y'); 145 | 146 | cache.set(3, 'z'); 147 | expect(cache.get(3)).to.equal('z'); 148 | 149 | cache.set('a', 'b'); 150 | expect(cache.get('a')).to.equal('b'); 151 | 152 | cache.set('b', 'c'); 153 | expect(cache.get('b')).to.equal('c'); 154 | 155 | cache.set({}, 'ignore'); 156 | expect(cache.get({})).to.not.exist(); 157 | 158 | cache.set(1, 'v'); 159 | expect(cache.get(1)).to.equal('v'); 160 | 161 | cache.set(null, 'x'); 162 | expect(cache.get(null)).to.equal('x'); 163 | 164 | expect(cache).to.have.length(5); 165 | 166 | expect(cache.get(2)).to.not.exist(); 167 | expect(cache.get(1)).to.equal('v'); 168 | }); 169 | }); 170 | 171 | describe('Joi.cache', () => { 172 | 173 | it('generates cache with default max', () => { 174 | 175 | const cache = Joi.cache.provision(); 176 | for (let i = 0; i < 1020; ++i) { 177 | cache.set(i, i + 10); 178 | expect(cache.get(i)).to.equal(i + 10); 179 | } 180 | 181 | expect(cache).to.have.length(1000); 182 | }); 183 | 184 | it('errors on invalid max option', () => { 185 | 186 | expect(() => Joi.cache.provision({ max: null })).to.throw('Invalid max cache size'); 187 | expect(() => Joi.cache.provision({ max: Infinity })).to.throw('Invalid max cache size'); 188 | expect(() => Joi.cache.provision({ max: -1 })).to.throw('Invalid max cache size'); 189 | expect(() => Joi.cache.provision({ max: 0 })).to.throw('Invalid max cache size'); 190 | }); 191 | }); 192 | }); 193 | }); 194 | -------------------------------------------------------------------------------- /test/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Code = require('@hapi/code'); 4 | const Lab = require('@hapi/lab'); 5 | 6 | const Common = require('../lib/common'); 7 | 8 | 9 | const internals = {}; 10 | 11 | 12 | const { describe, it } = exports.lab = Lab.script(); 13 | const { expect } = Code; 14 | 15 | 16 | describe('Common', () => { 17 | 18 | describe('assertOptions', () => { 19 | 20 | it('validates null', () => { 21 | 22 | expect(() => Common.assertOptions()).to.throw('Options must be of type object'); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/compile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Code = require('@hapi/code'); 4 | const Joi = require('..'); 5 | const Lab = require('@hapi/lab'); 6 | const Legacy = require('@hapi/joi-legacy-test'); 7 | 8 | const Helper = require('./helper'); 9 | 10 | 11 | const internals = {}; 12 | 13 | 14 | const { describe, it } = exports.lab = Lab.script(); 15 | const { expect } = Code; 16 | 17 | 18 | describe('cast', () => { 19 | 20 | describe('schema()', () => { 21 | 22 | it('casts templates', () => { 23 | 24 | const schema = Joi.object({ 25 | a: Joi.number(), 26 | b: Joi.x('{a + 1}') 27 | }); 28 | 29 | Helper.validate(schema, [[{ a: 5, b: 6 }, true]]); 30 | }); 31 | 32 | it('compiles null schema', () => { 33 | 34 | Helper.validate(Joi.compile(null), [ 35 | ['a', false, { 36 | message: '"value" must be [null]', 37 | path: [], 38 | type: 'any.only', 39 | context: { value: 'a', valids: [null], label: 'value' } 40 | }], 41 | [null, true] 42 | ]); 43 | }); 44 | 45 | it('compiles number literal', () => { 46 | 47 | Helper.validate(Joi.compile(5), [ 48 | [6, false, { 49 | message: '"value" must be [5]', 50 | path: [], 51 | type: 'any.only', 52 | context: { value: 6, valids: [5], label: 'value' } 53 | }], 54 | [5, true] 55 | ]); 56 | }); 57 | 58 | it('compiles string literal', () => { 59 | 60 | Helper.validate(Joi.compile('5'), [ 61 | ['6', false, { 62 | message: '"value" must be [5]', 63 | path: [], 64 | type: 'any.only', 65 | context: { value: '6', valids: ['5'], label: 'value' } 66 | }], 67 | ['5', true] 68 | ]); 69 | }); 70 | 71 | it('compiles boolean literal', () => { 72 | 73 | Helper.validate(Joi.compile(true), [ 74 | [false, false, { 75 | message: '"value" must be [true]', 76 | path: [], 77 | type: 'any.only', 78 | context: { value: false, valids: [true], label: 'value' } 79 | }], 80 | [true, true] 81 | ]); 82 | }); 83 | 84 | it('compiles date literal', () => { 85 | 86 | const now = Date.now(); 87 | const dnow = new Date(now); 88 | Helper.validate(Joi.compile(dnow), [ 89 | [new Date(now), true], 90 | [now, true, new Date(now)], 91 | [now * 2, false, { 92 | message: `"value" must be [${dnow.toISOString()}]`, 93 | path: [], 94 | type: 'any.only', 95 | context: { value: new Date(now * 2), valids: [dnow], label: 'value' } 96 | }] 97 | ]); 98 | }); 99 | 100 | it('compile [null]', () => { 101 | 102 | const schema = Joi.compile([null]); 103 | Helper.equal(schema, Joi.valid(Joi.override, null)); 104 | }); 105 | 106 | it('compile [1]', () => { 107 | 108 | const schema = Joi.compile([1]); 109 | Helper.equal(schema, Joi.valid(Joi.override, 1)); 110 | }); 111 | 112 | it('compile ["a"]', () => { 113 | 114 | const schema = Joi.compile(['a']); 115 | Helper.equal(schema, Joi.valid(Joi.override, 'a')); 116 | }); 117 | 118 | it('compile [null, null, null]', () => { 119 | 120 | const schema = Joi.compile([null]); 121 | Helper.equal(schema, Joi.valid(Joi.override, null)); 122 | }); 123 | 124 | it('compile [1, 2, 3]', () => { 125 | 126 | const schema = Joi.compile([1, 2, 3]); 127 | Helper.equal(schema, Joi.valid(Joi.override, 1, 2, 3)); 128 | }); 129 | 130 | it('compile ["a", "b", "c"]', () => { 131 | 132 | const schema = Joi.compile(['a', 'b', 'c']); 133 | Helper.equal(schema, Joi.valid(Joi.override, 'a', 'b', 'c')); 134 | }); 135 | 136 | it('compile [null, "a", 1, true]', () => { 137 | 138 | const schema = Joi.compile([null, 'a', 1, true]); 139 | Helper.equal(schema, Joi.valid(Joi.override, null, 'a', 1, true)); 140 | }); 141 | }); 142 | 143 | describe('compile()', () => { 144 | 145 | it('compiles object with plain keys', () => { 146 | 147 | const schema = { 148 | a: 1 149 | }; 150 | 151 | expect(Joi.isSchema(schema)).to.be.false(); 152 | 153 | const compiled = Joi.compile(schema); 154 | expect(Joi.isSchema(compiled)).to.be.true(); 155 | }); 156 | 157 | it('compiles object with schema keys', () => { 158 | 159 | const schema = { 160 | a: Joi.number() 161 | }; 162 | 163 | expect(Joi.isSchema(schema)).to.be.false(); 164 | 165 | const compiled = Joi.compile(schema); 166 | expect(Joi.isSchema(compiled)).to.be.true(); 167 | }); 168 | 169 | it('errors on legacy schema', () => { 170 | 171 | const schema = Legacy.number(); 172 | expect(() => Joi.compile(schema)).to.throw(`Cannot mix different versions of joi schemas: ${require('@hapi/joi-legacy-test/package.json').version} ${require('../package.json').version}`); 173 | expect(() => Joi.compile(schema, { legacy: true })).to.not.throw(); 174 | }); 175 | 176 | it('errors on legacy keys', () => { 177 | 178 | const schema = { 179 | a: Legacy.number() 180 | }; 181 | 182 | expect(() => Joi.compile(schema)).to.throw('Cannot mix different versions of joi schemas (a)'); 183 | }); 184 | 185 | describe('legacy', () => { 186 | 187 | it('compiles object with plain keys', () => { 188 | 189 | const schema = { 190 | a: 1, 191 | b: [2, 3] 192 | }; 193 | 194 | expect(Joi.isSchema(schema)).to.be.false(); 195 | 196 | const compiled = Joi.compile(schema, { legacy: true }); 197 | expect(Joi.isSchema(compiled)).to.be.true(); 198 | }); 199 | 200 | it('compiles object with schema keys (v16)', () => { 201 | 202 | const schema = { 203 | a: Joi.number() 204 | }; 205 | 206 | expect(Joi.isSchema(schema)).to.be.false(); 207 | 208 | const compiled = Joi.compile(schema, { legacy: true }); 209 | expect(Joi.isSchema(compiled)).to.be.true(); 210 | }); 211 | 212 | it('compiles object with schema array items (v16)', () => { 213 | 214 | const schema = { 215 | a: [Joi.number()] 216 | }; 217 | 218 | expect(Joi.isSchema(schema)).to.be.false(); 219 | 220 | const compiled = Joi.compile(schema, { legacy: true }); 221 | expect(Joi.isSchema(compiled)).to.be.true(); 222 | }); 223 | 224 | it('compiles object with schema keys (v15)', () => { 225 | 226 | const schema = { 227 | a: Legacy.number() 228 | }; 229 | 230 | expect(Joi.isSchema(schema)).to.be.false(); 231 | 232 | const compiled = Joi.compile(schema, { legacy: true }); 233 | expect(Joi.isSchema(compiled, { legacy: true })).to.be.true(); 234 | expect(() => Joi.isSchema(compiled)).to.throw('Cannot mix different versions of joi schemas'); 235 | }); 236 | 237 | it('compiles object with schema keys (v15)', () => { 238 | 239 | const schema = { 240 | a: [Legacy.number()] 241 | }; 242 | 243 | expect(Joi.isSchema(schema)).to.be.false(); 244 | 245 | const compiled = Joi.compile(schema, { legacy: true }); 246 | expect(Joi.isSchema(compiled, { legacy: true })).to.be.true(); 247 | expect(() => Joi.isSchema(compiled)).to.throw('Cannot mix different versions of joi schemas'); 248 | }); 249 | }); 250 | }); 251 | }); 252 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Code = require('@hapi/code'); 4 | 5 | 6 | const internals = {}; 7 | 8 | 9 | const { expect } = Code; 10 | 11 | 12 | exports.skip = Symbol('skip'); 13 | 14 | 15 | exports.equal = function (a, b) { 16 | 17 | try { 18 | expect(a).to.equal(b, { deepFunction: true, skip: ['$_temp', '$_root'] }); 19 | } 20 | catch (err) { 21 | console.error(err.stack); 22 | err.at = internals.thrownAt(); // Adjust error location to test 23 | throw err; 24 | } 25 | }; 26 | 27 | 28 | exports.validate = function (schema, prefs, tests) { 29 | 30 | if (!tests) { 31 | tests = prefs; 32 | prefs = null; 33 | } 34 | 35 | try { 36 | expect(schema.$_root.build(schema.describe())).to.equal(schema, { deepFunction: true, skip: ['$_temp'] }); 37 | 38 | for (const test of tests) { 39 | const [input, pass, expected] = test; 40 | if (!pass) { 41 | expect(expected, 'Failing tests messages must be tested').to.exist(); 42 | } 43 | 44 | const { error: errord, value: valued } = schema.validate(input, Object.assign({ debug: true }, prefs)); 45 | const { error, value } = schema.validate(input, prefs); 46 | 47 | expect(error).to.equal(errord); 48 | expect(value).to.equal(valued); 49 | 50 | if (error && 51 | pass) { 52 | 53 | console.log(error); 54 | } 55 | 56 | if (!error && 57 | !pass) { 58 | 59 | console.log(input); 60 | } 61 | 62 | expect(!error).to.equal(pass); 63 | 64 | if (test.length === 2) { 65 | if (pass) { 66 | expect(input).to.equal(value); 67 | } 68 | 69 | continue; 70 | } 71 | 72 | if (pass) { 73 | if (expected !== exports.skip) { 74 | expect(value).to.equal(expected); 75 | } 76 | 77 | continue; 78 | } 79 | 80 | if (typeof expected === 'string') { 81 | expect(error.message).to.equal(expected); 82 | continue; 83 | } 84 | 85 | if (schema._preferences && schema._preferences.abortEarly === false || 86 | prefs && prefs.abortEarly === false) { 87 | 88 | expect(error.message).to.equal(expected.message); 89 | expect(error.details).to.equal(expected.details); 90 | } 91 | else { 92 | expect(error.details).to.have.length(1); 93 | expect(error.message).to.equal(error.details[0].message); 94 | expect(error.details[0]).to.equal(expected); 95 | } 96 | } 97 | } 98 | catch (err) { 99 | console.error(err.stack); 100 | err.at = internals.thrownAt(); // Adjust error location to test 101 | throw err; 102 | } 103 | }; 104 | 105 | 106 | internals.thrownAt = function () { 107 | 108 | const error = new Error(); 109 | const frame = error.stack.replace(error.toString(), '').split('\n').slice(1).filter((line) => !line.includes(__filename))[0]; 110 | const at = frame.match(/^\s*at \(?(.+)\:(\d+)\:(\d+)\)?$/); 111 | return { 112 | filename: at[1], 113 | line: at[2], 114 | column: at[3] 115 | }; 116 | }; 117 | -------------------------------------------------------------------------------- /test/types/any.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Code = require('@hapi/code'); 4 | const Joi = require('../..'); 5 | const Lab = require('@hapi/lab'); 6 | 7 | const Helper = require('../helper'); 8 | 9 | 10 | const internals = {}; 11 | 12 | 13 | const { describe, it } = exports.lab = Lab.script(); 14 | const { expect } = Code; 15 | 16 | 17 | describe('any', () => { 18 | 19 | describe('custom()', () => { 20 | 21 | it('uses custom validation', () => { 22 | 23 | const error = new Error('nope'); 24 | const method = (value, helpers) => { 25 | 26 | if (value === '1') { 27 | throw error; 28 | } 29 | 30 | if (value === '2') { 31 | return '3'; 32 | } 33 | 34 | if (value === '4') { 35 | return helpers.error('any.invalid'); 36 | } 37 | 38 | if (value === '5') { 39 | return undefined; 40 | } 41 | 42 | return value; 43 | }; 44 | 45 | const schema = Joi.string().custom(method, 'custom validation'); 46 | Helper.validate(schema, [ 47 | ['x', true, 'x'], 48 | ['2', true, '3'], 49 | ['5', true, undefined], 50 | ['1', false, { 51 | message: '"value" failed custom validation because nope', 52 | path: [], 53 | type: 'any.custom', 54 | context: { label: 'value', value: '1', error } 55 | }], 56 | ['4', false, { 57 | message: '"value" contains an invalid value', 58 | path: [], 59 | type: 'any.invalid', 60 | context: { label: 'value', value: '4' } 61 | }] 62 | ]); 63 | }); 64 | 65 | it('errors on invalid arguments', () => { 66 | 67 | const method = () => null; 68 | expect(() => Joi.any().custom({})).to.throw('Method must be a function'); 69 | expect(() => Joi.any().custom(method)).to.not.throw(); 70 | expect(() => Joi.any().custom(method, '')).to.throw('Description must be a non-empty string'); 71 | expect(() => Joi.any().custom(method, 0)).to.throw('Description must be a non-empty string'); 72 | expect(() => Joi.any().custom(method, [])).to.throw('Description must be a non-empty string'); 73 | }); 74 | }); 75 | 76 | describe('messages()', () => { 77 | 78 | it('aliases preferences', () => { 79 | 80 | const messages = { 81 | english: { 82 | value: 'it' 83 | } 84 | }; 85 | 86 | Helper.equal(Joi.string().valid('x').messages(messages), Joi.string().valid('x').prefs({ messages })); 87 | }); 88 | }); 89 | 90 | describe('shared()', () => { 91 | 92 | it('errors on missing id', () => { 93 | 94 | expect(() => Joi.any().shared(Joi.number())).to.throw('Schema must be a schema with an id'); 95 | expect(() => Joi.any().shared(1)).to.throw('Schema must be a schema with an id'); 96 | }); 97 | }); 98 | 99 | describe('warning()', () => { 100 | 101 | it('errors on invalid code', () => { 102 | 103 | expect(() => Joi.any().warning()).to.throw('Invalid warning code'); 104 | expect(() => Joi.any().warning(123)).to.throw('Invalid warning code'); 105 | }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /test/types/binary.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Code = require('@hapi/code'); 4 | const Lab = require('@hapi/lab'); 5 | const Joi = require('../..'); 6 | 7 | const Helper = require('../helper'); 8 | 9 | 10 | const internals = {}; 11 | 12 | 13 | const { describe, it } = exports.lab = Lab.script(); 14 | const { expect } = Code; 15 | 16 | 17 | describe('binary', () => { 18 | 19 | it('should throw an exception if arguments were passed.', () => { 20 | 21 | expect(() => Joi.binary('invalid argument.')).to.throw('The binary type does not allow arguments'); 22 | }); 23 | 24 | it('converts a string to a buffer', () => { 25 | 26 | const value = Joi.binary().validate('test').value; 27 | expect(value instanceof Buffer).to.equal(true); 28 | expect(value.length).to.equal(4); 29 | expect(value.toString('utf8')).to.equal('test'); 30 | }); 31 | 32 | it('converts a JSON encoded and decoded buffer to a buffer', () => { 33 | 34 | const testPngMagicNumber = Buffer.from('89504E470D0A', 'hex'); 35 | const jsonEncodedBuffer = JSON.stringify(testPngMagicNumber); 36 | const jsonDecodedBuffer = JSON.parse(jsonEncodedBuffer); 37 | 38 | const value = Joi.binary().validate(jsonDecodedBuffer).value; 39 | expect(value instanceof Buffer).to.equal(true); 40 | expect(value.length).to.equal(testPngMagicNumber.length); 41 | expect(value).to.equal(testPngMagicNumber); 42 | }); 43 | 44 | it('validates allowed buffer content', () => { 45 | 46 | const hello = Buffer.from('hello'); 47 | const schema = Joi.binary().valid(hello); 48 | 49 | Helper.validate(schema, [ 50 | ['hello', true, Buffer.from('hello')], 51 | [hello, true], 52 | [Buffer.from('hello'), true], 53 | ['goodbye', false, { 54 | message: '"value" must be [hello]', 55 | path: [], 56 | type: 'any.only', 57 | context: { value: Buffer.from('goodbye'), valids: [hello], label: 'value' } 58 | }], 59 | [Buffer.from('goodbye'), false, { 60 | message: '"value" must be [hello]', 61 | path: [], 62 | type: 'any.only', 63 | context: { value: Buffer.from('goodbye'), valids: [hello], label: 'value' } 64 | }], 65 | [Buffer.from('HELLO'), false, { 66 | message: '"value" must be [hello]', 67 | path: [], 68 | type: 'any.only', 69 | context: { value: Buffer.from('HELLO'), valids: [hello], label: 'value' } 70 | }] 71 | ]); 72 | }); 73 | 74 | describe('cast()', () => { 75 | 76 | it('casts value to string', () => { 77 | 78 | const schema = Joi.binary().cast('string'); 79 | 80 | Helper.validate(schema, [ 81 | [Buffer.from('test'), true, 'test'] 82 | ]); 83 | }); 84 | 85 | it('casts value to string (in object)', () => { 86 | 87 | const schema = Joi.object({ 88 | a: Joi.binary().cast('string') 89 | }); 90 | 91 | Helper.validate(schema, [ 92 | [{ a: Buffer.from('test') }, true, { a: 'test' }], 93 | [{}, true] 94 | ]); 95 | }); 96 | 97 | it('ignores null', () => { 98 | 99 | const schema = Joi.binary().allow(null).cast('string'); 100 | 101 | Helper.validate(schema, [ 102 | [null, true] 103 | ]); 104 | }); 105 | 106 | it('ignores string', () => { 107 | 108 | const schema = Joi.binary().allow('x').cast('string'); 109 | 110 | Helper.validate(schema, [ 111 | ['x', true] 112 | ]); 113 | }); 114 | 115 | it('does not leak casts to any', () => { 116 | 117 | expect(() => Joi.any().cast('string')).to.throw('Type any does not support casting to string'); 118 | }); 119 | }); 120 | 121 | describe('validate()', () => { 122 | 123 | it('returns an error when a non-buffer or non-string is used', () => { 124 | 125 | Helper.validate(Joi.binary(), [ 126 | [5, false, { 127 | message: '"value" must be a buffer or a string', 128 | path: [], 129 | type: 'binary.base', 130 | context: { label: 'value', value: 5 } 131 | }] 132 | ]); 133 | }); 134 | 135 | it('returns an error when malformed JSON object is used', () => { 136 | 137 | Helper.validate(Joi.binary(), [ 138 | [{ foo: 'bar' }, false, { 139 | message: '"value" must be a buffer or a string', 140 | path: [], 141 | type: 'binary.base', 142 | context: { label: 'value', value: { foo: 'bar' } } 143 | }], 144 | [null, false, { 145 | message: '"value" must be a buffer or a string', 146 | path: [], 147 | type: 'binary.base', 148 | context: { label: 'value', value: null } 149 | }], 150 | [{ type: 'Buffer' }, false, { 151 | message: '"value" must be a buffer or a string', 152 | path: [], 153 | type: 'binary.base', 154 | context: { label: 'value', value: { type: 'Buffer' } } 155 | }] 156 | ]); 157 | }); 158 | 159 | it('returns an error when a JSON encoded & decoded buffer object is used in strict mode', () => { 160 | 161 | // Generate Buffer and stringify it as JSON. 162 | const testPngMagicNumber = Buffer.from('89504E470D0A', 'hex'); 163 | const jsonEncodedBuffer = JSON.stringify(testPngMagicNumber); 164 | const jsonDecodedBuffer = JSON.parse(jsonEncodedBuffer); 165 | 166 | Helper.validate(Joi.binary().strict(), [ 167 | [jsonDecodedBuffer, false, { 168 | message: '"value" must be a buffer or a string', 169 | path: [], 170 | type: 'binary.base', 171 | context: { label: 'value', value: jsonDecodedBuffer } 172 | }] 173 | ]); 174 | }); 175 | 176 | it('accepts a buffer object', () => { 177 | 178 | Helper.validate(Joi.binary(), [ 179 | [Buffer.from('hello world'), true] 180 | ]); 181 | }); 182 | 183 | it('accepts a buffer object in strict mode', () => { 184 | 185 | Helper.validate(Joi.binary().strict(), [ 186 | [Buffer.from('hello world'), true], 187 | ['hello world', false, '"value" must be a buffer or a string'] 188 | ]); 189 | }); 190 | }); 191 | 192 | describe('encoding()', () => { 193 | 194 | it('applies encoding', () => { 195 | 196 | const schema = Joi.binary().encoding('base64'); 197 | 198 | Helper.validate(schema, [ 199 | [Buffer.from('abcdef'), true] 200 | ]); 201 | }); 202 | 203 | it('throws when encoding is invalid', () => { 204 | 205 | expect(() => Joi.binary().encoding('base6')).to.throw('Invalid encoding: base6'); 206 | }); 207 | 208 | it('avoids unnecessary cloning when called twice', () => { 209 | 210 | const schema = Joi.binary().encoding('base64'); 211 | expect(schema.encoding('base64')).to.shallow.equal(schema); 212 | }); 213 | }); 214 | 215 | describe('min()', () => { 216 | 217 | it('validates buffer size', () => { 218 | 219 | const schema = Joi.binary().min(5); 220 | 221 | Helper.validate(schema, [ 222 | [Buffer.from('testing'), true], 223 | [Buffer.from('test'), false, { 224 | message: '"value" must be at least 5 bytes', 225 | path: [], 226 | type: 'binary.min', 227 | context: { limit: 5, value: Buffer.from('test'), label: 'value' } 228 | }] 229 | ]); 230 | }); 231 | 232 | it('throws when min is not a number', () => { 233 | 234 | expect(() => Joi.binary().min('a')).to.throw('limit must be a positive integer or reference'); 235 | }); 236 | 237 | it('throws when min is not an integer', () => { 238 | 239 | expect(() => Joi.binary().min(1.2)).to.throw('limit must be a positive integer or reference'); 240 | }); 241 | }); 242 | 243 | describe('max()', () => { 244 | 245 | it('validates buffer size', () => { 246 | 247 | const schema = Joi.binary().max(5); 248 | 249 | Helper.validate(schema, [ 250 | [Buffer.from('testing'), false, { 251 | message: '"value" must be less than or equal to 5 bytes', 252 | path: [], 253 | type: 'binary.max', 254 | context: { 255 | limit: 5, 256 | value: Buffer.from('testing'), 257 | label: 'value' 258 | } 259 | }], 260 | [Buffer.from('test'), true] 261 | ]); 262 | }); 263 | 264 | it('throws when max is not a number', () => { 265 | 266 | expect(() => Joi.binary().max('a')).to.throw('limit must be a positive integer or reference'); 267 | }); 268 | 269 | it('throws when max is not an integer', () => { 270 | 271 | expect(() => Joi.binary().max(1.2)).to.throw('limit must be a positive integer or reference'); 272 | }); 273 | }); 274 | 275 | describe('length()', () => { 276 | 277 | it('validates buffer size', () => { 278 | 279 | const schema = Joi.binary().length(4); 280 | 281 | Helper.validate(schema, [ 282 | [Buffer.from('test'), true], 283 | [Buffer.from('testing'), false, { 284 | message: '"value" must be 4 bytes', 285 | path: [], 286 | type: 'binary.length', 287 | context: { 288 | limit: 4, 289 | value: Buffer.from('testing'), 290 | label: 'value' 291 | } 292 | }] 293 | ]); 294 | }); 295 | 296 | it('throws when length is not a number', () => { 297 | 298 | expect(() => Joi.binary().length('a')).to.throw('limit must be a positive integer or reference'); 299 | }); 300 | 301 | it('throws when length is not an integer', () => { 302 | 303 | expect(() => Joi.binary().length(1.2)).to.throw('limit must be a positive integer or reference'); 304 | }); 305 | }); 306 | }); 307 | -------------------------------------------------------------------------------- /test/types/symbol.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Code = require('@hapi/code'); 4 | const Lab = require('@hapi/lab'); 5 | const Joi = require('../..'); 6 | 7 | const Helper = require('../helper'); 8 | 9 | 10 | const internals = {}; 11 | 12 | 13 | const { describe, it } = exports.lab = Lab.script(); 14 | const { expect } = Code; 15 | 16 | 17 | describe('symbol', () => { 18 | 19 | it('cannot be called on its own', () => { 20 | 21 | const symbol = Joi.symbol; 22 | expect(() => symbol()).to.throw('Must be invoked on a Joi instance.'); 23 | }); 24 | 25 | it('should throw an exception if arguments were passed.', () => { 26 | 27 | expect(() => Joi.symbol('invalid argument.')).to.throw('The symbol type does not allow arguments'); 28 | }); 29 | 30 | describe('clone()', () => { 31 | 32 | it('clones a symbols type', () => { 33 | 34 | const schema = Joi.symbol(); 35 | const clone = schema.clone(); 36 | Helper.equal(schema, clone); 37 | expect(schema).to.not.shallow.equal(clone); 38 | }); 39 | }); 40 | 41 | describe('validate()', () => { 42 | 43 | it('handles plain symbols', () => { 44 | 45 | const symbols = [Symbol(1), Symbol(2)]; 46 | const rule = Joi.symbol(); 47 | Helper.validate(rule, [ 48 | [symbols[0], true, symbols[0]], 49 | [symbols[1], true, symbols[1]], 50 | [1, false, { 51 | message: '"value" must be a symbol', 52 | path: [], 53 | type: 'symbol.base', 54 | context: { label: 'value', value: 1 } 55 | }] 56 | ]); 57 | }); 58 | 59 | it('handles simple lookup', () => { 60 | 61 | const symbols = [Symbol(1), Symbol(2)]; 62 | const otherSymbol = Symbol(1); 63 | const rule = Joi.symbol().valid(...symbols); 64 | Helper.validate(rule, [ 65 | [symbols[0], true, symbols[0]], 66 | [symbols[1], true, symbols[1]], 67 | [otherSymbol, false, { 68 | message: '"value" must be one of [Symbol(1), Symbol(2)]', 69 | path: [], 70 | type: 'any.only', 71 | context: { value: otherSymbol, label: 'value', valids: symbols } 72 | }] 73 | ]); 74 | }); 75 | 76 | describe('map', () => { 77 | 78 | it('converts keys to correct symbol', () => { 79 | 80 | const symbols = [Symbol(1), Symbol(2)]; 81 | const otherSymbol = Symbol(1); 82 | const map = new Map([[1, symbols[0]], ['two', symbols[1]]]); 83 | const rule = Joi.symbol().map(map); 84 | Helper.validate(rule, [ 85 | [1, true, symbols[0]], 86 | [symbols[0], true, symbols[0]], 87 | ['1', false, { 88 | message: `"value" must be one of [1 -> Symbol(1), two -> Symbol(2)]`, 89 | path: [], 90 | type: 'symbol.map', 91 | context: { label: 'value', value: '1', map } 92 | }], 93 | ['two', true, symbols[1]], 94 | [otherSymbol, false, { 95 | message: '"value" must be one of [Symbol(1), Symbol(2)]', 96 | path: [], 97 | type: 'any.only', 98 | context: { value: otherSymbol, label: 'value', valids: symbols } 99 | }] 100 | ]); 101 | }); 102 | 103 | it('converts keys from object', () => { 104 | 105 | const symbols = [Symbol('one'), Symbol('two')]; 106 | const otherSymbol = Symbol('one'); 107 | const rule = Joi.symbol().map({ one: symbols[0], two: symbols[1] }); 108 | Helper.validate(rule, [ 109 | [symbols[0], true, symbols[0]], 110 | ['one', true, symbols[0]], 111 | ['two', true, symbols[1]], 112 | [otherSymbol, false, { 113 | message: '"value" must be one of [Symbol(one), Symbol(two)]', 114 | path: [], 115 | type: 'any.only', 116 | context: { value: otherSymbol, label: 'value', valids: symbols } 117 | }], 118 | ['toString', false, { 119 | message: `"value" must be one of [one -> Symbol(one), two -> Symbol(two)]`, 120 | path: [], 121 | type: 'symbol.map', 122 | context: { label: 'value', value: 'toString', map: new Map([['one', symbols[0]], ['two', symbols[1]]]) } 123 | }] 124 | ]); 125 | }); 126 | 127 | it('appends to existing map', () => { 128 | 129 | const symbols = [Symbol(1), Symbol(2)]; 130 | const otherSymbol = Symbol(1); 131 | const rule = Joi.symbol().map([[1, symbols[0]]]).map([[2, symbols[1]]]); 132 | Helper.validate(rule, [ 133 | [1, true, symbols[0]], 134 | [2, true, symbols[1]], 135 | [otherSymbol, false, { 136 | message: '"value" must be one of [Symbol(1), Symbol(2)]', 137 | path: [], 138 | type: 'any.only', 139 | context: { value: otherSymbol, label: 'value', valids: symbols } 140 | }] 141 | ]); 142 | }); 143 | 144 | it('throws on bad input', () => { 145 | 146 | expect( 147 | () => Joi.symbol().map() 148 | ).to.throw('Iterable must be an iterable or object'); 149 | 150 | expect( 151 | () => Joi.symbol().map(Symbol()) 152 | ).to.throw('Iterable must be an iterable or object'); 153 | 154 | expect( 155 | () => Joi.symbol().map([undefined]) 156 | ).to.throw('Entry must be an iterable'); 157 | 158 | expect( 159 | () => Joi.symbol().map([123]) 160 | ).to.throw('Entry must be an iterable'); 161 | 162 | expect( 163 | () => Joi.symbol().map([[123, 456]]) 164 | ).to.throw('Value must be a Symbol'); 165 | 166 | expect( 167 | () => Joi.symbol().map([[{}, Symbol()]]) 168 | ).to.throw('Key must not be of type object, function, or Symbol'); 169 | 170 | expect( 171 | () => Joi.symbol().map([[() => { }, Symbol()]]) 172 | ).to.throw('Key must not be of type object, function, or Symbol'); 173 | 174 | expect( 175 | () => Joi.symbol().map([[Symbol(), Symbol()]]) 176 | ).to.throw('Key must not be of type object, function, or Symbol'); 177 | }); 178 | }); 179 | 180 | it('handles plain symbols when convert is disabled', () => { 181 | 182 | const symbols = [Symbol(1), Symbol(2)]; 183 | const schema = Joi.symbol().map([[1, symbols[0]], ['two', symbols[1]]]).prefs({ convert: false }); 184 | Helper.validate(schema, [[symbols[1], true, symbols[1]]]); 185 | }); 186 | 187 | it('errors on mapped input and convert is disabled', () => { 188 | 189 | const symbols = [Symbol(1), Symbol(2)]; 190 | const schema = Joi.symbol().map([[1, symbols[0]], ['two', symbols[1]]]).prefs({ convert: false }); 191 | Helper.validate(schema, [[1, false, { 192 | message: '"value" must be one of [Symbol(1), Symbol(2)]', 193 | path: [], 194 | type: 'any.only', 195 | context: { value: 1, valids: symbols, label: 'value' } 196 | }]]); 197 | }); 198 | }); 199 | }); 200 | -------------------------------------------------------------------------------- /test/values.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Code = require('@hapi/code'); 4 | const Joi = require('..'); 5 | const Lab = require('@hapi/lab'); 6 | 7 | const Helper = require('./helper'); 8 | const Values = require('../lib/values'); 9 | 10 | 11 | const internals = {}; 12 | 13 | 14 | const { describe, it } = exports.lab = Lab.script(); 15 | const { expect } = Code; 16 | 17 | 18 | describe('Values', () => { 19 | 20 | describe('add()', () => { 21 | 22 | it('allows valid values to be set', () => { 23 | 24 | expect(() => { 25 | 26 | const set = new Values(); 27 | set.add(true); 28 | set.add(1); 29 | set.add('hello'); 30 | set.add(new Date()); 31 | set.add(Symbol('foo')); 32 | }).not.to.throw(); 33 | }); 34 | 35 | it('ignores refs added multiple times', () => { 36 | 37 | const set = new Values(); 38 | const ref = Joi.ref('x'); 39 | set.add(ref); 40 | set.add(ref); 41 | expect(set).to.have.length(1); 42 | }); 43 | }); 44 | 45 | describe('clone()', () => { 46 | 47 | it('returns a new Values', () => { 48 | 49 | const set = new Values(); 50 | set.add(null); 51 | const otherValids = set.clone(); 52 | otherValids.add('null'); 53 | expect(set.has(null)).to.equal(true); 54 | expect(otherValids.has(null)).to.equal(true); 55 | expect(set.has('null')).to.equal(false); 56 | expect(otherValids.has('null')).to.equal(true); 57 | }); 58 | }); 59 | 60 | describe('concat()', () => { 61 | 62 | it('merges into a new Values', () => { 63 | 64 | const set = new Values(); 65 | const otherValids = set.clone(); 66 | set.add(null); 67 | otherValids.add('null'); 68 | const thirdSet = otherValids.concat(set); 69 | expect(set.has(null)).to.equal(true); 70 | expect(otherValids.has(null)).to.equal(false); 71 | expect(set.has('null')).to.equal(false); 72 | expect(otherValids.has('null')).to.equal(true); 73 | expect(thirdSet.has(null)).to.equal(true); 74 | expect(thirdSet.has('null')).to.equal(true); 75 | }); 76 | 77 | it('merges keeps refs flag set', () => { 78 | 79 | const set = new Values(); 80 | set.add(Joi.ref('x')); 81 | set.concat(new Values()); 82 | expect(set._refs.size).to.equal(1); 83 | }); 84 | }); 85 | 86 | describe('get()', () => { 87 | 88 | it('compares empty string to refs when insensitive', () => { 89 | 90 | const schema = Joi.object({ 91 | a: Joi.string().allow(3).default(''), 92 | b: Joi.string().insensitive().valid(Joi.ref('a')) 93 | }); 94 | 95 | Helper.validate(schema, [ 96 | [{ b: '' }, true, { b: '', a: '' }], 97 | [{ b: 'x' }, false, '"b" must be [ref:a]'], 98 | [{ b: 2 }, false, '"b" must be [ref:a]'], 99 | [{ a: 3, b: 3 }, true] 100 | ]); 101 | }); 102 | }); 103 | 104 | describe('has()', () => { 105 | 106 | it('compares date to null', () => { 107 | 108 | const set = new Values(); 109 | set.add(null); 110 | expect(set.has(new Date())).to.be.false(); 111 | }); 112 | 113 | it('compares buffer to null', () => { 114 | 115 | const set = new Values(); 116 | set.add(null); 117 | expect(set.has(Buffer.from(''))).to.be.false(); 118 | }); 119 | 120 | it('compares different types of values', () => { 121 | 122 | let set = new Values(); 123 | set.add(1); 124 | expect(set.has(1)).to.be.true(); 125 | expect(set.has(2)).to.be.false(); 126 | 127 | const d = new Date(); 128 | set = new Values(); 129 | set.add(d); 130 | expect(set.has(new Date(d.getTime()))).to.be.true(); 131 | expect(set.has(new Date(d.getTime() + 1))).to.be.false(); 132 | 133 | const str = 'foo'; 134 | set = new Values(); 135 | set.add(str); 136 | expect(set.has(str)).to.be.true(); 137 | expect(set.has('foobar')).to.be.false(); 138 | 139 | const s = Symbol('foo'); 140 | set = new Values(); 141 | set.add(s); 142 | expect(set.has(s)).to.be.true(); 143 | expect(set.has(Symbol('foo'))).to.be.false(); 144 | 145 | const o = {}; 146 | set = new Values(); 147 | set.add(o); 148 | expect(set.has(o)).to.be.true(); 149 | expect(set.has({})).to.be.true(); 150 | 151 | const f = () => { }; 152 | set = new Values(); 153 | set.add(f); 154 | expect(set.has(f)).to.be.true(); 155 | expect(set.has(() => { })).to.be.false(); 156 | 157 | const b = Buffer.from('foo'); 158 | set = new Values(); 159 | set.add(b); 160 | expect(set.has(b)).to.be.true(); 161 | expect(set.has(Buffer.from('foobar'))).to.be.false(); 162 | }); 163 | }); 164 | 165 | describe('values()', () => { 166 | 167 | it('returns array', () => { 168 | 169 | const set = new Values(); 170 | set.add('x'); 171 | set.add('y'); 172 | expect(set.values()).to.equal(['x', 'y']); 173 | }); 174 | 175 | it('strips undefined', () => { 176 | 177 | const set = new Values(); 178 | set.add(undefined); 179 | set.add('x'); 180 | expect(set.values({ display: true })).to.not.include(undefined).and.to.equal(['x']); 181 | }); 182 | 183 | it('ignores absent display option', () => { 184 | 185 | const set = new Values(); 186 | set.add(undefined); 187 | set.add('x'); 188 | expect(set.values({})).to.equal([undefined, 'x']); 189 | }); 190 | }); 191 | }); 192 | --------------------------------------------------------------------------------