Skip to content

Commit

Permalink
assertThat() now records exceptions instead of throwing them immediat…
Browse files Browse the repository at this point in the history
…ely.
  • Loading branch information
cowwoc committed Sep 9, 2024
1 parent fb2a8ce commit 399091a
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 97 deletions.
169 changes: 113 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
[![npm version](https://badge.fury.io/js/%40cowwoc%2Frequirements.svg)](https://badge.fury.io/js/%40cowwoc%2Frequirements)
[![build-status](https://github.com/cowwoc/requirements.js/workflows/Build/badge.svg)](https://github.com/cowwoc/requirements.js/actions?query=workflow%3ABuild)

# <img src="https://raw.githubusercontent.com/cowwoc/requirements.js/release-4.0.0/docs/checklist.svg?sanitize=true" width=64 height=64 alt="checklist"> Fluent API for Design Contracts
# <img src="https://raw.githubusercontent.com/cowwoc/requirements.js/release-4.0.0/docs/checklist.svg?sanitize=true" width=64 height=64 alt="checklist"> Requirements API

[![API](https://img.shields.io/badge/api_docs-5B45D5.svg)](https://cowwoc.github.io/requirements.js/4.0.0/docs/api/)
[![Changelog](https://img.shields.io/badge/changelog-A345D5.svg)](docs/Changelog.md)
[![java](https://img.shields.io/badge/other%20languages-java-457FD5.svg)](../../../requirements.java)

A [fluent API](https://en.wikipedia.org/wiki/Fluent_interface) for enforcing
[design contracts](https://en.wikipedia.org/wiki/Design_by_contract)
with [automatic message generation](#usage).
A [fluent API](https://en.m.wikipedia.org/docs/Fluent_interface) for enforcing
[design contracts](https://en.wikipedia.org/docs/Design_by_contract) with
[automatic message generation](docs/Features.md#automatic-message-generation):

✔️ Easy to use
✔️ Fast
Expand All @@ -27,83 +27,140 @@ or [pnpm](https://pnpm.io/):
pnpm add @cowwoc/[email protected]
```

## Sample Code
## Usage Example

```typescript
import {requireThatString} from "@cowwoc/requirements";


class Address
class Cake
{
private bitesTaken = 0;
private piecesLeft;

public constructor(piecesLeft: number)
{
requireThat(piecesLeft, "piecesLeft").isPositive();
this.piecesLeft = piecesLeft;
}

public eat(): number
{
++bitesTaken;
assertThat(bitesTaken, "bitesTaken").isNotNegative().elseThrow();

piecesLeft -= ThreadLocalRandom.current().nextInt(5);

assertThat(piecesLeft, "piecesLeft").isNotNegative().elseThrow();
return piecesLeft;
}

public getFailures(): String[]
{
return checkIf(bitesTaken, "bitesTaken").isNotNegative().
and(checkIf(piecesLeft, "piecesLeft").isGreaterThan(3)).
elseGetMessages();
}
}
```

class PublicAPI
{
constructor(name: string | null, age: number, address: Address | undefined)
{
requireThatString(name, "name").length().isBetween(1, 30);
requireThatNumber(age, "age").isBetween(18, 30);

// Methods that conduct runtime type-checks, such as isString() or isNotNull(), update the
// compile-time type returned by getValue().
const nameIsString: string = requireThat(name as unknown, "name").isString().getValue();
const address: Address = requireThat(address as unknown, "address").isInstance(Address).getValue();
}
}
If you violate a **precondition**:

```typescript
const cake = new Cake(-1000);
```

You'll get:

```
RangeError: "piecesLeft" must be positive.
actual: -1000
```

If you violate a **class invariant**:

```typescript
const cake = new Cake(1_000_000);
while (true)
cake.eat();
```

You'll get:

```
lang.AssertionError: "bitesTaken" may not be negative.
actual: -128
```

If you violate a **postcondition**:

```typescript
const cake = new Cake(100);
while (true)
cake.eat();
```

You'll get:

class PrivateAPI
{
public static toCamelCase(text): string
{
// Trusted input does not need to be casted to "unknown". The input type will be inferred
// and runtime checks will be skipped. Notice the lack of isString() or isNumber() invocations
// in the following code.
assertThat(r => r.requireThat(name, "name").length().isBetween(1, 30));
assertThat(r => r.requireThat(age, "age").isBetween(18, 30));
}
}
```
AssertionError: "piecesLeft" may not be negative.
actual: -4
```

If you violate **multiple** conditions at once:

Failure messages will look like this:
```typescript
const cake = new Cake(1);
cake.bitesTaken = -1;
cake.piecesLeft = 2;
const failures = [];
for (const failure of cake.getFailures())
failures.add(failure);
console.log(failures.join("\n\n"));
```

```text
TypeError: name may not be null
You'll get:

RangeError: name may not be empty
```
"bitesTaken" may not be negative.
actual: -1
RangeError: age must be in range [18, 30).
Actual: 15
"piecesLeft" must be greater than 3.
actual: 2
```

## Features

* [Automatic message generation](docs/Features.md#automatic-message-generation)
* [Diffs provided whenever possible](docs/Features.md#diffs-provided-whenever-possible)
* [Assertion support](docs/Features.md#assertion-support)
* [Grouping nested requirements](docs/Features.md#grouping-nested-requirements)
* [String diff](docs/Features.md#string-diff)
This library offers the following features:

* [Automatic message generation](docs/Features.md#automatic-message-generation) for validation failures
* [Diffs provided whenever possible](docs/Features.md#diffs-provided-whenever-possible) to highlight the
differences between expected and actual values
* [Zero overhead when assertions are disabled](docs/Features.md#assertion-support) for better performance
* [Multiple validation failures](docs/Features.md#multiple-validation-failures) that report all the errors at
once
* [Nested validations](docs/Features.md#nested-validations) that allow you to validate complex objects
* [String diff](docs/Features.md#string-diff) that shows the differences between two strings

## Getting Started
## Entry Points

The best way to learn about the API is using your IDE's auto-complete engine.
There are six entry points you can navigate from:
Designed for discovery using your favorite IDE's auto-complete feature.
The main entry points are:

* [requireThat(value, name)](https://cowwoc.github.io/requirements.js/4.0.0/docs/api/module-DefaultRequirements.html#~requireThat)
* [validateThat(value, name)](https://cowwoc.github.io/requirements.js/4.0.0/docs/api/module-DefaultRequirements.html#~validateThat)
* [assertThat(Function)](https://cowwoc.github.io/requirements.js/4.0.0/docs/api/module-DefaultRequirements.html#~assertThat)
* [assertThatAndReturn(Function)](https://cowwoc.github.io/requirements.js/4.0.0/docs/api/module-DefaultRequirements.html#~assertThatAndReturn)
for method preconditions.
* [assertThat(value, name)](https://cowwoc.github.io/requirements.js/4.0.0/docs/api/module-DefaultRequirements.html#~assertThat)
for [class invariants, method postconditions and private methods](docs/Features.md#assertion-support).
* [checkIf(value, name)](https://cowwoc.github.io/requirements.js/4.0.0/docs/api/module-DefaultRequirements.html#~checkIf)
for multiple failures and customized error handling.

* [Requirements](https://cowwoc.github.io/requirements.js/4.0.0/docs/api/module-Requirements-Requirements.html)
* [GlobalRequirements](https://cowwoc.github.io/requirements.js/4.0.0/docs/api/module-GlobalRequirements-GlobalRequirements.html)
See the [API documentation](https://cowwoc.github.io/requirements.java/10.0/docs/api/) for more details.

## Best practices

* Use `requireThat()` to verify pre-conditions of public APIs.
* Use `assertThat()` to verify object invariants and method post-conditions.
This results in excellent performance when assertions are disabled.
Have your cake and eat it too!
* Don't bother validating any constraints that are already enforced by the Typescript compiler (such as the
type of a variable) unless it will result in silent failures or security vulnerabilities when violated.
* Use `checkIf().elseGetMessages()` to return failure messages without throwing an exception.
This is the fastest validation approach, ideal for web services.
* To enhance the clarity of failure messages, you should provide parameter names, even when they are optional.
In other words, favor `assert that(value, name)` over `assert that(value)`.

## Related Projects

Expand Down
80 changes: 56 additions & 24 deletions docs/Features.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,54 +31,86 @@ Missing: [1, 5]

## Assertion support

All verifiers allocate memory which is especially hard to justify given that most checks are never going to
fail. If
you need to run in a high-performance, zero allocation environment (to reduce latency and jitter) look no
further than
`DefaultRequirements.assertThat()`.
If you need to run in a high performance, zero allocation environment (to reduce latency and jitter) look no
further than the following design pattern:

`assertThat()` skips verification if assertions are disabled. `DefaultRequirements` might be less flexible
than `Requirements` but it only allocates `Requirements` once per application. Together, they guarantee high
performance and no allocations if assertions are disabled.
```typescript
import {assertThat} from "@cowwoc/requirements";

class Person
{
public void eatLunch()
{
assertThat("time", new Date().getHours()).isGreaterThanOrEqualTo(12, "noon").elseThrow();
}
}
```

Use a build tool like Terser to declare `assertThat()` as a pure function and it will be stripped out from production builds.

## Multiple validation failures

```typescript
const name = "George";
const province = "Florida";
const provinces = ["Ontario", "Quebec", "Nova Scotia", "New Brunswick", "Manitoba",
"British Columbia", "Prince Edward Island", "Saskatchewan", "Alberta", "Newfoundland and Labrador"];

## Grouping nested requirements
const failures = checkIf(name, "name").length().isBetween(10, 30).elseGetFailures();
failures.addAll(checkIf(provinces, "provinces").contains(province).elseGetFailures());

Some classes provide a mechanism for grouping nested requirements. For example, `MapVerifier` has
methods `keys()` and
`keys(consumer)`, `values()` and `values(consumer)`. This enables one to group requirements that share the
same parent.
For example,
for (const failure of failures)
console.log(failure.getMessage());
```

Output will look like:

```
name must contain [10, 30) characters.
"provinces" must contain provide "province".
province: Florida
Actual: [Ontario, Quebec, Nova Scotia, New Brunswick, Manitoba, British Columbia, Prince Edward Island, Saskatchewan, Alberta, Newfoundland and Labrador]
```

## Nested validations

Nested validations facilitate checking multiple properties of a value. For example,

```typescript
const nameToAge = new Map();
nameToAge.set("Leah", 3);
nameToAge.set("Nathaniel", 1);

requireThat(nameToAge, "nameToAge").asMap().keys().containsAll(["Leah", "Nathaniel"]);
requireThat(nameToAge, "nameToAge").asMap().values().containsAll([3, 1]);
requireThat(nameToAge, "nameToAge").
keys().containsAll(["Leah", "Nathaniel"]);
requireThat(nameToAge, "nameToAge").
values().containsAll([3, 1]);
```

can be rewritten as:
can be converted to:

```typescript
requireThat(nameToAge, "nameToAge").asMap().
keys(k => k.containsAll(["Leah", "Nathaniel"])).
values(v => v.containsAll([3, 1]));
requireThat(nameToAge, "nameToAge").
and(k => k.keys().containsAll(["Leah", "Nathaniel"])).
and(v => v.values().containsAll([3, 1]));
```

## String diff

When
a [String comparison](https://cowwoc.github.io/requirements.js/4.0.0/docs/api/ObjectVerifier.html#isEqualTo)
fails, the library outputs a [diff](String_Diff.md) of the values being compared.
fails, the library outputs a diff of the values being compared.

Depending on the terminal capability, you will see a [textual](Textual_Diff.md) or a colored diff.

![colored-diff-example4.png](colored-diff-example4.png)

Node supports colored messages. Browsers do not.
Node supports colored exception messages. Browsers do not.

## Getting the actual value
## Returning the value after validation

Sometimes it is convenient to retrieve the actual value after a verification/validation:
You can get the value after validating or transforming it, e.g.

```typescript
class Player
Expand Down
2 changes: 1 addition & 1 deletion src/internal/validator/AbstractValidators.mts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ abstract class AbstractValidators<S> implements Validators<S>
requireThatValueIsNotNull(configuration, "configuration");
this.requireThatConfiguration = configuration;
this.assertThatConfiguration = MutableConfiguration.from(configuration).
errorTransformer(AbstractValidators.CONVERT_TO_ASSERTION_ERROR).toImmutable();
throwOnFailure(false).errorTransformer(AbstractValidators.CONVERT_TO_ASSERTION_ERROR).toImmutable();
this.checkIfConfiguration = MutableConfiguration.from(configuration).
throwOnFailure(false).toImmutable();
}
Expand Down
Loading

0 comments on commit 399091a

Please sign in to comment.