Skip to content
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 support for truthy/falsy boolean values #998

Merged
merged 8 commits into from Oct 22, 2016
26 changes: 24 additions & 2 deletions API.md
Expand Up @@ -52,6 +52,8 @@
- [`array.length(limit)`](#arraylengthlimit)
- [`array.unique([comparator])`](#arrayuniquecomparator)
- [`boolean`](#boolean)
- [`boolean.truthy(value)`](#booleantruthyvalue)
- [`boolean.falsy(value)`](#booleanfalsyvalue)
- [`binary`](#binary)
- [`binary.encoding(encoding)`](#binaryencodingencoding)
- [`binary.min(limit)`](#binaryminlimit)
Expand Down Expand Up @@ -799,13 +801,33 @@ const schema = Joi.array().unique((a, b) => a.property === b.property);

### `boolean`

Generates a schema object that matches a boolean data type (as well as the strings 'true', 'false', 'yes', 'no', 'on', 'off', 1, 0, '1', or '0'). Can also be called via `bool()`.
Generates a schema object that matches a boolean data type. Can also be called via `bool()`.

Supports the same methods of the [`any()`](#any) type.

```js
const boolean = Joi.boolean();
boolean.validate(true, (err, value) => { });
boolean.validate(true, (err, value) => { }); // Valid

boolean.validate(1, (err, value) => { }); // Invalid
```

#### `boolean.truthy(value)`

Allows for additional values to be considered valid booleans by converting them to `true` during validation. Accepts a value or an array of values.

```js
const boolean = Joi.boolean().truthy('Y');
boolean.validate('Y', (err, value) => { }); // Valid
```

#### `boolean.falsy(value)`

Allows for additional values to be considered valid booleans by converting them to `false` during validation. Accepts a value or an array of values.

```js
const boolean = Joi.boolean().falsy('N');
boolean.validate('N', (err, value) => { }); // Valid
```

### `binary`
Expand Down
95 changes: 3 additions & 92 deletions lib/any.js
Expand Up @@ -11,7 +11,9 @@ let Cast = null;

// Declare internals

const internals = {};
const internals = {
Set: require('./set')
};


internals.defaults = {
Expand Down Expand Up @@ -784,97 +786,6 @@ internals._try = function (fn, arg) {
};
};


internals.Set = class {

constructor() {

this._set = [];
}

add(value, refs) {

if (!Ref.isRef(value) && this.has(value, null, null, false)) {

return;
}

if (refs !== undefined) { // If it's a merge, we don't have any refs
Ref.push(refs, value);
}

this._set.push(value);
}

merge(add, remove) {

for (let i = 0; i < add._set.length; ++i) {
this.add(add._set[i]);
}

for (let i = 0; i < remove._set.length; ++i) {
this.remove(remove._set[i]);
}
}

remove(value) {

this._set = this._set.filter((item) => value !== item);
}

has(value, state, options, insensitive) {

for (let i = 0; i < this._set.length; ++i) {
let items = this._set[i];

if (state && Ref.isRef(items)) { // Only resolve references if there is a state, otherwise it's a merge
items = items(state.reference || state.parent, options);
}

if (!Array.isArray(items)) {
items = [items];
}

for (let j = 0; j < items.length; ++j) {
const item = items[j];
if (typeof value !== typeof item) {
continue;
}

if (value === item ||
(value instanceof Date && item instanceof Date && value.getTime() === item.getTime()) ||
(insensitive && typeof value === 'string' && value.toLowerCase() === item.toLowerCase()) ||
(Buffer.isBuffer(value) && Buffer.isBuffer(item) && value.length === item.length && value.toString('binary') === item.toString('binary'))) {

return true;
}
}
}

return false;
}

values(options) {

if (options && options.stripUndefined) {
const values = [];

for (let i = 0; i < this._set.length; ++i) {
const item = this._set[i];
if (item !== undefined) {
values.push(item);
}
}

return values;
}

return this._set.slice();
}

};


internals.concatSettings = function (target, source) {

// Used to avoid cloning context
Expand Down
58 changes: 46 additions & 12 deletions lib/boolean.js
Expand Up @@ -3,18 +3,23 @@
// Load modules

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


// Declare internals

const internals = {};
const internals = {
Set: require('./set')
};


internals.Boolean = class extends Any {
constructor() {

super();
this._type = 'boolean';
this._inner._truthySet = new internals.Set();
this._inner._falsySet = new internals.Set();
}

_base(value, state, options) {
Expand All @@ -23,23 +28,52 @@ internals.Boolean = class extends Any {
value
};

if (typeof value === 'string' &&
options.convert) {
result.value = (this._inner._truthySet.has(value) ? true
: (this._inner._falsySet.has(value) ? false : value));

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

truthy() {

const obj = this.clone();
const values = Hoek.flatten(Array.prototype.slice.call(arguments));
for (let i = 0; i < values.length; ++i) {
const value = values[i];

Hoek.assert(value !== undefined, 'Cannot call truthy/falsy with undefined');
obj._inner._truthySet.add(value);
}
return obj;
}

falsy() {

const obj = this.clone();
const values = Hoek.flatten(Array.prototype.slice.call(arguments));
for (let i = 0; i < values.length; ++i) {
const value = values[i];

const lower = value.toLowerCase();
result.value = (lower === 'true' || lower === 'yes' || lower === 'on' || lower === '1' ? true
: (lower === 'false' || lower === 'no' || lower === 'off' || lower === '0' ? false : value));
Hoek.assert(value !== undefined, 'Cannot call truthy/falsy with undefined');
obj._inner._falsySet.add(value);
}
return obj;
}

describe() {

if (typeof value === 'number' &&
options.convert) {
const description = Any.prototype.describe.call(this);

result.value = (value === 1 ? true
: (value === 0 ? false : value));
if (this._inner._truthySet.values().length) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

values() is already a copy, so I guess description.truthyValues = this._inner._truthySet.values() is enough.

description.truthyValues = this._inner._truthySet.values();
}

result.errors = (typeof result.value === 'boolean') ? null : this.createError('boolean.base', null, state, options);
return result;
if (this._inner._falsySet.values().length) {
description.falsyValues = this._inner._falsySet.values();
}

return description;
}
};

Expand Down
107 changes: 107 additions & 0 deletions lib/set.js
@@ -0,0 +1,107 @@
'use strict';

const Ref = require('./ref');

module.exports = class Set {

constructor() {

this._set = [];
}

add(value, refs) {

if (!Ref.isRef(value) && this.has(value, null, null, false)) {

return;
}

if (refs !== undefined) { // If it's a merge, we don't have any refs
Ref.push(refs, value);
}

this._set.push(value);
}

merge(add, remove) {

for (let i = 0; i < add._set.length; ++i) {
this.add(add._set[i]);
}

for (let i = 0; i < remove._set.length; ++i) {
this.remove(remove._set[i]);
}
}

remove(value) {

this._set = this._set.filter((item) => value !== item);
}

has(value, state, options, insensitive) {

for (let i = 0; i < this._set.length; ++i) {
let items = this._set[i];

if (state && Ref.isRef(items)) { // Only resolve references if there is a state, otherwise it's a merge
items = items(state.reference || state.parent, options);
}

if (!Array.isArray(items)) {
items = [items];
}

for (let j = 0; j < items.length; ++j) {
const item = items[j];
if (typeof value !== typeof item) {
continue;
}

if (value === item ||
(value instanceof Date && item instanceof Date && value.getTime() === item.getTime()) ||
(insensitive && typeof value === 'string' && value.toLowerCase() === item.toLowerCase()) ||
(Buffer.isBuffer(value) && Buffer.isBuffer(item) && value.length === item.length && value.toString('binary') === item.toString('binary'))) {

return true;
}
}
}

return false;
}

values(options) {

if (options && options.stripUndefined) {
const values = [];

for (let i = 0; i < this._set.length; ++i) {
const item = this._set[i];
if (item !== undefined) {
values.push(item);
}
}

return values;
}

return this._set.slice();
}

slice() {

const newSet = new Set();
newSet._set = this._set.slice();

return newSet;
}

concat(source) {

const newSet = new Set();
newSet._set = this._set.concat(source._set);

return newSet;
}
};