icc-contracts

0.1.0 • Public • Published

🦔 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.

Meta

LICENSE (Mozilla Public License)

Readme

Keywords

Package Sidebar

Install

npm i icc-contracts

Weekly Downloads

2

Version

0.1.0

License

MIT

Unpacked Size

31.5 kB

Total Files

5

Last publish

Collaborators

  • dgkimpton