Skip to content

Instantly share code, notes, and snippets.

@eikowagenknecht
Last active December 17, 2024 13:47
Show Gist options
  • Select an option

  • Save eikowagenknecht/44d653fadf6fb7b59fb5cab218c4d66a to your computer and use it in GitHub Desktop.

Select an option

Save eikowagenknecht/44d653fadf6fb7b59fb5cab218c4d66a to your computer and use it in GitHub Desktop.
Helpers to pack data for Telegram callbacks
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];
}
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