1/** @effect-diagnostics schemaStructWithTag:off */
2/** @effect-diagnostics schemaNumber:off */
3/** @effect-diagnostics preferTypedSchemaDecoder:off */
4import { assert, describe, it } from "@effect/vitest"
5import {
6 Array as Arr,
7 BigDecimal,
8 Cause,
9 Channel,
10 Chunk,
11 DateTime,
12 Duration,
13 Effect,
14 Exit,
15 HashMap,
16 HashSet,
17 Option,
18 Redacted,
19 Result,
20 Schema,
21 SchemaIssue,
22 SchemaParser,
23 SchemaTransformation,
24 Stream
25} from "effect"
26import * as SchemaBinary from "effect/encoding/SchemaBinary"
27
28const encode = <A, I>(schema: Schema.Codec<A, I>, value: A): Uint8Array<ArrayBuffer> =>
29 Schema.encodeUnknownSync(SchemaBinary.toCodec(schema))(value)
30
31const roundtrip = <A, I>(schema: Schema.Codec<A, I>, value: A): A => {
32 const codec = SchemaBinary.toCodec(schema)
33 return Schema.decodeUnknownSync(codec)(Schema.encodeUnknownSync(codec)(value))
34}
35
36const concat = (...chunks: ReadonlyArray<Uint8Array>): Uint8Array => {
37 const out = new Uint8Array(chunks.reduce((length, chunk) => length + chunk.length, 0))
38 let offset = 0
39 for (const chunk of chunks) {
40 out.set(chunk, offset)
41 offset += chunk.length
42 }
43 return out
44}
45
46const uvarint = (value: number): Array<number> => {
47 const out: Array<number> = []
48 while (value > 0x7F) {
49 out.push((value & 0x7F) | 0x80)
50 value = Math.floor(value / 128)
51 }
52 out.push(value)
53 return out
54}
55
56const nestedArrayFrame = (depth: number): Uint8Array<ArrayBuffer> => {
57 const lengths = [1]
58 for (let i = 0; i < depth; i++) {
59 const childLength = lengths[i]
60 lengths.push(1 + uvarint(childLength).length + childLength)
61 }
62 const bodyLength = lengths[depth]
63 const header = uvarint(bodyLength + 1)
64 const out = new Uint8Array(header.length + 1 + bodyLength)
65 out.set(header)
66 let offset = header.length
67 out[offset++] = 0x20
68 for (let i = depth; i > 0; i--) {
69 out[offset++] = 1
70 const childHeader = uvarint(lengths[i - 1])
71 out.set(childHeader, offset)
72 offset += childHeader.length
73 }
74 out[offset] = 0
75 return out
76}
77
78const int64Frame = (value: bigint): Uint8Array<ArrayBuffer> => {
79 const out = new Uint8Array(10)
80 out[0] = 9
81 out[1] = 0x20
82 new DataView(out.buffer).setBigInt64(2, value, true)
83 return out
84}
85
86const sameNumber = (actual: unknown, expected: number) => {
87 assert.isTrue(
88 Object.is(actual, expected),
89 `expected ${globalThis.String(actual)} to be exactly ${globalThis.String(expected)}`
90 )
91}
92
93const encodeFingerprint = <A, I>(schema: Schema.Codec<A, I>, value: A): Uint8Array<ArrayBuffer> =>
94 Schema.encodeUnknownSync(SchemaBinary.toCodec(schema, { fingerprint: true }))(value)
95
96const roundtripFingerprint = <A, I>(schema: Schema.Codec<A, I>, value: A): A => {
97 const codec = SchemaBinary.toCodec(schema, { fingerprint: true })
98 return Schema.decodeUnknownSync(codec)(Schema.encodeUnknownSync(codec)(value))
99}
100
101// Splits a fingerprint frame into its layout hash and the payload after it,
102// both as comparable strings.
103const fingerprintFrame = <A, I>(
104 schema: Schema.Codec<A, I>,
105 value: A
106): { readonly fingerprint: string; readonly payload: string } => {
107 const bytes = encodeFingerprint(schema, value)
108 let offset = 0
109 while ((bytes[offset] & 0x80) !== 0) offset++
110 offset++
111 assert.strictEqual(bytes[offset], 0x21)
112 return {
113 fingerprint: Array.from(bytes.subarray(offset + 1, offset + 9)).join(","),
114 payload: Array.from(bytes.subarray(offset + 9)).join(",")
115 }
116}
117
118const fingerprintOf = <A, I>(schema: Schema.Codec<A, I>, value: A): string =>
119 fingerprintFrame(schema, value).fingerprint
120
121const schemaError = (f: () => unknown): Schema.SchemaError => {
122 try {
123 f()
124 } catch (error) {
125 if (Schema.isSchemaError(error)) return error
126 throw error
127 }
128 throw new Error("expected SchemaError")
129}
130
131describe("SchemaBinary", () => {
132 // Rows whose present optional fields differ, shared by the row-run shape
133 // tests in both wire modes.
134 const Varying = Schema.Struct({
135 id: Schema.String,
136 a: Schema.optionalKey(Schema.String),
137 b: Schema.optionalKey(Schema.Number)
138 })
139 const varying = [
140 { id: "0", a: "x" },
141 { id: "1" },
142 { id: "2", a: "x", b: 1 },
143 { id: "3", a: "x" },
144 { id: "4", b: 2 }
145 ]
146
147 describe("wire layout", () => {
148 it("packs fixed-size array elements", () => {
149 const numbers = [1.5, Number.NaN, -0, Number.POSITIVE_INFINITY]
150 const numberBytes = encode(Schema.Array(Schema.Number), numbers)
151 const decoded = roundtrip(Schema.Array(Schema.Number), numbers)
152 // length, envelope, count, mode byte, four f64 elements
153 assert.strictEqual(numberBytes.length, 36)
154 assert.strictEqual(decoded[0], 1.5)
155 assert.isTrue(Number.isNaN(decoded[1]))
156 assert.isTrue(Object.is(decoded[2], -0))
157 assert.strictEqual(decoded[3], Number.POSITIVE_INFINITY)
158
159 const boolBytes = encode(Schema.Array(Schema.Boolean), [true, false, true])
160 assert.strictEqual(boolBytes.length, 6)
161 assert.deepStrictEqual(roundtrip(Schema.Array(Schema.Boolean), [true, false, true]), [true, false, true])
162 })
163
164 it("length-prefixes variable-size array elements and nested arrays", () => {
165 assert.deepStrictEqual(
166 roundtrip(Schema.Array(Schema.String), ["ab", "", "cdé"]),
167 ["ab", "", "cdé"]
168 )
169 assert.deepStrictEqual(
170 roundtrip(Schema.Array(Schema.Array(Schema.Number)), [[1, 2], [], [3]]),
171 [[1, 2], [], [3]]
172 )
173 })
174
175 it("round-trips zero-width array elements", () => {
176 assert.deepStrictEqual(roundtrip(Schema.Array(Schema.Null), [null, null, null]), [null, null, null])
177 assert.deepStrictEqual(
178 roundtrip(Schema.Array(Schema.Undefined), [undefined, undefined]),
179 [undefined, undefined]
180 )
181 })
182
183 it("distinguishes absent optionals from present zero-width values", () => {
184 const schema = Schema.Struct({ value: Schema.optionalKey(Schema.Undefined) })
185 const absent = roundtrip(schema, {})
186 const present = roundtrip(schema, { value: undefined })
187
188 assert.isFalse(Object.hasOwn(absent, "value"))
189 assert.isTrue(Object.hasOwn(present, "value"))
190 assert.strictEqual(present.value, undefined)
191 assert.strictEqual(roundtrip(Schema.Null, null), null)
192 assert.strictEqual(roundtrip(Schema.Void, undefined), undefined)
193 })
194
195 it("includes non-enumerable fields in row presence masks", () => {
196 const Row = Schema.Struct({ id: Schema.String, value: Schema.optionalKey(Schema.Undefined) })
197 const row = Object.defineProperty({ id: "1" }, "value", { value: undefined })
198 for (const decoded of [roundtrip(Schema.Array(Row), [row]), roundtripFingerprint(Schema.Array(Row), [row])]) {
199 assert.isTrue(Object.hasOwn(decoded[0], "value"))
200 }
201 })
202
203 it("round-trips every literal leaf kind and registered symbols", () => {
204 const literals = Schema.Union([
205 Schema.Literal("text"),
206 Schema.Literal(1),
207 Schema.Literal(true),
208 Schema.Literal(2n)
209 ])
210 for (const value of ["text", 1, true, 2n] as const) {
211 assert.strictEqual(roundtrip(literals, value), value)
212 }
213
214 const registered = Symbol.for("SchemaBinary/registered")
215 assert.strictEqual(roundtrip(Schema.Symbol, registered), registered)
216 assert.strictEqual(roundtrip(Schema.UniqueSymbol(registered), registered), registered)
217 })
218
219 it("omits the count for fixed tuples and includes it for optional tuples", () => {
220 const pair = Schema.Tuple([Schema.String, Schema.Number])
221 // length, envelope, then a length-prefixed slot each: "key" and varint 42
222 assert.strictEqual(encode(pair, ["key", 42]).length, 8)
223 assert.deepStrictEqual(roundtrip(pair, ["key", 42]), ["key", 42])
224
225 const optional = Schema.Tuple([Schema.Number, Schema.optionalKey(Schema.Number)])
226 assert.deepStrictEqual(roundtrip(optional, [1]), [1])
227 assert.deepStrictEqual(roundtrip(optional, [1, 2]), [1, 2])
228 })
229
230 it("supports tuple rest and trailing slots", () => {
231 const schema = Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number, Schema.Boolean])
232 assert.deepStrictEqual(
233 roundtrip(schema, ["head", 1.5, 2.5, 3.5, true]),
234 ["head", 1.5, 2.5, 3.5, true]
235 )
236 assert.deepStrictEqual(roundtrip(schema, ["head", false]), ["head", false])
237 })
238
239 it("writes a length-first frame and rejects envelope or leftovers", () => {
240 const codec = SchemaBinary.toCodec(Schema.Number)
241 const bytes = Schema.encodeUnknownSync(codec)(1)
242 assert.deepStrictEqual(Array.from(bytes), [2, 0x20, 0x02])
243
244 const flags = bytes.slice()
245 flags[1] = 0x21
246 assert.match(schemaError(() => Schema.decodeUnknownSync(codec)(flags)).message, /version 2 envelope, flags 0/)
247
248 const version = bytes.slice()
249 version[1] = 0x30
250 assert.match(schemaError(() => Schema.decodeUnknownSync(codec)(version)).message, /version 2 envelope, flags 0/)
251 assert.match(
252 schemaError(() => Schema.decodeUnknownSync(codec)(concat(bytes, bytes))).message,
253 /no leftover bytes/
254 )
255 })
256 })
257
258 describe("string content", () => {
259 it("preserves a leading U+FEFF in strings", () => {
260 assert.strictEqual(roundtrip(Schema.String, "\uFEFFreport"), "\uFEFFreport")
261 })
262 })
263
264 describe("output arena", () => {
265 it("keeps an earlier result stable through later encodes and arena rollover", () => {
266 const codec = SchemaBinary.toCodec(Schema.String)
267 const encode = Schema.encodeUnknownSync(codec)
268 const first = encode("first")
269 const expected = first.slice()
270 let rolledOver = false
271
272 for (let i = 0; i < 500; i++) {
273 const later = encode(`later-${i}-${"x".repeat(512)}`)
274 if (later.buffer !== first.buffer) rolledOver = true
275 assert.deepStrictEqual(first, expected)
276 }
277
278 assert.isTrue(rolledOver)
279 assert.strictEqual(Schema.decodeUnknownSync(codec)(first), "first")
280 })
281
282 it("keeps different results independent inside a shared arena", () => {
283 const codec = SchemaBinary.toCodec(Schema.String)
284 const encode = Schema.encodeUnknownSync(codec)
285 const decode = Schema.decodeUnknownSync(codec)
286 const results = [encode("alpha"), encode("bravo"), encode("charlie")]
287
288 assert.deepStrictEqual(results.map((bytes) => decode(bytes)), ["alpha", "bravo", "charlie"])
289 const shared = results.flatMap((left, index) => results.slice(index + 1).map((right) => [left, right] as const))
290 .find(([left, right]) => left.buffer === right.buffer)
291 assert.isDefined(shared)
292 assert.notStrictEqual(shared![0].byteOffset, shared![1].byteOffset)
293 })
294
295 it("preserves nested two-phase codec composition", () => {
296 const Inner = Schema.Struct({ id: Schema.Number, label: Schema.String })
297 const Outer = Schema.Struct({ id: Schema.String, inner: SchemaBinary.toCodec(Inner) })
298 const codec = SchemaBinary.toCodec(Outer)
299 const value = { id: "outer", inner: { id: 1, label: "inner" } }
300
301 assert.deepStrictEqual(Schema.decodeUnknownSync(codec)(Schema.encodeUnknownSync(codec)(value)), value)
302 })
303
304 it.effect("keeps concurrent fiber results readable after their producing turn", () => {
305 const Value = Schema.Struct({ id: Schema.Number, value: Schema.String })
306 const codec = SchemaBinary.toCodec(Value)
307 const encode = Schema.encodeUnknownEffect(codec)
308 const decode = Schema.decodeUnknownSync(codec)
309 const values = Array.from({ length: 100 }, (_, id) => ({ id, value: `value-${id}` }))
310
311 return Effect.gen(function*() {
312 const encoded = yield* Effect.forEach(
313 values,
314 (value) => encode(value).pipe(Effect.flatMap((bytes) => Effect.yieldNow.pipe(Effect.as(bytes)))),
315 { concurrency: "unbounded" }
316 )
317 yield* Effect.yieldNow
318 assert.deepStrictEqual(encoded.map((bytes) => decode(bytes)), values)
319 })
320 })
321 })
322
323 describe("numbers", () => {
324 it("encodes integral values as sign-magnitude varints", () => {
325 assert.deepStrictEqual(Array.from(encode(Schema.Number, 0)), [2, 0x20, 0])
326 assert.deepStrictEqual(Array.from(encode(Schema.Number, -0)), [2, 0x20, 1])
327 assert.deepStrictEqual(Array.from(encode(Schema.Number, 1)), [2, 0x20, 2])
328 assert.deepStrictEqual(Array.from(encode(Schema.Number, -1)), [2, 0x20, 3])
329 for (const value of [0, -0, 1, -1, 42, -42, 127, -128]) {
330 sameNumber(roundtrip(Schema.Number, value), value)
331 }
332 })
333
334 it("covers every varint width up to the seven byte cap, both signs", () => {
335 for (let width = 1; width <= 7; width++) {
336 // the widest magnitude this many varint bytes can hold, and the first
337 // magnitude that needs one more
338 const last = 2 ** (7 * width - 1) - 1
339 const next = 2 ** (7 * width - 1)
340 for (const sign of [1, -1]) {
341 const inside = sign * last
342 const outside = sign * next
343 assert.strictEqual(encode(Schema.Number, inside).length, 2 + width, `${inside}`)
344 sameNumber(roundtrip(Schema.Number, inside), inside)
345 // past the cap the value takes the f64 form instead of an eighth byte
346 assert.strictEqual(encode(Schema.Number, outside).length, width === 7 ? 10 : 3 + width, `${outside}`)
347 sameNumber(roundtrip(Schema.Number, outside), outside)
348 }
349 }
350 })
351
352 it("encodes short decimals as a mantissa varint plus one scale byte", () => {
353 assert.deepStrictEqual(Array.from(encode(Schema.Number, 1.5)), [3, 0x20, 30, 1])
354 assert.deepStrictEqual(Array.from(encode(Schema.Number, -0.5)), [3, 0x20, 11, 1])
355 assert.deepStrictEqual(Array.from(encode(Schema.Number, 0.001)), [3, 0x20, 2, 3])
356 assert.deepStrictEqual(Array.from(encode(Schema.Number, 12.5)), [4, 0x20, 0xFA, 0x01, 1])
357 for (const value of [0.1, 1.3, 4.7, 98.5, -123.456, 12345678.5, 0.00000001]) {
358 sameNumber(roundtrip(Schema.Number, value), value)
359 }
360 // the widest mantissa a decimal may carry, and the first one past it
361 const inside = 219902325555.1 // (2 ** 41 - 1) / 10
362 assert.strictEqual(encode(Schema.Number, inside).length, 9)
363 sameNumber(roundtrip(Schema.Number, inside), inside)
364 const outside = 219902325555.3
365 assert.strictEqual(encode(Schema.Number, outside).length, 10)
366 sameNumber(roundtrip(Schema.Number, outside), outside)
367 })
368
369 it("keeps f64 for values neither a capped varint nor a decimal can hold", () => {
370 const values = [
371 Number.EPSILON,
372 Number.NaN,
373 Number.POSITIVE_INFINITY,
374 Number.NEGATIVE_INFINITY,
375 Number.MAX_SAFE_INTEGER,
376 Number.MIN_SAFE_INTEGER,
377 Number.MAX_VALUE,
378 Math.PI,
379 1e-9,
380 2 ** 48,
381 -(2 ** 48)
382 ]
383 for (const value of values) {
384 assert.strictEqual(encode(Schema.Number, value).length, 10, `${value}`)
385 sameNumber(roundtrip(Schema.Number, value), value)
386 }
387 })
388
389 it("discriminates struct number forms with the field wire kind", () => {
390 const schema = Schema.Struct({ n: Schema.Number })
391 // length, envelope, five-byte field tag, payload
392 assert.strictEqual(encode(schema, { n: 7 }).length, 8)
393 assert.strictEqual(encode(schema, { n: 7.5 }).length, 10)
394 assert.strictEqual(encode(schema, { n: Math.PI }).length, 15)
395 assert.deepStrictEqual(roundtrip(schema, { n: 7 }), { n: 7 })
396 assert.deepStrictEqual(roundtrip(schema, { n: 7.5 }), { n: 7.5 })
397 assert.deepStrictEqual(roundtrip(schema, { n: Math.PI }), { n: Math.PI })
398 sameNumber(roundtrip(schema, { n: -0 }).n, -0)
399 })
400
401 it("uses the decimal wire kind in struct fields and record pairs", () => {
402 const Field = Schema.Struct({ n: Schema.Number.pipe(SchemaBinary.fieldId(1)) })
403 assert.deepStrictEqual([...encode(Field, { n: 7.5 })], [5, 0x20, 13, 0x96, 1, 1])
404
405 const Record = Schema.Record(Schema.String, Schema.Number)
406 assert.deepStrictEqual([...encode(Record, { x: 7.5 })], [8, 0x20, 0, 5, 13, 120, 0x96, 1, 1])
407 assert.deepStrictEqual(roundtrip(Record, { x: 7.5 }), { x: 7.5 })
408 })
409
410 it("packs a uniform number array behind one mode byte", () => {
411 const integers = [0, -0, 1, -1, 1000, -1000]
412 // length, envelope, count, mode, four one-byte and two two-byte varints
413 assert.strictEqual(encode(Schema.Array(Schema.Number), integers).length, 12)
414 const decoded = roundtrip(Schema.Array(Schema.Number), integers)
415 integers.forEach((value, index) => sameNumber(decoded[index], value))
416
417 // one short decimal moves the run to the packed decimal mode
418 const mixed = [1, 2.5]
419 // length, envelope, count, mode, a one-byte and a two-byte code
420 assert.strictEqual(encode(Schema.Array(Schema.Number), mixed).length, 7)
421 assert.deepStrictEqual(roundtrip(Schema.Array(Schema.Number), mixed), mixed)
422 const decimals = [-0, 0.25, 1e6, -12.75]
423 const decoded2 = roundtrip(Schema.Array(Schema.Number), decimals)
424 decimals.forEach((value, index) => sameNumber(decoded2[index], value))
425
426 // one value outside both varint and decimal moves the whole run to f64
427 const wide = [1, Math.PI]
428 assert.strictEqual(encode(Schema.Array(Schema.Number), wide).length, 20)
429 assert.deepStrictEqual(roundtrip(Schema.Array(Schema.Number), wide), wide)
430 assert.deepStrictEqual(roundtrip(Schema.Array(Schema.Number), []), [])
431 assert.deepStrictEqual(
432 roundtrip(Schema.Array(Schema.Array(Schema.Number)), [[1, 2], [], [3.5]]),
433 [[1, 2], [], [3.5]]
434 )
435 })
436
437 it("rejects non-number elements in uniform number arrays", () => {
438 const codec = SchemaBinary.toCodec(Schema.Array(Schema.Number))
439 for (const value of ["x", undefined, null, 1n]) {
440 const error = schemaError(() => Schema.encodeUnknownSync(codec)([1, value, 3] as any))
441 assert.include(error.message, "Expected a number")
442 assert.include(error.message, "at [1]")
443 }
444 })
445
446 it("length-prefixes each number slot of a tuple", () => {
447 const pair = Schema.Tuple([Schema.Number, Schema.Number])
448 assert.strictEqual(encode(pair, [1, 2]).length, 6)
449 assert.strictEqual(encode(pair, [1, 2.5]).length, 7)
450 assert.strictEqual(encode(pair, [1, Math.PI]).length, 13)
451 assert.deepStrictEqual(roundtrip(pair, [1, 2.5]), [1, 2.5])
452 assert.deepStrictEqual(roundtrip(pair, [1, Math.PI]), [1, Math.PI])
453 const rest = Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number])
454 assert.deepStrictEqual(roundtrip(rest, ["head", 1, 2.5, 3]), ["head", 1, 2.5, 3])
455 })
456
457 it("carries every form behind the union kind byte", () => {
458 const schema = Schema.Union([Schema.Number, Schema.String])
459 assert.strictEqual(encode(schema, 5).length, 4)
460 assert.strictEqual(encode(schema, 5.5).length, 5)
461 assert.strictEqual(encode(schema, Math.PI).length, 11)
462 assert.strictEqual(roundtrip(schema, 5), 5)
463 assert.strictEqual(roundtrip(schema, 5.5), 5.5)
464 assert.strictEqual(roundtrip(schema, Math.PI), Math.PI)
465 assert.strictEqual(roundtrip(schema, "x"), "x")
466 sameNumber(roundtrip(schema, -0), -0)
467
468 const tagged = Schema.Union([
469 Schema.Struct({ _tag: Schema.Literal("A"), n: Schema.Number }),
470 Schema.Struct({ _tag: Schema.Literal("B"), n: Schema.Int })
471 ])
472 assert.deepStrictEqual(roundtrip(tagged, { _tag: "A", n: 1.5 }), { _tag: "A", n: 1.5 })
473 assert.deepStrictEqual(roundtrip(tagged, { _tag: "B", n: -7 }), { _tag: "B", n: -7 })
474 })
475
476 it("parses a stream whose numbers change width", () => {
477 const values = [0, -0, 1, -1, 63, 64, 1_000_000, -1_000_000, 1.5, Number.NaN, Number.MAX_SAFE_INTEGER]
478 const bytes = concat(...values.map((value) => encode(Schema.Number, value)))
479 const parser = SchemaBinary.parser(Schema.Number)
480 const out: Array<number> = []
481 for (const byte of bytes) out.push(...parser.feedSync(Uint8Array.of(byte)))
482 parser.endSync()
483 assert.strictEqual(out.length, values.length)
484 values.forEach((value, index) => sameNumber(out[index], value))
485 })
486
487 it("emits a bare varint when the schema proves the value is an integer", () => {
488 assert.deepStrictEqual(Array.from(encode(Schema.Int, 1)), [2, 0x20, 2])
489 assert.deepStrictEqual(Array.from(encode(Schema.Natural, 1)), [2, 0x20, 2])
490 assert.deepStrictEqual(Array.from(encode(Schema.Number.check(Schema.isInt32()), 1)), [2, 0x20, 2])
491
492 // no mode byte for an array, no length prefix for a tuple slot
493 assert.strictEqual(encode(Schema.Array(Schema.Int), [1, 2, 3]).length, 6)
494 assert.strictEqual(encode(Schema.Array(Schema.Number), [1, 2, 3]).length, 7)
495 assert.strictEqual(encode(Schema.Tuple([Schema.Int, Schema.Int]), [1, 2]).length, 4)
496 assert.strictEqual(encode(Schema.Tuple([Schema.Number, Schema.Number]), [1, 2]).length, 6)
497
498 for (const value of [0, -0, 1, -1, 2 ** 48, -(2 ** 48), Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER]) {
499 sameNumber(roundtrip(Schema.Int, value), value)
500 sameNumber(roundtrip(Schema.Array(Schema.Int), [value])[0], value)
501 sameNumber(roundtrip(Schema.Struct({ n: Schema.Int }), { n: value }).n, value)
502 }
503 assert.deepStrictEqual(
504 roundtrip(Schema.Array(Schema.Int), [1, -2, 3]),
505 [1, -2, 3]
506 )
507 })
508
509 it("rejects a non-integer that reaches the integer layout", () => {
510 const codec = SchemaBinary.toCodec(Schema.Int)
511 assert.match(
512 schemaError(() => Schema.encodeUnknownSync(codec, { disableChecks: true })(1.5)).message,
513 /an integer/
514 )
515 })
516
517 it("rejects integer varints outside the safe-number range", () => {
518 const codec = SchemaBinary.toCodec(Schema.Int)
519 // Sign-magnitude encoding of Number.MAX_SAFE_INTEGER + 2. Converting it
520 // to number would silently round it down to MAX_SAFE_INTEGER + 1.
521 const unsafe = Uint8Array.of(9, 0x20, 0x82, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x20)
522
523 assert.match(schemaError(() => Schema.decodeUnknownSync(codec)(unsafe)).message, /integer/)
524 })
525
526 it("rejects malformed number payloads", () => {
527 const codec = SchemaBinary.toCodec(Schema.Number)
528 // nine payload bytes are neither the varint nor the f64 form
529 const wide = new Uint8Array([10, 0x20, 0, 0, 0, 0, 0, 0, 0, 0, 0])
530 assert.match(schemaError(() => Schema.decodeUnknownSync(codec)(wide)).message, /Expected f64/)
531 // a varint that never terminates inside its extent
532 const truncated = new Uint8Array([3, 0x20, 0x80, 0x80])
533 assert.match(schemaError(() => Schema.decodeUnknownSync(codec)(truncated)).message, /complete value/)
534 // a decimal tail whose scale byte is zero or out of range
535 const zeroScale = new Uint8Array([3, 0x20, 30, 0])
536 assert.match(schemaError(() => Schema.decodeUnknownSync(codec)(zeroScale)).message, /Expected decimal/)
537 const wideScale = new Uint8Array([3, 0x20, 30, 9])
538 assert.match(schemaError(() => Schema.decodeUnknownSync(codec)(wideScale)).message, /Expected decimal/)
539 const longTail = new Uint8Array([4, 0x20, 30, 1, 1])
540 assert.match(schemaError(() => Schema.decodeUnknownSync(codec)(longTail)).message, /Expected decimal/)
541
542 const arrayCodec = SchemaBinary.toCodec(Schema.Array(Schema.Number))
543 const run = Schema.encodeUnknownSync(arrayCodec)([1, 2, 3])
544 const unknownMode = run.slice()
545 unknownMode[3] = 3
546 assert.match(schemaError(() => Schema.decodeUnknownSync(arrayCodec)(unknownMode)).message, /Expected f64/)
547 const shortRun = run.slice(0, run.length - 1)
548 shortRun[0] -= 1
549 assert.match(schemaError(() => Schema.decodeUnknownSync(arrayCodec)(shortRun)).message, /complete value/)
550 })
551 })
552
553 describe("schema evolution", () => {
554 it("skips unknown fields, accepts field reorder, and leaves missing optionals absent", () => {
555 const Writer = Schema.Struct({ a: Schema.Number, extra: Schema.String, b: Schema.String })
556 const Reader = Schema.Struct({ b: Schema.String, a: Schema.Number, optional: Schema.optionalKey(Schema.Boolean) })
557 const bytes = encode(Writer, { a: 1, extra: "drop", b: "keep" })
558 assert.deepStrictEqual(Schema.decodeUnknownSync(SchemaBinary.toCodec(Reader))(bytes), { a: 1, b: "keep" })
559
560 const OrderedA = Schema.Struct({ a: Schema.Number, b: Schema.String })
561 const OrderedB = Schema.Struct({ b: Schema.String, a: Schema.Number })
562 assert.deepStrictEqual([...encode(OrderedA, { a: 1, b: "x" })], [...encode(OrderedB, { a: 1, b: "x" })])
563 })
564
565 it("orders extra keys by raw UTF-8 whatever the insertion order", () => {
566 const record = Schema.Record(Schema.String, Schema.Number)
567 // Code unit order puts the surrogate pair first, raw UTF-8 does not.
568 const wide = "\u{10000}"
569 const replacement = "\uFFFD"
570 assert.deepStrictEqual(
571 [...encode(record, { [wide]: 1, [replacement]: 2 })],
572 [...encode(record, { [replacement]: 2, [wide]: 1 })]
573 )
574 assert.deepStrictEqual(roundtrip(record, { [wide]: 1, [replacement]: 2 }), { [wide]: 1, [replacement]: 2 })
575 assert.deepStrictEqual(
576 [...encode(record, { b: 1, a: 2 })],
577 [...encode(record, { a: 2, b: 1 })]
578 )
579 })
580
581 it("keeps canonical key order when record shapes change within a frame", () => {
582 const rows = Schema.Array(Schema.Record(Schema.String, Schema.Number))
583 const mixed = [{ b: 2, a: 1 }, { a: 1, b: 2 }, { a: 1 }, { c: 3, a: 1 }, { a: 1, b: 2 }]
584 assert.deepStrictEqual(roundtrip(rows, mixed), mixed)
585 assert.deepStrictEqual(
586 [...encode(rows, mixed)],
587 [...encode(rows, [{ a: 1, b: 2 }, { b: 2, a: 1 }, { a: 1 }, { a: 1, c: 3 }, { b: 2, a: 1 }])]
588 )
589 })
590
591 it("uses fieldId as the encoded-side field identity", () => {
592 const Before = Schema.Struct({ oldName: Schema.String.pipe(SchemaBinary.fieldId(1)) })
593 const After = Schema.Struct({ newName: Schema.String.pipe(SchemaBinary.fieldId(1)) })
594 const bytes = encode(Before, { oldName: "value" })
595 assert.deepStrictEqual(Schema.decodeUnknownSync(SchemaBinary.toCodec(After))(bytes), { newName: "value" })
596 assert.throws(() => SchemaBinary.fieldId(0))
597 assert.throws(() => SchemaBinary.fieldId(1.5))
598 })
599
600 it("packs explicit field ids with scalar wire kinds", () => {
601 const schema = Schema.Struct({
602 n: Schema.Number.pipe(SchemaBinary.fieldId(1)),
603 flag: Schema.Boolean.pipe(SchemaBinary.fieldId(2)),
604 text: Schema.String.pipe(SchemaBinary.fieldId(3))
605 })
606 const value = { n: 7, flag: true, text: "x" }
607
608 assert.deepStrictEqual([...encode(schema, value)], [7, 0x20, 9, 14, 20, 24, 1, 120])
609 assert.deepStrictEqual(roundtrip(schema, value), value)
610 })
611
612 it("validates the full fieldId range and ignores tuple annotations", () => {
613 for (const id of [-1, Number.NaN, Number.POSITIVE_INFINITY, 0x1_0000_0000]) {
614 assert.throws(() => SchemaBinary.fieldId(id), /integer in \[1, 4294967295\]/)
615 }
616 assert.doesNotThrow(() => SchemaBinary.fieldId(0xFFFFFFFF))
617
618 const Max = Schema.Struct({ flag: Schema.Boolean.pipe(SchemaBinary.fieldId(0xFFFFFFFF)) })
619 const maxBytes = encode(Max, { flag: true })
620 assert.strictEqual(maxBytes.length, 7)
621 assert.deepStrictEqual([...maxBytes.subarray(2)], [0xFC, 0xFF, 0xFF, 0xFF, 0x7F])
622 assert.deepStrictEqual(roundtrip(Max, { flag: true }), { flag: true })
623
624 const plain = Schema.Tuple([Schema.String])
625 const annotated = Schema.Tuple([Schema.String.pipe(SchemaBinary.fieldId(123))])
626 assert.deepStrictEqual([...encode(annotated, ["value"])], [...encode(plain, ["value"])])
627 })
628
629 it("keeps canonically distinct Unicode field names distinct", () => {
630 const composed = "\u00E9"
631 const decomposed = "e\u0301"
632 const schema = Schema.Struct({ [composed]: Schema.String, [decomposed]: Schema.String })
633 const value = { [composed]: "composed", [decomposed]: "decomposed" }
634
635 assert.deepStrictEqual(roundtrip(schema, value), value)
636 })
637
638 it("skips unknown fields as opaque bytes and still rejects repeated unknown ids", () => {
639 const Writer = Schema.Struct({ future: Schema.Uint8Array.pipe(SchemaBinary.fieldId(7)) })
640 const Reader = Schema.Struct({})
641 const bytes = encode(Writer, { future: Uint8Array.of(0x80, 0xFF, 0x10) })
642 assert.deepStrictEqual(Schema.decodeUnknownSync(SchemaBinary.toCodec(Reader))(bytes), {})
643
644 const field = bytes.slice(2)
645 const duplicate = concat(Uint8Array.of(1 + field.length * 2, 0x20), field, field)
646 assert.match(
647 schemaError(() => Schema.decodeUnknownSync(SchemaBinary.toCodec(Reader))(duplicate)).message,
648 /unique field ids/
649 )
650 })
651
652 it("skips every scalar wire shape for unknown fields", () => {
653 const Reader = Schema.Struct({})
654 const bytes = Uint8Array.of(
655 10,
656 0x20,
657 7 * 8 + 5,
658 0,
659 0,
660 8 * 8 + 6,
661 0,
662 0,
663 0,
664 0,
665 9 * 8 + 7
666 )
667
668 assert.deepStrictEqual(Schema.decodeUnknownSync(SchemaBinary.toCodec(Reader))(bytes), {})
669 })
670
671 it("skips an unknown union member on an optional struct field", () => {
672 const A = Schema.Struct({ _tag: Schema.Literal("A"), n: Schema.Number })
673 const B = Schema.Struct({ _tag: Schema.Literal("B"), text: Schema.String })
674 const Writer = Schema.Struct({ event: Schema.optionalKey(Schema.Union([A, B])) })
675 const Reader = Schema.Struct({ event: Schema.optionalKey(Schema.Union([A])) })
676 assert.deepStrictEqual(
677 Schema.decodeUnknownSync(SchemaBinary.toCodec(Reader))(encode(Writer, { event: { _tag: "B", text: "new" } })),
678 {}
679 )
680 assert.deepStrictEqual(
681 Schema.decodeUnknownSync(SchemaBinary.toCodec(Reader))(encode(Writer, { event: { _tag: "A", n: 1 } })),
682 { event: { _tag: "A", n: 1 } }
683 )
684 })
685
686 it("keeps tuple sentinels in variant payloads", () => {
687 const A = Schema.Tuple([Schema.Literal("A"), Schema.Number])
688 const B = Schema.Tuple([Schema.Literal("B"), Schema.String])
689 const schema = Schema.Union([A, B])
690 assert.deepStrictEqual(roundtrip(schema, ["A", 1]), ["A", 1])
691 assert.deepStrictEqual(roundtrip(schema, ["B", "value"]), ["B", "value"])
692 })
693
694 it("rejects unknown union members in positional slots", () => {
695 const A = Schema.Struct({ _tag: Schema.Literal("A") })
696 const B = Schema.Struct({ _tag: Schema.Literal("B") })
697 const WriterArray = Schema.Array(Schema.Union([A, B]))
698 const ReaderArray = Schema.Array(Schema.Union([A]))
699 assert.match(
700 schemaError(() =>
701 Schema.decodeUnknownSync(SchemaBinary.toCodec(ReaderArray))(encode(WriterArray, [{ _tag: "B" }]))
702 ).message,
703 /Missing key/
704 )
705
706 const WriterTuple = Schema.Tuple([Schema.optionalKey(Schema.Union([A, B]))])
707 const ReaderTuple = Schema.Tuple([Schema.optionalKey(Schema.Union([A]))])
708 assert.match(
709 schemaError(() =>
710 Schema.decodeUnknownSync(SchemaBinary.toCodec(ReaderTuple))(encode(WriterTuple, [{ _tag: "B" }]))
711 ).message,
712 /known union member/
713 )
714 })
715
716 it("reports the array or tuple index for an unknown positional union member", () => {
717 const A = Schema.Struct({ _tag: Schema.Literal("A") })
718 const B = Schema.Struct({ _tag: Schema.Literal("B") })
719 const WriterArray = Schema.Struct({ xs: Schema.Array(Schema.Union([A, B])) })
720 const ReaderArray = Schema.Struct({ xs: Schema.Array(Schema.Union([A])) })
721 assert.match(
722 schemaError(() =>
723 Schema.decodeUnknownSync(SchemaBinary.toCodec(ReaderArray))(
724 encode(WriterArray, { xs: [{ _tag: "B" }] })
725 )
726 ).message,
727 /Missing key\n at \["xs"\]\[0\]/
728 )
729
730 const WriterTuple = Schema.Struct({ xs: Schema.Tuple([Schema.String, Schema.Union([A, B])]) })
731 const ReaderTuple = Schema.Struct({ xs: Schema.Tuple([Schema.String, Schema.Union([A])]) })
732 assert.match(
733 schemaError(() =>
734 Schema.decodeUnknownSync(SchemaBinary.toCodec(ReaderTuple))(
735 encode(WriterTuple, { xs: ["head", { _tag: "B" }] })
736 )
737 ).message,
738 /Missing key\n at \["xs"\]\[1\]/
739 )
740 })
741
742 it("uses kind tags for mixed enums", () => {
743 const Mixed = { Text: "text", Code: 1 } as const
744 const schema = Schema.Enum(Mixed)
745 assert.strictEqual(roundtrip(schema, Mixed.Text), "text")
746 assert.strictEqual(roundtrip(schema, Mixed.Code), 1)
747 })
748
749 it("encodes record entries in the reserved field zero map", () => {
750 const schema = Schema.Record(Schema.String, Schema.Number)
751 const value = { z: 1, a: 2 }
752 assert.deepStrictEqual(roundtrip(schema, value), value)
753 assert.deepStrictEqual([...encode(schema, value)], [...encode(schema, { a: 2, z: 1 })])
754 })
755
756 it("round-trips __proto__ named fields without changing the prototype", () => {
757 const schema = Schema.Struct({ ["__proto__"]: Schema.String, ok: Schema.String })
758 const value = { ["__proto__"]: "named", ok: "yes" }
759
760 for (const result of [roundtrip(schema, value), roundtripFingerprint(schema, value)]) {
761 assert.strictEqual(Object.getPrototypeOf(result), Object.prototype)
762 assert.isTrue(Object.prototype.propertyIsEnumerable.call(result, "__proto__"))
763 assert.strictEqual(result.__proto__, "named")
764 assert.strictEqual(({} as Record<string, unknown>).polluted, undefined)
765 }
766 })
767
768 it("round-trips __proto__ record entries with object values without changing the prototype", () => {
769 const schema = Schema.Record(Schema.String, Schema.Struct({ polluted: Schema.Boolean }))
770 const value = { ["__proto__"]: { polluted: true } }
771
772 for (const result of [roundtrip(schema, value), roundtripFingerprint(schema, value)]) {
773 assert.strictEqual(Object.getPrototypeOf(result), Object.prototype)
774 assert.isTrue(Object.prototype.propertyIsEnumerable.call(result, "__proto__"))
775 assert.deepStrictEqual(result.__proto__, { polluted: true })
776 assert.strictEqual(({} as Record<string, unknown>).polluted, undefined)
777 }
778 })
779
780 it("round-trips __proto__ tagged-union sentinels without changing the prototype", () => {
781 const A = Schema.Struct({ ["__proto__"]: Schema.Literal("A"), value: Schema.String })
782 const B = Schema.Struct({ ["__proto__"]: Schema.Literal("B"), value: Schema.String })
783 const schema = Schema.Union([A, B])
784 const value = { ["__proto__"]: "A" as const, value: "a" }
785
786 for (const result of [roundtrip(schema, value), roundtripFingerprint(schema, value)]) {
787 assert.strictEqual(Object.getPrototypeOf(result), Object.prototype)
788 assert.isTrue(Object.prototype.propertyIsEnumerable.call(result, "__proto__"))
789 assert.strictEqual(result.__proto__, "A")
790 assert.strictEqual(({} as Record<string, unknown>).polluted, undefined)
791 }
792 })
793 })
794
795 describe("struct row runs", () => {
796 const Row = Schema.Struct({
797 id: Schema.String,
798 label: Schema.String,
799 tags: Schema.Array(Schema.String),
800 attributes: Schema.Record(Schema.String, Schema.String)
801 })
802 const rows = Array.from({ length: 6 }, (_, index) => ({
803 id: `row-${index}`,
804 label: index % 2 === 0 ? "even" : "odd",
805 tags: index % 3 === 0 ? ["red", "blue"] : ["blue"],
806 attributes: { region: "eu", worker: `w${index}` }
807 }))
808 const nonEmptyRows = rows as [typeof rows[number], ...Array<typeof rows[number]>]
809
810 it("shares field ids and repeated strings across rows", () => {
811 assert.deepStrictEqual(roundtrip(Schema.Array(Row), rows), rows)
812 assert.deepStrictEqual(roundtrip(Schema.NonEmptyArray(Row), nonEmptyRows), nonEmptyRows)
813 const one = encode(Schema.Array(Row), rows.slice(0, 1)).length
814 const six = encode(Schema.Array(Row), rows).length
815 assert.isBelow(six, one * 6)
816 })
817
818 it("keeps evolution rules inside a run", () => {
819 const Reader = Schema.Struct({
820 tags: Schema.Array(Schema.String),
821 id: Schema.String,
822 later: Schema.optionalKey(Schema.Boolean)
823 })
824 assert.deepStrictEqual(
825 Schema.decodeUnknownSync(SchemaBinary.toCodec(Schema.Array(Reader)))(encode(Schema.Array(Row), rows)),
826 rows.map(({ id, tags }) => ({ id, tags }))
827 )
828 })
829
830 it("declares a new shape whenever the present fields change", () => {
831 assert.deepStrictEqual(roundtrip(Schema.Array(Varying), varying), varying)
832 })
833
834 it("interns index signature keys on the row struct itself", () => {
835 const WithRest = Schema.StructWithRest(Schema.Struct({ id: Schema.String }), [
836 Schema.Record(Schema.String, Schema.String)
837 ])
838 const values = [{ id: "a", note: "1" }, { id: "b", note: "2" }, { id: "c" }]
839 assert.deepStrictEqual(roundtrip(Schema.Array(WithRest), values), values)
840 })
841
842 it("runs nested inside a run", () => {
843 const Inner = Schema.Struct({ k: Schema.String, v: Schema.String })
844 const Outer = Schema.Struct({ name: Schema.String, items: Schema.Array(Inner) })
845 const outers = Array.from({ length: 4 }, (_, index) => ({
846 name: `n${index}`,
847 items: [{ k: "a", v: "1" }, { k: "b", v: "1" }]
848 }))
849 assert.deepStrictEqual(roundtrip(Schema.Array(Outer), outers), outers)
850 })
851
852 it("keeps rows independent past the 30 field shape limit", () => {
853 const fields: Record<string, Schema.String> = {}
854 const value: Record<string, string> = {}
855 for (let i = 0; i < 35; i++) {
856 fields[`f${i}`] = Schema.String
857 value[`f${i}`] = `v${i}`
858 }
859 const Wide = Schema.Struct(fields)
860 assert.deepStrictEqual(roundtrip(Schema.Array(Wide), [value, value, value]), [value, value, value])
861 })
862
863 it("rejects an unknown row shape and an out of range back-reference", () => {
864 const Simple = Schema.Array(Schema.Struct({ a: Schema.String }))
865 const codec = SchemaBinary.toCodec(Simple)
866 // The second row reuses shape 0 and back-references the first `a`, so its
867 // last two bytes are the shape code and the region code.
868 const bytes = encode(Simple, [{ a: "x" }, { a: "x" }])
869 assert.deepStrictEqual([...bytes.subarray(bytes.length - 2)], [1, 1])
870
871 const unknownShape = bytes.slice()
872 unknownShape[unknownShape.length - 2] = 9
873 assert.match(schemaError(() => Schema.decodeUnknownSync(codec)(unknownShape)).message, /row shape/)
874
875 const unknownRef = bytes.slice()
876 unknownRef[unknownRef.length - 1] = 5
877 assert.match(schemaError(() => Schema.decodeUnknownSync(codec)(unknownRef)).message, /back-reference/)
878 })
879
880 it("keeps a skipped field's own run out of the reader's tables", () => {
881 const Writer = Schema.Struct({
882 keep: Schema.String,
883 gone: Schema.Array(Schema.Struct({ p: Schema.String, q: Schema.String }))
884 })
885 const Reader = Schema.Struct({ keep: Schema.String })
886 const values = Array.from({ length: 4 }, (_, index) => ({
887 keep: `k${index}`,
888 gone: [{ p: "z", q: "z" }, { p: "z", q: "z" }]
889 }))
890 assert.deepStrictEqual(
891 Schema.decodeUnknownSync(SchemaBinary.toCodec(Schema.Array(Reader)))(encode(Schema.Array(Writer), values)),
892 values.map(({ keep }) => ({ keep }))
893 )
894 })
895
896 it("interns every index signature key, including ones it has no signature for", () => {
897 const Writer = Schema.Struct({ attrs: Schema.Record(Schema.String, Schema.String) })
898 const Reader = Schema.Struct({
899 attrs: Schema.Record(
900 Schema.String.check(Schema.makeFilter((key: string) => key.startsWith("keep"))),
901 Schema.String
902 )
903 })
904 // The reader drops `drop` but must still number it, or row 2's reference
905 // to `keep1` resolves to `keep2`.
906 const values = [
907 { attrs: { drop: "d", keep1: "a" } },
908 { attrs: { keep2: "b" } },
909 { attrs: { keep1: "c" } }
910 ]
911 assert.deepStrictEqual(
912 Schema.decodeUnknownSync(SchemaBinary.toCodec(Schema.Array(Reader)))(encode(Schema.Array(Writer), values)),
913 [{ attrs: { keep1: "a" } }, { attrs: { keep2: "b" } }, { attrs: { keep1: "c" } }]
914 )
915 })
916
917 it("keeps two fields sharing one layout on separate tables", () => {
918 const Writer = Schema.Struct({ keep: Schema.String, gone: Schema.String })
919 const Reader = Schema.Struct({ keep: Schema.String })
920 const values = [
921 { keep: "first", gone: "other" },
922 { keep: "other", gone: "first" },
923 { keep: "first", gone: "first" }
924 ]
925 assert.deepStrictEqual(
926 Schema.decodeUnknownSync(SchemaBinary.toCodec(Schema.Array(Reader)))(encode(Schema.Array(Writer), values)),
927 values.map(({ keep }) => ({ keep }))
928 )
929 })
930
931 it("holds references past the point a slot switches to a map", () => {
932 const Many = Schema.Struct({ v: Schema.String })
933 const many = Array.from({ length: 60 }, (_, index) => ({ v: `v${index % 21}` }))
934 assert.deepStrictEqual(roundtrip(Schema.Array(Many), many), many)
935 })
936
937 it("round-trips a slot that stops interning values that never repeat", () => {
938 const Many = Schema.Struct({ v: Schema.String })
939 // All-distinct values disable the writer's table at the decide point;
940 // repeats past it are written literally and must still round-trip.
941 const many = Array.from({ length: 160 }, (_, index) => ({ v: `value-${index % 80}` }))
942 assert.deepStrictEqual(roundtrip(Schema.Array(Many), many), many)
943 const keyed = Schema.Struct({ attributes: Schema.Record(Schema.String, Schema.String) })
944 const rows = Array.from({ length: 160 }, (_, index) => ({
945 attributes: { [`key-${index % 80}`]: `w${index}` }
946 }))
947 assert.deepStrictEqual(roundtrip(Schema.Array(keyed), rows), rows)
948 })
949
950 it("gives each recursive occurrence its own run", () => {
951 interface Node {
952 readonly name: string
953 readonly children: ReadonlyArray<Node>
954 }
955 const Node = Schema.Struct({
956 name: Schema.String,
957 children: Schema.Array(Schema.suspend((): Schema.Codec<Node> => Node))
958 })
959 const tree = [
960 { name: "a", children: [{ name: "x", children: [] }, { name: "x", children: [] }] },
961 { name: "a", children: [{ name: "y", children: [{ name: "x", children: [] }] }] }
962 ]
963 assert.deepStrictEqual(roundtrip(Schema.Array(Node), tree), tree)
964 })
965
966 it("reports the row index and field name for a missing key", () => {
967 const Required = Schema.Struct({ a: Schema.String, b: Schema.String })
968 assert.match(
969 schemaError(() =>
970 Schema.encodeUnknownSync(SchemaBinary.toCodec(Schema.Array(Required)))(
971 [{ a: "1", b: "2" }, { a: "1" } as never]
972 )
973 ).message,
974 /Missing key\n at \[1\]\["b"\]/
975 )
976 })
977
978 it("runs rows in fingerprint mode with mask-declared shapes", () => {
979 assert.deepStrictEqual(roundtripFingerprint(Schema.Array(Row), rows), rows)
980 // repeated strings are interned across rows
981 const one = encodeFingerprint(Schema.Array(Row), rows.slice(0, 1)).length
982 const six = encodeFingerprint(Schema.Array(Row), rows).length
983 assert.isBelow(six, one * 6)
984 // the mask shape has no id list, so fingerprint rows undercut default rows
985 assert.isBelow(six, encode(Schema.Array(Row), rows).length)
986 })
987
988 it("declares fingerprint shapes whenever the present fields change", () => {
989 assert.deepStrictEqual(roundtripFingerprint(Schema.Array(Varying), varying), varying)
990 })
991
992 it("rejects a fingerprint shape mask with unknown bits", () => {
993 const codec = SchemaBinary.toCodec(Schema.Array(Row), { fingerprint: true })
994 const bytes = Schema.encodeUnknownSync(codec)(rows.slice(0, 1)).slice()
995 // length | envelope | 8-byte fingerprint | count | row length | code 0 | mask
996 assert.strictEqual(bytes[12], 0)
997 bytes[13] = 0x7F
998 assert.match(
999 schemaError(() => Schema.decodeUnknownSync(codec)(bytes)).message,
1000 /known row shape/
1001 )
1002 })
1003 })
1004
1005 describe("parser", () => {
1006 it("accepts empty input and spends the parser after a successful end", () => {
1007 const parser = SchemaBinary.parser(Schema.String)
1008 assert.deepStrictEqual(parser.feedSync(new Uint8Array()), [])
1009 parser.endSync()
1010 assert.match(schemaError(() => parser.endSync()).message, /parser is spent/)
1011 })
1012
1013 it("parses concatenated frames split across chunks", () => {
1014 const schema = Schema.Struct({ a: Schema.Number })
1015 const first = encode(schema, { a: 1 })
1016 const second = encode(schema, { a: 2 })
1017 const bytes = concat(first, second)
1018 const parser = SchemaBinary.parser(schema)
1019 assert.deepStrictEqual(parser.feedSync(bytes.slice(0, first.length + 3)), [{ a: 1 }])
1020 assert.deepStrictEqual(parser.feedSync(bytes.slice(first.length + 3)), [{ a: 2 }])
1021 parser.endSync()
1022 assert.match(schemaError(() => parser.feedSync(new Uint8Array())).message, /parser is spent/)
1023 })
1024
1025 it("parses exact nested structs, arrays, and string records", () => {
1026 const schema = Schema.Struct({
1027 id: Schema.Number,
1028 active: Schema.Boolean,
1029 tags: Schema.Array(Schema.String),
1030 metrics: Schema.Record(Schema.String, Schema.Number)
1031 })
1032 const value = { id: 1, active: true, tags: ["a", "b"], metrics: { x: 1, y: 2 } }
1033 const parser = SchemaBinary.parser(schema)
1034
1035 assert.deepStrictEqual(parser.feedSync(encode(schema, value)), [value])
1036 parser.endSync()
1037 })
1038
1039 it("fails deeply nested recursive frames with SchemaError", () => {
1040 type Nested = ReadonlyArray<Nested>
1041 let Nested: Schema.Codec<Nested>
1042 Nested = Schema.Array(Schema.suspend(() => Nested))
1043 const parser = SchemaBinary.parser(Nested)
1044
1045 assert.match(schemaError(() => parser.feedSync(nestedArrayFrame(10_000))).message, /nesting depth at most/)
1046 assert.match(schemaError(() => parser.feedSync(new Uint8Array())).message, /parser is spent/)
1047 })
1048
1049 it("keeps __proto__ safe on the exact parser path", () => {
1050 const schema = Schema.Struct({ ["__proto__"]: Schema.String, value: Schema.Number })
1051 const value = { ["__proto__"]: "own", value: 1 }
1052 const parser = SchemaBinary.parser(schema)
1053 const [result] = parser.feedSync(encode(schema, value))
1054
1055 assert.strictEqual(Object.getPrototypeOf(result), Object.prototype)
1056 assert.isTrue(Object.prototype.propertyIsEnumerable.call(result, "__proto__"))
1057 assert.strictEqual(result.__proto__, "own")
1058 assert.strictEqual(({} as Record<string, unknown>).polluted, undefined)
1059 parser.endSync()
1060 })
1061
1062 it("retains Schema predicates erased by the binary layout", () => {
1063 const rejects = (
1064 reader: Schema.Constraint,
1065 writer: Schema.Codec<unknown, unknown>,
1066 value: unknown
1067 ) => {
1068 const parser = SchemaBinary.parser(reader)
1069 assert.isTrue(Schema.isSchemaError(schemaError(() => parser.feedSync(encode(writer, value)))))
1070 }
1071
1072 rejects(Schema.TemplateLiteral(["a"]), Schema.String, "zzz")
1073 rejects(Schema.Literal("a"), Schema.String, "zzz")
1074 rejects(Schema.Enum({ A: "a", B: "b" }), Schema.String, "zzz")
1075 rejects(Schema.UniqueSymbol(Symbol.for("expected")), Schema.Symbol, Symbol.for("other"))
1076 rejects(Schema.ObjectKeyword, Schema.Unknown, 1)
1077 })
1078
1079 it("retains Schema parsing for numeric record keys", () => {
1080 const Writer = Schema.Record(Schema.String, Schema.Number)
1081 const Reader = Schema.Record(Schema.Number, Schema.Number)
1082 const parser = SchemaBinary.parser(Reader)
1083
1084 assert.deepStrictEqual(parser.feedSync(encode(Writer, { "01": 1, "1e2": 2, "-0": 3 })), [{
1085 "0": 3,
1086 "1": 1,
1087 "100": 2
1088 }])
1089 parser.endSync()
1090 })
1091
1092 it("retains transformations, checks, unions, declarations, and recursive validation", () => {
1093 const transformed = SchemaBinary.parser(Schema.NumberFromString)
1094 assert.deepStrictEqual(transformed.feedSync(encode(Schema.NumberFromString, 123)), [123])
1095 transformed.endSync()
1096
1097 const NonNegative = Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))
1098 assert.match(
1099 schemaError(() => SchemaBinary.parser(NonNegative).feedSync(encode(Schema.Number, -1))).message,
1100 /greater than or equal to 0/
1101 )
1102
1103 const CheckedUnion = Schema.Union([NonNegative, Schema.String])
1104 const BroadUnion = Schema.Union([Schema.Number, Schema.String])
1105 assert.match(
1106 schemaError(() => SchemaBinary.parser(CheckedUnion).feedSync(encode(BroadUnion, -1))).message,
1107 /greater than or equal to 0/
1108 )
1109
1110 const CheckedOption = Schema.Option(NonNegative)
1111 const BroadOption = Schema.Option(Schema.Number)
1112 assert.match(
1113 schemaError(() => SchemaBinary.parser(CheckedOption).feedSync(encode(BroadOption, Option.some(-1)))).message,
1114 /greater than or equal to 0/
1115 )
1116
1117 interface Node {
1118 readonly value: number
1119 readonly children: ReadonlyArray<Node>
1120 }
1121 let Writer: Schema.Codec<Node>
1122 Writer = Schema.Struct({
1123 value: Schema.Number,
1124 children: Schema.Array(Schema.suspend(() => Writer))
1125 })
1126 let Reader: Schema.Codec<Node>
1127 Reader = Schema.Struct({
1128 value: NonNegative,
1129 children: Schema.Array(Schema.suspend(() => Reader))
1130 })
1131 const invalid = { value: 1, children: [{ value: -1, children: [] }] }
1132 assert.match(
1133 schemaError(() => SchemaBinary.parser(Reader).feedSync(encode(Writer, invalid))).message,
1134 /greater than or equal to 0/
1135 )
1136 })
1137
1138 it("retains partial frames across one-byte feeds", () => {
1139 const bytes = concat(...Array.from({ length: 100 }, (_, i) => encode(Schema.Number, i)))
1140 const parser = SchemaBinary.parser(Schema.Number)
1141 const values: Array<number> = []
1142 for (const byte of bytes) values.push(...parser.feedSync(Uint8Array.of(byte)))
1143 parser.endSync()
1144 assert.deepStrictEqual(values, Array.from({ length: 100 }, (_, i) => i))
1145 })
1146
1147 it("waits for a fragmented multi-byte frame header", () => {
1148 const value = "x".repeat(300)
1149 const bytes = encode(Schema.String, value)
1150 const parser = SchemaBinary.parser(Schema.String)
1151
1152 assert.deepStrictEqual(parser.feedSync(bytes.slice(0, 1)), [])
1153 assert.deepStrictEqual(parser.feedSync(bytes.slice(1)), [value])
1154 parser.endSync()
1155 })
1156
1157 it("uses the bigint header path for longer safe lengths", () => {
1158 // Canonical uvarint(Number.MAX_SAFE_INTEGER): seven continuation groups
1159 // followed by the final four bits. maxFrameSize rejects it before the
1160 // parser waits for an impractically large body.
1161 const bytes = Uint8Array.of(0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F)
1162 const parser = SchemaBinary.parser(Schema.String, { maxFrameSize: 1 })
1163
1164 assert.deepStrictEqual(parser.feedSync(bytes.slice(0, 7)), [])
1165 assert.match(schemaError(() => parser.feedSync(bytes.slice(7))).message, /frame within maxFrameSize/)
1166 })
1167
1168 it("keeps parser index-signature caching bounded with FIFO eviction", () => {
1169 let checks = 0
1170 const Key = Schema.String.check(Schema.makeFilter((_: string) => {
1171 checks++
1172 return true
1173 }))
1174 const Writer = Schema.Record(Schema.String, Schema.Number)
1175 const parser = SchemaBinary.parser(Schema.Record(Key, Schema.Number))
1176 const feed = (key: string) => {
1177 const value = { [key]: 1 }
1178 assert.deepStrictEqual(parser.feedSync(encode(Writer, value)), [value])
1179 }
1180
1181 // The parser cache holds 256 attacker-controlled (layout, key) pairs.
1182 // The 257th evicts key-0, while key-1 remains cached.
1183 for (let i = 0; i < 257; i++) feed(`key-${i}`)
1184 const beforeHit = checks
1185 feed("key-1")
1186 const hitChecks = checks - beforeHit
1187 const beforeMiss = checks
1188 feed("key-0")
1189 const missChecks = checks - beforeMiss
1190
1191 assert.strictEqual(missChecks, hitChecks + 1)
1192 parser.endSync()
1193 })
1194
1195 it("does not thrash the index-signature cache above its bound", () => {
1196 let checks = 0
1197 const Key = Schema.String.check(Schema.makeFilter((_: string) => {
1198 checks++
1199 return true
1200 }))
1201 const Writer = Schema.Record(Schema.String, Schema.Number)
1202 const Reader = Schema.Record(Key, Schema.Number)
1203 const allHitParser = SchemaBinary.parser(Reader)
1204 const allHitValue = Object.fromEntries(Array.from({ length: 256 }, (_, index) => [`hit-${index}`, index]))
1205 const allHitBytes = encode(Writer, allHitValue)
1206
1207 assert.deepStrictEqual(allHitParser.feedSync(allHitBytes), [allHitValue])
1208 const beforeAllHit = checks
1209 assert.deepStrictEqual(allHitParser.feedSync(allHitBytes), [allHitValue])
1210 const allHitChecksPerKey = (checks - beforeAllHit) / 256
1211 allHitParser.endSync()
1212
1213 const parser = SchemaBinary.parser(Reader)
1214 const value = Object.fromEntries(Array.from({ length: 300 }, (_, index) => [`key-${index}`, index]))
1215 const bytes = encode(Writer, value)
1216
1217 assert.deepStrictEqual(parser.feedSync(bytes), [value])
1218 const firstChecks = checks
1219 assert.deepStrictEqual(parser.feedSync(bytes), [value])
1220 const secondChecks = checks - firstChecks
1221 const misses = secondChecks - allHitChecksPerKey * 300
1222
1223 // Derive the decoder's per-key work from a fully warm parser. Each cache
1224 // miss adds one classification check beyond that all-hit baseline.
1225 assert.strictEqual(misses, 45)
1226 parser.endSync()
1227 })
1228
1229 it("isolates index-signature caches by parser options", () => {
1230 const Key = Schema.String.check(Schema.makeFilter((key: string) => key.startsWith("allowed-")))
1231 const Writer = Schema.Record(Schema.String, Schema.Number)
1232 const Reader = Schema.Record(Key, Schema.Number)
1233 const bytes = encode(Writer, { denied: 1 })
1234 const strict = SchemaBinary.parser(Reader)
1235 const unchecked = SchemaBinary.parser(Reader, { disableChecks: true })
1236
1237 assert.deepStrictEqual(strict.feedSync(bytes), [{}])
1238 assert.deepStrictEqual(unchecked.feedSync(bytes), [{ denied: 1 }])
1239 strict.endSync()
1240 unchecked.endSync()
1241 })
1242
1243 it("keeps nested SchemaBinary decodes independent from the outer reader", () => {
1244 const Inner = Schema.Struct({ id: Schema.Number, label: Schema.String })
1245 const Outer = Schema.Struct({ id: Schema.String, inner: SchemaBinary.toCodec(Inner) })
1246 const first = { id: "first", inner: { id: 1, label: "one" } }
1247 const second = { id: "second", inner: { id: 2, label: "two" } }
1248 const parser = SchemaBinary.parser(Outer)
1249
1250 assert.deepStrictEqual(parser.feedSync(concat(encode(Outer, first), encode(Outer, second))), [first, second])
1251 parser.endSync()
1252 })
1253
1254 it("returns a checked-out one-shot reader after an exceptional decode", () => {
1255 const codec = SchemaBinary.toCodec(Schema.String)
1256 const decode = Schema.decodeUnknownSync(codec)
1257
1258 assert.match(schemaError(() => decode(Uint8Array.of(2, 0x20, 0xFF))).message, /utf-8/)
1259 assert.strictEqual(decode(encode(Schema.String, "after failure")), "after failure")
1260 })
1261
1262 it("delivers completed values before reporting a later failure", () => {
1263 const good = encode(Schema.Number, 1)
1264 const bad = encode(Schema.Number, 2).slice(0, 2)
1265 const parser = SchemaBinary.parser(Schema.Number)
1266 assert.deepStrictEqual(parser.feedSync(concat(good, bad)), [1])
1267 assert.match(schemaError(() => parser.endSync()).message, /complete value/)
1268 assert.match(schemaError(() => parser.feedSync(new Uint8Array())).message, /parser is spent/)
1269 })
1270
1271 it("stashes a malformed complete frame after returning earlier values", () => {
1272 const good = encode(Schema.Number, 1)
1273 const bad = encode(Schema.Number, 2).slice()
1274 bad[1] = 0x10
1275 const parser = SchemaBinary.parser(Schema.Number)
1276
1277 assert.deepStrictEqual(parser.feedSync(concat(good, bad)), [1])
1278 assert.match(schemaError(() => parser.feedSync(new Uint8Array())).message, /version 2 envelope, flags 0/)
1279 assert.match(schemaError(() => parser.endSync()).message, /parser is spent/)
1280 })
1281
1282 it("enforces maxFrameSize", () => {
1283 const bytes = encode(Schema.String, "too large")
1284 const parser = SchemaBinary.parser(Schema.String, { maxFrameSize: 2 })
1285 assert.match(schemaError(() => parser.feedSync(bytes)).message, /frame within maxFrameSize/)
1286
1287 const exact = SchemaBinary.parser(Schema.String, { maxFrameSize: bytes[0] })
1288 assert.deepStrictEqual(exact.feedSync(bytes), ["too large"])
1289 exact.endSync()
1290 })
1291
1292 it("fails a ten-byte unterminated length immediately", () => {
1293 const bytes = new Uint8Array(10).fill(0x80)
1294 const parser = SchemaBinary.parser(Schema.String, { reportInput: true })
1295 const error = schemaError(() => parser.feedSync(bytes))
1296 assert.match(error.message, /uvarint/)
1297 assert.isTrue(SchemaIssue.hasInput(error.issue))
1298 })
1299
1300 it("rejects a terminated header above the safe-integer bound", () => {
1301 const bytes = Uint8Array.of(0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x10)
1302 const parser = SchemaBinary.parser(Schema.String)
1303
1304 assert.match(schemaError(() => parser.feedSync(bytes)).message, /safe integer length/)
1305 assert.match(schemaError(() => parser.feedSync(new Uint8Array())).message, /parser is spent/)
1306 })
1307
1308 it.effect("wraps feed and end in SchemaError effects", () =>
1309 Effect.gen(function*() {
1310 const parser = SchemaBinary.parser(Schema.Number)
1311 const values = yield* parser.feed(encode(Schema.Number, 1))
1312 assert.deepStrictEqual(values, [1])
1313 yield* parser.end
1314 }))
1315
1316 it.effect("fails malformed feed and truncated end through the Effect surface", () =>
1317 Effect.gen(function*() {
1318 const malformed = SchemaBinary.parser(Schema.Boolean)
1319 const feedError = yield* malformed.feed(Uint8Array.of(2, 0x20, 2)).pipe(Effect.flip)
1320 assert.isTrue(Schema.isSchemaError(feedError))
1321
1322 const truncated = SchemaBinary.parser(Schema.String)
1323 yield* truncated.feed(Uint8Array.of(2, 0x20))
1324 const endError = yield* truncated.end.pipe(Effect.flip)
1325 assert.isTrue(Schema.isSchemaError(endError))
1326 }))
1327 })
1328
1329 describe("native declarations", () => {
1330 it("round-trips bigint, Date, bytes, Option, and Result", () => {
1331 assert.strictEqual(roundtrip(Schema.BigInt, -12345678901234567890n), -12345678901234567890n)
1332 assert.strictEqual(roundtrip(Schema.Date, new Date(-123456789)).getTime(), -123456789)
1333 assert.deepStrictEqual([...roundtrip(Schema.Uint8Array, Uint8Array.of(0, 1, 255))], [0, 1, 255])
1334
1335 const option = roundtrip(Schema.Option(Schema.String), Option.some("value"))
1336 assert.isTrue(Option.isSome(option))
1337 if (Option.isSome(option)) assert.strictEqual(option.value, "value")
1338
1339 const result = roundtrip(Schema.Result(Schema.Number, Schema.String), Result.fail("error"))
1340 assert.isTrue(Result.isFailure(result))
1341 if (Result.isFailure(result)) assert.strictEqual(result.failure, "error")
1342 })
1343
1344 it("round-trips Duration and normalized BigDecimal", () => {
1345 const duration = roundtrip(Schema.Duration, Duration.millis(1.5))
1346 assert.strictEqual(Duration.toNanosUnsafe(duration), 1_500_000n)
1347 assert.strictEqual(roundtrip(Schema.Duration, Duration.infinity), Duration.infinity)
1348 assert.strictEqual(roundtrip(Schema.Duration, Duration.negativeInfinity), Duration.negativeInfinity)
1349 assert.strictEqual(Duration.toNanosUnsafe(roundtrip(Schema.Duration, Duration.nanos(-7n))), -7n)
1350
1351 const decimal = roundtrip(Schema.BigDecimal, BigDecimal.make(100n, 2))
1352 assert.strictEqual(decimal.value, 1n)
1353 assert.strictEqual(decimal.scale, 0)
1354 assert.deepStrictEqual(roundtrip(Schema.BigDecimal, BigDecimal.make(-123n, -4)), BigDecimal.make(-123n, -4))
1355 })
1356
1357 it("round-trips the empty and success branches of native sums", () => {
1358 assert.isTrue(Option.isNone(roundtrip(Schema.Option(Schema.String), Option.none())))
1359
1360 const result = roundtrip(Schema.Result(Schema.Number, Schema.String), Result.succeed(42))
1361 assert.isTrue(Result.isSuccess(result))
1362 if (Result.isSuccess(result)) assert.strictEqual(result.success, 42)
1363
1364 const exit = roundtrip(Schema.Exit(Schema.Number, Schema.String, Schema.Unknown), Exit.succeed(42))
1365 assert.isTrue(Exit.isSuccess(exit))
1366 if (Exit.isSuccess(exit)) assert.strictEqual(exit.value, 42)
1367 })
1368
1369 it("skips future CauseReason tags inside Cause", () => {
1370 const schema = Schema.Cause(Schema.String, Schema.Unknown)
1371 const bytes = Uint8Array.of(7, 0x20, 2, 1, 99, 2, 0, 0x78)
1372 const decoded = Schema.decodeUnknownSync(SchemaBinary.toCodec(schema))(bytes)
1373
1374 assert.strictEqual(decoded.reasons.length, 1)
1375 assert.strictEqual(decoded.reasons[0]._tag, "Fail")
1376 assert.strictEqual((decoded.reasons[0] as Cause.Fail<string>).error, "x")
1377 assert.match(
1378 schemaError(() =>
1379 Schema.decodeUnknownSync(SchemaBinary.toCodec(Schema.CauseReason(Schema.String, Schema.Unknown)))(
1380 Uint8Array.of(2, 0x20, 99)
1381 )
1382 ).message,
1383 /Missing key/
1384 )
1385 })
1386
1387 it("round-trips UTC and zoned DateTime values", () => {
1388 const utc = DateTime.makeUnsafe(-123456789)
1389 assert.strictEqual(DateTime.toEpochMillis(roundtrip(Schema.DateTimeUtc, utc)), -123456789)
1390
1391 for (
1392 const zoned of [
1393 DateTime.makeZonedUnsafe(123456789, { timeZone: 3_600_000 }),
1394 DateTime.makeZonedUnsafe(123456789, { timeZone: "Europe/London" })
1395 ]
1396 ) {
1397 const decoded = roundtrip(Schema.DateTimeZoned, zoned)
1398 assert.strictEqual(DateTime.toEpochMillis(decoded), DateTime.toEpochMillis(zoned))
1399 assert.strictEqual(DateTime.zoneToString(decoded.zone), DateTime.zoneToString(zoned.zone))
1400 }
1401 })
1402
1403 it("validates DateTimeUtc epoch millisecond boundaries", () => {
1404 const codec = SchemaBinary.toCodec(Schema.DateTimeUtc)
1405 for (const millis of [-8_640_000_000_000_000n, 8_640_000_000_000_000n]) {
1406 const decoded = Schema.decodeUnknownSync(codec)(int64Frame(millis))
1407 assert.strictEqual(DateTime.toEpochMillis(decoded), Number(millis))
1408 }
1409 for (const millis of [-8_640_000_000_000_001n, 8_640_000_000_000_001n]) {
1410 assert.match(
1411 schemaError(() => Schema.decodeUnknownSync(codec)(int64Frame(millis))).message,
1412 /valid DateTime/
1413 )
1414 }
1415 })
1416
1417 it("round-trips Exit, Cause, and CauseReason", () => {
1418 const causeSchema = Schema.Cause(Schema.String, Schema.Unknown)
1419 const cause = Cause.fromReasons([
1420 Cause.makeFailReason("boom"),
1421 Cause.makeDieReason({ defect: true }),
1422 Cause.makeInterruptReason(0),
1423 Cause.makeInterruptReason()
1424 ])
1425 const decoded = roundtrip(causeSchema, cause)
1426 assert.deepStrictEqual(decoded.reasons.map((reason) => reason._tag), ["Fail", "Die", "Interrupt", "Interrupt"])
1427 assert.strictEqual((decoded.reasons[2] as Cause.Interrupt).fiberId, 0)
1428 assert.strictEqual((decoded.reasons[3] as Cause.Interrupt).fiberId, undefined)
1429
1430 const reason = roundtrip(
1431 Schema.CauseReason(Schema.String, Schema.Unknown),
1432 Cause.makeFailReason("failure")
1433 )
1434 assert.strictEqual(reason._tag, "Fail")
1435
1436 const exit = roundtrip(Schema.Exit(Schema.Number, Schema.String, Schema.Unknown), Exit.failCause(cause))
1437 assert.isTrue(Exit.isFailure(exit))
1438 })
1439
1440 // An `Exit` layout carries the `Cause` node its failure branch writes,
1441 // rather than rebuilding one per value. Both branches must still round-trip
1442 // in fingerprint mode, where the failure branch has no length prefix to
1443 // resynchronise on, and the shared node must not make the two schemas hash
1444 // alike.
1445 it("round-trips both Exit branches in fingerprint mode", () => {
1446 const ExitSchema = Schema.Exit(Schema.Number, Schema.String, Schema.Unknown)
1447 const cause = Cause.fromReasons([Cause.makeFailReason("boom"), Cause.makeDieReason({ defect: true })])
1448
1449 assert.isTrue(Exit.isSuccess(roundtripFingerprint(ExitSchema, Exit.succeed(7))))
1450
1451 const failure = roundtripFingerprint(ExitSchema, Exit.failCause(cause))
1452 assert.deepStrictEqual(
1453 Exit.isFailure(failure) ? failure.cause.reasons.map((reason) => reason._tag) : [],
1454 ["Fail", "Die"]
1455 )
1456
1457 assert.notStrictEqual(
1458 fingerprintFrame(ExitSchema, Exit.failCause(cause)).fingerprint,
1459 fingerprintFrame(Schema.Cause(Schema.String, Schema.Unknown), cause).fingerprint
1460 )
1461 })
1462 })
1463
1464 describe("generic declarations and recursion", () => {
1465 it("uses declaration codec links for collections, Redacted, Class, and TaggedClass", () => {
1466 const chunk = roundtrip(Schema.Chunk(Schema.Number), Chunk.make(1, 2, 3))
1467 assert.deepStrictEqual(Chunk.toReadonlyArray(chunk), [1, 2, 3])
1468
1469 const map = roundtrip(Schema.HashMap(Schema.String, Schema.Number), HashMap.make(["a", 1], ["b", 2]))
1470 assert.strictEqual(HashMap.get(map, "a").pipe(Option.getOrUndefined), 1)
1471 assert.strictEqual(HashMap.get(map, "b").pipe(Option.getOrUndefined), 2)
1472
1473 const set = roundtrip(Schema.HashSet(Schema.String), HashSet.make("a", "b"))
1474 assert.isTrue(HashSet.has(set, "a"))
1475 assert.isTrue(HashSet.has(set, "b"))
1476
1477 const redacted = roundtrip(Schema.Redacted(Schema.String), Redacted.make("secret"))
1478 assert.strictEqual(Redacted.value(redacted), "secret")
1479
1480 class Person extends Schema.Class<Person>("Person")({ name: Schema.String }) {}
1481 const person = roundtrip(Person, new Person({ name: "Ada" }))
1482 assert.instanceOf(person, Person)
1483 assert.strictEqual(person.name, "Ada")
1484
1485 class Event extends Schema.TaggedClass<Event>()("Event", { value: Schema.Number }) {}
1486 const event = roundtrip(Event, new Event({ value: 1 }))
1487 assert.instanceOf(event, Event)
1488 assert.strictEqual(event._tag, "Event")
1489 assert.strictEqual(event.value, 1)
1490 })
1491
1492 it("compiles and round-trips recursive suspended schemas", () => {
1493 interface Node {
1494 readonly value: number
1495 readonly children: ReadonlyArray<Node>
1496 }
1497 let Node: Schema.Codec<Node>
1498 Node = Schema.Struct({
1499 value: Schema.Number,
1500 children: Schema.Array(Schema.suspend(() => Node))
1501 })
1502 const value: Node = { value: 1, children: [{ value: 2, children: [] }] }
1503 assert.deepStrictEqual(roundtrip(Node, value), value)
1504 })
1505
1506 it("fails cyclic JSON values with the acyclic value issue", () => {
1507 const value: Record<string, unknown> = {}
1508 value.self = value
1509 assert.match(schemaError(() => encode(Schema.Unknown, value)).message, /acyclic value/)
1510 })
1511
1512 it("fails cyclic recursive struct values with the acyclic value issue", () => {
1513 interface Node {
1514 readonly value: number
1515 readonly next?: Node
1516 }
1517 let Node: Schema.Codec<Node>
1518 Node = Schema.Struct({
1519 value: Schema.Number,
1520 next: Schema.optionalKey(Schema.suspend(() => Node))
1521 })
1522 const value: { value: number; next?: Node } = { value: 1 }
1523 value.next = value
1524 assert.match(schemaError(() => encode(Node, value)).message, /acyclic value/)
1525 })
1526
1527 it("fails array-mediated cycles with the acyclic value issue", () => {
1528 interface Node {
1529 readonly name: string
1530 readonly children: ReadonlyArray<Node>
1531 }
1532 let Node: Schema.Codec<Node>
1533 Node = Schema.Struct({
1534 name: Schema.String,
1535 children: Schema.Array(Schema.suspend(() => Node))
1536 })
1537 const node: { name: string; children: Array<Node> } = { name: "root", children: [] }
1538 node.children.push(node)
1539 assert.match(schemaError(() => encode(Node, node)).message, /acyclic value/)
1540
1541 const unknown: Array<unknown> = []
1542 unknown.push(unknown)
1543 assert.match(schemaError(() => encode(Schema.Unknown, unknown)).message, /acyclic value/)
1544 })
1545
1546 it("accepts shared recursive values that are DAGs rather than cycles", () => {
1547 interface Node {
1548 readonly value: number
1549 readonly children: ReadonlyArray<Node>
1550 }
1551 let Node: Schema.Codec<Node>
1552 Node = Schema.Struct({
1553 value: Schema.Number,
1554 children: Schema.Array(Schema.suspend(() => Node))
1555 })
1556 const leaf: Node = { value: 2, children: [] }
1557 const value: Node = { value: 1, children: [leaf, leaf] }
1558
1559 assert.deepStrictEqual(roundtrip(Node, value), {
1560 value: 1,
1561 children: [{ value: 2, children: [] }, { value: 2, children: [] }]
1562 })
1563 })
1564
1565 it("rejects non-serializable JSON values without calling them cycles", () => {
1566 for (const value of [undefined, 1n, Symbol.for("json"), () => 1]) {
1567 const error = schemaError(() => encode(Schema.Unknown, value))
1568 assert.match(error.message, /JSON-serializable value/)
1569 assert.isFalse(error.message.includes("acyclic value"))
1570 }
1571 })
1572
1573 it("runs user encoding links before the binary layer", () => {
1574 const bytes = encode(Schema.NumberFromString, 123)
1575 assert.strictEqual(bytes.length, 5)
1576 assert.strictEqual(roundtrip(Schema.NumberFromString, 123), 123)
1577
1578 const Before = Schema.Struct({ value: Schema.NumberFromString.pipe(SchemaBinary.fieldId(7)) })
1579 const After = Schema.Struct({ renamed: Schema.NumberFromString.pipe(SchemaBinary.fieldId(7)) })
1580 assert.deepStrictEqual(
1581 Schema.decodeUnknownSync(SchemaBinary.toCodec(After))(encode(Before, { value: 123 })),
1582 { renamed: 123 }
1583 )
1584 })
1585
1586 it.effect("round-trips a yielding transformEffect through SchemaParser Effect APIs", () =>
1587 Effect.gen(function*() {
1588 const schema = Schema.String.pipe(
1589 Schema.decodeTo(
1590 Schema.Number,
1591 SchemaTransformation.transformEffect({
1592 decode: (s) => Effect.yieldNow.pipe(Effect.as(Number(s))),
1593 encode: (n) => Effect.yieldNow.pipe(Effect.as(String(n)))
1594 })
1595 )
1596 )
1597 const codec = SchemaBinary.toCodec(schema)
1598 const bytes = yield* SchemaParser.encodeUnknownEffect(codec)(123)
1599 assert.strictEqual(yield* SchemaParser.decodeUnknownEffect(codec)(bytes), 123)
1600 }))
1601
1602 it("keeps a sound runtime type guard on the derived codec", () => {
1603 const codec = SchemaBinary.toCodec(Schema.Struct({ name: Schema.String, age: Schema.Number }))
1604 assert.isTrue(Schema.is(codec)({ name: "Ada", age: 42 }))
1605 assert.isFalse(Schema.is(codec)({ nope: 1 }))
1606 })
1607
1608 it("keeps a sound runtime type guard on recursive derived codecs", () => {
1609 interface Node {
1610 readonly value: number
1611 readonly children: ReadonlyArray<Node>
1612 }
1613 let Node: Schema.Codec<Node>
1614 Node = Schema.Struct({
1615 value: Schema.Number,
1616 children: Schema.Array(Schema.suspend(() => Node))
1617 })
1618 const codec = SchemaBinary.toCodec(Node)
1619
1620 assert.isTrue(Schema.is(codec)({ value: 1, children: [] }))
1621 assert.isFalse(Schema.is(codec)({ nope: 1 }))
1622 assert.isFalse(Schema.is(Schema.Struct({ inner: codec }))({ inner: { nope: 1 } }))
1623 })
1624 })
1625
1626 describe("parse options", () => {
1627 it("honors checks and disableChecks", () => {
1628 const NonNegative = Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))
1629 const bytes = encode(Schema.Number, -1.5)
1630 assert.match(
1631 schemaError(() => Schema.decodeUnknownSync(SchemaBinary.toCodec(NonNegative))(bytes)).message,
1632 /greater than or equal to 0/
1633 )
1634
1635 const parser = SchemaBinary.parser(NonNegative, { disableChecks: true })
1636 assert.deepStrictEqual(parser.feedSync(bytes), [-1.5])
1637 parser.endSync()
1638 })
1639
1640 it("honors disableChecks for recursive schemas", () => {
1641 interface Node {
1642 readonly value: number
1643 readonly children: ReadonlyArray<Node>
1644 }
1645 let Writer: Schema.Codec<Node>
1646 Writer = Schema.Struct({
1647 value: Schema.Number,
1648 children: Schema.Array(Schema.suspend(() => Writer))
1649 })
1650 let Reader: Schema.Codec<Node>
1651 Reader = Schema.Struct({
1652 value: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)),
1653 children: Schema.Array(Schema.suspend(() => Reader))
1654 })
1655 const value: Node = { value: -1.5, children: [] }
1656 const parser = SchemaBinary.parser(Reader, { disableChecks: true })
1657
1658 assert.deepStrictEqual(parser.feedSync(encode(Writer, value)), [value])
1659 parser.endSync()
1660 assert.deepStrictEqual(
1661 Schema.decodeUnknownSync(SchemaBinary.toCodec(Reader), { disableChecks: true })(encode(Writer, value)),
1662 value
1663 )
1664 })
1665
1666 it("keeps one-shot index-signature caching within each options call", () => {
1667 const Key = Schema.String.check(Schema.makeFilter((key: string) => key.startsWith("allowed-")))
1668 const Writer = Schema.Record(Schema.String, Schema.Number)
1669 const codec = SchemaBinary.toCodec(Schema.Record(Key, Schema.Number))
1670 const bytes = encode(Writer, { denied: 1 })
1671
1672 assert.deepStrictEqual(Schema.decodeUnknownSync(codec)(bytes), {})
1673 assert.deepStrictEqual(Schema.decodeUnknownSync(codec, { disableChecks: true })(bytes), { denied: 1 })
1674 })
1675
1676 it("honors errors all for missing fields", () => {
1677 const bytes = encode(Schema.Struct({}), {})
1678 const Reader = Schema.Struct({ a: Schema.String, b: Schema.Number })
1679 const error = schemaError(() => Schema.decodeUnknownSync(SchemaBinary.toCodec(Reader), { errors: "all" })(bytes))
1680 assert.strictEqual(error.message.match(/Missing key/g)?.length, 2)
1681 })
1682
1683 it("ignores excess-property options at the binary boundary", () => {
1684 const Writer = Schema.Struct({ extra: Schema.String, known: Schema.Number })
1685 const Reader = Schema.Struct({ known: Schema.Number })
1686 const bytes = encode(Writer, { extra: "drop", known: 1 })
1687
1688 assert.deepStrictEqual(
1689 Schema.decodeUnknownSync(SchemaBinary.toCodec(Reader), { onExcessProperty: "error" })(bytes),
1690 { known: 1 }
1691 )
1692 assert.deepStrictEqual(
1693 SchemaBinary.parser(Reader, { onExcessProperty: "error" }).feedSync(bytes),
1694 [{ known: 1 }]
1695 )
1696 })
1697 })
1698
1699 describe("layout and data errors", () => {
1700 it("throws Error while compiling invalid layouts", () => {
1701 const declaration = Schema.declare((_): _ is { readonly value: string } => true)
1702 assert.throws(
1703 () => SchemaBinary.toCodec(declaration),
1704 /Binary layout: declaration <anonymous> has no toCodecJson or toCodec/
1705 )
1706 assert.throws(
1707 () =>
1708 SchemaBinary.toCodec(Schema.Union([
1709 Schema.Struct({ a: Schema.String }),
1710 Schema.Struct({ b: Schema.Number })
1711 ])),
1712 /union members are not uniquely identifiable/
1713 )
1714 assert.throws(
1715 () =>
1716 SchemaBinary.toCodec(Schema.Struct({
1717 a: Schema.String.pipe(SchemaBinary.fieldId(1)),
1718 b: Schema.String.pipe(SchemaBinary.fieldId(1))
1719 })),
1720 /Binary layout field id collision: 1/
1721 )
1722 assert.throws(
1723 () =>
1724 SchemaBinary.toCodec(Schema.Struct({
1725 hashed: Schema.String,
1726 explicit: Schema.String.pipe(SchemaBinary.fieldId(1836165160))
1727 })),
1728 /Binary layout field id collision: 1836165160 \(hashed, explicit\)/
1729 )
1730 assert.throws(
1731 () =>
1732 SchemaBinary.toCodec(Schema.Union([
1733 Schema.Literal("a"),
1734 Schema.UniqueSymbol(Symbol.for("SchemaBinary/symbol"))
1735 ])),
1736 /union members are not uniquely identifiable/
1737 )
1738 assert.throws(
1739 () => SchemaBinary.toCodec(Schema.Struct({ [Symbol.for("SchemaBinary/key")]: Schema.String })),
1740 /symbol property names are illegal/
1741 )
1742 assert.throws(
1743 () =>
1744 SchemaBinary.toCodec(Schema.Union([
1745 Schema.Struct({ _tag: Schema.Literal("1a0a49t3mq") }),
1746 Schema.Struct({ _tag: Schema.Literal("m7r4q02dm7") })
1747 ])),
1748 /Binary layout sentinel collision: 3370793117/
1749 )
1750 assert.throws(
1751 () =>
1752 SchemaBinary.toCodec(Schema.Union([
1753 Schema.Struct({ _tag: Schema.UniqueSymbol(Symbol("local")) })
1754 ])),
1755 /unregistered unique symbol/
1756 )
1757 assert.throws(
1758 () => SchemaBinary.toCodec(Schema.Union([Schema.Date, Schema.DateTimeUtc])),
1759 /union members are not uniquely identifiable/
1760 )
1761 })
1762
1763 it("uses SchemaIssue for binary failures", () => {
1764 const codec = SchemaBinary.toCodec(Schema.Boolean)
1765 const error = schemaError(() => Schema.decodeUnknownSync(codec)(Uint8Array.of(2, 0x20, 2)))
1766 assert.isTrue(SchemaIssue.isIssue(error.issue))
1767 assert.match(error.message, /bool/)
1768 })
1769
1770 it.effect("preserves the SchemaParser Issue and Schema SchemaError surfaces", () =>
1771 Effect.gen(function*() {
1772 const codec = SchemaBinary.toCodec(Schema.Boolean)
1773 const bytes = Uint8Array.of(2, 0x20, 2)
1774 const issue = yield* SchemaParser.decodeUnknownEffect(codec)(bytes).pipe(Effect.flip)
1775 assert.isTrue(SchemaIssue.isIssue(issue))
1776 const error = yield* Schema.decodeUnknownEffect(codec)(bytes).pipe(Effect.flip)
1777 assert.isTrue(Schema.isSchemaError(error))
1778 }))
1779
1780 it("reports a missing fixed tuple slot as MissingKey", () => {
1781 const emptyTuple = encode(Schema.Tuple([]), [])
1782 const error = schemaError(() =>
1783 Schema.decodeUnknownSync(SchemaBinary.toCodec(Schema.Tuple([Schema.String])))(emptyTuple)
1784 )
1785 assert.match(error.message, /Missing key/)
1786 })
1787
1788 it("rejects malformed lengths and text", () => {
1789 const string = SchemaBinary.toCodec(Schema.String)
1790 assert.match(
1791 schemaError(() => Schema.decodeUnknownSync(string)(Uint8Array.of(0))).message,
1792 /nonzero frame length/
1793 )
1794 assert.match(
1795 schemaError(() =>
1796 Schema.decodeUnknownSync(string)(Uint8Array.of(0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x10))
1797 ).message,
1798 /safe integer length/
1799 )
1800 assert.match(
1801 schemaError(() => Schema.decodeUnknownSync(string)(Uint8Array.of(2, 0x20, 0xFF))).message,
1802 /utf-8/
1803 )
1804 assert.match(
1805 schemaError(() =>
1806 Schema.decodeUnknownSync(SchemaBinary.toCodec(Schema.Date))(Uint8Array.of(8, 0x20, 0, 0, 0, 0, 0, 0, 0))
1807 ).message,
1808 /int64/
1809 )
1810 assert.match(
1811 schemaError(() => Schema.decodeUnknownSync(SchemaBinary.toCodec(Schema.Null))(Uint8Array.of(2, 0x20, 0)))
1812 .message,
1813 /empty/
1814 )
1815 assert.match(
1816 schemaError(() => Schema.decodeUnknownSync(SchemaBinary.toCodec(Schema.Unknown))(Uint8Array.of(2, 0x20, 0x7B)))
1817 .message,
1818 /json/
1819 )
1820 })
1821
1822 it("rejects invalid native tags and payload shapes", () => {
1823 const rejects = <A, I>(schema: Schema.Codec<A, I>, bytes: Uint8Array, expected: RegExp) => {
1824 assert.match(schemaError(() => Schema.decodeUnknownSync(SchemaBinary.toCodec(schema))(bytes)).message, expected)
1825 }
1826 rejects(Schema.Duration, Uint8Array.of(2, 0x20, 3), /duration/)
1827 rejects(Schema.Option(Schema.String), Uint8Array.of(2, 0x20, 2), /bool/)
1828 rejects(Schema.Option(Schema.String), Uint8Array.of(3, 0x20, 0, 0), /empty/)
1829 rejects(Schema.Result(Schema.String, Schema.String), Uint8Array.of(2, 0x20, 2), /bool/)
1830 rejects(Schema.Exit(Schema.String, Schema.String, Schema.Unknown), Uint8Array.of(2, 0x20, 2), /bool/)
1831 rejects(Schema.DateTimeZoned, Uint8Array.of(10, 0x20, 0, 0, 0, 0, 0, 0, 0, 0, 2), /time zone/)
1832 rejects(Schema.DateTimeZoned, Uint8Array.of(10, 0x20, 0, 0, 0, 0, 0, 0, 0, 0, 0), /time zone/)
1833 })
1834
1835 it("rejects attacker-sized zero-width array counts", () => {
1836 const codec = SchemaBinary.toCodec(Schema.Array(Schema.Null))
1837 const bytes = Uint8Array.of(6, 0x20, 0x80, 0x80, 0x80, 0x80, 0x10)
1838 assert.match(
1839 schemaError(() => Schema.decodeUnknownSync(codec)(bytes)).message,
1840 /array count within allocation limit/
1841 )
1842 })
1843
1844 it("round-trips zero-width arrays across the allocation slack boundary", () => {
1845 const verify = <A, I>(schema: Schema.Codec<A, I>, value: A) => {
1846 const codec = SchemaBinary.toCodec(Schema.Array(schema))
1847 for (const count of [1_048_576, 1_048_577]) {
1848 const bytes = Schema.encodeUnknownSync(codec)(new Array<A>(count).fill(value))
1849 assert.strictEqual(bytes.length, 5)
1850 const decoded = Schema.decodeUnknownSync(codec)(bytes)
1851 assert.strictEqual(decoded.length, count)
1852 assert.strictEqual(decoded[0], value)
1853 assert.strictEqual(decoded[count - 1], value)
1854 }
1855 }
1856
1857 verify(Schema.Null, null)
1858 verify(Schema.Undefined, undefined)
1859 })
1860
1861 it("rejects duplicate struct field ids and extra keys", () => {
1862 const struct = Schema.Struct({ value: Schema.String })
1863 const encodedStruct = encode(struct, { value: "x" })
1864 const field = encodedStruct.slice(2)
1865 const duplicateField = concat(Uint8Array.of(1 + field.length * 2, 0x20), field, field)
1866 assert.match(
1867 schemaError(() => Schema.decodeUnknownSync(SchemaBinary.toCodec(struct))(duplicateField)).message,
1868 /unique field ids/
1869 )
1870
1871 const record = Schema.Record(Schema.String, Schema.Number)
1872 const encodedRecord = encode(record, { a: 1 })
1873 const pair = encodedRecord.slice(4)
1874 const duplicateKey = concat(Uint8Array.of(3 + pair.length * 2, 0x20, 0, pair.length * 2), pair, pair)
1875 assert.match(
1876 schemaError(() => Schema.decodeUnknownSync(SchemaBinary.toCodec(record))(duplicateKey)).message,
1877 /unique extra keys/
1878 )
1879 })
1880
1881 it("rejects declared field names in the extra-key map", () => {
1882 const Declared = Schema.Struct({
1883 id: Schema.String,
1884 note: Schema.optionalKey(Schema.String)
1885 })
1886 const Reader = Schema.StructWithRest(
1887 Declared,
1888 [Schema.Record(Schema.String, Schema.Number)]
1889 )
1890 const extras = Schema.Record(Schema.String, Schema.Number)
1891 const body = (frame: Uint8Array): Uint8Array => {
1892 let offset = 0
1893 while ((frame[offset] & 0x80) !== 0) offset++
1894 assert.strictEqual(frame[++offset], 0x20)
1895 return frame.subarray(offset + 1)
1896 }
1897 const collide = (key: "id" | "note") => {
1898 const bytes = concat(body(encode(Declared, { id: "ok" })), body(encode(extras, { [key]: 7 })))
1899 return concat(Uint8Array.from([...uvarint(bytes.length + 1), 0x20]), bytes)
1900 }
1901
1902 for (const key of ["id", "note"] as const) {
1903 assert.match(
1904 schemaError(() => Schema.decodeUnknownSync(SchemaBinary.toCodec(Reader))(collide(key))).message,
1905 /declared field/
1906 )
1907 }
1908 })
1909
1910 it("rejects a duplicate field id after an unknown union decodes as absent", () => {
1911 const A = Schema.Struct({ _tag: Schema.Literal("A") })
1912 const B = Schema.Struct({ _tag: Schema.Literal("B"), value: Schema.String })
1913 const Writer = Schema.Struct({ event: Schema.optionalKey(Schema.Union([A, B])) })
1914 const Reader = Schema.Struct({ event: Schema.optionalKey(Schema.Union([A])) })
1915 const encoded = encode(Writer, { event: { _tag: "B", value: "new" } })
1916 const field = encoded.slice(2)
1917 const duplicateField = concat(Uint8Array.of(1 + field.length * 2, 0x20), field, field)
1918
1919 assert.match(
1920 schemaError(() => Schema.decodeUnknownSync(SchemaBinary.toCodec(Reader))(duplicateField)).message,
1921 /unique field ids/
1922 )
1923 })
1924
1925 it("does not satisfy wide-field presence from the extra-key map", () => {
1926 const fields: Record<string, typeof Schema.String> = {}
1927 const extras: Record<string, string> = {}
1928 for (let i = 0; i < 34; i++) {
1929 fields[`field${i}`] = Schema.String
1930 extras[`extra${i}`] = `value${i}`
1931 }
1932 const record = Schema.Record(Schema.String, Schema.String)
1933 const struct = Schema.StructWithRest(Schema.Struct(fields), [record])
1934 const error = schemaError(() =>
1935 Schema.decodeUnknownSync(SchemaBinary.toCodec(struct), { errors: "all" })(encode(record, extras))
1936 )
1937
1938 assert.strictEqual(error.message.match(/Missing key/g)?.length, 34)
1939 })
1940
1941 it("validates literals instead of leaning on the schema pass", () => {
1942 const Tagged = Schema.Struct({ kind: Schema.Literal("ok") })
1943 const Loose = Schema.Struct({ kind: Schema.String })
1944 assert.match(
1945 schemaError(() => Schema.decodeUnknownSync(SchemaBinary.toCodec(Tagged))(encode(Loose, { kind: "nope" })))
1946 .message,
1947 /"ok"/
1948 )
1949 assert.match(schemaError(() => encode(Tagged, { kind: "nope" } as never)).message, /"ok"/)
1950
1951 const Level = Schema.Struct({ kind: Schema.Literals(["info", "warning"]) })
1952 assert.deepStrictEqual(
1953 Schema.decodeUnknownSync(SchemaBinary.toCodec(Level))(encode(Loose, { kind: "warning" })),
1954 { kind: "warning" }
1955 )
1956 assert.match(
1957 schemaError(() => Schema.decodeUnknownSync(SchemaBinary.toCodec(Level))(encode(Loose, { kind: "debug" })))
1958 .message,
1959 /"info" \| "warning"/
1960 )
1961 })
1962
1963 it("rejects a Date outside the representable range", () => {
1964 const codec = SchemaBinary.toCodec(Schema.Date)
1965 const bytes = Schema.encodeUnknownSync(codec)(new Date(0))
1966 bytes.set(Uint8Array.of(0, 0, 0, 0, 0, 0, 0, 0x7F), bytes.length - 8)
1967 assert.match(schemaError(() => Schema.decodeUnknownSync(codec)(bytes)).message, /a valid Date/)
1968 })
1969
1970 it("fails Never values and unregistered symbols through SchemaError", () => {
1971 assert.isTrue(Schema.isSchemaError(schemaError(() => encode(Schema.Never, undefined))))
1972 assert.isTrue(
1973 Schema.isSchemaError(
1974 schemaError(() => Schema.decodeUnknownSync(SchemaBinary.toCodec(Schema.Never))(Uint8Array.of(1, 0x20)))
1975 )
1976 )
1977 assert.match(schemaError(() => encode(Schema.Symbol, Symbol("local"))).message, /registered symbol/)
1978 })
1979 })
1980
1981 describe("fingerprint mode", () => {
1982 const Person = Schema.Struct({
1983 name: Schema.String,
1984 age: Schema.Number.check(Schema.isInt()),
1985 active: Schema.Boolean,
1986 nickname: Schema.optional(Schema.String)
1987 })
1988 const person = { name: "Ada", age: 36, active: true }
1989
1990 it("writes a fingerprint envelope, a presence bitmap, and no field ids", () => {
1991 // length | envelope 0x21 | fingerprint | bitmap | age varint | name | active
1992 assert.deepStrictEqual([...encodeFingerprint(Person, person)], [
1993 16,
1994 0x21,
1995 184,
1996 213,
1997 85,
1998 38,
1999 67,
2000 231,
2001 116,
2002 53,
2003 0,
2004 72,
2005 3,
2006 65,
2007 100,
2008 97,
2009 1
2010 ])
2011 // the same frame with the optional field present: its bit is set and it
2012 // moves ahead of `age`, which is where its wire id sorts it
2013 assert.deepStrictEqual([...encodeFingerprint(Person, { ...person, nickname: "A" })], [
2014 19,
2015 0x21,
2016 184,
2017 213,
2018 85,
2019 38,
2020 67,
2021 231,
2022 116,
2023 53,
2024 1,
2025 2,
2026 1,
2027 65,
2028 72,
2029 3,
2030 65,
2031 100,
2032 97,
2033 1
2034 ])
2035 })
2036
2037 it("leaves the default mode untouched", () => {
2038 const expected = [...encode(Person, person)]
2039 assert.deepStrictEqual(expected[1], 0x20)
2040 assert.deepStrictEqual([...Schema.encodeUnknownSync(SchemaBinary.toCodec(Person, {}))(person)], expected)
2041 assert.deepStrictEqual(
2042 [...Schema.encodeUnknownSync(SchemaBinary.toCodec(Person, { fingerprint: false }))(person)],
2043 expected
2044 )
2045 })
2046
2047 it("addresses union members by canonical position", () => {
2048 const Click = Schema.Struct({ _tag: Schema.Literal("click"), x: Schema.Number.check(Schema.isInt()) })
2049 const Key = Schema.Struct({ _tag: Schema.Literal("key"), code: Schema.String })
2050 const Event = Schema.Union([Click, Key])
2051 assert.deepStrictEqual([...encodeFingerprint(Event, { _tag: "click", x: 3 })], [
2052 11,
2053 0x21,
2054 237,
2055 163,
2056 78,
2057 151,
2058 96,
2059 43,
2060 76,
2061 0,
2062 0,
2063 6
2064 ])
2065 const key = { _tag: "key", code: "Esc" } as const
2066 const keyFrame = [...encodeFingerprint(Event, key)]
2067 assert.deepStrictEqual(keyFrame, [14, 0x21, 237, 163, 78, 151, 96, 43, 76, 0, 1, 3, 69, 115, 99])
2068 // declaration order never reaches the wire
2069 assert.deepStrictEqual([...encodeFingerprint(Schema.Union([Key, Click]), key)], keyFrame)
2070 assert.deepStrictEqual(roundtripFingerprint(Event, key), key)
2071 })
2072
2073 it("counts the extra-key map instead of reserving field zero", () => {
2074 const schema = Schema.Record(Schema.String, Schema.Number)
2075 assert.deepStrictEqual([...encodeFingerprint(schema, { b: 2, a: 1 })], [
2076 16,
2077 0x21,
2078 189,
2079 249,
2080 115,
2081 23,
2082 65,
2083 225,
2084 229,
2085 6,
2086 2,
2087 9,
2088 97,
2089 2,
2090 9,
2091 98,
2092 4
2093 ])
2094 assert.deepStrictEqual(roundtripFingerprint(schema, { z: 1, a: 2 }), { z: 1, a: 2 })
2095 assert.deepStrictEqual(roundtripFingerprint(schema, {}), {})
2096 })
2097
2098 it("keeps the default tuple and array layout", () => {
2099 assert.deepStrictEqual([...encodeFingerprint(Schema.Tuple([Schema.Boolean, Schema.String]), [true, "x"])], [
2100 12,
2101 0x21,
2102 3,
2103 44,
2104 71,
2105 25,
2106 31,
2107 66,
2108 141,
2109 82,
2110 1,
2111 1,
2112 120
2113 ])
2114 assert.deepStrictEqual([...encodeFingerprint(Schema.Array(Schema.Number.check(Schema.isInt())), [1, -2, 3])], [
2115 13,
2116 0x21,
2117 206,
2118 94,
2119 13,
2120 113,
2121 158,
2122 175,
2123 76,
2124 32,
2125 3,
2126 2,
2127 5,
2128 6
2129 ])
2130 })
2131
2132 it("inlines fixed-size and zero-width leaves", () => {
2133 const schema = Schema.Struct({
2134 nothing: Schema.Null,
2135 flag: Schema.Boolean,
2136 missing: Schema.Undefined,
2137 when: Schema.Date,
2138 count: Schema.Number.check(Schema.isInt()),
2139 label: Schema.String
2140 })
2141 const value = { nothing: null, flag: true, missing: undefined, when: new Date(1000), count: -7, label: "hi" }
2142 // 1 envelope + 8 fingerprint + 1 varint + 8 int64 + 3 string + 1 bool
2143 assert.strictEqual(encodeFingerprint(schema, value).length, 23)
2144 assert.deepStrictEqual(roundtripFingerprint(schema, value), value)
2145 })
2146
2147 it("spans a presence bitmap across several bytes", () => {
2148 const schema = Schema.Struct({
2149 a: Schema.optional(Schema.Number),
2150 b: Schema.optional(Schema.Number),
2151 c: Schema.optional(Schema.Number),
2152 d: Schema.optional(Schema.Number),
2153 e: Schema.optional(Schema.Number),
2154 f: Schema.optional(Schema.Number),
2155 g: Schema.optional(Schema.Number),
2156 h: Schema.optional(Schema.Number),
2157 i: Schema.optional(Schema.Number),
2158 j: Schema.optional(Schema.Number)
2159 })
2160 assert.deepStrictEqual(roundtripFingerprint(schema, {}), {})
2161 assert.deepStrictEqual(roundtripFingerprint(schema, { a: 1, j: 2 }), { a: 1, j: 2 })
2162 const full = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10 }
2163 assert.deepStrictEqual(roundtripFingerprint(schema, full), full)
2164 })
2165
2166 it("round-trips native declarations, recursion, and mixed unions", () => {
2167 const natives = Schema.Struct({
2168 option: Schema.Option(Schema.String),
2169 result: Schema.Result(Schema.Number, Schema.String),
2170 big: Schema.BigInt,
2171 bytes: Schema.Uint8Array,
2172 duration: Schema.Duration,
2173 decimal: Schema.BigDecimal
2174 })
2175 const nativeValue = {
2176 option: Option.some("x"),
2177 result: Result.fail("boom"),
2178 big: 2n ** 70n,
2179 bytes: new Uint8Array([1, 2, 3]),
2180 duration: Duration.nanos(1_500_000_000n),
2181 decimal: BigDecimal.make(123n, 2)
2182 }
2183 const natived = roundtripFingerprint(natives, nativeValue)
2184 assert.deepStrictEqual(natived.option, nativeValue.option)
2185 assert.deepStrictEqual(natived.result, nativeValue.result)
2186 assert.strictEqual(natived.big, nativeValue.big)
2187 assert.deepStrictEqual([...natived.bytes], [1, 2, 3])
2188 assert.strictEqual(Duration.toNanosUnsafe(natived.duration), 1_500_000_000n)
2189 assert.strictEqual(natived.decimal.value, 123n)
2190 assert.strictEqual(natived.decimal.scale, 2)
2191
2192 interface Tree {
2193 readonly value: number
2194 readonly children: ReadonlyArray<Tree>
2195 }
2196 const Tree: Schema.Codec<Tree> = Schema.Struct({
2197 value: Schema.Number,
2198 children: Schema.Array(Schema.suspend((): Schema.Codec<Tree> => Tree))
2199 })
2200 const tree = { value: 1, children: [{ value: 2, children: [] }, { value: 3, children: [] }] }
2201 assert.deepStrictEqual(roundtripFingerprint(Tree, tree), tree)
2202
2203 const mixed = Schema.Union([Schema.String, Schema.Number, Schema.Struct({ n: Schema.Boolean })])
2204 assert.deepStrictEqual(roundtripFingerprint(mixed, "x"), "x")
2205 assert.deepStrictEqual(roundtripFingerprint(mixed, 1.5), 1.5)
2206 assert.deepStrictEqual(roundtripFingerprint(mixed, { n: true }), { n: true })
2207 })
2208 })
2209
2210 describe("layout fingerprint", () => {
2211 interface Tree {
2212 readonly value: number
2213 readonly children: ReadonlyArray<Tree>
2214 }
2215 const makeTree = (): Schema.Codec<Tree> => {
2216 const self: Schema.Codec<Tree> = Schema.Struct({
2217 value: Schema.Number,
2218 children: Schema.Array(Schema.suspend((): Schema.Codec<Tree> => self))
2219 })
2220 return self
2221 }
2222
2223 const base = Schema.Struct({ name: Schema.String, age: Schema.Number })
2224 const value = { name: "a", age: 1 }
2225 const baseline = fingerprintOf(base, value)
2226
2227 it("ignores schema changes that do not reach the wire", () => {
2228 assert.strictEqual(
2229 fingerprintOf(
2230 Schema.Struct({
2231 name: Schema.String.check(Schema.isMinLength(1)).annotate({ description: "the name" }),
2232 age: Schema.Number
2233 }),
2234 value
2235 ),
2236 baseline
2237 )
2238 // property declaration order: encode sorts by wire id
2239 assert.strictEqual(fingerprintOf(Schema.Struct({ age: Schema.Number, name: Schema.String }), value), baseline)
2240 // a decoded-side transformation leaves the encoded layout alone
2241 assert.strictEqual(
2242 fingerprintOf(
2243 Schema.Struct({ name: Schema.String, age: Schema.Number.pipe(Schema.decodeTo(Schema.Number)) }),
2244 value
2245 ),
2246 baseline
2247 )
2248 })
2249
2250 it("does not depend on the order a literal union declares its members", () => {
2251 assert.strictEqual(
2252 fingerprintOf(Schema.Literals(["a", "b", "c"]), "a"),
2253 fingerprintOf(Schema.Literals(["c", "a", "b"]), "a")
2254 )
2255 assert.notStrictEqual(
2256 fingerprintOf(Schema.Literals(["a", "b"]), "a"),
2257 fingerprintOf(Schema.Literals(["a", "c"]), "a")
2258 )
2259 assert.notStrictEqual(fingerprintOf(Schema.Literal("a"), "a"), fingerprintOf(Schema.String, "a"))
2260 })
2261
2262 it("does not depend on whether an acyclic sub-schema is shared or repeated", () => {
2263 const Point = Schema.Struct({ x: Schema.Number, y: Schema.Number })
2264 const pair = { a: { x: 1, y: 2 }, b: { x: 3, y: 4 } }
2265 const shared = fingerprintOf(Schema.Struct({ a: Point, b: Point }), pair)
2266 const repeated = fingerprintOf(
2267 Schema.Struct({
2268 a: Schema.Struct({ x: Schema.Number, y: Schema.Number }),
2269 b: Schema.Struct({ x: Schema.Number, y: Schema.Number })
2270 }),
2271 pair
2272 )
2273 assert.strictEqual(shared, repeated)
2274
2275 const leaf = { value: 1, children: [] }
2276 assert.strictEqual(fingerprintOf(makeTree(), leaf), fingerprintOf(makeTree(), leaf))
2277 // one recursive node reached from two fields, versus two of them. Both
2278 // sides have the same cycle structure, which is the only recursive case
2279 // the hash canonicalises; see the factoring test below for the limit.
2280 const both = { left: leaf, right: leaf }
2281 const one = makeTree()
2282 assert.strictEqual(
2283 fingerprintOf(Schema.Struct({ left: one, right: one }), both),
2284 fingerprintOf(Schema.Struct({ left: makeTree(), right: makeTree() }), both)
2285 )
2286 })
2287
2288 it("hashes the layout graph, so re-factoring a recursive schema moves it", () => {
2289 // Three finite graphs denoting the same infinite wire shape: the cycle
2290 // itself, the cycle behind one non-recursive alias, and a two-node
2291 // mutual recursion of the same shape.
2292 const direct = makeTree()
2293 const aliased = Schema.Struct({
2294 value: Schema.Number,
2295 children: Schema.Array(makeTree())
2296 }) as unknown as Schema.Codec<Tree>
2297 const mutualA: Schema.Codec<Tree> = Schema.Struct({
2298 value: Schema.Number,
2299 children: Schema.Array(Schema.suspend((): Schema.Codec<Tree> => mutualB))
2300 })
2301 const mutualB: Schema.Codec<Tree> = Schema.Struct({
2302 value: Schema.Number,
2303 children: Schema.Array(Schema.suspend((): Schema.Codec<Tree> => mutualA))
2304 })
2305
2306 const tree = { value: 1, children: [{ value: 2, children: [] }] }
2307 const frames = [direct, aliased, mutualA].map((schema) => fingerprintFrame(schema, tree))
2308
2309 // identical bytes on the wire
2310 assert.strictEqual(new Set(frames.map((frame) => frame.payload)).size, 1)
2311 // and identical in the tolerant default mode, in both directions
2312 const directDefault = SchemaBinary.toCodec(direct)
2313 const aliasedDefault = SchemaBinary.toCodec(aliased)
2314 assert.deepStrictEqual(
2315 Schema.decodeUnknownSync(aliasedDefault)(Schema.encodeUnknownSync(directDefault)(tree)),
2316 tree
2317 )
2318 assert.deepStrictEqual(
2319 Schema.decodeUnknownSync(directDefault)(Schema.encodeUnknownSync(aliasedDefault)(tree)),
2320 tree
2321 )
2322
2323 // but each factoring hashes differently, and they reject each other
2324 assert.strictEqual(new Set(frames.map((frame) => frame.fingerprint)).size, 3)
2325 assert.include(
2326 schemaError(() =>
2327 Schema.decodeUnknownSync(SchemaBinary.toCodec(aliased, { fingerprint: true }))(
2328 encodeFingerprint(direct, tree)
2329 )
2330 ).message,
2331 "Expected matching layout fingerprint"
2332 )
2333 })
2334
2335 it("changes whenever the wire layout changes", () => {
2336 const changed = [
2337 fingerprintOf(Schema.Struct({ label: Schema.String, age: Schema.Number }), { label: "a", age: 1 }),
2338 fingerprintOf(
2339 Schema.Struct({ name: Schema.String, age: Schema.Number, extra: Schema.Boolean }),
2340 { name: "a", age: 1, extra: true }
2341 ),
2342 fingerprintOf(Schema.Struct({ name: Schema.String, age: Schema.optional(Schema.Number) }), value),
2343 fingerprintOf(Schema.Struct({ name: Schema.String, age: Schema.Number.check(Schema.isInt()) }), value),
2344 fingerprintOf(
2345 Schema.Struct({ name: Schema.String.pipe(SchemaBinary.fieldId(1)), age: Schema.Number }),
2346 value
2347 ),
2348 fingerprintOf(Schema.Struct({ name: Schema.String, age: Schema.String }), { name: "a", age: "1" })
2349 ]
2350 assert.strictEqual(new Set([baseline, ...changed]).size, changed.length + 1)
2351
2352 assert.notStrictEqual(
2353 fingerprintOf(Schema.Tuple([Schema.Number, Schema.Number]), [1, 2]),
2354 fingerprintOf(Schema.Tuple([Schema.Number, Schema.Number, Schema.Number]), [1, 2, 3])
2355 )
2356 assert.notStrictEqual(
2357 fingerprintOf(Schema.Array(Schema.Number), [1]),
2358 fingerprintOf(Schema.Tuple([Schema.Number]), [1])
2359 )
2360 assert.notStrictEqual(
2361 fingerprintOf(Schema.Date, new Date(0)),
2362 fingerprintOf(Schema.DateTimeUtc, DateTime.makeUnsafe(0))
2363 )
2364
2365 const A = Schema.Struct({ _tag: Schema.Literal("a"), n: Schema.Number })
2366 const B = Schema.Struct({ _tag: Schema.Literal("b"), s: Schema.String })
2367 const C = Schema.Struct({ _tag: Schema.Literal("c"), s: Schema.String })
2368 const a = { _tag: "a", n: 1 } as const
2369 assert.strictEqual(fingerprintOf(Schema.Union([A, B]), a), fingerprintOf(Schema.Union([B, A]), a))
2370 assert.notStrictEqual(fingerprintOf(Schema.Union([A, B]), a), fingerprintOf(Schema.Union([A, B, C]), a))
2371 })
2372 })
2373
2374 describe("fingerprint mode failures", () => {
2375 const Person = Schema.Struct({ name: Schema.String, age: Schema.Number })
2376 const person = { name: "Ada", age: 36 }
2377
2378 it("fails closed when the layout fingerprint differs", () => {
2379 const frame = encodeFingerprint(Person, person)
2380 const other = SchemaBinary.toCodec(Schema.Struct({ label: Schema.String, age: Schema.Number }), {
2381 fingerprint: true
2382 })
2383 assert.include(
2384 schemaError(() => Schema.decodeUnknownSync(other)(frame)).message,
2385 "Expected matching layout fingerprint"
2386 )
2387 })
2388
2389 it("rejects the other mode's frames in both directions", () => {
2390 const fingerprintFrame = encodeFingerprint(Person, person)
2391 const defaultFrame = encode(Person, person)
2392 assert.include(
2393 schemaError(() => Schema.decodeUnknownSync(SchemaBinary.toCodec(Person))(fingerprintFrame)).message,
2394 "Expected version 2 envelope, flags 0"
2395 )
2396 assert.include(
2397 schemaError(() => Schema.decodeUnknownSync(SchemaBinary.toCodec(Person, { fingerprint: true }))(defaultFrame))
2398 .message,
2399 "Expected version 2 envelope, flags 1"
2400 )
2401 })
2402
2403 it("rejects truncated and oversized frames", () => {
2404 const codec = SchemaBinary.toCodec(Person, { fingerprint: true })
2405 const frame = encodeFingerprint(Person, person)
2406 // the fingerprint itself does not fit
2407 const shortFrame = concat(new Uint8Array([5, 0x21]), frame.subarray(2, 6))
2408 assert.include(
2409 schemaError(() => Schema.decodeUnknownSync(codec)(shortFrame)).message,
2410 "Expected complete value"
2411 )
2412 assert.include(
2413 schemaError(() => Schema.decodeUnknownSync(codec)(frame.subarray(0, frame.length - 1))).message,
2414 "Expected complete value"
2415 )
2416 assert.include(
2417 schemaError(() => Schema.decodeUnknownSync(codec)(concat(frame, new Uint8Array([0])))).message,
2418 "Expected no leftover bytes"
2419 )
2420 const withLeftover = new Uint8Array(frame)
2421 withLeftover[0] = frame[0] + 1
2422 assert.include(
2423 schemaError(() => Schema.decodeUnknownSync(codec)(concat(withLeftover, new Uint8Array([0])))).message,
2424 "Expected no leftover bytes"
2425 )
2426 })
2427
2428 it("rejects a union position outside the member table", () => {
2429 const Event = Schema.Union([
2430 Schema.Struct({ _tag: Schema.Literal("a"), n: Schema.Number }),
2431 Schema.Struct({ _tag: Schema.Literal("b"), s: Schema.String })
2432 ])
2433 const codec = SchemaBinary.toCodec(Event, { fingerprint: true })
2434 const frame = new Uint8Array(encodeFingerprint(Event, { _tag: "b", s: "x" }))
2435 // the byte after the envelope and fingerprint is the member position
2436 frame[10] = 7
2437 assert.include(
2438 schemaError(() => Schema.decodeUnknownSync(codec)(frame)).message,
2439 "Expected known union member"
2440 )
2441 })
2442
2443 it("reports a required field a newer writer left unreadable as missing", () => {
2444 const schema = Schema.Struct({ reason: Schema.CauseReason(Schema.String, Schema.String) })
2445 const frame = new Uint8Array(encodeFingerprint(schema, { reason: Cause.makeFailReason("boom") }))
2446 // the reason payload is length-prefixed; its first byte is the tag
2447 frame[11] = 9
2448 const error = schemaError(() =>
2449 Schema.decodeUnknownSync(SchemaBinary.toCodec(schema, { fingerprint: true }))(frame)
2450 )
2451 assert.include(error.message, "reason")
2452 assert.include(error.message, "Missing key")
2453 })
2454
2455 it("rejects duplicate extra keys", () => {
2456 const schema = Schema.Record(Schema.String, Schema.Number)
2457 const frame = encodeFingerprint(schema, { a: 1, b: 2 })
2458 const duplicated = new Uint8Array(frame)
2459 // rewrite the second key so both pairs claim "a"
2460 duplicated[duplicated.length - 2] = 97
2461 assert.include(
2462 schemaError(() => Schema.decodeUnknownSync(SchemaBinary.toCodec(schema, { fingerprint: true }))(duplicated))
2463 .message,
2464 "Expected unique extra keys"
2465 )
2466 })
2467 })
2468
2469 describe("codec memoization", () => {
2470 it("memoizes by schema and wire mode", () => {
2471 const schema = Schema.Struct({ id: Schema.Number, label: Schema.String })
2472
2473 const codec = SchemaBinary.toCodec(schema)
2474 assert.strictEqual(SchemaBinary.toCodec(schema), codec)
2475 assert.strictEqual(SchemaBinary.toCodec(schema, {}), codec)
2476 assert.strictEqual(SchemaBinary.toCodec(schema, { fingerprint: false }), codec)
2477
2478 const fingerprintCodec = SchemaBinary.toCodec(schema, { fingerprint: true })
2479 assert.strictEqual(SchemaBinary.toCodec(schema, { fingerprint: true }), fingerprintCodec)
2480 assert.notStrictEqual(fingerprintCodec, codec)
2481
2482 const directCodec = SchemaBinary.toCodecDirect(schema)
2483 assert.strictEqual(SchemaBinary.toCodecDirect(schema), directCodec)
2484 assert.strictEqual(SchemaBinary.toCodecDirect(schema, { fingerprint: false }), directCodec)
2485
2486 const directFingerprintCodec = SchemaBinary.toCodecDirect(schema, { fingerprint: true })
2487 assert.strictEqual(
2488 SchemaBinary.toCodecDirect(schema, { fingerprint: true }),
2489 directFingerprintCodec
2490 )
2491 assert.notStrictEqual(directFingerprintCodec, directCodec)
2492 })
2493 })
2494
2495 describe("generated regressions", () => {
2496 const isWellFormedString = (value: string): boolean => {
2497 for (let index = 0; index < value.length; index++) {
2498 const code = value.charCodeAt(index)
2499 if (code >= 0xD800 && code <= 0xDBFF) {
2500 if (index + 1 >= value.length) return false
2501 const trailing = value.charCodeAt(++index)
2502 if (trailing < 0xDC00 || trailing > 0xDFFF) return false
2503 } else if (code >= 0xDC00 && code <= 0xDFFF) {
2504 return false
2505 }
2506 }
2507 return true
2508 }
2509 const WellFormedString = Schema.String.check(
2510 Schema.makeFilter(isWellFormedString, { expected: "a well-formed Unicode string" })
2511 )
2512 const Event = Schema.Union([
2513 Schema.Struct({ _tag: Schema.tag("Text"), value: WellFormedString }),
2514 Schema.Struct({ _tag: Schema.tag("Count"), value: Schema.Number })
2515 ])
2516 const Generated = Schema.Struct({
2517 flag: Schema.Boolean,
2518 count: Schema.Int,
2519 ratio: Schema.Number,
2520 label: WellFormedString,
2521 bytes: Schema.Uint8Array,
2522 big: Schema.BigInt,
2523 optional: Schema.optionalKey(WellFormedString),
2524 tuple: Schema.Tuple([WellFormedString, Schema.Number, Schema.Boolean]),
2525 items: Schema.Array(Schema.Struct({ id: Schema.Int, name: WellFormedString })),
2526 attributes: Schema.Record(
2527 WellFormedString,
2528 Schema.Union([WellFormedString, Schema.Number, Schema.Boolean, Schema.Null])
2529 ),
2530 event: Event
2531 })
2532 it.live.prop(
2533 "keeps every encoding entry point equivalent for generated nested values",
2534 { value: Generated },
2535 ({ value }) =>
2536 Effect.sync(() => {
2537 for (const fingerprint of [false, true]) {
2538 const options = { fingerprint } as const
2539 const codec = SchemaBinary.toCodec(Generated, options)
2540 const direct = SchemaBinary.toCodecDirect(Generated, options)
2541 const frame = Schema.encodeUnknownSync(codec)(value)
2542 const optimizedFrame = SchemaBinary.encodeUnknownSync(Generated, options)(value)
2543 const statefulFrame = SchemaBinary.encoder(Generated, options).encode(value)
2544
2545 assert.deepStrictEqual(optimizedFrame, frame)
2546 assert.deepStrictEqual(statefulFrame, frame)
2547 assert.deepStrictEqual(Schema.decodeUnknownSync(codec)(frame), value)
2548 assert.deepStrictEqual(Schema.decodeUnknownSync(direct)(frame), value)
2549 assert.deepStrictEqual(SchemaBinary.parser(Generated, options).feedSync(frame), [value])
2550 }
2551 }),
2552 { arbitrary: { runs: 200 } }
2553 )
2554
2555 it.live.prop(
2556 "parses generated batches across arbitrary chunk boundaries",
2557 {
2558 values: Schema.Array(Generated).check(Schema.isMaxLength(20)),
2559 chunkSizes: Schema.Array(
2560 Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 64 }))
2561 ).check(Schema.isMinLength(1), Schema.isMaxLength(30)),
2562 fingerprint: Schema.Boolean
2563 },
2564 ({ chunkSizes, fingerprint, values }) =>
2565 Effect.sync(() => {
2566 const options = fingerprint ? { fingerprint: true } as const : undefined
2567 const bytes = SchemaBinary.encodeManyUnknownSync(Generated, options)(values)
2568 const parser = SchemaBinary.parser(Generated, options)
2569 const decoded: Array<typeof Generated.Type> = []
2570 let offset = 0
2571 for (const size of chunkSizes) {
2572 if (offset >= bytes.length) break
2573 const end = Math.min(offset + size, bytes.length)
2574 decoded.push(...parser.feedSync(bytes.subarray(offset, end)))
2575 offset = end
2576 }
2577 if (offset < bytes.length) decoded.push(...parser.feedSync(bytes.subarray(offset)))
2578 parser.endSync()
2579 assert.deepStrictEqual(decoded, values)
2580 }),
2581 { arbitrary: { runs: 100 } }
2582 )
2583
2584 it.live.prop(
2585 "rejects every generated truncated frame",
2586 {
2587 value: Generated,
2588 offset: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
2589 fingerprint: Schema.Boolean
2590 },
2591 ({ fingerprint, offset, value }) =>
2592 Effect.sync(() => {
2593 const options = fingerprint ? { fingerprint: true } as const : undefined
2594 const bytes = SchemaBinary.encodeUnknownSync(Generated, options)(value)
2595 const cut = 1 + offset % (bytes.length - 1)
2596 const parser = SchemaBinary.parser(Generated, options)
2597
2598 assert.deepStrictEqual(parser.feedSync(bytes.subarray(0, cut)), [])
2599 assert.match(schemaError(() => parser.endSync()).message, /complete value/)
2600 }),
2601 { arbitrary: { runs: 100 } }
2602 )
2603
2604 it("owns buffered and decoded bytes independently of caller buffers", () => {
2605 const schema = Schema.Struct({ label: Schema.String, bytes: Schema.Uint8Array })
2606 const value = { label: "fragmented", bytes: Uint8Array.of(1, 2, 3, 4) }
2607 const frame = encode(schema, value)
2608 const split = Math.floor(frame.length / 2)
2609 const prefix = frame.slice(0, split)
2610 const parser = SchemaBinary.parser(schema)
2611
2612 assert.deepStrictEqual(parser.feedSync(prefix), [])
2613 prefix.fill(0)
2614 const [decoded] = parser.feedSync(frame.subarray(split))
2615 frame.fill(0)
2616
2617 assert.deepStrictEqual(decoded, value)
2618 assert.notStrictEqual(decoded.bytes.buffer, frame.buffer)
2619 parser.endSync()
2620 })
2621 })
2622
2623 describe("direct codec", () => {
2624 const Person = Schema.Struct({ name: Schema.String, age: Schema.Number })
2625
2626 it("matches toCodec on the wire and round-trips", () => {
2627 const direct = SchemaBinary.toCodecDirect(Person)
2628 const value = { name: "Ada", age: 36 }
2629 const bytes = Schema.encodeUnknownSync(direct)(value)
2630 assert.deepStrictEqual(Array.from(bytes), Array.from(encode(Person, value)))
2631 assert.deepStrictEqual(Schema.decodeUnknownSync(direct)(bytes), value)
2632 })
2633
2634 it("still rejects invalid input, through the binary layer", () => {
2635 const direct = SchemaBinary.toCodecDirect(Person)
2636 assert.include(schemaError(() => Schema.encodeUnknownSync(direct)({ name: "Ada", age: "old" })).message, "age")
2637 assert.include(schemaError(() => Schema.encodeUnknownSync(direct)({ name: "Ada" })).message, "age")
2638 assert.include(schemaError(() => Schema.encodeUnknownSync(direct)("Ada")).message, "Expected an object")
2639 })
2640
2641 it("rejects a wrong runtime type for every leaf an exact schema can reach", () => {
2642 const cases: ReadonlyArray<readonly [Schema.Top, unknown, string]> = [
2643 [Schema.String, 1, "a string"],
2644 [Schema.Number, "1", "a number"],
2645 [Schema.Boolean, 1, "a boolean"],
2646 [Schema.BigInt, 1, "a bigint"],
2647 [Schema.Symbol, "s", "a symbol"],
2648 [Schema.Null, 1, "null"],
2649 [Schema.Undefined, 1, "undefined"],
2650 [Schema.Uint8Array, [1, 2], "a Uint8Array"],
2651 [Schema.Date, 0, "a Date"],
2652 [Schema.Array(Schema.String), "ab", "an array"],
2653 [Schema.Struct({ a: Schema.String }), "a", "an object"]
2654 ]
2655 for (const [schema, value, expected] of cases) {
2656 const direct = SchemaBinary.toCodecDirect(schema as Schema.Codec<unknown>)
2657 assert.include(
2658 schemaError(() => Schema.encodeUnknownSync(direct)(value)).message,
2659 `Expected ${expected}`,
2660 `${expected} for ${globalThis.String(value)}`
2661 )
2662 }
2663 })
2664
2665 it("keeps failure exits on the schema pass when only the success side is exact", () => {
2666 const ExitSchema = Schema.Exit(
2667 Schema.Struct({ id: Schema.String }),
2668 Schema.String,
2669 Schema.Defect()
2670 )
2671 const direct = SchemaBinary.toCodecDirect(ExitSchema as Schema.Codec<unknown, unknown>)
2672 const sound = SchemaBinary.toCodec(ExitSchema as Schema.Codec<unknown, unknown>)
2673 const exits = [
2674 Exit.succeed({ id: "user-1" }),
2675 Exit.fail("nope"),
2676 Exit.die(new Error("boom")),
2677 Exit.failCause(Cause.interrupt(7))
2678 ]
2679 for (const exit of exits) {
2680 const bytes = Schema.encodeUnknownSync(direct)(exit)
2681 assert.deepStrictEqual(Array.from(bytes), Array.from(Schema.encodeUnknownSync(sound)(exit)))
2682 assert.deepStrictEqual(
2683 Schema.decodeUnknownSync(direct)(bytes.slice()),
2684 Schema.decodeUnknownSync(sound)(bytes.slice())
2685 )
2686 }
2687 const decodedDie = Schema.decodeUnknownSync(direct)(
2688 Schema.encodeUnknownSync(direct)(Exit.die(new Error("boom"))).slice()
2689 ) as Exit.Exit<unknown, unknown>
2690 assert.isTrue(Exit.isFailure(decodedDie))
2691 const reason = (decodedDie as Exit.Failure<unknown, unknown>).cause.reasons[0] as Cause.Die
2692 assert.instanceOf(reason.defect, Error)
2693 assert.strictEqual((reason.defect as Error).message, "boom")
2694
2695 assert.isDefined(schemaError(() => Schema.encodeUnknownSync(direct)(Exit.succeed({ id: 42 }))))
2696 assert.isDefined(schemaError(() => Schema.encodeUnknownSync(direct)("not an exit")))
2697
2698 const excess = Exit.succeed({ id: "ok", extra: true })
2699 assert.deepStrictEqual(
2700 Array.from(Schema.encodeUnknownSync(direct)(excess)),
2701 Array.from(Schema.encodeUnknownSync(sound)(excess))
2702 )
2703 assert.include(
2704 schemaError(() => Schema.encodeUnknownSync(direct, { onExcessProperty: "error" })(excess)).message,
2705 "extra"
2706 )
2707 assert.include(
2708 schemaError(() =>
2709 SchemaBinary.encodeUnknownSync(
2710 ExitSchema as Schema.Codec<unknown, unknown>,
2711 { onExcessProperty: "error" }
2712 )(excess)
2713 ).message,
2714 "extra"
2715 )
2716 })
2717
2718 it("rejects an inherited discriminator, like the schema pass", () => {
2719 const schema = Schema.Union([
2720 Schema.Struct({ _tag: Schema.Literal("A"), value: Schema.Number }),
2721 Schema.Struct({ _tag: Schema.Literal("B"), value: Schema.String })
2722 ])
2723 const inherited = Object.assign(Object.create({ _tag: "A" }), { value: 1 })
2724 for (const options of [undefined, { fingerprint: true }]) {
2725 const encodeDirect = Schema.encodeUnknownSync(SchemaBinary.toCodecDirect(schema, options))
2726 assert.isDefined(schemaError(() => encodeDirect(inherited)))
2727 }
2728 })
2729
2730 it("reports excess properties when the call site asks for it", () => {
2731 const direct = SchemaBinary.toCodecDirect(Person)
2732 const excess = { name: "Ada", age: 36, extra: true }
2733 assert.deepStrictEqual(
2734 Array.from(Schema.encodeUnknownSync(direct)(excess)),
2735 Array.from(encode(Person, { name: "Ada", age: 36 }))
2736 )
2737 assert.include(
2738 schemaError(() => Schema.encodeUnknownSync(direct, { onExcessProperty: "error" })(excess)).message,
2739 "extra"
2740 )
2741 })
2742
2743 it("reports excess properties through encodeUnknownSync", () => {
2744 const excess = { name: "Ada", age: 36, extra: true }
2745 assert.include(
2746 schemaError(() => SchemaBinary.encodeUnknownSync(Person, { onExcessProperty: "error" })(excess)).message,
2747 "extra"
2748 )
2749 })
2750
2751 it("falls back to toCodec when the binary layer cannot prove the schema", () => {
2752 const NonNegative = Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))
2753 const direct = SchemaBinary.toCodecDirect(NonNegative)
2754 assert.isTrue(Schema.is(direct)(1))
2755 assert.isFalse(Schema.is(direct)(-1))
2756 assert.include(schemaError(() => Schema.encodeUnknownSync(direct)(-1)).message, "greater than or equal to 0")
2757 })
2758 })
2759
2760 describe("encodeManyUnknownSync", () => {
2761 const Person = Schema.Struct({ name: Schema.String, age: Schema.Number })
2762 const people = [{ name: "Ada", age: 36 }, { name: "Grace", age: 45 }]
2763
2764 it("writes the same bytes as concatenated single frames", () => {
2765 const many = SchemaBinary.encodeManyUnknownSync(Person, { fingerprint: true })
2766 assert.deepStrictEqual(
2767 Array.from(many(people)),
2768 Array.from(concat(...people.map((person) => encodeFingerprint(Person, person))))
2769 )
2770 assert.deepStrictEqual(SchemaBinary.parser(Person, { fingerprint: true }).feedSync(many(people)), people)
2771 assert.deepStrictEqual(Array.from(many([])), [])
2772 })
2773
2774 it("reports the failing frame and takes the fallback path for checked schemas", () => {
2775 const many = SchemaBinary.encodeManyUnknownSync(Person)
2776 assert.include(schemaError(() => many([people[0], { name: "Grace" }])).message, "age")
2777
2778 const Checked = Schema.Struct({ age: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)) })
2779 const checked = SchemaBinary.encodeManyUnknownSync(Checked)
2780 assert.deepStrictEqual(
2781 Array.from(checked([{ age: 1 }, { age: 2 }])),
2782 Array.from(concat(encode(Checked, { age: 1 }), encode(Checked, { age: 2 })))
2783 )
2784 assert.include(schemaError(() => checked([{ age: -1 }])).message, "greater than or equal to 0")
2785 })
2786
2787 it("reports excess properties", () => {
2788 const excess = { name: "Ada", age: 36, extra: true }
2789 assert.include(
2790 schemaError(() =>
2791 SchemaBinary.encodeManyUnknownSync(Person, { onExcessProperty: "error" })([people[0], excess])
2792 ).message,
2793 "extra"
2794 )
2795 })
2796 })
2797
2798 describe("schemas the binary layer validates on its own", () => {
2799 // `dictionary: true` is refused for anything the binary layer does not
2800 // fully validate, so it doubles as an assertion that the schema pass
2801 // around the codec is redundant and gets skipped.
2802 const validatesItself = (schema: Schema.Codec<any, any>): boolean => {
2803 try {
2804 SchemaBinary.encoder(schema, { dictionary: true })
2805 return true
2806 } catch (error) {
2807 if (error instanceof Error && error.message.includes("dictionary")) return false
2808 throw error
2809 }
2810 }
2811
2812 it("covers never, which an inferred schema reaches through an empty array", () => {
2813 assert.isTrue(validatesItself(Schema.Never))
2814 const Row = Schema.Struct({ id: Schema.String, tags: Schema.Array(Schema.Never) })
2815 assert.isTrue(validatesItself(Row))
2816 const value = { id: "a", tags: [] as ReadonlyArray<never> }
2817 assert.deepStrictEqual(roundtrip(Row, value), value)
2818 assert.include(schemaError(() => encode(Row, { id: "a", tags: ["x"] } as any)).message, "never")
2819 assert.isFalse(Schema.is(SchemaBinary.toCodec(Row))({ id: 1, tags: [] } as any))
2820 })
2821
2822 it("covers unknown and any, which the JSON layer already rejects or accepts", () => {
2823 assert.isTrue(validatesItself(Schema.Unknown))
2824 assert.isTrue(validatesItself(Schema.Any))
2825 // A JSON payload can be a string, so an object keyword still needs the pass.
2826 assert.isFalse(validatesItself(Schema.ObjectKeyword))
2827 const Row = Schema.Struct({ id: Schema.String, payload: Schema.Unknown })
2828 assert.isTrue(validatesItself(Row))
2829 const value = { id: "a", payload: { nested: [1, "two", true, null] } }
2830 assert.deepStrictEqual(roundtrip(Row, value), value)
2831 assert.isFalse(Schema.is(SchemaBinary.toCodec(Row))({ id: 1, payload: 1 } as any))
2832 const cyclic: any = { id: "a", payload: {} }
2833 cyclic.payload.self = cyclic.payload
2834 assert.include(schemaError(() => encode(Row, cyclic)).message, "acyclic value")
2835 })
2836
2837 it("covers recursion on decode while encode keeps the cycle walk", () => {
2838 interface TreeType {
2839 readonly value: string
2840 readonly children: ReadonlyArray<TreeType>
2841 }
2842 const Tree = Schema.Struct({
2843 value: Schema.String,
2844 children: Schema.Array(Schema.suspend((): Schema.Codec<TreeType> => Tree as any))
2845 })
2846 assert.isTrue(validatesItself(Tree))
2847 const tree = { value: "root", children: [{ value: "leaf", children: [] }] }
2848 assert.deepStrictEqual(roundtrip(Tree, tree), tree)
2849 assert.isFalse(Schema.is(SchemaBinary.toCodec(Tree))({ value: 1, children: [] } as any))
2850 assert.include(schemaError(() => encode(Tree, { value: "root", children: [1] } as any)).message, "object")
2851 const cyclic: any = { value: "root", children: [] }
2852 cyclic.children.push(cyclic)
2853 assert.include(schemaError(() => encode(Tree, cyclic)).message, "acyclic value")
2854 })
2855 })
2856
2857 describe("connection dictionary", () => {
2858 const Message = Schema.Struct({
2859 tag: Schema.String,
2860 traceId: Schema.String,
2861 body: Schema.String,
2862 n: Schema.Number
2863 })
2864 const message = (index: number) => ({
2865 tag: ["Search", "Get", "Delete"][index % 3],
2866 traceId: "0123456789abcdef0123456789abcdef",
2867 body: `body ${index}`,
2868 n: index
2869 })
2870
2871 it("round-trips a stream and shrinks repeated strings", () => {
2872 for (const fingerprint of [false, true]) {
2873 const options = { dictionary: true, fingerprint } as const
2874 const encoder = SchemaBinary.encoder(Message, options)
2875 const parser = SchemaBinary.parser(Message, options)
2876 const plain = SchemaBinary.encoder(Message, { fingerprint })
2877 const values = Array.from({ length: 30 }, (_, index) => message(index))
2878 const decoded: Array<unknown> = []
2879 const sizes: Array<number> = []
2880 for (const value of values) {
2881 const frame = encoder.encode(value)
2882 sizes.push(frame.length)
2883 decoded.push(...parser.feedSync(frame))
2884 }
2885 assert.deepStrictEqual(decoded, values)
2886 // The first frame carries every string, so it costs what no dictionary
2887 // would; later frames reference the ones that came back.
2888 assert.strictEqual(sizes[0], plain.encode(values[0]).length)
2889 assert.isBelow(sizes[sizes.length - 1], plain.encode(values[values.length - 1]).length * 0.6)
2890 }
2891 })
2892
2893 it("round-trips arrays of strings across frames", () => {
2894 const values = [["a", "b"], ["a", "c"], ["a", "a"]]
2895 const encoder = SchemaBinary.encoder(Schema.Array(Schema.String), { dictionary: true })
2896 const parser = SchemaBinary.parser(Schema.Array(Schema.String), { dictionary: true })
2897 const decoded = values.flatMap((value) => parser.feedSync(encoder.encode(value)))
2898 assert.deepStrictEqual(decoded, values)
2899
2900 const Message = Schema.Struct({ name: Schema.String, tags: Schema.Array(Schema.String) })
2901 const messages = [
2902 { name: "shared", tags: ["a", "b"] },
2903 { name: "other", tags: ["shared", "c"] },
2904 { name: "shared", tags: ["a", "a"] }
2905 ]
2906 const messageEncoder = SchemaBinary.encoder(Message, { dictionary: true })
2907 const messageParser = SchemaBinary.parser(Message, { dictionary: true })
2908 const decodedMessages = messages.flatMap((value) => messageParser.feedSync(messageEncoder.encode(value)))
2909 assert.deepStrictEqual(decodedMessages, messages)
2910 })
2911
2912 it("round-trips arrays of strings inside row runs across frames", () => {
2913 const Row = Schema.Struct({ id: Schema.Number, tags: Schema.Array(Schema.String) })
2914 const Rows = Schema.Array(Row)
2915 const values = [
2916 [{ id: 1, tags: ["x"] }],
2917 [{ id: 2, tags: ["x"] }],
2918 [{ id: 3, tags: ["x"] }]
2919 ]
2920 const encoder = SchemaBinary.encoder(Rows, { dictionary: true })
2921 const parser = SchemaBinary.parser(Rows, { dictionary: true })
2922 const decoded = values.flatMap((value) => parser.feedSync(encoder.encode(value)))
2923 assert.deepStrictEqual(decoded, values)
2924 })
2925
2926 it("round-trips batched frames and fragmented feeds", () => {
2927 const encoder = SchemaBinary.encoder(Message, { dictionary: true })
2928 const parser = SchemaBinary.parser(Message, { dictionary: true })
2929 const values = Array.from({ length: 12 }, (_, index) => message(index))
2930 assert.deepStrictEqual(parser.feedSync(encoder.encodeMany(values)), values)
2931
2932 const fragmented = SchemaBinary.encoder(Message, { dictionary: true })
2933 const fragmentedParser = SchemaBinary.parser(Message, { dictionary: true })
2934 const decoded: Array<unknown> = []
2935 for (const value of values) {
2936 const frame = fragmented.encode(value).slice()
2937 decoded.push(...fragmentedParser.feedSync(frame.subarray(0, 1)))
2938 decoded.push(...fragmentedParser.feedSync(frame.subarray(1)))
2939 }
2940 assert.deepStrictEqual(decoded, values)
2941 })
2942
2943 it("keeps the reader in step when a frame fails to encode", () => {
2944 const encoder = SchemaBinary.encoder(Message, { dictionary: true })
2945 const parser = SchemaBinary.parser(Message, { dictionary: true })
2946 const first = message(0)
2947 const decoded: Array<unknown> = [...parser.feedSync(encoder.encode(first))]
2948 // Never reaches the reader, so its strings must not reach the writer's
2949 // tables either.
2950 assert.include(
2951 schemaError(() => encoder.encode({ ...message(1), tag: "NeverSent", n: "not a number" })).message,
2952 "number"
2953 )
2954 assert.include(
2955 schemaError(() => encoder.encodeMany([message(2), { ...message(3), n: "not a number" }])).message,
2956 "number"
2957 )
2958 const next = message(4)
2959 decoded.push(...parser.feedSync(encoder.encode(next)))
2960 assert.deepStrictEqual(decoded, [first, next])
2961 })
2962
2963 it("reports excess properties before advancing the dictionary", () => {
2964 const encoder = SchemaBinary.encoder(Message, {
2965 dictionary: true,
2966 onExcessProperty: "error"
2967 })
2968 const parser = SchemaBinary.parser(Message, { dictionary: true })
2969 assert.include(
2970 schemaError(() => encoder.encode({ ...message(0), extra: "NeverSent" })).message,
2971 "extra"
2972 )
2973 assert.include(
2974 schemaError(() => encoder.encodeMany([message(1), { ...message(2), extra: "NeverSent" }])).message,
2975 "extra"
2976 )
2977 const next = message(3)
2978 assert.deepStrictEqual(parser.feedSync(encoder.encode(next)), [next])
2979 })
2980
2981 it("costs nothing on a field whose strings never repeat", () => {
2982 const Unique = Schema.Struct({ s: Schema.String })
2983 const encoder = SchemaBinary.encoder(Unique, { dictionary: true })
2984 const parser = SchemaBinary.parser(Unique, { dictionary: true })
2985 const plain = SchemaBinary.encoder(Unique, {})
2986 const values = Array.from({ length: 200 }, (_, index) => ({ s: `value-${index}` }))
2987 const decoded: Array<unknown> = []
2988 let dictionaryBytes = 0
2989 let plainBytes = 0
2990 for (const value of values) {
2991 const frame = encoder.encode(value)
2992 dictionaryBytes += frame.length
2993 plainBytes += plain.encode(value).length
2994 decoded.push(...parser.feedSync(frame))
2995 }
2996 assert.deepStrictEqual(decoded, values)
2997 assert.strictEqual(dictionaryBytes, plainBytes)
2998 })
2999
3000 it("rejects a schema the binary layer does not validate on its own", () => {
3001 const Checked = Schema.Struct({ s: Schema.String.check(Schema.isMinLength(2)) })
3002 assert.throws(() => SchemaBinary.encoder(Checked, { dictionary: true }), /dictionary/)
3003 assert.throws(() => SchemaBinary.parser(Checked, { dictionary: true }), /dictionary/)
3004 })
3005
3006 it("rejects a reference the stream never sent", () => {
3007 const encoder = SchemaBinary.encoder(Message, { dictionary: true })
3008 const parser = SchemaBinary.parser(Message, { dictionary: true })
3009 for (let index = 0; index < 4; index++) parser.feedSync(encoder.encode(message(index)))
3010 const frame = encoder.encode(message(4)).slice()
3011 // Point the tag at a table entry past the end of what was sent.
3012 const tagged = frame.map((byte) => byte === 1 ? 0xFF : byte)
3013 assert.throws(() => parser.feedSync(tagged))
3014 })
3015
3016 it("stays off by default", () => {
3017 const encoder = SchemaBinary.encoder(Message, {})
3018 assert.deepStrictEqual(Array.from(encoder.encode(message(0))), Array.from(encode(Message, message(0))))
3019 })
3020 })
3021
3022 describe("fingerprint mode parser", () => {
3023 const Person = Schema.Struct({ name: Schema.String, age: Schema.Number })
3024 const first = { name: "Ada", age: 36 }
3025 const second = { name: "Grace", age: 45 }
3026
3027 it("parses concatenated frames, one byte at a time, and mid-frame splits", () => {
3028 const frames = concat(encodeFingerprint(Person, first), encodeFingerprint(Person, second))
3029 assert.deepStrictEqual(
3030 SchemaBinary.parser(Person, { fingerprint: true }).feedSync(frames),
3031 [first, second]
3032 )
3033
3034 const byteParser = SchemaBinary.parser(Person, { fingerprint: true })
3035 const out: Array<unknown> = []
3036 for (const byte of frames) out.push(...byteParser.feedSync(new Uint8Array([byte])))
3037 byteParser.endSync()
3038 assert.deepStrictEqual(out, [first, second])
3039
3040 // a split inside the fingerprint must wait rather than fail
3041 const split = SchemaBinary.parser(Person, { fingerprint: true })
3042 assert.deepStrictEqual(split.feedSync(frames.subarray(0, 6)), [])
3043 assert.deepStrictEqual(split.feedSync(frames.subarray(6)), [first, second])
3044 })
3045
3046 it("fails closed on a default-mode frame in a fingerprint stream", () => {
3047 const parser = SchemaBinary.parser(Person, { fingerprint: true })
3048 const mixed = concat(encodeFingerprint(Person, first), encode(Person, second))
3049 assert.deepStrictEqual(parser.feedSync(mixed), [first])
3050 assert.include(
3051 schemaError(() => parser.feedSync(new Uint8Array(0))).message,
3052 "Expected version 2 envelope, flags 1"
3053 )
3054 assert.include(schemaError(() => parser.endSync()).message, "Expected parser is spent")
3055 })
3056
3057 it("honours maxFrameSize and reports truncation at end", () => {
3058 const bounded = SchemaBinary.parser(Person, { fingerprint: true, maxFrameSize: 4 })
3059 assert.include(
3060 schemaError(() => bounded.feedSync(encodeFingerprint(Person, first))).message,
3061 "Expected frame within maxFrameSize"
3062 )
3063 const truncated = SchemaBinary.parser(Person, { fingerprint: true })
3064 const frame = encodeFingerprint(Person, first)
3065 assert.deepStrictEqual(truncated.feedSync(frame.subarray(0, frame.length - 2)), [])
3066 assert.include(schemaError(() => truncated.endSync()).message, "Expected complete value")
3067 })
3068 })
3069
3070 describe("channels", () => {
3071 const Person = Schema.Struct({ name: Schema.String, age: Schema.Number })
3072 const ada = { name: "Ada", age: 36 }
3073 const grace = { name: "Grace", age: 45 }
3074
3075 it.effect("encode then decode round-trips a stream", () =>
3076 Effect.gen(function*() {
3077 const values = yield* Stream.make(ada, grace).pipe(
3078 Stream.pipeThroughChannel(SchemaBinary.encode(Person)()),
3079 Stream.pipeThroughChannel(SchemaBinary.decode(Person)()),
3080 Stream.runCollect
3081 )
3082
3083 assert.deepStrictEqual([...values], [ada, grace])
3084 }))
3085
3086 it.effect("encode batches a chunk into one concatenated byte element", () =>
3087 Effect.gen(function*() {
3088 const frames = yield* Stream.make(ada, grace).pipe(
3089 Stream.pipeThroughChannel(SchemaBinary.encode(Person)()),
3090 Stream.runCollect
3091 )
3092
3093 assert.deepStrictEqual([...frames], [concat(encode(Person, ada), encode(Person, grace))])
3094 }))
3095
3096 it.effect("encode rejects non-number elements in uniform number arrays", () =>
3097 Effect.gen(function*() {
3098 for (const value of ["x", undefined, null]) {
3099 const error = yield* Stream.make([1, value, 3] as any).pipe(
3100 Stream.pipeThroughChannel(SchemaBinary.encode(Schema.Array(Schema.Number))()),
3101 Stream.runCollect,
3102 Effect.flip
3103 )
3104 assert.instanceOf(error, Schema.SchemaError)
3105 assert.include(error.message, "Expected a number")
3106 assert.include(error.message, "at [1]")
3107 }
3108 }))
3109
3110 it.effect("encode reports excess properties", () =>
3111 Effect.gen(function*() {
3112 const error = yield* Stream.make({ ...ada, extra: true }).pipe(
3113 Stream.pipeThroughChannel(SchemaBinary.encode(Person, { onExcessProperty: "error" })()),
3114 Stream.runCollect,
3115 Effect.flip
3116 )
3117 assert.instanceOf(error, Schema.SchemaError)
3118 assert.include(error.message, "extra")
3119 }))
3120
3121 it.effect("decodes a value split across byte chunks", () =>
3122 Effect.gen(function*() {
3123 const frame = encode(Person, ada)
3124 const values = yield* Stream.make(frame.subarray(0, 3), frame.subarray(3)).pipe(
3125 Stream.pipeThroughChannel(SchemaBinary.decode(Person)()),
3126 Stream.runCollect
3127 )
3128
3129 assert.deepStrictEqual([...values], [ada])
3130 }))
3131
3132 it.effect("decodes concatenated frames in one chunk", () =>
3133 Effect.gen(function*() {
3134 const bytes = concat(encode(Person, ada), encode(Person, grace)) as Uint8Array<ArrayBuffer>
3135 const values = yield* Stream.make(bytes).pipe(
3136 Stream.pipeThroughChannel(SchemaBinary.decode(Person)()),
3137 Stream.runCollect
3138 )
3139
3140 assert.deepStrictEqual([...values], [ada, grace])
3141 }))
3142
3143 it.effect("fails deeply nested recursive frames in the error channel", () =>
3144 Effect.gen(function*() {
3145 type Nested = ReadonlyArray<Nested>
3146 let Nested: Schema.Codec<Nested>
3147 Nested = Schema.Array(Schema.suspend(() => Nested))
3148 const error = yield* Stream.make(nestedArrayFrame(10_000)).pipe(
3149 Stream.pipeThroughChannel(SchemaBinary.decode(Nested)()),
3150 Stream.runCollect,
3151 Effect.flip
3152 )
3153
3154 assert.instanceOf(error, Schema.SchemaError)
3155 assert.include(error.message, "nesting depth at most")
3156 }))
3157
3158 it.effect("fails when the stream ends with an incomplete frame", () =>
3159 Effect.gen(function*() {
3160 const frame = encode(Person, ada)
3161 const error = yield* Stream.make(frame.subarray(0, frame.length - 1)).pipe(
3162 Stream.pipeThroughChannel(SchemaBinary.decode(Person)()),
3163 Stream.runCollect,
3164 Effect.flip
3165 )
3166
3167 assert.instanceOf(error, Schema.SchemaError)
3168 assert.include(error.message, "Expected complete value")
3169 }))
3170
3171 it.effect("emits completed values before reporting a later failure", () =>
3172 Effect.gen(function*() {
3173 const bad = encode(Person, grace).slice()
3174 bad[1] = 0x10
3175 const seen: Array<typeof ada> = []
3176 const error = yield* Stream.make(
3177 concat(encode(Person, ada), bad) as Uint8Array<ArrayBuffer>,
3178 encode(Person, grace)
3179 ).pipe(
3180 Stream.pipeThroughChannel(SchemaBinary.decode(Person)()),
3181 Stream.tap((value) =>
3182 Effect.sync(() => {
3183 seen.push(value)
3184 })
3185 ),
3186 Stream.runDrain,
3187 Effect.flip
3188 )
3189
3190 assert.deepStrictEqual(seen, [ada])
3191 assert.include(error.message, "version 2 envelope, flags 0")
3192 }))
3193
3194 it.effect("enforces maxFrameSize on decode and ignores it on encode", () =>
3195 Effect.gen(function*() {
3196 const values = yield* Stream.make(ada).pipe(
3197 Stream.pipeThroughChannel(SchemaBinary.encode(Person, { maxFrameSize: 1 })()),
3198 Stream.pipeThroughChannel(SchemaBinary.decode(Person)()),
3199 Stream.runCollect
3200 )
3201 assert.deepStrictEqual([...values], [ada])
3202
3203 const error = yield* Stream.make(encode(Person, ada)).pipe(
3204 Stream.pipeThroughChannel(SchemaBinary.decode(Person, { maxFrameSize: 2 })()),
3205 Stream.runCollect,
3206 Effect.flip
3207 )
3208 assert.include(error.message, "Expected frame within maxFrameSize")
3209 }))
3210
3211 it.effect("duplex encodes requests and decodes responses with different schemas", () =>
3212 Effect.gen(function*() {
3213 const Request = Schema.Struct({ id: Schema.Number })
3214 const Response = Schema.Struct({ id: Schema.Number, name: Schema.String })
3215 const socket = SchemaBinary.decode(Request)<Schema.SchemaError>().pipe(
3216 Channel.map((chunk) => Arr.map(chunk, (request) => ({ id: request.id, name: `user-${request.id}` }))),
3217 Channel.pipeTo(SchemaBinary.encode(Response)<Schema.SchemaError>())
3218 )
3219
3220 const values = yield* Stream.make({ id: 1 }, { id: 2 }).pipe(
3221 Stream.pipeThroughChannel(
3222 SchemaBinary.duplex(socket, { inputSchema: Request, outputSchema: Response })
3223 ),
3224 Stream.runCollect
3225 )
3226
3227 assert.deepStrictEqual([...values], [{ id: 1, name: "user-1" }, { id: 2, name: "user-2" }])
3228 }))
3229
3230 it.effect("duplex supports the data-last call shape", () =>
3231 Effect.gen(function*() {
3232 const Request = Schema.Struct({ id: Schema.Number })
3233 const Response = Schema.Struct({ ok: Schema.Boolean })
3234 const socket = SchemaBinary.decode(Request)<Schema.SchemaError>().pipe(
3235 Channel.map((chunk) => Arr.map(chunk, () => ({ ok: true }))),
3236 Channel.pipeTo(SchemaBinary.encode(Response)<Schema.SchemaError>())
3237 )
3238
3239 const values = yield* Stream.make({ id: 1 }).pipe(
3240 Stream.pipeThroughChannel(
3241 socket.pipe(SchemaBinary.duplex({ inputSchema: Request, outputSchema: Response }))
3242 ),
3243 Stream.runCollect
3244 )
3245
3246 assert.deepStrictEqual([...values], [{ ok: true }])
3247 }))
3248
3249 it.effect("round-trips fingerprint mode through the channel path", () =>
3250 Effect.gen(function*() {
3251 const frames = yield* Stream.make(ada, grace).pipe(
3252 Stream.pipeThroughChannel(SchemaBinary.encode(Person, { fingerprint: true })()),
3253 Stream.runCollect
3254 )
3255 assert.deepStrictEqual(
3256 [...frames],
3257 [concat(encodeFingerprint(Person, ada), encodeFingerprint(Person, grace))]
3258 )
3259
3260 const values = yield* Stream.fromArray([...frames]).pipe(
3261 Stream.pipeThroughChannel(SchemaBinary.decode(Person, { fingerprint: true })()),
3262 Stream.runCollect
3263 )
3264 assert.deepStrictEqual([...values], [ada, grace])
3265 }))
3266
3267 const AsyncName = Schema.String.pipe(
3268 Schema.decodeTo(
3269 Schema.String,
3270 SchemaTransformation.transformEffect({
3271 decode: (value) => Effect.promise(() => Promise.resolve(value.toUpperCase())),
3272 encode: (value) => Effect.promise(() => Promise.resolve(value.toLowerCase()))
3273 })
3274 )
3275 )
3276 const AsyncPerson = Schema.Struct({ name: AsyncName, age: Schema.Number })
3277
3278 it.effect("supports async transformations on encode and decode", () =>
3279 Effect.gen(function*() {
3280 const frames = yield* Stream.make(ada, grace).pipe(
3281 Stream.pipeThroughChannel(SchemaBinary.encode(AsyncPerson)()),
3282 Stream.runCollect
3283 )
3284 // the async encode transformation ran before the bytes hit the wire
3285 assert.deepStrictEqual(
3286 [...frames],
3287 [concat(encode(Person, { name: "ada", age: 36 }), encode(Person, { name: "grace", age: 45 }))]
3288 )
3289
3290 const values = yield* Stream.fromArray([...frames]).pipe(
3291 Stream.pipeThroughChannel(SchemaBinary.decode(AsyncPerson)()),
3292 Stream.runCollect
3293 )
3294 assert.deepStrictEqual([...values], [{ name: "ADA", age: 36 }, { name: "GRACE", age: 45 }])
3295 }))
3296
3297 it.effect("supports async transformations across a duplex channel", () =>
3298 Effect.gen(function*() {
3299 const echo = Channel.identity<
3300 Arr.NonEmptyReadonlyArray<Uint8Array<ArrayBuffer>>,
3301 Schema.SchemaError,
3302 unknown
3303 >()
3304
3305 const values = yield* Stream.make(ada).pipe(
3306 Stream.pipeThroughChannel(
3307 SchemaBinary.duplex(echo, { inputSchema: AsyncPerson, outputSchema: AsyncPerson })
3308 ),
3309 Stream.runCollect
3310 )
3311
3312 assert.deepStrictEqual([...values], [{ name: "ADA", age: 36 }])
3313 }))
3314
3315 it.effect("emits values decoded before an async transformation failure", () =>
3316 Effect.gen(function*() {
3317 const Wire = Schema.Struct({ name: Schema.String })
3318 const Reader = Schema.Struct({
3319 name: Schema.String.pipe(
3320 Schema.decodeTo(
3321 Schema.String,
3322 SchemaTransformation.transformEffect({
3323 decode: (value, options) =>
3324 value === "bob"
3325 ? Effect.fail(new SchemaIssue.InvalidValue({ expected: "not bob" }, value, options))
3326 : Effect.promise(() => Promise.resolve(value)),
3327 encode: (value) => Effect.succeed(value)
3328 })
3329 )
3330 )
3331 })
3332 const bytes = concat(encode(Wire, { name: "ada" }), encode(Wire, { name: "bob" })) as Uint8Array<ArrayBuffer>
3333 const seen: Array<{ name: string }> = []
3334 const error = yield* Stream.make(bytes).pipe(
3335 Stream.pipeThroughChannel(SchemaBinary.decode(Reader)()),
3336 Stream.tap((value) =>
3337 Effect.sync(() => {
3338 seen.push(value)
3339 })
3340 ),
3341 Stream.runDrain,
3342 Effect.flip
3343 )
3344
3345 assert.deepStrictEqual(seen, [{ name: "ada" }])
3346 assert.include(error.message, "not bob")
3347 }))
3348 })
3349})