Have ideas to improve npm?Join in the discussion! »

    offensive
    TypeScript icon, indicating that this package has built-in type declarations

    2.7.40 • Public • Published

    offensive 👊 js

    Build Status Dependency Status NPM version devDependency Status

    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,
    2. Provides very intuitive and extensible DSL for writing assertions,
    3. Low core bundle size (22.5kB minified) and a way of bundling only needed assertions,
    4. Has zero runtime dependencies which greatly increases package security,
    5. It's TypeScript-friendly (contains its own .d.ts files).

    Installation

    npm install --save offensive

    Loading The Library

    // node-style require
    const { check } = require('offensive');
    
    // es6-style default import
    import check from 'offensive';

    Loading Assertions

    In order to minimize bundle payload, each assertion must be imported separately. It can be done during application bootup or in each file where specific assertion is used (importing an assertion multiple times is harmless).

    // node-style require
    require('offensive/assertions/aString/register');
    
    // es6-style default import
    import 'offensive/assertions/aString/register';

    Usage Examples

    Precondition Checks A.K.A. Offensive Programming

    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.

    import 'offensive/assertions/fieldThat/register';
    import 'offensive/assertions/aNumber/register';
    
    import check from 'offensive';
    
    class Point2D {
      /**
       * @param init initializer object containing `x` and `y` properties.
       */
      constructor(init) {
        // Contract is satisfied if init contains
        // `x` and `y` property of type number.
        check(init, 'init')
          .has.fieldThat('x', x => x.is.aNumber)
          .and.fieldThat('y', y => y.is.aNumber)
          ();
        this.x = init.x;
        this.y = init.y;
      }
    }

    Now, following erroneus call...

    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.<anonymous> (example.js:22:15)
    

    Alternatively, above contract could be implemented using multiple checks, but the error would only contain information about first failed check.

    check(init, 'init').is.anObject();
    check(init.x, 'init.x').is.aNumber();
    check(init.y, 'init.y').is.aNumber();

    Above examples use only .anObject, .aNumber and .fieldThat assertions.

    See full list of offensive.js built-in assertions.

    Defensive Programming

    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 with defensive checks on HTTP request implemented using offensive.js.

    import * as express from 'express';
    import * as bodyParser from 'body-parser';
    
    import 'offensive/assertions/aString/register';
    import 'offensive/assertions/fieldThat/register';
    import check 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) {
      try {
        // Contract is satisfied if body has a message which is a string
        // (.propertyThat is an alias of .fieldThat assertion)
        check(req.body, 'req.body')
          .contains.propertyThat('message', message => message.is.aString)();
    
        const { message } = body;
        res.json({ message });
    
      } catch (e) {
        // In case contract is not satisfied, an instance
        // of ContractError will be passed to next middleware.
        next(e);
      }
    });
    
    // Error handling middleware.
    app.use(function (err, req, res, next) {
    
      // Failed offensive.js assertions can be easily differentiated
      // from other errors by checking error name.
      switch (err.name) {
        case 'ContractError':
          // In case its an assertion from offensive.js
          // HTTP status which indicates a client error is apropriate.
          res.status(400);
          // Could also be HTTP 412 Precondition Failed
          // in case there's a need of being more specific.
    
          // It's safe to reveil error message in response
          // as it doesn't contain information about the contract
          // and not about the implementation.
          const { name, message } = err;
          res.json({ error: `${name}: ${message}` });
    
          break;
        default:
          // Any other error will result in HTTP 500 Internal Server Error.
          res.status(500);
          res.json({ 'error': 'InternalServerError: ${err.name}' });
          break;
      }
    });

    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.

    API Reference

    Table of Contents

    1. Check Function
    2. Call Operator
    3. Assertions
    4. Boolean Operators

    Check Function

    function check<T>(testedValue : T, varName : string) : AssertionBuilder<T>;

    Creates an instance of AssertionBuilder. Methods of returned instance add assertions to the builder. Requested assertions will be checked against given testedValue after executing assertion expression. In case some assertions fail, given name will be used as part of error message.

    import check from 'offensive';
    ...
    
    check(arg, 'arg')...

    Call Operator

    interface AssertionBuilder<T> {
      () : T;
    }

    Executes built assert expression. Returns testedValue if assertion succeeds. Throws ContractError in case it fails.

    import 'offensive/assertions/length';
    import check from 'offensive';
    
    check(arg, 'arg')
      .has.length(10)
      (); // <- executes built assert expression

    NOTE: Assertion will not be run unless call operator is invoked.

    Assertions

    offensive.js contains following built-in assertions.

    Table of Contents

    1. .Null()
    2. .Undefined()
    3. .Empty()
    4. .ofType(requiredType)
    5. .aBoolean()
    6. .aNumber()
    7. .anInteger()
    8. .aString()
    9. .anObject()
    10. .aFunction()
    11. .anArray()
    12. .anInstanceOf(RequiredClass)
    13. .aDate()
    14. .aRegExp()
    15. .True()
    16. .False()
    17. .truthy()
    18. .falsy()
    19. .matches(regexp)
    20. .anEmail()
    21. .anIntegerString()
    22. .equalTo()
    23. .exactly()
    24. .lessThan(rightBounds)
    25. .lessThanOrEqualTo(rightBounds)
    26. .greaterThan(leftBounds)
    27. .greaterThanOrEqualTo(leftBounds)
    28. .inRange(leftBounds, rightBounds)
    29. .before(rightBounds, boundsVarName?)
    30. .after(leftBounds, boundsVarName?)
    31. .field(fieldName)
    32. .fieldThat(fieldName)
    33. .allFieldsThat(condition)
    34. .method(methodName)
    35. .length(requiredLength)
    36. .oneOf(set, name)
    37. .elementThat(index, assertName, condition)
    38. .allElementsThat(assertName, condition)

    .Null() aliases: .null, .Nil, .nil

    Asserts that checked value is null using ===. Typically used in combination with .not operator.

    check(arg, 'arg').is.not.Null();

    .Undefined() aliases: .undefined

    Asserts that checked value is undefined. Typically used in combination with .not operator.

    check(arg, 'arg').is.not.Undefined();

    .Empty() aliases: .empty

    Asserts that checked value is null or undefined. Typically used in combination with .not operator.

    check(arg, 'arg').is.not.Empty();

    .ofType(requiredType : string) aliases: .type

    Asserts that checked value is of requiredType by ivoking typeof operator.

    check(arg, 'arg').is.ofType('boolean')();

    .aBoolean() aliases: .Boolean, .boolean

    Asserts that checked value is a boolean by ivoking typeof operator.

    check(arg, 'arg').is.aBoolean();

    .aNumber() aliases: .Number, .number

    Asserts that checked value is a number by ivoking typeof operator.

    check(arg, 'arg').is.aNumber();

    .anInteger() aliases: .Integer, .anInt, .int

    Asserts that checked value is an integer by ivoking Number.isInteger.

    check(arg, 'arg').is.anInteger();

    .aString() aliases: .String, .string

    Asserts that checked value is a string by ivoking typeof operator.

    check(arg, 'arg').is.aString();

    .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 and .Null in order to test for these specific cases.

    check(arg, 'arg').is.anObject();

    .aFunction() aliases: .Function, .function

    Asserts that checked value is a function by ivoking typeof operator.

    check(arg, 'arg').is.aFunction();

    .anArray() aliases: .Array, .array

    Asserts that checked value is an array by invoking Array.isArray.

    check(arg, 'arg').is.anArray();

    .anInstanceOf(RequiredClass : Function) aliases: .instanceOf

    Asserts that checked value is a instance of RequiredClass, by using instanceof operator.

    check(arg, 'arg').is.anInstanceOf(RegExp)();

    .aDate() aliases: .Date, .date

    Asserts that checked value is a instance of Date, by using instanceof operator.

    check(arg, 'arg').is.aDate();

    .aRegExp() aliases: .RegExp, .regexp

    Asserts that checked value is a instance of RegExp, by using instanceof operator.

    check(arg, 'arg').is.aRegExp();

    .True() aliases: .true

    Asserts that checked value is a boolean of value true.

    check(arg, 'arg').is.True();

    .False() aliases: .false

    Asserts that checked value is a boolean of value false.

    check(arg, 'arg').is.False();

    .truthy() aliases: .Truthy, .truethy, .Truethy

    Asserts that checked value is truthy (converts to true).

    check(arg, 'arg').is.truthy();

    .falsy() aliases: .Falsy, .falsey, .Falsey

    Asserts that checked value is falsy (converts to false).

    check(arg, 'arg').is.falsy();

    .matches(regexp : RegExp) aliases: .matchesRegexp, .matchesRegExp

    Asserts that checked value fully matches given regexp.

    check(arg, 'arg').matches(/[a-z]+/)();

    .anEmail() aliases: .Email, .email

    Asserts that checked value is a valid email.

    check(arg, 'arg').is.anEmail();

    .anIntegerString() aliases: .IntegerString, .intString

    Asserts that checked value is a valid string form of an integer.

    check(arg, 'arg').is.anIntegerString();

    .equalTo(another : any) aliases: .equal, .equals

    Asserts that checked value is equal to another. Comparison is made with == (double equals) operator.

    check(arg, 'arg').is.equalTo(100)();

    .exactly(another : any)

    Asserts that checked value is exactly the same as another. Comparison is made with === (triple equals) operator.

    check(arg, 'arg').is.exactly(instance)();

    .lessThan(rightBounds : number) aliases: .lt, .less

    Asserts that checked value is less than rightBounds.

    check(arg, 'arg').is.lessThan(100)();

    .lessThanOrEqualTo(rightBounds : number) aliases: .lte, .lessThanEqual

    Asserts that checked value is less than or equal to rightBounds.

    check(arg, 'arg').is.lessThanOrEqualTo(100)();

    .greaterThan(leftBounds : number) aliases: .gt, .greater

    Asserts that checked value is greater than leftBounds.

    check(arg, 'arg').is.greaterThan(0)();

    .greaterThanOrEqualTo(leftBounds : number) aliases: .gte, .greaterThanEqual

    Asserts that checked value is greater than or equal to leftBounds.

    check(arg, 'arg').is.greaterThanOrEqualTo(0)();

    .inRange(leftBounds : number, rightBounds : number) aliases: .between

    Asserts that checked value is grater than or equal to leftBounds and less than rightBounds.

    check(arg, 'arg').is.inRange(0, 100)();

    .before(rightBounds : Date, boundsVarName ?: string)

    Asserts that checked value a Date chronologically before rightBounds.

    check(arg, 'arg').is.before(new Date(0), 'Epoch')();

    .after(leftBounds : Date, boundsVarName ?: string)

    Asserts that checked value a Date chronologically after leftBounds.

    check(arg, 'arg').is.after(new Date(0), 'Epoch')();

    .field(fieldName : string) aliases: .property

    Asserts that checked value has field of name propertyName.

    check(arg, 'arg').has.property('length')();

    .fieldThat(fieldName : string, builder : FieldAssertionBuilder)

    Asserts that checked value has field of name propertyName, which satisfied assertion created in gived builder.

    check(arg, 'arg').has.propertyThat('x', x => x.is.aNumber)();

    .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.
    check(arg, 'arg').has.allFieldsThat(field => field.is.aNumber)();

    .method(methodName : string)

    Asserts that checked value has field of name methodName which is a function.

    check(arg, 'arg').has.method('toString')();

    .length(requiredLength : number) aliases: .len

    Asserts that checked value has property of name "length" and value of requiredLength.

    check(arg, 'arg').has.length(0)();

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

    check(arg, 'arg').is.oneOf([ 'started', 'running', 'finished' ])();
    check(arg, 'arg')
      .is.oneOf([ 'started', 'running', 'finished' ], 'valid status')
      ();

    .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.
    check(arg, 'arg').has.elementThat(0, elem => elem.is.anInteger)();

    .allElementThat(builder : ElemAssertionBuilder) aliases: .allElementsWhich

    Asserts that:

    1. Checked value is an array,
    2. Each element of this array satisfies assertuin created by given builder.
    check(arg, 'arg').has.allElementsThat(elem => elem.is.anInteger)();

    Boolean Operators

    offensive.js implements following operators.

    Table of Contents

    1. .and
    2. .or
    3. .not

    .and aliases: .of, .with

    Logical conjunction of two boolean values which are separated by call to .and operator.

    check(arg, 'arg')
      .has.length(2)
      .and.allElementsThat(elem => elem.is.aNumber)
      ();

    .or()

    Logical alternative of two (or more) values which are separated by call to .or operator.

    check(arg, 'arg').is.anObject.or.aFunction();

    .not aliases: .no, .dont, .doesnt

    Logical negation of an assertion after .not operator.

    check(arg, 'arg').is.not.Undefined();

    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, operators and also at the interface of Registry class.

    License

    Copyright © 2016 - 2019 Maciej Chałapuk. Released under MIT license.

    Install

    npm i offensive

    DownloadsWeekly Downloads

    227

    Version

    2.7.40

    License

    MIT

    Unpacked Size

    202 kB

    Total Files

    263

    Last publish

    Collaborators

    • avatar