-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add symbol() type #1562
Add symbol() type #1562
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
'use strict'; | ||
|
||
// Load modules | ||
|
||
const Any = require('../any'); | ||
const Hoek = require('hoek'); | ||
|
||
|
||
// Declare internals | ||
|
||
const internals = {}; | ||
|
||
|
||
internals.Map = class extends Map { | ||
|
||
slice() { | ||
|
||
return new internals.Map(this); | ||
} | ||
|
||
toString() { | ||
|
||
const entries = [...this].map(([key, symbol]) => { | ||
|
||
key = typeof key === 'symbol' ? key.toString() : JSON.stringify(key); | ||
return `${key} => ${symbol.toString()}`; | ||
}); | ||
|
||
return `Map { ${entries.join(', ')} }`; | ||
} | ||
}; | ||
|
||
|
||
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', 'Key must be a simple type'); | ||
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 = Any.prototype.describe.call(this); | ||
description.map = new Map(this._inner.map); | ||
return description; | ||
} | ||
}; | ||
|
||
|
||
module.exports = new internals.Symbol(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
'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), Symbol(3)]; | ||
const otherSymbol = Symbol(1); | ||
const map = new Map([[1, symbols[0]], ['two', symbols[1]], [symbols[0], symbols[2]]]); | ||
const rule = Joi.symbol().map(map); | ||
Helper.validate(rule, [ | ||
[1, true, null, symbols[0]], | ||
[symbols[0], true, null, symbols[0]], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why wouldn't it become There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, yeah I didn't write that test as I intended to. However, it might makes sense to work as it does, since the implementation adds all map values to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then I would deny symbols as keys, that doesn't make sense to me. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, normally the mapping would work – it just doesn't in the case, since the symbol is also used as a value. Anyway, I don't have a problem with removing it. We can always re-add later if it somehow makes sense. |
||
['1', false, null, { | ||
message: '"value" must be one of Map { 1 => Symbol(1), "two" => Symbol(2), Symbol(1) => Symbol(3) }', | ||
details: [{ | ||
message: '"value" must be one of Map { 1 => Symbol(1), "two" => Symbol(2), Symbol(1) => Symbol(3) }', | ||
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), Symbol(3)]', | ||
details: [{ | ||
message: '"value" must be one of [Symbol(1), Symbol(2), Symbol(3)]', | ||
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 be a simple type'); | ||
}); | ||
}); | ||
|
||
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 | ||
}); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That PR made me realize we could do this in ES6 now, see fd28a30.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I thought as well, but chose to copy the existing style.