Skip to content

Commit

Permalink
Refactor module root. Closes #1985
Browse files Browse the repository at this point in the history
  • Loading branch information
hueniverse committed Jul 23, 2019
1 parent 45861ac commit 59195cd
Show file tree
Hide file tree
Showing 12 changed files with 178 additions and 157 deletions.
29 changes: 16 additions & 13 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
- [Joi](#joi)
- [`assert(value, schema, [message], [options])` - aliases: `attempt`](#assertvalue-schema-message-options---aliases-attempt)
- [`cache.provision([options])`](#cacheprovisionoptions)
- [`bind()`](#bind)
- [`compile(schema, [options])`](#compileschema-options)
- [`defaults(fn)`](#defaultsfn)
- [`expression(template, [options])` - aliases: `x`](#expressiontemplate-options---aliases-x)
Expand All @@ -19,6 +18,7 @@
- [`ref(key, [options])`](#refkey-options)
- [Relative references](#relative-references)
- [`version`](#version)
- [`types()`](#types)
- [`any`](#any)
- [`any.type`](#anytype)
- [`any.allow(...values)`](#anyallowvalues)
Expand Down Expand Up @@ -311,18 +311,6 @@ booleans) where:
- `max` - number of items to store in the cache before the least used items are dropped.
Defaults to `1000`.

### `bind()`

By default, some **joi** methods to function properly need to rely on the **joi** instance they are attached to because they use `this` internally. So `Joi.string()` works but if you extract the function from it and call `string()` it won't. `bind()` creates a new **joi** instance where all the functions relying on `this` are bound to the **joi** instance.

```js
const { object, string } = require('@hapi/joi').bind();

const schema = object({
property: string().min(4)
});
```

### `compile(schema, [options])`

Converts literal schema definition to **joi** schema object (or returns the same back if already a
Expand Down Expand Up @@ -569,6 +557,21 @@ Note that if a reference tries to reach beyond the value root, validation fails.

Property showing the current version of **joi** being used.

### `types()`

Returns an object where each key is a plain joi schema type. Useful for creating type shortcuts
using deconstruction. Note that the types are already formed and do not need to be called as
functions (e.g. `string`, not `string()`).

```js
const Joi = require('@hapi/joi');
const { object, string } = Joi.types();

const schema = object.keys({
property: string.min(4)
});
```

### `any`

Generates a schema object that matches any data type.
Expand Down
8 changes: 3 additions & 5 deletions lib/extend.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,8 @@ exports.root = function (root, extensions) {
Hoek.assert(extensions.length, 'You need to provide at least one extension');
root.assert(extensions, Schemas.extensions);

const joi = Object.create(root.any());
Object.assign(joi, root);
joi._root = joi;
joi._binds = new Set(joi._binds);
const joi = Object.assign({}, root);
joi._types = new Set(joi._types);

for (let extension of extensions) {
if (typeof extension === 'function') {
Expand All @@ -39,7 +37,7 @@ exports.root = function (root, extensions) {
return Common.callWithDefaults(this, generator(args), args);
};

joi._binds.add(extension.name);
joi._types.add(extension.name);
}

return joi;
Expand Down
102 changes: 38 additions & 64 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,76 +16,50 @@ const Pkg = require('../package.json');


const internals = {
alternatives: require('./types/alternatives'),
array: require('./types/array'),
boolean: require('./types/boolean'),
binary: require('./types/binary'),
date: require('./types/date'),
func: require('./types/func'),
link: require('./types/link'),
number: require('./types/number'),
object: require('./types/object'),
string: require('./types/string'),
symbol: require('./types/symbol'),

binds: [
'any',
'alt',
'alternatives',
'array',
'bool',
'boolean',
'binary',
'date',
'func',
'link',
'number',
'object',
'string',
'symbol',

'bind',
'build',
'compile',
'defaults',
'extend'
]
types: {
alternatives: require('./types/alternatives'),
any: new Any(),
array: require('./types/array'),
boolean: require('./types/boolean'),
binary: require('./types/binary'),
date: require('./types/date'),
func: require('./types/func'),
link: require('./types/link'),
number: require('./types/number'),
object: require('./types/object'),
string: require('./types/string'),
symbol: require('./types/symbol')
}
};


internals.anyMethods = Object.keys(Any.prototype)
.filter((key) => key[0] !== '_' && key !== 'isImmutable')
.concat(internals.binds);


internals.root = function () {

const any = new Any();
const root = any.clone();
root._root = root;
root._binds = new Set(internals.anyMethods);

root.any = function (...args) {

Hoek.assert(!args.length, 'The any type does not allow arguments');
return Common.callWithDefaults(this, any, args);
const root = {
_types: new Set(Object.keys(internals.types))
};

for (const type of ['array', 'boolean', 'binary', 'date', 'func', 'number', 'string', 'symbol']) {
// Types

for (const type of root._types) {
root[type] = function (...args) {

Hoek.assert(!args.length, 'The', type, 'type does not allow arguments');
return Common.callWithDefaults(this, internals[type], args);
Hoek.assert(!args.length || ['alternatives', 'link', 'object'].includes(type), 'The', type, 'type does not allow arguments');
return Common.callWithDefaults(this, internals.types[type], args);
};
}

for (const type of ['alternatives', 'link', 'object']) {
root[type] = function (...args) {
// Shortcuts

for (const method of ['allow', 'disallow', 'equal', 'exist', 'forbidden', 'invalid', 'not', 'optional', 'options', 'prefs', 'preferences', 'required', 'valid', 'when']) {
root[method] = function (...args) {

return Common.callWithDefaults(this, internals[type], args);
return this.any()[method](...args);
};
}

// Methods

Object.assign(root, internals.methods);

// Aliases
Expand Down Expand Up @@ -130,16 +104,6 @@ internals.methods = {
throw error;
},

bind: function () {

const joi = Object.create(this);
for (const bind of joi._binds) {
joi[bind] = joi[bind].bind(joi);
}

return joi;
},

build: function (desc) {

return Manifest.build(this, desc);
Expand Down Expand Up @@ -194,6 +158,16 @@ internals.methods = {
ref: function (...args) {

return Ref.create(...args);
},

types: function () {

const types = {};
for (const type of this._types) {
types[type] = this[type]();
}

return types;
}
};

Expand Down
5 changes: 4 additions & 1 deletion lib/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ const internals = {
insensitive: false,
once: true,
only: false,
presence: 'optional',
sparse: false,
strip: false,
timestamp: false,
Expand Down Expand Up @@ -392,6 +391,10 @@ internals.Builder = class {

build(desc, options = {}) {

if (desc === null) {
return null;
}

if (Array.isArray(desc)) {
return desc.map((item) => this.build(item));
}
Expand Down
7 changes: 7 additions & 0 deletions lib/ref.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,13 @@ internals.Ref.prototype[Common.symbols.ref] = true;

exports.build = function (desc) {

desc = Object.assign({}, internals.defaults, desc);
if (desc.type === 'value' &&
desc.ancestor === undefined) {

desc.ancestor = 1;
}

return new internals.Ref(desc);
};

Expand Down
11 changes: 0 additions & 11 deletions test/extend.js
Original file line number Diff line number Diff line change
Expand Up @@ -1215,17 +1215,6 @@ describe('extension', () => {
});
});

it('should return a custom Joi as an instance of Any', () => {

const customJoi = Joi.extend({
name: 'myType'
});

const Any = require('../lib/types/any');

expect(customJoi).to.be.an.instanceof(Any);
});

it('should return a custom Joi with types not inheriting root properties', () => {

const customJoi = Joi.extend({
Expand Down
35 changes: 35 additions & 0 deletions test/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,39 @@ const internals = {};
const { expect } = Code;


exports.compare = function (a, b) {

const clearRuleset = function (schema, _seen) {

const seen = _seen || new Set();

if (!schema ||
typeof schema !== 'object') {

return;
}

if (seen.has(schema)) {
return;
}

seen.add(schema);

for (const key in schema) {
if (key === '_ruleset') {
schema._ruleset = null;
}

clearRuleset(schema[key], seen);
}

return schema;
};

expect(clearRuleset(a.clone())).to.equal(clearRuleset(b.clone()));
};


exports.validate = function (schema, config) {

return exports.validateOptions(schema, config, null);
Expand All @@ -20,6 +53,8 @@ exports.validateOptions = function (schema, config, options) {

try {
const compiled = Joi.compile(schema);
//exports.compare(Joi.build(compiled.describe()), compiled);

for (let i = 0; i < config.length; ++i) {

const item = config[i];
Expand Down
34 changes: 14 additions & 20 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2408,13 +2408,6 @@ describe('Joi', () => {
}]);
});

it('validates using the root any object', () => {

const result = Joi.validate('abc');
expect(result.error).to.not.exist();
expect(result.value).to.equal('abc');
});

it('accepts no options', async () => {

await Joi.string().validate('test');
Expand Down Expand Up @@ -2581,7 +2574,8 @@ describe('Joi', () => {
expect(schema.describe()).to.equal({
type: 'any',
flags: {
description: 'defaulted'
description: 'defaulted',
presence: 'optional'
}
});
});
Expand Down Expand Up @@ -2929,24 +2923,24 @@ describe('Joi', () => {
});
});

describe('bind()', () => {
describe('types()', () => {

it('binds functions', () => {
it('returns type shortcut methods', () => {

expect(() => {

const string = Joi.string;
string();
}).to.throw('Must be invoked on a Joi instance.');

const { string } = Joi.bind();
expect(() => string()).to.not.throw();
const { string } = Joi.types();
expect(() => string.allow('x')).to.not.throw();

const { error } = string().validate(0);
const { error } = string.validate(0);
expect(error).to.be.an.error('"value" must be a string');
});

it('binds functions on an extended joi', () => {
it('returns extended shortcuts', () => {

const customJoi = Joi.extend({
base: Joi.string(),
Expand All @@ -2959,14 +2953,14 @@ describe('Joi', () => {
string();
}).to.throw('Must be invoked on a Joi instance.');

const { string, myType } = customJoi.bind();
expect(() => string()).to.not.throw();
expect(string().validate(0).error).to.be.an.error('"value" must be a string');
const { string, myType } = customJoi.types();
expect(() => string.allow('x')).to.not.throw();
expect(string.validate(0).error).to.be.an.error('"value" must be a string');

expect(() => myType()).to.not.throw();
expect(myType().validate(0).error).to.be.an.error('"value" must be a string');
expect(() => myType.allow('x')).to.not.throw();
expect(myType.validate(0).error).to.be.an.error('"value" must be a string');

expect(customJoi._binds.size).to.equal(Joi._binds.size + 1);
expect(customJoi._types.size).to.equal(Joi._types.size + 1);
});
});
});
Loading

0 comments on commit 59195cd

Please sign in to comment.