From 6c5bf9e722215647c41112842cbc29930d3e0222 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Simon=20H=C3=B8jberg?= <r.hackr@gmail.com>
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<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,
@@ -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<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}`;
   }
 }