diff --git a/.gitignore b/.gitignore
index e69de29..cba87a3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/coverage/
+/node_modules/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..0a50a3a
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,27 @@
+# Contributing
+
+Note: __README.md__ is generated from comments in __index.js__. Do not modify
+__README.md__ directly.
+
+1. Update local master branch:
+
+ $ git checkout master
+ $ git pull upstream master
+
+2. Create feature branch:
+
+ $ git checkout -b feature-x
+
+3. Make one or more atomic commits, and ensure that each commit has a
+ descriptive commit message. Commit messages should be line wrapped
+ at 72 characters.
+
+4. Run `make lint test`, and address any errors. Preferably, fix commits
+ in place using `git rebase` or `git commit --amend` to make the changes
+ easier to review.
+
+5. Push:
+
+ $ git push origin feature-x
+
+6. Open a pull request.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..e89bdfd
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,25 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Sanctuary
+Copyright (c) 2015 Simon Friis Vindum
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..38f5c42
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,44 @@
+ESLINT = node_modules/.bin/eslint --config node_modules/sanctuary-style/eslint-es6.json --env es6
+ISTANBUL = node_modules/.bin/istanbul
+NPM = npm
+TRANSCRIBE = node_modules/.bin/transcribe
+XYZ = node_modules/.bin/xyz --repo git@github.com:sanctuary-js/sanctuary-union-type.git --script scripts/prepublish
+
+
+.PHONY: all
+all: LICENSE README.md
+
+.PHONY: LICENSE
+LICENSE:
+ cp -- '$@' '$@.orig'
+ sed 's/Copyright (c) .* Sanctuary/Copyright (c) $(shell git log --date=format:%Y --pretty=format:%ad | sort -r | head -n 1) Sanctuary/' '$@.orig' >'$@'
+ rm -- '$@.orig'
+
+README.md: index.js
+ $(TRANSCRIBE) \
+ --heading-level 4 \
+ --url 'https://github.com/sanctuary-js/sanctuary-union-type/blob/v$(VERSION)/index.js#L{line}' \
+ -- '$<' \
+ | LC_ALL=C sed 's/
\(.*\)\1#\2/\3\1#\2/' >'$@'
+
+
+.PHONY: lint
+lint:
+ $(ESLINT) --env node -- index.js
+ $(ESLINT) --env node --env mocha -- test/index.js
+
+
+.PHONY: release-major release-minor release-patch
+release-major release-minor release-patch:
+ @$(XYZ) --increment $(@:release-%=%)
+
+
+.PHONY: setup
+setup:
+ $(NPM) install
+
+
+.PHONY: test
+test:
+ $(ISTANBUL) cover node_modules/.bin/_mocha -- --ui tdd -- test/index.js
+ $(ISTANBUL) check-coverage --branches 100
diff --git a/circle.yml b/circle.yml
new file mode 100644
index 0000000..77380c5
--- /dev/null
+++ b/circle.yml
@@ -0,0 +1,12 @@
+dependencies:
+ override:
+ - printf '%s\n' color=false progress=false >.npmrc
+ - make setup
+
+machine:
+ node:
+ version: 6.1.0
+
+test:
+ override:
+ - make lint test
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..45d7e6e
--- /dev/null
+++ b/index.js
@@ -0,0 +1,133 @@
+'use strict';
+
+const $ = require('sanctuary-def');
+const Z = require('sanctuary-type-classes');
+
+
+// stripNamespace :: String -> String
+const stripNamespace = s => s.slice(s.indexOf('/') + 1);
+
+// values :: Any -> Array a
+const values = o =>
+ Array.isArray(o) ? o : Z.map(k => o[k], Object.keys(o));
+
+// zipObj :: (Array String, Array a) -> StrMap a
+const zipObj = (ks, vs) =>
+ ks.reduce((acc, k, idx) => { acc[k] = vs[idx]; return acc; }, {});
+
+const a = $.TypeVariable('a');
+
+const createIterator = function() {
+ return {
+ idx: 0,
+ val: this,
+ next() {
+ const keys = this.val._keys;
+ return this.idx === keys.length ?
+ {done: true} :
+ // eslint-disable-next-line no-plusplus
+ {value: this.val[keys[this.idx++]]};
+ },
+ };
+};
+
+
+module.exports = opts => {
+
+ const def = $.create(opts);
+
+ const CreateUnionType = (typeName, _cases, _prototype) => {
+ const unprefixedTypeName = stripNamespace(typeName);
+ // Type :: Type
+ const Type = $.NullaryType(
+ typeName,
+ x => x != null && x['@@type'] === typeName
+ );
+ const env = Z.concat(opts.env, [Type]);
+ const def = $.create({checkTypes: opts.checkTypes, env});
+ const cases =
+ Z.map(xs => Z.map(x => x === undefined ? Type : x, xs),
+ _cases);
+ const CaseRecordType =
+ $.RecordType(Z.map(x => $.Function(Z.concat(values(x), [a])), cases));
+
+ const prototype$case = function(cases) {
+ return '_' in cases ?
+ cases._(this) :
+ def(
+ `${unprefixedTypeName}::case`,
+ {},
+ [CaseRecordType, a],
+ cases => cases[this._name](...Z.map(k => this[k], this._keys))
+ )(cases);
+ };
+
+ Type.prototype = Object.assign(_prototype, {case: prototype$case});
+
+ Type.case = function(o, ..._args) {
+ return def(
+ `${unprefixedTypeName}.case`,
+ {},
+ ['_' in o ? $.Any : CaseRecordType, Type, a],
+ (cases, value) =>
+ value._name in cases ?
+ cases[value._name](...Z.map(k => value[k], value._keys)) :
+ cases._(value)
+ ).apply(this, Z.concat([o], _args));
+ };
+
+ Object.keys(cases).forEach(name => {
+ const type = cases[name];
+ const isArray = Array.isArray(type);
+ const keys = Object.keys(type);
+ const types = isArray ? type : values(type);
+ const recordType = $.RecordType(isArray ? zipObj(keys, types) : type);
+
+ const of =
+ def(`${unprefixedTypeName}.${name}Of`,
+ {},
+ [recordType, recordType],
+ r => {
+ const prototype = Object.create(_prototype);
+ prototype._keys = keys;
+ prototype._name = name;
+ prototype['@@type'] = typeName;
+ prototype[Symbol.iterator] = createIterator;
+ Object.keys(r).forEach(k => { prototype[k] = r[k]; });
+ return prototype;
+ });
+
+ const ctor =
+ def(`${unprefixedTypeName}.${name}`,
+ {},
+ Z.concat(types, [recordType]),
+ (...args) => of(zipObj(keys, args)));
+
+ Type[`${name}Of`] = of;
+ Type[name] = ctor;
+ });
+
+ return Type;
+ };
+
+ const Named =
+ def('UnionType.Named',
+ {},
+ [$.String, $.StrMap($.Any), $.Any],
+ (typeName, _cases) => CreateUnionType(typeName, _cases, {}));
+
+ const Anonymous =
+ def('UnionType.Anonymous',
+ {},
+ [$.StrMap($.Any), $.Any],
+ enums =>
+ CreateUnionType(`(${Object.keys(enums).join(' | ')})`, enums, {}));
+
+ const Class =
+ def('UnionType.Class',
+ {},
+ [$.String, $.StrMap($.Any), $.Object, $.Any],
+ CreateUnionType);
+
+ return {Anonymous, Named, Class};
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..ddf9d4e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "sanctuary-union-type",
+ "version": "0.0.0",
+ "description": "Descendant of paldepind/union-type with more descriptive error messages",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/sanctuary-js/sanctuary-union-type.git"
+ },
+ "scripts": {
+ "test": "make lint test"
+ },
+ "dependencies": {
+ "sanctuary-def": "github:sanctuary-js/sanctuary-def",
+ "sanctuary-type-classes": "0.3.x"
+ },
+ "devDependencies": {
+ "eslint": "2.9.x",
+ "istanbul": "0.4.x",
+ "mocha": "3.x.x",
+ "ramda": "0.22.x",
+ "sanctuary-style": "0.3.x",
+ "transcribe": "0.5.x",
+ "xyz": "1.1.x"
+ },
+ "files": [
+ "/LICENSE",
+ "/README.md",
+ "/index.js",
+ "/package.json"
+ ]
+}
diff --git a/scripts/prepublish b/scripts/prepublish
new file mode 100755
index 0000000..567df7d
--- /dev/null
+++ b/scripts/prepublish
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -e
+
+rm -f README.md
+make
+git add LICENSE README.md
diff --git a/test/index.js b/test/index.js
new file mode 100644
index 0000000..7796d54
--- /dev/null
+++ b/test/index.js
@@ -0,0 +1,240 @@
+'use strict';
+
+const assert = require('assert');
+
+const R = require('ramda');
+const $ = require('sanctuary-def');
+const Z = require('sanctuary-type-classes');
+
+const UnionType = require('..');
+
+
+const a = $.TypeVariable('a');
+
+const UT = UnionType({checkTypes: true, env: $.env});
+
+const Type = UT.Anonymous;
+const Named = UT.Named;
+const Class = UT.Class;
+
+// eq :: (a, b) -> Undefined !
+const eq = (...args) => {
+ assert.strictEqual(args.length, 2);
+ const [actual, expected] = args;
+ assert.strictEqual(Z.toString(actual), Z.toString(expected));
+ assert.strictEqual(Z.equals(actual, expected), true);
+};
+
+// throws :: (Function, TypeRep a, String) -> Undefined !
+const throws = (...args) => {
+ assert.strictEqual(args.length, 3);
+ const [f, type, message] = args;
+ assert.throws(f, err => err.constructor === type && err.message === message);
+};
+
+
+test('defining a union type with predicates', () => {
+ const Num = $.NullaryType('my-package/Num', x => typeof x === 'number');
+ const Point = Type({Point: [Num, Num]});
+ const p = Point.Point(2, 3);
+ const [x, y] = p;
+
+ eq([x, y], [2, 3]);
+});
+
+test('defining a union type with built ins', () => {
+ const I = R.identity;
+ [
+ [2, $.Number, I],
+ ['2', $.String, I],
+ [true, $.Boolean, I],
+ [{a: 1}, $.Object, I],
+ [[0, 1, 2], $.Array($.Any), I],
+ [() => 1, $.AnyFunction, f => f()],
+ ]
+ .forEach(
+ ([expected, $, f]) => {
+ const Class = Type({$: [$]});
+ const instance = Class.$(expected);
+ const actual = instance[0];
+
+ eq(f(actual), f(expected));
+ }
+ );
+});
+
+test('defining a record type', () => {
+ const Point = Type({Point: {x: $.Number, y: $.Number}});
+ const [x, y] = Point.Point(2, 3);
+ const [x1, y1] = Point.PointOf({x: 2, y: 3});
+
+ eq([x, y], [x1, y1]);
+});
+
+test('create instance methods', () => {
+ const Maybe = Type({Just: [$.Any], Nothing: []});
+
+ const Nothing = Maybe.Nothing();
+ const Just = Maybe.Just;
+
+ Maybe.prototype['fantasy-land/equals'] = function(other) {
+ return this._name === 'Nothing' ?
+ other._name === 'Nothing' :
+ other._name === 'Just' && Z.equals(other[0], this[0]);
+ };
+
+ Maybe.prototype.map = function(f) {
+ return Maybe.case({Nothing: R.always(Nothing), Just: R.compose(Just, f)},
+ this);
+ };
+
+ eq(Nothing.map(Math.sqrt), Nothing);
+ eq(Just(9).map(Math.sqrt), Just(3));
+});
+
+test('create instance methods declaratively', () => {
+ const Maybe = Class('my-package/Maybe', {Just: [a], Nothing: []}, {
+ 'fantasy-land/equals'(other) {
+ return this._name === 'Nothing' ?
+ other._name === 'Nothing' :
+ other._name === 'Just' && Z.equals(other[0], this[0]);
+ },
+ map(f) {
+ return Maybe.case({Nothing: R.always(Nothing), Just: R.compose(Just, f)},
+ this);
+ },
+ });
+
+ const Nothing = Maybe.Nothing();
+ const Just = Maybe.Just;
+
+ eq(Nothing.map(Math.sqrt), Nothing);
+ eq(Just(9).map(Math.sqrt), Just(3));
+});
+
+test('Fields can be described in terms of other types', () => {
+ const Point = Type({Point: {x: $.Number, y: $.Number}});
+
+ const Shape = Type({
+ Circle: [$.Number, Point],
+ Rectangle: [Point, Point],
+ });
+
+ const [radius, [x, y]] = Shape.Circle(4, Point.Point(2, 3));
+
+ eq([radius, x, y], [4, 2, 3]);
+});
+
+test('The values of a type can also have no fields at all', () => {
+ const NotifySetting = Type({Mute: [], Vibrate: [], Sound: [$.Number]});
+
+ eq('Mute', NotifySetting.Mute()._name);
+});
+
+test('If a field value does not match the spec an error is thrown', () => {
+ const Point = Named('my-package/Point', {Point: {x: $.Number, y: $.Number}});
+
+ throws(() => { Point.Point(4, 'foo'); },
+ TypeError,
+ 'Invalid value\n' +
+ '\n' +
+ 'Point.Point :: Number -> Number -> { x :: Number, y :: Number }\n' +
+ ' ^^^^^^\n' +
+ ' 1\n' +
+ '\n' +
+ '1) "foo" :: String\n' +
+ '\n' +
+ 'The value at position 1 is not a member of ‘Number’.\n');
+});
+
+test('Switching on union types', () => {
+ const Action = Type({Up: [], Right: [], Down: [], Left: []});
+ const player = {x: 0, y: 0};
+
+ const advancePlayer = (action, player) =>
+ Action.case({
+ Up: () => ({x: player.x, y: player.y - 1}),
+ Right: () => ({x: player.x + 1, y: player.y}),
+ Down: () => ({x: player.x, y: player.y + 1}),
+ Left: () => ({x: player.x - 1, y: player.y}),
+ }, action);
+
+ eq(advancePlayer(Action.Up(), player), {x: 0, y: -1});
+});
+
+test('Switch on union types point free', () => {
+ const Point = Type({Point: {x: $.Number, y: $.Number}});
+
+ const Shape = Type({
+ Circle: [$.Number, Point],
+ Rectangle: [Point, Point],
+ });
+
+ const p1 = Point.PointOf({x: 0, y: 0});
+ const p2 = Point.PointOf({x: 10, y: 10});
+
+ {
+ const area = Shape.case({
+ Circle: (radius, _) => Math.PI * radius * radius,
+ Rectangle: (p1, p2) => (p2.x - p1.x) * (p2.y - p1.y),
+ });
+
+ eq(area(Shape.Rectangle(p1, p2)), 100);
+ }
+
+ {
+ const area = Shape.Rectangle(p1, p2).case({
+ Circle: (radius, _) => Math.PI * radius * radius,
+ Rectangle: (p1, p2) => (p2.x - p1.x) * (p2.y - p1.y),
+ });
+
+ eq(area, 100);
+ }
+});
+
+test('Destructuring assignment to extract values', () => {
+ const Point = Type({Point: {x: $.Number, y: $.Number}});
+ const [x, y] = Point.PointOf({x: 0, y: 0});
+
+ eq({x, y}, {x: 0, y: 0});
+});
+
+test('Recursive Union Types', () => {
+ const List = Type({Nil: [], Cons: [a, undefined]});
+
+ const toString = List.case({
+ Cons: (head, tail) => `${head} : ${toString(tail)}`,
+ Nil: () => 'Nil',
+ });
+
+ const list = List.Cons(1, List.Cons(2, List.Cons(3, List.Nil())));
+
+ eq(toString(list), '1 : 2 : 3 : Nil');
+});
+
+test('Disabling Type Checking', () => {
+ const Type = UnionType({checkTypes: false, env: $.env}).Anonymous;
+ const Point = Type({Point: {x: $.Number, y: $.Number}});
+ const p = Point.Point('foo', 4);
+
+ eq(p.x, 'foo');
+});
+
+test('Use placeholder for cases without matches', () => {
+ const List = Type({Nil: [], Cons: [a, undefined]});
+
+ eq(List.case({Cons: () => 'Cons', _: () => 'Nil'}, List.Nil()), 'Nil');
+ eq(List.Nil().case({Cons: () => 'Cons', _: () => 'Nil'}), 'Nil');
+});
+
+test('Create a Type with no cases', () => {
+ Type({});
+});
+
+test('Can iterate through a instance\'s values', () => {
+ const T = Type({Values: [$.Number, $.Number, $.Number]});
+ const instance = T.Values(1, 2, 3);
+ const results = [];
+ for (const x of instance) results.push(x);
+ eq(results, [1, 2, 3]);
+});