Last active
July 25, 2017 02:53
-
-
Save Colouratura/fb55b0bdef7e4593ef4057e4bdfb972a to your computer and use it in GitHub Desktop.
A contract library for JS supporting pre- and post-conditions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example contract
Assuming we have a function that expects two even
number
s as input and is expected to output anumber
that is divisible by16
we could write a contract as follows.