SnowLuma v2.4.2 source snapshot
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
This commit is contained in:
commit
fa9c73363d
1367 files changed
+355735
No files matched your search
@@ -0,0 +1,332 @@
|
||||
import { bundle } from '../project';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { run, imports } from '../compile';
|
||||
|
||||
const schema = `interface Msg { value: pb<1,uint_32>; }
|
||||
function encode<T>(value:T) { return protobuf_encode<T>(value); }`;
|
||||
|
||||
describe('generic callbacks', () => {
|
||||
it.each([
|
||||
'const callbacks=[encode];',
|
||||
'const api={send:encode};',
|
||||
'[{value:7}].map(<T>(value:T)=>protobuf_encode<T>(value));',
|
||||
'[{value:7}].map((encode));',
|
||||
])('rejects callback values without enough schema context: %s', (expression) => {
|
||||
expect(() => run(schema + expression)).toThrow(/Cannot infer callback schema/);
|
||||
});
|
||||
it('supports callbacks in default parameters and codec calls in parameter initializers', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
function apply<T>(value:T, callback:(value:T)=>Uint8Array=protobuf_encode) {return callback(value);}
|
||||
function defaults<T>(value:T, bytes=protobuf_encode<T>(value)) {return bytes;}
|
||||
export const result=[[...apply<Msg>({value:7})],[...defaults<Msg>({value:8})]];
|
||||
export const lengths=[apply.length, defaults.length];
|
||||
`),
|
||||
).toEqual({
|
||||
result: [
|
||||
[8, 7],
|
||||
[8, 8],
|
||||
],
|
||||
lengths: [1, 1],
|
||||
});
|
||||
});
|
||||
|
||||
it('supports destructured polymorphic callback parameters', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
interface Api {encode:<T>(value:T)=>Uint8Array;}
|
||||
function use<T>({encode:fn}:Api,value:T) {return fn<T>(value);}
|
||||
export const result=[...use<Msg>({encode},{value:7})];
|
||||
`).result,
|
||||
).toEqual([8, 7]);
|
||||
});
|
||||
it('propagates object callback signatures through function parameters', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
interface Api {encode:<T>(value:T)=>Uint8Array;}
|
||||
function use<T>(api:Api,value:T) {return api.encode<T>(value);}
|
||||
export const result=[...use<Msg>({encode},{value:7})];
|
||||
`).result,
|
||||
).toEqual([8, 7]);
|
||||
});
|
||||
|
||||
it('adapts typed assignments and callbacks stored in arrays', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
type Encoder=(value:Msg)=>Uint8Array;
|
||||
let callback:Encoder;
|
||||
callback=encode;
|
||||
const callbacks:Encoder[]=[encode,callback];
|
||||
export const result=callbacks.map(fn=>[...fn({value:7})]);
|
||||
`).result,
|
||||
).toEqual([
|
||||
[8, 7],
|
||||
[8, 7],
|
||||
]);
|
||||
});
|
||||
|
||||
it('reports opaque polymorphic consumers at build time', () => {
|
||||
expect(() =>
|
||||
run(`${schema}
|
||||
declare function external(fn:<T>(value:T)=>Uint8Array):void;
|
||||
external(encode);
|
||||
`),
|
||||
).toThrow(/receiving implementation/);
|
||||
});
|
||||
it('supports class arrow fields as callbacks with lexical this', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
class Codec<T> { calls=0; encode=(value:T)=>{this.calls++;return protobuf_encode<T>(value);}; }
|
||||
const codec=new Codec<Msg>();
|
||||
export const result=[{value:7}].map(codec.encode).map(bytes=>[...bytes]);
|
||||
export const calls=codec.calls;
|
||||
`),
|
||||
).toEqual({ result: [[8, 7]], calls: 1 });
|
||||
});
|
||||
|
||||
it('preserves spread evaluation, method receivers and rest arguments', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
const events:string[]=[];
|
||||
const api={calls:0,send<T>(value:T,...labels:string[]){this.calls++;events.push(...labels);return protobuf_encode<T>(value);}};
|
||||
function args():[Msg,string,string] {events.push('args');return [{value:7},'a','b'];}
|
||||
export const result=[...api.send<Msg>(...args())];
|
||||
export const calls=api.calls;
|
||||
export {events};
|
||||
`),
|
||||
).toEqual({ result: [8, 7], calls: 1, events: ['args', 'a', 'b'] });
|
||||
});
|
||||
it('analyzes shared higher order functions across multiple build entries', async () => {
|
||||
expect(
|
||||
await bundle(
|
||||
{
|
||||
'apply.ts':
|
||||
'export function apply<T>(value:T, callback:<U>(value:U)=>Uint8Array) { return callback<T>(value); }',
|
||||
'plain.ts': `${imports} import {apply} from './apply'; interface Msg {value:pb<1,uint_32>;}
|
||||
function encode<T>(value:T) {return protobuf_encode<T>(value);}
|
||||
export const result=[...apply<Msg>({value:7},encode)];`,
|
||||
'boxed.ts': `${imports} import {apply} from './apply'; interface Msg {value:pb<1,uint_32>;} interface Box<T> {value:pb<1,T>;}
|
||||
function encode<T>(value:T) {return protobuf_encode<Box<T>>({value});}
|
||||
export const result=[...apply<Msg>({value:8},encode)];`,
|
||||
},
|
||||
['plain.ts', 'boxed.ts'],
|
||||
),
|
||||
).toEqual({ plain: { result: [8, 7] }, boxed: { result: [10, 2, 8, 8] } });
|
||||
});
|
||||
it('preserves this parameters, bound generic methods and class arguments', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
interface Pair<T,U> {first:pb<1,T>;second:pb<2,U>;}
|
||||
class Codec<T> {
|
||||
calls=0;
|
||||
encode<U>(this:Codec<T>, value:Pair<T,U>) { this.calls++; return protobuf_encode<Pair<T,U>>(value); }
|
||||
}
|
||||
const codec = new Codec<uint_32>();
|
||||
const callback = codec.encode<string>.bind(codec);
|
||||
export const result = [...callback({first:7,second:'x'})];
|
||||
export const calls = codec.calls;
|
||||
`),
|
||||
).toEqual({ result: [8, 7, 18, 1, 120], calls: 1 });
|
||||
});
|
||||
|
||||
it('supports returned polymorphic functions and typed object callbacks', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
type Encoder = <T>(value:T)=>Uint8Array;
|
||||
function factory(): Encoder { return encode; }
|
||||
const callback = factory();
|
||||
const api: {encode:Encoder} = {encode};
|
||||
export const result = [[...callback<Msg>({value:7})], [...api.encode<Msg>({value:8})], [...factory()<Msg>({value:9})]];
|
||||
`).result,
|
||||
).toEqual([
|
||||
[8, 7],
|
||||
[8, 8],
|
||||
[8, 9],
|
||||
]);
|
||||
});
|
||||
|
||||
it('supports nested generic callback aliases', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
type Callback<T> = (value:T)=>Uint8Array;
|
||||
type Alias<T> = Callback<T>;
|
||||
const callback: Alias<Msg> = encode;
|
||||
export const result = [...callback({value:7})];
|
||||
`).result,
|
||||
).toEqual([8, 7]);
|
||||
});
|
||||
|
||||
it('passes all required codecs when callback implementations differ', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
interface Box<T> {value:pb<1,T>;}
|
||||
function boxed<T>(value:T) { return protobuf_encode<Box<T>>({value}); }
|
||||
function apply<T>(value:T, callback:<U>(value:U)=>Uint8Array) { return callback<T>(value); }
|
||||
export const result = [[...apply<Msg>({value:7}, encode)], [...apply<Msg>({value:8}, boxed)]];
|
||||
`).result,
|
||||
).toEqual([
|
||||
[8, 7],
|
||||
[10, 2, 8, 8],
|
||||
]);
|
||||
});
|
||||
|
||||
it('supports a callback implementation accepting fewer parameters than its signature', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
function apply(callback:<T>(value:T, index:number)=>Uint8Array) { return callback<Msg>({value:7}, 42); }
|
||||
export const result = [...apply(encode)];
|
||||
`).result,
|
||||
).toEqual([8, 7]);
|
||||
});
|
||||
|
||||
it('preserves default parameter evaluation and callback length', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
let defaults=0;
|
||||
function callback<T>(value:T, flag=++defaults) { return flag ? protobuf_encode<T>(value) : new Uint8Array(); }
|
||||
const cb = callback<Msg>;
|
||||
export const result = [...cb({value:7})];
|
||||
export const length = cb.length;
|
||||
export {defaults};
|
||||
`),
|
||||
).toEqual({ result: [8, 7], length: 1, defaults: 1 });
|
||||
});
|
||||
|
||||
it('propagates nested schema substitutions through callback chains', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
interface Box<T> {value:pb<1,T>;}
|
||||
function nested<T>(value:Box<T>) { return protobuf_encode<Box<T>>(value); }
|
||||
function apply<T>(value:Box<T>, fn:<U>(value:Box<U>)=>Uint8Array) { return fn<T>(value); }
|
||||
export const result = [...apply<Msg>({value:{value:7}}, nested)];
|
||||
`).result,
|
||||
).toEqual([10, 2, 8, 7]);
|
||||
});
|
||||
|
||||
it('preserves async callback bodies', async () => {
|
||||
const result = run(`${schema}
|
||||
async function send<T>(value:T) { await Promise.resolve(); return protobuf_encode<T>(value); }
|
||||
const values: Msg[] = [{value:7},{value:8}];
|
||||
export const result = Promise.all(values.map(send));
|
||||
`);
|
||||
expect((await result.result).map((bytes: Uint8Array) => [...bytes])).toEqual([
|
||||
[8, 7],
|
||||
[8, 8],
|
||||
]);
|
||||
});
|
||||
it('materializes instantiation expressions in variables, returns and arrays', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
const callback = encode<Msg>;
|
||||
function factory() { return encode<Msg>; }
|
||||
const values = [encode<Msg>, factory(), callback];
|
||||
export const result = values.map(fn => [...fn({value:7})]);
|
||||
`).result,
|
||||
).toEqual([
|
||||
[8, 7],
|
||||
[8, 7],
|
||||
[8, 7],
|
||||
]);
|
||||
});
|
||||
|
||||
it('infers array callbacks and preserves side effects and rest arguments', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
const values: Msg[] = [{value:7}, {value:8}];
|
||||
const calls: number[] = [];
|
||||
function callback<T>(value:T, ...rest: unknown[]) { calls.push(rest[0] as number); return protobuf_encode<T>(value); }
|
||||
export const result = values.map(callback).map(bytes => [...bytes]);
|
||||
export { calls };
|
||||
`),
|
||||
).toEqual({
|
||||
result: [
|
||||
[8, 7],
|
||||
[8, 8],
|
||||
],
|
||||
calls: [0, 1],
|
||||
});
|
||||
});
|
||||
|
||||
it('adapts runtime function values directly', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
const values: Msg[] = [{value:7}];
|
||||
const encodeMsg = protobuf_encode<Msg>;
|
||||
const decodeMsg: (bytes: Uint8Array) => Msg = protobuf_decode;
|
||||
export const result = [decodeMsg(encodeMsg(values[0])), ...values.map(protobuf_encode).map(decodeMsg)];
|
||||
`).result,
|
||||
).toEqual([{ value: 7 }, { value: 7 }]);
|
||||
});
|
||||
|
||||
it('passes codecs through a generic higher order function', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
function apply<T>(value:T, callback:(value:T)=>Uint8Array) { return callback(value); }
|
||||
export const result = [...apply<Msg>({value:7}, encode)];
|
||||
`).result,
|
||||
).toEqual([8, 7]);
|
||||
});
|
||||
|
||||
it('supports independently generic callbacks and transitive forwarding', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
type Encoder = <T>(value:T)=>Uint8Array;
|
||||
function invoke<T>(value:T, callback:Encoder) { return callback<T>(value); }
|
||||
function forward<T>(value:T, callback:Encoder) { return invoke<T>(value, callback); }
|
||||
export const result = [...forward<Msg>({value:7}, encode)];
|
||||
`).result,
|
||||
).toEqual([8, 7]);
|
||||
});
|
||||
|
||||
it('keeps distinct schemas when one generic callback is reused', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
interface Text { value: pb<1,string>; }
|
||||
function both(callback:<T>(value:T)=>Uint8Array) {
|
||||
return [[...callback<Msg>({value:7})], [...callback<Text>({value:'x'})]];
|
||||
}
|
||||
export const result = both(encode);
|
||||
`).result,
|
||||
).toEqual([
|
||||
[8, 7],
|
||||
[10, 1, 120],
|
||||
]);
|
||||
});
|
||||
|
||||
it('captures codecs in returned callbacks and contextual aliases', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
type Encoder<T> = (value:T)=>Uint8Array;
|
||||
function factory<T>(): Encoder<T> { return encode; }
|
||||
const callback: Encoder<Msg> = encode;
|
||||
export const result = [[...callback({value:7})], [...factory<Msg>()({value:8})]];
|
||||
`).result,
|
||||
).toEqual([
|
||||
[8, 7],
|
||||
[8, 8],
|
||||
]);
|
||||
});
|
||||
|
||||
it('supports generic inline callbacks and preserves native thisArg', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
const values: Msg[] = [{value:7}];
|
||||
const state = { calls:0 };
|
||||
function callback<T>(value:T) { this.calls++; return protobuf_encode<T>(value); }
|
||||
export const a = values.map(callback, state).map(bytes => [...bytes]);
|
||||
export const b = values.map(<T>(value:T) => protobuf_encode<T>(value)).map(bytes => [...bytes]);
|
||||
export const calls = state.calls;
|
||||
`),
|
||||
).toEqual({ a: [[8, 7]], b: [[8, 7]], calls: 1 });
|
||||
});
|
||||
|
||||
it('handles imported higher order functions and callback aliases', async () => {
|
||||
expect(
|
||||
await bundle({
|
||||
'encoder.ts': `${imports} export function encode<T>(value:T) { return protobuf_encode<T>(value); }`,
|
||||
'apply.ts': `export function apply<T>(value:T, fn:<U>(value:U)=>Uint8Array) { return fn<T>(value); }`,
|
||||
'entry.ts': `import {encode as callback} from './encoder'; import {apply} from './apply'; interface Msg {value:pb<1,uint_32>;} export const result = [...apply<Msg>({value:7}, callback)];`,
|
||||
}),
|
||||
).toEqual({ result: [8, 7] });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,196 @@
|
||||
import { bundle } from '../project';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { run, imports } from '../compile';
|
||||
import { ProtobufCompiler } from '../../../src/compiler/compiler';
|
||||
|
||||
const box = 'interface Box<T> {value:pb<1,T>;}';
|
||||
const codec = `${box}
|
||||
class Codec<T> {
|
||||
encode(value:Box<T>) {return protobuf_encode<Box<T>>(value);}
|
||||
decode(bytes:Uint8Array) {return protobuf_decode<Box<T>>(bytes);}
|
||||
}`;
|
||||
|
||||
describe('generic class instances', () => {
|
||||
it('isolates uint32 and string instances across alternating calls', () => {
|
||||
const result = run(`${codec}
|
||||
const numbers=new Codec<uint_32>(); const strings=new Codec<string>();
|
||||
export const result=Array.from({length:32},(_,i)=>[numbers.decode(numbers.encode({value:i})),strings.decode(strings.encode({value:'v'+i}))]);
|
||||
export const sharedMethod=numbers.encode===strings.encode;
|
||||
export const keys=Object.keys(numbers);
|
||||
`);
|
||||
expect(result.result).toEqual(
|
||||
Array.from({ length: 32 }, (_, i) => [{ value: i }, { value: 'v' + i }]),
|
||||
);
|
||||
expect(result.sharedMethod).toBe(true);
|
||||
expect(result.keys).toEqual([]);
|
||||
});
|
||||
|
||||
it('emits one codec per concrete type for many instances', () => {
|
||||
const declarations = Array.from(
|
||||
{ length: 32 },
|
||||
(_, i) => `const n${i}=new Codec<uint_32>(); const s${i}=new Codec<string>();`,
|
||||
).join('\n');
|
||||
const output = new ProtobufCompiler().compile(imports + codec + declarations, 'reuse.ts')!;
|
||||
expect(output.match(/function protobuf_encode_Box__uint_32\(/g)).toHaveLength(1);
|
||||
expect(output.match(/function protobuf_decode_Box__uint_32\(/g)).toHaveLength(1);
|
||||
expect(output.match(/function protobuf_encode_Box__string\(/g)).toHaveLength(1);
|
||||
expect(output.match(/function protobuf_decode_Box__string\(/g)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('alternates method type arguments on one instance', () => {
|
||||
expect(
|
||||
run(`${box}
|
||||
class Codec {roundtrip<T>(value:Box<T>) {return protobuf_decode<Box<T>>(protobuf_encode<Box<T>>(value));}}
|
||||
const instance=new Codec();
|
||||
export const result=[instance.roundtrip<uint_32>({value:7}),instance.roundtrip<string>({value:'x'}),instance.roundtrip<uint_32>({value:8})];
|
||||
`).result,
|
||||
).toEqual([{ value: 7 }, { value: 'x' }, { value: 8 }]);
|
||||
});
|
||||
|
||||
it('forwards outer generics through factories returning class instances', () => {
|
||||
expect(
|
||||
run(`${codec}
|
||||
function create<T>() {return new Codec<T>();}
|
||||
const n=create<uint_32>(); const s=create<string>();
|
||||
export const result=[n.decode(n.encode({value:7})),s.decode(s.encode({value:'x'}))];
|
||||
`).result,
|
||||
).toEqual([{ value: 7 }, { value: 'x' }]);
|
||||
});
|
||||
|
||||
it('combines factory class arguments and independent method arguments', () => {
|
||||
expect(
|
||||
run(`interface Pair<T,U> {first:pb<1,T>;second:pb<2,U>;}
|
||||
class Codec<T> {roundtrip<U>(value:Pair<T,U>) {return protobuf_decode<Pair<T,U>>(protobuf_encode<Pair<T,U>>(value));}}
|
||||
function create<T>():Codec<T> {return new Codec<T>();}
|
||||
const n=create<uint_32>(); const s=create<string>(); const alias=n;
|
||||
export const result=[n.roundtrip<string>({first:7,second:'x'}),s.roundtrip<uint_32>({first:'y',second:8}),alias.roundtrip<uint_32>({first:9,second:10}),create<string>().roundtrip<string>({first:'a',second:'b'})];
|
||||
`).result,
|
||||
).toEqual([
|
||||
{ first: 7, second: 'x' },
|
||||
{ first: 'y', second: 8 },
|
||||
{ first: 9, second: 10 },
|
||||
{ first: 'a', second: 'b' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('infers constructor generics from annotated arguments', () => {
|
||||
expect(
|
||||
run(`${box}
|
||||
class Codec<T> {constructor(readonly value:Box<T>) {} encode(){return protobuf_encode<Box<T>>(this.value);}}
|
||||
const n:Box<uint_32>={value:7}; const s:Box<string>={value:'x'};
|
||||
const numbers=new Codec(n); const strings=new Codec(s);
|
||||
export const result=[[...numbers.encode()],[...strings.encode()]];
|
||||
`).result,
|
||||
).toEqual([
|
||||
[8, 7],
|
||||
[10, 1, 120],
|
||||
]);
|
||||
});
|
||||
|
||||
it('infers factories and inherited constructors with renamed parameters', () => {
|
||||
expect(
|
||||
run(`${box}
|
||||
class Codec<T> {constructor(readonly value:Box<T>) {} encode(){return protobuf_encode<Box<T>>(this.value);}}
|
||||
class Middle<V> extends Codec<V> {}
|
||||
class Derived<U> extends Middle<U> {}
|
||||
function create<W>(value:Box<W>) {return new Derived(value);}
|
||||
const n:Box<uint_32>={value:7}; const s:Box<string>={value:'x'};
|
||||
const numbers=create(n); const strings=create(s);
|
||||
export const result=[[...numbers.encode()],[...strings.encode()]];
|
||||
`).result,
|
||||
).toEqual([
|
||||
[8, 7],
|
||||
[10, 1, 120],
|
||||
]);
|
||||
});
|
||||
|
||||
it('specializes unannotated factories, aliases and default class arguments', () => {
|
||||
expect(
|
||||
run(`interface Pair<T,U> {first:pb<1,T>;second:pb<2,U>;}
|
||||
class Codec<T=string> {roundtrip<U>(value:Pair<T,U>) {return protobuf_decode<Pair<T,U>>(protobuf_encode<Pair<T,U>>(value));}}
|
||||
const create=<T>()=>new Codec<T>();
|
||||
type Alias<T> = Codec<T>;
|
||||
function use<T>(instance:Alias<T>, first:T) {return instance.roundtrip<string>({first,second:'ok'});}
|
||||
export const result=[create<uint_32>().roundtrip<string>({first:7,second:'x'}),new Codec().roundtrip<uint_32>({first:'y',second:8}),use<string>(create<string>(),'z')];
|
||||
`).result,
|
||||
).toEqual([
|
||||
{ first: 7, second: 'x' },
|
||||
{ first: 'y', second: 8 },
|
||||
{ first: 'z', second: 'ok' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('retains class bindings in generic callback adapters', () => {
|
||||
expect(
|
||||
run(`interface Pair<T,U> {first:pb<1,T>;second:pb<2,U>;}
|
||||
class Codec<T> {roundtrip<U>(value:Pair<T,U>) {return protobuf_decode<Pair<T,U>>(protobuf_encode<Pair<T,U>>(value));}}
|
||||
function create<T>():Codec<T> {return new Codec<T>();}
|
||||
const numbers=create<uint_32>(); const strings=create<string>();
|
||||
const n=numbers.roundtrip<string>.bind(numbers); const s=strings.roundtrip<uint_32>.bind(strings);
|
||||
export const result=[n({first:7,second:'x'}),s({first:'y',second:8})];
|
||||
`).result,
|
||||
).toEqual([
|
||||
{ first: 7, second: 'x' },
|
||||
{ first: 'y', second: 8 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('separates specializations through inherited constructors', () => {
|
||||
expect(
|
||||
run(`${codec}
|
||||
class Derived<T> extends Codec<T> {}
|
||||
const n=new Derived<uint_32>(); const s=new Derived<string>();
|
||||
export const result=[n.decode(n.encode({value:7})),s.decode(s.encode({value:'x'}))];
|
||||
`).result,
|
||||
).toEqual([{ value: 7 }, { value: 'x' }]);
|
||||
});
|
||||
|
||||
it('keeps cross-file factory instances specialized', async () => {
|
||||
expect(
|
||||
await bundle({
|
||||
'codec.ts': `${imports} ${codec} export function create<T>() {return new Codec<T>();}`,
|
||||
'entry.ts': `import {create} from './codec'; const n=create<uint_32>();const s=create<string>(); export const result=[n.decode(n.encode({value:7})),s.decode(s.encode({value:'x'}))];`,
|
||||
}),
|
||||
).toEqual({ result: [{ value: 7 }, { value: 'x' }] });
|
||||
});
|
||||
|
||||
it('resolves method defaults from the class type argument', () => {
|
||||
expect(
|
||||
run(`interface Pair<T,U> {first:pb<1,T>;second:pb<2,U>;}
|
||||
class Codec<T> {roundtrip<U=T>(value:Pair<T,U>) {return protobuf_decode<Pair<T,U>>(protobuf_encode<Pair<T,U>>(value));}}
|
||||
function create<T>():Codec<T> {return new Codec<T>();}
|
||||
export const result=[create<uint_32>().roundtrip({first:7,second:8}),create<string>().roundtrip({first:'x',second:'y'})];
|
||||
`).result,
|
||||
).toEqual([
|
||||
{ first: 7, second: 8 },
|
||||
{ first: 'x', second: 'y' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('specializes inherited static generic methods', () => {
|
||||
expect(
|
||||
run(`${box}
|
||||
class Base {static roundtrip<T>(value:Box<T>) {return protobuf_decode<Box<T>>(protobuf_encode<Box<T>>(value));}}
|
||||
class Codec extends Base {}
|
||||
export const result=[Codec.roundtrip<uint_32>({value:7}),Codec.roundtrip<string>({value:'x'})];
|
||||
`).result,
|
||||
).toEqual([{ value: 7 }, { value: 'x' }]);
|
||||
});
|
||||
|
||||
it('combines cross-file class and method generics', async () => {
|
||||
expect(
|
||||
await bundle({
|
||||
'codec.ts': `${imports} export interface Pair<T,U> {first:pb<1,T>;second:pb<2,U>;}
|
||||
class Codec<T> {roundtrip<U>(value:Pair<T,U>) {return protobuf_decode<Pair<T,U>>(protobuf_encode<Pair<T,U>>(value));}}
|
||||
export const create=<T>()=>new Codec<T>();`,
|
||||
'entry.ts': `import {create} from './codec'; const n=create<uint_32>(); const s=create<string>();
|
||||
export const result=[n.roundtrip<string>({first:7,second:'x'}),s.roundtrip<uint_32>({first:'y',second:8})];`,
|
||||
}),
|
||||
).toEqual({
|
||||
result: [
|
||||
{ first: 7, second: 'x' },
|
||||
{ first: 'y', second: 8 },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,424 @@
|
||||
import { bundle } from '../project';
|
||||
import { packageName } from '../../../src/package';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ProtobufCompiler } from '../../../src/compiler/compiler';
|
||||
|
||||
import { run, imports } from '../compile';
|
||||
|
||||
describe('semantic compiler', () => {
|
||||
it('invalidates preloaded schema types when the transform receives different source', () => {
|
||||
const compiler = new ProtobufCompiler();
|
||||
const source = `${imports} interface Msg {value:pb<1,uint_32>;} protobuf_encode<Msg>({value:7});`;
|
||||
compiler.context.source('preloaded.ts', source);
|
||||
compiler.prepare(['preloaded.ts']);
|
||||
expect(compiler.compile(source.replace('pb<1,', 'pb<2,'), 'preloaded.ts')).toContain(
|
||||
'buf[offset++] = 16;',
|
||||
);
|
||||
});
|
||||
it('preserves async generic function bodies and return values', async () => {
|
||||
const result = run(`interface Msg { value: pb<1,uint_32>; }
|
||||
async function encode<T>(value:T) { await Promise.resolve(); return protobuf_encode<T>(value); }
|
||||
export const result = encode<Msg>({value:7});
|
||||
`);
|
||||
expect([...(await result.result)]).toEqual([8, 7]);
|
||||
});
|
||||
it('supports namespace markers and namespace runtime functions', () => {
|
||||
expect(
|
||||
run(`
|
||||
import * as p from '${packageName}';
|
||||
interface Msg { value: p.pb<1, p.uint_32>; }
|
||||
export const result = [...p.protobuf_encode<Msg>({value: 7})];
|
||||
`).result,
|
||||
).toEqual([8, 7]);
|
||||
});
|
||||
|
||||
it('supports default generic classes and re-exported runtime bindings', async () => {
|
||||
expect(
|
||||
await bundle({
|
||||
'runtime.ts': `export { protobuf_encode as encode } from '${packageName}';`,
|
||||
'codec.ts': `import { encode } from './runtime'; export default class Codec<T> { encode(value:T) { return encode<T>(value); } }`,
|
||||
'entry.ts': `import Codec from './codec'; interface Msg { value: pb<1,uint_32>; } const codec = new Codec<Msg>(); export const result = [...codec.encode({value:7})];`,
|
||||
}),
|
||||
).toEqual({ result: [8, 7] });
|
||||
});
|
||||
|
||||
it('resolves generic parameters containing dollar signs without text substitution', () => {
|
||||
expect(
|
||||
run(`interface Box<$T> { value: pb<1,$T>; }
|
||||
function encode<$T>(value:Box<$T>) { return protobuf_encode<Box<$T>>(value); }
|
||||
export const result = [...encode<string>({value:'x'})];
|
||||
`).result,
|
||||
).toEqual([10, 1, 120]);
|
||||
});
|
||||
|
||||
it('preserves directive prologues', () => {
|
||||
const code = `'use client';\n${imports}\ninterface Msg { value: pb<1,uint_32>; } protobuf_encode<Msg>({value:7});`;
|
||||
expect(new ProtobufCompiler().compile(code, 'directive.ts')).toMatch(/^'use client';/);
|
||||
});
|
||||
|
||||
it('does not mix anonymous fields instantiated with different generic arguments', () => {
|
||||
expect(
|
||||
run(`
|
||||
interface Box<T> { nested: pb<1, { value: pb<1, T>; }>; }
|
||||
export const a = protobuf_decode<Box<uint_32>>(protobuf_encode<Box<uint_32>>({ nested: { value: 7 } }));
|
||||
export const b = protobuf_decode<Box<string>>(protobuf_encode<Box<string>>({ nested: { value: 'x' } }));
|
||||
`),
|
||||
).toEqual({ a: { nested: { value: 7 } }, b: { nested: { value: 'x' } } });
|
||||
});
|
||||
|
||||
it('evaluates conditional schema types structurally', () => {
|
||||
expect(
|
||||
run(`
|
||||
interface Base { id: pb<1, uint_32>; }
|
||||
interface Child extends Base { text: pb<2, string>; }
|
||||
type Chosen<T> = T extends Base ? Child : never;
|
||||
export const result = protobuf_decode<Chosen<Child>>(protobuf_encode<Chosen<Child>>({ id: 7, text: 'x' }));
|
||||
`).result,
|
||||
).toEqual({ id: 7, text: 'x' });
|
||||
});
|
||||
|
||||
it('supports destructured generic object methods and aliases', () => {
|
||||
expect(
|
||||
run(`
|
||||
interface Msg { value: pb<1, uint_32>; }
|
||||
const api = { encode<T>(value: T) { return protobuf_encode<T>(value); } };
|
||||
const { encode: send } = api;
|
||||
export const result = [...send<Msg>({ value: 7 })];
|
||||
`).result,
|
||||
).toEqual([8, 7]);
|
||||
});
|
||||
|
||||
it('rejects generic callbacks without a concrete adapter', () => {
|
||||
expect(() =>
|
||||
run(`
|
||||
interface Msg { value: pb<1, uint_32>; }
|
||||
function encode<T>(value: T) { return protobuf_encode<T>(value); }
|
||||
[{ value: 7 }].map(encode);
|
||||
`),
|
||||
).toThrow(/Cannot infer callback schema/);
|
||||
});
|
||||
|
||||
it('rejects detached instance codec methods', () => {
|
||||
expect(() =>
|
||||
run(`interface Msg { value: pb<1,uint_32>; }
|
||||
class Codec<T> { encode(value:T) { return protobuf_encode<T>(value); } }
|
||||
const codec = new Codec<Msg>(); const encode = codec.encode;
|
||||
`),
|
||||
).toThrow(/receiver/);
|
||||
});
|
||||
|
||||
it('rejects class field calls that run before codec initialization', () => {
|
||||
expect(() => run(`class Codec<T> { value = protobuf_encode<T>({} as T); }`)).toThrow(
|
||||
/field initializers/,
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps function length stable after generic codec injection', () => {
|
||||
expect(
|
||||
run(`
|
||||
interface Msg { value: pb<1, uint_32>; }
|
||||
function encode<T>(value: T) { return protobuf_encode<T>(value); }
|
||||
export const length = encode.length;
|
||||
export const result = [...encode<Msg>({ value: 7 })];
|
||||
`),
|
||||
).toEqual({ length: 1, result: [8, 7] });
|
||||
});
|
||||
|
||||
it('reuses exact transform results and clears changed-source caches', () => {
|
||||
const compiler = new ProtobufCompiler();
|
||||
const source = `${imports} interface Msg { value: pb<1, uint_32>; } protobuf_encode<Msg>({value: 7});`;
|
||||
const first = compiler.compile(source, 'cache.ts');
|
||||
expect(compiler.compile(source, 'cache.ts')).toBe(first);
|
||||
const second = compiler.compile(source.replace('pb<1,', 'pb<2,'), 'cache.ts');
|
||||
expect(second).toContain('buf[offset++] = 16;');
|
||||
expect(second).not.toEqual(first);
|
||||
});
|
||||
|
||||
it('resolves tsconfig path aliases', async () => {
|
||||
expect(
|
||||
await bundle({
|
||||
'tsconfig.json': JSON.stringify({
|
||||
compilerOptions: { baseUrl: '.', paths: { '@schema/*': ['schema/*'] } },
|
||||
}),
|
||||
'schema/msg.ts': 'export interface Msg { value: pb<1, uint_32>; }',
|
||||
'entry.ts': `${imports} import type { Msg } from '@schema/msg'; export const result = [...protobuf_encode<Msg>({value:7})];`,
|
||||
}),
|
||||
).toEqual({ result: [8, 7] });
|
||||
});
|
||||
|
||||
it('supports recursive generic aliases and nested generic inference', () => {
|
||||
expect(
|
||||
run(`
|
||||
type Node<T> = { value: pb<1, T>; next?: pb<2, Node<T>>; };
|
||||
function encode<T>(value: Node<T>) { return protobuf_encode<Node<T>>(value); }
|
||||
const value: Node<string> = { value: 'a', next: { value: 'b' } };
|
||||
export const result = protobuf_decode<Node<string>>(encode(value));
|
||||
`).result,
|
||||
).toEqual({ value: 'a', next: { value: 'b', next: null } });
|
||||
});
|
||||
|
||||
it('supports inferred generic forwarding and inherited generic methods', () => {
|
||||
expect(
|
||||
run(`
|
||||
interface Pair<T, U> { a: pb<1, T>; b: pb<2, U>; }
|
||||
class Base<T> { encode<U>(v: Pair<T, U>) { return protobuf_encode<Pair<T, U>>(v); } }
|
||||
class Derived<T> extends Base<T> {}
|
||||
const codec = new Derived<uint_32>();
|
||||
export const result = [...codec.encode<string>({ a: 7, b: 'x' })];
|
||||
`).result,
|
||||
).toEqual([8, 7, 18, 1, 120]);
|
||||
});
|
||||
|
||||
it('keeps generated helper names and user property strings separate', () => {
|
||||
expect(
|
||||
run(`
|
||||
const __td = 'user decoder';
|
||||
const protobuf_encode_Msg = 'user encoder';
|
||||
interface Msg { '__td': pb<1, string>; }
|
||||
export const result = protobuf_decode<Msg>(protobuf_encode<Msg>({ __td: 'ok' }));
|
||||
export const untouched = [__td, protobuf_encode_Msg];
|
||||
`),
|
||||
).toEqual({ result: { __td: 'ok' }, untouched: ['user decoder', 'user encoder'] });
|
||||
});
|
||||
|
||||
it('shares codecs across distinct instantiations without mixing schemas', () => {
|
||||
expect(
|
||||
run(`
|
||||
interface A { value: pb<1, uint_32>; }
|
||||
interface B { value: pb<2, string>; }
|
||||
const encode = <T>(value: T) => protobuf_encode<T>(value);
|
||||
export const result = [[...encode<A>({ value: 7 })], [...encode<B>({ value: 'x' })]];
|
||||
`).result,
|
||||
).toEqual([
|
||||
[8, 7],
|
||||
[18, 1, 120],
|
||||
]);
|
||||
});
|
||||
|
||||
it('supports generic field-number aliases and preserves mapped optionality', () => {
|
||||
expect(
|
||||
run(`
|
||||
type Field<N extends number, T> = pb_optional<N, T>;
|
||||
interface Msg { value?: Field<3, uint_32>; }
|
||||
type Copy<T> = { [K in keyof T]: T[K] };
|
||||
export const result = [...protobuf_encode<Copy<Msg>>({ value: 0 })];
|
||||
`).result,
|
||||
).toEqual([24, 0]);
|
||||
});
|
||||
|
||||
it('forwards codecs through implicit and explicit generic derived constructors', () => {
|
||||
expect(
|
||||
run(`
|
||||
interface Msg { value: pb<1, uint_32>; }
|
||||
class Base<T> {
|
||||
constructor(public label = 'base') {}
|
||||
encode(value: T) { return protobuf_encode<T>(value); }
|
||||
}
|
||||
class Middle<T> extends Base<T> {}
|
||||
class Derived<T> extends Middle<T> {
|
||||
constructor(label: string) { super(label); }
|
||||
}
|
||||
const codec = new Derived<Msg>('ok');
|
||||
export const result = [...codec.encode({ value: 7 })];
|
||||
export const label = codec.label;
|
||||
`),
|
||||
).toEqual({ result: [8, 7], label: 'ok' });
|
||||
});
|
||||
|
||||
it('forwards concrete codecs through a non-generic derived class', () => {
|
||||
expect(
|
||||
run(`
|
||||
interface Msg { value: pb<1, uint_32>; }
|
||||
class Base<T> { encode(value: T) { return protobuf_encode<T>(value); } }
|
||||
class Codec extends Base<Msg> {}
|
||||
export const result = [...new Codec().encode({ value: 7 })];
|
||||
`).result,
|
||||
).toEqual([8, 7]);
|
||||
});
|
||||
|
||||
it('preserves explicitly present optional default values and quoted field names', () => {
|
||||
const result = run(`
|
||||
interface Msg { 'a-b'?: pb_optional<1, uint_32>; flag?: pb_optional<2, bool>; text?: pb_optional<3, string>; big?: pb_optional<4, uint_64>; '__proto__'?: pb<5, string>; }
|
||||
const input = { 'a-b': 0, flag: false, text: '', big: 0n, ['__proto__']: 'safe' };
|
||||
export const result = protobuf_decode<Msg>(protobuf_encode<Msg>(input));
|
||||
`).result;
|
||||
expect(result).toEqual({ 'a-b': 0, flag: false, text: '', big: 0n, ['__proto__']: 'safe' });
|
||||
expect(Object.getPrototypeOf(result)).toBe(Object.prototype);
|
||||
});
|
||||
it('preserves imported generic wrappers and transitive aliases across .js imports and barrels', async () => {
|
||||
const result = await bundle({
|
||||
'types.ts': `export interface Box<T> { value: pb<1, T>; }`,
|
||||
'codec.ts': `${imports} import type { Box as B } from './types.js';
|
||||
let count = 0;
|
||||
export function wrap<T>(value: B<T>) { count++; return protobuf_encode<B<T>>(value); }
|
||||
export function calls() { return count; }`,
|
||||
'barrel.ts': `export { wrap as encode, calls } from './codec.js'; export type { Box as Shape } from './types.js';`,
|
||||
'entry.ts': `${imports} import { encode, calls } from './barrel.js'; import type { Shape } from './barrel.js';
|
||||
export const result = protobuf_decode<Shape<string>>(encode<string>({ value: 'ok' }));
|
||||
export const count = calls();`,
|
||||
});
|
||||
expect(result).toEqual({ result: { value: 'ok' }, count: 1 });
|
||||
});
|
||||
|
||||
it('keeps same-named schemas from different files distinct', async () => {
|
||||
const result = await bundle({
|
||||
'a.ts': `export interface Msg { value: pb<1, uint_32>; }`,
|
||||
'b.ts': `export interface Msg { value: pb<2, string>; }`,
|
||||
'entry.ts': `${imports} import type { Msg as A } from './a'; import type * as b from './b';
|
||||
export const a = [...protobuf_encode<A>({ value: 7 })];
|
||||
export const b = [...protobuf_encode<b.Msg>({ value: 'x' })];`,
|
||||
});
|
||||
expect(result).toEqual({ a: [8, 7], b: [18, 1, 120] });
|
||||
});
|
||||
|
||||
it('supports generic arrow functions and rest parameters without reordering user arguments', () => {
|
||||
expect(
|
||||
run(`
|
||||
interface Msg { value: pb<1, uint_32>; }
|
||||
const encode = <T>(value: T, ...suffix: number[]) => [...protobuf_encode<T>(value), ...suffix];
|
||||
export const result = encode<Msg>({ value: 7 }, 9, 10);
|
||||
`).result,
|
||||
).toEqual([8, 7, 9, 10]);
|
||||
});
|
||||
|
||||
it('does not rewrite shadowed protobuf identifiers or unrelated object methods', () => {
|
||||
expect(
|
||||
run(`
|
||||
interface Msg { value: pb<1, uint_32>; }
|
||||
function custom(protobuf_encode: <T>(v: T) => number) { return protobuf_encode<Msg>({ value: 7 }); }
|
||||
const unrelated = { encode<T>(value: T) { return value; } };
|
||||
export const result = custom(() => 99) + unrelated.encode<number>(1);
|
||||
`).result,
|
||||
).toBe(100);
|
||||
});
|
||||
|
||||
it('preserves generic wrapper branches, argument positions, closures and side effects', () => {
|
||||
const result = run(`
|
||||
interface Msg { value: pb<1, uint_32>; }
|
||||
let count = 0;
|
||||
function wrap<I, T>(ignored: I, data: T, enabled = true) {
|
||||
count++;
|
||||
if (!enabled) return new Uint8Array([99]);
|
||||
return protobuf_encode<T>(data);
|
||||
}
|
||||
export const a = [...wrap<string, Msg>('unused', { value: 7 })];
|
||||
export const b = [...wrap<string, Msg>('unused', { value: 8 }, false)];
|
||||
export const calls = count;
|
||||
`);
|
||||
expect(result).toEqual({ a: [8, 7], b: [99], calls: 2 });
|
||||
});
|
||||
|
||||
it('propagates concrete codecs through multiple generic functions and nested closures', () => {
|
||||
const result = run(`
|
||||
interface Box<T> { value: pb<1, T>; }
|
||||
function inner<T>(value: Box<T>) { return protobuf_encode<Box<T>>(value); }
|
||||
function outer<T>(value: Box<T>) {
|
||||
function forward() { return inner<T>(value); }
|
||||
return forward();
|
||||
}
|
||||
export const result = protobuf_decode<Box<string>>(outer<string>({ value: 'ok' }));
|
||||
`);
|
||||
expect(result.result).toEqual({ value: 'ok' });
|
||||
});
|
||||
|
||||
it('resolves aliases, generic defaults, inheritance, intersections and indexed access', () => {
|
||||
const result = run(`
|
||||
type Scalar = uint_32;
|
||||
interface Base<T = Scalar> { id: pb<1, T>; }
|
||||
interface Extra { title: pb<2, string>; }
|
||||
interface Child extends Base { title: Extra['title']; }
|
||||
type Alias<T> = T;
|
||||
type Shape = Alias<Pick<Child & Extra, 'id' | 'title'>>;
|
||||
export const result = protobuf_decode<Shape>(protobuf_encode<Shape>({ id: 7, title: 'x' }));
|
||||
`);
|
||||
expect(result.result).toEqual({ id: 7, title: 'x' });
|
||||
});
|
||||
|
||||
it('supports mapped and conditional generic types', () => {
|
||||
const result = run(`
|
||||
interface Msg { id: pb<1, uint_32>; text: pb<2, string>; }
|
||||
type Copy<T> = { [K in keyof T]: T[K] };
|
||||
type Choice<T> = T extends string ? Copy<Msg> : never;
|
||||
export const result = protobuf_decode<Choice<string>>(protobuf_encode<Choice<string>>({ id: 5, text: 'ok' }));
|
||||
`);
|
||||
expect(result.result).toEqual({ id: 5, text: 'ok' });
|
||||
});
|
||||
|
||||
it('passes class type parameters through constructor and instance methods', () => {
|
||||
const result = run(`
|
||||
interface Msg { value: pb<1, uint_32>; }
|
||||
class Codec<T> {
|
||||
constructor(public label = 'default') {}
|
||||
encode(data: T) { return protobuf_encode<T>(data); }
|
||||
decode(bytes: Uint8Array) { return protobuf_decode<T>(bytes); }
|
||||
}
|
||||
const a = new Codec<Msg>();
|
||||
const b = new Codec<Msg>('named');
|
||||
export const result = a.decode(b.encode({ value: 9 }));
|
||||
export const labels = [a.label, b.label];
|
||||
export const keys = Object.keys(a);
|
||||
`);
|
||||
expect(result).toEqual({
|
||||
result: { value: 9 },
|
||||
labels: ['default', 'named'],
|
||||
keys: ['label'],
|
||||
});
|
||||
});
|
||||
|
||||
it('supports generic class methods with class and method type arguments', () => {
|
||||
const result = run(`
|
||||
interface Pair<T, U> { first: pb<1, T>; second: pb<2, U>; }
|
||||
class Codec<T> {
|
||||
encode<U>(value: Pair<T, U>) { return protobuf_encode<Pair<T, U>>(value); }
|
||||
}
|
||||
const codec = new Codec<uint_32>();
|
||||
export const result = protobuf_decode<Pair<uint_32, string>>(codec.encode<string>({ first: 7, second: 'x' }));
|
||||
`);
|
||||
expect(result.result).toEqual({ first: 7, second: 'x' });
|
||||
});
|
||||
|
||||
it('supports recursive message schemas and nullable links', () => {
|
||||
const result = run(`
|
||||
interface Node { value: pb<1, uint_32>; next?: pb<2, Node | undefined>; }
|
||||
export const result = protobuf_decode<Node>(protobuf_encode<Node>({ value: 1, next: { value: 2 } }));
|
||||
`);
|
||||
expect(result.result).toEqual({ value: 1, next: { value: 2, next: null } });
|
||||
});
|
||||
|
||||
it('infers a generic argument from an annotated variable', () => {
|
||||
const result = run(`
|
||||
interface Msg { value: pb<1, uint_32>; }
|
||||
const data: Msg = { value: 7 };
|
||||
function wrap<T>(value: T) { return protobuf_encode<T>(value); }
|
||||
export const result = [...wrap(data)];
|
||||
`);
|
||||
expect(result.result).toEqual([8, 7]);
|
||||
});
|
||||
|
||||
it('does not inspect unused unsupported schema declarations', () => {
|
||||
expect(
|
||||
run(`
|
||||
interface Unused { value: pb<1, MissingType>; }
|
||||
interface Msg { value: pb<1, uint_32>; }
|
||||
export const result = [...protobuf_encode<Msg>({ value: 7 })];
|
||||
`).result,
|
||||
).toEqual([8, 7]);
|
||||
});
|
||||
|
||||
it.each(['0', '-1', '1.5', '19000', '536870912'])(
|
||||
'rejects invalid field number %s at compile time',
|
||||
(number) => {
|
||||
expect(() =>
|
||||
run(
|
||||
`interface Msg { value: pb<${number}, uint_32>; } protobuf_encode<Msg>({ value: 7 });`,
|
||||
),
|
||||
).toThrow(packageName);
|
||||
},
|
||||
);
|
||||
|
||||
it('rejects duplicate field numbers', () => {
|
||||
expect(() =>
|
||||
run('interface Msg { a: pb<1, uint_32>; b: pb<1, string>; } protobuf_encode<Msg>({});'),
|
||||
).toThrow(/Duplicate/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,201 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { run, imports } from '../compile';
|
||||
import { bundle } from '../project';
|
||||
import { packageName } from '../../../src/package';
|
||||
|
||||
const schema =
|
||||
'interface Box<T> {value:pb<1,T>;} interface Pair<T,U> {first:pb<1,T>;second:pb<2,U>;}';
|
||||
const codec = `${schema} class Codec<T> {
|
||||
roundtrip<U>(value:Pair<T,U>) {return protobuf_decode<Pair<T,U>>(protobuf_encode<Pair<T,U>>(value));}
|
||||
}`;
|
||||
|
||||
describe('generic composition', () => {
|
||||
it('propagates two specializations across a 64-function call chain', () => {
|
||||
const functions = Array.from(
|
||||
{ length: 64 },
|
||||
(_, i) =>
|
||||
`function f${i}<T>(value:Box<T>) {return ${i === 63 ? 'protobuf_encode<Box<T>>(value)' : `f${i + 1}<T>(value)`};}`,
|
||||
).join('\n');
|
||||
expect(
|
||||
run(
|
||||
`${schema} ${functions} export const result=[[...f0<uint_32>({value:7})],[...f0<string>({value:'x'})]];`,
|
||||
).result,
|
||||
).toEqual([
|
||||
[8, 7],
|
||||
[10, 1, 120],
|
||||
]);
|
||||
});
|
||||
|
||||
it('composes deferred mapped types, promises, classes and polymorphic callbacks', () => {
|
||||
expect(
|
||||
run(`
|
||||
interface Msg {id:pb<1,uint_32>;text:pb<2,string>;ignore:pb<3,bool>;}
|
||||
type Payload<T> = Pick<Awaited<T>,'id'|'text'>;
|
||||
interface Envelope<T> {value:pb<1,Payload<T>>;}
|
||||
function encode<T>(value:Envelope<T>) {return protobuf_encode<Envelope<T>>(value);}
|
||||
function apply<T>(value:Envelope<T>,fn:<U>(value:Envelope<U>)=>Uint8Array) {return fn<T>(value);}
|
||||
class Codec<T> {encode(value:Envelope<T>){return apply<T>(value,encode);}}
|
||||
const codec=new Codec<Promise<Msg>>();
|
||||
export const result=protobuf_decode<Envelope<Msg>>(codec.encode({value:{id:7,text:'x'}}));
|
||||
`).result,
|
||||
).toEqual({ value: { id: 7, text: 'x' } });
|
||||
});
|
||||
it('retains outer type arguments across nested local factories', () => {
|
||||
expect(
|
||||
run(`${codec}
|
||||
function outer<T>() {function inner() {return new Codec<T>();} return inner();}
|
||||
const n=outer<uint_32>(); const s=outer<string>();
|
||||
export const result=[n.roundtrip<string>({first:7,second:'x'}),s.roundtrip<uint_32>({first:'y',second:8})];
|
||||
`).result,
|
||||
).toEqual([
|
||||
{ first: 7, second: 'x' },
|
||||
{ first: 'y', second: 8 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('resolves instances through parameter properties and getters', () => {
|
||||
expect(
|
||||
run(`${codec}
|
||||
class Holder<T> {
|
||||
constructor(readonly codec:Codec<T>) {}
|
||||
get current():Codec<T> {return this.codec;}
|
||||
}
|
||||
const holder=new Holder<string>(new Codec<string>());
|
||||
export const result=holder.current.roundtrip<uint_32>({first:'x',second:7});
|
||||
`).result,
|
||||
).toEqual({ first: 'x', second: 7 });
|
||||
});
|
||||
|
||||
it('resolves static element access without changing this', () => {
|
||||
expect(
|
||||
run(`${codec}
|
||||
const c=new Codec<uint_32>();
|
||||
export const result=c['roundtrip']<string>({first:7,second:'x'});
|
||||
`).result,
|
||||
).toEqual({ first: 7, second: 'x' });
|
||||
});
|
||||
|
||||
it('supports overloaded generic methods and constructors', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
class Codec<T> {
|
||||
value:Box<T>;
|
||||
constructor(value:Box<T>);
|
||||
constructor(value:Box<T>, label:string);
|
||||
constructor(value:Box<T>, public label='default') {this.value=value;}
|
||||
encode() {return protobuf_encode<Box<T>>(this.value);}
|
||||
roundtrip<U>(value:Box<U>):Box<U>;
|
||||
roundtrip<U>(value:Box<U>):Box<U> {return protobuf_decode<Box<U>>(protobuf_encode<Box<U>>(value));}
|
||||
}
|
||||
const n:Box<uint_32>={value:7}; const c=new Codec(n);
|
||||
export const result=[[...c.encode()],c.roundtrip<string>({value:'x'}),c.label];
|
||||
`).result,
|
||||
).toEqual([[8, 7], { value: 'x' }, 'default']);
|
||||
});
|
||||
|
||||
it('forwards shadowed generic names through deeply nested callbacks', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
function roundtrip<T>(value:Box<T>) {return protobuf_decode<Box<T>>(protobuf_encode<Box<T>>(value));}
|
||||
function outer<T>(first:Box<T>, fn:<T>(value:Box<T>)=>Box<T>) {
|
||||
function middle<U>(next:Box<U>) {
|
||||
function inner<T>(value:Box<T>) {return fn<T>(value);}
|
||||
return [inner<T>(first),inner<U>(next)];
|
||||
}
|
||||
return middle<string>({value:'x'});
|
||||
}
|
||||
export const result=outer<uint_32>({value:7},roundtrip);
|
||||
`).result,
|
||||
).toEqual([{ value: 7 }, { value: 'x' }]);
|
||||
});
|
||||
|
||||
it('converges through mutually recursive generic functions', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
function a<T>(value:Box<T>, depth:number):Box<T> {return depth ? b<T>(value,depth-1) : protobuf_decode<Box<T>>(protobuf_encode<Box<T>>(value));}
|
||||
function b<U>(value:Box<U>, depth:number):Box<U> {return a<U>(value,depth);}
|
||||
export const result=[a<uint_32>({value:7},32),a<string>({value:'x'},33)];
|
||||
`).result,
|
||||
).toEqual([{ value: 7 }, { value: 'x' }]);
|
||||
});
|
||||
|
||||
it('keeps class factory specializations through cyclic re-export modules', async () => {
|
||||
expect(
|
||||
await bundle({
|
||||
'codec.ts': `${imports} ${codec} export const create=<T>()=>new Codec<T>();`,
|
||||
'first.ts': `export * from './second'; export {create} from './codec';`,
|
||||
'second.ts': `export * from './first';`,
|
||||
'entry.ts': `import {create} from './second'; export const result=create<string>().roundtrip<uint_32>({first:'x',second:7});`,
|
||||
}),
|
||||
).toEqual({ result: { first: 'x', second: 7 } });
|
||||
});
|
||||
|
||||
it('recognizes parenthesized runtime aliases and namespace element access', () => {
|
||||
expect(
|
||||
run(`${schema} import * as runtime from '${packageName}';
|
||||
const encode=protobuf_encode; const alias=encode;
|
||||
export const result=[
|
||||
[...(alias)<Box<uint_32>>({value:7})],
|
||||
runtime['protobuf_decode']<Box<string>>(runtime['protobuf_encode']<Box<string>>({value:'x'}))
|
||||
];
|
||||
`).result,
|
||||
).toEqual([[8, 7], { value: 'x' }]);
|
||||
});
|
||||
|
||||
it('adapts typed callbacks selected by static element access', () => {
|
||||
expect(
|
||||
run(`${codec}
|
||||
const c=new Codec<uint_32>();
|
||||
const callback:(value:Pair<uint_32,string>)=>Pair<uint_32,string> = c['roundtrip'].bind(c);
|
||||
export const result=callback({first:7,second:'x'});
|
||||
`).result,
|
||||
).toEqual({ first: 7, second: 'x' });
|
||||
});
|
||||
|
||||
it('captures outer generics in a local class returned from a factory', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
function create<T>() {
|
||||
class Local {roundtrip<U>(value:Pair<T,U>) {return protobuf_decode<Pair<T,U>>(protobuf_encode<Pair<T,U>>(value));}}
|
||||
return new Local();
|
||||
}
|
||||
export const result=[create<uint_32>().roundtrip<string>({first:7,second:'x'}),create<string>().roundtrip<uint_32>({first:'y',second:8})];
|
||||
`).result,
|
||||
).toEqual([
|
||||
{ first: 7, second: 'x' },
|
||||
{ first: 'y', second: 8 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('resolves namespaced base schemas and separates imported type and value names', async () => {
|
||||
expect(
|
||||
await bundle({
|
||||
'types.ts': `export interface Base<T> {value:pb<1,T>;} export const Base=123;`,
|
||||
'entry.ts': `${imports} import type * as types from './types';
|
||||
interface Msg<T> extends types.Base<T> {text:pb<2,string>;}
|
||||
export const result=protobuf_decode<Msg<uint_32>>(protobuf_encode<Msg<uint_32>>({value:7,text:'x'}));`,
|
||||
}),
|
||||
).toEqual({ result: { value: 7, text: 'x' } });
|
||||
});
|
||||
|
||||
it('keeps valid recursive references through inherited generic fields', () => {
|
||||
expect(
|
||||
run(`
|
||||
interface Base<T> {next:pb<1,Link<T>|null>;}
|
||||
interface Link<T> extends Base<T> {value:pb<2,T>;}
|
||||
const value:Link<string>={value:'a',next:{value:'b',next:null}};
|
||||
export const result=protobuf_decode<Link<string>>(protobuf_encode<Link<string>>(value));
|
||||
`).result,
|
||||
).toEqual({ value: 'a', next: { value: 'b', next: null } });
|
||||
});
|
||||
|
||||
it('resolves imported type and value names independently of declaration order', async () => {
|
||||
expect(
|
||||
await bundle({
|
||||
'types.ts': `export const Msg=42; export interface Msg {value:pb<1,uint_32>;}`,
|
||||
'barrel.ts': `export {Msg} from './types';`,
|
||||
'entry.ts': `${imports} import {Msg} from './barrel'; export const result=[Msg,[...protobuf_encode<Msg>({value:7})]];`,
|
||||
}),
|
||||
).toEqual({ result: [42, [8, 7]] });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ProtobufCompiler } from '../../../src/compiler/compiler';
|
||||
import { imports, run } from '../compile';
|
||||
import { withProject } from '../project';
|
||||
import { join } from 'node:path';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
|
||||
describe('compiler failure recovery', () => {
|
||||
it('rejects circular class inheritance without looping during member resolution', () => {
|
||||
expect(() =>
|
||||
run(
|
||||
`class A<T> extends B<T> {} class B<U> extends A<U> {} new A<uint_32>().missing();`,
|
||||
),
|
||||
).toThrow(/Circular class inheritance/);
|
||||
});
|
||||
|
||||
it('rejects circular schema inheritance without caching an empty message', () => {
|
||||
expect(() =>
|
||||
run(`interface A extends B {} interface B extends A {} protobuf_encode<A>({});`),
|
||||
).toThrow(/Circular schema inheritance/);
|
||||
});
|
||||
it('does not cache a partially validated schema after failure', () => {
|
||||
const compiler = new ProtobufCompiler();
|
||||
const source = `${imports} interface Msg {a:pb<1,uint_32>;b:pb<1,string>;} protobuf_encode<Msg>({a:7,b:'x'});`;
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
expect(() => compiler.compile(source, 'invalid.ts')).toThrow(
|
||||
/Duplicate protobuf field number/,
|
||||
);
|
||||
}
|
||||
|
||||
expect(compiler.compile(source.replace('b:pb<1,', 'b:pb<2,'), 'invalid.ts')).toContain(
|
||||
'protobuf_encode_Msg',
|
||||
);
|
||||
});
|
||||
|
||||
it('reports expanding recursive schema aliases before overflowing the stack', () => {
|
||||
expect(() =>
|
||||
run(
|
||||
`type Grow<T>={value:pb<1,T>;next?:pb<2,Grow<Grow<T>>>;}; protobuf_encode<Grow<uint_32>>({value:7});`,
|
||||
),
|
||||
).toThrow(/128 levels|does not converge/);
|
||||
});
|
||||
|
||||
it('reports circular aliases with a source location', () => {
|
||||
expect(() => run(`type A=B; type B=A; protobuf_encode<A>({});`)).toThrow(
|
||||
/test-entry.ts:\d+:\d+: Circular type alias/,
|
||||
);
|
||||
});
|
||||
|
||||
it('replaces specialized code when the same module repeatedly changes wire types', () => {
|
||||
const compiler = new ProtobufCompiler();
|
||||
|
||||
for (let i = 0; i < 32; i++) {
|
||||
const type = i % 2 ? 'string' : 'uint_32';
|
||||
const other = i % 2 ? 'uint_32' : 'string';
|
||||
const source = `${imports} interface Box<T> {value:pb<1,T>;} class Codec<T> {encode(value:Box<T>) {return protobuf_encode<Box<T>>(value);}} new Codec<${type}>();`;
|
||||
const output = compiler.compile(source, 'changing.ts');
|
||||
expect(output).toContain(`function protobuf_encode_Box__${type}(`);
|
||||
expect(output).not.toContain(`function protobuf_encode_Box__${other}(`);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects ambiguous factory specializations with a build diagnostic', () => {
|
||||
expect(() =>
|
||||
run(`interface Box<T> {value:pb<1,T>;}
|
||||
class Codec<T> {encode<U>(value:Box<T>) {return protobuf_encode<Box<T>>(value);}}
|
||||
function create(flag:boolean) {if(flag) return new Codec<uint_32>();return new Codec<string>();}
|
||||
create(true).encode<bool>({value:7});
|
||||
`),
|
||||
).toThrow(/Receiver branches must resolve to the same specialization/);
|
||||
});
|
||||
|
||||
it('reconstructs all entry requirements after a failed compilation', async () => {
|
||||
const shared =
|
||||
'export function apply<T>(value:T, fn:<U>(value:U)=>Uint8Array) {return fn<T>(value);}';
|
||||
await withProject(
|
||||
{
|
||||
'shared.ts': shared,
|
||||
'a.ts': `${imports} import {apply} from './shared'; interface Msg {value:pb<1,uint_32>;} function encode<T>(value:T) {return protobuf_encode<T>(value);} apply<Msg>({value:7},encode);`,
|
||||
'b.ts': `${imports} import {apply} from './shared'; interface Msg {value:pb<1,uint_32>;} interface Box<T> {value:pb<1,T>;} function encode<T>(value:T) {return protobuf_encode<Box<T>>({value});} apply<Msg>({value:8},encode);`,
|
||||
},
|
||||
(directory) => {
|
||||
const compiler = new ProtobufCompiler();
|
||||
compiler.prepare([join(directory, 'a.ts'), join(directory, 'b.ts')]);
|
||||
const baseline = compiler.compile(shared, join(directory, 'shared.ts'));
|
||||
expect(() =>
|
||||
compiler.compile(
|
||||
`${imports} protobuf_encode<Missing>({});`,
|
||||
join(directory, 'bad.ts'),
|
||||
),
|
||||
).toThrow(/Unresolved type/);
|
||||
expect(compiler.compile(shared, join(directory, 'shared.ts'))).toBe(baseline);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('recovers when entry preparation fails before a corrected source is saved', async () => {
|
||||
const source = `${imports} interface Msg {value:pb<1,uint_32>;} function encode<T>(value:T) {return protobuf_encode<T>(value);} encode({value:7});`;
|
||||
await withProject({ 'entry.ts': source }, (directory) => {
|
||||
const compiler = new ProtobufCompiler();
|
||||
const file = join(directory, 'entry.ts');
|
||||
|
||||
for (let i = 0; i < 2; i++) {
|
||||
expect(() => compiler.prepare([file])).toThrow(/Cannot infer T/);
|
||||
}
|
||||
|
||||
const corrected = source.replace('encode({', 'encode<Msg>({');
|
||||
writeFileSync(file, corrected);
|
||||
compiler.prepare([file]);
|
||||
expect(compiler.compile(corrected, file)).toContain('protobuf_encode_Msg');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { run } from '../compile';
|
||||
|
||||
const schema = 'interface Msg {value:pb<1,uint_32>;}';
|
||||
|
||||
describe('call evaluation', () => {
|
||||
it('preserves optional receiver short circuiting and single evaluation', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
const events:string[]=[];
|
||||
class Codec {encode<T>(value:T) {events.push('encode');return protobuf_encode<T>(value);}}
|
||||
function get(present:boolean):Codec|undefined {events.push('get');return present ? new Codec() : undefined;}
|
||||
function argument():Msg {events.push('argument');return {value:7};}
|
||||
export const absent=get(false)?.encode<Msg>(argument());
|
||||
export const result=[...get(true)?.encode<Msg>(argument())!];
|
||||
export {events};
|
||||
`),
|
||||
).toEqual({
|
||||
absent: undefined,
|
||||
result: [8, 7],
|
||||
events: ['get', 'get', 'argument', 'encode'],
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves getter receivers, multiple spreads and trailing commas', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
const events:string[]=[];
|
||||
const codec={encode<T>(value:T,...labels:string[]) {events.push(...labels);return protobuf_encode<T>(value);}};
|
||||
const holder={get current(){events.push('get');return codec;}};
|
||||
function values():[Msg] {events.push('values');return [{value:7}];}
|
||||
function labels():string[] {events.push('labels');return ['a','b'];}
|
||||
export const result=[...holder.current.encode<Msg>(...values(),...labels(),)];
|
||||
export {events};
|
||||
`),
|
||||
).toEqual({ result: [8, 7], events: ['get', 'values', 'labels', 'a', 'b'] });
|
||||
});
|
||||
|
||||
it('evaluates constructor defaults before the body and preserves rest arguments', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
const events:string[]=[];
|
||||
class Codec<T> {
|
||||
constructor(public value:T, public bytes=protobuf_encode<T>(value), ...labels:string[]) {events.push(...labels);}
|
||||
decode() {return protobuf_decode<T>(this.bytes);}
|
||||
}
|
||||
function args():[Msg,undefined,string,string] {events.push('args');return [{value:7},undefined,'a','b'];}
|
||||
const c=new Codec<Msg>(...args());
|
||||
export const result=c.decode(); export {events};
|
||||
export const length=Codec.length;
|
||||
`),
|
||||
).toEqual({ result: { value: 7 }, events: ['args', 'a', 'b'], length: 1 });
|
||||
});
|
||||
|
||||
it('preserves default codec calls in generic callback implementations', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
function encode<T>(value:T, bytes=protobuf_encode<T>(value)) {return bytes;}
|
||||
function apply<T>(value:T, fn:<U>(value:U, bytes?:Uint8Array)=>Uint8Array) {return fn<T>(value);}
|
||||
export const result=[...apply<Msg>({value:7},encode)];
|
||||
`).result,
|
||||
).toEqual([8, 7]);
|
||||
});
|
||||
|
||||
it('supports generator and async generator bodies', async () => {
|
||||
const result = run(`${schema}
|
||||
function* encode<T>(value:T) {yield protobuf_encode<T>(value);}
|
||||
async function* later<T>(value:T) {yield protobuf_encode<T>(value);}
|
||||
export const result=[...encode<Msg>({value:7})].map(bytes=>[...bytes]);
|
||||
export const pending=later<Msg>({value:8}).next();
|
||||
`);
|
||||
expect(result.result).toEqual([[8, 7]]);
|
||||
expect([...(await result.pending).value]).toEqual([8, 8]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { bundle } from '../project';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { imports } from '../compile';
|
||||
|
||||
describe('module graph', () => {
|
||||
it('resolves nested imported schemas and renamed exports', async () => {
|
||||
expect(
|
||||
await bundle({
|
||||
'inner.ts': 'export interface Inner {value:pb<1,uint_32>;} ',
|
||||
'outer.ts':
|
||||
"import type {Inner} from './inner'; export interface Outer {inner:pb<1,Inner>;}",
|
||||
'barrel.ts': "export {Outer as Message} from './outer';",
|
||||
'entry.ts': `${imports} import type {Message as Msg} from './barrel'; export const result = protobuf_decode<Msg>(protobuf_encode<Msg>({inner:{value:7}}));`,
|
||||
}),
|
||||
).toEqual({ result: { inner: { value: 7 } } });
|
||||
});
|
||||
|
||||
it('keeps wrapper control flow, object receivers and transitive generic aliases', async () => {
|
||||
expect(
|
||||
await bundle({
|
||||
'schema.ts':
|
||||
'export interface Box<T> {value:pb<1,T>;} export type Message<T> = Box<Box<T>>;',
|
||||
'codec.ts': `${imports} import type {Message} from './schema';
|
||||
export const api = { count:0, encode<T>(value:Message<T>, enabled=true) {
|
||||
this.count++; if (!enabled) return new Uint8Array();
|
||||
const inner = () => protobuf_encode<Message<T>>(value); return inner();
|
||||
}};`,
|
||||
'entry.ts': `${imports} import {api} from './codec'; import type {Message} from './schema';
|
||||
export const result = protobuf_decode<Message<string>>(api.encode<string>({value:{value:'x'}}));
|
||||
export const empty = api.encode<string>({value:{value:'x'}}, false).length;
|
||||
export const count = api.count;`,
|
||||
}),
|
||||
).toEqual({ result: { value: { value: 'x' } }, empty: 0, count: 2 });
|
||||
});
|
||||
|
||||
it('handles recursive types across modules', async () => {
|
||||
expect(
|
||||
await bundle({
|
||||
'a.ts': "import type {B} from './b'; export interface A {value:pb<1,uint_32>; next?:pb<2,B>;}",
|
||||
'b.ts': "import type {A} from './a'; export interface B {text:pb<1,string>; next?:pb<2,A>;}",
|
||||
'entry.ts': `${imports} import type {A} from './a'; export const result = protobuf_decode<A>(protobuf_encode<A>({value:7,next:{text:'x'}}));`,
|
||||
}),
|
||||
).toEqual({ result: { value: 7, next: { text: 'x', next: null } } });
|
||||
});
|
||||
|
||||
it('resolves nested generic functions with non-first schema parameters', async () => {
|
||||
expect(
|
||||
await bundle({
|
||||
'codec.ts': `${imports} export function send<Context,T>(context:Context, value:T) { if (!context) throw new Error('context'); return protobuf_encode<T>(value); }`,
|
||||
'entry.ts': `import {send} from './codec'; interface Msg {value:pb<1,uint_32>;} export const result = [...send<string,Msg>('ok',{value:7})];`,
|
||||
}),
|
||||
).toEqual({ result: [8, 7] });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { run } from '../compile';
|
||||
|
||||
const schema = 'interface Msg {id:pb<1,uint_32>; text:pb<2,string>;}';
|
||||
|
||||
describe('schema type transformations', () => {
|
||||
it('uses class parameter properties and ignores static metadata', () => {
|
||||
expect(
|
||||
run(`
|
||||
class Msg<T> {static label='message';constructor(public value:pb<1,T>) {}}
|
||||
export const result=protobuf_decode<Msg<uint_32>>(protobuf_encode<Msg<uint_32>>(new Msg<uint_32>(7)));
|
||||
`).result,
|
||||
).toEqual({ value: 7 });
|
||||
});
|
||||
it('captures function generics in local schemas without mixing instantiations', () => {
|
||||
expect(
|
||||
run(`
|
||||
function encode<T>(value:T) { interface Local {value:pb<1,T>;} return protobuf_encode<Local>({value}); }
|
||||
export const result=[[...encode<uint_32>(7)], [...encode<string>('x')]];
|
||||
`).result,
|
||||
).toEqual([
|
||||
[8, 7],
|
||||
[10, 1, 120],
|
||||
]);
|
||||
});
|
||||
it('extracts schema types through infer, functions and promises', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
type Result<T> = T extends (...args:any[])=>infer R ? R : never;
|
||||
type Unwrap<T> = T extends Promise<infer U> ? Unwrap<U> : T;
|
||||
type Schema = Unwrap<Result<(id:number)=>Promise<Promise<Msg>>>>;
|
||||
export const result = protobuf_decode<Schema>(protobuf_encode<Schema>({id:7,text:'x'}));
|
||||
`).result,
|
||||
).toEqual({ id: 7, text: 'x' });
|
||||
});
|
||||
|
||||
it('supports Awaited, ReturnType, Parameters and type queries', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
function load(value:Msg): Promise<Msg> {return Promise.resolve(value);}
|
||||
type Result = Awaited<ReturnType<typeof load>>;
|
||||
type Input = Parameters<typeof load>[0];
|
||||
export const result = protobuf_decode<Result>(protobuf_encode<Input>({id:7,text:'x'}));
|
||||
`).result,
|
||||
).toEqual({ id: 7, text: 'x' });
|
||||
});
|
||||
|
||||
it('propagates deferred conditional transformations through generic functions', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
type Unwrap<T> = T extends Promise<infer U> ? U : T;
|
||||
function encode<T>(value:Unwrap<T>) {return protobuf_encode<Unwrap<T>>(value);}
|
||||
export const result = protobuf_decode<Msg>(encode<Promise<Msg>>({id:7,text:'x'}));
|
||||
`).result,
|
||||
).toEqual({ id: 7, text: 'x' });
|
||||
});
|
||||
|
||||
it('remaps literal field names and omits never keys', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
type Rename<T> = {[K in keyof T as K extends 'text' ? never : \`field_\${Capitalize<K>}\`]:T[K]};
|
||||
export const result = protobuf_decode<Rename<Msg>>(protobuf_encode<Rename<Msg>>({field_Id:7}));
|
||||
`).result,
|
||||
).toEqual({ field_Id: 7 });
|
||||
});
|
||||
|
||||
it('selects explicit schemas with Extract and Exclude', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
type Schema = Extract<Msg|string,object>;
|
||||
type Chosen = Exclude<Msg|string,string>;
|
||||
export const result = protobuf_decode<Chosen>(protobuf_encode<Schema>({id:7,text:'x'}));
|
||||
`).result,
|
||||
).toEqual({ id: 7, text: 'x' });
|
||||
});
|
||||
|
||||
it('supports constructor signatures and indexed tuple transformations', () => {
|
||||
expect(
|
||||
run(`${schema}
|
||||
type Constructor = new(value:Msg)=>Msg;
|
||||
type Instance = InstanceType<Constructor>;
|
||||
type Input = ConstructorParameters<Constructor>[0];
|
||||
export const result = protobuf_decode<Instance>(protobuf_encode<Input>({id:7,text:'x'}));
|
||||
`).result,
|
||||
).toEqual({ id: 7, text: 'x' });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user