Skip to content
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
133 changes: 133 additions & 0 deletions packages/client/lib/RESP/types.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { strict as assert } from 'node:assert';
import { describe, it, before, after } from 'mocha';
import { createClient } from '../../index';
import { RESP_TYPES } from './decoder';
import { VerbatimString } from './verbatim-string';

describe('Comprehensive RESP Type Mapping', () => {
let client: any;

before(async () => {
client = createClient();
await client.connect();
});


after(async () => {
if (client) {
await client.destroy();
}
});

describe('Scalar Primitives', () => {
it('INTEGER: EXISTS returns number (0|1)', async () => {
const res: number = await client
.withTypeMapping({})
.exists('some_key');

assert.strictEqual(typeof res, 'number');
});

it('BIG_NUMBER: should infer as primitive bigint', async () => {
const res: bigint | string | number = await client
.withTypeMapping({
[RESP_TYPES.BIG_NUMBER]: BigInt
})
.hello();

assert.ok(typeof res === 'bigint' || typeof res === 'object');
});

it('DOUBLE: should infer as primitive number', async () => {
const res: number | null = await client
.withTypeMapping({
[RESP_TYPES.DOUBLE]: Number
})
.hello();

assert.ok(res === null || typeof res === 'number' || typeof res === 'object');
});
});

describe('Complex Strings', () => {
it('VERBATIM_STRING: should map to string, not object', async () => {
const res: string | Buffer | VerbatimString = await client
.withTypeMapping({
[RESP_TYPES.VERBATIM_STRING]: String
})
.get('key');

assert.ok(
res === null ||
typeof res === 'string' ||
Buffer.isBuffer(res)
);
});
});

describe('Recursive Collections', () => {
it('ARRAY: should correctly infer nested mapped types', async () => {
const res: string[] = await client
.withTypeMapping({
[RESP_TYPES.BLOB_STRING]: String
})
.lRange('key', 0, -1);

assert.ok(Array.isArray(res));
});

it('SET: should correctly infer Set of primitives', async () => {
const res: Set<string> | string[] = await client
.withTypeMapping({
[RESP_TYPES.BLOB_STRING]: String
})
.sMembers('key');

assert.ok(res instanceof Set || Array.isArray(res));
});

it('MAP: should correctly infer Map with mapped keys and values', async () => {
const res: Map<string, string> | Record<string, string> = await client
.withTypeMapping({
[RESP_TYPES.BLOB_STRING]: String
})
.hGetAll('key');

assert.ok(res instanceof Map || typeof res === 'object');
});
});

describe('Edge Cases', () => {
it('SIMPLE_ERROR: should still return Error objects', async () => {

try {
const res: Error = await client
.withTypeMapping({
[RESP_TYPES.SIMPLE_ERROR]: Error
})
.hello();
assert.ok(typeof res === 'object');
} catch (e) {
assert.ok(e instanceof Error);
}
});

it('NULL: should always remain null regardless of mapping', async () => {
const res: string | null = await client
.withTypeMapping({})
.get('missing-key-random-12345');

assert.strictEqual(res, null);
});

it('hGet: should infer string | null (fixing string | {})', async () => {
const res: string | null = await client
.withTypeMapping({
[RESP_TYPES.BLOB_STRING]: String
})
.hGet('foo', 'bar');

assert.ok(res === null || typeof res === 'string');
});
});
});
116 changes: 61 additions & 55 deletions packages/client/lib/RESP/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ export interface RespType<
export interface NullReply extends RespType<
RESP_TYPES['NULL'],
null
> {}
> { }

export interface BooleanReply<
T extends boolean = boolean
> extends RespType<
RESP_TYPES['BOOLEAN'],
T
> {}
> { }

export interface NumberReply<
T extends number = number
Expand All @@ -43,7 +43,7 @@ export interface NumberReply<
T,
`${T}`,
number | string
> {}
> { }

export interface BigNumberReply<
T extends bigint = bigint
Expand All @@ -52,7 +52,7 @@ export interface BigNumberReply<
T,
number | `${T}`,
bigint | number | string
> {}
> { }

