UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

766 lines (608 loc) 21.1 kB
import { $inject, Alepha, CodecManager, TypeBoxError, z } from "alepha"; import { describe, expect, it } from "vitest"; import { JsonSchemaCodec } from "../providers/JsonSchemaCodec.ts"; import { SchemaCodec } from "../providers/SchemaCodec.ts"; import type { TSchema } from "../providers/TypeProvider.ts"; describe("CodecManager", () => { describe("initialization", () => { it("should initialize with JSON codec as default", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; expect(codecManager).toBeInstanceOf(CodecManager); expect(codecManager.default).toBe("json"); }); it("should get JSON codec by default", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const jsonCodec = codecManager.getCodec("json"); expect(jsonCodec).toBeInstanceOf(JsonSchemaCodec); }); }); describe("codec registration", () => { it("should register custom codec", () => { class CustomCodec extends SchemaCodec { public encodeToString(schema: TSchema, value: any): string { return `custom:${JSON.stringify(value)}`; } public encodeToBinary(schema: TSchema, value: any): Uint8Array { const str = this.encodeToString(schema, value); return new TextEncoder().encode(str); } public decode<T>(schema: TSchema, value: unknown): T { return value as T; } } const alepha = Alepha.create(); const codecManager = alepha.codec; const customCodec = new CustomCodec(); codecManager.register({ name: "custom", codec: customCodec, }); const retrieved = codecManager.getCodec("custom"); expect(retrieved).toBe(customCodec); }); it("should throw error when codec not found", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; expect(() => codecManager.getCodec("nonexistent")).toThrow( 'Codec "nonexistent" not found. Available codecs: json', ); }); it("should list available codecs in error message", () => { class MockCodec extends SchemaCodec { public encodeToString(): string { return ""; } public encodeToBinary(): Uint8Array { return new Uint8Array(); } public decode<T>(schema: TSchema, value: unknown): T { return value as T; } } const alepha = Alepha.create(); const codecManager = alepha.codec; codecManager.register({ name: "mock", codec: new MockCodec(), }); expect(() => codecManager.getCodec("missing")).toThrow( 'Codec "missing" not found. Available codecs: json, keyless, mock', ); }); }); describe("encoding - object format", () => { it("should encode to object by default", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ name: z.text(), age: z.integer(), }); const result = codecManager.encode(schema, { name: "John", age: 30, }); expect(result).toEqual({ name: "John", age: 30 }); }); it("should encode nested objects", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ user: z.object({ name: z.text(), address: z.object({ city: z.text(), zip: z.text(), }), }), }); const result = codecManager.encode(schema, { user: { name: "John", address: { city: "NYC", zip: "10001", }, }, }); expect(result).toEqual({ user: { name: "John", address: { city: "NYC", zip: "10001", }, }, }); }); it("should encode arrays", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ items: z.array(z.text()), }); const result = codecManager.encode(schema, { items: ["a", "b", "c"], }); expect(result).toEqual({ items: ["a", "b", "c"] }); }); it("should transform values according to schema", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ value: z.text(), flag: z.boolean(), }); const result = codecManager.encode(schema, { value: "test", flag: true }); expect(result).toEqual({ value: "test", flag: true }); }); it("should keep bigint string as-is", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ bigNum: z.bigint(), }); const result = codecManager.encode(schema, { bigNum: "123456789" }); expect(result.bigNum).toBe("123456789"); }); it("should handle optional fields", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ required: z.text(), optional: z.text().optional(), }); const result = codecManager.encode(schema, { required: "value" }); expect(result).toEqual({ required: "value" }); }); }); describe("encoding - string format", () => { it("should encode to JSON string with 'as: string' option", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ name: z.text(), age: z.integer(), }); const result = codecManager.encode( schema, { name: "John", age: 30 }, { as: "string" }, ); expect(typeof result).toBe("string"); expect(result).toBe('{"name":"John","age":30}'); }); it("should encode complex objects to JSON string", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ items: z.array( z.object({ id: z.integer(), name: z.text(), }), ), }); const result = codecManager.encode( schema, { items: [ { id: 1, name: "Item 1" }, { id: 2, name: "Item 2" }, ], }, { as: "string" }, ); expect(result).toBe( '{"items":[{"id":1,"name":"Item 1"},{"id":2,"name":"Item 2"}]}', ); }); }); describe("encoding - binary format", () => { it("should encode to Uint8Array with 'as: binary' option", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ name: z.text(), }); const result = codecManager.encode( schema, { name: "John" }, { as: "binary" }, ); expect(result).toBeInstanceOf(Uint8Array); const decoded = new TextDecoder().decode(result); expect(decoded).toBe('{"name":"John"}'); }); it("should encode and decode binary roundtrip", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ name: z.text(), count: z.integer(), }); const original = { name: "Test", count: 42 }; const binary = codecManager.encode(schema, original, { as: "binary" }); const decoded = codecManager.decode(schema, binary); expect(decoded).toEqual(original); }); }); describe("custom encoder option", () => { it("should use custom encoder when specified", () => { class CustomCodec extends SchemaCodec { public encodeToString(_schema: TSchema, value: any): string { return `CUSTOM:${JSON.stringify(value)}`; } public encodeToBinary(): Uint8Array { return new Uint8Array(); } public decode<T>(_schema: TSchema, value: unknown): T { return value as T; } } const alepha = Alepha.create(); const codecManager = alepha.codec; codecManager.register({ name: "custom", codec: new CustomCodec() }); const schema = z.object({ value: z.text() }); const result = codecManager.encode( schema, { value: "test" }, { encoder: "custom", as: "string" }, ); expect(result).toBe('CUSTOM:{"value":"test"}'); }); it("should throw error when custom encoder not found", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ value: z.text() }); expect(() => codecManager.encode(schema, { value: "test" }, { encoder: "missing" }), ).toThrow('Codec "missing" not found'); }); }); describe("decoding", () => { it("should decode object", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ name: z.text(), age: z.integer(), }); const result = codecManager.decode(schema, { name: "John", age: 30, }); expect(result).toEqual({ name: "John", age: 30 }); }); it("should decode JSON string", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ name: z.text(), age: z.integer(), }); const result = codecManager.decode(schema, '{"name":"John","age":30}'); expect(result).toEqual({ name: "John", age: 30 }); }); it("should decode Uint8Array", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ name: z.text(), }); const binary = new TextEncoder().encode('{"name":"John"}'); const result = codecManager.decode(schema, binary); expect(result).toEqual({ name: "John" }); }); it("should decode numbers correctly", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ count: z.integer(), price: z.number(), }); const result = codecManager.decode(schema, { count: 42, price: 19.99, }); expect(result.count).toBe(42); expect(result.price).toBe(19.99); }); it("should decode string to string (bigint format)", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ bigNum: z.bigint(), }); const result = codecManager.decode(schema, { bigNum: "123456789" }); expect(result.bigNum).toBe("123456789"); }); it("should apply default values during validation", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ name: z.text(), role: z.text({ default: "user" }), }); const decoded = codecManager.decode(schema, { name: "John" }); const result = codecManager.validate(schema, decoded); expect(result).toEqual({ name: "John", role: "user" }); }); it("should trim strings when trim option is set during validation", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ name: z.text({ trim: true }), }); const decoded = codecManager.decode(schema, { name: " John " }); const result = codecManager.validate(schema, decoded); expect(result.name).toBe("John"); }); it("should reject null for a non-nullable optional field (no null->undefined coercion)", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ required: z.text(), optional: z.text().optional(), }); // Strict zod: an optional field accepts `undefined`, not `null`. expect(() => codecManager.decode(schema, { required: "value", optional: null, }), ).toThrow(); const decoded = codecManager.decode(schema, { required: "value", optional: undefined, }); const result = codecManager.validate(schema, decoded); expect(result).toEqual({ required: "value" }); }); it("should validate and throw error on invalid data", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ age: z.integer(), }); expect(() => codecManager.decode(schema, { age: "not a number" }), ).toThrow(TypeBoxError); }); it("should decode arrays", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ items: z.array( z.object({ id: z.integer(), name: z.text(), }), ), }); const result = codecManager.decode(schema, { items: [ { id: 1, name: "Item 1" }, { id: 2, name: "Item 2" }, ], }); expect(result.items).toHaveLength(2); expect(result.items[0]).toEqual({ id: 1, name: "Item 1" }); }); it("should use custom decoder when specified", () => { class CustomCodec extends SchemaCodec { public encodeToString(): string { return ""; } public encodeToBinary(): Uint8Array { return new Uint8Array(); } public decode<T>(_schema: TSchema, value: unknown): T { // Custom decoding logic: prefix all strings with "DECODED:" if (typeof value === "object" && value !== null) { const result: any = {}; const obj = value as Record<string, any>; for (const key in obj) { if (typeof obj[key] === "string") { result[key] = `DECODED:${obj[key]}`; } else { result[key] = obj[key]; } } return result as T; } return value as T; } } const alepha = Alepha.create(); const codecManager = alepha.codec; codecManager.register({ name: "custom", codec: new CustomCodec() }); const schema = z.object({ value: z.text() }); const result = codecManager.decode( schema, { value: "test" }, { encoder: "custom" }, ); expect(result.value).toBe("DECODED:test"); }); }); describe("roundtrip encoding/decoding", () => { it("should handle roundtrip for complex objects", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ user: z.object({ id: z.integer(), name: z.text(), email: z.text(), active: z.boolean(), }), tags: z.array(z.text()), count: z.bigint(), }); const original = { user: { id: 1, name: "John Doe", email: "john@example.com", active: true, }, tags: ["typescript", "testing"], count: "999999999999", }; // Encode to string const encoded = codecManager.encode(schema, original, { as: "string" }); // Decode back const decoded = codecManager.decode(schema, encoded); expect(decoded.user.id).toBe(original.user.id); expect(decoded.user.name).toBe(original.user.name); expect(decoded.user.email).toBe(original.user.email); expect(decoded.user.active).toBe(original.user.active); expect(decoded.tags).toEqual(original.tags); expect(decoded.count).toBe(original.count); }); it("should handle roundtrip with binary format", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ data: z.text(), value: z.integer(), }); const original = { data: "test", value: 42 }; const binary = codecManager.encode(schema, original, { as: "binary" }); const decoded = codecManager.decode(schema, binary); expect(decoded).toEqual(original); }); }); describe("integration with Alepha container", () => { it("should inject CodecManager via $inject", () => { class TestService { codec = $inject(CodecManager); } const alepha = Alepha.create(); const service = alepha.inject(TestService); expect(service.codec).toBeInstanceOf(CodecManager); expect(service.codec).toBe(alepha.codec); }); it("should use same CodecManager instance across services", () => { class Service1 { codec = $inject(CodecManager); } class Service2 { codec = $inject(CodecManager); } const alepha = Alepha.create(); const s1 = alepha.inject(Service1); const s2 = alepha.inject(Service2); expect(s1.codec).toBe(s2.codec); }); it("should register custom codec in service", () => { class CustomCodec extends SchemaCodec { public encodeToString(): string { return "custom"; } public encodeToBinary(): Uint8Array { return new Uint8Array(); } public decode<T>(_schema: TSchema, value: unknown): T { return value as T; } } class ConfigService { codec = $inject(CodecManager); constructor() { this.codec.register({ name: "custom", codec: new CustomCodec() }); } } const alepha = Alepha.create(); alepha.inject(ConfigService); const codecManager = alepha.codec; const customCodec = codecManager.getCodec("custom"); expect(customCodec).toBeInstanceOf(CustomCodec); }); }); describe("JsonSchemaCodec specifics", () => { it("should handle null values correctly during validation", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ nullable: z.text().nullable(), optional: z.text().optional(), }); const decoded = codecManager.decode(schema, { nullable: null, optional: undefined, }); const result = codecManager.validate(schema, decoded); expect(result.nullable).toBeNull(); expect(result.optional).toBeUndefined(); // Strict zod: `null` on the non-nullable optional field is rejected // (no null->undefined coercion in the validator). expect(() => codecManager.decode(schema, { nullable: null, optional: null }), ).toThrow(); }); it("should handle array preprocessing during validation", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ items: z.array(z.text({ trim: true })), }); const decoded = codecManager.decode(schema, { items: [" a ", " b ", " c "], }); const result = codecManager.validate(schema, decoded); expect(result.items).toEqual(["a", "b", "c"]); }); it("should keep an explicit undefined optional key present during validation", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ a: z.text(), b: z.text().optional(), }); const decoded = codecManager.decode(schema, { a: "value", b: undefined, }); const result = codecManager.validate(schema, decoded); // Strict zod: no deleteUndefined — an explicitly-undefined optional key // is preserved (toEqual ignores undefined props, so it still matches). expect(result).toEqual({ a: "value" }); expect(result.b).toBeUndefined(); expect("b" in result).toBe(true); }); }); describe("error handling", () => { it("should throw TypeBoxError on encoding validation failure", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ age: z.integer().min(0).max(150), }); expect(() => codecManager.encode(schema, { age: 200 })).toThrow( TypeBoxError, ); }); it("should throw TypeBoxError when validating invalid decoded data", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; // A real format-validating schema (`z.email()`); `z.text({ format })` is // only a metadata tag now and does not enforce the format. const schema = z.object({ email: z.email(), }); expect(() => codecManager.decode(schema, { email: "not-an-email" }), ).toThrow(TypeBoxError); }); it("should throw error on invalid JSON string", () => { const alepha = Alepha.create(); const codecManager = alepha.codec; const schema = z.object({ value: z.text() }); expect(() => codecManager.decode(schema, "{ invalid json }")).toThrow(); }); }); });