From 6c5bf9e722215647c41112842cbc29930d3e0222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8jberg?= Date: Tue, 28 Apr 2020 14:49:48 -0400 Subject: [PATCH] Add a `type` field to SumType * 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. --- __tests__/sumtype.test.ts | 102 ++++++++++++++++++++++---------------- src/sumtype.ts | 46 +++++++++++------ 2 files changed, 92 insertions(+), 56 deletions(-) diff --git a/__tests__/sumtype.test.ts b/__tests__/sumtype.test.ts index 9a37fe9..0a4ffc5 100644 --- a/__tests__/sumtype.test.ts +++ b/__tests__/sumtype.test.ts @@ -1,59 +1,74 @@ -import SumType from '../src/sumtype'; -import sinon, { SinonSpy } from 'sinon'; +import SumType from "../src/sumtype"; +import sinon, { SinonSpy } from "sinon"; // Mock implementation class Maybe extends SumType<{ Just: [T]; Nothing: [] }> {} function Just(value: T): Maybe { - return new Maybe('Just', value); + return new Maybe("Just", value); } function Nothing(): Maybe { - 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, @@ -61,52 +76,55 @@ describe('SumType', () => { }); }); - 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"); }); }); }); diff --git a/src/sumtype.ts b/src/sumtype.ts index 0527a2f..3198c6f 100644 --- a/src/sumtype.ts +++ b/src/sumtype.ts @@ -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; @@ -12,42 +12,60 @@ function arrayEquals(a: unknown[] | undefined, b: unknown[] | undefined) { } export type Variants = { [key: string]: unknown[] }; -export type KindAndData = { [K in keyof T]: Unshift }[keyof T]; -export type ExhaustiveCasePattern = { [K in keyof T]: (...args: T[K]) => R }; +export type VariantNameAndData = { + [K in keyof T]: Unshift; +}[keyof T]; +export type ExhaustiveCasePattern = { + [K in keyof T]: (...args: T[K]) => R; +}; export type CasePattern = | ExhaustiveCasePattern - | Partial> & { _: () => R }; + | (Partial> & { _: () => R }); abstract class SumType implements Setoid, Show { - private kind: keyof M; + private variantName: keyof M; private data: unknown[]; - constructor(...args: KindAndData) { - let [kind, ...data] = args; - this.kind = kind; + constructor(...args: VariantNameAndData) { + 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(pattern: CasePattern): 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): 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}`; } }