Skip to content

Commit

Permalink
Merge pull request #1562 from kanongil/symbol-support
Browse files Browse the repository at this point in the history
Add symbol() type
  • Loading branch information
Marsup committed Aug 13, 2018
2 parents 5eff33e + 070d3c9 commit da70a73
Show file tree
Hide file tree
Showing 4 changed files with 338 additions and 1 deletion.
10 changes: 9 additions & 1 deletion lib/index.js
Expand Up @@ -22,7 +22,8 @@ const internals = {
func: require('./types/func'),
number: require('./types/number'),
object: require('./types/object'),
string: require('./types/string')
string: require('./types/string'),
symbol: require('./types/symbol')
};

internals.callWithDefaults = function (schema, args) {
Expand Down Expand Up @@ -112,6 +113,13 @@ internals.root = function () {
return internals.callWithDefaults.call(this, internals.string, args);
};

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

Hoek.assert(args.length === 0, 'Joi.symbol() does not allow arguments.');

return internals.callWithDefaults.call(this, internals.symbol, args);
};

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

return Ref.create(...args);
Expand Down
4 changes: 4 additions & 0 deletions lib/language.js
Expand Up @@ -157,5 +157,9 @@ exports.errors = {
ref: 'references "{{ref}}" which is not a number',
ip: 'must be a valid ip address with a {{cidr}} CIDR',
ipVersion: 'must be a valid ip address of one of the following versions {{version}} with a {{cidr}} CIDR'
},
symbol: {
base: 'must be a symbol',
map: 'must be one of {{map}}'
}
};
93 changes: 93 additions & 0 deletions lib/types/symbol/index.js
@@ -0,0 +1,93 @@
'use strict';

// Load modules

const Util = require('util');

const Any = require('../any');
const Hoek = require('hoek');


// Declare internals

const internals = {};


internals.Map = class extends Map {

slice() {

return new internals.Map(this);
}

toString() {

return Util.inspect(this);
}
};


internals.Symbol = class extends Any {

constructor() {

super();
this._type = 'symbol';
this._inner.map = new internals.Map();
}

_base(value, state, options) {

if (options.convert) {
const lookup = this._inner.map.get(value);
if (lookup) {
value = lookup;
}

if (this._flags.allowOnly) {
return {
value,
errors: (typeof value === 'symbol') ? null : this.createError('symbol.map', { map: this._inner.map }, state, options)
};
}
}

return {
value,
errors: (typeof value === 'symbol') ? null : this.createError('symbol.base', null, state, options)
};
}

map(iterable) {

if (iterable && !iterable[Symbol.iterator] && typeof iterable === 'object') {
iterable = Object.entries(iterable);
}

Hoek.assert(iterable && iterable[Symbol.iterator], 'Iterable must be an iterable or object');
const obj = this.clone();

const symbols = [];
for (const entry of iterable) {
Hoek.assert(entry && entry[Symbol.iterator], 'Entry must be an iterable');
const [key, value] = entry;

Hoek.assert(typeof key !== 'object' && typeof key !== 'function' && typeof key !== 'symbol', 'Key must not be an object, function, or Symbol');
Hoek.assert(typeof value === 'symbol', 'Value must be a Symbol');
obj._inner.map.set(key, value);
symbols.push(value);
}

return obj.valid(...symbols);
}

describe() {

const description = super.describe();
description.map = new Map(this._inner.map);
return description;
}
};


module.exports = new internals.Symbol();
232 changes: 232 additions & 0 deletions test/types/symbol.js
@@ -0,0 +1,232 @@
'use strict';

// Load modules

const Lab = require('lab');
const Joi = require('../..');
const Helper = require('../helper');


// Declare internals

const internals = {};


// Test shortcuts

const { describe, it, expect } = exports.lab = Lab.script();


