Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
4 changes: 2 additions & 2 deletions src/type/TypeBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export class TypeBuilder {
options,
);

public readonly tuple = <F extends Type[]>(...types: F) => this.Tuple(...types);
public readonly tuple = <F extends (Type | classes.ObjectFieldType<any, any>)[]>(...types: F) => this.Tuple(...types);

/**
* Creates an object type with the specified properties. This is a shorthand for
Expand Down Expand Up @@ -204,7 +204,7 @@ export class TypeBuilder {
return arr;
}

public Tuple<F extends Type[]>(...types: F) {
public Tuple<F extends (Type | classes.ObjectFieldType<any, any>)[]>(...types: F) {
const tup = new classes.TupleType<F>(types);
tup.system = this.system;
return tup;
Expand Down
5 changes: 1 addition & 4 deletions src/type/__tests__/SchemaOf.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,5 @@ test('string patch', () => {
[0, 'World'],
[-1, '!'],
];
const v2: T = [
// @ts-expect-error
[2, 'Test'],
];
const v2: T = [[2, 'Test']];
});
70 changes: 70 additions & 0 deletions src/type/__tests__/tuple-naming.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {t} from '../index';

describe('Tuple naming functionality', () => {
test('can create a tuple with regular types', () => {
const tuple = t.Tuple(t.num, t.str);
const schema = tuple.getSchema();

expect(schema).toStrictEqual({
kind: 'tup',
types: [{kind: 'num'}, {kind: 'str'}],
});
});

test('can create a tuple with named fields', () => {
const tuple = t.Tuple(t.prop('x', t.num), t.prop('y', t.str));
const schema = tuple.getSchema();

expect(schema).toStrictEqual({
kind: 'tup',
types: [
{kind: 'field', key: 'x', value: {kind: 'num'}},
{kind: 'field', key: 'y', value: {kind: 'str'}},
],
});
});

test('can create a tuple with mixed named and unnamed fields', () => {
const tuple = t.Tuple(t.prop('x', t.num), t.str);
const schema = tuple.getSchema();

expect(schema).toStrictEqual({
kind: 'tup',
types: [{kind: 'field', key: 'x', value: {kind: 'num'}}, {kind: 'str'}],
});
});

test('can use shorthand tuple method with named fields', () => {
const tuple = t.tuple(t.prop('x', t.num), t.prop('y', t.str));
const schema = tuple.getSchema();

expect(schema).toStrictEqual({
kind: 'tup',
types: [
{kind: 'field', key: 'x', value: {kind: 'num'}},
{kind: 'field', key: 'y', value: {kind: 'str'}},
],
});
});

test('validation works with named tuples', () => {
const tuple = t.Tuple(t.prop('x', t.num), t.prop('y', t.str));

// Valid data
expect(() => tuple.validate([42, 'hello'])).not.toThrow();

// Invalid data - wrong types
expect(() => tuple.validate(['hello', 42])).toThrow();

// Invalid data - wrong length
expect(() => tuple.validate([42])).toThrow();
expect(() => tuple.validate([42, 'hello', 'extra'])).toThrow();
});

test('JSON encoding works with named tuples', () => {
const tuple = t.Tuple(t.prop('x', t.num), t.prop('y', t.str));

const result = tuple.toJson([42, 'hello']);
expect(result).toBe('[42,"hello"]');
});
});
81 changes: 67 additions & 14 deletions src/type/classes/TupleType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,28 @@ import type {MessagePackEncoderCodegenContext} from '../../codegen/binary/Messag
import type {CapacityEstimatorCodegenContext} from '../../codegen/capacity/CapacityEstimatorCodegenContext';
import {MaxEncodingOverhead} from '@jsonjoy.com/util/lib/json-size';
import {AbstractType} from './AbstractType';
import {ObjectFieldType} from './ObjectType';
import type * as jsonSchema from '../../json-schema';
import type {SchemaOf, Type} from '../types';
import type {TypeSystem} from '../../system/TypeSystem';
import type {json_string} from '@jsonjoy.com/util/lib/json-brand';
import type * as ts from '../../typescript/types';
import type {TypeExportContext} from '../../system/TypeExportContext';

export class TupleType<T extends Type[]> extends AbstractType<schema.TupleSchema<{[K in keyof T]: SchemaOf<T[K]>}>> {
// Helper type to extract the underlying type from either Type or ObjectFieldType
type TupleElement = Type | ObjectFieldType<any, any>;

// Helper type to extract the schema from a tuple element
type SchemaOfTupleElement<T> = T extends ObjectFieldType<any, infer V>
? SchemaOf<V>
: T extends Type
? SchemaOf<T>
: never;

// Helper type for the schema mapping
type TupleSchemaMapping<T extends TupleElement[]> = {[K in keyof T]: SchemaOfTupleElement<T[K]>};

export class TupleType<T extends TupleElement[]> extends AbstractType<schema.TupleSchema<any>> {
protected schema: schema.TupleSchema<any>;

constructor(
Expand All @@ -29,14 +43,24 @@ export class TupleType<T extends Type[]> extends AbstractType<schema.TupleSchema
this.schema = {...schema.s.Tuple(), ...options};
}

public getSchema(): schema.TupleSchema<{[K in keyof T]: SchemaOf<T[K]>}> {
public getSchema(): schema.TupleSchema<any> {
return {
...this.schema,
types: this.types.map((type) => type.getSchema()) as any,
types: this.types.map((type) => {
// If it's an ObjectFieldType, wrap in a field structure, otherwise get the type's schema directly
if (type instanceof ObjectFieldType) {
return {
kind: 'field',
key: type.key,
value: type.value.getSchema(),
};
}
return type.getSchema();
}) as any,
};
}

public getOptions(): schema.Optional<schema.TupleSchema<{[K in keyof T]: SchemaOf<T[K]>}>> {
public getOptions(): schema.Optional<schema.TupleSchema<any>> {
const {kind, types, ...options} = this.schema;
return options as any;
}
Expand All @@ -48,7 +72,10 @@ export class TupleType<T extends Type[]> extends AbstractType<schema.TupleSchema
for (let i = 0; i < this.types.length; i++) {
const rv = ctx.codegen.getRegister();
ctx.js(/* js */ `var ${rv} = ${r}[${i}];`);
types[i].codegenValidator(ctx, [...path, i], rv);
const type = types[i];
// If it's an ObjectFieldType, validate the value type
const typeToValidate = type instanceof ObjectFieldType ? type.value : type;
typeToValidate.codegenValidator(ctx, [...path, i], rv);
}
ctx.emitCustomValidators(this, path, r);
}
Expand All @@ -59,10 +86,14 @@ export class TupleType<T extends Type[]> extends AbstractType<schema.TupleSchema
const length = types.length;
const last = length - 1;
for (let i = 0; i < last; i++) {
types[i].codegenJsonTextEncoder(ctx, new JsExpression(() => `${value.use()}[${i}]`));
const type = types[i];
const typeToEncode = type instanceof ObjectFieldType ? type.value : type;
typeToEncode.codegenJsonTextEncoder(ctx, new JsExpression(() => `${value.use()}[${i}]`));
ctx.writeText(',');
}
types[last].codegenJsonTextEncoder(ctx, new JsExpression(() => `${value.use()}[${last}]`));
const lastType = types[last];
const lastTypeToEncode = lastType instanceof ObjectFieldType ? lastType.value : lastType;
lastTypeToEncode.codegenJsonTextEncoder(ctx, new JsExpression(() => `${value.use()}[${last}]`));
ctx.writeText(']');
}

