Skip to content

Commit

Permalink
fix(pattern-matching): Corrected the signature of the match function
Browse files Browse the repository at this point in the history
  • Loading branch information
jan-molak committed Feb 20, 2018
1 parent c5e6bb0 commit 076d0e2
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 148 deletions.
161 changes: 66 additions & 95 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"semantic-release": "^12.4.1",
"semantic-release-cli": "3.6.2",
"travis-deploy-once": "^4.3.3",
"ts-node": "4.1.0",
"ts-node": "5.0.0",
"tslint": "5.9.1",
"tslint-microsoft-contrib": "5.0.2",
"typescript": "2.7.1"
Expand Down
121 changes: 86 additions & 35 deletions spec/match.spec.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,105 @@
import 'mocha';
import { given } from 'mocha-testdata';
import { match, TinyType } from '../src';
import { match, TinyType, TinyTypeOf } from '../src';
import { IdentityMatcher, ObjectMatcher, StringMatcher } from '../src/pattern-matching';
import { expect } from './expect';

/** @test {match} */
describe(`::match`, () => {

abstract class DomainEvent {
constructor(public readonly timestamp: Date = new Date()) {
/** @test {match} */
describe('default rules', () => {
it(`uses the default rule if a more specific one has not been defined`, () => {
const result = match('four')
.when('five', _ => `high five`)
.when('six', _ => `got your six`)
.else(n => `got ${n}`);

expect(result).to.equal('got four');
});

it(`uses the default rule if a more specific one has not been defined`, () => {
const result = match(4)
.when(5, _ => `high five`)
.when(6, _ => `got your six`)
.else(n => `got ${n}`);

expect(result).to.equal('got 4');
});
});

describe('when selecting a matcher', () => {
abstract class DomainEvent {
constructor(public readonly timestamp: Date = new Date()) {
}
}
}

class ConcreteEvent extends DomainEvent {
}
class ConcreteEvent extends DomainEvent {
}

class EmaiAddress extends TinyType {
constructor(public readonly value: string) {
super();
class EmaiAddress extends TinyType {
constructor(public readonly value: string) {
super();
}
}
}

given([
{ input: 5, pattern: 1, expected_matcher: IdentityMatcher },
{ input: Symbol('some'), pattern: Symbol('other'), expected_matcher: IdentityMatcher },
{ input: 'hello', pattern: 'hello', expected_matcher: StringMatcher },
{ input: 'hello', pattern: /^[Hh]ello/, expected_matcher: StringMatcher },
{ input: new EmaiAddress('user@domain.org'), pattern: new EmaiAddress('user@domain.org'), expected_matcher: ObjectMatcher },
{ input: new ConcreteEvent(), pattern: ConcreteEvent, expected_matcher: ObjectMatcher },
{ input: new ConcreteEvent(), pattern: DomainEvent, expected_matcher: ObjectMatcher },
]).
it(`uses a matcher appropriate to the input`, ({input, pattern, expected_matcher}) => {
expect(match(input).when(pattern, _ => _)).to.be.instanceOf(expected_matcher);

given([
{ input: 5, pattern: 1, expected_matcher: IdentityMatcher },
{ input: Symbol('some'), pattern: Symbol('other'), expected_matcher: IdentityMatcher },
{ input: 'hello', pattern: 'hello', expected_matcher: StringMatcher },
{ input: 'hello', pattern: /^[Hh]ello/, expected_matcher: StringMatcher },
{ input: new EmaiAddress('user@domain.org'), pattern: new EmaiAddress('user@domain.org'), expected_matcher: ObjectMatcher },
{ input: new ConcreteEvent(), pattern: ConcreteEvent, expected_matcher: ObjectMatcher },
{ input: new ConcreteEvent(), pattern: DomainEvent, expected_matcher: ObjectMatcher },
]).
it(`uses a matcher appropriate to the input`, ({input, pattern, expected_matcher}) => {
expect(match(input).when(pattern, _ => _)).to.be.instanceOf(expected_matcher);
});
});

it(`uses the default rule if a more specific one has not been defined`, () => {
const result = match('four')
.when('five', _ => `high five`)
.when('six', _ => `got your six`)
.else(n => `got ${n}`);
/**
* @test {match}
* @test {TinyType}
* @test {TinyTypeOf}
*/
describe('when working with TinyTypes', () => {

expect(result).to.equal('got four');
});
class AccountId extends TinyTypeOf<number>() {}
abstract class Command extends TinyTypeOf<AccountId>() {}
class OpenAccount extends Command {}
class CloseAccount extends Command {}
class SuspendAccount extends Command {}

given([
{ command: new OpenAccount(new AccountId(42)), expected: 'Open AccountId(value=42)' },
{ command: new CloseAccount(new AccountId(42)), expected: 'Close AccountId(value=42)' },
{ command: new SuspendAccount(new AccountId(42)), expected: 'Command: SuspendAccount(value=AccountId(value=42))' },
{ command: null, expected: 'Unrecognised input: null' },
]).
it('matches a TinyType by type', ({ command, expected }) => {
const result = match(command)
.when(OpenAccount, ({ value }) => `Open ${ value }`)
.when(CloseAccount, ({ value }) => `Close ${ value }`)
.when(Command, _ => `Command: ${ _ }`)
.else(_ => `Unrecognised input: ${_}`);

expect(result).to.equal(expected);
});

it(`uses the default rule if a more specific one has not been defined`, () => {
const result = match(4)
.when(5, _ => `high five`)
.when(6, _ => `got your six`)
.else(n => `got ${n}`);
given([
{ command: new OpenAccount(new AccountId(42)), expected: 'Open AccountId(value=42)' },
{ command: new CloseAccount(new AccountId(42)), expected: 'Close AccountId(value=42)' },
{ command: new SuspendAccount(new AccountId(42)), expected: 'Command: SuspendAccount(value=AccountId(value=42))' },
{ command: null, expected: 'Unrecognised input: null' },
]).
it('matches a TinyType by value', ({ command, expected }) => {
const result = match(command)
.when(new OpenAccount(new AccountId(42)), ({ value }) => `Open ${ value }`)
.when(new CloseAccount(new AccountId(42)), ({ value }) => `Close ${ value }`)
.when(new SuspendAccount(new AccountId(42)), _ => `Command: ${ _ }`)
.else(_ => `Unrecognised input: ${_}`);

expect(result).to.equal('got 4');
expect(result).to.equal(expected);
});
});
});
15 changes: 3 additions & 12 deletions src/match.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { IdentityMatcher, ObjectMatcher, PatternMatcher, StringMatcher } from './pattern-matching';
import { TinyType } from './TinyType';
import { ConstructorOrAbstract } from './types';
import { ConstructorAbstractOrInstance } from './types';

// boolean equality matcher
export function match<Output_Type>(_: boolean): {
Expand Down Expand Up @@ -37,21 +36,13 @@ export function match<Output_Type>(_: string): {
) => PatternMatcher<string, string | RegExp, string, Output_Type>,
};

// Tiny Type equality matcher
export function match<Input_Type>(_: Input_Type): {
when: <Output_Type>(
pattern: TinyType,
transformation: (v: TinyType) => Output_Type,
) => PatternMatcher<Input_Type, TinyType | ConstructorOrAbstract<Input_Type>, TinyType | Input_Type, Output_Type>,
};

// type matcher
export function match<Input_Type, Output_Type>(_: Input_Type): {

when: <MT extends Input_Type>(
pattern: ConstructorOrAbstract<MT>,
pattern: ConstructorAbstractOrInstance<MT>,
transformation: (v: MT) => Output_Type,
) => PatternMatcher<Input_Type, TinyType | ConstructorOrAbstract<Input_Type>, TinyType | Input_Type, Output_Type>,
) => PatternMatcher<Input_Type, ConstructorAbstractOrInstance<Input_Type>, Input_Type, Output_Type>,
};

/**
Expand Down
6 changes: 3 additions & 3 deletions src/pattern-matching/ObjectMatcher.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { TinyType } from '../TinyType';
import { ConstructorOrAbstract } from '../types';
import { ConstructorAbstractOrInstance } from '../types';
import { PatternMatcher } from './PatternMatcher';
import { MatcherRule, MatchesEqualTinyType, MatchesIdentical, MatchesObjectsWithCommonPrototype } from './rules';

/**
* @access private
*/
export class ObjectMatcher<Input_Type, Output_Type> extends PatternMatcher<Input_Type, TinyType | ConstructorOrAbstract<Input_Type>, TinyType | Input_Type, Output_Type> {
export class ObjectMatcher<Input_Type, Output_Type> extends PatternMatcher<Input_Type, TinyType | ConstructorAbstractOrInstance<Input_Type>, TinyType | Input_Type, Output_Type> {

when<MT extends Input_Type>(pattern: ConstructorOrAbstract<MT>, transformation: (v: MT) => Output_Type): ObjectMatcher<Input_Type, Output_Type>;
when<MT extends Input_Type>(pattern: ConstructorAbstractOrInstance<MT>, transformation: (v: MT) => Output_Type): ObjectMatcher<Input_Type, Output_Type>;
when(pattern: TinyType, transformation: (v: TinyType) => Output_Type): ObjectMatcher<Input_Type, Output_Type>;
when(pattern: Input_Type, transformation: (v: Input_Type) => Output_Type): ObjectMatcher<Input_Type, Output_Type>;
when(pattern: any, transformation: (v: any) => Output_Type) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { MatcherRule } from './MatcherRule';
* @access private
*/
export class MatchesObjectsWithCommonPrototype<Input_Type, Output_Type> extends MatcherRule<Input_Type, Output_Type> {
constructor(private readonly pattern: ConstructorOrAbstract<Input_Type>, transformation: (v: Input_Type) => Output_Type) {
constructor(private readonly pattern: ConstructorOrAbstract<Input_Type>,
transformation: (v: Input_Type) => Output_Type,
) {
super(transformation);
}

Expand Down
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export type List<T> = T[];
export type ConstructorOrAbstract<T = {}> = Function & { prototype: T }; // tslint:disable-line:ban-types
export type ConstructorOrAbstract<T = {}> = Function & { prototype: T }; // tslint:disable-line:ban-types
export type ConstructorAbstractOrInstance<T = {}> = T | ConstructorOrAbstract; // tslint:disable-line:ban-types

export type JSONPrimitive = string | number | boolean | null;
export interface JSONObject {
Expand Down

0 comments on commit 076d0e2

Please sign in to comment.