Fast and boilerplate-free precondition checks for javascript.
npm install offensive[npm-url]: https://npmjs.org/package/offensive
[npm-image]: https://badge.fury.io/js/offensive.svg
[![NPM version][npm-image]][npm-url]
> A human-readable, fast and boilerplate-free contract programming library
for JavaScript.
Why would I want it?
1. It reduces the boilerplate of writing assertion messsages to zero,
1. Provides very intuitive and extensible DSL for writing assertions,
1. Low core bundle size (22.5kB minified) and a way of bundling only needed assertions,
1. Has zero runtime dependencies which greatly increases package security,
1. It's TypeScript-friendly (contains its own .d.ts files).
``shell`
npm install --save offensive
`js
// node-style require
const { contract } = require('offensive');
// or (with all assertions pre-loaded)
const { contract } = require('offensive/all');
// es6-style default import
import contract from 'offensive';
// or (with all assertions pre-loaded)
import contract from 'offensive/all';
`
In order to minimize the bundle payload, each assertion can be imported
separately, either during application bootup or in each file where the specific
assertions are used. The same assertion can be safely imported multiple times
(importing an assertion second time is a no op).
`js
// node-style require
require('offensive/assertions/aString/register');
// es6-style import
import 'offensive/assertions/aString/register';
`
When using the library on server-side, the bundle size is typically of no
concern. For those situations, the library supports loading all assertions at
once.
`js
import 'offensive/assertions/register';
import { contract } from 'offensive';
// or even shorter
import { contract } from 'offensive/all';
`
###
Programming offensively is about throwing exceptions a lot. As soon
as corrupted state or illegal parameter is detected, program is crashed
with a descriptive error message. This technique greatly helps in finding
bugs at their cause.
`js
import 'offensive/assertions/fieldThat/register';
import 'offensive/assertions/aNumber/register';
import contract from 'offensive';
class Point2D {
/**
* @param init initializer object containing x and y properties.x
*/
constructor(init) {
// Contract is satisfied if init contains
// and y property of type number.`
contract('init', init)
.has.fieldThat('x', x => x.is.aNumber)
.and.fieldThat('y', y => y.is.aNumber)
.check();
this.x = init.x;
this.y = init.y;
}
}`
Now, following erroneus call...js`
const point = new Point2D({ x: 'a', y: null });`
...will result in throwing following exception.`
ContractError: init.x must be a number (got 'a') and init.y be a number (got null)
at operatorContext (offensives/ContextFactory.js:34:33)
at new Point2D (example.js:16:7)
at Object.
Alternatively, above contract could be implemented using multiple checks, but
the error would only contain information about first failed check.
`js`
contract('init', init).is.anObject.check();
contract('init.x', init.x).is.aNumber.check();
contract('init.y', init.y).is.aNumber.check();
Above examples use only [.anObject][object], [.aNumber][number].fieldThat
and [][field-that] assertions.
[See full list of offensive.js built-in assertions][assertions].
Offensive programming is not applicable when collaborating with
external components. A program should not crash in response
to a bug in another program. Logging an error and trying to correct
it by using default value or simply ignoring erroneus input
would be a better way of handling such cases.
#### Ping Server
Following example is a fully functional HTTP-based ping server
implemented using [express.js][express] with defensive checks
on HTTP request implemented using [offensive.js][offensive].
[express]: https://github.com/expressjs/express
[offensive]: https://github.com/mchalapuk/offensive.js
`js
import * as express from 'express';
import * as bodyParser from 'body-parser';
import 'offensive/assertions/aString/register';
import 'offensive/assertions/fieldThat/register';
import contract from 'offensive';
const app = express();
app.use(bodyParser.json());
// A simple ping service which reflects messages sent to it.
app.post('/ping', function (req, res, next) {
// Contract is satisfied if body has a message which is a string
// (.propertyThat is an alias of .fieldThat assertion)
const error = contract('req.body', req.body)
.contains.propertyThat('message', message => message.is.aString)
.getError();
if (error) {
res.status(400).json({ error });
return;
}
const { message } = body;
res.json({ message });
});
`
Above code presents defensive programming on the server side, but the same
technique is applicable in the client. Client-server contract should be tested
both, after receiving request from the client, and after receiving response
from the server.
Table of Contents
1. [Contract Function][contract-function]
1. [.check()][check]
1. [.getError()][get-error]
1. [Assertions][assertions]
1. [Boolean Operators][operators]
1. [Legacy Call Operator][legacy-call-operator]
[contract-function]: #contract-function
js
function contract(varName : string, testedValue : T) : AssertionBuilder;
`
Creates an instance of AssertionBuilder. [Methods of returned
instance][assertions] add assertions to the builder. Requested assertions
will be checked against given testedValue after [executing assertion
expression][call-operator]. In case some assertions fail, given name
will be used as part of error message.
`js
import contract from 'offensive';
...contract('arg', arg)...
`$3
`js
interface AssertionBuilder {
check(errorName?: string = 'ContractError') : T;
}
`
Executes built assert expression. Returns testedValue if assertion succeeds.
Throws ContractError in case it fails. intended for offensive programming.
`js
import 'offensive/assertions/length';
import contract from 'offensive';contract('arg', arg)
.has.length(10)
.check(); // <- executes built assert expression
`
NOTE: Assertion will not be run unless this method or .getError() is invoked.$3
`js
interface AssertionBuilder {
getError(errorName?: string = 'ContractError') : string | null;
}
`
Executes built assert expression. Returns error message if assertion fails.
Returns null in case it succeeds. Intended for defensive programming.`js
import 'offensive/assertions/length';
import contract from 'offensive';const error = contract('arg', arg)
.has.length(10)
.getError(); // <- executes built assert expression
`
NOTE: Assertion will not be run unless this method or .check() is invoked.[assertions]: #assertions
$3
offensive.js contains following built-in assertions.
Table of Contents
1. [
.Null][null]
1. [.Undefined][undefined]
1. [.Empty][empty]
1. [.ofType(requiredType)][of-type]
1. [.aBoolean][boolean]
1. [.aNumber][number]
1. [.anInteger][integer]
1. [.aString][string]
1. [.anObject][object]
1. [.aFunction][function]
1. [.anArray][array]
1. [.anInstanceOf(RequiredClass)][instance-of]
1. [.aDate][date]
1. [.aRegExp][regexp]
1. [.True][true]
1. [.False][false]
1. [.truthy][truthy]
1. [.falsy][falsy]
1. [.matches(regexp)][matches]
1. [.anEmail][email]
1. [.aUUID][uuid]
1. [.anEmptyString][empty-string]
1. [.aNonEmptyString][non-empty-string]
1. [.anIntegerString][integer-string]
1. [.startsWith(substring)][starts-with]
1. [.endsWith(substring)][ends-with]
1. [.substring(substring)][substring]
1. [.equalTo][equal-to]
1. [.exactly][exactly]
1. [.lessThan(rightBounds)][less-than]
1. [.lessThanOrEqualTo(rightBounds)][less-than-or-equal-to]
1. [.greaterThan(leftBounds)][greater-than]
1. [.greaterThanOrEqualTo(leftBounds)][greater-than-or-equal-to]
1. [.inRange(leftBounds, rightBounds)][in-range]
1. [.before(rightBounds, boundsVarName?)][before]
1. [.after(leftBounds, boundsVarName?)][after]
1. [.field(fieldName)][field]
1. [.fieldThat(fieldName, condition)][field-that]
1. [.allFieldsThat(condition)][all-fields-that]
1. [.method(methodName)][method]
1. [.length(requiredLength)][length]
1. [.oneOf(set, name)][one-of]
1. [.elementThat(index, assertName, condition)][element-that]
1. [.allElementsThat(assertName, condition)][all-elements-that]
1. [.includes(element)][includes]
1. [.includesAllOf(element)][includes-all-of]
1. [.includesElementThat(condition)][includes-element-that].Null aliases: .null, .Nil, .nil
Asserts that checked value is null using ===.
Typically used in combination with [.not][not] operator.
`js
contract('arg', arg).is.not.Null.check();
`.Undefined aliases: .undefined
Asserts that checked value is undefined.
Typically used in combination with [.not][not] operator.
`js
contract('arg', arg).is.not.Undefined.check();
`.Empty aliases: .empty
Asserts that checked value is null or undefined.
Typically used in combination with [.not][not] operator.
`js
contract('arg', arg).is.not.Empty.check();
`.ofType(requiredType : string) aliases: .type
Asserts that checked value is of requiredType by ivoking typeof operator.
`js
contract('arg', arg).is.ofType('boolean').check();
`.aBoolean aliases: .Boolean, .boolean
Asserts that checked value is a boolean by ivoking typeof operator.
`js
contract('arg', arg).is.aBoolean.check();
`.aNumber aliases: .Number, .number
Asserts that checked value is a number by ivoking typeof operator.
`js
contract('arg', arg).is.aNumber.check();
`.anInteger aliases: .Integer, .anInt, .int
Asserts that checked value is an integer by ivoking Number.isInteger.
`js
contract('arg', arg).is.anInteger.check();
`.aString aliases: .String, .string
Asserts that checked value is a string by ivoking typeof operator.
`js
contract('arg', arg).is.aString.check();
`.anObject aliases: .Object, .object
Asserts that checked value is an object by ivoking typeof operator.
Be wary that this will be true also for array instances and null.
Use [.anArray][array] and [.Null][null] in order to test for these
specific cases.
`js
contract('arg', arg).is.anObject.check();
`.aFunction aliases: .Function, .function
Asserts that checked value is a function by ivoking typeof operator.
`js
contract('arg', arg).is.aFunction.check();
`.anArray aliases: .Array, .array
Asserts that checked value is an array by invoking Array.isArray.
`js
contract('arg', arg).is.anArray.check();
`.anInstanceOf(RequiredClass : Function) aliases: .instanceOf
Asserts that checked value is a instance of RequiredClass, by
using instanceof operator.
`js
contract('arg', arg).is.anInstanceOf(RegExp).check();
`.aDate aliases: .Date, .date
Asserts that checked value is a instance of Date, by
using instanceof operator.
`js
contract('arg', arg).is.aDate.check();
`.aRegExp aliases: .RegExp, .regexp
Asserts that checked value is a instance of RegExp, by
using instanceof operator.
`js
contract('arg', arg).is.aRegExp.check();
`.True aliases: .true
Asserts that checked value is a boolean of value true.
`js
contract('arg', arg).is.True.check();
`.False aliases: .false
Asserts that checked value is a boolean of value false.
`js
contract('arg', arg).is.False.check();
`.truthy aliases: .Truthy, .truethy, .Truethy
Asserts that checked value is truthy (converts to true).
`js
contract('arg', arg).is.truthy.check();
`.falsy aliases: .Falsy, .falsey, .Falsey
Asserts that checked value is falsy (converts to false).
`js
contract('arg', arg).is.falsy.check();
`.matches(regexp : RegExp) aliases: .matchesRegexp, .matchesRegExp
Asserts that checked value fully matches given regexp.
`js
contract('arg', arg).matches(/[a-z]+/).check();
`.anEmail aliases: .Email, .email
Asserts that checked value is a valid email.
`js
contract('arg', arg).is.anEmail();
`.aUUID aliases: .UUID
Asserts that checked value is a valid UUID.
`js
contract('arg', arg).is.aUUID();
`.anEmptyString aliases: .emptyString
Asserts that checked value is an empty string (string of length 0).
`js
contract('arg', arg).is.anEmptyString.check();
`.aNonEmptyString aliases: .nonEmptyString
Asserts that checked value is an non-empty string (string of length > 0).
`js
contract('arg', arg).is.aNonEmptyString.check();
`.anIntegerString aliases: .IntegerString, .intString
Asserts that checked value is a valid string form of an integer.
`js
contract('arg', arg).is.anIntegerString.check();
`.startsWith(substring : string) aliases: .startWith, .startingWith
Asserts that checked value is a string that starts with given substring.
`js
contract('arg', arg).is.startsWith('abc').check();
`.endsWith(substring : string) aliases: .endWith, .endingWith
Asserts that checked value is a string that ends with given substring.
`js
contract('arg', arg).is.endsWith('xyz').check();
`.substring(substring : string) aliases: .substr
Asserts that checked value is a string that is contains given substring.
`js
contract('arg', arg).has.substring('xyz').check();
`.equalTo(another : any) aliases: .equal, .equals
Asserts that checked value is equal to another.
Comparison is made with == (double equals) operator.
`js
contract('arg', arg).is.equalTo(100).check();
`.exactly(another : any)
Asserts that checked value is exactly the same as another.
Comparison is made with === (triple equals) operator.
`js
contract('arg', arg).is.exactly(instance).check();
`.lessThan(rightBounds : number) aliases: .lt, .less
Asserts that checked value is less than rightBounds.
`js
contract('arg', arg).is.lessThan(100).check();
`[less-than-or-equal-to]: #less-than-or-equal-to-assertion
####
.lessThanOrEqualTo(rightBounds : number) aliases: .lte, .lessThanEqual
Asserts that checked value is less than or equal to rightBounds.
`js
contract('arg', arg).is.lessThanOrEqualTo(100).check();
`.greaterThan(leftBounds : number) aliases: .gt, .greater
Asserts that checked value is greater than leftBounds.
`js
contract('arg', arg).is.greaterThan(0).check();
`[greater-than-or-equal-to]: #greater-than-or-equal-to-assertion
####
.greaterThanOrEqualTo(leftBounds : number) aliases: .gte, .greaterThanEqual
Asserts that checked value is greater than or equal to leftBounds.
`js
contract('arg', arg).is.greaterThanOrEqualTo(0).check();
`.inRange(leftBounds : number, rightBounds : number) aliases: .between
Asserts that checked value is grater than or equal to leftBounds
and less than rightBounds.
`js
contract('arg', arg).is.inRange(0, 100).check();
`.before(rightBounds : Date, boundsVarName ?: string)
Asserts that checked value a Date chronologically before rightBounds.
`js
contract('arg', arg).is.before(new Date(0), 'Epoch').check();
`.after(leftBounds : Date, boundsVarName ?: string)
Asserts that checked value a Date chronologically after leftBounds.
`js
contract('arg', arg).is.after(new Date(0), 'Epoch').check();
`.field(fieldName : string) aliases: .property
Asserts that checked value has field of name propertyName.
`js
contract('arg', arg).has.property('length').check();
`.fieldThat(fieldName : string, builder : FieldAssertionBuilder)Asserts that checked value has field of name propertyName, which satisfied
assertion created in gived builder.
`js
contract('arg', arg)
.has.propertyThat('x', x => x.is.aNumber)
.check();
`.allFieldsThat(builder : FieldAssertionBuilder)
Asserts that:
1. Checked value is not null or undefined,
2. Value of each field of this object satisfies assertuin created
by given builder.`js
contract('arg', arg)
.has.allFieldsThat(field => field.is.aNumber)
.check();
`.method(methodName : string)
Asserts that checked value has field of name methodName which is a function.
`js
contract('arg', arg).has.method('toString').check();
`.length(requiredLength : number) aliases: .len
Asserts that checked value has property of name "length" and value
of requiredLength.
`js
contract('arg', arg).has.length(0).check();
`.oneOf(set : any[], name ?: string) aliases: .elementOf, .containedIn
Asserts that checked value is contained in given set. Given name (if
present) is used as a name of set in produced error message.
`js
contract('arg', arg)
.is.oneOf([ 'started', 'running', 'finished' ])
.check();// or (with set name used in the error message)
contract('arg', arg)
.is.oneOf([ 'started', 'running', 'finished' ], 'valid status')
.check();
`.elementThat(index : number, builder : ElemAssertionBuilder) aliases: .elementWhichIs
Asserts that:
1. Checked value is an array of length at least index + 1,
2. Element under index satisfies assertion created by given builder.
`js
contract('arg', arg)
.has.elementThat(0, elem => elem.is.anInteger)
.check();
`.allElementsThat(builder : ElemAssertionBuilder) aliases: .allElementsWhich
Asserts that:
1. Checked value is an array,
2. Each element of this array satisfies assertion created by given builder.`js
contract('arg', arg)
.has.allElementsThat(elem => elem.is.anInteger)
.check();
`.includes(element : any) aliases: .contains
Asserts that:
1. Checked value is an array,
2. The array contains given element.`js
contract('arg', arg)
.has.includes(elem)
.check();
`.includesAllOf(elements : any[]) aliases: .includesAll
Asserts that:
1. Checked value is an array,
2. The array contains all elements of elements.`js
contract('categories', categories)
.has.includesAlOf(['functional', 'performance'])
.check();
`[includes-element-that]: #includes-element-that-assertion
####
.includesElementThat(builder: ElemAssertionBuilder) aliases: .includesElement
Asserts that:
1. Checked value is an array,
2. The array contains at least one element that satisfies assertion created by
given builder.`js
contract('arg', arg)
.includesElementThat(elem => elem.is.anInteger)
.check();
`[operators]: #boolean-operators
$3
offensive.js implements following operators.
Table of Contents
1. [
.and][and]
1. [.or][or]
1. [.not][not].and aliases: .of, .with
Logical conjunction of two boolean values which are separated by call to .and
operator.
`js
contract('arg', arg)
.has.length(2)
.and.allElementsThat(elem => elem.is.aNumber)
.check();
`.or()
Logical alternative of two (or more) values which are separated by call to .or
operator.
`js
contract('arg', arg)
.is.anObject.or.aFunction
.check();
`.not aliases: .no, .dont, .doesnt
Logical negation of an assertion after .not operator.
`js
contract('arg', arg).is.not.Undefined.check();
`[legacy-call-operator]: #legacy-call-operator
$3
`js
interface AssertionBuilder {
() : T;
}
`
Alias for [.check()][check].`js
import 'offensive/assertions/aString';
import contract from 'offensive';contract('arg', arg).is.aString.check(); // <- executes the expression
contract('arg', arg).is.aString(); // <- the same but with a call operator
`The call operator was the only way to execute an offensive expression until
version 2. Initially, it was seen as an elegant API with the least amount of
boilerplate possible. While this is true for all assertions without arguments,
assertions with arguments have their own call operator. This led to situations
where two consecutive call operators were needed in order to execute
the expression.
`js
import 'offensive/assertions/length';
import contract from 'offensive';contract('arg', arg).has.length(3)(); // <- double call operators weirdness
contract('arg', arg).has.length(3).check(); // <- this looks much better
`[
.check()][check] (introduced in version 3) solves the problem
of readability and adds a bit of explicitness at the cost of a little bit more
code. The call operator is still supported for backwards compatilibity.Extension API
offensive.js is extensible, but extension API is not documented yet.
If you wish to write an extension, take a look at the implementation
of [built-in assertions][assertions-code], [operators][operators-code]
and also at the interface of [
Registry`][registry-code] class.[assertions-code]: src/assertions
[operators-code]: src/operators
[registry-code]: src/Registry.ts
Released under MIT license.