signet
Version:
Signet type library
616 lines (511 loc) • 19.8 kB
JavaScript
function signetBuilder(
typelog,
validator,
checker,
parser,
assembler,
duckTypes,
coreTypes,
recursiveTypes) {
'use strict';
var placeholderPattern = /([<\;\,]\s*)(_)(\s*[>\;\,])/;
function hasPlaceholder(typeStr) {
return placeholderPattern.test(typeStr);
}
function replacePlaceholders(typeStr, typeValues) {
return typeValues.reduce(function (result, typeValue) {
return result.replace(placeholderPattern, '$1' + typeValue + '$3');
}, typeStr);
}
function buildTypeAlias(typeDef) {
var checkValue = typelog.isTypeOf(typeDef);
return function typeCheck(value) {
return checkValue(value);
};
}
function buildPartialTypeAlias(typeStr) {
return function typeCheck(value, typeValues) {
var finalTypeStr = replacePlaceholders(typeStr, typeValues);
var typeDef = parser.parseType(finalTypeStr);
return buildTypeAlias(typeDef)(value);
};
}
function alias(key, typeStr) {
var typeAlias = hasPlaceholder(typeStr)
? buildPartialTypeAlias(typeStr)
: buildTypeAlias(parser.parseType(typeStr));
extend(key, typeAlias);
}
function isTypeOf(typeValue) {
return typeof typeValue === 'string'
? typelog.isTypeOf(parser.parseType(typeValue))
: typeValue;
}
function verifyValueType(typeValue) {
var isValidType = isTypeOf(typeValue);
return function (value) {
if (!isValidType(value)) {
throw new TypeError('Expected value of type ' + typeValue + ', but got ' + String(value) + ' of type ' + typeof value);
}
return value;
};
}
function addImmutableProperty(obj, key, value) {
Object.defineProperty(obj, key, {
value: value,
writeable: false
});
return obj;
}
function attachSignatureAssembler(fn, signatureTree) {
addImmutableProperty(fn, 'signatureTree', signatureTree);
Object.defineProperty(fn, 'signature', {
writeable: false,
get: function () {
return assembler.assembleSignature(fn.signatureTree);
}
});
return fn;
}
function throwOnSignatureError(signatureTree, fn) {
var signatureCheckResult = checker.checkSignature(signatureTree);
var lastIndex = signatureTree.length - 1;
if (signatureTree.length < 2) {
throw new SyntaxError('Signature must have both input and output types');
}
if (signatureTree[0].length < fn.length) {
throw new Error('Signature declaration too short for function with ' + fn.length + ' arguments.');
}
if (signatureTree[lastIndex].length > 1) {
throw new SyntaxError('Signature can only have a single output type');
}
if (signatureCheckResult !== null) {
var invalidTypes = signatureCheckResult.map(assembler.assembleType);
throw new TypeError("Signature contains invalid types: " + invalidTypes.join(', '));
}
}
function signFn(signatureTree, fn) {
attachSignatureAssembler(fn, signatureTree);
return addImmutableProperty(fn, 'signatureTree', signatureTree);
}
function sign(signature, fn) {
var signatureTree = parser.parseSignature(signature);
throwOnSignatureError(signatureTree, fn);
return signFn(signatureTree, fn);
}
function buildEvaluationError(validationResult, prefixMixin, functionName) {
var expectedType = validationResult[0];
var value = validationResult[1];
var valueType = typeof value;
var errorMessage = functionName + ' expected a ' + prefixMixin + 'value of type ' +
expectedType + ' but got ' +
validationResult[1] + ' of type ' + valueType;
return errorMessage;
}
function evaluationErrorFactory(prefix) {
return function throwEvaluationError(
validationResult,
errorBuilder,
args,
signatureTree,
functionName
) {
var errorMessage = '';
if (typeof errorBuilder === 'function') {
errorMessage = errorBuilder(validationResult, args, signatureTree, functionName);
} else {
errorMessage = buildEvaluationError(validationResult, prefix, functionName);
}
throw new TypeError(errorMessage);
}
}
var throwInputError = evaluationErrorFactory('');
var throwOutputError = evaluationErrorFactory('return ');
function buildInputErrorMessage(validationResult, args, signatureTree, functionName) {
return buildEvaluationError(validationResult, '', functionName);
}
function buildOutputErrorMessage(validationResult, args, signatureTree, functionName) {
return buildEvaluationError(validationResult, 'return ', functionName);
}
function verify(fn, args, environment) {
var result = validator.validateArguments(fn.signatureTree[0], environment)(args);
if (result !== null) {
throwInputError(result, null, args, fn.signatureTree, getFunctionName(fn));
}
}
function enforceArguments(types) {
return function (rawArgs) {
var args = Array.prototype.slice.call(rawArgs, 0);
var parsedTypes = types.map(parser.parseType);
var result = validator.validateArguments(parsedTypes, undefined)(args);
if (result !== null) {
throwInputError(result, null, args, [parsedTypes], 'Called Function');
}
}
}
function getFunctionName(fn) {
return fn.name === '' ? 'Anonymous' : fn.name;
}
function processFunction(fn, type, options) {
var signature = type.subtype.join(', ').trim();
var returnFn = signature !== ''
? enforce(signature, fn, options)
: fn;
return returnFn;
}
function processArg(arg, type, options) {
var cleanType = typeof type === 'object' ? type : {};
if (cleanType.type === 'function') {
return processFunction(arg, cleanType, options);
} else {
return arg;
}
}
function processArgs(args, typeList, options) {
var argResults = [];
var argIndex = 0;
var typeIndex = 0;
for (argIndex; argIndex < args.length; argIndex++) {
var currentArg = args[argIndex];
var currentType = undefined
for (typeIndex; typeIndex < typeList.length; typeIndex++) {
currentType = typeList[typeIndex];
if (currentType.typeCheck(currentArg)) {
break;
}
}
argResults.push(processArg(currentArg, currentType, options));
}
return argResults;
}
function quickSliceFrom(index, values) {
var result = [];
for (var i = index; i < values.length; i++) {
result.push(values[i]);
}
return result;
}
function buildEnforcer(signatureTree, fn, options) {
var functionName = getFunctionName(fn);
return function () {
var args = quickSliceFrom(0, arguments);
var environmentTable = typeof signatureTree.environment === 'object'
? Object.create(signatureTree.environment)
: {};
var validationResult = validator.validateArguments(signatureTree[0], environmentTable)(args);
var nextTree = quickSliceFrom(1, signatureTree);
if (validationResult !== null) {
throwInputError(
validationResult,
options.inputErrorBuilder,
args,
signatureTree,
functionName);
}
var signatureIsCurried = signatureTree.length > 2;
var processedArgs = processArgs(args, signatureTree[0], options);
var result = fn.apply(this, processedArgs);
var isFinalResult = nextTree.length === 1;
var resultCheckTree = nextTree[0];
resultCheckTree.dependent = signatureTree[0].dependent;
var resultValidation = isFinalResult
? validator.validateArguments(resultCheckTree, environmentTable)([result])
: null;
if (resultValidation !== null) {
throwOutputError(
resultValidation,
options.outputErrorBuilder,
processedArgs,
signatureTree,
functionName);
}
nextTree.environment = environmentTable;
nextTree[0].dependent = signatureTree[0].dependent;
return signatureIsCurried
? enforceOnTree(nextTree, result, options)
: result;
};
}
function buildEnforceDecorator(enforcer) {
return function enforceDecorator() {
var args = quickSliceFrom(0, arguments);
return enforcer.apply(this, args);
}
}
function attachProps(fn, enforcedFn) {
var keys = Object.keys(fn);
keys.reduce(function (enforcedFn, key) {
enforcedFn[key] = fn[key];
return enforcedFn;
}, enforcedFn);
return enforcedFn;
}
function enforceOnTree(signatureTree, fn, options) {
var enforcer = buildEnforcer(signatureTree, fn, options);
var enforceDecorator = buildEnforceDecorator(enforcer);
enforceDecorator.toString = Function.prototype.toString.bind(fn);
var signedEnforceDecorator = signFn(signatureTree, enforceDecorator);
return attachProps(fn, signedEnforceDecorator);
}
function addTypeCheck(typeDef) {
typeDef.typeCheck = typelog.isTypeOf(typeDef);
return typeDef;
}
function prepareSubtree(subtree) {
var updatedSubtree = subtree.map(addTypeCheck);
updatedSubtree.dependent = subtree.dependent;
return updatedSubtree;
}
function prepareSignature(signatureTree) {
return signatureTree.map(prepareSubtree);
}
function enforce(signature, fn, options) {
var signatureTree = prepareSignature(parser.parseSignature(signature));
var cleanOptions = typeof options === 'object' && options !== null ? options : {};
return enforceOnTree(signatureTree, fn, cleanOptions);
}
function attachPreprocessor(typeCheck, preprocessor) {
if (typeof preprocessor === 'function') {
typeCheck.preprocess = preprocessor;
}
}
var typeArityPattern = /^([^\{]+)\{([^\}]+)\}$/;
function getArity(typeName, typeStr) {
var arityStr = typeStr.replace(typeArityPattern, '$2');
var arityData = arityStr.split(/\,\s*/g);
var min = 0;
var max = Infinity;
if (arityStr !== typeStr) {
min = parseInt(arityData[0]);
max = arityData.length === 1 ? min : parseInt(arityData[1]);
if (min > max) {
throw new Error('Error in ' + typeName + ' arity declaration: min cannot be greater than max');
}
min = isNaN(min) || min < 0 ? 0 : min;
max = isNaN(max) || max < 0 ? Infinity : max;
}
return [min, max];
}
function getTypeName(typeStr) {
return typeStr.replace(typeArityPattern, '$1').trim();
}
function checkTypeArity(typeName, arity, options) {
var optionsIsArray = Object.prototype.toString.call(options) === '[object Array]';
var errorMessage = null;
if (optionsIsArray && options.length < arity[0]) {
errorMessage = 'Type ' + typeName + ' requires, at least, ' + arity[0] + ' arguments';
} else if (optionsIsArray && options.length > arity[1]) {
errorMessage = 'Type ' + typeName + ' accepts, at most, ' + arity[1] + ' arguments';
}
if (errorMessage !== null) {
throw new Error(errorMessage);
}
}
function decorateWithArityCheck(typeName, typeArity, typeCheck) {
return function decoratedTypeCheck(value, options) {
checkTypeArity(typeName, typeArity, options);
return typeCheck(value, options);
}
}
function extend(typeStr, typeCheck, preprocessor) {
var typeName = getTypeName(typeStr);
var typeArity = getArity(typeName, typeStr);
var decoratedTypeCheck = decorateWithArityCheck(typeName, typeArity, typeCheck);
attachPreprocessor(decoratedTypeCheck, preprocessor);
typelog.define(typeName, decoratedTypeCheck);
}
function subtype(parentTypeName) {
var defineSubtype = typelog.defineSubtypeOf(parentTypeName);
return function (typeStr, typeCheck, preprocessor) {
var typeName = getTypeName(typeStr);
var typeArity = getArity(typeName, typeStr);
var decoratedTypeCheck = decorateWithArityCheck(typeName, typeArity, typeCheck);
attachPreprocessor(decoratedTypeCheck, preprocessor);
defineSubtype(typeName, decoratedTypeCheck);
};
}
var typeApi = coreTypes(
parser,
extend,
isTypeOf,
typelog.isType,
typelog.isSubtypeOf,
subtype,
alias,
typelog.defineDependentOperatorOn);
var duckTypesModule = duckTypes(
typelog,
isTypeOf,
parser.parseType,
assembler.assembleType);
var recursiveTypeModule = recursiveTypes(extend, isTypeOf);
return {
alias: enforce(
'aliasName != typeString ' +
':: aliasName:string, ' +
'typeString:string ' +
'=> undefined',
alias),
buildInputErrorMessage: enforce(
'validationResult:tuple<' +
'expectedType:type, ' +
'actualValue:*' +
'>, ' +
'args:array<*>, ' +
'signatureTree:array<array<object>>, ' +
'functionName:string ' +
'=> string',
buildInputErrorMessage
),
buildOutputErrorMessage: enforce(
'validationResult:tuple<' +
'expectedType:type, ' +
'actualValue:*' +
'>, ' +
'args:array<*>, ' +
'signatureTree:array<array<object>>, ' +
'functionName:string ' +
'=> string',
buildOutputErrorMessage
),
classTypeFactory: enforce(
'class:function, ' +
'otherProps:[composite<not<null>, object>] ' +
'=> function',
duckTypesModule.classTypeFactory),
defineClassType: enforce(
'class:function, ' +
'otherProps:[composite<not<null>, object>] ' +
'=> undefined',
duckTypesModule.defineClassType),
defineDuckType: enforce(
'typeName:string, ' +
'duckTypeDef:object ' +
'=> undefined',
duckTypesModule.defineDuckType),
defineExactDuckType: enforce(
'typeName:string, ' +
'duckTypeDef:object ' +
'=> undefined',
duckTypesModule.defineExactDuckType),
defineDependentOperatorOn: enforce(
'typeName:string => ' +
'operator:string, operatorCheck:function<' +
'valueA:*, ' +
'valueB:*, ' +
'typeDefinitionA:[object], ' +
'typeDefinitionB:[object] ' +
'=> boolean' +
'> ' +
'=> undefined',
typelog.defineDependentOperatorOn),
defineRecursiveType: enforce(
'typeName:string, ' +
'iteratorFactory:function, ' +
'nodeType:type, ' +
'typePreprocessor:[function] ' +
'=> undefined',
recursiveTypeModule.defineRecursiveType),
duckTypeFactory: enforce(
'duckTypeDef:object => function',
duckTypesModule.duckTypeFactory),
enforce: enforce(
'signature:string, ' +
'functionToEnforce:function, ' +
'options:[object] ' +
'=> function',
enforce),
enforceArguments: enforce(
'array<string> => arguments => undefined',
enforceArguments),
exactDuckTypeFactory: enforce(
'duckTypeDef:object => function',
duckTypesModule.exactDuckTypeFactory),
extend: enforce(
'typeName:string, ' +
'typeCheck:function, ' +
'preprocessor:[function<string => string>] ' +
'=> undefined',
extend),
isRegisteredDuckType: enforce(
'typeName:string ' +
'=> boolean',
duckTypesModule.isRegisteredDuckType),
isSubtypeOf: enforce(
'rootTypeName:string ' +
'=> typeNameUnderTest:string ' +
'=> boolean',
typelog.isSubtypeOf),
isType: enforce(
'typeName:string => boolean',
typelog.isType),
isTypeOf: enforce(
'typeToCheck:type ' +
'=> value:* ' +
'=> boolean',
isTypeOf),
iterateOn: enforce(
'propertyKey:string ' +
'=> value:* ' +
'=> undefined ' +
'=> *',
recursiveTypeModule.iterateOn
),
iterateOnArray: enforce(
'iterationArray:array ' +
'=> undefined ' +
'=> *',
recursiveTypeModule.iterateOnArray
),
recursiveTypeFactory: enforce(
'iteratorFactory:function, ' +
'nodeType:type ' +
'=> valueToCheck:* ' +
'=> boolean',
recursiveTypeModule.recursiveTypeFactory),
registerTypeLevelMacro: enforce(
'macro:function => undefined',
parser.registerTypeLevelMacro),
reportDuckTypeErrors: enforce(
'duckTypeName:string ' +
'=> valueToCheck:* ' +
'=> array<tuple<string, string, *>>',
duckTypesModule.reportDuckTypeErrors),
sign: enforce(
'signature:string, functionToSign:function => function',
sign),
subtype: enforce(
'rootTypeName:string ' +
'=> subtypeName:string, ' +
'subtypeCheck:function, ' +
'preprocessor:[function<string => string>] ' +
'=> undefined',
subtype),
typeChain: enforce(
'typeName:string => string',
typelog.getTypeChain),
verify: enforce(
'signedFunctionToVerify:function, ' +
'functionArguments:arguments ' +
'=> undefined',
verify),
verifyValueType: enforce(
'typeToCheck:type ' +
'=> value:* ' +
'=> result:*',
verifyValueType),
whichType: enforce(
'typeNames:array<string> => ' +
'value:* ' +
'=> variant<string, null>',
typeApi.whichType),
whichVariantType: enforce(
'variantString:string => ' +
'value:* ' +
'=> variant<string, null>',
typeApi.whichVariantType)
};
}
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = signetBuilder;
}