Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
1 change: 0 additions & 1 deletion src/type/__tests__/SchemaOf.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ test('string patch', () => {
[-1, '!'],
];
const v2: T = [
// @ts-expect-error
[2, 'Test'],
];
});
91 changes: 91 additions & 0 deletions src/type/__tests__/tuple-naming.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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: 'num' },
{ 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: 'num' },
{ kind: 'str' }
]
});
Copy link
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
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: 'num' },
{ kind: 'str' }
]
});
});

test('tuple toString shows field names when present', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Remove this test.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the toString test as requested. (957ed5c)

const namedTuple = t.Tuple(t.prop('x', t.num), t.prop('y', t.str));
const mixedTuple = t.Tuple(t.prop('x', t.num), t.str);
const regularTuple = t.Tuple(t.num, t.str);

const namedStr = namedTuple.toString();
const mixedStr = mixedTuple.toString();
const regularStr = regularTuple.toString();

expect(namedStr).toContain('"x"');
expect(namedStr).toContain('"y"');
expect(mixedStr).toContain('"x"');
expect(regularStr).not.toContain('"');
});

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"]');
});
});
71 changes: 57 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,17 @@ 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, get the value type's schema, otherwise get the type's schema directly
return type instanceof ObjectFieldType ? type.value.getSchema() : 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 +65,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 +79,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 +103,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 +137,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 +156,27 @@ 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);
})
]);
}
}
Loading