signet
Version:
Signet type library
906 lines (667 loc) • 32.3 kB
JavaScript
var signetBuilder = require('../index');
// var signetBuilder = require('../dist/signet');
var signetParser = require('signet-parser');
var assert = require('chai').assert;
var timerFactory = require('./timer');
var sinon = require('sinon');
describe('Signet API', function () {
var parser;
var signet;
var timer;
function addBuilder() {
return function (a, b) {
return a + b;
}
}
beforeEach(function () {
parser = signetParser();
signet = signetBuilder();
timer = timerFactory();
timer.setMaxAcceptableTime(3);
timer.start();
});
afterEach(function () {
timer.stop();
timer.report();
});
describe('isTypeOf', function () {
it('should verify against an ad-hoc type', function () {
function is5(value) {
return value === 5;
}
assert.equal(signet.isTypeOf(is5)(5), true);
assert.equal(signet.isTypeOf(is5)(6), false);
});
});
describe('sign', function () {
it('should sign a function', function () {
var expectedSignature = 'A < B :: A:number, B:number => number';
var signedAdd = signet.sign(expectedSignature, addBuilder());
var expectedTree = parser.parseSignature(expectedSignature);
assert.equal(JSON.stringify(signedAdd.signatureTree), JSON.stringify(expectedTree));
assert.equal(signedAdd.signature, expectedSignature);
});
it('should throw an error if signature contains a bad type', function () {
var fnUnderTest = signet.sign.bind(null, 'number, foo => bar', addBuilder());
var expectedMessage = "Signature contains invalid types: foo, bar";
assert.throws(fnUnderTest, expectedMessage);
});
it('should throw an error if signature does not satisfy all declared arguments', function () {
var fnUnderTest = signet.sign.bind(null, 'number => number', addBuilder());
var expectedMessage = 'Signature declaration too short for function with 2 arguments';
assert.throws(fnUnderTest, expectedMessage);
});
it('should throw error if signature has no output type', function () {
var fnUnderTest = signet.sign.bind(null, 'number, number', addBuilder());
var expectedMessage = 'Signature must have both input and output types';
assert.throws(fnUnderTest, expectedMessage);
});
it('should throw error if signature has multiple output types', function () {
var fnUnderTest = signet.sign.bind(null, 'number, number => number, number', addBuilder());
var expectedMessage = 'Signature can only have a single output type';
assert.throws(fnUnderTest, expectedMessage);
});
});
describe('enforce', function () {
describe('Core Behaviors', function () {
it('tuple should produce reliable signatures', function () {
const expectedSignature = 'tuple<*;*> => *';
const testFn = signet.enforce(expectedSignature, () => null);
assert.equal(testFn.signature, expectedSignature);
});
it('should wrap an enforced function with an appropriate enforcer', function () {
var originalAdd = addBuilder();
var add = signet.enforce('number, number => number', originalAdd);
assert.equal(add.toString(), originalAdd.toString());
});
it('should enforce a function with a correct argument count', function () {
var add = signet.enforce('number, number => number', addBuilder());
var expectedMessage = 'Anonymous expected a value of type number but got 6 of type string';
assert.throws(add.bind(null, 5, '6'), expectedMessage);
});
it('should enforce a function return value', function () {
var add = signet.enforce('number, number => number', function (a, b) {
(a, b);
return true;
});
var expectedMessage = 'Anonymous expected a return value of type number but got true of type boolean';
assert.throws(add.bind(null, 3, 4), expectedMessage);
});
it('should return result from enforced function', function () {
var add = signet.enforce('number, number => number', addBuilder());
assert.equal(add(3, 4), 7);
});
it('should not throw on unfulfilled optional int argument in a higher-order function containing a variant type', function () {
function slice(start, end) { (end) }
var enforcedSlice = signet.enforce('int, [int] => *', slice);
assert.doesNotThrow(function () {
enforcedSlice(5);
});
});
it('should enforce a curried function properly', function () {
function add(a) {
return function (b) {
(a, b);
return 'bar';
}
}
var curriedAdd = signet.enforce('number => number => number', add);
assert.throws(curriedAdd.bind(null, 'foo'));
assert.throws(curriedAdd(5).bind(null, 'foo'));
assert.throws(curriedAdd(5).bind(null, 6));
});
});
describe('Custom Errors', function () {
it('should throw a custom error on bad input', function () {
var add = signet.enforce('number, number => number', function (a, b) {
(a, b);
return true;
}, {
inputErrorBuilder: function (validationResult, args, signatureTree) {
return 'This is a custom input error!' + validationResult.toString() + args.toString() + signatureTree.toString();
}
});
var expectedMessage = 'This is a custom input error!number,no3,no[object Object],[object Object],[object Object]';
assert.throws(add.bind(null, 3, 'no'), expectedMessage);
});
it('should throw a default error on bad input with core builder', function () {
var add = signet.enforce('number, number => number', function (a, b) {
(a, b);
return true;
}, {
inputErrorBuilder: function (validationResult, args, signatureTree, functionName) {
return signet.buildInputErrorMessage(validationResult, args, signatureTree, functionName);
}
});
var expectedMessage = 'Anonymous expected a value of type number but got no of type string';
assert.throws(add.bind(null, 3, 'no'), expectedMessage);
});
it('should throw a default error on bad output with core builder', function () {
var add = signet.enforce('number, number => number', function (a, b) {
(a, b);
return true;
}, {
outputErrorBuilder: function (validationResult, args, signatureTree, functionName) {
return signet.buildOutputErrorMessage(validationResult, args, signatureTree, functionName);
}
});
var expectedMessage = 'Anonymous expected a return value of type number but got true of type boolean';
assert.throws(add.bind(null, 3, 4), expectedMessage);
});
it('should throw a custom error on bad output', function () {
var add = signet.enforce('number, number => number', function (a, b) {
(a, b);
return true;
}, {
outputErrorBuilder: function (validationResult, args, signatureTree) {
return 'This is a custom output error!' + validationResult.toString() + args.toString() + signatureTree.toString();
}
});
var expectedMessage = 'This is a custom output error!number,true3,4[object Object],[object Object],[object Object]';
assert.throws(add.bind(null, 3, 4), expectedMessage);
});
});
describe('Dependent Type Operator Support', function () {
it('should properly check symbolic dependent types', function () {
function orderedProperly(a, b) {
return a > b;
}
var enforcedFn = signet.enforce('A > B :: A:number, B:number => boolean', orderedProperly);
function testWith(a, b) {
return function () {
return enforcedFn(a, b);
};
}
assert.throws(testWith(5, 6), 'orderedProperly expected a value of type A > B but got A = 5 and B = 6 of type string');
assert.equal(testWith(7, 3)(), true);
});
it('should properly check symbolic type dependencies', function () {
function testFnFactory() {
return function (a, b) {
a + b;
return a;
};
}
assert.throws(signet.enforce(
'A <: B :: A:variant<string;nativeNumber>, B:variant<string;int> => number',
testFnFactory()).bind(null, 2.2, 3),
'Anonymous expected a value of type A <: B but got A = 2.2 and B = 3 of type string');
assert.throws(signet.enforce(
'A < B, B > C :: A:int, B:int, C:int => number',
testFnFactory()).bind(null, 5, 6, 7),
'Anonymous expected a value of type B > C but got B = 6 and C = 7 of type string');
assert.doesNotThrow(signet.enforce(
'A <: B :: A:variant<string;int>, B:variant<string;nativeNumber> => number',
testFnFactory()).bind(null, 5, 6));
assert.doesNotThrow(signet.enforce(
'A < B, B < C :: A:int, B:int, C:int => number',
testFnFactory()).bind(null, 5, 6, 7));
});
});
describe('Object and Constructor Support', function () {
it('should properly enforce constructors', function () {
var testMethodSpy = sinon.spy();
var MyObj = signet.enforce(
'a:int, b:string => undefined',
function (a, b) {
this.testMethod(a, b);
}
);
MyObj.prototype.testMethod = testMethodSpy;
new MyObj(5, 'foo');
var result = JSON.stringify(testMethodSpy.args[0]);
var expectedResult = JSON.stringify([5, 'foo']);
assert.equal(testMethodSpy.callCount, 1);
assert.equal(result, expectedResult);
assert.throws(
function () { return new MyObj('foo', 5); },
'Anonymous expected a value of type a:int but got foo of type string'
);
});
it('should properly enforce object methods', function () {
function MyObj(a) {
this.a = a;
}
MyObj.prototype = {
testMethod: signet.enforce(
'b:int => result:int',
function (b) {
return this.a + b;
}
)
}
var objInstance = new MyObj(6);
assert.equal(objInstance.testMethod(7), 13);
assert.throws(
objInstance.testMethod.bind(objInstance, '7'),
'Anonymous expected a value of type b:int but got 7 of type string'
);
});
});
describe('Function Properties', function () {
it('should preserve properties on enforced function', function () {
function adder(a, b) {
return a + b;
}
adder.myProp = () => 'yay!';
const add = signet.enforce(
'number, number => number',
adder
);
assert.equal(add.myProp(), 'yay!');
});
});
describe('Higher-order Function Support', function () {
it('should support a cross-execution environment table', function () {
const addIncreasing = signet.enforce(
'a < b, b < sum :: a:int => b:int => sum:int',
a => b => a - b
);
assert.throws(addIncreasing(5).bind(null, 4), 'Anonymous expected a value of type a < b but got a = 5 and b = 4 of type string');
assert.throws(addIncreasing(5).bind(null, 6), 'Anonymous expected a return value of type b < sum but got b = 6 and sum = -1 of type string');
});
it('should enforce passed functions when a signature is provided', function () {
const testFn = signet.enforce(
'function<* => boolean> => * => boolean',
function (fn) { return () => fn(); });
function badFn() { return 'foo'; }
assert.throws(testFn(badFn), 'badFn expected a return value of type boolean but got foo of type string');
});
it('should should pass options along to sub-enforcement', function () {
const options = {
outputErrorBuilder: function (validationResult, args, signatureTree) {
return 'This is a custom output error!' + validationResult.toString() + args.toString() + signatureTree.toString();
}
};
const testFn = signet.enforce(
'function<* => boolean> => * => string',
function (fn) { return () => fn(); },
options);
function badFn() { return 'foo'; }
assert.throws(testFn(badFn), 'This is a custom output error!boolean,foo[object Object],[object Object]');
});
it('should not throw when function type is declared with constructor argument', function () {
function doStuff() { return []; }
signet.enforce('function<*, * => string, boolean => boolean> => array', doStuff);
assert.doesNotThrow(doStuff.bind(null, function () { }));
});
});
});
describe('extend', function () {
it('should register a new type', function () {
signet.extend('foo', function (value) { return value === 'foo'; });
assert.equal(signet.isType('foo'), true);
assert.equal(signet.isTypeOf('foo')('foo'), true);
});
it('should handle type arity up front', function () {
signet.extend('myTestType0', function () { });
signet.extend('myTestType1{1}', function () { });
signet.extend('myTestType1OrMore{1,}', function () { });
signet.extend('myTestType2To5{2, 5}', function () { });
assert.doesNotThrow(signet.isTypeOf.bind(null, 'myTestType0<1, 2, 3>'));
assert.throws(
signet.isTypeOf('myTestType1<1, 2, 3>').bind(null, 'foo'),
'Type myTestType1 accepts, at most, 1 arguments');
assert.throws(
signet.isTypeOf('myTestType1').bind(null, 'foo'),
'Type myTestType1 requires, at least, 1 arguments');
assert.doesNotThrow(signet.isTypeOf('myTestType1OrMore<1, 2, 3>').bind(null, 'foo'));
assert.throws(
signet.isTypeOf('myTestType1OrMore').bind(null, 'foo'),
'Type myTestType1OrMore requires, at least, 1 arguments');
assert.doesNotThrow(signet.isTypeOf('myTestType2To5<1, 2, 3>').bind(null, 'foo'));
assert.throws(
signet.isTypeOf('myTestType2To5').bind(null, 'foo'),
'Type myTestType2To5 requires, at least, 2 arguments');
assert.throws(
signet.isTypeOf('myTestType2To5<1, 2, 3, 4, 5, 6>').bind(null, 'foo'),
'Type myTestType2To5 accepts, at most, 5 arguments');
assert.throws(
signet.extend.bind(null, 'myTestTypeBroken{5, 1}', function () { }),
'Error in myTestTypeBroken arity declaration: min cannot be greater than max');
});
});
describe('subtype', function () {
it('should register a subtype', function () {
signet.subtype('number')('intFoo', function (value) { return Math.floor(value) === value; });
assert.equal(signet.isSubtypeOf('number')('intFoo'), true);
assert.equal(signet.isTypeOf('intFoo')(15), true);
});
});
describe('alias', function () {
it('should allow aliasing of types by other names', function () {
signet.alias('foo', 'string');
assert.equal(signet.isTypeOf('foo')('bar'), true);
assert.equal(signet.isTypeOf('foo')(5), false);
});
it('should partially apply a type value', function () {
signet.alias('testTuple', 'tuple<_; _>');
signet.alias('testPartialTuple', 'testTuple<int; _>');
assert.equal(signet.isTypeOf('testTuple<array; object>')([[], {}]), true);
assert.equal(signet.isTypeOf('testPartialTuple<string>')([5, 'foo']), true);
assert.equal(signet.isTypeOf('testPartialTuple<string>')([5, 6]), false);
});
});
describe('verify', function () {
it('should allow function argument verification inside a function body', function () {
function test(a, b) {
(a, b);
signet.verify(test, arguments);
}
signet.sign('string, number => undefined', test);
assert.throws(test.bind(5, 'five'));
});
});
describe('enforceArguments', function () {
it('throws an error if arguments do not match requirement', function () {
function test(a) {
signet.enforceArguments(['a:string'])(arguments);
}
assert.throws(test.bind(null, 5));
});
it('verifies arguments and skips unfulfilled optional arguments', function () {
function test(a, b, c) {
signet.enforceArguments(['a: string', 'b: [number]', 'c: string'])(arguments);
}
assert.doesNotThrow(test.bind(null, 'foo', 'bar'));
});
});
describe('typeChain', function () {
it('should return correct type chains', function () {
const arrayTypeChain = signet.typeChain('array');
const numberTypeChain = signet.typeChain('number');
assert.equal(arrayTypeChain, '* -> object -> array');
assert.equal(numberTypeChain, '* -> nativeNumber -> number');
});
});
describe('duckTypeFactory', function () {
it('should duck type check an object', function () {
var isMyObj = signet.duckTypeFactory({
foo: 'string',
bar: 'int',
baz: 'array'
});
signet.subtype('object')('myObj', isMyObj);
assert.equal(signet.isTypeOf('myObj')({ foo: 55 }), false);
assert.equal(signet.isTypeOf('myObj')({ foo: 'blah', bar: 55, baz: [] }), true);
});
it('should return false if value is not duck-type verifiable', function () {
var isMyObj = signet.duckTypeFactory({
foo: 'string',
bar: 'int',
baz: 'array'
});
assert.equal(isMyObj(null), false);
});
it('should produce a recursively defined type', function() {
var isMyObj = signet.duckTypeFactory({
_something: {
anotherThing: 'string'
},
foo: 'string',
bar: 'int',
baz: 'array'
})
signet.subtype('object')('myObj', isMyObj)
var testObject = {
_something: {
anotherThing: 'yay!'
},
foo: 'blah',
bar: 127,
baz: []
}
assert.equal(signet.isTypeOf('myObj')(testObject), true);
})
});
describe('exactDuckTypeFactory', function () {
it('should check and exact duck type on an object', function () {
var isMyObj = signet.exactDuckTypeFactory({
foo: 'string',
bar: 'int',
baz: 'array'
});
signet.subtype('object')('myExactObj', isMyObj);
assert.equal(signet.isTypeOf('myExactObj')({ foo: 'blah', bar: 55, baz: [], quux: '' }), false);
assert.equal(signet.isTypeOf('myExactObj')({ foo: 'blah', bar: 55, baz: [] }), true);
});
it('should check and exact duck type on an object', function () {
var isMyObj = signet.exactDuckTypeFactory({
foo: 'string',
bar: 'int',
baz: 'array'
});
signet.subtype('object')('myExactObj', isMyObj);
assert.equal(signet.isTypeOf('myExactObj')({ foo: 'blah', bar: 55, baz: [], quux: '' }), false);
assert.equal(signet.isTypeOf('myExactObj')({ foo: 'blah', bar: 55, baz: [] }), true);
});
});
describe('defineDuckType', function () {
it('should allow duck types to be defined directly', function () {
signet.defineDuckType('myObj', {
foo: 'string',
bar: 'int',
baz: 'array'
});
assert.equal(signet.isTypeOf('myObj')({ foo: 55 }), false);
assert.equal(signet.isTypeOf('myObj')({ foo: 'blah', bar: 55, baz: [] }), true);
});
it('should allow reporting of duck type errors', function () {
signet.defineDuckType('aTestThingy', {
quux: '!*'
});
signet.defineDuckType('myObj', {
foo: 'string',
bar: 'int',
baz: 'array',
deeperType: 'aTestThingy'
});
var result = signet.reportDuckTypeErrors('myObj')({ foo: 55, bar: 'bad value', baz: null, deeperType: {} });
var expected = '[["foo","string",55],["bar","int","bad value"],["baz","array",null],["deeperType","aTestThingy",[["quux","not<variant<undefined, null>>",null]]]]';
assert.equal(JSON.stringify(result), expected);
assert.equal(signet.isTypeOf('myObj')({ foo: 'blah', bar: 55, baz: [], deeperType: { quux: 'something' } }), true);
});
});
describe('defineClassType', function () {
it('verifies a type based on provided class', function () {
class MyClass {
constructor() {}
test() {}
test1() {}
}
signet.defineClassType(MyClass);
const myInstance = new MyClass();
assert.isTrue(signet.isTypeOf('MyClass')(myInstance));
assert.isFalse(signet.isTypeOf('MyClass')({}));
});
it('allows for extra properties to be defined', function () {
class MyClass {
constructor() {
this.foo = 'bar';
this.someInt = 1234;
}
}
signet.defineClassType(MyClass, { foo: 'string', someInt: 'int' });
const myInstance = new MyClass();
assert.isTrue(signet.isTypeOf('MyClass')(myInstance));
assert.isFalse(signet.isTypeOf('MyClass')({}));
});
it('throws an error when on attempt to override existing property', function () {
class MyClass {
constructor() {
this.someInt = 1234;
}
foo() {}
}
const classTypeDefiner = () => signet.defineClassType(MyClass, { foo: 'string', someInt: 'int' });
assert.throws(classTypeDefiner);
});
});
describe('classTypeFactory', function () {
it('verifies a type based on provided class', function () {
class MyClass {
constructor() {}
test() {}
test1() {}
}
const isMyClass = signet.classTypeFactory(MyClass);
const myInstance = new MyClass();
assert.isTrue(isMyClass(myInstance));
assert.isFalse(isMyClass({}));
});
it('allows for extra properties to be defined', function () {
class MyClass {
constructor() {
this.foo = 'bar';
this.someInt = 1234;
}
}
const isMyClass = signet.classTypeFactory(MyClass, { foo: 'string', someInt: 'int' });
const myInstance = new MyClass();
assert.isTrue(isMyClass(myInstance));
assert.isFalse(isMyClass({}));
});
it('throws an error when on attempt to override existing property', function () {
class MyClass {
constructor() {
this.someInt = 1234;
}
foo() {}
}
const classTypeBuilder = () => signet.classTypeFactory(MyClass, { foo: 'string', someInt: 'int' });
assert.throws(classTypeBuilder);
});
});
describe('reportDuckTypeErrors', function () {
it('should return duck type error on bad object value', function () {
signet.defineDuckType('duckTest', {});
let checkDuckTest = signet.reportDuckTypeErrors('duckTest');
const nullCheck = checkDuckTest(null);
const intCheck = checkDuckTest(55);
const stringCheck = checkDuckTest('foo');
assert.equal(JSON.stringify(nullCheck), '[["badDuckTypeValue","object",null]]');
assert.equal(JSON.stringify(intCheck), '[["badDuckTypeValue","object",55]]');
assert.equal(JSON.stringify(stringCheck), '[["badDuckTypeValue","object","foo"]]');
});
});
describe('isRegisteredDuckType', function () {
it('should allow querying of registered duck types', function () {
signet.defineDuckType('duckFoo', {});
assert.equal(signet.isRegisteredDuckType('duckFoo'), true);
assert.equal(signet.isRegisteredDuckType('duckBar'), false);
});
});
describe('whichVariantType', function () {
it('should get variant type of value', function () {
var getValueType = signet.whichVariantType('variant<string; int>');
assert.equal(getValueType('foo'), 'string');
assert.equal(getValueType(17), 'int');
assert.equal(getValueType(17.5), null);
});
});
describe('verifyValueType', function () {
it('should return value when it matches type correctly', function () {
var stringValue = 'foo';
var stringResult = signet.verifyValueType('string')(stringValue);
var boundedIntValue = 5;
var boundedIntResult = signet.verifyValueType('leftBoundedInt<4>')(boundedIntValue);
assert.equal(stringResult, stringValue);
assert.equal(boundedIntResult, boundedIntValue);
});
it('should throw an error if the value is of incorrect type', function () {
var verifyStringValue = signet.verifyValueType('string');
var verifyBoundedIntValue = signet.verifyValueType('leftBoundedInt<4>');
assert.throws(() => verifyStringValue({}));
assert.throws(() => verifyBoundedIntValue(-3));
});
});
describe('iterateOn and recursiveTypeFactory', function () {
function setImmutableValue(obj, key, value) {
Object.defineProperty(obj, key, {
writeable: false,
value: value
});
}
function cons(value, list) {
const newNode = {};
setImmutableValue(newNode, 'value', value);
setImmutableValue(newNode, 'next', list);
return newNode;
}
it('should allow easy creation of a recursive type', function () {
const isListNode = signet.duckTypeFactory({
value: 'int',
next: 'composite<not<array>, object>'
});
const iterableFactory = signet.iterateOn('next');
const isIntList = signet.recursiveTypeFactory(iterableFactory, isListNode);
const testList = cons(1, cons(2, cons(3, cons(4, cons(5, null)))));
assert.equal(isIntList(testList), true);
assert.equal(isIntList({ value: 1 }), false);
assert.equal(isIntList('blerg'), false);
});
it('should properly recurse through a binary tree with left and right values', function () {
const isBinaryTreeNode = signet.duckTypeFactory({
value: 'int',
left: 'composite<^array, object>',
right: 'composite<^array, object>',
});
function isOrderedNode(node) {
return isBinaryTreeNode(node)
&& ((node.left === null || node.right === null)
|| (node.value > node.left.value
&& node.value <= node.right.value));
}
signet.subtype('object')('orderedBinaryTreeNode', isOrderedNode);
function iteratorFactory(value) {
var iterable = [];
iterable = value.left !== null ? iterable.concat([value.left]) : iterable;
iterable = value.right !== null ? iterable.concat([value.right]) : iterable;
return signet.iterateOnArray(iterable);
}
signet.defineRecursiveType('orderedBinaryTree', iteratorFactory, 'orderedBinaryTreeNode');
const isOrderedIntTree = signet.isTypeOf('orderedBinaryTree');
const goodBinaryTree = {
value: 0,
left: {
value: -1,
left: null,
right: null
},
right: {
value: 1,
left: {
value: 1,
left: null,
right: null
},
right: null
}
};
const badTree = {
value: 0,
left: {
value: -1,
left: null,
right: null
},
right: {
value: -3,
left: {
value: 1,
left: null,
right: null
},
right: null
}
};
const malformedTree = {
value: 0,
left: null
};
assert.equal(isOrderedIntTree(goodBinaryTree), true);
assert.equal(isOrderedIntTree(badTree), false);
assert.equal(isOrderedIntTree(malformedTree), false);
});
});
});