From 9aa3dc11ae3208fe764b058edb78b6bdef73a434 Mon Sep 17 00:00:00 2001 From: Alec Helmturner Date: Thu, 4 Jan 2024 00:34:34 -0600 Subject: [PATCH] fix: disambiguate serialization output of builtins --- src/async/asyncTypes2.ts | 60 ++++++++++++++----------- src/async/handlers/tsonPromise2.test.ts | 10 ++--- src/async/serializeAsync2.test.ts | 42 ++++++++--------- src/async/serializeAsync2.ts | 44 +++++++++--------- 4 files changed, 84 insertions(+), 72 deletions(-) diff --git a/src/async/asyncTypes2.ts b/src/async/asyncTypes2.ts index 032e30c..5298e2b 100644 --- a/src/async/asyncTypes2.ts +++ b/src/async/asyncTypes2.ts @@ -10,6 +10,38 @@ import { createTsonAsyncUnfoldFn, } from "./createUnfoldAsyncFn.js"; +export const ChunkTypes = { + BODY: "BODY", + ERROR: "ERROR", + HEAD: "HEAD", + LEAF: "LEAF", + REF: "REF", + TAIL: "TAIL", +} as const; + +export type ChunkTypes = { + [key in keyof typeof ChunkTypes]: (typeof ChunkTypes)[key]; +}; + +export const TsonStatus = { + //MULTI_STATUS: 207, + ERROR: 500, + INCOMPLETE: 203, + OK: 200, +} as const; + +export type TsonStatus = { + [key in keyof typeof TsonStatus]: (typeof TsonStatus)[key]; +}; + +export const TsonStructures = { + ARRAY: 0, + ITERABLE: 2, + POJO: 1, +} as const; + +export type TsonStructures = typeof TsonStructures; + export interface TsonAsyncChunk { chunk: T; key?: null | number | string | undefined; @@ -45,7 +77,6 @@ export type TsonAsyncType< TSerializedType extends SerializedType, > = TsonTypeTesterCustom & TsonAsyncMarshaller; - export interface TsonAsyncOptions { /** * A list of guards to apply to every value @@ -63,30 +94,6 @@ export interface TsonAsyncOptions { types: (TsonAsyncType | TsonType)[]; } -export const ChunkTypes = { - BODY: "BODY", - ERROR: "ERROR", - HEAD: "HEAD", - LEAF: "LEAF", - REF: "REF", - TAIL: "TAIL", -} as const; - -export type ChunkTypes = { - [key in keyof typeof ChunkTypes]: (typeof ChunkTypes)[key]; -}; - -export const TsonStatus = { - //MULTI_STATUS: 207, - ERROR: 500, - INCOMPLETE: 203, - OK: 200, -} as const; - -export type TsonStatus = { - [key in keyof typeof TsonStatus]: (typeof TsonStatus)[key]; -}; - export type TsonAsyncTupleHeader = [ Id: `${TsonNonce}${number}`, ParentId: `${TsonNonce}${"" | number}`, @@ -110,7 +117,7 @@ export type TsonAsyncBodyTuple = [ export type TsonAsyncHeadTuple = [ ChunkType: ChunkTypes["HEAD"], Header: TsonAsyncTupleHeader, - TypeHandlerKey?: string | undefined, + TypeHandlerKey?: TsonStructures[keyof TsonStructures] | string | undefined, ]; export type TsonAsyncReferenceTuple = [ @@ -142,3 +149,4 @@ export type TsonAsyncTuple = | TsonAsyncLeafTuple | TsonAsyncReferenceTuple | TsonAsyncTailTuple; + diff --git a/src/async/handlers/tsonPromise2.test.ts b/src/async/handlers/tsonPromise2.test.ts index bf59f09..7c990f2 100644 --- a/src/async/handlers/tsonPromise2.test.ts +++ b/src/async/handlers/tsonPromise2.test.ts @@ -115,21 +115,21 @@ test("serialize promise that returns a promise", async () => { expect(leaves).toHaveLength(3); expect(tails).toHaveLength(6); - expect(heads[0]).toStrictEqual([ChunkTypes.HEAD, [idOf(0), nonce, null]]); + expect(heads[0]).toStrictEqual([ChunkTypes.HEAD, [idOf(0), nonce, null], 1]); expect(heads[1]).toStrictEqual([ ChunkTypes.HEAD, [anyId, idOf(0), "promise"], tsonPromise.key, ]); - expect(heads[2]).toStrictEqual([ChunkTypes.HEAD, [anyId, anyId, null]]); - expect(heads[3]).toStrictEqual([ChunkTypes.HEAD, [anyId, anyId, 1]]); + expect(heads[2]).toStrictEqual([ChunkTypes.HEAD, [anyId, anyId, null], 0]); + expect(heads[3]).toStrictEqual([ChunkTypes.HEAD, [anyId, anyId, 1], 1]); expect(heads[4]).toStrictEqual([ ChunkTypes.HEAD, [anyId, anyId, "anotherPromise"], tsonPromise.key, ]); - expect(heads[5]).toStrictEqual([ChunkTypes.HEAD, [anyId, anyId, null]]); + expect(heads[5]).toStrictEqual([ChunkTypes.HEAD, [anyId, anyId, null], 0]); }); test("promise that rejects", async () => { @@ -184,7 +184,7 @@ test("promise that rejects", async () => { tsonPromise.key, ]); - expect(heads[1]).toEqual([ChunkTypes.HEAD, [anyId, idOf(0), null]]); + expect(heads[1]).toEqual([ChunkTypes.HEAD, [anyId, idOf(0), null], 0]); expect(leaves[0]).toEqual([ChunkTypes.LEAF, [anyId, anyId, 0], 1]); expect(leaves[1]).toEqual([ ChunkTypes.LEAF, diff --git a/src/async/serializeAsync2.test.ts b/src/async/serializeAsync2.test.ts index f57d3de..7f29491 100644 --- a/src/async/serializeAsync2.test.ts +++ b/src/async/serializeAsync2.test.ts @@ -2,7 +2,12 @@ import { assertType, describe, expect, test } from "vitest"; import { tsonBigint } from "../index.js"; import { expectSequence } from "../internals/testUtils.js"; -import { ChunkTypes, TsonAsyncTuple, TsonStatus } from "./asyncTypes2.js"; +import { + ChunkTypes, + TsonAsyncTuple, + TsonStatus, + TsonStructures, +} from "./asyncTypes2.js"; import { tsonPromise } from "./handlers/tsonPromise2.js"; import { createTsonSerializeAsync } from "./serializeAsync2.js"; @@ -10,7 +15,7 @@ const nonce = "__tson"; const anyId = expect.stringMatching(`^${nonce}[0-9]+$`); const idOf = (id: number | string) => `${nonce}${id}`; -describe("serialize", (it) => { +describe("AsyncSerialize", (it) => { it("should handle primitives correctly", async ({ expect }) => { const options = { guards: [], @@ -56,7 +61,7 @@ describe("serialize", (it) => { expect(chunks.length).toBe(3); expect(chunks).toEqual([ - [ChunkTypes.HEAD, [rootId, nonce, null]], + [ChunkTypes.HEAD, [rootId, nonce, null], TsonStructures.POJO], [ChunkTypes.REF, [anyId, rootId, "self"], rootId], [ChunkTypes.TAIL, [anyId, rootId, null], TsonStatus.OK], ]); @@ -67,22 +72,17 @@ describe("serialize", (it) => { ["string", "hello"], ["boolean", true], ["null", null], - ])( - `should serialize %s primitives without a handler`, - async (type, value) => { - const options = { guards: [], nonce: () => nonce, types: [] }; - const serialize = createTsonSerializeAsync(options); - const chunks: TsonAsyncTuple[] = []; - for await (const chunk of serialize(value)) { - chunks.push(chunk); - } + ])(`should serialize %s primitives without a handler`, async (_, value) => { + const options = { guards: [], nonce: () => nonce, types: [] }; + const serialize = createTsonSerializeAsync(options); + const chunks: TsonAsyncTuple[] = []; + for await (const chunk of serialize(value)) { + chunks.push(chunk); + } - expect(chunks.length).toBe(1); - expect(chunks).toEqual([ - [ChunkTypes.LEAF, [idOf(0), nonce, null], value], - ]); - }, - ); + expect(chunks.length).toBe(1); + expect(chunks).toEqual([[ChunkTypes.LEAF, [idOf(0), nonce, null], value]]); + }); it("should serialize values with a sync handler", async ({ expect }) => { const options = { @@ -105,9 +105,7 @@ describe("serialize", (it) => { [ChunkTypes.LEAF, [idOf(0), nonce, null], "0", "bigint"], ]); }); -}); -describe("serializeAsync", (it) => { it("should serialize values with an async handler", async ({ expect }) => { const options = { guards: [], @@ -153,7 +151,7 @@ describe("serializeAsync", (it) => { expect(heads).toStrictEqual([ [ChunkTypes.HEAD, [head_1_id, nonce, null], tsonPromise.key], - [ChunkTypes.HEAD, [head_2_id, head_1_id, null]], + [ChunkTypes.HEAD, [head_2_id, head_1_id, null], 0], ]); expect(leaves).toStrictEqual([ @@ -166,7 +164,9 @@ describe("serializeAsync", (it) => { [ChunkTypes.TAIL, [anyId, head_2_id, null], TsonStatus.OK], ]); }); +}); +describe("TsonGuards", (it) => { it("should apply guards and throw if they fail", async ({ expect }) => { const options = { guards: [{ assert: (val: unknown) => val !== "fail", key: "testGuard" }], diff --git a/src/async/serializeAsync2.ts b/src/async/serializeAsync2.ts index b3c4498..cc7b984 100644 --- a/src/async/serializeAsync2.ts +++ b/src/async/serializeAsync2.ts @@ -18,6 +18,7 @@ import { TsonAsyncTuple, TsonAsyncType, TsonStatus, + TsonStructures, } from "./asyncTypes2.js"; import { isAsyncIterableEsque, isIterableEsque } from "./iterableUtils.js"; @@ -149,7 +150,6 @@ export function createTsonSerializeAsync(opts: TsonAsyncOptions) { // Try to find a matching handler and initiate serialization const handler = selectHandler({ handlers, value }); - // fallback to parsing as json if (!handler) { applyGuards(value); @@ -161,6 +161,7 @@ export function createTsonSerializeAsync(opts: TsonAsyncOptions) { Promise.resolve([ ChunkTypes.HEAD, [thisId, parentId, key], + typeofStruct(value), ] as TsonAsyncHeadTuple).then(initializeIterable(iterator)), ); @@ -309,25 +310,28 @@ async function* toAsyncGenerator( } } -// function typeofStruct< -// T extends -// | AsyncIterable -// | Iterable -// | Record -// | any[], -// >(item: T): "array" | "iterable" | "pojo" { -// switch (true) { -// case Symbol.asyncIterator in item: -// return "iterable"; -// case Array.isArray(item): -// return "array"; -// case Symbol.iterator in item: -// return "iterable"; -// default: -// // we intentionally treat functions as pojos -// return "pojo"; -// } -// } +function typeofStruct< + T extends + | AsyncIterable + | Iterable + | Record + | any[], +>(item: T): TsonStructures[keyof TsonStructures] { + switch (true) { + case Symbol.asyncIterator in item: + return TsonStructures.ITERABLE; + case Array.isArray(item): + return TsonStructures.ARRAY; + case Symbol.iterator in item: + return TsonStructures.ITERABLE; + case typeof item === "object": + case typeof item === "function": + // we intentionally treat functions as pojos + return TsonStructures.POJO; + default: + throw new Error("Unexpected type"); + } +} // /** // * - Async iterables are iterated, and each value yielded is walked.