From 25f5c0066cc961cd9a49a111173a2d792ad5a61f Mon Sep 17 00:00:00 2001 From: Paul Marechal Date: Thu, 14 Oct 2021 11:34:36 -0400 Subject: [PATCH] improve typings and `createNormalizer` function Signed-off-by: Paul Marechal --- .../src/models/annotation.ts | 1 + tsp-typescript-client/src/models/entry.ts | 3 + .../src/models/output-descriptor.ts | 2 +- .../src/models/response/responses.ts | 6 +- tsp-typescript-client/src/models/styles.ts | 6 + tsp-typescript-client/src/models/timegraph.ts | 3 + .../src/protocol/rest-client.ts | 12 +- .../src/protocol/serialization.ts | 162 +++++++++++------- .../src/protocol/tsp-client.test.ts | 1 + 9 files changed, 120 insertions(+), 76 deletions(-) diff --git a/tsp-typescript-client/src/models/annotation.ts b/tsp-typescript-client/src/models/annotation.ts index d6c2098..83cf453 100644 --- a/tsp-typescript-client/src/models/annotation.ts +++ b/tsp-typescript-client/src/models/annotation.ts @@ -14,6 +14,7 @@ export const Annotation = createNormalizer({ duration: BigInt, entryId: assertNumber, time: BigInt, + style: OutputElementStyle, }); /** diff --git a/tsp-typescript-client/src/models/entry.ts b/tsp-typescript-client/src/models/entry.ts index ef5f79c..ec7a3b5 100644 --- a/tsp-typescript-client/src/models/entry.ts +++ b/tsp-typescript-client/src/models/entry.ts @@ -4,6 +4,9 @@ import { OutputElementStyle } from './styles'; export const Entry = createNormalizer({ id: assertNumber, parentId: assertNumber, + style: { + values: undefined, + }, }); /** diff --git a/tsp-typescript-client/src/models/output-descriptor.ts b/tsp-typescript-client/src/models/output-descriptor.ts index d270d7a..2e703b3 100644 --- a/tsp-typescript-client/src/models/output-descriptor.ts +++ b/tsp-typescript-client/src/models/output-descriptor.ts @@ -1,8 +1,8 @@ import { createNormalizer } from '../protocol/serialization'; export const OutputDescriptor = createNormalizer({ - queryParameters: undefined, end: BigInt, + queryParameters: undefined, start: BigInt, }); diff --git a/tsp-typescript-client/src/models/response/responses.ts b/tsp-typescript-client/src/models/response/responses.ts index 80f86a1..152feaf 100644 --- a/tsp-typescript-client/src/models/response/responses.ts +++ b/tsp-typescript-client/src/models/response/responses.ts @@ -1,4 +1,4 @@ -import { BigintOrNumber, createNormalizer, Normalizer } from '../../protocol/serialization'; +import { Deserialized, createNormalizer, Normalizer } from '../../protocol/serialization'; /** * Response status @@ -24,9 +24,9 @@ export enum ResponseStatus { CANCELLED = 'CANCELLED' } -export function GenericResponse(): Normalizer>>; +export function GenericResponse(): Normalizer>>; export function GenericResponse(normalizer: Normalizer): Normalizer>; -export function GenericResponse(normalizer?: Normalizer): Normalizer> | Normalizer>> { +export function GenericResponse(normalizer?: Normalizer): Normalizer> | Normalizer>> { return createNormalizer>({ model: normalizer, }); diff --git a/tsp-typescript-client/src/models/styles.ts b/tsp-typescript-client/src/models/styles.ts index eaf271b..c5ae19e 100644 --- a/tsp-typescript-client/src/models/styles.ts +++ b/tsp-typescript-client/src/models/styles.ts @@ -1,3 +1,9 @@ +import { createNormalizer } from '../protocol/serialization'; + +export const OutputElementStyle = createNormalizer({ + 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 diff --git a/tsp-typescript-client/src/models/timegraph.ts b/tsp-typescript-client/src/models/timegraph.ts index 9ab05a8..856f07f 100644 --- a/tsp-typescript-client/src/models/timegraph.ts +++ b/tsp-typescript-client/src/models/timegraph.ts @@ -7,6 +7,7 @@ export const TimeGraphEntry = createNormalizer({ id: assertNumber, parentId: assertNumber, start: BigInt, + style: OutputElementStyle, }); /** @@ -28,6 +29,7 @@ const TimeGraphState = createNormalizer({ end: BigInt, start: BigInt, tags: assertNumber, + style: OutputElementStyle, }); /** @@ -96,6 +98,7 @@ export const TimeGraphArrow = createNormalizer({ sourceId: assertNumber, start: BigInt, targetId: assertNumber, + style: OutputElementStyle, }); /** diff --git a/tsp-typescript-client/src/protocol/rest-client.ts b/tsp-typescript-client/src/protocol/rest-client.ts index 63b09a5..e1657cf 100644 --- a/tsp-typescript-client/src/protocol/rest-client.ts +++ b/tsp-typescript-client/src/protocol/rest-client.ts @@ -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'); @@ -27,7 +27,7 @@ export interface HttpResponse { */ export class RestClient { - static get(url: string, parameters?: Map): Promise>>; + static get(url: string, parameters?: Map): Promise>>; static get(url: string, parameters: Map | undefined, normalizer: Normalizer): Promise>; /** * Perform GET @@ -44,7 +44,7 @@ export class RestClient { return this.performRequest('get', getUrl, undefined, normalizer); } - static post(url: string, body?: any): Promise>>; + static post(url: string, body?: any): Promise>>; static post(url: string, body: any, normalizer: Normalizer): Promise>; /** * Perform POST @@ -56,7 +56,7 @@ export class RestClient { return this.performRequest('post', url, body, normalizer); } - static put(url: string, body?: any): Promise>>; + static put(url: string, body?: any): Promise>>; static put(url: string, body: any, normalizer: Normalizer): Promise>; /** * Perform PUT @@ -68,7 +68,7 @@ export class RestClient { return this.performRequest('put', url, body, normalizer); } - static delete(url: string, parameters?: Map): Promise>>; + static delete(url: string, parameters?: Map): Promise>>; static delete(url: string, parameters: Map | undefined, normalizer: Normalizer): Promise>; /** * Perform DELETE @@ -90,7 +90,7 @@ export class RestClient { url: string, body?: any, normalizer?: Normalizer, - ): Promise>> { + ): Promise>> { const response = await this.httpRequest({ url, method, diff --git a/tsp-typescript-client/src/protocol/serialization.ts b/tsp-typescript-client/src/protocol/serialization.ts index db27b7f..2e66b4e 100644 --- a/tsp-typescript-client/src/protocol/serialization.ts +++ b/tsp-typescript-client/src/protocol/serialization.ts @@ -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 = +export type Deserialized = bigint extends T ? T | number : number extends T ? T | bigint - : T extends (infer U)[] - ? BigintOrNumber[] - : T extends { [key: string]: any } - ? { [K in keyof T]: BigintOrNumber } + : T extends object + ? { [K in keyof T]: Deserialized } : T ; /** * Given a possibly altered input, get a normalized output. */ -export type Normalizer = (input: BigintOrNumber) => T; +export type Normalizer = (input: Deserialized) => T; + +/** + * `true` if `bigint` or `number` can be assigned to `T`, `false` otherwise. + */ +export type IsBigIntOrNumber = + 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 extends undefined - ? never +export type Replace = + U extends T + ? V : T extends object - ? Required + ? { [K in keyof T]: Replace } : T ; +/** + * `true` if `T` must be normalized, `false` otherwise. + */ +export type MustBeNormalized = + Replace, unknown, 0> extends Replace ? false : true; + /** * Mapped type that only keeps properties that need to be normalized. */ -export type MustBeNormalized> = - unknown extends U - ? U - : unknown[] extends U - ? U - : U extends (infer V)[] - ? unknown extends V - ? U - : unknown[] extends V - ? U - : BigintOrNumber extends V - ? never - : U - : U extends { [key: string]: any } +export type OnlyMustBeNormalized = + MustBeNormalized 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 extends U[K] - ? never - : K - ]: MustBeNormalized + [K in keyof T as + unknown extends T[K] // Is the value any? + ? K // Keep + : MustBeNormalized extends true + ? K // Keep + : never // Discard + ]-?: + OnlyMustBeNormalized; // Keep } - : U + : T // Keep ; +/** + * Remove the `undefined` variant from `T`. + */ +export type NonUndefined = T extends undefined ? never : T; + /** * Object passed to `createNormalizer` that acts as a template. */ -export type NormalizerDescriptor> = - unknown extends U - ? Normalizer | undefined - : unknown[] extends U - ? Normalizer | undefined - : bigint extends U - ? Normalizer - : number extends U - ? Normalizer - : U extends any[] - ? Normalizer - : U extends { [key: string]: any } - ? { [K in keyof U]: NormalizerDescriptor } | Normalizer - : never +export type NormalizerDescriptor< + T, + U = OnlyMustBeNormalized, + V = NonUndefined, + > = + unknown extends V // Is U any? + ? Normalizer | undefined // U is any. + : IsBigIntOrNumber extends true // Is U a bigint or a number? + ? Normalizer // U is a bigint or a number. + : U extends (infer Z)[] // Is U an array of V? + ? unknown extends Z // Is V any? + ? Normalizer | undefined // U is any[]. + : Normalizer // U is an array. + : U extends object // Is U an object? + ? string extends keyof U // Is U a record? + ? U extends Record // Is U a record of any? + ? Normalizer | undefined // U is a record of any. + : Normalizer // U is NOT a record of any. + : Normalizer | { + [K in keyof U]: NormalizerDescriptor + } // 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(descriptor: NormalizerDescriptor): Normalizer { return input => normalize(input, descriptor); } -function normalize(input: BigintOrNumber, descriptor?: NormalizerDescriptor): T { +/** + * Create a deep-copy of `input` while applying normalizers from `descriptor`. + */ +function normalize(input: Deserialized, descriptor?: NormalizerDescriptor): 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 = {}; for (const [key, value] of Object.entries(input)) { @@ -107,24 +144,17 @@ function normalize(input: BigintOrNumber, descriptor?: NormalizerDescripto return output as T; } } + // Primitive return typeof descriptor === 'function' - ? descriptor(input) - : input; -} - -export function _debug>(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(normalizer: Normalizer): Normalizer { - return arr => arr.map(element => normalizer(element)); + return input => input.map(element => normalizer(element)); } /** diff --git a/tsp-typescript-client/src/protocol/tsp-client.test.ts b/tsp-typescript-client/src/protocol/tsp-client.test.ts index 4bca19d..fe7f869 100644 --- a/tsp-typescript-client/src/protocol/tsp-client.test.ts +++ b/tsp-typescript-client/src/protocol/tsp-client.test.ts @@ -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'); }