export interface DoubleReply<
T extends number = number
Expand All @@ -61,7 +61,7 @@ export interface DoubleReply<
T,
`${T}`,
number | string
> {}
> { }

export interface SimpleStringReply<
T extends string = string
Expand All @@ -70,7 +70,7 @@ export interface SimpleStringReply<
T,
Buffer,
string | Buffer
> {}
> { }

export interface BlobStringReply<
T extends string = string
Expand All @@ -90,65 +90,65 @@ export interface VerbatimStringReply<
T,
Buffer | VerbatimString,
string | Buffer | VerbatimString
> {}
> { }

export interface SimpleErrorReply extends RespType<
RESP_TYPES['SIMPLE_ERROR'],
SimpleError,
Buffer
> {}
> { }

export interface BlobErrorReply extends RespType<
RESP_TYPES['BLOB_ERROR'],
BlobError,
Buffer
> {}
> { }

export interface ArrayReply<T> extends RespType<
RESP_TYPES['ARRAY'],
Array<T>,
never,
Array<any>
> {}
> { }

export interface TuplesReply<T extends [...Array<unknown>]> extends RespType<
RESP_TYPES['ARRAY'],
T,
never,
Array<any>
> {}
> { }

export interface SetReply<T> extends RespType<
RESP_TYPES['SET'],
Array<T>,
Set<T>,
Array<any> | Set<any>
> {}
> { }

export interface MapReply<K, V> extends RespType<
RESP_TYPES['MAP'],
{ [key: string]: V },
Map<K, V> | Array<K | V>,
Map<any, any> | Array<any>
> {}
> { }

type MapKeyValue = [key: BlobStringReply | SimpleStringReply, value: unknown];

type MapTuples = Array<MapKeyValue>;

type ExtractMapKey<T> = (
T extends BlobStringReply<infer S> ? S :
T extends SimpleStringReply<infer S> ? S :
never
T extends BlobStringReply<infer S> ? S :
T extends SimpleStringReply<infer S> ? S :
never
);

export interface TuplesToMapReply<T extends MapTuples> extends RespType<
RESP_TYPES['MAP'],
{
[P in T[number] as ExtractMapKey<P[0]>]: P[1];
[P in T[number]as ExtractMapKey<P[0]>]: P[1];
},
Map<ExtractMapKey<T[number][0]>, T[number][1]> | FlattenTuples<T>
> {}
> { }

type FlattenTuples<T> = (
T extends [] ? [] :
Expand Down Expand Up @@ -193,32 +193,38 @@ type MapKey<
[RESP_TYPES.BLOB_STRING]: StringConstructor;
}>;

type UnwrapConstructor<T> =
T extends StringConstructor ? string :
T extends NumberConstructor ? number :
T extends BooleanConstructor ? boolean :
T extends BigIntConstructor ? bigint :
T;
export type UnwrapReply<REPLY extends RespType<any, any, any, any>> = REPLY['DEFAULT' | 'TYPES'];

