Skip to content

Commit

Permalink
Refactor lazy. For #1867
Browse files Browse the repository at this point in the history
  • Loading branch information
hueniverse committed Jul 2, 2019
1 parent a54c900 commit 3ba54e3
Show file tree
Hide file tree
Showing 11 changed files with 96 additions and 115 deletions.
7 changes: 1 addition & 6 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,6 @@
- [`function.class`](#functionclass)
- [`function.maxArity`](#functionmaxarity)
- [`function.minArity`](#functionminarity)
- [`lazy.base`](#lazybase)
- [`lazy.schema`](#lazyschema)
- [`number.base`](#numberbase)
- [`number.greater`](#numbergreater)
Expand Down Expand Up @@ -2003,7 +2002,7 @@ const Person = Joi.object({
});
```

Possible validation errors: [`lazy.base`](#lazybase), [`lazy.schema`](#lazyschema)
Possible validation errors: [`lazy.schema`](#lazyschema)

### `number` - inherits from `Any`

Expand Down Expand Up @@ -3661,10 +3660,6 @@ Additional local context properties:
}
```

#### `lazy.base`

The lazy function is not set.

#### `lazy.schema`

The lazy function didn't return a **joi** schema.
Expand Down
2 changes: 1 addition & 1 deletion lib/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ exports.isResolvable = function (obj) {

exports.isSchema = function (schema, options = {}) {

const any = schema[exports.symbols.any];
const any = schema && schema[exports.symbols.any];
if (!any) {
return false;
}
Expand Down
2 changes: 1 addition & 1 deletion lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ exports.ValidationError = class extends Error {
for (let j = 0; ; ++j) {
const seg = path[j];

if (node instanceof Any) {
if (Common.isSchema(node)) {
node = node.clone(); // joi schemas are not cloned by hoek, we have to take this extra step
}

Expand Down
5 changes: 2 additions & 3 deletions lib/extend.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

const Hoek = require('@hapi/hoek');

const Any = require('./types/any');
const Cast = require('./cast');
const Common = require('./common');
const Errors = require('./errors');
Expand Down Expand Up @@ -134,7 +133,7 @@ internals.generator = function (joi, root, extension) {

if (extension.rules) {
for (const rule of extension.rules) {
const ruleArgs = rule.params ? (rule.params instanceof Any ? rule.params._inners.children.map((k) => k.key) : Object.keys(rule.params)) : [];
const ruleArgs = rule.params ? (Common.isSchema(rule.params) ? rule.params._inners.children.map((k) => k.key) : Object.keys(rule.params)) : [];
const validateArgs = rule.params ? Cast.schema(root, rule.params) : null;

type.prototype[rule.name] = function (...rArgs) { // eslint-disable-line no-loop-func
Expand Down Expand Up @@ -175,7 +174,7 @@ internals.generator = function (joi, root, extension) {
if (rule.setup) {
const newSchema = rule.setup.call(schema, arg);
if (newSchema !== undefined) {
Hoek.assert(newSchema instanceof Any, `Setup of extension Joi.${this._type}().${rule.name}() must return undefined or a Joi object`);
Hoek.assert(Common.isSchema(newSchema), `Setup of extension Joi.${this._type}().${rule.name}() must return undefined or a Joi object`);
schema = newSchema;
}

Expand Down
2 changes: 1 addition & 1 deletion lib/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ exports.describe = function (schema) {
};
}
else {
description.flags[flag] = value;
description.flags[flag] = { value };
}

break;
Expand Down
1 change: 0 additions & 1 deletion lib/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ exports.errors = {
'function.maxArity': '"{{#label}}" must have an arity lesser or equal to {{#n}}',
'function.minArity': '"{{#label}}" must have an arity greater or equal to {{#n}}',

'lazy.base': 'schema error: lazy schema must be set',
'lazy.schema': 'schema error: lazy schema function must return a schema',

'object.base': '"{{#label}}" must be an object',
Expand Down
1 change: 0 additions & 1 deletion lib/types/any.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ module.exports = internals.Any = class {
func: false,
insensitive: false,
label: undefined,
lazy: undefined,
once: true,
presence: 'optional', // optional, required, forbidden, ignore
single: undefined,
Expand Down
67 changes: 36 additions & 31 deletions lib/types/lazy.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,31 @@ internals.Lazy = class extends Any {

super('lazy');

this._flags.once = true;
this._immutables.lazy = {}; // Must be set to a fresh object when `schema` is modified
}

_init(fn, options) {
_init(fn, options = {}) {

return this.set(fn, options);
Common.assertOptions(options, ['once']);
Hoek.assert(typeof fn === 'function', 'You must provide a function as first argument');
Hoek.assert(options.once === undefined || typeof options.once === 'boolean', 'Option "once" must be a boolean');

const obj = this.clone();

obj._immutables.lazy = {
schema: fn,
once: options.once === undefined ? true : options.once,
generated: null
};

return obj;
}

_base(value, state, prefs) {

let schema = this._immutables.lazy.generated;
if (!schema) {
const lazy = this._immutables.lazy.schema;
if (!lazy) {
return { value, errors: this.createError('lazy.base', value, null, state, prefs) };
}

schema = lazy();
if (!(schema instanceof Any)) {
return { value, errors: this.createError('lazy.schema', value, { schema }, state, prefs) };
}

if (this._flags.once) {
this._immutables.lazy.generated = schema;
}
const schema = this._generate();
if (!Common.isSchema(schema)) {
return { value, errors: this.createError('lazy.schema', value, { schema }, state, prefs) };
}

return schema._validate(value, state, prefs);
Expand All @@ -52,30 +51,36 @@ internals.Lazy = class extends Any {

const description = super.describe();
description.schema = this._immutables.lazy.schema;
description.once = this._immutables.lazy.once;
return description;
}

// Rules
// Helpers

set(fn, options = {}) {
concat(source) {

Common.assertOptions(options, ['once']);
Hoek.assert(typeof fn === 'function', 'You must provide a function as first argument');
Hoek.assert(options.once === undefined || typeof options.once === 'boolean', 'Option "once" must be a boolean');
Hoek.assert(Common.isSchema(source), 'Invalid schema object');
Hoek.assert(source._type === 'any', 'Cannot merge type lazy with another type:', source._type);

const obj = this._flag('once', options.once === undefined ? true : options.once);
obj._immutables.lazy = { schema: fn, generated: null };
return obj;
return super.concat(source);
}

// Helpers
// Internals

concat(source) {
_generate() {

Hoek.assert(source instanceof Any, 'Invalid schema object');
Hoek.assert(source._type === 'any', 'Cannot merge type lazy with another type:', source._type);
if (this._immutables.lazy.generated) {
return this._immutables.lazy.generated;
}

return super.concat(source);
const schema = this._immutables.lazy.schema();
if (Common.isSchema(schema) &&
this._immutables.lazy.once) {

this._immutables.lazy.generated = schema;
}

return schema;
}
};

Expand Down
52 changes: 27 additions & 25 deletions lib/types/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ internals.Object = class extends Any {
keys(schema) {

Hoek.assert(schema === null || schema === undefined || typeof schema === 'object', 'Object schema must be a valid object');
Hoek.assert(!schema || !(schema instanceof Any), 'Object schema cannot be a joi schema');
Hoek.assert(!schema || !Common.isSchema(schema), 'Object schema cannot be a joi schema');
Hoek.assert(!this._inRuleset(), 'Cannot set key rules inside a ruleset');

const obj = this.clone();
Expand Down Expand Up @@ -499,7 +499,7 @@ internals.Object = class extends Any {
pattern(pattern, schema, options = {}) {

const isRegExp = pattern instanceof RegExp;
Hoek.assert(isRegExp || pattern instanceof Any, 'pattern must be a regex or schema');
Hoek.assert(isRegExp || Common.isSchema(pattern), 'pattern must be a regex or schema');
Hoek.assert(schema !== undefined, 'Invalid rule');
Common.assertOptions(options, ['exclusive', 'matches']);

Expand Down Expand Up @@ -564,6 +564,30 @@ internals.Object = class extends Any {
return this._rule('schema', { args: { type } });
}

unknown(allow) {

return this._flag('allowUnknown', allow !== false);
}

with(key, peers, options = {}) {

return this._dependency('with', key, peers, options);
}

without(key, peers, options = {}) {

return this._dependency('without', key, peers, options);
}

xor(...peers /*, [options] */) {

Common.verifyFlat(peers, 'xor');

return this._dependency('xor', null, peers);
}

// Helpers

tailor(targets) {

let obj = super.tailor(targets);
Expand Down Expand Up @@ -602,28 +626,6 @@ internals.Object = class extends Any {
return obj._rebuild();
}

unknown(allow) {

return this._flag('allowUnknown', allow !== false);
}

with(key, peers, options = {}) {

return this._dependency('with', key, peers, options);
}

without(key, peers, options = {}) {

return this._dependency('without', key, peers, options);
}

xor(...peers /*, [options] */) {

Common.verifyFlat(peers, 'xor');

return this._dependency('xor', null, peers);
}

// Internals

_dependency(type, key, peers, options) {
Expand Down Expand Up @@ -869,7 +871,7 @@ Common.extend(internals.Object, 'rules', {

schema: function (value, helpers, { type }) {

if (value instanceof Any &&
if (Common.isSchema(value) &&
(type === 'any' || value._type === type)) {

return value;
Expand Down
4 changes: 2 additions & 2 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2527,8 +2527,8 @@ describe('Joi', () => {
max: {
type: 'string',
flags: {
default: 0,
failover: 1
default: { value: 0 },
failover: { value: 1 }
},
invalids: [''],
rules: [{ name: 'max', arg: { limit: 3 } }]
Expand Down
68 changes: 25 additions & 43 deletions test/types/lazy.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const Lab = require('@hapi/lab');
const Joi = require('../..');

const Helper = require('../helper');
const Lazy = require('../../lib/types/lazy');


const internals = {};
Expand All @@ -17,49 +16,34 @@ const { expect } = Code;

describe('lazy', () => {

describe('set()', () => {
it('should require a function', () => {

it('should require a function', () => {

expect(() => Joi.lazy()).to.throw('You must provide a function as first argument');
expect(() => Joi.lazy(true)).to.throw('You must provide a function as first argument');
});

it('validates a schema is set', async () => {

const schema = Lazy;
const err = await expect(schema.validate('bar')).to.reject('schema error: lazy schema must be set');
expect(err.details).to.equal([{
message: 'schema error: lazy schema must be set',
path: [],
type: 'lazy.base',
context: { label: 'value', value: 'bar' }
}]);
});
expect(() => Joi.lazy()).to.throw('You must provide a function as first argument');
expect(() => Joi.lazy(true)).to.throw('You must provide a function as first argument');
});

it('validates a schema is returned', async () => {

const fn = () => true;
const schema = Joi.lazy(fn);
const err = await expect(schema.validate('bar')).to.reject('schema error: lazy schema function must return a schema');
expect(err.details).to.equal([{
message: 'schema error: lazy schema function must return a schema',
path: [],
type: 'lazy.schema',
context: { label: 'value', schema: true, value: 'bar' }
}]);
});
it('validates a schema is returned', async () => {

const fn = () => true;
const schema = Joi.lazy(fn);
const err = await expect(schema.validate('bar')).to.reject('schema error: lazy schema function must return a schema');
expect(err.details).to.equal([{
message: 'schema error: lazy schema function must return a schema',
path: [],
type: 'lazy.schema',
context: { label: 'value', schema: true, value: 'bar' }
}]);
});

it('checks options', () => {
it('checks options', () => {

expect(() => Joi.lazy(() => { }, false)).to.throw('Options must be an object');
expect(() => Joi.lazy(() => { }, true)).to.throw('Options must be an object');
expect(() => Joi.lazy(() => { }, [])).to.throw('Options must be an object');
expect(() => Joi.lazy(() => { }, { oce: true })).to.throw('Options contain unknown keys: oce');
expect(() => Joi.lazy(() => { }, { once: 'foo' })).to.throw('Option "once" must be a boolean');
expect(() => Joi.lazy(() => { }, {})).to.not.throw();
expect(() => Joi.lazy(() => { }, { once: true })).to.not.throw();
});
expect(() => Joi.lazy(() => { }, false)).to.throw('Options must be an object');
expect(() => Joi.lazy(() => { }, true)).to.throw('Options must be an object');
expect(() => Joi.lazy(() => { }, [])).to.throw('Options must be an object');
expect(() => Joi.lazy(() => { }, { oce: true })).to.throw('Options contain unknown keys: oce');
expect(() => Joi.lazy(() => { }, { once: 'foo' })).to.throw('Option "once" must be a boolean');
expect(() => Joi.lazy(() => { }, {})).to.not.throw();
expect(() => Joi.lazy(() => { }, { once: true })).to.not.throw();
});

describe('validate()', () => {
Expand Down Expand Up @@ -184,9 +168,7 @@ describe('lazy', () => {
type: 'lazy',
description: 'person',
schema: lazy,
flags: {
once: true
}
once: true
}
]
},
Expand Down

0 comments on commit 3ba54e3

Please sign in to comment.