diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8fa381e..5a73089 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,11 @@
- `Option.map2`, `Option.map3`, `Option.map4` and test cleanup
- `Option.flatten`
- `Array.filterSome` and `Array.filterSomeWith`
+- For untagged unions
+ - `Pattern.MakeOption` - Turn any pattern into a `t | undefined`
+ - `Pattern.MakeNullable` - Turn any pattern into a `t | undefined | null`
+ - `Pattern.MakeNull` - Turn any pattern into a `t | null`
+ - `Pattern.MakeTuple2` and `Pattern.MakeTuple3`
- Option to only show test failures
## Version 0.20.0
diff --git a/src/Extras__Pattern.res b/src/Extras__Pattern.res
index 27f0435..345dfd7 100644
--- a/src/Extras__Pattern.res
+++ b/src/Extras__Pattern.res
@@ -7,6 +7,82 @@ module type T = {
let equals: (t, t) => bool
}
+module MakeOption = (P: T) => {
+ type t = option
+ let isTypeOf = v =>
+ switch v->Unknown.isUndefined {
+ | true => true
+ | false => P.isTypeOf(v)
+ }
+ let equals = (a, b) => Option.eq(a, b, P.equals)
+}
+
+module MakeNullable = (P: T) => {
+ type t = Js.Nullable.t
+ let isTypeOf = (v: unknown) =>
+ switch v->Unknown.isNullOrUndefined {
+ | true => true
+ | false => P.isTypeOf(v->Unknown.make)
+ }
+ let equals = (a: t, b: t) => {
+ switch Unknown.isNull(a) {
+ | true => Unknown.isNull(b)
+ | false =>
+ switch Unknown.isUndefined(a) {
+ | true => Unknown.isUndefined(b)
+ | false => !Unknown.isNullOrUndefined(b) && P.equals(a->Obj.magic, b->Obj.magic)
+ }
+ }
+ }
+}
+
+module MakeNull = (P: T) => {
+ type t = Js.Null.t
+ let isTypeOf = (v: unknown) =>
+ switch v->Unknown.isNull {
+ | true => true
+ | false => P.isTypeOf(v->Unknown.make)
+ }
+ let equals = (a: t, b: t) =>
+ switch a->Unknown.isNull {
+ | true => b->Unknown.isNull
+ | false => !(b->Unknown.isNull) && P.equals(a->Obj.magic, b->Obj.magic)
+ }
+}
+
+module MakeTuple2 = (
+ P: {
+ module A: T
+ module B: T
+ },
+) => {
+ type t = (P.A.t, P.B.t)
+ let isTypeOf = (u: unknown) =>
+ u->Js.Array2.isArray &&
+ u->Obj.magic->Js.Array2.length == 2 &&
+ P.A.isTypeOf(u->Obj.magic->Js.Array2.unsafe_get(0)) &&
+ P.B.isTypeOf(u->Obj.magic->Js.Array2.unsafe_get(1))
+ let equals = ((a1, b1): t, (a2, b2): t) => P.A.equals(a1, a2) && P.B.equals(b1, b2)
+}
+
+module MakeTuple3 = (
+ P: {
+ module A: T
+ module B: T
+ module C: T
+ },
+) => {
+ type t = (P.A.t, P.B.t, P.C.t)
+ let isTypeOf = (u: unknown) =>
+ u->Js.Array2.isArray &&
+ u->Obj.magic->Js.Array2.length == 3 &&
+ P.A.isTypeOf(u->Obj.magic->Js.Array2.unsafe_get(0)) &&
+ P.B.isTypeOf(u->Obj.magic->Js.Array2.unsafe_get(1)) &&
+ P.C.isTypeOf(u->Obj.magic->Js.Array2.unsafe_get(2))
+ let equals = ((a1, b1, c1): t, (a2, b2, c2): t) =>
+ P.A.equals(a1, a2) && P.B.equals(b1, b2) && P.C.equals(c1, c2)
+}
+
module MakeTools = (P: T) => {
let make = x => x->Unknown.make->P.isTypeOf ? Some((Obj.magic(x): P.t)) : None
let eq = (x, y) => x->make->Option.flatMap(x => y->make->Option.map(y => P.equals(x, y)))
diff --git a/src/Extras__Pattern.resi b/src/Extras__Pattern.resi
index 4380eae..99a55bf 100644
--- a/src/Extras__Pattern.resi
+++ b/src/Extras__Pattern.resi
@@ -21,6 +21,45 @@ module type T = {
let equals: (t, t) => bool
}
+/**
+Given a `Pattern.T`, construct a pattern corresponding to an option of that
+type. An option maps to `undefined | T`
+*/
+module MakeOption: (P: T) => (T with type t = option)
+
+/**
+Given a `Pattern.T`, construct a pattern corresponding to a `null | undefined |
+t` of that type.
+*/
+module MakeNullable: (P: T) => (T with type t = Js.Nullable.t)
+
+/**
+Given a `Pattern.T`, construct a pattern corresponding to a `null | t` of that
+type.
+*/
+module MakeNull: (P: T) => (T with type t = Js.Null.t)
+
+/**
+Given two patterns, construct a tuple pattern (array of length 2).
+*/
+module MakeTuple2: (
+ P: {
+ module A: T
+ module B: T
+ },
+) => (T with type t = (P.A.t, P.B.t))
+
+/**
+Given three patterns, construct a tuple pattern (array of length 3).
+*/
+module MakeTuple3: (
+ P: {
+ module A: T
+ module B: T
+ module C: T
+ },
+) => (T with type t = (P.A.t, P.B.t, P.C.t))
+
/**
Given a `Pattern.T`, constructs some utility functions.
*/
diff --git a/tests/Extras__PatternTests.res b/tests/Extras__PatternTests.res
index 01255e7..3bacb3b 100644
--- a/tests/Extras__PatternTests.res
+++ b/tests/Extras__PatternTests.res
@@ -7,14 +7,24 @@ module Literal = Extras__Literal
// Ensure basic int, string, date, and user-defined literals can be pattern matched
// ================================================================================
-let makeTest = (~title, ~guard, ~ok, ~invalid1, ~invalid2, ~invalid3) =>
- Test.make(~category="Patterns", ~title, ~expectation="", ~predicate=() =>
+let isTypeOfTest = (~title, ~guard, ~ok, ~invalid1, ~invalid2, ~invalid3) =>
+ Test.make(~category="Patterns", ~title, ~expectation="isTypeOf", ~predicate=() =>
true == ok->Js.Array2.every(i => i->Unknown.make->guard) &&
false == invalid1->Unknown.make->guard &&
false == invalid2->Unknown.make->guard &&
false == invalid3->Unknown.make->guard
)
+let areEqual = (~title, ~equals, ~expectation, ~a, ~b) =>
+ Test.make(~category="Patterns", ~title=`${title} equals`, ~expectation, ~predicate=() =>
+ equals(a, b) && equals(b, a)
+ )
+
+let areNotEqual = (~title, ~equals, ~expectation, ~a, ~b) =>
+ Test.make(~category="Patterns", ~title=`${title} equals`, ~expectation, ~predicate=() =>
+ !equals(a, b) && !equals(b, a)
+ )
+
module Negative1 = Literal.MakeInt({
let value = -1
})
@@ -25,9 +35,20 @@ module Yes = Literal.MakeString({
let caseInsensitive = true
})
+// Option, Nullable, and Null wrappers
+module OptionString = Pattern.MakeOption(Pattern.String)
+module NullableString = Pattern.MakeNullable(Pattern.String)
+module NullInt = Pattern.MakeNull(Pattern.Int)
+
+// Tuples
+module IntStringTuple = Pattern.MakeTuple2({
+ module A = Pattern.Int
+ module B = Pattern.String
+})
+
let tests = {
[
- makeTest(
+ isTypeOfTest(
~title="Null (null literal)",
~guard=Literal.Null.isTypeOf,
~ok=[Js.null->Obj.magic],
@@ -35,7 +56,7 @@ let tests = {
~invalid2=33,
~invalid3="abc",
),
- makeTest(
+ isTypeOfTest(
~title="True (bool literal)",
~guard=Literal.True.isTypeOf,
~ok=[true, Literal.True.value->Obj.magic],
@@ -43,7 +64,7 @@ let tests = {
~invalid2=33,
~invalid3="abc",
),
- makeTest(
+ isTypeOfTest(
~title="Yes (string literal)",
~guard=Yes.isTypeOf,
~ok=["yes", " YES", " yEs"],
@@ -51,7 +72,7 @@ let tests = {
~invalid2=33,
~invalid3=false,
),
- makeTest(
+ isTypeOfTest(
~title="Negative1 (int literal)",
~guard=Negative1.isTypeOf,
~ok=[-1],
@@ -59,7 +80,7 @@ let tests = {
~invalid2=3.4,
~invalid3="abc",
),
- makeTest(
+ isTypeOfTest(
~title="Int",
~guard=Pattern.Int.isTypeOf,
~ok=[1, -1, 34, Int32.max_int],
@@ -67,7 +88,7 @@ let tests = {
~invalid2=false,
~invalid3={"a": 1},
),
- makeTest(
+ isTypeOfTest(
~title="String",
~guard=Pattern.String.isTypeOf,
~ok=["abc", "", " a b c"],
@@ -75,7 +96,7 @@ let tests = {
~invalid2=43,
~invalid3=4.3,
),
- makeTest(
+ isTypeOfTest(
~title="Bool",
~guard=Pattern.Bool.isTypeOf,
~ok=[true, false],
@@ -83,7 +104,7 @@ let tests = {
~invalid2="abc",
~invalid3=4.3,
),
- makeTest(
+ isTypeOfTest(
~title="Date",
~guard=Pattern.Date.isTypeOf,
~ok=[Js.Date.now()->Js.Date.fromFloat],
@@ -91,5 +112,157 @@ let tests = {
~invalid2=3,
~invalid3=Js.Date.fromString("abc"),
),
+ isTypeOfTest(
+ ~title="MakeOption (string)",
+ ~guard=OptionString.isTypeOf,
+ ~ok=[Some("abc"), None, Some("")],
+ ~invalid1=1,
+ ~invalid2=false,
+ ~invalid3=4.5,
+ ),
+ areNotEqual(
+ ~title="MakeOption (string)",
+ ~equals=OptionString.equals,
+ ~expectation="when one undefined => false",
+ ~a=None,
+ ~b=Some("abc"),
+ ),
+ areEqual(
+ ~title="MakeOption (string)",
+ ~equals=OptionString.equals,
+ ~expectation="when both undefined => true",
+ ~a=None,
+ ~b=None,
+ ),
+ areNotEqual(
+ ~title="MakeOption (string)",
+ ~equals=OptionString.equals,
+ ~expectation="when both Some but values different => false",
+ ~a=Some("xyz"),
+ ~b=Some("abc"),
+ ),
+ areEqual(
+ ~title="MakeOption (string)",
+ ~equals=OptionString.equals,
+ ~expectation="when both Some but values same => true",
+ ~a=Some("xyz"),
+ ~b=Some("xyz"),
+ ),
+ isTypeOfTest(
+ ~title="MakeNullable (string)",
+ ~guard=NullableString.isTypeOf,
+ ~ok=[
+ Js.Nullable.return("abc"),
+ Js.Nullable.return(Js.null->Obj.magic),
+ Js.Nullable.return(Js.undefined->Obj.magic),
+ ],
+ ~invalid1=1,
+ ~invalid2=false,
+ ~invalid3=4.5,
+ ),
+ areEqual(
+ ~title="MakeNullable (string)",
+ ~expectation="when both undefined => true",
+ ~a=Js.Nullable.undefined,
+ ~b=Js.Nullable.undefined,
+ ~equals=NullableString.equals,
+ ),
+ areEqual(
+ ~title="MakeNullable (string)",
+ ~expectation="when both null => true",
+ ~a=Js.Nullable.null,
+ ~b=Js.Nullable.null,
+ ~equals=NullableString.equals,
+ ),
+ areNotEqual(
+ ~title="MakeNullable (string)",
+ ~expectation="when one undefined and other null => false",
+ ~a=Js.Nullable.null,
+ ~b=Js.Nullable.undefined,
+ ~equals=NullableString.equals,
+ ),
+ areEqual(
+ ~title="MakeNullable (string)",
+ ~expectation="when same string => true",
+ ~a="abc"->Js.Nullable.return,
+ ~b="abc"->Js.Nullable.return,
+ ~equals=NullableString.equals,
+ ),
+ areNotEqual(
+ ~title="MakeNullable (string)",
+ ~expectation="when different string => false",
+ ~a="abc"->Js.Nullable.return,
+ ~b="xyz"->Js.Nullable.return,
+ ~equals=NullableString.equals,
+ ),
+ areNotEqual(
+ ~title="MakeNullable (string)",
+ ~expectation="when one undefined and other is string => false",
+ ~a="abc"->Js.Nullable.return,
+ ~b=Js.Nullable.undefined,
+ ~equals=NullableString.equals,
+ ),
+ areNotEqual(
+ ~title="MakeNullable (string)",
+ ~expectation="when one null and other is string => false",
+ ~a="abc"->Js.Nullable.return,
+ ~b=Js.Nullable.null,
+ ~equals=NullableString.equals,
+ ),
+ Test.make(
+ ~category="Patterns",
+ ~title="MakeNullable (string)",
+ ~expectation="null != undefined != abc for built-in Js.Nullable using == or ===",
+ ~predicate=() => {
+ let a: Js.Nullable.t = Js.null->Obj.magic
+ let b: Js.Nullable.t = Js.undefined->Obj.magic
+ let c = Js.Nullable.return("abc")
+ a !== b && a !== c && b !== c && a != b && a != c && b != c
+ },
+ ),
+ isTypeOfTest(
+ ~title="MakeNull (int)",
+ ~guard=NullInt.isTypeOf,
+ ~ok=[3->Js.Null.return, 6->Js.Null.return, Js.null],
+ ~invalid1="abc",
+ ~invalid2=false,
+ ~invalid3={"a": 1},
+ ),
+ areEqual(
+ ~title="MakeNull (int)",
+ ~equals=NullInt.equals,
+ ~expectation="when both null => true",
+ ~a=Js.null,
+ ~b=Js.null,
+ ),
+ areEqual(
+ ~title="MakeNull (int)",
+ ~equals=NullInt.equals,
+ ~expectation="when both same non-null value => true",
+ ~a=3->Js.Null.return,
+ ~b=3->Js.Null.return,
+ ),
+ areNotEqual(
+ ~title="MakeNull (int)",
+ ~equals=NullInt.equals,
+ ~expectation="when one is null => false",
+ ~a=Js.null,
+ ~b=3->Js.Null.return,
+ ),
+ areNotEqual(
+ ~title="MakeNull (int)",
+ ~equals=NullInt.equals,
+ ~expectation="when both different non-null value => false",
+ ~a=5->Js.Null.return,
+ ~b=3->Js.Null.return,
+ ),
+ isTypeOfTest(
+ ~title="Tuple",
+ ~guard=IntStringTuple.isTypeOf,
+ ~ok=[(1, "abc"), (4, "x")],
+ ~invalid1="abc",
+ ~invalid2=false,
+ ~invalid3=(1, "abc", false), // too long
+ ),
]
}
diff --git a/tests/TestSuite.res b/tests/TestSuite.res
index c135c09..72c426e 100644
--- a/tests/TestSuite.res
+++ b/tests/TestSuite.res
@@ -13,7 +13,7 @@ let isLocalDevelopment = () => {
}
let filter = _ => true
-let onlyShowFailures = true
+let onlyShowFailures = false
let throwIfAnyTestFails = !isLocalDevelopment()
let tests =