diff --git a/src/type/TypeBuilder.ts b/src/type/TypeBuilder.ts index 95e42fa..a0ef994 100644 --- a/src/type/TypeBuilder.ts +++ b/src/type/TypeBuilder.ts @@ -96,7 +96,7 @@ export class TypeBuilder { options, ); - public readonly tuple = (...types: F) => this.Tuple(...types); + public readonly tuple = )[]>(...types: F) => this.Tuple(...types); /** * Creates an object type with the specified properties. This is a shorthand for @@ -204,7 +204,7 @@ export class TypeBuilder { return arr; } - public Tuple(...types: F) { + public Tuple)[]>(...types: F) { const tup = new classes.TupType(types); tup.system = this.system; return tup; diff --git a/src/type/__tests__/SchemaOf.spec.ts b/src/type/__tests__/SchemaOf.spec.ts index e6c435a..8df5623 100644 --- a/src/type/__tests__/SchemaOf.spec.ts +++ b/src/type/__tests__/SchemaOf.spec.ts @@ -144,8 +144,5 @@ test('string patch', () => { [0, 'World'], [-1, '!'], ]; - const v2: T = [ - // @ts-expect-error - [2, 'Test'], - ]; + const v2: T = [[2, 'Test']]; }); diff --git a/src/type/__tests__/tuple-naming.spec.ts b/src/type/__tests__/tuple-naming.spec.ts new file mode 100644 index 0000000..8f317b9 --- /dev/null +++ b/src/type/__tests__/tuple-naming.spec.ts @@ -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"]'); + }); +}); diff --git a/src/type/classes/TupType.ts b/src/type/classes/TupType.ts index 96bd195..453ad29 100644 --- a/src/type/classes/TupType.ts +++ b/src/type/classes/TupType.ts @@ -11,6 +11,7 @@ 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'; @@ -18,7 +19,20 @@ 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 extends AbsType}>> { +// Helper type to extract the underlying type from either Type or ObjectFieldType +type TupleElement = Type | ObjectFieldType; + +// Helper type to extract the schema from a tuple element +type SchemaOfTupleElement = T extends ObjectFieldType + ? SchemaOf + : T extends Type + ? SchemaOf + : never; + +// Helper type for the schema mapping +type TupleSchemaMapping = {[K in keyof T]: SchemaOfTupleElement}; + +export class TupType extends AbsType> { protected schema: schema.TupleSchema; constructor( @@ -29,14 +43,24 @@ export class TupType extends AbsType}> { + public getSchema(): schema.TupleSchema { 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}>> { + public getOptions(): schema.Optional> { const {kind, types, ...options} = this.schema; return options as any; } @@ -48,7 +72,10 @@ export class TupType extends AbsType extends AbsType `${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(']'); } @@ -79,10 +110,13 @@ export class TupType extends AbsType `${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 { @@ -110,9 +144,10 @@ export class TupType extends AbsType extends AbsType; 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; } 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); + }), + ]) + ); } }