export type ReplyWithTypeMapping<
REPLY,
TYPE_MAPPING extends TypeMapping
> = (
// if REPLY is a type, extract the coresponding type from TYPE_MAPPING or use the default type
REPLY extends RespType<infer RESP_TYPE, infer DEFAULT, infer TYPES, unknown> ?
// if REPLY is a type, extract the coresponding type from TYPE_MAPPING or use the default type
REPLY extends RespType<infer RESP_TYPE, infer DEFAULT, infer TYPES, unknown> ?
TYPE_MAPPING[RESP_TYPE] extends MappedType<infer T> ?
ReplyWithTypeMapping<Extract<DEFAULT | TYPES, T>, TYPE_MAPPING> :
ReplyWithTypeMapping<DEFAULT, TYPE_MAPPING>
: (
// if REPLY is a known generic type, convert its generic arguments
// TODO: tuples?
REPLY extends Array<infer T> ? Array<ReplyWithTypeMapping<T, TYPE_MAPPING>> :
REPLY extends Set<infer T> ? Set<ReplyWithTypeMapping<T, TYPE_MAPPING>> :
REPLY extends Map<infer K, infer V> ? Map<MapKey<K, TYPE_MAPPING>, ReplyWithTypeMapping<V, TYPE_MAPPING>> :
// `Date | Buffer | Error` are supersets of `Record`, so they need to be checked first
REPLY extends Date | Buffer | Error ? REPLY :
REPLY extends Record<PropertyKey, any> ? {
[P in keyof REPLY]: ReplyWithTypeMapping<REPLY[P], TYPE_MAPPING>;
} :
// otherwise, just return the REPLY as is
REPLY
)
);
ReplyWithTypeMapping<Extract<DEFAULT | TYPES, UnwrapConstructor<T>>, TYPE_MAPPING> :
ReplyWithTypeMapping<DEFAULT, TYPE_MAPPING>
: (
// if REPLY is a known generic type, convert its generic arguments
// TODO: tuples?
REPLY extends Array<infer T> ? Array<ReplyWithTypeMapping<T, TYPE_MAPPING>> :
REPLY extends Set<infer T> ? Set<ReplyWithTypeMapping<T, TYPE_MAPPING>> :
REPLY extends Map<infer K, infer V> ? Map<MapKey<K, TYPE_MAPPING>, ReplyWithTypeMapping<V, TYPE_MAPPING>> :
// `Date | Buffer | Error` are supersets of `Record`, so they need to be checked first
REPLY extends Date | Buffer | Error ? REPLY :
REPLY extends Record<PropertyKey, any> ? {
[P in keyof REPLY]: ReplyWithTypeMapping<REPLY[P], TYPE_MAPPING>;
} :
// otherwise, just return the REPLY as is
REPLY
)
);

export type TransformReply = (this: void, reply: any, preserve?: any, typeMapping?: TypeMapping) => any; // TODO;

Expand Down Expand Up @@ -342,17 +348,17 @@ type Resp2Array<T> = (

export type Resp2Reply<RESP3REPLY> = (
RESP3REPLY extends RespType<infer RESP_TYPE, infer DEFAULT, infer TYPES, unknown> ?
// TODO: RESP3 only scalar types
RESP_TYPE extends RESP_TYPES['DOUBLE'] ? BlobStringReply :
RESP_TYPE extends RESP_TYPES['ARRAY'] | RESP_TYPES['SET'] ? RespType<
RESP_TYPE,
Resp2Array<DEFAULT>
> :
RESP_TYPE extends RESP_TYPES['MAP'] ? RespType<
RESP_TYPES['ARRAY'],
Resp2Array<Extract<TYPES, Array<any>>>
> :
RESP3REPLY :
// TODO: RESP3 only scalar types
RESP_TYPE extends RESP_TYPES['DOUBLE'] ? BlobStringReply :
RESP_TYPE extends RESP_TYPES['ARRAY'] | RESP_TYPES['SET'] ? RespType<
RESP_TYPE,
Resp2Array<DEFAULT>
> :
RESP_TYPE extends RESP_TYPES['MAP'] ? RespType<
RESP_TYPES['ARRAY'],
Resp2Array<Extract<TYPES, Array<any>>>
> :
RESP3REPLY :
RESP3REPLY
);

Expand All @@ -362,13 +368,13 @@ export type CommandReply<
COMMAND extends Command,
RESP extends RespVersions
> = (
// if transformReply is a function, use its return type
COMMAND['transformReply'] extends (...args: any) => infer T ? T :
// if transformReply[RESP] is a function, use its return type
COMMAND['transformReply'] extends Record<RESP, (...args: any) => infer T> ? T :
// otherwise use the generic reply type
ReplyUnion
);
// if transformReply is a function, use its return type
COMMAND['transformReply'] extends (...args: any) => infer T ? T :
// if transformReply[RESP] is a function, use its return type
COMMAND['transformReply'] extends Record<RESP, (...args: any) => infer T> ? T :
// otherwise use the generic reply type
ReplyUnion
);

export type CommandSignature<
COMMAND extends Command,
Expand Down
Loading