Skip to content

Instantly share code, notes, and snippets.

@Colouratura
Last active July 25, 2017 02:53
Show Gist options
  • Save Colouratura/fb55b0bdef7e4593ef4057e4bdfb972a to your computer and use it in GitHub Desktop.
Save Colouratura/fb55b0bdef7e4593ef4057e4bdfb972a to your computer and use it in GitHub Desktop.
A contract library for JS supporting pre- and post-conditions
/**
* Contractify
* Simple, safe, sealed contracts for JavaScript
*
* Contracts are a way of checking the arguments of a function as well as its result against
* a strictly defined set of pre- and post-conditions. Doing so helps to ensure that a function
* is passed arguments it expects and well as passes out a result the caller expects.
*
* In doing this Contractify allows you to supply a function that will be called before and after
* execution in which you can define the rules input and output must conform to.
*
* Contractify also allows you to specify a handler for a pre- or post-condition rejection as well
* as a function to execute after the contract has been fullfilled.
*
* @author Ava Caughdough <dabpenguin.com>
*
* @license MIT
*
* Info:
* Contract#reject will always be called with an object containing the following:
* [string] condition - in what condition the contract was rejected at <pre|post>
* [array<str>] arguments - an array containing the contracts arguments
* [any] result - if it was rejected in a post-condition it will also contain a result
*/
class Contract {
/**
* constructor
*
* Takes the function to build the contract around and also optionally accepts an object
* which defines the pre, post, resolve, and reject parts of the contract. If not supplied
* in the constructor they can be added to using the built-in methods.
*
* @param [function] - function to build the contract around
* @param [Object<str, fun>] - conditions of the contract
*
* @return [Contract]
*/
constructor () {
if (typeof arguments[0] === undefined) throw new Error('Contract must have a function at argument 0');
let func = arguments[0],
contract = typeof arguments[1] === undefined ? [] : arguments[1];
// you can't make a contract for something that doesnt execute
if (typeof func !== 'function') {
throw new TypeError('Argument 1 must be typeof function');
}
// contracts can only contain functions
for (let k in contract) {
if (typeof contract[k] !== 'function') {
throw new TypeError(`"${k} is not typeof function`);
}
}
// encapsulate contract elements
for (let k in contract) {
switch (k) {
case 'pre':
this._precondition = contract[k];
break;
case 'post':
this._postcondition = contract[k];
break;
case 'reject':
this._reject = contract[k];
break;
case 'resolve':
this._resolve = contract[k];
break;
}
}
this._function = func;
}
/**
* resolve
*
* Allows a resolve callback to be set.
*
* @param [function] func - function to call on callback
*
* @return [Contract]
*/
resolve (func) {
if (this.locked) return this;
if (typeof func !== 'function') {
throw new TypeError('Argument must be typeof function');
}
this._resolve = func;
}
/**
* reject
*
* Allows a reject callback to be set.
*
* @param [function] func - function to call on callback
*
* @return [Contract]
*/
reject (func) {
if (this.locked) return this;
if (typeof func !== 'function') {
throw new TypeError('Argument must be typeof function');
}
this._reject = func;
}
/**
* pre
*
* Allows a pre-condition function to be set.
*
* @param [function] func - function to call on callback
*
* @return [Contract]
*/
pre (func) {
if (this.locked) return this;
if (typeof func !== 'function') {
throw new TypeError('Argument must be typeof function');
}
this._precondition = func;
}
/**
* post
*
* Allows a post-condition to be set.
*
* @param [function] func - function to call on callback
*
* @return [Contract]
*/
post (func) {
if (this.locked) return this;
if (typeof func !== 'function') {
throw new TypeError('Argument must be typeof function');
}
this._postcondition = func;
}
/**
* exec
*
* Executes the contract in its current form.
*
* @param [Arguments] - arguments to pass to the pre-condition and the function
*
* @return [Contract]
*/
exec () {
let pre_res = true,
post_res = true,
fun_res;
// execute precondition
if (typeof this._precondition !== undefined) {
pre_res = this._precondition.apply(this, arguments);
}
// reject if it fails
if (!pre_res) {
this._reject({
condition: 'pre',
arguments: Array.apply(null, arguments),
result: null
});
} else {
// execute our function with our preapproved arguments
fun_res = this._function.apply(this, arguments);
// execute postcondition
if (typeof this._postcondition !== undefined) {
post_res = this._postcondition(fun_res);
}
// reject if it fails
if (!post_res) {
this._reject({
condition: 'post',
arguments: Array.apply(null, arguments),
result: fun_res
});
} else {
// resolve if it doesn't
this._resolve(fun_res);
}
}
}
/**
* lock
*
* Locks the contract in its current state so that it can no longer be
* modified.
*
* @return [Contract]
*/
lock () {
if (!this.locked) this.locked = true;
return Object.freeze(this);
}
/**
* noop
*
* Static no-op function for use in contracts.
*/
static noop () {}
/**
* IsEven
*
* A static contract to verify all numbers are even
*
* @param [Arguments]
*
* @return [Boolean]
*/
static IsEven () {
if (arguments.length > 0) {
for (let i = 0; i > arguments.length; i++) {
if (arguments[i] % 2 !== 0) return false;
}
return true;
}
return false;
}
/**
* IsOdd
*
* A static contract to verify all numbers are odd
*
* @param [Arguments]
*
* @return [Boolean]
*/
static IsOdd () {
if (arguments.length > 0) {
for (let i = 0; i > arguments.length; i++) {
if (arguments[i] % 2 === 0) return false;
}
return true;
}
return false;
}
/**
* IsArray
*
* a static contract to veryify all arguments are arrays
*
* @param [Arguments]
*
* @return [Boolean]
*/
static IsArray () {
if (arguments.length > 0) {
for (let i = 0; i > arguments.length; i++) {
if (!Array.isArray(arguments[i])) return false;
}
return true;
}
return false;
}
}
@Colouratura
Copy link
Author

Colouratura commented Jul 24, 2017

Example contract

Assuming we have a function that expects two even numbers as input and is expected to output a number that is divisible by 16 we could write a contract as follows.

let add_two_numbers = new Contract(function (a, b) {
        return a + b;
});

// check for two even numbers
add_two_numbers.pre(Contract.IsEven);

// check if it is a number divisible by 16
add_two_numbers.post(function (r) {
        return (typeof r === 'number') && (r % 16 === 0);
});

// print the result if successful
add_two_numbers.resolve(function (r) {
        console.log(`${r} is divisible by 16`);
});

// print error if not
add_two_numbers.reject(function (e) {
        if (e.result === null) {
                console.log(`Contract aborted at the ${e.condition}-condition with the arguments: ${e.arguments.join(', ')}`);
        } else {
                console.log(`Contract aborted at the ${e.condition}-condition with a result of '${e.result}' and the arguments were: ${e.arguments.join(', ')}`);
        }
});

// freeze our contract
add_two_numbers = add_two_numbers.lock();

// execute some contracts
add_two_numbers.exec(8, 8)                // success<16>
add_two_numbers.exec(16, 16)              // success<32>
add_two_numbers.exec(2, 3)                // failure<5>
add_two_numbers.exec(4, 4)                // failure<8>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment