Skip to content

Commit

Permalink
improve typings and createNormalizer function
Browse files Browse the repository at this point in the history
Signed-off-by: Paul Marechal <paul.marechal@ericsson.com>
  • Loading branch information
paul-marechal committed Oct 14, 2021
1 parent b855c21 commit 25f5c00
Show file tree
Hide file tree
Showing 9 changed files with 120 additions and 76 deletions.
1 change: 1 addition & 0 deletions tsp-typescript-client/src/models/annotation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const Annotation = createNormalizer<Annotation>({
duration: BigInt,
entryId: assertNumber,
time: BigInt,
style: OutputElementStyle,
});

/**
Expand Down
3 changes: 3 additions & 0 deletions tsp-typescript-client/src/models/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { OutputElementStyle } from './styles';
export const Entry = createNormalizer<Entry>({
id: assertNumber,
parentId: assertNumber,
style: {
values: undefined,
},
});

/**
Expand Down
2 changes: 1 addition & 1 deletion tsp-typescript-client/src/models/output-descriptor.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { createNormalizer } from '../protocol/serialization';

export const OutputDescriptor = createNormalizer<OutputDescriptor>({
queryParameters: undefined,
end: BigInt,
queryParameters: undefined,
start: BigInt,
});

Expand Down
6 changes: 3 additions & 3 deletions tsp-typescript-client/src/models/response/responses.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BigintOrNumber, createNormalizer, Normalizer } from '../../protocol/serialization';
import { Deserialized, createNormalizer, Normalizer } from '../../protocol/serialization';

/**
* Response status
Expand All @@ -24,9 +24,9 @@ export enum ResponseStatus {
CANCELLED = 'CANCELLED'
}

export function GenericResponse<T>(): Normalizer<GenericResponse<BigintOrNumber<T>>>;
export function GenericResponse<T>(): Normalizer<GenericResponse<Deserialized<T>>>;
export function GenericResponse<T>(normalizer: Normalizer<T>): Normalizer<GenericResponse<T>>;
export function GenericResponse<T>(normalizer?: Normalizer<T>): Normalizer<GenericResponse<T>> | Normalizer<GenericResponse<BigintOrNumber<T>>> {
export function GenericResponse<T>(normalizer?: Normalizer<T>): Normalizer<GenericResponse<T>> | Normalizer<GenericResponse<Deserialized<T>>> {
return createNormalizer<GenericResponse<any>>({
model: normalizer,
});
Expand Down
6 changes: 6 additions & 0 deletions tsp-typescript-client/src/models/styles.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import { createNormalizer } from '../protocol/serialization';

export const OutputElementStyle = createNormalizer<OutputElementStyle>({
values: undefined,
});

/**
* Output element style object for one style key. It supports style
* inheritance. To avoid creating new styles the element style can have a parent
Expand Down
3 changes: 3 additions & 0 deletions tsp-typescript-client/src/models/timegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const TimeGraphEntry = createNormalizer<TimeGraphEntry>({
id: assertNumber,
parentId: assertNumber,
start: BigInt,
style: OutputElementStyle,
});

/**
Expand All @@ -28,6 +29,7 @@ const TimeGraphState = createNormalizer<TimeGraphState>({
end: BigInt,
start: BigInt,
tags: assertNumber,
style: OutputElementStyle,
});

/**
Expand Down Expand Up @@ -96,6 +98,7 @@ export const TimeGraphArrow = createNormalizer<TimeGraphArrow>({
sourceId: assertNumber,
start: BigInt,
targetId: assertNumber,
style: OutputElementStyle,
});

/**
Expand Down
12 changes: 6 additions & 6 deletions tsp-typescript-client/src/protocol/rest-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import fetch from 'node-fetch';
import { BigintOrNumber, Normalizer } from './serialization';
import { Deserialized, Normalizer } from './serialization';
import { TspClientResponse } from './tsp-client-response';
import JSONBigConfig = require('json-bigint');

Expand Down Expand Up @@ -27,7 +27,7 @@ export interface HttpResponse {
*/
export class RestClient {

static get<T>(url: string, parameters?: Map<string, string>): Promise<TspClientResponse<BigintOrNumber<T>>>;
static get<T>(url: string, parameters?: Map<string, string>): Promise<TspClientResponse<Deserialized<T>>>;
static get<T>(url: string, parameters: Map<string, string> | undefined, normalizer: Normalizer<T>): Promise<TspClientResponse<T>>;
/**
* Perform GET
Expand All @@ -44,7 +44,7 @@ export class RestClient {
return this.performRequest('get', getUrl, undefined, normalizer);
}

static post<T>(url: string, body?: any): Promise<TspClientResponse<BigintOrNumber<T>>>;
static post<T>(url: string, body?: any): Promise<TspClientResponse<Deserialized<T>>>;
static post<T>(url: string, body: any, normalizer: Normalizer<T>): Promise<TspClientResponse<T>>;
/**
* Perform POST
Expand All @@ -56,7 +56,7 @@ export class RestClient {
return this.performRequest<T>('post', url, body, normalizer);
}

static put<T>(url: string, body?: any): Promise<TspClientResponse<BigintOrNumber<T>>>;
static put<T>(url: string, body?: any): Promise<TspClientResponse<Deserialized<T>>>;
static put<T>(url: string, body: any, normalizer: Normalizer<T>): Promise<TspClientResponse<T>>;
/**
* Perform PUT
Expand All @@ -68,7 +68,7 @@ export class RestClient {
return this.performRequest<T>('put', url, body, normalizer);
}

static delete<T>(url: string, parameters?: Map<string, string>): Promise<TspClientResponse<BigintOrNumber<T>>>;
static delete<T>(url: string, parameters?: Map<string, string>): Promise<TspClientResponse<Deserialized<T>>>;
static delete<T>(url: string, parameters: Map<string, string> | undefined, normalizer: Normalizer<T>): Promise<TspClientResponse<T>>;
/**
* Perform DELETE
Expand All @@ -90,7 +90,7 @@ export class RestClient {
url: string,
body?: any,
normalizer?: Normalizer<T>,
): Promise<TspClientResponse<BigintOrNumber<T>>> {
): Promise<TspClientResponse<Deserialized<T>>> {
const response = await this.httpRequest({
url,
method,
Expand Down
162 changes: 96 additions & 66 deletions tsp-typescript-client/src/protocol/serialization.ts
Original file line number Diff line number Diff line change
@@ -1,104 +1,141 @@
/**
* Whenever a protocol message contains a `bigint` or `number` field, it may
* Whenever a protocol message contains a numeric field, it may
* be deserialized as `bigint` or as `number` depending on its size.
* `BigintOrNumber` is a mapped type that reflects that behavior.
* `Deserialized` is a mapped type that reflects that behavior.
*/
export type BigintOrNumber<T> =
export type Deserialized<T> =
bigint extends T
? T | number
: number extends T
? T | bigint
: T extends (infer U)[]
? BigintOrNumber<U>[]
: T extends { [key: string]: any }
? { [K in keyof T]: BigintOrNumber<T[K]> }
: T extends object
? { [K in keyof T]: Deserialized<T[K]> }
: T
;

/**
* Given a possibly altered input, get a normalized output.
*/
export type Normalizer<T> = (input: BigintOrNumber<T>) => T;
export type Normalizer<T> = (input: Deserialized<T>) => T;

/**
* `true` if `bigint` or `number` can be assigned to `T`, `false` otherwise.
*/
export type IsBigIntOrNumber<T> =
bigint extends T
? true
: number extends T
? true
: false
;

/**
* Remove all occurences of the `undefined` variant in `T`.
* For `T`, replace by `V` all types that can be assigned `U`.
*/
export type NonUndefined<T> = T extends undefined
? never
export type Replace<T, U, V> =
U extends T
? V
: T extends object
? Required<T>
? { [K in keyof T]: Replace<T[K], U, V> }
: T
;

/**
* `true` if `T` must be normalized, `false` otherwise.
*/
export type MustBeNormalized<T> =
Replace<Deserialized<T>, unknown, 0> extends Replace<T, unknown, 1> ? false : true;

/**
* Mapped type that only keeps properties that need to be normalized.
*/
export type MustBeNormalized<T, U = NonUndefined<T>> =
unknown extends U
? U
: unknown[] extends U
? U
: U extends (infer V)[]
? unknown extends V
? U
: unknown[] extends V
? U
: BigintOrNumber<V> extends V
? never
: U
: U extends { [key: string]: any }
export type OnlyMustBeNormalized<T> =
MustBeNormalized<T> extends false
? never // Discard
: T extends any[] // Is T an array of U?
? T // Keep
: T extends object // Is T an object?
? {
[K in keyof U as
unknown extends U[K]
? K
: unknown[] extends U[K]
? K
: BigintOrNumber<U[K]> extends U[K]
? never
: K
]: MustBeNormalized<U[K]>
[K in keyof T as
unknown extends T[K] // Is the value any?
? K // Keep
: MustBeNormalized<T[K]> extends true
? K // Keep
: never // Discard
]-?:
OnlyMustBeNormalized<T[K]>; // Keep
}
: U
: T // Keep
;

/**
* Remove the `undefined` variant from `T`.
*/
export type NonUndefined<T> = T extends undefined ? never : T;

/**
* Object passed to `createNormalizer` that acts as a template.
*/
export type NormalizerDescriptor<T, U = MustBeNormalized<T>> =
unknown extends U
? Normalizer<any> | undefined
: unknown[] extends U
? Normalizer<any> | undefined
: bigint extends U
? Normalizer<U>
: number extends U
? Normalizer<U>
: U extends any[]
? Normalizer<U>
: U extends { [key: string]: any }
? { [K in keyof U]: NormalizerDescriptor<U[K]> } | Normalizer<U>
: never
export type NormalizerDescriptor<
T,
U = OnlyMustBeNormalized<T>,
V = NonUndefined<T>,
> =
unknown extends V // Is U any?
? Normalizer<any> | undefined // U is any.
: IsBigIntOrNumber<U> extends true // Is U a bigint or a number?
? Normalizer<V> // U is a bigint or a number.
: U extends (infer Z)[] // Is U an array of V?
? unknown extends Z // Is V any?
? Normalizer<V> | undefined // U is any[].
: Normalizer<V> // U is an array.
: U extends object // Is U an object?
? string extends keyof U // Is U a record?
? U extends Record<string, unknown> // Is U a record of any?
? Normalizer<V> | undefined // U is a record of any.
: Normalizer<V> // U is NOT a record of any.
: Normalizer<V> | {
[K in keyof U]: NormalizerDescriptor<K extends keyof V ? V[K] : never>
} // U is a regular object.
: never // U is none of the above.
;

/**
* Create a normalizer function based on `descriptor` for `T` .
*
* General rules for a descriptor:
* - If a field is `any`-like then you should either define a generic normalizer
* for the value or explicitly use `undefined`.
* - Record objects (`{ [key: string]: any }`) are considered `any`-like.
* - Any field that directly or indirectly contains `bigint` or `number` must
* have a normalizer function attached to it.
*/
export function createNormalizer<T>(descriptor: NormalizerDescriptor<T>): Normalizer<T> {
return input => normalize(input, descriptor);
}

function normalize<T>(input: BigintOrNumber<T>, descriptor?: NormalizerDescriptor<T>): T {
/**
* Create a deep-copy of `input` while applying normalizers from `descriptor`.
*/
function normalize<T>(input: Deserialized<T>, descriptor?: NormalizerDescriptor<T>): T {
if (input === undefined) {
// Undefined
return undefined as any; // Pass-through
}
if (typeof input === 'object') {
if (input === null) {
// Null
// tslint:disable-next-line: no-null-keyword
return null as any;
return null as any; // Pass-through
} else if (Array.isArray(input)) {
// Array
return typeof descriptor === 'function'
? descriptor(input)
: input.map(element => normalize(element));
? descriptor(input as any) // Normalize
: input.map(element => normalize(element)); // Deep-copy
} else {
// Object
if (typeof descriptor === 'function') {
return descriptor(input);
return descriptor(input as any); // Normalize
}
const output: Partial<T> = {};
for (const [key, value] of Object.entries(input)) {
Expand All @@ -107,24 +144,17 @@ function normalize<T>(input: BigintOrNumber<T>, descriptor?: NormalizerDescripto
return output as T;
}
}
// Primitive
return typeof descriptor === 'function'
? descriptor(input)
: input;
}

export function _debug<T extends Normalizer<any>>(normalizer: T, label: string = ''): T {
return (input => {
const output = normalizer(input);
console.log(`DEBUG [${label}]: "${input}" -> "${output}"`);
return output;
}) as T;
? descriptor(input as any) // Normalize
: input; // Copy (because it is neither `object` nor `array`)
}

/**
* Create a normalizer that operates on JS Array objects.
*/
export function array<T>(normalizer: Normalizer<T>): Normalizer<T[]> {
return arr => arr.map(element => normalizer(element));
return input => input.map(element => normalizer(element));
}

/**
Expand Down
1 change: 1 addition & 0 deletions tsp-typescript-client/src/protocol/tsp-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ describe('TspClient Deserialization', () => {
for (const serie of xy.series) {
expect(typeof serie.seriesId).toEqual('number');
expect(serie.xValues).toHaveLength(3);
expect(serie.yValues).toHaveLength(3);
for (const xValue of serie.xValues) {
expect(typeof xValue).toEqual('number');
}
Expand Down

0 comments on commit 25f5c00

Please sign in to comment.