contracts-js
Version:
A contract library for JavaScript
695 lines (632 loc) • 30.1 kB
JavaScript
(function() {
"use strict";
if (typeof require === "function") {
// importing patches Proxy to be in line with the new direct proxies
require("harmony-reflect");
}
operator (|?|) 4 left {$l, $r} => #{ typeof $l !== 'undefined' ? $l : $r }
var unproxy = new WeakMap();
var typeVarMap = new WeakMap();
var Blame = {
create: function(name, pos, neg, lineNumber) {
var o = new BlameObj(name, pos, neg, lineNumber);
return o;
},
clone: function(old, props) {
var o = new BlameObj(props.name |?| old.name,
props.pos |?| old.pos,
props.neg |?| old.neg,
props.lineNuber |?| old.lineNumber);
o.expected = props.expected |?| old.expected;
o.given = props.given |?| old.given;
o.loc = props.loc |?| old.loc;
o.parents = props.parents |?| old.parents;
return o;
}
};
function BlameObj(name, pos, neg, lineNumber) {
this.name = name;
this.pos = pos;
this.neg = neg;
this.lineNumber = lineNumber;
}
BlameObj.prototype.swap = function() {
return Blame.clone(this, {
pos: this.neg,
neg: this.pos
});
};
BlameObj.prototype.addExpected = function(expected, override) {
if (this.expected === undefined || override) {
return Blame.clone(this, {
expected: expected
});
}
return Blame.clone(this, {});
};
BlameObj.prototype.addGiven = function(given) {
return Blame.clone(this, {
given: given
});
};
BlameObj.prototype.addLocation = function(loc) {
return Blame.clone(this, {
loc: this.loc != null ? this.loc.concat(loc) : [loc]
})
;
};
BlameObj.prototype.addParents = function(parent) {
return Blame.clone(this, {
parents: this.parents != null ? this.parents.concat(parent) : [parent]
});
};
BlameObj.prototype.setNeg = function(neg) {
return Blame.clone(this, {
neg: neg
});
};
function assert(cond, msg) {
if(!cond) {
throw new Error(msg);
}
}
class Contract {
constructor(name, type, proj) {
this.name = name;
this.type = type;
this.proj = proj.bind(this);
}
closeCycle(contract) {
this.cycleContract = contract;
return contract;
}
toString() {
return this.name;
}
}
function addQuotes(val) {
if (typeof val === "string") {
return "'" + val + "'";
}
return val;
}
function raiseBlame(blame) {
var lineMessage = blame.lineNumber !== undefined ?
"function " + blame.name + " guarded at line: " + blame.lineNumber + "\n"
: "";
var msg = blame.name + ": contract violation\n" +
"expected: " + blame.expected + "\n" +
"given: " + addQuotes(blame.given) + "\n" +
"in: " + blame.loc.slice().reverse().join("\n ") + "\n" +
" " + blame.parents[0] + "\n" +
lineMessage +
"blaming: " + blame.pos + "\n";
throw new Error(msg);
}
function makeCoffer(name) {
return new Contract(name, "coffer", function(blame, unwrapTypeVar, projOptions) {
return function(val) {
var locationMsg = "in the type variable " + name + " of";
if (unwrapTypeVar) {
if (val && typeof val === "object" && unproxy.has(val)) {
var unwraperProj = typeVarMap.get(this).contract.proj(blame.addLocation(locationMsg));
return unwraperProj(unproxy.get(val));
} else {
raiseBlame(blame.addExpected("an opaque value")
.addGiven(val)
.addLocation(locationMsg));
}
} else {
var towrap = val && typeof val === "object" ? val : {};
var p = new Proxy(towrap, {
getOwnPropertyDescriptor: function() {
raiseBlame(blame.swap()
.addExpected("value to not be manipulated")
.addGiven("called Object.getOwnPropertyDescriptor")
.addLocation(locationMsg));
},
getOwnPropertyName: function() {
raiseBlame(blame.swap()
.addExpected("value to not be manipulated")
.addGiven("called Object.getOwnPropertyName")
.addLocation(locationMsg));
},
defineProperty: function() {
raiseBlame(blame.swap()
.addExpected("value to not be manipulated")
.addGiven("called Object.defineProperty")
.addLocation(locationMsg));
},
deleteProperty: function(target, propName) {
raiseBlame(blame.swap()
.addExpected("value to not be manipulated")
.addGiven("called delete on property" + propName)
.addLocation(locationMsg));
},
freeze: function() {
raiseBlame(blame.swap()
.addExpected("value to not be manipulated")
.addGiven("called Object.freeze")
.addLocation(locationMsg));
},
seal: function() {
raiseBlame(blame.swap()
.addExpected("value to not be manipulated")
.addGiven("called Object.seal")
.addLocation(locationMsg));
},
preventExtensions: function() {
raiseBlame(blame.swap()
.addExpected("value to not be manipulated")
.addGiven("called Object.preventExtensions")
.addLocation(locationMsg));
},
has: function(target, propName) {
raiseBlame(blame.swap()
.addExpected("value to not be manipulated")
.addGiven("called `in` for property " + propName)
.addLocation(locationMsg));
},
hasOwn: function(target, propName) {
raiseBlame(blame.swap()
.addExpected("value to not be manipulated")
.addGiven("called Object.hasOwnProperty on property " + propName)
.addLocation(locationMsg));
},
get: function(target, propName) {
var givenMsg = "performed obj." + propName;
if (propName === "valueOf") {
givenMsg = "attempted to inspect the value";
}
raiseBlame(blame.swap()
.addExpected("value to not be manipulated")
.addGiven(givenMsg)
.addLocation(locationMsg));
},
set: function(target, propName, val) {
raiseBlame(blame.swap()
.addExpected("value to not be manipulated")
.addGiven("performed obj." + propName + " = " + val)
.addLocation(locationMsg));
},
enumerate: function() {
raiseBlame(blame.swap()
.addExpected("value to not be manipulated")
.addGiven("value used in a `for in` loop")
.addLocation(locationMsg));
},
iterate: function() {
raiseBlame(blame.swap()
.addExpected("value to not be manipulated")
.addGiven("value used in a `for of` loop")
.addLocation(locationMsg));
},
keys: function() {
raiseBlame(blame.swap()
.addExpected("value to not be manipulated")
.addGiven("called Object.keys")
.addLocation(locationMsg));
},
apply: function() {
raiseBlame(blame.swap()
.addExpected("value to not be manipulated")
.addGiven("attempted to invoke the value")
.addLocation(locationMsg));
},
construct: function() {
raiseBlame(blame.swap()
.addExpected("value to not be manipulated")
.addGiven("attempted to invoke the value with new")
.addLocation(locationMsg));
}
});
if (!typeVarMap.has(this)) {
var valType = typeof val;
var inferedContract = check(function(checkVal) {
return (typeof checkVal) === valType;
}, "(x) => typeof x === '" + valType + "'");
typeVarMap.set(this, {
contract: inferedContract
});
} else {
var inferedProj = typeVarMap.get(this).contract.proj(blame.addLocation(locationMsg));
inferedProj(val);
}
unproxy.set(p, val);
return p;
}
}.bind(this);
});
}
function check(predicate, name) {
var c = new Contract(name, "check", function(blame) {
return function(val) {
if (predicate(val)) {
return val;
} else {
raiseBlame(blame.addExpected(name).addGiven(val));
}
};
});
return c;
}
function addTh {
0 => "0th",
1 => "1st",
2 => "2nd",
3 => "3rd",
(x) => x + "th"
}
function pluralize {
(0, str) => str + "s",
(1, str) => str,
(n, str) => str + "s"
}
function toContract(f) {
return check(f, if f.name then f.name else "custom contract");
}
function fun(domRaw, rngRaw, options) {
var dom = domRaw.map(function(d) {
if (!(d instanceof Contract)) {
if (typeof d === "function") {
return toContract(d);
}
throw new Error(d + " is not a contract");
}
return d;
});
var domStr = dom.map(function (d, idx) {
return options && options.namesStr ? options.namesStr[idx] + ": " + d : d;
}).join(", ");
var domName = "(" + domStr + ")";
var rng = rngRaw;
if (!(rngRaw instanceof Contract)) {
if (typeof rngRaw === "function") {
rng = toContract(rngRaw);
} else {
throw new Error(rng + " is not a contract");
}
}
var rngStr = options && options.namesStr ? options.namesStr[options.namesStr.length - 1] + ": " + rng : rng;
var thisName = options && options.thisContract ? "\n | this: " + options.thisContract : "";
var contractName = domName + " -> " + rngStr + thisName +
(options && options.dependencyStr ? " | " + options.dependencyStr : "");
var c = new Contract(contractName, "fun", function(blame, unwrapTypeVar, projOptions) {
return function(f) {
blame = blame.addParents(contractName);
if (typeof f !== "function") {
raiseBlame(blame.addExpected("a function that takes " +
dom.length + pluralize(dom.length, " argument"))
.addGiven(f));
}
function applyTrap(target, thisVal, args) {
var checkedArgs = [];
var depArgs = [];
for (var i = 0; i < dom.length; i++) {
if (dom[i].type === "optional" && args[i] === undefined) {
continue;
} else {
var location = "the " + addTh(i+1) + " argument of";
var unwrapForProj = dom[i].type === "fun" ? !unwrapTypeVar : unwrapTypeVar;
var domProj = dom[i].proj(blame.swap()
.addLocation(location), unwrapForProj);
checkedArgs.push(domProj(args[i]));
if (options && options.dependency) {
var depProj = dom[i].proj(blame.swap()
.setNeg("the contract of " + blame.name)
.addLocation(location));
depArgs.push(depProj(args[i]));
}
}
}
checkedArgs = checkedArgs.concat(args.slice(i));
var checkedThis = thisVal;
if((options && options.thisContract) || (projOptions && projOptions.overrideThisContract)) {
var thisContract = if projOptions && projOptions.overrideThisContract
then projOptions.overrideThisContract
else options.thisContract
var thisProj = thisContract.proj(blame.swap()
.addLocation("the this value of"));
checkedThis = thisProj(thisVal);
}
assert(rng instanceof Contract, "The range is not a contract");
var rawResult = target.apply(checkedThis, checkedArgs);
var rngUnwrap = rng.type === "fun" ? unwrapTypeVar : !unwrapTypeVar;
var rngProj = rng.proj(blame.addLocation("the return of"), rngUnwrap);
var rngResult = rngProj(rawResult);
if (options && options.dependency && typeof options.dependency === "function") {
var depResult = options.dependency.apply(this, depArgs.concat(rngResult));
if (!depResult) {
raiseBlame(blame.addExpected(options.dependencyStr)
.addGiven(false)
.addLocation("the return dependency of"));
}
}
return rngResult;
}
// only use expensive proxies when needed (to distinguish between apply and construct)
if (options && options.needs_proxy) {
var p = new Proxy(f, {
apply: function(target, thisVal, args) {
return applyTrap(target, thisVal, args);
}
});
return p;
} else {
return function() {
return applyTrap(f, this, Array.prototype.slice.call(arguments));
};
}
};
});
return c;
}
function optional(contract, options) {
if (!(contract instanceof Contract)) {
if (typeof contract === "function") {
contract = toContract(contract);
} else {
throw new Error(contract + " is not a contract");
}
}
var contractName = "?" + contract;
return new Contract(contractName, "optional", function(blame, unwrapTypeVar) {
return function(val) {
var proj = contract.proj(blame, unwrapTypeVar);
return proj(val);
};
});
}
function repeat(contract, options) {
if (!(contract instanceof Contract)) {
if (typeof contract === "function") {
contract = toContract(contract);
} else {
throw new Error(contract + " is not a contract");
}
}
var contractName = "...." + contract;
return new Contract(contractName, "repeat", function(blame, unwrapTypeVar) {
return function (val) {
var proj = contract.proj(blame, unwrapTypeVar);
return proj(val);
};
});
}
function array(arrContractRaw, options) {
var proxyPrefix = options && options.proxy ? "!" : "";
var arrContract = arrContractRaw.map(function(c) {
if (!(c instanceof Contract)) {
if (typeof c === "function") {
return toContract(c);
}
throw new Error(c + " is not a contract");
}
return c;
});
var contractName = proxyPrefix + "[" + arrContract.map(function(c) {
return c;
}).join(", ") + "]";
var contractNum = arrContract.length;
var c = new Contract(contractName, "array", function(blame, unwrapTypeVar) {
return function(arr) {
if (typeof arr === "number" ||
typeof arr === "string" ||
typeof arr === "boolean" || arr == null) {
raiseBlame(blame.addGiven(arr)
.addExpected("an array with at least " +
contractNum + pluralize(contractNum, " field")));
}
for (var ctxIdx = 0, arrIdx = 0; ctxIdx < arrContract.length; ctxIdx++) {
if (arrContract[ctxIdx].type === "repeat" && arr.length <= ctxIdx) {
break;
}
var unwrapForProj = arrContract[ctxIdx].type === "fun" ? !unwrapTypeVar : unwrapTypeVar;
var fieldProj = arrContract[ctxIdx].proj(blame.addLocation("the " +
addTh(arrIdx) +
" field of"),
unwrapForProj);
var checkedField = fieldProj(arr[arrIdx]);
arr[arrIdx] = checkedField;
arrIdx++;
if (arrContract[ctxIdx].type === "repeat") {
if (ctxIdx !== arrContract.length - 1) {
throw new Error("The repeated contract must come last in " + contractName);
}
for (; arrIdx < arr.length; arrIdx++) {
var repeatProj = arrContract[ctxIdx].proj(blame.addLocation("the " +
addTh(arrIdx) +
" field of"),
unwrapForProj);
arr[arrIdx] = repeatProj(arr[arrIdx]);
}
}
}
if (options && options.proxy) {
return new Proxy(arr, {
set: function(target, key, value) {
var lastContract = arrContract[arrContract.length - 1];
var fieldProj;
if (arrContract[key] !== undefined && arrContract[key].type !== "repeat") {
fieldProj = arrContract[key].proj(blame.swap()
.addLocation("the " + addTh(key) +
" field of"));
target[key] = fieldProj(value);
} else if (lastContract && lastContract.type === "repeat") {
fieldProj = lastContract.proj(blame.swap()
.addLocation("the " + addTh(key) +
" field of"));
target[key] = fieldProj(value);
}
}
});
} else {
return arr;
}
};
});
return c;
}
function object(objContract, options) {
var contractKeys = Object.keys(objContract);
contractKeys.forEach(function (prop) {
if (!(objContract[prop] instanceof Contract)) {
if (typeof objContract[prop] === "function") {
objContract[prop] = toContract(objContract[prop]);
} else {
throw new Error(objContract[prop] + " is not a contract");
}
}
});
var proxyPrefix = options && options.proxy ? "!" : "";
var contractName = proxyPrefix + "{" + contractKeys.map(function(prop) {
return prop + ": " + objContract[prop];
}).join(", ") + "}";
var keyNum = contractKeys.length;
var c = new Contract(contractName, "object", function(blame) {
return function(obj) {
if (typeof obj === "number" ||
typeof obj === "string" ||
typeof obj === "boolean" || obj == null) {
raiseBlame(blame.addGiven(obj)
.addExpected("an object with at least " +
keyNum + pluralize(keyNum, " key")));
}
contractKeys.forEach(function(key) {
if (!(objContract[key].type === "optional" && obj[key] === undefined)) {
var propProjOptions = if objContract[key].type === "fun"
then {overrideThisContract: this}
else {}
var c = if objContract[key].type === "cycle"
then objContract[key].cycleContract
else objContract[key];
var propProj = c.proj(blame.addLocation("the " +
key +
" property of"), false, propProjOptions);
var checkedProperty = propProj(obj[key]);
obj[key] = checkedProperty;
}
}.bind(this));
if (options && options.proxy) {
return new Proxy(obj, {
set: function(target, key, value) {
if (objContract.hasOwnProperty(key)) {
var c = if objContract[key].type === "cycle"
then objContract[key].cycleContract
else objContract[key];
var propProj = c.proj(blame.swap()
.addLocation("setting the " +
key + " property of"));
var checkedProperty = propProj(value);
target[key] = checkedProperty;
} else {
target[key] = value;
}
}
});
} else {
return obj;
}
}.bind(this);
});
return c;
}
function reMatch(re) {
var contractName = re.toString();
return check(function(val) {
return re.test(val);
}, contractName);
}
function and(left, right) {
if (!(left instanceof Contract)) {
if (typeof left === "function") {
left = toContract(left);
} else {
throw new Error(left + " is not a contract");
}
}
if (!(right instanceof Contract)) {
if (typeof right === "function") {
right = toContract(right);
} else {
throw new Error(right + " is not a contract");
}
}
var contractName = left + " and " + right;
return new Contract(contractName, "and", function(blame) {
return function(val) {
var leftProj = left.proj(blame.addExpected(contractName, true));
var leftResult = leftProj(val);
var rightProj = right.proj(blame.addExpected(contractName, true));
return rightProj(leftResult);
};
})
}
function or(left, right) {
if (!(left instanceof Contract)) {
if (typeof left === "function") {
left = toContract(left);
} else {
throw new Error(left + " is not a contract");
}
}
if (!(right instanceof Contract)) {
if (typeof right === "function") {
right = toContract(right);
} else {
throw new Error(right + " is not a contract");
}
}
var contractName = left + " or " + right;
return new Contract(contractName, "or", function(blame) {
return function(val) {
try {
var leftProj = left.proj(blame.addExpected(contractName, true));
return leftProj(val);
} catch (b) {
var rightProj = right.proj(blame.addExpected(contractName, true));
return rightProj(val);
}
};
});
}
function cyclic(name) {
return new Contract(name, "cycle", function() {
throw new Error("Stub, should never be called");
});
}
function guard(contract, value, name) {
var proj = contract.proj(Blame.create(name,
"function " + name,
"(calling context for " + name + ")"));
return proj(value);
}
return {
Num: check(function(val) { return typeof val === "number"; }, "Num"),
Str: check(function(val) { return typeof val === "string"; }, "Str"),
Bool: check(function(val) { return typeof val === "boolean"; }, "Bool"),
Odd: check(function(val) { return (val % 2) === 1; }, "Odd"),
Even: check(function(val) { return (val % 2) !== 1; }, "Even"),
Pos: check(function(val) { return val >= 0; }, "Pos"),
Nat: check(function(val) { return val > 0; }, "Nat"),
Neg: check(function(val) { return val < 0; }, "Neg"),
Any: check(function(val) { return true; }, "Any"),
None: check(function(val) { return false; }, "None"),
Null: check(function(val) { return null === val; }, "Null"),
Undefined: check(function(val) { return void 0 === val; }, "Null"),
Void: check(function(val) { return null == val; }, "Null"),
check: check,
reMatch: reMatch,
fun: fun,
or: or,
and: and,
repeat: repeat,
optional: optional,
object: object,
array: array,
cyclic: cyclic,
Blame: Blame,
makeCoffer: makeCoffer,
guard: guard
};
})();