Skip to content
Merged
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
99 changes: 99 additions & 0 deletions packages/client/lib/RESP/types.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { RESP_TYPES } from './decoder';

describe('RESP Type Mapping', () => {
testUtils.testWithClient('type mappings', async client => {
// Scalar Primitives
// INTEGER: EXISTS returns number (0|1)
const existsRes = await client.withTypeMapping({}).exists('some_key');
assert.strictEqual(typeof existsRes, 'number');

// BIG_NUMBER: maps to bigint when configured
const bigNumRes = await client
.withTypeMapping({
[RESP_TYPES.BIG_NUMBER]: BigInt
})
.hello();
assert.ok(
typeof bigNumRes === 'bigint' ||
typeof bigNumRes === 'number' ||
typeof bigNumRes === 'object'
);

// DOUBLE: maps to number when configured
// Use ZINCRBY which returns a DoubleReply
const doubleRes = await client
.withTypeMapping({
[RESP_TYPES.DOUBLE]: Number
})
.zIncrBy('zset-double-test', 1.5, 'member');
assert.strictEqual(typeof doubleRes, 'number');
assert.strictEqual(doubleRes, 1.5);

// Complex Strings
// VERBATIM_STRING maps to string
const verbatimRes = await client
.withTypeMapping({
[RESP_TYPES.VERBATIM_STRING]: String
})
.get('key');
assert.ok(
verbatimRes === null ||
typeof verbatimRes === 'string' ||
Buffer.isBuffer(verbatimRes)
);

// Recursive Collections
// ARRAY infers nested mapped types
const arrayRes = await client
.withTypeMapping({
[RESP_TYPES.BLOB_STRING]: String
})
.lRange('key', 0, -1);
assert.ok(Array.isArray(arrayRes));

// SET infers Set or array
const setRes = await client
.withTypeMapping({
[RESP_TYPES.BLOB_STRING]: String
})
.sMembers('key');
assert.ok(setRes instanceof Set || Array.isArray(setRes));

// MAP infers Map or object
const mapRes = await client
.withTypeMapping({
[RESP_TYPES.BLOB_STRING]: String
})
.hGetAll('key');
assert.ok(mapRes instanceof Map || typeof mapRes === 'object');

// Edge Cases
// SIMPLE_ERROR remains Error
try {
await client
.withTypeMapping({
[RESP_TYPES.SIMPLE_ERROR]: Error
})
.hello();
assert.fail('Expected error');
} catch (e) {
assert.ok(e instanceof Error);
}

// NULL always remains null
const nullRes = await client
.withTypeMapping({})
.get('missing-key-random-12345');
assert.strictEqual(nullRes, null);

// hGet infers string | null
const hGetRes = await client
.withTypeMapping({
[RESP_TYPES.BLOB_STRING]: String
})
.hGet('foo', 'bar');
assert.ok(hGetRes === null || typeof hGetRes === 'string');
}, GLOBAL.SERVERS.OPEN);
});
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