|
1 | 1 | import { describe, expect, it } from "vitest"; |
2 | | -import { json, ndjson, lines } from "../src/transform.js"; |
| 2 | +import { json, ndjson, lines, number, safe, pipe } from "../src/transform.js"; |
3 | 3 |
|
4 | 4 | // ── json ────────────────────────────────────────────────────────────────────── |
5 | 5 |
|
@@ -80,3 +80,107 @@ describe("lines", () => { |
80 | 80 | expect(lines("")).toEqual([]); |
81 | 81 | }); |
82 | 82 | }); |
| 83 | + |
| 84 | +// ── number ──────────────────────────────────────────────────────────────────── |
| 85 | + |
| 86 | +describe("number", () => { |
| 87 | + it("parses an integer string", () => { |
| 88 | + expect(number("42")).toBe(42); |
| 89 | + }); |
| 90 | + |
| 91 | + it("parses a float string", () => { |
| 92 | + expect(number("3.14")).toBe(3.14); |
| 93 | + }); |
| 94 | + |
| 95 | + it("parses a negative number", () => { |
| 96 | + expect(number("-7")).toBe(-7); |
| 97 | + }); |
| 98 | + |
| 99 | + it("converts an empty string to 0", () => { |
| 100 | + expect(number("")).toBe(0); |
| 101 | + }); |
| 102 | + |
| 103 | + it("converts a non-numeric string to NaN", () => { |
| 104 | + expect(number("not a number")).toBeNaN(); |
| 105 | + }); |
| 106 | +}); |
| 107 | + |
| 108 | +// ── safe ────────────────────────────────────────────────────────────────────── |
| 109 | + |
| 110 | +describe("safe", () => { |
| 111 | + it("returns the transform result when successful", () => { |
| 112 | + expect(safe(json)('{"a":1}')).toEqual({ a: 1 }); |
| 113 | + }); |
| 114 | + |
| 115 | + it("returns undefined when the inner transform throws (no fallback)", () => { |
| 116 | + expect(safe(json)("bad json")).toBeUndefined(); |
| 117 | + }); |
| 118 | + |
| 119 | + it("returns the fallback when the inner transform throws", () => { |
| 120 | + expect(safe(json, null)("bad json")).toBeNull(); |
| 121 | + }); |
| 122 | + |
| 123 | + it("returns a numeric fallback on error", () => { |
| 124 | + expect(safe(number, 0)("NaN")).toBe(NaN); // Number("NaN") === NaN, not throwing |
| 125 | + // Demonstrate fallback with a throwing transform: |
| 126 | + const throwing = (_: string): number => { |
| 127 | + throw new Error("fail"); |
| 128 | + }; |
| 129 | + expect(safe(throwing, -1)("any")).toBe(-1); |
| 130 | + }); |
| 131 | + |
| 132 | + it("is composable: safe(ndjson) returns undefined on invalid line", () => { |
| 133 | + expect(safe(ndjson)('{"a":1}\nbad')).toBeUndefined(); |
| 134 | + }); |
| 135 | + |
| 136 | + it("overload without fallback infers T | undefined return type", () => { |
| 137 | + const result: { a: number } | undefined = safe(json<{ a: number }>)('{"a":1}'); |
| 138 | + expect(result).toEqual({ a: 1 }); |
| 139 | + }); |
| 140 | + |
| 141 | + it("overload with fallback infers T return type", () => { |
| 142 | + const result: { a: number } = safe(json<{ a: number }>, { a: 0 })("bad"); |
| 143 | + expect(result).toEqual({ a: 0 }); |
| 144 | + }); |
| 145 | +}); |
| 146 | + |
| 147 | +// ── pipe ────────────────────────────────────────────────────────────────────── |
| 148 | + |
| 149 | +describe("pipe", () => { |
| 150 | + it("passes the string through both transforms in order", () => { |
| 151 | + const upper = (s: string) => s.toUpperCase(); |
| 152 | + const exclaim = (s: string) => `${s}!`; |
| 153 | + expect(pipe(upper, exclaim)("hello")).toBe("HELLO!"); |
| 154 | + }); |
| 155 | + |
| 156 | + it("composes json with a post-processing step", () => { |
| 157 | + const getLabel = pipe(json<{ label: string }>, ev => ev.label); |
| 158 | + expect(getLabel('{"label":"tick"}')).toBe("tick"); |
| 159 | + }); |
| 160 | + |
| 161 | + it("composes ndjson with a filter step", () => { |
| 162 | + type Row = { type: string }; |
| 163 | + const ticks = pipe( |
| 164 | + ndjson<Row>, |
| 165 | + rows => rows.filter(r => r.type === "tick"), |
| 166 | + ); |
| 167 | + expect(ticks('{"type":"tick"}\n{"type":"other"}\n{"type":"tick"}')).toEqual([ |
| 168 | + { type: "tick" }, |
| 169 | + { type: "tick" }, |
| 170 | + ]); |
| 171 | + }); |
| 172 | + |
| 173 | + it("composes safe(json) with a fallback mapping", () => { |
| 174 | + const getLabel = pipe(safe(json<{ label: string }>), ev => ev?.label ?? ""); |
| 175 | + expect(getLabel('{"label":"ok"}')).toBe("ok"); |
| 176 | + expect(getLabel("bad")).toBe(""); |
| 177 | + }); |
| 178 | + |
| 179 | + it("infers the correct return type", () => { |
| 180 | + const toLength: (raw: string) => number = pipe( |
| 181 | + (s: string) => s.split(","), |
| 182 | + arr => arr.length, |
| 183 | + ); |
| 184 | + expect(toLength("a,b,c")).toBe(3); |
| 185 | + }); |
| 186 | +}); |
0 commit comments