Skip to content

Commit

Permalink
make the isInstance system work for inherited things
Browse files Browse the repository at this point in the history
BREAKING CHANGE: if using the non-decorator syntax for @instance,
the return value shall be used now instead of the original. also
the TS target is now ES6 due to some problems regarding inheritance
  • Loading branch information
veigaribo committed Oct 11, 2020
1 parent 767b807 commit 8f9d7a3
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 39 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ class Number {
// ...
}

// will throw if anything goes bad
instance(addable)(Number)
// will throw if anything goes bad.
// new instances shall be instantiated using the returned constructor
const VNumber = instance(addable)(Number)
```
46 changes: 36 additions & 10 deletions src/decorators.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { Class } from './classes'
import { InstanceConstructor } from './instances'
import {
InstanceConstructor,
InstanceMetadata,
KnownInstance,
KnownInstanceConstructor,
} from './instances'
import { metadataKey } from './private'
import { MaybeError } from './utils'

Expand Down Expand Up @@ -30,24 +35,45 @@ import { MaybeError } from './utils'
*/
export function instance<T extends InstanceConstructor>(
theClass: Class,
): (constructor: T) => T {
return function (constructor: T): T {
const result = theClass.validate(constructor)
): (Constructor: T) => KnownInstanceConstructor {
return function (Constructor: T): KnownInstanceConstructor {
const result = theClass.validate(Constructor)

if (result.isSuccess()) {
if (constructor[metadataKey]) {
constructor[metadataKey]!.classIds.push(theClass.id)
} else {
constructor[metadataKey] = { classIds: [theClass.id] }
const existingMetadata = (Constructor as any)[metadataKey]

const newIds = [
theClass.id,
...theClass.parents.map((parent) => parent.id),
]

const newMetadata = existingMetadata
? { classIds: [...existingMetadata.classIds, ...newIds] }
: { classIds: [...newIds] }

class NewClass extends Constructor implements KnownInstance {
public [metadataKey]: InstanceMetadata
public static [metadataKey]: InstanceMetadata

constructor(...args: any[]) {
super(...args)

// insert the metadata into the values so isInstance may see it
this[metadataKey] = newMetadata!
}
}

return constructor
// insert the metadata into the constructor so we may easily see it
// in later invocations and append things if necessary
NewClass[metadataKey] = newMetadata!

return NewClass
}

throw new Error(
result.conjoin(
MaybeError.fail(
`${constructor.name} is not an instance of ${theClass.name}.`,
`${Constructor.name} is not an instance of ${theClass.name}.`,
),
).value!,
)
Expand Down
20 changes: 11 additions & 9 deletions src/instances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,21 @@ export interface InstanceMetadata {
*
* Those random instances will be used to check against the class laws.
*/
export interface InstanceConstructor
extends Function,
Metadatable<InstanceMetadata> {
// @ts-ignore
new (...args: any[]): InstanceType<this>
export interface InstanceConstructor extends Function {
new (...args: any[]): any
generateData(...xs: number[]): InstanceType<this>
}

export interface Instance {}
export interface KnownInstanceConstructor
extends InstanceConstructor,
Metadatable<InstanceMetadata> {
new (...args: any[]): KnownInstance
}

export interface KnownInstance extends Metadatable<InstanceMetadata> {}

export function isInstance(value: Instance, theClass: Class) {
const metadata = (value.constructor as InstanceConstructor)[metadataKey]
?.classIds
export function isInstance(value: KnownInstance, theClass: Class) {
const metadata = value[metadataKey]?.classIds

return !!metadata && metadata.includes(theClass.id)
}
4 changes: 2 additions & 2 deletions src/validators.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { config } from './config'
import { Instance, InstanceConstructor } from './instances'
import { InstanceConstructor } from './instances'
import { arrayWithLength, Maybe, MaybeError } from './utils'

export type ValidationResult = MaybeError
Expand All @@ -8,7 +8,7 @@ export interface Validator<T> {
check(instance: T): ValidationResult
}

type Predicate<T extends Instance> = (...data: T[]) => boolean
type Predicate<T> = (...data: T[]) => boolean

type InstanceValidator = Validator<InstanceConstructor>

Expand Down
6 changes: 3 additions & 3 deletions test/decorators.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Class } from '../src/classes'
import { instance } from '../src/decorators'
import { Instance, InstanceConstructor } from '../src/instances'
import { InstanceConstructor, KnownInstanceConstructor } from '../src/instances'
import { metadataKey } from '../src/private'
import { all, obey } from '../src/validators'

interface Eq extends Instance {
interface Eq {
equals(other: this): boolean
}

Expand Down Expand Up @@ -51,7 +51,7 @@ test('instance will mark if validation succeeds', () => {
const n = new VNumber(Math.PI)
expect(n instanceof VNumber).toBe(true)

expect((VNumber as InstanceConstructor)[metadataKey]).toMatchObject({
expect((VNumber as KnownInstanceConstructor)[metadataKey]).toMatchObject({
classIds: [eq.id],
})
}).not.toThrow()
Expand Down
88 changes: 82 additions & 6 deletions test/instances.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { Class } from '../src/classes'
import { instance } from '../src/decorators'
import { Instance, isInstance } from '../src/instances'
import { isInstance, KnownInstance } from '../src/instances'
import { all, obey } from '../src/validators'

interface Eq extends Instance {
interface Eq {
equals(other: this): boolean
}

interface Neq extends Instance {
interface Neq {
nequals(other: this): boolean
}

interface Show extends Instance {
interface Show {
show(): void
}

Expand Down Expand Up @@ -39,7 +39,7 @@ const show = new Class({
),
})

test('Returns whether the value is an instance of the class', () => {
test('isInstance returns whether the value is an instance of the class', () => {
@instance(eq)
@instance(show)
class VNumber implements Eq {
Expand All @@ -56,9 +56,85 @@ test('Returns whether the value is an instance of the class', () => {
}
}

const showNumber = new VNumber(6)
const showNumber = new VNumber(6) as KnownInstance

expect(isInstance(showNumber, eq)).toBe(true)
expect(isInstance(showNumber, show)).toBe(true)
expect(isInstance(showNumber, neq)).toBe(false)
})

test('isInstance works for inherited constructors', () => {
@instance(show)
class Showable implements Show {
constructor(public readonly n: number) {}

show() {}

static generateData(x: number) {
return new Showable(x)
}
}

@instance(eq)
class Eqable extends Showable {
equals(another: Eqable) {
return this.n === another.n
}

static generateData(x: number) {
return new Eqable(x)
}
}

const eqable = new Eqable(20) as KnownInstance
const showable = new Showable(66) as KnownInstance

expect(isInstance(eqable, show)).toBe(true)
expect(isInstance(eqable, eq)).toBe(true)
expect(isInstance(eqable, neq)).toBe(false)

expect(isInstance(showable, show)).toBe(true)
expect(isInstance(showable, eq)).toBe(false)
expect(isInstance(showable, neq)).toBe(false)
})

interface Semigroup extends Eq {
add(y: Semigroup): this
}

const semigroup = new Class({
extends: [eq],
laws: all(
obey((x: Semigroup, y: Semigroup, z: Semigroup) => {
return x
.add(y)
.add(z)
.equals(x.add(y.add(z)))
}),
),
})

test('isInstance works for inherited classes', () => {
@instance(semigroup)
class NAddition {
constructor(public readonly n: number) {}

equals(another: NAddition) {
return this.n === another.n
}

add(y: NAddition) {
return new NAddition(this.n + y.n)
}

static generateData(x: number) {
return new NAddition(x)
}
}

const nadd = new NAddition(20) as KnownInstance

expect(isInstance(nadd, semigroup)).toBe(true)
expect(isInstance(nadd, eq)).toBe(true)
expect(isInstance(nadd, neq)).toBe(false)
})
10 changes: 6 additions & 4 deletions test/readme.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,13 @@ test('Readme examples work', () => {
}
}

instance(addable)(Number)
// will throw if anything goes bad.
// new instances shall be instantiated using the returned constructor
const VNumber = instance(addable)(Number)

const n = new Number(50)
const is = isInstance(n, addable) // true, because Numbers are addable
const n = new VNumber(50)

expect(is).toBe(true)
const isAddable = isInstance(n, addable) // true, because Numbers are addable
expect(isAddable).toBe(true)
}).not.toThrow()
})
3 changes: 1 addition & 2 deletions test/validators.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
jest.mock('../src/config')

import { config } from '../src/config'
import { Instance } from '../src/instances'
import { all, any, obey } from '../src/validators'

class EqInstance implements Instance {
class EqInstance {
constructor(public readonly x: number) {}

equals(b: EqInstance): boolean {
Expand Down
4 changes: 3 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
"declarationMap": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"module": "CommonJS",
"moduleResolution": "node",
"outDir": "lib",
"rootDir": "src",
"sourceMap": true,
"strict": true,
"target": "ES5"
"target": "ES6"
},
"exclude": ["node_modules/**/*", "test/**/*", "lib/**/*", "**/__mocks__/**/*"]
}

0 comments on commit 8f9d7a3

Please sign in to comment.