diff --git a/API.md b/API.md index 3d4665aee..62fc37100 100644 --- a/API.md +++ b/API.md @@ -301,7 +301,12 @@ The extension makes use of some common structures that need to be described prio #### Extension -`extension` can be a single extension object or an array of extension objects using the following parameters : +`extension` can be : +- a single extension object +- a factory function generating an extension object +- or an array of those + +Extension objects use the following parameters : * `name` - name of the new type you are defining, this can be an existing type. **Required**. * `base` - an existing Joi schema to base your type upon. Defaults to `Joi.any()`. * `coerce` - an optional function that runs before the base, usually serves when you want to coerce values of a different type than your base. It takes 3 arguments `value`, `state` and `options`. @@ -315,6 +320,8 @@ The extension makes use of some common structures that need to be described prio * `validate` - an optional function to validate values that takes 4 parameters `params`, `value`, `state` and `options`. One of `setup` or `validate` **must** be provided. * `description` - an optional string or function taking the parameters as argument to describe what the rule is doing. +Factory functions are advised if you intend to publish your extensions for others to use, because they are capable of using an extended joi being built, thus avoiding any erasure when using multiple extensions at the same time. See an example of a factory function in the section below. + #### npm note If you publish your extension on npm, make sure to add `joi` and `extension` as keywords so that it's discoverable more easily. @@ -323,8 +330,8 @@ If you publish your extension on npm, make sure to add `joi` and `extension` as ```js const Joi = require('joi'); -const customJoi = Joi.extend({ - base: Joi.number(), +const customJoi = Joi.extend((joi) => ({ + base: joi.number(), name: 'number', language: { round: 'needs to be a rounded number', // Used below as 'number.round' @@ -371,7 +378,7 @@ const customJoi = Joi.extend({ } } ] -}); +})); const schema = customJoi.number().round().dividable(3); ``` diff --git a/lib/index.js b/lib/index.js index 3fefd5d06..f2806feb4 100644 --- a/lib/index.js +++ b/lib/index.js @@ -206,7 +206,14 @@ internals.root = function () { Object.assign(joi, this); for (let i = 0; i < extensions.length; ++i) { - const extension = extensions[i]; + let extension = extensions[i]; + + if (typeof extension === 'function') { + extension = extension(joi); + } + + this.assert(extension, root.extensionSchema); + const base = (extension.base || this.any()).clone(); // Cloning because we're going to override language afterwards const ctor = base.constructor; const type = class extends ctor { // eslint-disable-line no-loop-func @@ -345,7 +352,7 @@ internals.root = function () { return joi; }; - root.extensionsSchema = internals.array.items(internals.object.keys({ + root.extensionSchema = internals.object.keys({ base: internals.object.type(Any, 'Joi object'), name: internals.string.required(), coerce: internals.object._func().arity(3), @@ -362,7 +369,9 @@ internals.root = function () { ], description: [internals.string, internals.object._func().arity(1)] }).or('setup', 'validate')) - })).strict(); + }).strict(); + + root.extensionsSchema = internals.array.items([internals.object, internals.object._func().arity(1)]).strict(); root.version = require('../package.json').version; diff --git a/package.json b/package.json index 253e86646..8e4540674 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ }, "scripts": { "test": "lab -t 100 -a code -L", - "test-debug": "node $NODE_DEBUG_OPTION ./node_modules/.bin/lab -a code", + "test-debug": "lab -a code", "test-cov-html": "lab -r html -o coverage.html -a code", "toc": "node generate-readme-toc.js", "version": "npm run toc && git add API.md README.md" diff --git a/test/index.js b/test/index.js index 1eaf32909..b87ce97a1 100644 --- a/test/index.js +++ b/test/index.js @@ -1866,10 +1866,10 @@ describe('Joi', () => { it('must be an object or array of objects', (done) => { - expect(() => Joi.extend(true)).to.throw(/"0" must be an object/); - expect(() => Joi.extend(null)).to.throw(/"0" must be an object/); - expect(() => Joi.extend([{ name: 'foo' }, true])).to.throw(/"1" must be an object/); - expect(() => Joi.extend([{ name: 'foo' }, null])).to.throw(/"1" must be an object/); + expect(() => Joi.extend(true)).to.throw(/"value" at position 0 does not match any of the allowed types/); + expect(() => Joi.extend(null)).to.throw(/"value" at position 0 does not match any of the allowed types/); + expect(() => Joi.extend([{ name: 'foo' }, true])).to.throw(/"value" at position 1 does not match any of the allowed types/); + expect(() => Joi.extend([{ name: 'foo' }, null])).to.throw(/"value" at position 1 does not match any of the allowed types/); expect(() => Joi.extend()).to.throw('You need to provide at least one extension'); done(); }); @@ -2786,5 +2786,103 @@ describe('Joi', () => { done(); }); + it('should be able to define a type in a factory function', (done) => { + + const customJoi = Joi.extend((joi) => ({ + name: 'myType' + })); + + expect(() => customJoi.myType()).to.not.throw(); + done(); + }); + + it('should be able to use types defined in the same extend call', (done) => { + + const customJoi = Joi.extend([ + { + name: 'myType' + }, + (joi) => ({ + name: 'mySecondType', + base: joi.myType() + }) + ]); + + expect(() => customJoi.mySecondType()).to.not.throw(); + done(); + }); + + it('should be able to merge rules when type is defined several times in the same extend call', (done) => { + + const customJoi = Joi.extend([ + (joi) => ({ + name: 'myType', + base: joi.myType ? joi.myType() : joi.number(), // Inherit an already existing implementation or number + rules: [ + { + name: 'foo', + validate(params, value, state, options) { + + return 1; + } + } + ] + }), + (joi) => ({ + name: 'myType', + base: joi.myType ? joi.myType() : joi.number(), + rules: [ + { + name: 'bar', + validate(params, value, state, options) { + + return 2; + } + } + ] + }) + ]); + + expect(() => customJoi.myType().foo().bar()).to.not.throw(); + expect(customJoi.attempt({ a: 123, b: 456 }, { a: customJoi.myType().foo(), b: customJoi.myType().bar() })).to.equal({ a: 1, b: 2 }); + done(); + }); + + it('should only keep last definition when type is defined several times with different bases', (done) => { + + const customJoi = Joi.extend([ + (joi) => ({ + name: 'myType', + base: Joi.number(), + rules: [ + { + name: 'foo', + validate(params, value, state, options) { + + return 1; + } + } + ] + }), + (joi) => ({ + name: 'myType', + base: Joi.string(), + rules: [ + { + name: 'bar', + validate(params, value, state, options) { + + return 2; + } + } + ] + }) + ]); + + expect(() => customJoi.myType().foo()).to.throw(); + expect(() => customJoi.myType().bar()).to.not.throw(); + done(); + }); + }); });