Skip to content

Commit

Permalink
remove runtime stuff & add generators
Browse files Browse the repository at this point in the history
no more decorators
  • Loading branch information
veigaribo committed May 11, 2021
1 parent 34bd8ab commit cdf9c96
Show file tree
Hide file tree
Showing 21 changed files with 477 additions and 775 deletions.
80 changes: 30 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,9 @@ const addable = new Class({

Instances are JavaScript classes that behave according to some (type) class.

Using the Addable example above, one could almost define an instance as:
Let's start with the following class:

```javascript
// you may use this decorator as many times as needed
@instance(addable)
class Number {
constructor(n) {
this.n = n
Expand All @@ -109,70 +107,52 @@ class Number {
}
```

The only extra step is to define a static method called `generateData`, that
will take any number of random numbers in the range [0, 1] as parameters
and should return a random instance value.
In order to declare it as an instance of something, you must provide a way of
generating values from your constructor. These are the values that will be used
for testing. (See [How it works](#lt-how-it-works))

There are two ways of doing that:

```javascript
@instance(addable)
class Number {
// ...
// you may ask for as many parameters as you want, and to each one will be
// assigned a random number between 0 and 1 (inclusive)
// from these numbers you may generate an instance of your constructor
const gen = continuous((n) => new Number(n))

static generateData(n) {
// this is quite a trivial example, we just wrap the n.
// in case you need more random values, just add them as parameters and they
// will be provided
return new Number(n)
}
}
// note that, to increase the likelihood of catching edge cases, sometimes the
// generated numbers will be all 0s or 1s
```

```javascript
// testing values will be sampled from the given array
const gen = discrete([new Number(0), new Number(1), new Number(2)])

// this method would be more useful if we had a finite number of possible
// values, which is not the case
```

With that done, you need only to call `validate` on the constructor and the
validators will run. **You should call this at some point in your tests.**
And then you only need to call `instance` with the correct parameters and
the validators will run. **You should call this at some point in your tests.**

```javascript
// will throw an Error if it fails
validate(Number)
instance(Number, addable, gen)
```

## Checking

You may also check whether a value is of an instance of a class using the
`isInstance` function:
Additionally, you may specify how many times each law will be tested (The
default is 15 times):

```javascript
const n = new Number(50)
isInstance(n, addable) // true, because Numbers are addable
instance(Number, addable, gen, { sampleSize: 10 })
```

## How it works

When you define your constructor using the `@instance` decorator, some metadata
will be injected into it and into every value it produces. That metadata will
be used to both run the proper validations during `validate` and also to allow
the `isInstance` to work.
<h2 id="lt-how-it-works">How it works</h2>

When `validate` is called, for each class that the constructor should be an
instance of, a sample of random instance values will be generated using your
constructor's `generateData`, and each class property will be tested using those.
When `instance` is called, a sample of random instance values will be created
using your provided generator, and each class property will be tested using
those.
If any of the laws fails to be asserted, an error is thrown, and you may be sure
that the constructor in question is not an instance of the class you declared in
the decorator.

In case it passes, you may have a high confidence that it is.

## What if I can't use decorators?

An approach would be to use the `instance(...)` decorator as a regular
function:

```javascript
class Number {
// ...
}

// new instances shall be instantiated using the returned constructor
const VNumber = instance(addable)(Number)

validate(VNumber)
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@
"jest --bail --findRelatedTests"
]
}
}
}
5 changes: 0 additions & 5 deletions src/__mocks__/config.ts

This file was deleted.

32 changes: 32 additions & 0 deletions src/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Class } from './classes'
import { Constructor } from './utils'

class Cache {
public readonly map: Map<Constructor, Class[]>

constructor() {
this.map = new Map()
}

set(Constructor: Constructor, clazz: Class): Class[] {
const existing = this.get(Constructor)

const newClasses = existing ? [...existing, clazz] : [clazz]

this.map.set(Constructor, newClasses)

return newClasses
}

get(Constructor: Constructor): Class[] | undefined {
return this.map.get(Constructor)
}

contains(Constructor: Constructor, clazz: Class) {
const existing = this.get(Constructor)

return existing && existing.some((clazz2) => clazz2.equals(clazz))
}
}

export const cache = new Cache()
50 changes: 36 additions & 14 deletions src/classes.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { InstanceConstructor } from './instances'
import { MaybeError } from './utils'
import { all, ValidationResult, Validator } from './validators'
import { cache } from './cache'
import { ConstructorValuesGenerator } from './generators'
import { Constructor, MaybeError } from './utils'
import {
all,
ValidationResult,
InstanceValidator,
ValidationOptions,
} from './validators'

type Laws = Validator<InstanceConstructor>
type Laws = InstanceValidator<Constructor>

export interface ClassOptions {
/** The name will be used to improve error messages. */
Expand Down Expand Up @@ -51,19 +57,35 @@ export class Class {
}

/**
* Checks if something is an instance of this class, not taking parents into
* account.
* Checks if something is an instance of this class.
*
* This is probably not what you're looking for: If you want to properly check
* if something is an instance of a class, check out the `validate` procedure.
*
* @param instance
* @param Constructor
* @returns
*
* @see {@link validate}
*/
validate(instance: InstanceConstructor): ValidationResult {
return this.laws.check(instance)
validate<T extends Constructor>(
Constructor: T,
values: ConstructorValuesGenerator<T>,
options: ValidationOptions = {},
): ValidationResult {
// check cache
if (cache.contains(Constructor, this)) {
return MaybeError.success()
}

// not cached
const result = MaybeError.foldConjoin([
// parents
...this.parents.map((parent) =>
parent.validate(Constructor, values, options),
),
// constructor itself
this.laws.check(Constructor, values, options),
])

// cache
cache.set(Constructor, this)

return result
}

equals(other: Class) {
Expand Down
42 changes: 0 additions & 42 deletions src/config.ts

This file was deleted.

65 changes: 0 additions & 65 deletions src/decorators.ts

This file was deleted.

Loading

0 comments on commit cdf9c96

Please sign in to comment.