Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,21 +66,15 @@ export function validateCodecTypeParams(descriptor: AnyCodecDescriptor, ref: Cod

/**
* Resolves a `Codec` instance: validates `ref.typeParams` via
* {@link validateCodecTypeParams} then calls `descriptor.factory(validated)(ctx)`.
*
* The descriptor's `factory` is typed against its own `P`; the registry erases
* `P` to `any`, so the factory is narrowed to `(params: unknown) => (ctx) => Codec`
* at the call boundary. The `paramsSchema` validates the input above before we
* forward it, so the narrowing is safe by construction.
* {@link validateCodecTypeParams} then calls `descriptor.factory(validated)(ctx)`
* as a method on `descriptor`, preserving `this` for factories that build
* their returned codec from the descriptor instance (e.g. `new XCodec(this)`).
*/
export function materializeCodec(
descriptor: AnyCodecDescriptor,
ref: CodecRef,
ctx: CodecInstanceContext,
): Codec {
const validated = validateCodecTypeParams(descriptor, ref);
return blindCast<
(params: unknown) => (ctx: CodecInstanceContext) => Codec,
'registry erases P to any; paramsSchema validates input before forwarding'
>(descriptor.factory)(validated)(ctx);
return descriptor.factory(validated)(ctx);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import type { JsonValue } from '@internal/contract/types';
import type { StandardSchemaV1 } from '@standard-schema/spec';
import { test } from 'vitest';
import {
type AnyCodecDescriptor,
type CodecCallContext,
type CodecDescriptor,
CodecDescriptorImpl,
CodecImpl,
type CodecInstanceContext,
type CodecRef,
type CodecTrait,
materializeCodec,
voidParamsSchema,
} from '../src/exports/codec';

class Int4FixtureCodec extends CodecImpl<'demo/int4@1', readonly ['equality'], number, number> {
async encode(value: number, _ctx: CodecCallContext): Promise<number> {
return value;
}
async decode(wire: number, _ctx: CodecCallContext): Promise<number> {
return wire;
}
encodeJson(value: number): JsonValue {
return value;
}
decodeJson(json: JsonValue): number {
return json as number;
}
}

class Int4FixtureDescriptor extends CodecDescriptorImpl<void> {
override readonly codecId = 'demo/int4@1' as const;
override readonly traits: readonly CodecTrait[] = ['equality'];
override readonly targetTypes: readonly string[] = ['int4'];
override readonly paramsSchema: StandardSchemaV1<void> = voidParamsSchema;
override factory(): (ctx: CodecInstanceContext) => Int4FixtureCodec {
return () => new Int4FixtureCodec(this);
}
}

const int4FixtureDescriptor = new Int4FixtureDescriptor();

type VectorParams = { readonly length: number };
const vectorFixtureParamsSchema: StandardSchemaV1<VectorParams> = {
'~standard': {
version: 1,
vendor: 'demo',
validate: (input) => ({ value: input as VectorParams }),
},
};

class VectorFixtureCodec<N extends number> extends CodecImpl<
'demo/vector@1',
readonly ['equality'],
string,
number[]
> {
constructor(
descriptor: CodecDescriptor<VectorParams>,
public readonly dimension: N,
) {
super(descriptor);
}
async encode(value: number[], _ctx: CodecCallContext): Promise<string> {
return `[${value.join(',')}]`;
}
async decode(wire: string, _ctx: CodecCallContext): Promise<number[]> {
return wire.slice(1, -1).split(',').map(Number);
}
encodeJson(value: number[]): JsonValue {
return value;
}
decodeJson(json: JsonValue): number[] {
return json as number[];
}
}

class VectorFixtureDescriptor extends CodecDescriptorImpl<VectorParams> {
override readonly codecId = 'demo/vector@1' as const;
override readonly traits: readonly CodecTrait[] = ['equality'];
override readonly targetTypes: readonly string[] = ['vector'];
override readonly paramsSchema = vectorFixtureParamsSchema;
override factory<N extends number>(params: {
readonly length: N;
}): (ctx: CodecInstanceContext) => VectorFixtureCodec<N> {
return () => new VectorFixtureCodec<N>(this, params.length);
}
}

const vectorFixtureDescriptor = new VectorFixtureDescriptor();

const stubCtx = {} as CodecInstanceContext;

function descriptorFor(ref: CodecRef): AnyCodecDescriptor {
if (ref.codecId === int4FixtureDescriptor.codecId) return int4FixtureDescriptor;
if (ref.codecId === vectorFixtureDescriptor.codecId) return vectorFixtureDescriptor;
throw new Error(`no fixture descriptor for ${ref.codecId}`);
}

test('materializeCodec resolves a non-parameterized codec whose id reads the descriptor codecId', ({
expect,
}) => {
const ref: CodecRef = { codecId: 'demo/int4@1' };
const codec = materializeCodec(descriptorFor(ref), ref, stubCtx);
expect(codec.id).toBe('demo/int4@1');
});

test('materializeCodec resolves a parameterized codec whose id reads the descriptor codecId', ({
expect,
}) => {
const ref: CodecRef = { codecId: 'demo/vector@1', typeParams: { length: 1536 } };
const codec = materializeCodec(descriptorFor(ref), ref, stubCtx);
expect(codec.id).toBe('demo/vector@1');
});

test('materializeCodec produces a codec whose encode/decode still run through the descriptor-bound factory', async ({
expect,
}) => {
const ref: CodecRef = { codecId: 'demo/vector@1', typeParams: { length: 3 } };
const codec = materializeCodec(descriptorFor(ref), ref, stubCtx);
const wire = await codec.encode([1, 2, 3], {});
expect(wire).toBe('[1,2,3]');
expect(await codec.decode(wire, {})).toEqual([1, 2, 3]);
});
Loading