Skip to content

Commit

Permalink
Merged PR 70: Implemented Invariant Decorator
Browse files Browse the repository at this point in the history
Related work items: #133, #134, #135, #136, #137, #138, #145, #146, #147, #148, #163, #187, #43
  • Loading branch information
mlhaufe committed Sep 26, 2019
2 parents c792969 + 14998cb commit 806765a
Show file tree
Hide file tree
Showing 8 changed files with 771 additions and 120 deletions.
9 changes: 9 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@
"problemMatcher": [
"$tsc"
]
},
{
"label": "build-types",
"type": "npm",
"script": "build-types",
"problemMatcher": [
"$tsc"
],
"group": "build"
}
]
}
94 changes: 88 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,56 @@ Then run the command:

## Usage

After installation the library can be imported as such:

```typescript
import Contracts from '@thenewobjective/decorator-contracts';
```

It is not enough to import the library though, there are two modes of usage:
`debug` and `production`. This is represented as a boolean argument to the
`Contracts` constructor.

debug mode: `true`

production mode: `false`

```typescript
let {invariant} = new Contracts(true);
```

During development and testing you will want to use debug mode. This will
enable all assertion checks. In production mode all assertion checks become
no-ops for run-time efficiency. As the number of contract definitions can
be numerous, using the appropriate mode becomes increasingly important.

You are not prevented from mixing modes in the event you desire you maintain
a number of checks in a production library.

### Invariants

The `@invariant` decorator describes and enforces the semantics of a class
via a provided assertion. This assertion is checked after the associated class
is constructed, before and after every method execution, and before and after
every property usage (get/set). An example of this is given below using a Stack:
every property usage (get/set). If any of these evaluate to false during class
usage, an `AssertionError` will be thrown. Truthy assertions do not throw an
error. An example of this is given below using a Stack:

```typescript
@invariant((self: Stack<any>) => self.size >= 0 && self.size <= self.maxSize)
@invariant((self: Stack<any>) => self.size >= 0 && self.size <= self.limit)
@invariant((self: Stack<any>) => self.isEmpty() == (self.size == 0))
@invariant((self: Stack<any>) => self.isFull() == (self.size == self.maxSize))
class Stack<T>{
@invariant((self: Stack<any>) => self.isFull() == (self.size == self.limit))
class Stack<T> {
protected _implementation: Array<T> = []

constructor(readonly maxSize: number) {}
constructor(readonly limit: number) {}

isEmpty(): boolean {
return this._implementation.length == 0
}

isFull(): boolean {
return this._implementation.length == this.maxSize
return this._implementation.length == this.limit
}

pop(): T {
Expand All @@ -62,6 +90,60 @@ class Stack<T>{
}
```

Custom messaging can be associated with each `@invariant` as well:

```typescript
@invariant((self: Stack<any>) => self.size >= 0 && self.size <= self.limit, "The size of a stack must be between 0 and its limit")
@invariant((self: Stack<any>) => self.isEmpty() == (self.size == 0), "An empty stack must have a size of 0")
@invariant((self: Stack<any>) => self.isFull() == (self.size == self.limit), "A full stack must have a size that equals its limit")
class Stack<T> {
//...
}
```

Declaring multiple invariants in this style is terribly verbose. A shorthand is also available.

Without messaging:

```typescript
@invariant<Stack<any>>(
self => self.size >= 0 && self.size <= self.limit,
self => self.isEmpty() == (self.size == 0),
self => self.isFull() == (self.size == self.limit)
)
class Stack<T> {
//...
}
```

With messaging:

```typescript
@invariant<Stack<any>>([
[self => self.size >= 0 && self.size <= self.limit, "The size of a stack must be between 0 and its limit"],
[self => self.isEmpty() == (self.size == 0), "An empty stack must have a size of 0"],
[self => self.isFull() == (self.size == self.limit), "A full stack must have a size that equals its limit"]
])
class Stack<T> {
//...
}
```

With the above invariants any attempt to construct an invalid stack will fail:

```typescript
let myStack = new Stack(-1)
```

Additionally, attempting to pop an item from an empty stack would be
nonsensical according to the invariants. Therefore the following will
throw an AssertionError and prevent pop() from being executed:

```typescript
let myStack = new Stack(3)
let item = myStack.pop();
```

## Contributing

Due to current licensing restrictions, contributions are not being accepted currently.
Expand Down
102 changes: 102 additions & 0 deletions src/ContractHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* @license
* Copyright (C) 2019 Michael L Haufe
* SPDX-License-Identifier: GPL-2.0-only
*/

import Assertion from './Assertion';

const contractHandler = Symbol('Contract handler');

/**
* The ContractHandler manages the registration and evaluation of contracts associated with a class
*/
class ContractHandler {
protected _assert: typeof Assertion.prototype.assert;

protected readonly _invariantRegistry: Map<Predicate<any>, string> = new Map();
// TODO: requiresRegistry
// TODO: rescueRegistry
// TODO: ensuresRegistry

/**
* Constructs a new instance of the ContractHandler
* @param _assert - The assertion implementation associated with the current debugMode
*/
constructor(
protected assert: typeof Assertion.prototype.assert
) {
this._assert = assert;
}

/**
* Wraps a method with invariant assertions
*
* @param feature
* @param target
*/
protected _decorated(feature: Function, target: object) {
this.assertInvariants(target);
let result = feature.apply(target, arguments);
this.assertInvariants(target);

return result;
}

/**
* Registers a new invariant contract
*
* @param predicate - The invariant predicate
* @param message - The custome error message
*/
addInvariant(
predicate: Predicate<any>,
message: string
) {
this._assert(!this._invariantRegistry.has(predicate), 'Duplicate invariant');
this._invariantRegistry.set(predicate, message);
}

/**
* Evaluates all registered invariants
*
* @param self - The context class
*/
assertInvariants(self: object) {
this._invariantRegistry.forEach((message, predicate) => {
this._assert(predicate(self), message);
});
}

/**
* The handler trap for getting property values
*
* @param target - The target object
* @param prop - The name or Symbol of the property to get
*/
get(target: object, prop: keyof typeof target) {
let feature = target[prop];

// TODO: get could be a getter
return typeof feature == 'function' ?
this._decorated.bind(this, feature, target) :
feature;
}

/**
* The handler trap for setting property values
*
* @param target - The target object
* @param prop - The name or Symbol of the property to set
* @param value - The new value of the property to set.
*/
set(target: object, prop: keyof typeof target, value: (typeof target)[keyof typeof target]) {
this.assertInvariants(target);
target[prop] = value;
this.assertInvariants(target);

return true;
}
}

export {ContractHandler, contractHandler};
Loading

0 comments on commit 806765a

Please sign in to comment.