describe('symbol', () => {

it('cannot be called on its own', () => {

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

it('should throw an exception if arguments were passed.', () => {

expect(
() => Joi.symbol('invalid argument.')
).to.throw('Joi.symbol() does not allow arguments.');
});

describe('validate()', () => {

it('handles plain symbols', () => {

const symbols = [Symbol(1), Symbol(2)];
const rule = Joi.symbol();
Helper.validate(rule, [
[symbols[0], true, null, symbols[0]],
[symbols[1], true, null, symbols[1]],
[1, false, null, {
message: '"value" must be a symbol',
details: [{
message: '"value" must be a symbol',
path: [],
type: 'symbol.base',
context: { label: 'value', key: undefined }
}]
}]
]);
});

it('handles simple lookup', () => {

const symbols = [Symbol(1), Symbol(2)];
const otherSymbol = Symbol(1);
const rule = Joi.symbol().valid(symbols);
Helper.validate(rule, [
[symbols[0], true, null, symbols[0]],
[symbols[1], true, null, symbols[1]],
[otherSymbol, false, null, {
message: '"value" must be one of [Symbol(1), Symbol(2)]',
details: [{
message: '"value" must be one of [Symbol(1), Symbol(2)]',
path: [],
type: 'any.allowOnly',
context: { value: otherSymbol, label: 'value', valids: symbols, key: undefined }
}]
}]
]);
});

describe('map', () => {

it('converts keys to correct symbol', () => {

const symbols = [Symbol(1), Symbol(2)];
const otherSymbol = Symbol(1);
const map = new Map([[1, symbols[0]], ['two', symbols[1]]]);
const rule = Joi.symbol().map(map);
Helper.validate(rule, [
[1, true, null, symbols[0]],
[symbols[0], true, null, symbols[0]],
['1', false, null, {
message: `"value" must be one of Map { 1 => Symbol(1), 'two' => Symbol(2) }`,
details: [{
message: `"value" must be one of Map { 1 => Symbol(1), 'two' => Symbol(2) }`,
path: [],
type: 'symbol.map',
context: { label: 'value', key: undefined, map }
}]
}],
['two', true, null, symbols[1]],
[otherSymbol, false, null, {
message: '"value" must be one of [Symbol(1), Symbol(2)]',
details: [{
message: '"value" must be one of [Symbol(1), Symbol(2)]',
path: [],
type: 'any.allowOnly',
context: { value: otherSymbol, label: 'value', valids: symbols, key: undefined }
}]
}]
]);
});

it('converts keys from object', () => {

const symbols = [Symbol('one'), Symbol('two')];
const otherSymbol = Symbol('one');
const rule = Joi.symbol().map({ one: symbols[0], two: symbols[1] });
Helper.validate(rule, [
[symbols[0], true, null, symbols[0]],
['one', true, null, symbols[0]],
['two', true, null, symbols[1]],
[otherSymbol, false, null, {
message: '"value" must be one of [Symbol(one), Symbol(two)]',
details: [{
message: '"value" must be one of [Symbol(one), Symbol(two)]',
path: [],
type: 'any.allowOnly',
context: { value: otherSymbol, label: 'value', valids: symbols, key: undefined }
}]
}],
['toString', false, null, {
message: `"value" must be one of Map { 'one' => Symbol(one), 'two' => Symbol(two) }`,
details: [{
message: `"value" must be one of Map { 'one' => Symbol(one), 'two' => Symbol(two) }`,
path: [],
type: 'symbol.map',
context: { label: 'value', key: undefined, map: new Map([['one', symbols[0]], ['two', symbols[1]]]) }
}]
}]
]);
});

it('appends to existing map', () => {

const symbols = [Symbol(1), Symbol(2)];
const otherSymbol = Symbol(1);
const rule = Joi.symbol().map([[1, symbols[0]]]).map([[2, symbols[1]]]);
Helper.validate(rule, [
[1, true, null, symbols[0]],
[2, true, null, symbols[1]],
[otherSymbol, false, null, {
message: '"value" must be one of [Symbol(1), Symbol(2)]',
details: [{
message: '"value" must be one of [Symbol(1), Symbol(2)]',
path: [],
type: 'any.allowOnly',
context: { value: otherSymbol, label: 'value', valids: symbols, key: undefined }
}]
}]
]);
});

it('throws on bad input', () => {

expect(
() => Joi.symbol().map()
).to.throw('Iterable must be an iterable or object');

expect(
() => Joi.symbol().map(Symbol())
).to.throw('Iterable must be an iterable or object');

expect(
() => Joi.symbol().map([undefined])
).to.throw('Entry must be an iterable');

expect(
() => Joi.symbol().map([123])
).to.throw('Entry must be an iterable');

expect(
() => Joi.symbol().map([[123, 456]])
).to.throw('Value must be a Symbol');

expect(
() => Joi.symbol().map([[{}, Symbol()]])
).to.throw('Key must not be an object, function, or Symbol');

expect(
() => Joi.symbol().map([[() => {}, Symbol()]])
).to.throw('Key must not be an object, function, or Symbol');

expect(
() => Joi.symbol().map([[Symbol(), Symbol()]])
).to.throw('Key must not be an object, function, or Symbol');
});
});

it('handles plain symbols when convert is disabled', async () => {

const symbols = [Symbol(1), Symbol(2)];
const schema = Joi.symbol().map([[1, symbols[0]], ['two', symbols[1]]]).options({ convert: false });
const result = await schema.validate(symbols[1]);
expect(result).to.equal(symbols[1]);
});

it('errors on mapped input and convert is disabled', async () => {

const symbols = [Symbol(1), Symbol(2)];
const schema = Joi.symbol().map([[1, symbols[0]], ['two', symbols[1]]]).options({ convert: false });
const err = await expect(schema.validate(1)).to.reject();
expect(err).to.be.an.error('"value" must be a symbol');
expect(err.details).to.equal([{
message: '"value" must be a symbol',
path: [],
type: 'symbol.base',
context: { label: 'value', key: undefined }
}]);
});

it('should describe value map', () => {

const symbols = [Symbol(1), Symbol(2)];
const map = new Map([[1, symbols[0]], ['two', symbols[1]]]);
const schema = Joi.symbol().map(map).describe();
expect(schema).to.equal({
type: 'symbol',
flags: {
allowOnly: true
},
map,
valids: symbols
});
});
});
});

0 comments on commit da70a73

Please sign in to comment.