Expand All @@ -79,10 +110,13 @@ export class TupleType<T extends Type[]> extends AbstractType<schema.TupleSchema
);
const r = ctx.codegen.r();
ctx.js(/* js */ `var ${r} = ${value.use()};`);
for (let i = 0; i < length; i++)
for (let i = 0; i < length; i++) {
const type = types[i];
const typeToEncode = type instanceof ObjectFieldType ? type.value : type;
if (ctx instanceof CborEncoderCodegenContext)
types[i].codegenCborEncoder(ctx, new JsExpression(() => `${r}[${i}]`));
else types[i].codegenMessagePackEncoder(ctx, new JsExpression(() => `${r}[${i}]`));
typeToEncode.codegenCborEncoder(ctx, new JsExpression(() => `${r}[${i}]`));
else typeToEncode.codegenMessagePackEncoder(ctx, new JsExpression(() => `${r}[${i}]`));
}
}

public codegenCborEncoder(ctx: CborEncoderCodegenContext, value: JsExpression): void {
Expand Down Expand Up @@ -110,9 +144,10 @@ export class TupleType<T extends Type[]> extends AbstractType<schema.TupleSchema
});
for (let i = 0; i < length; i++) {
const type = types[i];
const typeToEncode = type instanceof ObjectFieldType ? type.value : type;
const isLast = i === length - 1;
codegen.js(`${rItem} = ${r}[${i}];`);
type.codegenJsonEncoder(ctx, expr);
typeToEncode.codegenJsonEncoder(ctx, expr);
if (!isLast) ctx.blob(arrSepBlob);
}
ctx.blob(
Expand All @@ -128,12 +163,30 @@ export class TupleType<T extends Type[]> extends AbstractType<schema.TupleSchema
if (!length) return '[]' as json_string<unknown>;
const last = length - 1;
let str = '[';
for (let i = 0; i < last; i++) str += (types[i] as any).toJson((value as unknown[])[i] as any, system) + ',';
str += (types[last] as any).toJson((value as unknown[])[last] as any, system);
for (let i = 0; i < last; i++) {
const type = types[i];
const typeToEncode = type instanceof ObjectFieldType ? type.value : type;
str += (typeToEncode as any).toJson((value as unknown[])[i] as any, system) + ',';
}
const lastType = types[last];
const lastTypeToEncode = lastType instanceof ObjectFieldType ? lastType.value : lastType;
str += (lastTypeToEncode as any).toJson((value as unknown[])[last] as any, system);
return (str + ']') as json_string<unknown>;
}

public toString(tab: string = ''): string {
return super.toString(tab) + printTree(tab, [...this.types.map((type) => (tab: string) => type.toString(tab))]);
return (
super.toString(tab) +
printTree(tab, [
...this.types.map((type) => (tab: string) => {
const typeToShow = type instanceof ObjectFieldType ? type.value : type;
const key = type instanceof ObjectFieldType ? type.key : undefined;
if (key) {
return `"${key}": ${typeToShow.toString(tab)}`;
}
return typeToShow.toString(tab);
}),
])
);
}
}