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'}],
});
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Named fields should wrap the type node in a field node:

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the schema generation to wrap named fields in field nodes with the expected structure. The schema now correctly shows { kind: 'field', key: 'x', value: { kind: 'num' } } for named tuple members. (957ed5c)

});

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);
}),
])
);
}
}