Skip to content

Commit

Permalink
Enforce a maybe value when calling match()
Browse files Browse the repository at this point in the history
FossilOrigin-Name: b31673fc8d35ba01ce4f3cf57858812e06fce45697e2ba250753950a90e9b142
  • Loading branch information
iMarv committed Jun 5, 2020
1 parent d8ad0f3 commit d2c83c9
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 25 deletions.
53 changes: 33 additions & 20 deletions mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Deno.test({
Deno.test({
name: "maybe::match::freezes_value",
fn: () => {
const val = match("testi");
const val = match("testi" as Maybe<string>);

assert(Object.isFrozen(val.unwrap()));
},
Expand All @@ -45,7 +45,7 @@ Deno.test({
Deno.test({
name: "maybe::Matcher::maybe::return_value",
fn: () => {
const original: Maybe<string> = "testi";
const original: Maybe<string> = "testi" as Maybe<string>;
const value: Matcher<string> = match(original);

assertEquals(value.asMaybe(), original);
Expand All @@ -55,7 +55,7 @@ Deno.test({
Deno.test({
name: "maybe::Matcher::maybe::return_mapped_value",
fn: () => {
const original: Maybe<string> = "testi";
const original: Maybe<string> = "testi" as Maybe<string>;
const value: Matcher<string> = match(original).map((val) => `${val}2`);

assertEquals(value.asMaybe(), "testi2");
Expand All @@ -65,7 +65,7 @@ Deno.test({
Deno.test({
name: "maybe::Matcher::unwrap::return_value",
fn: () => {
const original: Maybe<string> = "testi";
const original: Maybe<string> = "testi" as Maybe<string>;
const value: Matcher<string> = match(original);

assertEquals(value.unwrap(), original);
Expand All @@ -75,7 +75,7 @@ Deno.test({
Deno.test({
name: "maybe::Matcher::unwrap::throw_nil",
fn: () => {
const value: Matcher<string> = match(null as unknown as string);
const value: Matcher<string> = match(null as Maybe<string>);

assertThrows(
() => {
Expand All @@ -90,7 +90,7 @@ Deno.test({
Deno.test({
name: "maybe::Matcher::unwrapOr::return_value",
fn: () => {
const original: Maybe<string> = "testi";
const original: Maybe<string> = "testi" as Maybe<string>;
const value: Matcher<string> = match(original);

assertEquals(value.unwrapOr("testo"), original);
Expand All @@ -101,17 +101,30 @@ Deno.test({
name: "maybe::Matcher::unwrapOr::return_default_on_nil",
fn: () => {
const expected: string = "testo";
const value: Matcher<string> = match(null as unknown as string);
const value: Matcher<string> = match(null as Maybe<string>);

assertEquals(value.unwrapOr(expected), expected);
},
});

Deno.test({
name: "maybe::Matcher::andThen::map_with_fn",
fn: () => {
type Ty = { p: Maybe<string> };
const num: Maybe<Ty> = { p: "testi" } as Maybe<Ty>;
const fn = (n: Ty): Matcher<string> => match(n.p);

const mt: Matcher<string> = match(num).andThen(fn);

assertEquals(mt.unwrap(), "testi");
},
});

Deno.test({
name: "maybe::Matcher::map::map_with_fn",
fn: () => {
const num: Maybe<number> = 1;
const fn = (n: number): Maybe<string> => `${n}`;
const num: Maybe<number> = 1 as Maybe<number>;
const fn = (n: number): Maybe<string> => `${n}` as Maybe<string>;

const mt: Matcher<string> = match(num).map(fn);

Expand All @@ -122,7 +135,7 @@ Deno.test({
Deno.test({
name: "maybe::Matcher::map::pass_through_nil",
fn: () => {
const num: Maybe<number> = null as unknown as number;
const num: Maybe<number> = null as Maybe<number>;
const fn = (n: number): Maybe<string> => `${n}`;

const mt: Matcher<string> = match(num).map(fn);
Expand All @@ -140,7 +153,7 @@ Deno.test({
Deno.test({
name: "maybe::Matcher::map::nil_dont_call_fn",
fn: () => {
const num: Maybe<number> = null as unknown as number;
const num: Maybe<number> = null as Maybe<number>;
let called: boolean = false;
const fn = (n: number): Maybe<string> => {
called = true;
Expand All @@ -164,7 +177,7 @@ Deno.test(
{
name: "maybe::Matcher::isOk::true_if_ok",
fn: () => {
const mt = match("some");
const mt = match("some" as Maybe<string>);

assert(mt.isOk());
},
Expand All @@ -175,7 +188,7 @@ Deno.test(
{
name: "maybe::Matcher::isOk::false_if_nil",
fn: () => {
const mt = match(null);
const mt = match(null as Maybe<string>);

assert(!mt.isOk());
},
Expand All @@ -186,7 +199,7 @@ Deno.test(
{
name: "maybe::Matcher::isNil::true_if_nil",
fn: () => {
const mt = match(null);
const mt = match(null as Maybe<string>);

assert(mt.isNil());
},
Expand All @@ -197,7 +210,7 @@ Deno.test(
{
name: "maybe::Matcher::isNil::false_if_ok",
fn: () => {
const mt = match("some");
const mt = match("some" as Maybe<string>);

assert(!mt.isNil());
},
Expand All @@ -207,7 +220,7 @@ Deno.test(
Deno.test({
name: "maybe::Matcher::nil::call_fn_if_nil",
fn: () => {
const mt = match(null);
const mt = match(null as Maybe<string>);
let called = false;
const fn = () => called = true;

Expand All @@ -220,7 +233,7 @@ Deno.test({
Deno.test({
name: "maybe::Matcher::nil::dont_call_fn_if_ok",
fn: () => {
const mt = match("some");
const mt = match("some" as Maybe<string>);
let called = false;
const fn = () => called = true;

Expand All @@ -233,7 +246,7 @@ Deno.test({
Deno.test({
name: "maybe::Matcher::ok::call_fn_if_ok",
fn: () => {
const mt = match("some");
const mt = match("some" as Maybe<string>);
let called: boolean = false;
const fn = () => called = true;

Expand All @@ -246,7 +259,7 @@ Deno.test({
Deno.test({
name: "maybe::Matcher::ok::dont_call_chain_nil_if_ok",
fn: () => {
const mt = match("some");
const mt = match("some" as Maybe<string>);
let called: string = "none";
const fn1 = () => called = "ok";
const fn2 = () => called = "nil";
Expand All @@ -260,7 +273,7 @@ Deno.test({
Deno.test({
name: "maybe::Matcher::ok::dont_call_fn_if_nil",
fn: () => {
const mt = match(null);
const mt = match(null as Maybe<string>);
let called: boolean = false;
const fn = () => called = true;

Expand Down
34 changes: 29 additions & 5 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const UNWRAP_ERROR_MSG: string = "Called unwrap on nil value";

export type Maybe<T> = null | undefined | T;
type NoMaybe<T> = Exclude<T, null | undefined>;
type EnforceMaybe<T> = null | undefined extends T ? T : never;

/**
* Equivalent to nodes `isNullOrUndefined`, asserts whether
Expand All @@ -18,12 +20,14 @@ export function isNil(value: Maybe<unknown>): value is null | undefined {
*
* @param maybe Value that is possibly null or undefined
*/
export function match<T>(maybe: Maybe<T>): Matcher<T> {
return new Matcher(Object.freeze(maybe));
export function match<T extends Maybe<{}>>(
maybe: EnforceMaybe<T>,
): Matcher<NoMaybe<T>> {
return new Matcher<NoMaybe<T>>(Object.freeze(maybe as Maybe<NoMaybe<T>>));
}

class NilMatcher<T> {
constructor(protected readonly _value: Readonly<Maybe<T>>) { }
constructor(protected readonly _value: Readonly<Maybe<T>>) {}

/**
* Checks if value of Matcher is Nil and runs given callback
Expand Down Expand Up @@ -66,6 +70,23 @@ export class Matcher<T> extends NilMatcher<T> {
return isNil(this._value);
}

/**
* Checks if Matcher value is not nil and applies given map
* function to it.
* Map function has to return a matcher
*
* @param fn Function to use to map value
*/
andThen<U extends NoMaybe<{}>>(
fn: (val: Readonly<T>) => Matcher<U>,
): Matcher<U> {
if (isNil(this._value)) {
return new Matcher<U>(null);
}

return fn(this._value);
}

/**
* Checks if Matcher value is not nil and applies given map
* function to it.
Expand All @@ -76,12 +97,15 @@ export class Matcher<T> extends NilMatcher<T> {
*/
map<U>(fn: (val: Readonly<T>) => Maybe<U>): Matcher<U> {
if (isNil(this._value)) {
return match<U>(null);
return new Matcher<U>(null);
}

return match<U>(fn(this._value));
return new Matcher<U>(fn(this._value));
}

/**
* Returns value as Maybe
*/
asMaybe(): Maybe<Readonly<T>> {
return this._value;
}
Expand Down

0 comments on commit d2c83c9

Please sign in to comment.