Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1562 from kanongil/symbol-support
Add symbol() type
- Loading branch information
Showing
4 changed files
with
338 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
}); | ||
}); | ||
}); | ||
}); |