Skip to content

Commit

Permalink
Allow extensions factory functions
Browse files Browse the repository at this point in the history
Fixes #1047
  • Loading branch information
Marsup committed Mar 18, 2017
1 parent 3b34640 commit 49a0c76
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 12 deletions.
15 changes: 11 additions & 4 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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.
Expand All @@ -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'
Expand Down Expand Up @@ -371,7 +378,7 @@ const customJoi = Joi.extend({
}
}
]
});
}));

const schema = customJoi.number().round().dividable(3);
```
Expand Down
15 changes: 12 additions & 3 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand All @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
106 changes: 102 additions & 4 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand Down Expand Up @@ -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();
});

});
});

0 comments on commit 49a0c76

Please sign in to comment.