Skip to content

Add support for naming tuple members #33

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.TupType<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/TupType.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 {AbsType} from './AbsType';
import {ObjectFieldType} from './ObjType';
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 TupType<T extends Type[]> extends AbsType<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 TupType<T extends TupleElement[]> extends AbsType<schema.TupleSchema<any>> {
protected schema: schema.TupleSchema<any>;

constructor(
Expand All @@ -29,14 +43,24 @@ export class TupType<T extends Type[]> extends AbsType<schema.TupleSchema<{[K in
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 TupType<T extends Type[]> extends AbsType<schema.TupleSchema<{[K in
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 TupType<T extends Type[]> extends AbsType<schema.TupleSchema<{[K in
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 TupType<T extends Type[]> extends AbsType<schema.TupleSchema<{[K in
);
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 TupType<T extends Type[]> extends AbsType<schema.TupleSchema<{[K in
});
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 TupType<T extends Type[]> extends AbsType<schema.TupleSchema<{[K in
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);
}),
])
);
}
}