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 =