Comparing version 0.1.6 to 0.2.0
@@ -5,32 +5,120 @@ /** | ||
var util = require('util'); | ||
var _ = require('lodash'); | ||
var rttc = require('./rttc'); | ||
var infer = require('./infer'); | ||
var types = require('./types'); | ||
module.exports = function coerce (expected, actual) { | ||
// Transform `expected` into rttc-compatible schema | ||
// e.g. { | ||
// foo: { type: 'string', required: false }, | ||
// bar: { type: { baz: 'number' }, required: false } | ||
// } | ||
var rttcSchema = { | ||
x: { | ||
type: infer(expected) | ||
/** | ||
* Coerce value to type schema | ||
* (very forgiving) | ||
* | ||
* @param {~Schema} expected type schema | ||
* @param {*} actual "mystery meat" | ||
* @return {<expected>} | ||
*/ | ||
module.exports = function coerce (expected, actual){ | ||
// Avoid damaging the provided parameters. | ||
expected = _.cloneDeep(expected); | ||
actual = _.cloneDeep(actual); | ||
var errors = []; | ||
var result = _coerceRecursive(expected, actual, errors); | ||
if (errors.length) { | ||
throw (function (){ | ||
var err = new Error(util.format('%d error(s) coercing value:\n', errors.length, errors)); | ||
err.code = errors[0].code; | ||
err.errors = errors; | ||
return err; | ||
})(); | ||
} | ||
return result; | ||
}; | ||
function _coerceRecursive (expected, actual, errors){ | ||
// Look up expected type from `types` object using `expected`. | ||
var expectedType; | ||
var isExpectingArray; | ||
var isExpectingDictionary; | ||
// Arrays | ||
if (_.isArray(expected)) { | ||
expectedType = types.arr; | ||
isExpectingArray = true; | ||
} | ||
// Dictionaries | ||
else if (_.isObject(expected)) { | ||
expectedType = types.obj; | ||
isExpectingDictionary = true; | ||
} | ||
// Primitives | ||
else { | ||
expectedType = types[expected]; | ||
// If this refers to an unknown type, default | ||
// to a string's base type and remember the error. | ||
if (_.isUndefined(expectedType)) { | ||
errors.push((function (){ | ||
var err = new Error('Unknown type: '+expected); | ||
err.code = 'E_UNKNOWN_TYPE'; | ||
return err; | ||
})()); | ||
return types.string.getBase(); | ||
} | ||
}; | ||
// Transform `actual` into rttc-compatible value set | ||
// e.g. { | ||
// foo: 'asdga' | ||
// bar: { baz: 32 } | ||
// } | ||
var rttcValueSet = { | ||
x: actual | ||
}; | ||
} | ||
return rttc(rttcSchema, rttcValueSet, { | ||
coerce: true | ||
}).x; | ||
}; | ||
// Default the coercedValue to the actual value. | ||
var coercedValue = actual; | ||
// If the actual value is undefined, fill in with the | ||
// appropriate base type. | ||
if(types.undefined.is(actual)) { | ||
coercedValue = expectedType.getBase(); | ||
} | ||
// Check `actual` value against expectedType | ||
if (!expectedType.is(actual)){ | ||
// Invalid expected type. Try to coerce: | ||
try { | ||
coercedValue = expectedType.to(actual); | ||
} | ||
catch (e) { | ||
// If that doesn't work, use the base type: | ||
coercedValue = expectedType.getBase(); | ||
// Saving this error here in case we need it: | ||
// (but turning it off for now because this function | ||
// is very forgiving and kind) | ||
// errors.push((function (){ | ||
// var err = new Error(util.format( | ||
// 'An invalid value was specified: \n' + util.inspect(actual, false, null) + '\n\n' + | ||
// 'This doesn\'t match the specified type: \n' + util.inspect(expected, false, null) | ||
// )); | ||
// err.code = 'E_INVALID_TYPE'; | ||
// return err; | ||
// })()); | ||
} | ||
} | ||
// Build partial result | ||
// (taking recursive step if necessary) | ||
if (isExpectingArray) { | ||
var arrayItemTpl = expected[0]; | ||
return [_coerceRecursive(arrayItemTpl, coercedValue[0], errors)]; | ||
} | ||
if (isExpectingDictionary) { | ||
return _.reduce(expected, function (memo, expectedVal, expectedKey) { | ||
memo[expectedKey] = _coerceRecursive(expected[expectedKey], coercedValue[expectedKey], errors); | ||
return memo; | ||
}, {}); | ||
} | ||
return coercedValue; | ||
} |
@@ -6,3 +6,3 @@ /** | ||
var _ = require('lodash'); | ||
var types = require('./types'); | ||
var types = require('./helpers/types'); | ||
@@ -9,0 +9,0 @@ |
138
lib/rttc.js
@@ -8,90 +8,9 @@ /** | ||
var infer = require('./infer'); | ||
var types = require('./types'); | ||
var types = require('./helpers/types'); | ||
var validateAndOrCoerceObject = require('./helpers/validate-and-or-coerce-object'); | ||
var validatePrimitive = require('./helpers/validate-primitive'); | ||
var coercePrimitive = require('./helpers/coerce-primitive'); | ||
/** | ||
* Run-time type checking. Given a set of typed inputs, ensure the run-time configured | ||
* inputs are valid. | ||
* ________________________________________________________________________________ | ||
* @param {String} type the expected type | ||
* | ||
* @param {*} val the "mystery meat" | ||
* | ||
* @param {Object} flags an object of boolean flags | ||
* @property {Boolean} coerce | ||
* @property {Boolean} baseType | ||
* ________________________________________________________________________________ | ||
* @returns {*} If everything worked | ||
* the value that what was formerly "mystery meat", now coerced to `type`. | ||
* @throws {E_UNDEFINED_VAL} If there were validation errors | ||
*/ | ||
function coercePrimitive (type, val, flags) { | ||
var coerceFlag = flags && flags.coerce || false; | ||
var baseTypeFlag = flags && flags.baseType || false; | ||
// Map types that are shorthand | ||
var to = type; | ||
if(type === 'string') to = 'str'; | ||
if(type === 'boolean') to = 'bool'; | ||
// WARNING: Will throw if the value can't be coerced | ||
if(!coerceFlag) return val; | ||
try { | ||
// If val === undefined lets throw and either error or use the base type | ||
if(val === undefined) { | ||
var err = new Error(); | ||
err.code = 'E_UNDEFINED_VAL'; | ||
err.message = 'Undefined value'; | ||
throw err; | ||
} | ||
val = types[to].to(val); | ||
} | ||
catch (e) { | ||
// If we want the base type for this input catch it here | ||
if(!baseTypeFlag) throw e; | ||
val = types[to].base && types[to].base(); | ||
} | ||
return val; | ||
} | ||
/** | ||
* Given a type and primitive value, check that it matches. | ||
* ________________________________________________________________________________ | ||
* @param {String} type the expected type | ||
* | ||
* @param {*} val the "mystery meat" | ||
* ________________________________________________________________________________ | ||
* @return {Boolean} is this a match? | ||
*/ | ||
function validatePrimitive (type, val) { | ||
// Check for string | ||
if(type === 'string') { | ||
return types.str.is(val); | ||
} | ||
// Check for number | ||
if(type === 'number') { | ||
return types.number.is(val); | ||
} | ||
// Check for boolean | ||
if(type === 'boolean') { | ||
return types.bool.is(val); | ||
} | ||
return false; | ||
} | ||
/** | ||
* Given a definition and a values object, ensure our types match up. | ||
@@ -112,3 +31,3 @@ * ________________________________________________________________________________ | ||
function validateInputSchema(def, val, options) { | ||
function validateInputs(def, val, options) { | ||
@@ -127,39 +46,4 @@ options = options || {}; | ||
var parseObject = function(input, value) { | ||
_.each(_.keys(input), function(key) { | ||
var _input = input[key]; | ||
var _value = value[key]; | ||
// If the input is an object continue recursively parsing it | ||
if(types.obj.is(_input)) { | ||
parseObject(_input, _value); | ||
return; | ||
} | ||
_value = coercePrimitive(_input, _value, { coerce: coerce, baseType: baseType }); | ||
var valid = validatePrimitive(_input, _value); | ||
if(!valid) { | ||
var err = new Error(); | ||
err.code = 'E_INVALID_TYPE'; | ||
err.message = 'Invalid input value '+ value; | ||
throw new Error(err); | ||
} | ||
value[key] = _value; | ||
}); | ||
// Find the difference in the input and the value and remove any keys that | ||
// exist on the value but not on the input definition. | ||
var inputKeys = _.keys(input); | ||
var valueKeys = _.keys(value); | ||
var invalidKeys = _.difference(valueKeys, inputKeys); | ||
_.each(invalidKeys, function(key) { | ||
delete value[key]; | ||
}); | ||
return value; | ||
}; | ||
// If we don't have an object then just check the tuple | ||
// If we don't have an object as our definition then just check the tuple | ||
// If the input type isn't an object or array we can just do a simple type check | ||
@@ -249,3 +133,3 @@ if(!_.isPlainObject(def)) { | ||
try { | ||
item = parseObject(input.type[0], item); | ||
item = validateAndOrCoerceObject(input.type[0], item, { coerce: coerce, baseType: baseType }); | ||
} | ||
@@ -255,4 +139,4 @@ catch (err) { | ||
err.message = util.format( | ||
'An invalid value was specified. The value ' + item + ' was used \n' + | ||
'and doesn\'t match the specified type: ' + input.type[0] | ||
'An invalid value was specified: \n' + util.inspect(item, false, null) + '\n\n' + | ||
'This doesn\'t match the specified type: \n' + util.inspect(input.type[0], false, null) | ||
); | ||
@@ -296,3 +180,3 @@ | ||
try { | ||
value = parseObject(input.type, value); | ||
value = validateAndOrCoerceObject(input.type, value, { coerce: coerce, baseType: baseType }); | ||
} | ||
@@ -353,2 +237,2 @@ catch (e) { | ||
module.exports = validateInputSchema; | ||
module.exports = validateInputs; |
196
lib/types.js
@@ -1,194 +0,2 @@ | ||
/** | ||
* Module dependencies | ||
*/ | ||
var _ = require('lodash'); | ||
/** | ||
* Basic type definitions. | ||
* | ||
* Roughly based on https://github.com/bishopZ/Typecast.js | ||
* ________________________________________________________________________________ | ||
* @type {Object} | ||
*/ | ||
var type = { | ||
// NaN | ||
nan: { | ||
is: _.isNaN, | ||
to: function () { return NaN; } | ||
}, | ||
// Null | ||
'null': { | ||
is: _.isNull, | ||
to: function () { return null; } | ||
}, | ||
// Undefined | ||
'undefined': { | ||
is: _.isUndefined, | ||
to: function () { return undefined; } | ||
}, | ||
// Boolean | ||
bool: { | ||
is: _.isBoolean, | ||
to: function(v) { | ||
if(_.isBoolean(v)) return v; | ||
if(v === 'true') return true; | ||
if(v === 'false') return false; | ||
if(v === 1) return true; | ||
if(v === 0) return false; | ||
if(v === '1') return true; | ||
if(v === '0') return false; | ||
throw new Error('E_runtimeInputTypeCoercionError'); | ||
}, | ||
base: false | ||
}, | ||
// Defined | ||
defined: { | ||
is: function(v) { | ||
return !( type.nan.is(v) || type.undefined.is(v) || type.null.is(v) ); | ||
}, | ||
to: function(v) { | ||
return (type.defined.is(v)) ? v : true; | ||
} | ||
}, | ||
// Integer | ||
'int': { | ||
is: function(v) { return (v == v + 0 && v == ~~v); }, | ||
to: function(v) { | ||
var value = parseInt(v, 10); | ||
if (!isNaN(value)) return value; | ||
return 1; | ||
}, | ||
base: 0 | ||
}, | ||
// String | ||
str: { | ||
is: _.isString, | ||
to: function(v) { | ||
if(_.isString(v)) return v; | ||
if(v instanceof Function) { | ||
throw new Error('E_runtimeInputTypeCoercionError'); | ||
} | ||
if(_.isDate(v)) { | ||
throw new Error('E_runtimeInputTypeCoercionError'); | ||
} | ||
if(v instanceof Function) { | ||
throw new Error('E_runtimeInputTypeCoercionError'); | ||
} | ||
if(v instanceof Object) { | ||
throw new Error('E_runtimeInputTypeCoercionError'); | ||
} | ||
if(v instanceof Array) { | ||
throw new Error('E_runtimeInputTypeCoercionError'); | ||
} | ||
if(v === Infinity) { | ||
throw new Error('E_runtimeInputTypeCoercionError'); | ||
} | ||
if(v === NaN) { | ||
throw new Error('E_runtimeInputTypeCoercionError'); | ||
} | ||
if(v === null) { | ||
throw new Error('E_runtimeInputTypeCoercionError'); | ||
} | ||
if(type.defined.is(v)) return String(v); | ||
return ''; | ||
}, | ||
base: '' | ||
}, | ||
// Object | ||
obj: { | ||
is: function(v) { | ||
return _.isObject(v) && !type.arr.is(v); | ||
}, | ||
to: function(v) { | ||
return {}; | ||
}, | ||
base: {} | ||
}, | ||
// Array | ||
arr: { | ||
is: _.isArray, | ||
to: function(v) { | ||
return _.toArray(v); | ||
}, | ||
base: [] | ||
}, | ||
// Date | ||
'date': { | ||
is: _.isDate, | ||
to: function(v) { | ||
return new Date(v); | ||
}, | ||
base: new Date() | ||
}, | ||
// Numeric | ||
'number': { | ||
is: function(v) { | ||
return _.isNumber(v) && !type.nan.is(parseFloat(v)); | ||
}, | ||
to: function(v) { | ||
// Check for Infinity | ||
if(v === Infinity) { | ||
throw new Error('E_runtimeInputTypeCoercionError'); | ||
} | ||
if(type.number.is(v)) return v; | ||
if(type.bool.is(v)) return v ? 1 : 0; | ||
if(type.str.is(v)) { | ||
// Check for Infinity | ||
if(v === 'Infinity') { | ||
throw new Error('E_runtimeInputTypeCoercionError'); | ||
} | ||
var num = v * 1; | ||
if(!_.isNumber(num)) { | ||
throw new Error('E_runtimeInputTypeCoercionError'); | ||
} | ||
return (num === 0 && !v.match(/^0+$/)) ? 0 : num; | ||
} | ||
throw new Error('E_runtimeInputTypeCoercionError'); | ||
}, | ||
base: 0 | ||
}, | ||
}; | ||
// Aliases | ||
type.string = type.email = type.url = type.str; | ||
type.boolean = type.bool; | ||
type.integer = type.int; | ||
type.float = type.number; | ||
module.exports = type; | ||
// Backwards-compat. | ||
module.exports = require('./helpers/types'); |
{ | ||
"name": "rttc", | ||
"version": "0.1.6", | ||
"version": "0.2.0", | ||
"description": "Runtime type-checking for JavaScript.", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "node ./node_modules/mocha/bin/mocha" | ||
"test": "node ./node_modules/mocha/bin/mocha --recursive" | ||
}, | ||
@@ -9,0 +9,0 @@ "keywords": [ |
@@ -11,2 +11,14 @@ # rttc | ||
## Rules | ||
#### General | ||
+ `null` is never allowed. | ||
+ `NaN` is never allowed. | ||
+ `Infinity` is never allowed. | ||
+ `-Infinity` is never allowed. | ||
## Legacy Usage | ||
@@ -13,0 +25,0 @@ |
var assert = require('assert'); | ||
var coerce = require('../lib/coerce'); | ||
describe('Run-time type checking', function() { | ||
describe('Runtime type checking', function() { | ||
describe('type coercion', function() { | ||
describe('.coerce()', function() { | ||
it.skip('should coerce primitive value', function() { | ||
assert.strictEqual(coerce('string', 'string'), 'foo'); | ||
describe('to string', function() { | ||
it('should coerce undefined to base type', function() { | ||
assert.strictEqual(coerce('string', undefined), ''); | ||
}); | ||
it('should fail on null', function (){ | ||
assert.throws(function (){ | ||
coerce('string', null); | ||
}); | ||
}); | ||
it('should fail on NaN', function (){ | ||
assert.throws(function (){ | ||
coerce('string', NaN); | ||
}); | ||
}); | ||
it('should fail on Infinity', function (){ | ||
assert.throws(function (){ | ||
coerce('string', Infinity); | ||
}); | ||
}); | ||
it('should fail on -Infinity', function (){ | ||
assert.throws(function (){ | ||
coerce('string', -Infinity); | ||
}); | ||
}); | ||
it('should not touch arbitrary string', function() { | ||
assert.strictEqual(coerce('string', 'foo'), 'foo'); | ||
}); | ||
it('should not touch empty string', function() { | ||
assert.strictEqual(coerce('string', ''), ''); | ||
}); | ||
it('should not touch integerish string', function() { | ||
assert.strictEqual(coerce('string', '2382'), '2382'); | ||
}); | ||
it('should not touch negative integerish string', function() { | ||
assert.strictEqual(coerce('string', '-2382'), '-2382'); | ||
}); | ||
it('should not touch negative zeroish string', function() { | ||
assert.strictEqual(coerce('string', '0'), '0'); | ||
}); | ||
it('should not touch decimalish string', function() { | ||
assert.strictEqual(coerce('string', '1.325'), '1.325'); | ||
}); | ||
it('should not touch negative decimalish string', function() { | ||
assert.strictEqual(coerce('string', '-1.325'), '-1.325'); | ||
}); | ||
it('should coerce numbers to strings', function() { | ||
assert.strictEqual(coerce('string', 2382), '2382'); | ||
assert.strictEqual(coerce('string', -2382), '-2382'); | ||
assert.strictEqual(coerce('string', 0), '0'); | ||
assert.strictEqual(coerce('string', 1.325), '1.325'); | ||
assert.strictEqual(coerce('string', -1.325), '-1.325'); | ||
}); | ||
}); | ||
describe('to number', function (){ | ||
it('should coerce undefined to base type', function() { | ||
assert.strictEqual(coerce('number', undefined), 0); | ||
}); | ||
it('should fail on null', function (){ | ||
assert.throws(function (){ | ||
coerce('number', null); | ||
}); | ||
}); | ||
it('should fail on NaN', function (){ | ||
assert.throws(function (){ | ||
coerce('number', NaN); | ||
}); | ||
}); | ||
it('should fail on Infinity', function (){ | ||
assert.throws(function (){ | ||
coerce('number', Infinity); | ||
}); | ||
}); | ||
it('should fail on -Infinity', function (){ | ||
assert.throws(function (){ | ||
coerce('number', -Infinity); | ||
}); | ||
}); | ||
it('should not touch positive integer', function (){ | ||
assert.strictEqual(coerce('number', 3), 3); | ||
}); | ||
it('should not touch negative integer', function (){ | ||
assert.strictEqual(coerce('number', -3), -3); | ||
}); | ||
it('should not touch negative decimal', function (){ | ||
assert.strictEqual(coerce('number', -3.2), -3.2); | ||
}); | ||
it('should not touch zero', function (){ | ||
assert.strictEqual(coerce('number', 0), 0); | ||
}); | ||
it('should coerce "3.25" to 3.25', function() { | ||
assert.strictEqual(coerce('number', '3.25'), 3.25); | ||
}); | ||
it('should coerce "-3.25" to -3.25', function() { | ||
assert.strictEqual(coerce('number', '-3.25'), -3.25); | ||
}); | ||
it('should coerce "0" to 0', function() { | ||
assert.strictEqual(coerce('number', '0'), 0); | ||
}); | ||
}); | ||
describe('to boolean', function (){ | ||
it('should coerce undefined to base type', function() { | ||
assert.strictEqual(coerce('boolean', undefined), false); | ||
}); | ||
it('should fail on null', function (){ | ||
assert.throws(function (){ | ||
coerce('boolean', null); | ||
}); | ||
}); | ||
it('should fail on NaN', function (){ | ||
assert.throws(function (){ | ||
coerce('boolean', NaN); | ||
}); | ||
}); | ||
it('should fail on Infinity', function (){ | ||
assert.throws(function (){ | ||
coerce('boolean', Infinity); | ||
}); | ||
}); | ||
it('should fail on -Infinity', function (){ | ||
assert.throws(function (){ | ||
coerce('boolean', -Infinity); | ||
}); | ||
}); | ||
it('should not touch true', function() { | ||
assert.strictEqual(coerce('boolean', true), true); | ||
}); | ||
it('should not touch false', function() { | ||
assert.strictEqual(coerce('boolean', false), false); | ||
}); | ||
it('should coerce "true" to true', function() { | ||
assert.strictEqual(coerce('boolean', 'true'), true); | ||
}); | ||
it('should coerce "false" to false', function() { | ||
assert.strictEqual(coerce('boolean', 'false'), false); | ||
}); | ||
}); | ||
}); | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
62505
25
1885
178
1