Last active
December 17, 2024 13:47
-
-
Save eikowagenknecht/44d653fadf6fb7b59fb5cab218c4d66a to your computer and use it in GitHub Desktop.
Helpers to pack data for Telegram callbacks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { z } from "zod"; | |
| /** | |
| * Represents a value that can be converted to a string using valueOf() | |
| * Typically used for enums or other string-like types | |
| */ | |
| interface EnumValue { | |
| valueOf(): string; | |
| } | |
| /** | |
| * Special marker used to represent boolean true in the packed string | |
| * @internal | |
| */ | |
| const SPECIAL_TRUE = "true"; | |
| /** | |
| * Special marker used to represent boolean false in the packed string | |
| * @internal | |
| */ | |
| const SPECIAL_FALSE = "false"; | |
| /** | |
| * Represents all valid field values that can be serialized | |
| * - string: Regular strings (will be escaped if containing special characters) | |
| * - EnumValue: Enum values or objects with string valueOf() | |
| * - number: Both integers and floating point numbers | |
| * - boolean: true/false values | |
| * - null: Represented as "null" in the packed string | |
| * - undefined: Represented as "undef" in the packed string | |
| */ | |
| type ValidFieldValue = string | EnumValue | number | boolean | null | undefined; | |
| /** | |
| * Special marker used to represent null values in the packed string | |
| * @internal | |
| */ | |
| const SPECIAL_NULL = "null"; | |
| /** | |
| * Special marker used to represent undefined values in the packed string | |
| * @internal | |
| */ | |
| const SPECIAL_UNDEFINED = "undef"; | |
| /** | |
| * Type guard to check if a value is valid for serialization | |
| * @param value - The value to check | |
| * @returns True if the value can be serialized, false otherwise | |
| * @internal | |
| */ | |
| function isValidFieldValue(value: unknown): value is ValidFieldValue { | |
| if (value === null || value === undefined) return true; | |
| if (typeof value === "number") return true; | |
| if (typeof value === "string") return true; | |
| if (typeof value === "boolean") return true; | |
| if (typeof value !== "object") return false; | |
| return ( | |
| "valueOf" in value && | |
| typeof (value as { valueOf: unknown }).valueOf === "function" && | |
| typeof (value as EnumValue).valueOf() === "string" | |
| ); | |
| } | |
| /** | |
| * Escapes special characters in a string value | |
| * @param value - The string to escape | |
| * @returns The escaped string | |
| * @internal | |
| */ | |
| const escapeValue = (value: string): string => { | |
| // Escape the escape character first to avoid double escaping | |
| let escaped = value.replace(/\\/g, "\\\\"); | |
| // Escape colons after backslashes are escaped | |
| escaped = escaped.replace(/:/g, "\\:"); | |
| // Escape special values when they appear as exact matches | |
| if ( | |
| escaped === SPECIAL_NULL || | |
| escaped === SPECIAL_UNDEFINED || | |
| escaped === SPECIAL_TRUE || | |
| escaped === SPECIAL_FALSE | |
| ) { | |
| escaped = `\\${escaped}`; | |
| } | |
| return escaped; | |
| }; | |
| /** | |
| * Unescapes special characters in a string value | |
| * @param value - The string to unescape | |
| * @returns The unescaped string | |
| * @internal | |
| */ | |
| const unescapeValue = (value: string): string => { | |
| // Handle special values first | |
| if ( | |
| value === SPECIAL_NULL || | |
| value === SPECIAL_UNDEFINED || | |
| value === SPECIAL_TRUE || | |
| value === SPECIAL_FALSE | |
| ) { | |
| return value; | |
| } | |
| // Handle escaped special values | |
| if (value.startsWith("\\") && value.length > 1) { | |
| const unescaped = value.slice(1); | |
| if ( | |
| unescaped === SPECIAL_NULL || | |
| unescaped === SPECIAL_UNDEFINED || | |
| unescaped === SPECIAL_TRUE || | |
| unescaped === SPECIAL_FALSE | |
| ) { | |
| return unescaped; | |
| } | |
| } | |
| let result = value; | |
| let match: RegExpExecArray | null; | |
| const pattern = /\\(\\|:)/g; | |
| const replacements: [number, string][] = []; | |
| // Find all escape sequences and store their positions | |
| match = pattern.exec(result); | |
| while (match !== null) { | |
| replacements.push([match.index, match[1]]); | |
| match = pattern.exec(result); | |
| } | |
| // Replace escape sequences from right to left to maintain positions | |
| for (let i = replacements.length - 1; i >= 0; i--) { | |
| const [pos, char] = replacements[i]; | |
| result = result.slice(0, pos) + char + result.slice(pos + 2); | |
| } | |
| return result; | |
| }; | |
| /** | |
| * Serializes a field value to string | |
| * @param value - The value to serialize | |
| * @returns The serialized string | |
| * @internal | |
| */ | |
| function serializeValue(value: ValidFieldValue): string { | |
| if (value === null) return SPECIAL_NULL; | |
| if (value === undefined) return SPECIAL_UNDEFINED; | |
| if (typeof value === "boolean") return value ? SPECIAL_TRUE : SPECIAL_FALSE; | |
| if (typeof value === "number") { | |
| if (Number.isNaN(value)) { | |
| throw new Error("Number value must not be NaN"); | |
| } | |
| if (!Number.isFinite(value)) { | |
| throw new Error("Number value must be finite"); | |
| } | |
| return value.toString(); | |
| } | |
| if (typeof value === "string") return escapeValue(value); | |
| return escapeValue(value.valueOf()); | |
| } | |
| /** | |
| * Packs an object into a colon-delimited string | |
| * | |
| * @param data - The object to pack | |
| * @param schema - Optional Zod schema for validation | |
| * @returns The packed string | |
| * @throws {Error} If any value is not a valid field value | |
| * @throws {ZodError} If the data doesn't match the schema | |
| * | |
| * @example | |
| * ```typescript | |
| * const schema = z.object({ | |
| * userId: z.number(), | |
| * status: z.enum(["active", "inactive"]), | |
| * email: z.string().nullable(), | |
| * isVerified: z.boolean(), | |
| * }); | |
| * | |
| * const data = { | |
| * userId: 123, | |
| * status: "active", | |
| * email: null, | |
| * isVerified: true, | |
| * }; | |
| * | |
| * const packed = packData(data, schema); | |
| * // Result: "123:active:null:true" | |
| * ``` | |
| */ | |
| export function packData<T extends z.ZodObject<z.ZodRawShape>>( | |
| data: z.infer<T>, | |
| schema?: T, | |
| ): string { | |
| if (schema !== undefined) { | |
| schema.parse(data); | |
| } | |
| const fields = Object.keys(data) as (keyof z.infer<T>)[]; | |
| return fields | |
| .map((field) => { | |
| const value = data[field]; | |
| if (!isValidFieldValue(value)) { | |
| throw new Error( | |
| `Value for field ${String(field)} must be a string, number, boolean, null, undefined, or string enum`, | |
| ); | |
| } | |
| return serializeValue(value); | |
| }) | |
| .join(":"); | |
| } | |
| /** | |
| * Unpacks a colon-delimited string into an object according to the provided schema | |
| * | |
| * @param packed - The packed string to unpack | |
| * @param schema - Zod schema defining the expected structure | |
| * @returns The unpacked and validated object | |
| * @throws {Error} If the number of fields doesn't match the schema | |
| * @throws {ZodError} If the unpacked data doesn't match the schema | |
| * | |
| * @example | |
| * ```typescript | |
| * const schema = z.object({ | |
| * userId: z.number(), | |
| * status: z.enum(["active", "inactive"]), | |
| * email: z.string().nullable(), | |
| * isVerified: z.boolean(), | |
| * }); | |
| * | |
| * const packed = "123:active:null:true"; | |
| * const unpacked = unpackData(packed, schema); | |
| * // Result: { userId: 123, status: "active", email: null, isVerified: true } | |
| * ``` | |
| */ | |
| export function unpackData<T extends z.ZodObject<z.ZodRawShape>>( | |
| packed: string, | |
| schema: T, | |
| ): z.infer<T> { | |
| const keys = Object.keys(schema.shape) as (keyof z.infer<T>)[]; | |
| const regex = /(?<!\\):/; | |
| const values = packed.split(regex); | |
| if (values.length !== keys.length) { | |
| throw new Error( | |
| `Expected ${keys.length.toFixed()} fields but got ${values.length.toFixed()}`, | |
| ); | |
| } | |
| const data = Object.fromEntries( | |
| keys.map((field, index) => { | |
| const value = values[index] ?? ""; | |
| const fieldSchema = ( | |
| schema.shape as Record<keyof z.infer<T>, z.ZodTypeAny> | |
| )[field]; | |
| // Check if the field schema expects a number (handles nullable/optional) | |
| if (fieldSchema.isNullable() && value === SPECIAL_NULL) | |
| return [field, null]; | |
| if (fieldSchema.isOptional() && value === SPECIAL_UNDEFINED) | |
| return [field, undefined]; | |
| // Get the innermost schema type (unwraps nullable/optional) | |
| function unwrapSchema(schema: z.ZodTypeAny): z.ZodTypeAny { | |
| if (schema instanceof z.ZodNullable) { | |
| return unwrapSchema(schema.unwrap() as z.ZodTypeAny); | |
| } | |
| if (schema instanceof z.ZodOptional) | |
| return unwrapSchema(schema.unwrap() as z.ZodTypeAny); | |
| return schema; | |
| } | |
| const innerSchema = unwrapSchema(fieldSchema); | |
| const unescaped = unescapeValue(value); | |
| // Check if the field schema expects a number | |
| if (innerSchema instanceof z.ZodNumber) { | |
| const num = Number(unescaped); | |
| if (Number.isNaN(num)) { | |
| throw new Error( | |
| `Invalid number value for field ${String(field)}: ${unescaped}`, | |
| ); | |
| } | |
| if (!Number.isFinite(num)) { | |
| throw new Error( | |
| `Number value for field ${String(field)} is not finite: ${unescaped}`, | |
| ); | |
| } | |
| return [field, num]; | |
| } | |
| // Check if the field schema expects a boolean | |
| if (innerSchema instanceof z.ZodBoolean) { | |
| if (unescaped === SPECIAL_TRUE) return [field, true]; | |
| if (unescaped === SPECIAL_FALSE) return [field, false]; | |
| throw new Error( | |
| `Invalid boolean value for field ${String(field)}: ${unescaped}`, | |
| ); | |
| } | |
| return [field, unescaped]; | |
| }), | |
| ) as z.infer<T>; | |
| return schema.parse(data); | |
| } | |
| export function unpackFirstField(packed: string) { | |
| const regex = /(?<!\\):/; | |
| const values = packed.split(regex); | |
| return values[0]; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { describe, expect, test } from "vitest"; | |
| import { z } from "zod"; | |
| import { packData, unpackData } from "./callbackPack"; | |
| describe("Callback Pack", () => { | |
| describe("Basic Types", () => { | |
| const BasicSchema = z.object({ | |
| str: z.string(), | |
| num: z.number(), | |
| nullField: z.null(), | |
| undefinedField: z.undefined(), | |
| }); | |
| test("should handle basic types correctly", () => { | |
| const data = { | |
| str: "hello", | |
| num: 123, | |
| nullField: null, | |
| undefinedField: undefined, | |
| }; | |
| const packed = packData(data, BasicSchema); | |
| const unpacked = unpackData(packed, BasicSchema); | |
| expect(unpacked).toEqual(data); | |
| }); | |
| test("should handle various number formats", () => { | |
| const NumberSchema = z.object({ | |
| integer: z.number(), | |
| decimal: z.number(), | |
| negative: z.number(), | |
| scientific: z.number(), | |
| zero: z.number(), | |
| }); | |
| const data = { | |
| integer: 42, | |
| decimal: Math.PI, | |
| negative: -123.456, | |
| scientific: 1.23e-4, | |
| zero: 0, | |
| }; | |
| const packed = packData(data, NumberSchema); | |
| const unpacked = unpackData(packed, NumberSchema); | |
| expect(unpacked).toEqual(data); | |
| expect(typeof unpacked.integer).toBe("number"); | |
| expect(typeof unpacked.decimal).toBe("number"); | |
| expect(typeof unpacked.negative).toBe("number"); | |
| expect(typeof unpacked.scientific).toBe("number"); | |
| expect(typeof unpacked.zero).toBe("number"); | |
| }); | |
| test("should handle floating point numbers", () => { | |
| const data = { | |
| str: "test", | |
| num: 123.456, | |
| nullField: null, | |
| undefinedField: undefined, | |
| }; | |
| const packed = packData(data, BasicSchema); | |
| const unpacked = unpackData(packed, BasicSchema); | |
| expect(unpacked).toEqual(data); | |
| }); | |
| test("should handle negative numbers", () => { | |
| const data = { | |
| str: "test", | |
| num: -123.456, | |
| nullField: null, | |
| undefinedField: undefined, | |
| }; | |
| const packed = packData(data, BasicSchema); | |
| const unpacked = unpackData(packed, BasicSchema); | |
| expect(unpacked).toEqual(data); | |
| }); | |
| }); | |
| describe("String Escaping", () => { | |
| const EscapeSchema = z.object({ | |
| value: z.string(), | |
| }); | |
| test("should escape colons in strings", () => { | |
| const data = { value: "hello:world" }; | |
| const packed = packData(data, EscapeSchema); | |
| expect(packed).toContain("\\:"); | |
| const unpacked = unpackData(packed, EscapeSchema); | |
| expect(unpacked).toEqual(data); | |
| }); | |
| test("should escape backslashes", () => { | |
| const data = { value: "hello\\world" }; | |
| const packed = packData(data, EscapeSchema); | |
| expect(packed).toContain("\\\\"); | |
| const unpacked = unpackData(packed, EscapeSchema); | |
| expect(unpacked).toEqual(data); | |
| }); | |
| test("should escape the word 'null' when it appears as a string", () => { | |
| const data = { value: "null" }; | |
| const packed = packData(data, EscapeSchema); | |
| expect(packed).toBe("\\null"); | |
| const unpacked = unpackData(packed, EscapeSchema); | |
| expect(unpacked).toEqual(data); | |
| }); | |
| test("should escape the word 'true' when it appears as a string", () => { | |
| const data = { value: "true" }; | |
| const packed = packData(data, EscapeSchema); | |
| expect(packed).toBe("\\true"); | |
| const unpacked = unpackData(packed, EscapeSchema); | |
| expect(unpacked).toEqual(data); | |
| }); | |
| test("should escape the word 'false' when it appears as a string", () => { | |
| const data = { value: "false" }; | |
| const packed = packData(data, EscapeSchema); | |
| expect(packed).toBe("\\false"); | |
| const unpacked = unpackData(packed, EscapeSchema); | |
| expect(unpacked).toEqual(data); | |
| }); | |
| test("should escape the word 'undef' when it appears as a string", () => { | |
| const data = { value: "undef" }; | |
| const packed = packData(data, EscapeSchema); | |
| expect(packed).toBe("\\undef"); | |
| const unpacked = unpackData(packed, EscapeSchema); | |
| expect(unpacked).toEqual(data); | |
| }); | |
| test("should handle multiple escapes in the same string", () => { | |
| const data = { value: "hello:world\\with:many:special\\chars" }; | |
| const packed = packData(data, EscapeSchema); | |
| const unpacked = unpackData(packed, EscapeSchema); | |
| expect(unpacked).toEqual(data); | |
| }); | |
| }); | |
| describe("Enums", () => { | |
| enum TestEnum { | |
| One = "ONE", | |
| Two = "TWO", | |
| } | |
| const EnumSchema = z.object({ | |
| enumValue: z.nativeEnum(TestEnum), | |
| }); | |
| test("should handle enum values", () => { | |
| const data = { enumValue: TestEnum.One }; | |
| const packed = packData(data, EnumSchema); | |
| const unpacked = unpackData(packed, EnumSchema); | |
| expect(unpacked).toEqual(data); | |
| }); | |
| test("should handle all enum values", () => { | |
| for (const enumValue of Object.values(TestEnum)) { | |
| const data = { enumValue }; | |
| const packed = packData(data, EnumSchema); | |
| const unpacked = unpackData(packed, EnumSchema); | |
| expect(unpacked).toEqual(data); | |
| } | |
| }); | |
| }); | |
| describe("Boolean Support", () => { | |
| const BooleanSchema = z.object({ | |
| flag: z.boolean(), | |
| nullableFlag: z.boolean().nullable(), | |
| optionalFlag: z.boolean().optional(), | |
| str: z.string(), | |
| }); | |
| test("should handle basic boolean values", () => { | |
| const data = { | |
| flag: true, | |
| nullableFlag: false, | |
| optionalFlag: true, | |
| str: "test", | |
| }; | |
| const packed = packData(data, BooleanSchema); | |
| const unpacked = unpackData(packed, BooleanSchema); | |
| expect(unpacked).toEqual(data); | |
| expect(typeof unpacked.flag).toBe("boolean"); | |
| expect(typeof unpacked.nullableFlag).toBe("boolean"); | |
| expect(typeof unpacked.optionalFlag).toBe("boolean"); | |
| }); | |
| test("should handle nullable and optional boolean values", () => { | |
| const data = { | |
| flag: false, | |
| nullableFlag: null, | |
| optionalFlag: undefined, | |
| str: "test", | |
| }; | |
| const packed = packData(data, BooleanSchema); | |
| const unpacked = unpackData(packed, BooleanSchema); | |
| expect(unpacked).toEqual(data); | |
| }); | |
| test("should escape the words 'true' and 'false' when they appear as strings", () => { | |
| const StringSchema = z.object({ | |
| value: z.string(), | |
| }); | |
| const testCases = [ | |
| { value: "true" }, | |
| { value: "false" }, | |
| { value: "some:true:text" }, | |
| { value: "some:false:text" }, | |
| ]; | |
| for (const data of testCases) { | |
| const packed = packData(data, StringSchema); | |
| const unpacked = unpackData(packed, StringSchema); | |
| expect(unpacked).toEqual(data); | |
| } | |
| }); | |
| test("should throw on invalid boolean values", () => { | |
| const packed = "invalid:null:undef:test"; | |
| expect(() => unpackData(packed, BooleanSchema)).toThrow( | |
| /Invalid boolean/, | |
| ); | |
| }); | |
| }); | |
| describe("Schema Validation", () => { | |
| const ValidationSchema = z.object({ | |
| id: z.number().positive(), | |
| email: z.string().email(), | |
| status: z.enum(["active", "inactive"]), | |
| }); | |
| test("should validate data during packing", () => { | |
| const invalidData = { | |
| id: -1, | |
| email: "not-an-email", | |
| status: "active", | |
| }; | |
| // Use "as z.infer" to bypass TypeScript type checking | |
| expect(() => | |
| packData( | |
| invalidData as z.infer<typeof ValidationSchema>, | |
| ValidationSchema, | |
| ), | |
| ).toThrow(); | |
| }); | |
| test("should validate data during unpacking", () => { | |
| // Create valid data first | |
| const validData = { | |
| id: 1, | |
| email: "test@example.com", | |
| status: "active" as const, | |
| }; | |
| const packed = packData(validData, ValidationSchema); | |
| // Manually corrupt the packed string | |
| const corruptedPacked = packed.replace( | |
| "test@example.com", | |
| "not-an-email", | |
| ); | |
| expect(() => unpackData(corruptedPacked, ValidationSchema)).toThrow(); | |
| }); | |
| test("should allow valid data", () => { | |
| const validData = { | |
| id: 1, | |
| email: "test@example.com", | |
| status: "active" as const, | |
| }; | |
| const packed = packData(validData, ValidationSchema); | |
| const unpacked = unpackData(packed, ValidationSchema); | |
| expect(unpacked).toEqual(validData); | |
| }); | |
| }); | |
| describe("Type Coercion", () => { | |
| const CoercionSchema = z.object({ | |
| numberLike: z.string(), | |
| actualNumber: z.coerce.number(), | |
| }); | |
| test("should respect schema types without premature coercion", () => { | |
| const data = { | |
| numberLike: "123", // Should stay a string | |
| actualNumber: 123, // Should be a number | |
| }; | |
| const packed = packData(data, CoercionSchema); | |
| const unpacked = unpackData(packed, CoercionSchema); | |
| expect(typeof unpacked.numberLike).toBe("string"); | |
| expect(typeof unpacked.actualNumber).toBe("number"); | |
| expect(unpacked).toEqual(data); | |
| }); | |
| }); | |
| describe("Edge Cases and Advanced Schema Validation", () => { | |
| describe("String Edge Cases", () => { | |
| const emptySchema = z.object({ | |
| field1: z.string(), | |
| field2: z.string(), | |
| }); | |
| test("should handle empty strings", () => { | |
| const packed = ":"; | |
| const unpacked = unpackData(packed, emptySchema); | |
| expect(unpacked).toEqual({ | |
| field1: "", | |
| field2: "", | |
| }); | |
| }); | |
| test("should preserve whitespace", () => { | |
| const data = { | |
| field1: " ", | |
| field2: " ", | |
| }; | |
| const packed = packData(data, emptySchema); | |
| const unpacked = unpackData(packed, emptySchema); | |
| expect(unpacked).toEqual(data); | |
| }); | |
| test("should handle consecutive colons in input", () => { | |
| const data = { | |
| field1: ":", | |
| field2: "::", | |
| }; | |
| const packed = packData(data, emptySchema); | |
| const unpacked = unpackData(packed, emptySchema); | |
| expect(unpacked).toEqual(data); | |
| }); | |
| }); | |
| describe("Number Handling", () => { | |
| const NumberSchema = z.object({ | |
| integer: z.number(), | |
| stringNumber: z.string(), | |
| optionalNumber: z.number().optional(), | |
| nullableNumber: z.number().nullable(), | |
| refinedNumber: z.number().min(0).max(100), | |
| }); | |
| test("should keep string numbers as strings when schema expects string", () => { | |
| const data = { | |
| integer: 42, | |
| stringNumber: "123", | |
| optionalNumber: undefined, | |
| nullableNumber: null, | |
| refinedNumber: 50, | |
| }; | |
| const packed = packData(data, NumberSchema); | |
| const unpacked = unpackData(packed, NumberSchema); | |
| expect(typeof unpacked.stringNumber).toBe("string"); | |
| }); | |
| test("should validate refined numbers", () => { | |
| const invalidData = { | |
| integer: 42, | |
| stringNumber: "123", | |
| optionalNumber: undefined, | |
| nullableNumber: null, | |
| refinedNumber: 101, // Over max | |
| }; | |
| expect(() => packData(invalidData, NumberSchema)).toThrow(); | |
| }); | |
| }); | |
| describe("String Escaping", () => { | |
| const EscapeSchema = z.object({ | |
| value: z.string(), | |
| }); | |
| interface TestCase { | |
| input: string; | |
| expectedPacked: string; | |
| } | |
| const testCases: TestCase[] = [ | |
| { | |
| input: ":", | |
| expectedPacked: "\\:", // Simple colon should be escaped | |
| }, | |
| { | |
| input: "\\", | |
| expectedPacked: "\\\\", // Single backslash should be escaped | |
| }, | |
| { | |
| input: "\\:", | |
| expectedPacked: "\\\\\\:", // Backslash-colon should have both escaped | |
| }, | |
| { | |
| input: ":", | |
| expectedPacked: "\\:", // Colon should be escaped | |
| }, | |
| { | |
| input: "null", | |
| expectedPacked: "\\null", // Special value should be escaped when it's content | |
| }, | |
| { | |
| input: "undef", | |
| expectedPacked: "\\undef", // Special value should be escaped when it's content | |
| }, | |
| ]; | |
| for (const { input, expectedPacked } of testCases) { | |
| test(`should correctly handle "${input}" through pack/unpack cycle`, () => { | |
| // Test packing | |
| const packed = packData({ value: input }, EscapeSchema); | |
| console.log("packed", packed); | |
| expect(packed).toBe(expectedPacked); | |
| // Test unpacking | |
| const unpacked = unpackData(packed, EscapeSchema); | |
| expect(unpacked.value).toBe(input); | |
| }); | |
| } | |
| }); | |
| describe("Schema Inheritance", () => { | |
| const BaseSchema = z.object({ | |
| id: z.number(), | |
| name: z.string(), | |
| }); | |
| const ExtendedSchema = BaseSchema.extend({ | |
| age: z.number(), | |
| email: z.string().email(), | |
| }); | |
| test("should handle extended schemas", () => { | |
| const data = { | |
| id: 1, | |
| name: "Test", | |
| age: 25, | |
| email: "test@example.com", | |
| }; | |
| const packed = packData(data, ExtendedSchema); | |
| const unpacked = unpackData(packed, ExtendedSchema); | |
| expect(unpacked).toEqual(data); | |
| }); | |
| }); | |
| describe("Error Handling", () => { | |
| const ErrorSchema = z.object({ | |
| num: z.number(), | |
| str: z.string(), | |
| }); | |
| test("should throw descriptive error for invalid number fields", () => { | |
| const packed = "not-a-number:test"; | |
| expect(() => unpackData(packed, ErrorSchema)).toThrow(/Invalid number/); | |
| }); | |
| test("should preserve original error messages from Zod", () => { | |
| const ValidationSchema = z.object({ | |
| email: z.string().email(), | |
| age: z.number().min(18), | |
| }); | |
| const data = { | |
| email: "not-an-email", | |
| age: 15, | |
| }; | |
| expect(() => packData(data, ValidationSchema)).toThrow( | |
| /Invalid email|Number must be greater than/, | |
| ); | |
| }); | |
| }); | |
| describe("Boundary Conditions", () => { | |
| const BoundarySchema = z.object({ | |
| num: z.number(), | |
| str: z.string(), | |
| }); | |
| test("should handle numeric edge cases", () => { | |
| const testCases = [ | |
| { num: Number.MAX_SAFE_INTEGER, str: "max" }, | |
| { num: Number.MIN_SAFE_INTEGER, str: "min" }, | |
| ]; | |
| for (const testCase of testCases) { | |
| const packed = packData(testCase, BoundarySchema); | |
| console.log("packed", packed); | |
| const unpacked = unpackData(packed, BoundarySchema); | |
| expect(unpacked).toEqual(testCase); | |
| expect(Object.is(unpacked.num, testCase.num)).toBe(true); // Handles -0 vs 0 | |
| } | |
| }); | |
| test("should reject invalid numeric values", () => { | |
| const invalidValues = ["NaN", "Infinity", "-Infinity"]; | |
| for (const invalid of invalidValues) { | |
| const packed = `${invalid}:test`; | |
| expect(() => unpackData(packed, BoundarySchema)).toThrow(); | |
| } | |
| }); | |
| }); | |
| }); | |
| describe("Error Cases", () => { | |
| const ErrorSchema = z.object({ | |
| field1: z.string(), | |
| field2: z.number(), | |
| }); | |
| test("should throw on incorrect number of fields", () => { | |
| const packed = "value1:123:extra"; | |
| expect(() => unpackData(packed, ErrorSchema)).toThrow(); | |
| }); | |
| test("should throw on invalid field values", () => { | |
| const invalidData = { | |
| field1: "valid", | |
| field2: {} as number, // Type assertion to bypass TypeScript | |
| }; | |
| expect(() => packData(invalidData, ErrorSchema)).toThrow(); | |
| }); | |
| test("should throw on missing fields", () => { | |
| const packed = "value1"; | |
| expect(() => unpackData(packed, ErrorSchema)).toThrow(); | |
| }); | |
| }); | |
| describe("Optional Fields", () => { | |
| const OptionalSchema = z.object({ | |
| required: z.string(), | |
| optional: z.string().optional(), | |
| nullable: z.string().nullable(), | |
| nullableOptional: z.string().nullable().optional(), | |
| }); | |
| test("should handle optional fields", () => { | |
| const data = { | |
| required: "value", | |
| optional: undefined, | |
| nullable: null, | |
| nullableOptional: undefined, | |
| }; | |
| const packed = packData(data, OptionalSchema); | |
| const unpacked = unpackData(packed, OptionalSchema); | |
| expect(unpacked).toEqual(data); | |
| }); | |
| test("should handle all combinations of optional and nullable", () => { | |
| const testCases = [ | |
| { | |
| required: "value", | |
| optional: "defined", | |
| nullable: "defined", | |
| nullableOptional: "defined", | |
| }, | |
| { | |
| required: "value", | |
| optional: undefined, | |
| nullable: null, | |
| nullableOptional: undefined, | |
| }, | |
| { | |
| required: "value", | |
| optional: "defined", | |
| nullable: null, | |
| nullableOptional: null, | |
| }, | |
| ]; | |
| for (const testCase of testCases) { | |
| const packed = packData(testCase, OptionalSchema); | |
| const unpacked = unpackData(packed, OptionalSchema); | |
| expect(unpacked).toEqual(testCase); | |
| } | |
| }); | |
| }); | |
| describe("Integration Tests", () => { | |
| const basicSchema = z.object({ | |
| name: z.string(), | |
| id: z.string(), | |
| }); | |
| test("should round-trip complex data correctly", () => { | |
| const testCases = [ | |
| { | |
| name: "John:Doe:\\Test", | |
| id: "123:456\\789", | |
| }, | |
| { | |
| name: "", | |
| id: ":::", | |
| }, | |
| { | |
| name: " ", | |
| id: "\\:\\:", | |
| }, | |
| ]; | |
| for (const data of testCases) { | |
| const packed = packData(data, basicSchema); | |
| const unpacked = unpackData(packed, basicSchema); | |
| expect(unpacked).toEqual(data); | |
| } | |
| }); | |
| }); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment