Skip to content

Commit

Permalink
Add a type field to SumType
Browse files Browse the repository at this point in the history
* Add a new `type` getter, wrapping `variantName` (previously `kind`) to
  get closer to FSA compliancy (full compliancy involves a payload field
  that will not be made part of SumsUp as it goes against the design
  goals)
* Add a deprecated `kind` getter. This was always private, but had
  previously be used in cases where the types were compiled away.
  • Loading branch information
hojberg committed Apr 28, 2020
1 parent f63b14e commit 6c5bf9e
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 56 deletions.
102 changes: 60 additions & 42 deletions __tests__/sumtype.test.ts
Original file line number Diff line number Diff line change
@@ -1,112 +1,130 @@
import SumType from '../src/sumtype';
import sinon, { SinonSpy } from 'sinon';
import SumType from "../src/sumtype";
import sinon, { SinonSpy } from "sinon";

// Mock implementation
class Maybe<T> extends SumType<{ Just: [T]; Nothing: [] }> {}

function Just<T>(value: T): Maybe<T> {
return new Maybe('Just', value);
return new Maybe("Just", value);
}

function Nothing<T>(): Maybe<T> {
return new Maybe('Nothing');
return new Maybe("Nothing");
}

describe('SumType', () => {
describe('equals', () => {
test('is equal when both the kind and the wrapped value match', () => {
describe("SumType", () => {
describe("equals", () => {
test("is equal when both the kind and the wrapped value match", () => {
expect(Just(1).equals(Just(1))).toEqual(true);
expect(Just(1).equals(Just(2))).toEqual(false);
});

test('is not equal when the kind differ', () => {
test("is not equal when the kind differ", () => {
expect(Just(1).equals(Nothing())).toEqual(false);
});
});

describe('toString', () => {
test('outputs the kind and the data', () => {
expect(Just(1).toString()).toEqual('Just [1]');
expect(Nothing().toString()).toEqual('Nothing');
describe("toString", () => {
test("outputs the kind and the data", () => {
expect(Just(1).toString()).toEqual("Just [1]");
expect(Nothing().toString()).toEqual("Nothing");
});
});

describe('caseOf', () => {
// @deprecated
describe("kind", () => {
test("returns the name of the variant", () => {
expect(Just(1).kind).toEqual("Just");
expect(Nothing().kind).toEqual("Nothing");
});
});

describe("type", () => {
test("returns the name of the variant", () => {
expect(Just(1).type).toEqual("Just");
expect(Nothing().type).toEqual("Nothing");
});
});

describe("caseOf", () => {
let nothingSpy: SinonSpy<[], string>;
let justSpy: SinonSpy<[unknown], string>;
beforeEach(() => {
nothingSpy = sinon.spy(() => 'nothing');
justSpy = sinon.spy((_) => 'just');
nothingSpy = sinon.spy(() => "nothing");
justSpy = sinon.spy((_) => "just");
});

describe('Just', () => {
describe("Just", () => {
beforeEach(() => {
Just('foo').caseOf({
Just("foo").caseOf({
Nothing: nothingSpy,
Just: justSpy,
});
});

test('calls the Just function on the pattern', () => {
test("calls the Just function on the pattern", () => {
expect(nothingSpy.called).toEqual(false);
expect(justSpy.calledWith('foo')).toEqual(true);
expect(justSpy.calledWith("foo")).toEqual(true);
});
});

describe('Nothing', () => {
describe("Nothing", () => {
beforeEach(() => {
Nothing().caseOf({
Nothing: nothingSpy,
Just: justSpy,
});
});

test('calls the Just function on the pattern', () => {
test("calls the Just function on the pattern", () => {
expect(nothingSpy.called).toEqual(true);
expect(justSpy.called).toEqual(false);
});
});

describe('_', () => {
describe("_", () => {
let wildcardSpy: SinonSpy<[], string>;
beforeEach(() => {
wildcardSpy = sinon.spy(() => 'wildcard');
wildcardSpy = sinon.spy(() => "wildcard");
});

test('calls the wildcard when no other kinds are given', () => {
let value = Just('hello').caseOf({ _: wildcardSpy });
expect(value).toEqual('wildcard');
test("calls the wildcard when no other kinds are given", () => {
let value = Just("hello").caseOf({ _: wildcardSpy });
expect(value).toEqual("wildcard");
expect(wildcardSpy.calledWithExactly()).toEqual(true);
});

test('calls the wildcard when no matching kind is given', () => {
let value = Just('hello').caseOf({ Nothing: nothingSpy, _: wildcardSpy });
expect(value).toEqual('wildcard');
test("calls the wildcard when no matching kind is given", () => {
let value = Just("hello").caseOf({
Nothing: nothingSpy,
_: wildcardSpy,
});
expect(value).toEqual("wildcard");
expect(wildcardSpy.calledWithExactly()).toEqual(true);
expect(nothingSpy.called).toEqual(false);
});

test('skips the wildcard when a matching kind is given', () => {
let value = Just('hello').caseOf({ _: wildcardSpy, Just: justSpy });
expect(value).toEqual('just');
test("skips the wildcard when a matching kind is given", () => {
let value = Just("hello").caseOf({ _: wildcardSpy, Just: justSpy });
expect(value).toEqual("just");
expect(wildcardSpy.called).toEqual(false);
expect(justSpy.calledWithExactly('hello')).toEqual(true);
expect(justSpy.calledWithExactly("hello")).toEqual(true);
});
});

describe('missing pattern', () => {
test('throws the expected error when passed no patterns', () => {
const value = Just('hello');
describe("missing pattern", () => {
test("throws the expected error when passed no patterns", () => {
const value = Just("hello");
expect(() => {
value.caseOf({} as any)
}).toThrowError('caseOf pattern is missing a function for Just');
value.caseOf({} as any);
}).toThrowError("caseOf pattern is missing a function for Just");
});

test('throws the expected error when missing specific pattern', () => {
const value = Just('hello');
test("throws the expected error when missing specific pattern", () => {
const value = Just("hello");
expect(() => {
value.caseOf({ Nothing: () => {} } as any)
}).toThrowError('caseOf pattern is missing a function for Just');
value.caseOf({ Nothing: () => {} } as any);
}).toThrowError("caseOf pattern is missing a function for Just");
});
});
});
Expand Down
46 changes: 32 additions & 14 deletions src/sumtype.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Setoid, Show, Unshift } from './utils';
import { Setoid, Show, Unshift } from "./utils";

function arrayEquals(a: unknown[] | undefined, b: unknown[] | undefined) {
if (a === b) return true;
Expand All @@ -12,42 +12,60 @@ function arrayEquals(a: unknown[] | undefined, b: unknown[] | undefined) {
}

export type Variants = { [key: string]: unknown[] };
export type KindAndData<T extends Variants> = { [K in keyof T]: Unshift<T[K], K> }[keyof T];
export type ExhaustiveCasePattern<T extends Variants, R> = { [K in keyof T]: (...args: T[K]) => R };
export type VariantNameAndData<T extends Variants> = {
[K in keyof T]: Unshift<T[K], K>;
}[keyof T];
export type ExhaustiveCasePattern<T extends Variants, R> = {
[K in keyof T]: (...args: T[K]) => R;
};
export type CasePattern<T extends Variants, R> =
| ExhaustiveCasePattern<T, R>
| Partial<ExhaustiveCasePattern<T, R>> & { _: () => R };
| (Partial<ExhaustiveCasePattern<T, R>> & { _: () => R });

abstract class SumType<M extends Variants> implements Setoid, Show {
private kind: keyof M;
private variantName: keyof M;
private data: unknown[];

constructor(...args: KindAndData<M>) {
let [kind, ...data] = args;
this.kind = kind;
constructor(...args: VariantNameAndData<M>) {
let [variantName, ...data] = args;
this.variantName = variantName;
this.data = data;
}

// @deprecated
public get kind(): keyof M {
return this.variantName;
}

// Semi FSA compliancy
public get type(): keyof M {
return this.variantName;
}

public caseOf<T>(pattern: CasePattern<M, T>): T {
if (this.kind in pattern) {
return (pattern[this.kind] as any)(...this.data);
if (this.variantName in pattern) {
return (pattern[this.variantName] as any)(...this.data);
} else if (pattern._) {
return pattern._();
} else {
throw new Error(`caseOf pattern is missing a function for ${this.kind}`);
throw new Error(
`caseOf pattern is missing a function for ${this.variantName}`
);
}
}

public equals(that: SumType<M>): boolean {
return this.kind === that.kind && arrayEquals(this.data, that.data);
return (
this.variantName === that.variantName && arrayEquals(this.data, that.data)
);
}

public toString(): string {
if (this.data.length) {
return `${this.kind} ${JSON.stringify(this.data)}`;
return `${this.variantName} ${JSON.stringify(this.data)}`;
}

return `${this.kind}`;
return `${this.variantName}`;
}
}

Expand Down

0 comments on commit 6c5bf9e

Please sign in to comment.