icc-contracts
v0.1.0
Published
Code-contract library for javascript. Supports pre-conditions.
Downloads
8
Readme
🦔 Integrated Code Contracts
icc-contracts, mostly invisible confidence
⚠ Warning
This repository is currently an experimental concept and is subject to heavy change. Not recommended for general use.
Overview
In general, code-contracts allow the specification of pre-conditions and post-conditions.
icc-contracts focuses on providing pre-conditions in pure JavaScript.
Motivation
But why? Are unit-tests not sufficient?
Well written unit-tests cover 100% of the functionality of the unit. Unit-tests don't cover the way that the unit is integrated into the product. For that we have IntegrationTests. However, when you combine your units and something doesn't work, how do you know what went wrong? Code-contracts to the rescue! Code-contracts help you quickly pinpoint the failing interaction. Since they also document the expectations, they help you understand how to resolve the problem too.
icc-contracts
Icc is a way of writing code-contract pre-conditions as JavaScript.
They run when the code does, providing guard rails as you develop. In other words, making sure the input is in the format you expected.
It must be assumed that, given correct input, your code will produce the expected output. If you aren't sure of that, add more unit tests!
Public Only
If you've written good unit-tests, then you are sure the unit is doing the correct thing. That is, it is internally consistent and meets the specifications. Therefore, the only time things can go wrong is when incorrect input is provided. Given that only public methods can provide input, these are the only ones adding uncertainty. So add code-contracts to your public methods (only).
Performance
Icc-contracts always run when the public methods do Clearly this means there is a performance cost to pay. My opinion is: the performance cost is fine when testing, it is not acceptable in production. So, icc is designed to make it super easy to completely eliminate this overhead in production code as part of your build. If you're a Rollup user, simply run the @rollup/plugin-strip to remove all icc: labeled code.
Installing
Requires Node 14 or higher.
At the command line run
npm install --save icc-contracts
Within your source files you need to add:
For es6 projects:
import contract from 'icc-contracts';
For CommonJS projects:
const contract = require('icc-contracts');
Usage Explanation
Given a (stupidly) simple public method
function squareRoot(n) {
// implementation
}
we can intuit that it should be taking a number. Since it's well named, we can also predict it will return the square root of that number. What will it do when it gets a negative number?
One option would be to "just do something" and carry on. Math.sqrt chose to return NaN.
A safer option might be to reject the negative input.
function squareRoot(n) {
if(n < 0) throw new Error("n must be positive");
// implementation
}
That, right there, is a code-contract pre-condition. We write them all the time. Let's not forget that we probably want to be sure we are given a number too:
function squareRoot(n) {
if('number' !== typeof n) throw new Error(`'n' should be a 'number' but was '${typeof n}'`);
if(n < 0) throw new Error(`'n' should be positive but was '${n}'`);
// implementation
}
It's getting tedious already, and this is a simple function.
How would this look using icc-contracts?
import contract from 'icc-contracts';
function squareRoot(n) {
icc:contract(is=>[[{n}, is.number, is.positive]]);
// implementation
}
Same readable errors, same verification, much more readable. The expressive pattern also reduces the chance for simple typos to trip you up.
And it is expressed as a single JavaScript labelled-statement, which means we can easily remove it during our build process. Something that would be nigh on impossible to do with the guards in the bare code version :)
Multiple arguments
It isn't any harder to handle multiple arguments.
import contract from 'icc-contracts';
function func(n, p) {
icc:contract(is=>[
[{n}, is.number, is.positive],
[{p}, is.string, is.notEmpty],
]);
// implementation
}
Objects
So far, so simple. But of course, most parameters are not simple native types. So what to do with objects?
No problem, the 'with' condition allows us to apply all the same logic to individual members of objects too.
import contract from 'icc-contracts';
function func(args) {
icc:contract(is=>[
[{args}, is.with({
n: [is.number, is.positive],
p: [is.string, is.notEmpty],
})],
]);
// implementation
}
Optionals
Ok, ok, but what about those times when we can reasonably accept two different things through the same argument, like optional parameters? Simple, there's a condition for that too.
import contract from 'icc-contracts';
function func(arg) {
icc:contract(is=>[
[{arg}, is.either([
[is.undefined],
[is.string, is.notEmpty],
])],
]);
// implementation
}
Conditions
Note: 'argument' refers to the argument of the method for which you are specifying the contract.
Until now, is a limited list of conditions that can be applied to arguments. I'm happy to take suggestions for new ones.
|type|condition|description|
|----|---------|-----------|
|is.undefined | | checks if the argument is undefined|
|is.number | | checks if the argument is a number |
| |is.positive | checks if an argument is a positive|
| |is.negative | checks if an argument is a negative|
| |is.greaterThan(v) | checks if an argument is > v|
| |is.lessThan(v) | checks if an argument is < v|
| |is.inRange(inclusiveBegin, inclusiveEnd) | checks if an argument is >= inclusiveBegin && <= inclusiveEnd|
|is.string | |checks if the argument is a string|
| |is.notEmpty | checks if the argument is not ''|
|is.object | |checks if the argument is an object|
| |is.instanceof(type)| checks if the argument is an instance of the specfied type|
| |is.with(obj) |'obj' must have a member for each desired member on the argument. Each member must consist of an Array of conditions.e.g.{a:[is.number], b:[is.string, is.notEmpty]}
Only check the properties you care about, the argument may well contain additional properties which your method can safely ignore. |
|is.either([...])| |takes an array of condition arrays. If any of the arrays pass, either passes. If none of them pass, a compound error of all the failures is thrown.|