flow examples
flow examples
Boundaries, the callback protocol, transformations, pipelines, collections, serialization, and input protection.
Generated from src/flow/flow.examples.test.ts
.
These examples are executable — they run as part of the library’s test
suite, so every line below is verified against the released code.
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import sinon from "sinon";
import { Err } from "../types/err";
import {
all,
any,
attempt,
attemptAsync,
attemptValue,
attemptValueAsync,
defaultTo,
effect,
either,
ensure,
fail,
flatMap,
fromJSON,
map,
mapErr,
ok,
onErr,
onOk,
onOkAsync,
onTuple,
pipe,
pipeAsync,
type ResultTuple,
} from "./flow";
// ══════════════════════════════════════════════════════════════════════════════
// 1. Creating results
// ══════════════════════════════════════════════════════════════════════════════
describe("Creating results", () => {
test("ok() and fail() are the boundary", () => {
const [value, error] = ok(42);
const [, failure] = fail("Not found", "NOT_FOUND");
expect(value).toBe(42);
expect(error).toBeNull();
expect(failure?.code).toBe("NOT_FOUND");
});
test("a result destructures directly — there is nothing to unwrap", () => {
const parsePort = (raw: string): ResultTuple<number> => {
const port = Number(raw);
return Number.isInteger(port) ? ok(port) : fail("not a port", "PORT");
};
const [port, error] = parsePort("8080");
expect(port).toBe(8080);
expect(error).toBeNull();
});
test("rich errors are built with Err and passed to fail()", () => {
const [, error] = fail(
Err.from("read failed", {
code: "READ_FAILED",
metadata: { path: "/etc/app.json" },
}),
);
expect(error?.code).toBe("READ_FAILED");
expect(error?.getMetadata<string>("path")).toBe("/etc/app.json");
});
test("null, undefined and void are ordinary successes", () => {
expect(ok(null)[1]).toBeNull();
expect(ok(undefined)[1]).toBeNull();
expect(ok()[0]).toBeUndefined();
});
test("ok() carries error-shaped data as success", () => {
const carried = Err.from("this is data, not a failure");
const [value, error] = ok(carried);
expect(error).toBeNull();
expect(value).toBe(carried);
});
test("ok() carries a tuple as success — nesting is explicit", () => {
const inner = ok(1);
const [value, error] = ok(inner);
expect(error).toBeNull();
expect(value).toBe(inner);
});
});
// ══════════════════════════════════════════════════════════════════════════════
// 2. Exception boundaries
// ══════════════════════════════════════════════════════════════════════════════
describe("Exception boundaries", () => {
test("attemptValue() wraps a throwing value API", () => {
const parse = (raw: string) => attemptValue(() => JSON.parse(raw));
expect(parse('{"a":1}')[0]).toEqual({ a: 1 });
expect(parse("nope")[1]).toBeInstanceOf(Err);
});
test("attempt() also hosts a fallible callback", () => {
const pick = (flag: boolean) =>
attempt(() => (flag ? ok("yes") : fail("no", "NO")));
expect(pick(true)[0]).toBe("yes");
expect(pick(false)[1]?.code).toBe("NO");
});
test("attemptAsync() converts rejections", async () => {
const [, error] = await attemptAsync(async () => {
throw new Error("network down");
});
expect(error?.message).toBe("network down");
});
test("a sync callback treats a promise as data — use the async form instead", () => {
const [value] = flatMap(ok(1), () => ok(Promise.resolve(2)));
expect(value).toBeInstanceOf(Promise);
});
});
describe("Safe boundary and side-effect patterns", () => {
let sandbox: sinon.SinonSandbox;
beforeEach(() => {
sandbox = sinon.createSandbox();
});
afterEach(() => sandbox.restore());
test("validate parsed unknown data before reading it", () => {
const parsePort = (raw: string): ResultTuple<number> =>
flatMap(
attemptValue(() => JSON.parse(raw)),
(data) => {
if (
typeof data !== "object" ||
data === null ||
!("port" in data) ||
typeof data.port !== "number"
) {
return fail("numeric port required", "CONFIG:PORT");
}
return ok(data.port);
},
);
expect(parsePort('{"port":8080}')).toEqual([8080, null]);
expect(parsePort("[7,null]")[1]?.code).toEqual("CONFIG:PORT");
});
test("attemptValueAsync wraps response data before schema validation", async () => {
const json = sandbox.stub<[], Promise<unknown>>().resolves({ port: 8080 });
const result = await attemptValueAsync(json);
expect(result).toEqual([{ port: 8080 }, null]);
sinon.assert.calledOnce(json);
});
test("fallible side effects use flatMap so failures propagate", () => {
const save = sandbox
.stub<[number], ResultTuple<void>>()
.returns(fail("disk full", "SAVE"));
const result = flatMap(ok(1), (n) => map(save(n), () => n));
expect(result[1]?.code).toEqual("SAVE");
sinon.assert.calledOnceWithExactly(save, 1);
save.returns(ok());
expect(flatMap(ok(1), (n) => map(save(n), () => n))).toEqual([1, null]);
});
test("compose longer pipelines in typed chunks", () => {
const step = onOk((n: number) => ok(n + 1));
const first = pipe(
ok(0),
step,
step,
step,
step,
step,
step,
step,
step,
step,
step,
);
const result: ResultTuple<number> = pipe(first, step);
expect(result).toEqual([11, null]);
});
});
// ══════════════════════════════════════════════════════════════════════════════
// 3. Transforming results
// ══════════════════════════════════════════════════════════════════════════════
describe("Transforming results", () => {
test("map() is return-preserving — its callback returns plain data", () => {
const [doubled] = map(ok(21), (n) => n * 2);
const [asData] = map(ok(1), () => Err.from("data"));
expect(doubled).toBe(42);
expect(asData).toBeInstanceOf(Err);
});
test("flatMap() takes a fallible callback and composes a callee directly", () => {
const readConfig = (id: number): ResultTuple<string> =>
id > 0 ? ok(`config-${id}`) : fail("bad id", "BAD_ID");
expect(flatMap(ok(7), readConfig)[0]).toBe("config-7");
expect(flatMap(ok(-1), readConfig)[1]?.code).toBe("BAD_ID");
});
test("a value-returning callback gains an explicit ok()", () => {
const [value] = flatMap(ok(2), (n) => ok(n * 2));
expect(value).toBe(4);
});
test("mapErr() enriches through Err methods", () => {
const [, error] = mapErr(fail("socket closed"), (e) =>
fail(
e
.wrap("Failed to load project")
.withCode("PROJECT_LOAD")
.withMetadata({ projectId: 42 }),
),
);
expect(error?.code).toBe("PROJECT_LOAD");
expect(error?.root.message).toBe("socket closed");
});
test("mapErr() recovers with ok()", () => {
const [value, error] = mapErr(fail("cache miss"), () => ok("default"));
expect(error).toBeNull();
expect(value).toBe("default");
});
test("ensure() validates and can refine the success type", () => {
const isNonEmpty = (v: string): v is string => v.length > 0;
const checked = ensure(ok("name"), isNonEmpty, () =>
Err.from("empty", "EMPTY"),
);
const [, error] = ensure(ok(""), isNonEmpty, () =>
Err.from("empty", "EMPTY"),
);
expect(checked[0]).toBe("name");
expect(error?.code).toBe("EMPTY");
});
test("effect() observes without changing the result", () => {
const seen: unknown[] = [];
const [value] = effect(ok(1), ([v]) => {
seen.push(v);
});
expect(value).toBe(1);
expect(seen).toEqual([1]);
});
});
// ══════════════════════════════════════════════════════════════════════════════
// 4. Pipelines
// ══════════════════════════════════════════════════════════════════════════════
describe("Pipelines", () => {
test("pipe() composes stages over a tuple source", () => {
const [value] = pipe(
ok(" 42 "),
onOk((raw: string) => ok(raw.trim())),
onOk((raw: string) => attempt(() => ok(Number(raw)))),
onOk((n: number) =>
ensure(ok(n), Number.isInteger, () => Err.from("not an int", "INT")),
),
);
expect(value).toBe(42);
});
test("a failing stage skips later success stages until onErr recovers", () => {
const [value] = pipe(
ok(1),
onOk(() => fail("stage failed", "STAGE")),
onOk((n: number) => ok(n * 2)),
onErr((e) => ok(`recovered:${e.code}`)),
);
expect(value).toBe("recovered:STAGE");
});
test("a throwing custom stage is caught per stage", () => {
const [value] = pipe(
ok(1),
() => {
throw new Error("stage boom");
},
onErr((e) => ok(e.message)),
);
expect(value).toBe("stage boom");
});
test("a return-preserving map stage is written explicitly", () => {
const [value] = pipe(ok(2), (tuple) => map(tuple, (n) => n * 3));
expect(value).toBe(6);
});
test("onTuple() sees both channels", () => {
const label = (source: ResultTuple<number>) =>
pipe(
source,
onTuple<number, string>(([v, e]) => ok(e === null ? `ok:${v}` : "err")),
)[0];
expect(label(ok(1))).toBe("ok:1");
expect(label(fail("nope"))).toBe("err");
});
test("pipeAsync() accepts a promised source and mixed stages", async () => {
const [value] = await pipeAsync(
Promise.resolve(ok(2)),
onOkAsync(async (n: number) => ok(n * 2)),
onOk((n: number) => ok(`n=${n}`)),
);
expect(value).toBe("n=4");
});
test("a rejected source is recoverable", async () => {
const [value] = await pipeAsync(
Promise.reject(new Error("source down")) as Promise<ResultTuple<number>>,
onErr((e) => ok(e.message)),
);
expect(value).toBe("source down");
});
test("a reusable pipeline is an ordinary typed function", () => {
const normalize = (source: ResultTuple<string>) =>
pipe(
source,
onOk((raw: string) => ok(raw.trim().toLowerCase())),
onErr(() => ok("")),
);
expect(normalize(ok(" Hello "))[0]).toBe("hello");
expect(normalize(fail("nope"))[0]).toBe("");
});
});
// ══════════════════════════════════════════════════════════════════════════════
// 5. Terminal operations
// ══════════════════════════════════════════════════════════════════════════════
describe("Terminal operations", () => {
test("defaultTo() is handler-only — a constant fallback is a thunk", () => {
expect(defaultTo(ok(1), () => 0)).toBe(1);
expect(defaultTo(fail("nope"), () => 0)).toBe(0);
});
test("defaultTo() can recover from the error itself", () => {
expect(defaultTo(fail("nope", "NOPE"), (e) => `<${e.code}>`)).toBe(
"<NOPE>",
);
});
test("either() folds both channels", () => {
const render = (source: ResultTuple<number>) =>
either(
source,
(v) => `value ${v}`,
(e) => `error ${e.message}`,
);
expect(render(ok(1))).toBe("value 1");
expect(render(fail("nope"))).toBe("error nope");
});
});
// ══════════════════════════════════════════════════════════════════════════════
// 6. Collections
// ══════════════════════════════════════════════════════════════════════════════
describe("Collections", () => {
test("all() keeps order and infers a tuple for heterogeneous input", () => {
const [value] = all([ok(1), ok("two"), ok(true)] as const);
expect(value).toEqual([1, "two", true]);
});
test("all() aggregates every failure — it evaluates already-started work", () => {
const [, error] = all([fail("a", "A"), ok(1), fail("b", "B")]);
expect(error?.flatten()).toHaveLength(2);
});
test("any() returns the first success", () => {
const [value] = any([fail("primary down"), ok("replica"), ok("cache")]);
expect(value).toBe("replica");
});
test("any([]) fails with EMPTY_INPUT", () => {
expect(any([])[1]?.code).toBe("EMPTY_INPUT");
});
test("filtering a collection of results", () => {
const results: ResultTuple<number>[] = [ok(1), fail("nope"), ok(3)];
const values = results
.filter((t): t is readonly [number, null] => t[1] === null)
.map(([v]) => v);
expect(values).toEqual([1, 3]);
});
});
// ══════════════════════════════════════════════════════════════════════════════
// 7. Serialization
// ══════════════════════════════════════════════════════════════════════════════
describe("Serialization", () => {
test("a result serializes with JSON.stringify — no wrapper needed", () => {
const wire = JSON.stringify(ok({ id: 1 }));
expect(JSON.parse(wire)).toEqual([{ id: 1 }, null]);
});
test("fromJSON() reconstructs an error slot as a real Err", () => {
const wire = JSON.parse(JSON.stringify(fail("wire", "WIRE")));
const [, error] = fromJSON(wire);
expect(error).toBeInstanceOf(Err);
expect(error?.code).toBe("WIRE");
});
test("fromJSON() validates only the envelope and the error slot", () => {
const [value] = fromJSON<{ kind: string }>([{ kind: "Err" }, null]);
expect(value).toEqual({ kind: "Err" });
});
test("fromJSON() rejects invalid payloads instead of throwing", () => {
const [, error] = fromJSON({ not: "a tuple" });
expect(error?.code).toBe("INVALID_JSON");
});
test("ok() does not round-trip as void — it comes back as ok(null)", () => {
const wire = JSON.parse(JSON.stringify(ok()));
expect(wire).toEqual([null, null]);
expect(fromJSON(wire)[0]).toBeNull();
});
});
// ══════════════════════════════════════════════════════════════════════════════
// 8. Input protection
// ══════════════════════════════════════════════════════════════════════════════
describe("Input protection", () => {
test("a pipeline copies its source once, so stages cannot reach it", () => {
const source: ResultTuple<number> = ok(1);
const [, error] = pipe(source, (tuple) => {
(tuple as [number | null, Err | null])[0] = 99;
return tuple;
});
expect(error).toEqual(null);
expect(source[0]).toBe(1);
});
test("a tuple callback receives a copy", () => {
const source: ResultTuple<number> = ok(1);
const [, error] = pipe(
source,
onTuple<number, number>((tuple) => {
(tuple as [number | null, Err | null])[0] = 99;
return ok(1);
}),
);
expect(error).toEqual(null);
expect(source[0]).toBe(1);
});
test("payloads are never cloned — nested data stays the caller's", () => {
const payload = { count: 0 };
const [value] = map(ok(payload), (p) => p);
payload.count = 1;
expect(value).toBe(payload);
expect(value?.count).toBe(1);
});
});
// ══════════════════════════════════════════════════════════════════════════════
// 9. Errors are nominal
// ══════════════════════════════════════════════════════════════════════════════
describe("Errors are nominal", () => {
test("a marker-shaped plain object is data, not an error", () => {
const marker = { kind: "Err", message: "looks like an error" };
expect(Err.isErr(marker)).toBe(false);
expect(ok(marker)[1]).toBeNull();
});
test("Err.from() reconstructs a real instance from wire data", () => {
const marker = { kind: "Err", message: "from the wire", code: "WIRE" };
const [, error] = fail(Err.from(marker));
expect(error).toBeInstanceOf(Err);
expect(error?.code).toBe("WIRE");
});
});