diff --git a/packages/authentication-adapters/src/compositeAuthenticationAdapter.ts b/packages/authentication-adapters/src/compositeAuthenticationAdapter.ts index 9b836f3a9..4ca5ac6bc 100644 --- a/packages/authentication-adapters/src/compositeAuthenticationAdapter.ts +++ b/packages/authentication-adapters/src/compositeAuthenticationAdapter.ts @@ -131,6 +131,10 @@ function findMatchingAuth( return securityRequirements.find( (andRequirements) => Object.keys(andRequirements).every( + // @ts-ignore TODO: REMOVE THIS COMMENT. DO NOT COPY. + // THIS WAS ONLY FOR MIGRATING UNCHECKED CODE TO STRICT TYPE CHECKING. + // As for the actual compiler error, TS does not support generic keys with the `in` operator. + // https://github.com/microsoft/TypeScript/issues/21732#issuecomment-1423655000 (key) => key in providerConfig && providerConfig[key] ) && Object.values(andRequirements).every((value) => value) ); @@ -145,7 +149,11 @@ function getHttpInterceptorsForAuths( ): Array> { return Object.entries(matchingRequirements).map( ([authProvider, authParam]) => { + // @ts-ignore TODO: REMOVE THIS COMMENT. DO NOT COPY. + // THIS WAS ONLY FOR MIGRATING UNCHECKED CODE TO STRICT TYPE CHECKING. if (providerConfig[authProvider] !== undefined) { + // @ts-ignore TODO: REMOVE THIS COMMENT. DO NOT COPY. + // THIS WAS ONLY FOR MIGRATING UNCHECKED CODE TO STRICT TYPE CHECKING. return providerConfig[authProvider](authParam); } else { return passThroughInterceptor; diff --git a/packages/authentication-adapters/test/accessTokenAdapter.test.ts b/packages/authentication-adapters/test/accessTokenAdapter.test.ts index bbafc7002..bbd9c9c47 100644 --- a/packages/authentication-adapters/test/accessTokenAdapter.test.ts +++ b/packages/authentication-adapters/test/accessTokenAdapter.test.ts @@ -26,7 +26,7 @@ describe('test access token authentication scheme', () => { const authenticationProvider = accessTokenAuthenticationProvider(config); const handler = authenticationProvider(true); const interceptor = [handler]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -51,7 +51,7 @@ describe('test access token authentication scheme', () => { const authenticationProvider = accessTokenAuthenticationProvider(config); const handler = authenticationProvider(false); const interceptor = [handler]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); diff --git a/packages/authentication-adapters/test/basicAuthenticationAdapter.test.ts b/packages/authentication-adapters/test/basicAuthenticationAdapter.test.ts index 5792ec93c..276a7ce97 100644 --- a/packages/authentication-adapters/test/basicAuthenticationAdapter.test.ts +++ b/packages/authentication-adapters/test/basicAuthenticationAdapter.test.ts @@ -59,7 +59,7 @@ describe('test basic authentication scheme', () => { ); const handler = authenticationProvider(enableAuthentication); const interceptor = [handler]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); diff --git a/packages/authentication-adapters/test/compositeAuthenticationAdapter.test.ts b/packages/authentication-adapters/test/compositeAuthenticationAdapter.test.ts index 255850783..cb9aa42a2 100644 --- a/packages/authentication-adapters/test/compositeAuthenticationAdapter.test.ts +++ b/packages/authentication-adapters/test/compositeAuthenticationAdapter.test.ts @@ -74,7 +74,7 @@ describe('test composite authentication adapter with false or empty security req const securityRequirements = false; const provider = compositeAuthenticationProvider(authConfig); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -91,10 +91,10 @@ describe('test composite authentication adapter with false or empty security req method: 'GET', url: 'http://apimatic.hopto.org:3000/test/requestBuilder', }; - const securityRequirements = []; + const securityRequirements: any[] = []; const provider = compositeAuthenticationProvider(authConfig); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -143,7 +143,7 @@ describe('test composite authentication adapter with missing credentials object const securityRequirements = [{ apiKey: true }, { apiHeader: true }]; const provider = compositeAuthenticationProvider(authConfig); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -181,7 +181,7 @@ describe('test composite authentication adapter with missing credentials object const securityRequirements = [{ apiKey: true }, { apiHeader: true }]; const provider = compositeAuthenticationProvider(authConfig); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -220,7 +220,7 @@ describe('test composite authentication adapter with missing credentials object const securityRequirements = [{ apiKey: false }, { apiHeader: true }]; const provider = compositeAuthenticationProvider(authConfig); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -263,7 +263,7 @@ describe('test composite authentication adapter with missing credentials object const securityRequirements = [{ apiKey: true }, { apiHeader: false }]; const provider = compositeAuthenticationProvider(authConfig); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -299,7 +299,7 @@ describe('test composite authentication adapter with missing credentials object const securityRequirements = [{ apiKey: false }, { apiHeader: false }]; const provider = compositeAuthenticationProvider(authConfig); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -342,7 +342,7 @@ describe('test composite authentication adapter with missing credentials object const securityRequirements = [{ apiKey: true, apiHeader: true }]; const provider = compositeAuthenticationProvider(authConfig); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -387,7 +387,7 @@ describe('test composite authentication adapter with missing credentials object const securityRequirements = [{ apiKey: true, apiHeader: false }]; const provider = compositeAuthenticationProvider(authConfig); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -432,7 +432,7 @@ describe('test composite authentication adapter with missing credentials object const securityRequirements = [{ apiKey: false, apiHeader: true }]; const provider = compositeAuthenticationProvider(authConfig); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -473,7 +473,7 @@ describe('test composite authentication adapter with missing credentials object const securityRequirements = [{ apiKey: false, apiHeader: false }]; const provider = compositeAuthenticationProvider(authConfig); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -563,7 +563,7 @@ describe('test composite authentication adapter with security requirements combi const securityRequirements = [{ accessToken: true }, { basicAuth: true }]; const provider = compositeAuthenticationProvider(authConfig); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -582,7 +582,7 @@ describe('test composite authentication adapter with security requirements combi const securityRequirements = [{ accessToken: true }, { basicAuth: false }]; const provider = compositeAuthenticationProvider(authConfig); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -601,7 +601,7 @@ describe('test composite authentication adapter with security requirements combi const securityRequirements = [{ accessToken: false }, { basicAuth: true }]; const provider = compositeAuthenticationProvider(authConfig); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -625,7 +625,7 @@ describe('test composite authentication adapter with security requirements combi ]; const provider = compositeAuthenticationProvider(authConfig); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -648,7 +648,7 @@ describe('test composite authentication adapter with security requirements combi const securityRequirements = [{ accessToken: false, basicAuth: true }]; const provider = compositeAuthenticationProvider(authConfig); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -672,7 +672,7 @@ describe('test composite authentication adapter with security requirements combi const securityRequirements = [{ accessToken: true, basicAuth: true }]; const provider = compositeAuthenticationProvider(authConfig); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -695,7 +695,7 @@ describe('test composite authentication adapter with security requirements combi const securityRequirements = [{ accessToken: true, basicAuth: false }]; const provider = compositeAuthenticationProvider(authConfig); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -721,7 +721,7 @@ describe('test composite authentication adapter with security requirements combi ]; const provider = compositeAuthenticationProvider(authConfig); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -750,7 +750,7 @@ describe('test composite authentication adapter with security requirements combi ]; const provider = compositeAuthenticationProvider(authConfig); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -769,7 +769,7 @@ describe('test composite authentication adapter with security requirements combi const securityRequirements = [{ oAuthACG: true, oAuthCCG: true }]; const provider = compositeAuthenticationProvider(authConfig); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); @@ -811,7 +811,7 @@ describe('test composite authentication adapter with security requirements combi const securityRequirements = [{ oAuthACG: true }, { oAuthCCG: true }]; const provider = compositeAuthenticationProvider(auth1Config); const interceptor = [provider(securityRequirements)]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); diff --git a/packages/authentication-adapters/test/customHeaderAuthenticationAdapter.test.ts b/packages/authentication-adapters/test/customHeaderAuthenticationAdapter.test.ts index 1e0b36661..65d536c00 100644 --- a/packages/authentication-adapters/test/customHeaderAuthenticationAdapter.test.ts +++ b/packages/authentication-adapters/test/customHeaderAuthenticationAdapter.test.ts @@ -63,7 +63,7 @@ describe('test custom header authentication scheme', () => { ); const handler = authenticationProvider(enableAuthentication); const interceptor = [handler]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); diff --git a/packages/authentication-adapters/test/customQueryAuthenticationAdapter.test.ts b/packages/authentication-adapters/test/customQueryAuthenticationAdapter.test.ts index a28f14eb2..b2c045342 100644 --- a/packages/authentication-adapters/test/customQueryAuthenticationAdapter.test.ts +++ b/packages/authentication-adapters/test/customQueryAuthenticationAdapter.test.ts @@ -62,7 +62,7 @@ describe('test custom query authentication scheme', () => { ); const handler = authenticationProvider(enableAuthentication); const interceptor = [handler]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); diff --git a/packages/authentication-adapters/test/noAuthenticationAdapter.test.ts b/packages/authentication-adapters/test/noAuthenticationAdapter.test.ts index 258e8bd1c..f3dcdd124 100644 --- a/packages/authentication-adapters/test/noAuthenticationAdapter.test.ts +++ b/packages/authentication-adapters/test/noAuthenticationAdapter.test.ts @@ -18,7 +18,7 @@ describe('test access token authentication scheme', () => { it('should test access token auth with enabled authentication', async () => { const handler = noneAuthenticationProvider(); const interceptor = [handler]; - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response }; }; const executor = callHttpInterceptors(interceptor, client); diff --git a/packages/core/src/http/httpInterceptor.ts b/packages/core/src/http/httpInterceptor.ts index a24f535c5..b8439e778 100644 --- a/packages/core/src/http/httpInterceptor.ts +++ b/packages/core/src/http/httpInterceptor.ts @@ -14,5 +14,7 @@ export function callHttpInterceptors( client: HttpCallExecutor ): HttpCallExecutor { return (request, options) => + // @ts-ignore TODO: REMOVE THIS COMMENT. DO NOT COPY. + // THIS WAS ONLY FOR MIGRATING UNCHECKED CODE TO STRICT TYPE CHECKING. combineHttpInterceptors(interceptors)(request, options, client); } diff --git a/packages/core/src/http/requestBuilder.ts b/packages/core/src/http/requestBuilder.ts index 550684a5e..08ef7e811 100644 --- a/packages/core/src/http/requestBuilder.ts +++ b/packages/core/src/http/requestBuilder.ts @@ -175,10 +175,13 @@ export interface RequestBuilder { ): RequestBuilder; paginate( createPagedIterable: ( - req: this, + req: RequestBuilder, updater: ( - req: this - ) => (pointer: string | null, setter: (value: any) => any) => this + req: RequestBuilder + ) => ( + pointer: string | null, + setter: (value: any) => any + ) => RequestBuilder ) => PagedAsyncIterable ): PagedAsyncIterable; call(requestOptions?: RequestOptions): Promise>; @@ -555,10 +558,13 @@ export class DefaultRequestBuilder } public paginate( createPagedIterable: ( - req: this, + req: RequestBuilder, updater: ( - req: this - ) => (pointer: string | null, setter: (value: any) => any) => this + req: RequestBuilder + ) => ( + pointer: string | null, + setter: (value: any) => any + ) => RequestBuilder ) => PagedAsyncIterable ): PagedAsyncIterable { return createPagedIterable(this, (req) => diff --git a/packages/core/test/http/apiLogger.test.ts b/packages/core/test/http/apiLogger.test.ts index 6c3985b0e..84dafa232 100644 --- a/packages/core/test/http/apiLogger.test.ts +++ b/packages/core/test/http/apiLogger.test.ts @@ -9,7 +9,10 @@ import { callHttpInterceptors } from '../../src/http/httpInterceptor'; import { NullLogger } from '../../src/logger/nullLogger'; import { mergeLoggingOptions } from '../../src/logger/defaultLoggingConfiguration'; -let loggerSpy; +let loggerSpy: jest.SpyInstance< + void, + [message?: any, ...optionalParams: any[]] +>; beforeEach(() => { // Reset the spy on console.log() before each test loggerSpy = jest.spyOn(console, 'log').mockImplementation(); @@ -304,7 +307,11 @@ describe('APILogger with NullLogging', () => { function mockInterceptor(loggingOpt: LoggingOptions) { const apiLogger = new ApiLogger(loggingOpt); - return async (req, options, next) => { + return async ( + req: HttpRequest, + options: any, + next: (arg0: any, arg1: any) => any + ) => { apiLogger.logRequest(req); const context = await next(req, options); apiLogger.logResponse(context.response); @@ -344,14 +351,18 @@ function mockResponse(): HttpResponse { } async function mockClient(loggingOpts: LoggingOptions) { - const client = async (req) => { + const client = async (req: HttpRequest) => { return { request: req, response: mockResponse() }; }; const executor = callHttpInterceptors([mockInterceptor(loggingOpts)], client); return await executor(mockRequest(), undefined); } -function expectLogsToBeLogged(logSpy, expectedConsoleLogs, index = 0) { +function expectLogsToBeLogged( + logSpy: jest.SpyInstance, + expectedConsoleLogs: string | any[], + index = 0 +) { for (let i = index; i < expectedConsoleLogs.length; i++) { expect(logSpy.mock.calls[i][0]).toEqual(expectedConsoleLogs[i]); } diff --git a/packages/schema/src/typeUtils.ts b/packages/schema/src/typeUtils.ts index ebde3aaca..640bd681e 100644 --- a/packages/schema/src/typeUtils.ts +++ b/packages/schema/src/typeUtils.ts @@ -4,6 +4,8 @@ * Some of these have been picked up from the superstruct library. */ +import { Schema } from './schema'; + /** * Type helper to Flatten the Union of optional and required properties. */ @@ -30,3 +32,48 @@ type RequiredKeys = { export type OptionalizeObject = Flatten< { [K in RequiredKeys]: T[K] } & { [K in OptionalKeys]?: T[K] } >; + +type SchemaType> = T extends Schema + ? U + : never; + +export type ArraySchemaType< + T extends Array> +> = T[number] extends Schema ? SchemaType : never; + +/** + * Type helper to work with schemas of a discriminated oneOf or anyOf type + */ +export type DiscriminatorMap>> = { + [K in ArraySchemaType]?: Schema>; +}; + +type ValueOf = T[keyof T]; + +/** + * Check a value's discriminator field and get its corresponding schema + */ +export function getDiscriminatedSchema>>( + value: unknown, + discriminatorMap: DiscriminatorMap, + discriminatorField: string, + useTypeOfCheck: boolean = true +): ValueOf> | false { + const discriminatorValue = + value && + (useTypeOfCheck ? typeof value === 'object' : true) && + (value as Record)[discriminatorField]; + + if (!discriminatorValue) { + return false; + } + + const schema = + discriminatorMap[discriminatorValue as keyof DiscriminatorMap]; + + if (schema) { + return schema; + } + + return false; +} diff --git a/packages/schema/src/types/anyOf.ts b/packages/schema/src/types/anyOf.ts index 78ca3130c..eb4846217 100644 --- a/packages/schema/src/types/anyOf.ts +++ b/packages/schema/src/types/anyOf.ts @@ -1,16 +1,9 @@ import { Schema, SchemaContextCreator } from '../schema'; - -type SchemaType> = T extends Schema - ? U - : never; - -type ArraySchemaType< - T extends Array> -> = T[number] extends Schema ? SchemaType : never; - -type DiscriminatorMap>> = { - [K in ArraySchemaType]?: Schema>; -}; +import { + ArraySchemaType, + DiscriminatorMap, + getDiscriminatedSchema, +} from '../typeUtils'; export function anyOf>>( schemas: [...T], @@ -36,63 +29,83 @@ function createAnyOfWithDiscriminator>>( return { type: () => `OneOf<${schemas.map((schema) => schema.type()).join(' | ')}>`, validateBeforeMap: (value, ctxt) => { - const discriminatorValue = - value && typeof value === 'object' && value[discriminatorField]; - if (discriminatorValue && discriminatorMap[discriminatorValue]) { - return discriminatorMap[discriminatorValue].validateBeforeMap( - value, - ctxt - ); + const discriminatedSchema = getDiscriminatedSchema( + value, + discriminatorMap, + discriminatorField + ); + if (discriminatedSchema) { + return discriminatedSchema.validateBeforeMap(value, ctxt); } return matchAndValidateBeforeMap(schemas, value, ctxt); }, validateBeforeUnmap: (value, ctxt) => { - const discriminatorValue = - value && typeof value === 'object' && value[discriminatorField]; - if (discriminatorValue && discriminatorMap[discriminatorValue]) { - return discriminatorMap[discriminatorValue].validateBeforeUnmap( - value, - ctxt - ); + const discriminatedSchema = getDiscriminatedSchema( + value, + discriminatorMap, + discriminatorField + ); + if (discriminatedSchema) { + return discriminatedSchema.validateBeforeUnmap(value, ctxt); } return matchAndValidateBeforeUnmap(schemas, value, ctxt); }, map: (value, ctxt) => { - const discriminatorValue = value && value[discriminatorField]; - if (discriminatorValue && discriminatorMap[discriminatorValue]) { - return discriminatorMap[discriminatorValue].map(value, ctxt); + const discriminatedSchema = getDiscriminatedSchema( + value, + discriminatorMap, + discriminatorField, + false + ); + if (discriminatedSchema) { + return discriminatedSchema.map(value, ctxt); } return matchAndMap(schemas, value, ctxt); }, unmap: (value, ctxt) => { - const discriminatorValue = value && value[discriminatorField]; - if (discriminatorValue && discriminatorMap[discriminatorValue]) { - return discriminatorMap[discriminatorValue].unmap(value, ctxt); + const discriminatedSchema = getDiscriminatedSchema( + value, + discriminatorMap, + discriminatorField, + false + ); + if (discriminatedSchema) { + return discriminatedSchema.unmap(value, ctxt); } return matchAndUnmap(schemas, value, ctxt); }, validateBeforeMapXml: (value, ctxt) => { - const discriminatorValue = - value && typeof value === 'object' && value[discriminatorField]; - if (discriminatorValue && discriminatorMap[discriminatorValue]) { - return discriminatorMap[discriminatorValue].validateBeforeMapXml( - value, - ctxt - ); + const discriminatedSchema = getDiscriminatedSchema( + value, + discriminatorMap, + discriminatorField + ); + if (discriminatedSchema) { + return discriminatedSchema.validateBeforeMapXml(value, ctxt); } return matchAndValidateBeforeMapXml(schemas, value, ctxt); }, mapXml: (value, ctxt) => { - const discriminatorValue = value && value[discriminatorField]; - if (discriminatorValue && discriminatorMap[discriminatorValue]) { - return discriminatorMap[discriminatorValue].mapXml(value, ctxt); + const discriminatedSchema = getDiscriminatedSchema( + value, + discriminatorMap, + discriminatorField, + false + ); + if (discriminatedSchema) { + return discriminatedSchema.mapXml(value, ctxt); } return matchAndMapXml(schemas, value, ctxt); }, unmapXml: (value, ctxt) => { - const discriminatorValue = value && value[discriminatorField]; - if (discriminatorValue && discriminatorMap[discriminatorValue]) { - return discriminatorMap[discriminatorValue].unmapXml(value, ctxt); + const discriminatedSchema = getDiscriminatedSchema( + value, + discriminatorMap, + discriminatorField, + false + ); + if (discriminatedSchema) { + return discriminatedSchema.unmapXml(value, ctxt); } return matchAndUnmapXml(schemas, value, ctxt); }, diff --git a/packages/schema/src/types/bigint.ts b/packages/schema/src/types/bigint.ts index 9a3580bd5..e28a3306f 100644 --- a/packages/schema/src/types/bigint.ts +++ b/packages/schema/src/types/bigint.ts @@ -5,7 +5,7 @@ import { toValidator, } from '../utils'; -function isValidBigIntValue(value: unknown, strict: boolean): value is bigint { +function isValidBigIntValue(value: unknown, strict?: boolean): value is bigint { return strict ? typeof value === 'bigint' : typeof value === 'bigint' || diff --git a/packages/schema/src/types/boolean.ts b/packages/schema/src/types/boolean.ts index cf1d148cb..d1ef744ae 100644 --- a/packages/schema/src/types/boolean.ts +++ b/packages/schema/src/types/boolean.ts @@ -1,7 +1,7 @@ import { Schema } from '../schema'; import { createSymmetricSchema, toValidator } from '../utils'; -function isValidBooleanValue(value: unknown, strict: boolean): boolean { +function isValidBooleanValue(value: unknown, strict?: boolean): boolean { return strict ? typeof value === 'boolean' : typeof value === 'boolean' || diff --git a/packages/schema/src/types/numberEnum.ts b/packages/schema/src/types/numberEnum.ts index 83e556534..a6bdf4c54 100644 --- a/packages/schema/src/types/numberEnum.ts +++ b/packages/schema/src/types/numberEnum.ts @@ -36,6 +36,8 @@ export function numberEnum( type: `Enum<${Object.values(enumVariable) .filter((v) => typeof v === 'number') .join(',')}>`, + // TODO: The correct return type should be TEnumValue | number + // when allowForUnknownProps is true but that would be a breaking change. map: coerceNumericStringToNumber as (value: TEnumValue) => TEnumValue, validate, }); diff --git a/packages/schema/src/types/object.ts b/packages/schema/src/types/object.ts index 172438c0e..75cd4167d 100644 --- a/packages/schema/src/types/object.ts +++ b/packages/schema/src/types/object.ts @@ -25,6 +25,14 @@ type AnyObjectSchema = Record< [string, Schema, ObjectXmlOptions?] >; +/** + * Type for dynamically calling validate methods + */ +type SchemaValidationMethods = keyof Pick< + Schema, + 'validateBeforeMap' | 'validateBeforeUnmap' | 'validateBeforeMapXml' +>; + type AllValues = { [P in keyof T]: { key: P; value: T[P][0]; schema: T[P][1] }; }[keyof T]; @@ -264,7 +272,7 @@ function validateObjectBeforeMapXml( const { $: attrs, ...elements } = valueObject; let validationObj = { - validationMethod: 'validateBeforeMapXml', + validationMethod: 'validateBeforeMapXml' as const, propTypeName: 'child elements', propTypePrefix: 'element', valueTypeName: 'element', @@ -388,7 +396,7 @@ function validateValueObject({ skipAdditionalPropValidation, mapAdditionalProps, }: { - validationMethod: string; + validationMethod: SchemaValidationMethods; propTypeName: string; propTypePrefix: string; valueTypeName: string; diff --git a/packages/schema/src/types/oneOf.ts b/packages/schema/src/types/oneOf.ts index 858430e2b..a80fcb19c 100644 --- a/packages/schema/src/types/oneOf.ts +++ b/packages/schema/src/types/oneOf.ts @@ -1,16 +1,9 @@ import { Schema, SchemaContextCreator } from '../schema'; - -type SchemaType> = T extends Schema - ? U - : never; - -type ArraySchemaType< - T extends Array> -> = T[number] extends Schema ? SchemaType : never; - -type DiscriminatorMap>> = { - [K in ArraySchemaType]?: Schema>; -}; +import { + ArraySchemaType, + DiscriminatorMap, + getDiscriminatedSchema, +} from '../typeUtils'; export function oneOf>>( schemas: [...T], @@ -36,63 +29,83 @@ function createOneOfWithDiscriminator>>( return { type: () => `OneOf<${schemas.map((schema) => schema.type()).join(' | ')}>`, validateBeforeMap: (value, ctxt) => { - const discriminatorValue = - value && typeof value === 'object' && value[discriminatorField]; - if (discriminatorValue && discriminatorMap[discriminatorValue]) { - return discriminatorMap[discriminatorValue].validateBeforeMap( - value, - ctxt - ); + const discriminatedSchema = getDiscriminatedSchema( + value, + discriminatorMap, + discriminatorField + ); + if (discriminatedSchema) { + return discriminatedSchema.validateBeforeMap(value, ctxt); } return matchAndValidateBeforeMap(schemas, value, ctxt); }, validateBeforeUnmap: (value, ctxt) => { - const discriminatorValue = - value && typeof value === 'object' && value[discriminatorField]; - if (discriminatorValue && discriminatorMap[discriminatorValue]) { - return discriminatorMap[discriminatorValue].validateBeforeUnmap( - value, - ctxt - ); + const discriminatedSchema = getDiscriminatedSchema( + value, + discriminatorMap, + discriminatorField + ); + if (discriminatedSchema) { + return discriminatedSchema.validateBeforeUnmap(value, ctxt); } return matchAndValidateBeforeUnmap(schemas, value, ctxt); }, map: (value, ctxt) => { - const discriminatorValue = value && value[discriminatorField]; - if (discriminatorValue && discriminatorMap[discriminatorValue]) { - return discriminatorMap[discriminatorValue].map(value, ctxt); + const discriminatedSchema = getDiscriminatedSchema( + value, + discriminatorMap, + discriminatorField, + false + ); + if (discriminatedSchema) { + return discriminatedSchema.map(value, ctxt); } return matchAndMap(schemas, value, ctxt); }, unmap: (value, ctxt) => { - const discriminatorValue = value && value[discriminatorField]; - if (discriminatorValue && discriminatorMap[discriminatorValue]) { - return discriminatorMap[discriminatorValue].unmap(value, ctxt); + const discriminatedSchema = getDiscriminatedSchema( + value, + discriminatorMap, + discriminatorField, + false + ); + if (discriminatedSchema) { + return discriminatedSchema.unmap(value, ctxt); } return matchAndUnmap(schemas, value, ctxt); }, validateBeforeMapXml: (value, ctxt) => { - const discriminatorValue = - value && typeof value === 'object' && value[discriminatorField]; - if (discriminatorValue && discriminatorMap[discriminatorValue]) { - return discriminatorMap[discriminatorValue].validateBeforeMapXml( - value, - ctxt - ); + const discriminatedSchema = getDiscriminatedSchema( + value, + discriminatorMap, + discriminatorField + ); + if (discriminatedSchema) { + return discriminatedSchema.validateBeforeMapXml(value, ctxt); } return matchAndValidateBeforeMapXml(schemas, value, ctxt); }, mapXml: (value, ctxt) => { - const discriminatorValue = value && value[discriminatorField]; - if (discriminatorValue && discriminatorMap[discriminatorValue]) { - return discriminatorMap[discriminatorValue].mapXml(value, ctxt); + const discriminatedSchema = getDiscriminatedSchema( + value, + discriminatorMap, + discriminatorField, + false + ); + if (discriminatedSchema) { + return discriminatedSchema.mapXml(value, ctxt); } return matchAndMapXml(schemas, value, ctxt); }, unmapXml: (value, ctxt) => { - const discriminatorValue = value && value[discriminatorField]; - if (discriminatorValue && discriminatorMap[discriminatorValue]) { - return discriminatorMap[discriminatorValue].unmapXml(value, ctxt); + const discriminatedSchema = getDiscriminatedSchema( + value, + discriminatorMap, + discriminatorField, + false + ); + if (discriminatedSchema) { + return discriminatedSchema.unmapXml(value, ctxt); } return matchAndUnmapXml(schemas, value, ctxt); }, diff --git a/packages/schema/src/utils.ts b/packages/schema/src/utils.ts index f813f3413..f10e17358 100644 --- a/packages/schema/src/utils.ts +++ b/packages/schema/src/utils.ts @@ -103,7 +103,9 @@ export function isNumericString( (typeof value === 'string' && !isNaN(value as any)); } -export function coerceNumericStringToNumber(value: number | string): number { +export function coerceNumericStringToNumber( + value: T | string +): number { return typeof value === 'number' ? value : +value; } diff --git a/packages/schema/test/types/numberEnum.test.ts b/packages/schema/test/types/numberEnum.test.ts new file mode 100644 index 000000000..11aeda862 --- /dev/null +++ b/packages/schema/test/types/numberEnum.test.ts @@ -0,0 +1,57 @@ +import { numberEnum, validateAndMap } from '../../src'; + +describe('Number Enum', () => { + enum SampleNumberEnum { + Hearts = 1, + Spades, + Clubs, + Diamonds, + } + describe('Mapping', () => { + it('should map known number to enum member', () => { + const input = 3; + const output = validateAndMap(input as any, numberEnum(SampleNumberEnum)); + expect(output.errors).toBeFalsy(); + expect((output as any).result).toBe(SampleNumberEnum.Clubs); + }); + + it('should fail for unknown number', () => { + const input = 5; + const output = validateAndMap(input as any, numberEnum(SampleNumberEnum)); + + expect(output.errors).toBeTruthy(); + expect(output.errors).toMatchInlineSnapshot(` + Array [ + Object { + "branch": Array [ + 5, + ], + "message": "Expected value to be of type 'Enum<1,2,3,4>' but found 'number'. + + Given value: 5 + Type: 'number' + Expected type: 'Enum<1,2,3,4>'", + "path": Array [], + "type": "Enum<1,2,3,4>", + "value": 5, + }, + ] + `); + }); + + it('should map unknown number when allowing unknown props', () => { + const input: SampleNumberEnum | number = 5; + const output = validateAndMap( + input as any, + numberEnum(SampleNumberEnum, true) + ); + + expect(output.errors).toBeFalsy(); + if (output.errors) { + throw new Error('This check is for type narrowing.'); + } + + expect(output.result).toBe(5); + }); + }); +}); diff --git a/packages/test-utilities/src/assertionUtils.ts b/packages/test-utilities/src/assertionUtils.ts index 028e7ccad..9b5969ccb 100644 --- a/packages/test-utilities/src/assertionUtils.ts +++ b/packages/test-utilities/src/assertionUtils.ts @@ -119,6 +119,7 @@ function checkObjects( // Check if right object keys contains this key from left object. expect(rightObjKeys).toContainEqual(key); // Recursive checking for each element in left and right object. + // @ts-expect-error NOTE: We already checked that both left and right objects contain the key. checkIfMatching(left[key], right[key], isOrdered, checkValues); }); } diff --git a/packages/test-utilities/test/assertionUtils.test.ts b/packages/test-utilities/test/assertionUtils.test.ts index 5d694c4b6..1797f6a22 100644 --- a/packages/test-utilities/test/assertionUtils.test.ts +++ b/packages/test-utilities/test/assertionUtils.test.ts @@ -281,7 +281,7 @@ describe('expectMatchingWithOptions', () => { }); it('array: should pass when expected is empty', () => { - const exp = []; + const exp: never[] = []; const actl = [1, 2, 3, 4]; const opts: ExpectOptions = { isOrdered: true, @@ -292,8 +292,8 @@ describe('expectMatchingWithOptions', () => { }); it('array: should pass when both expected and actual are empty', () => { - const exp = []; - const actl = []; + const exp: never[] = []; + const actl: never[] = []; const opts: ExpectOptions = { isOrdered: true, allowExtra: true, diff --git a/tsconfig.base.json b/tsconfig.base.json index a1892d536..fd0c196a0 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -19,6 +19,9 @@ "importHelpers": true, "paths": { "tslib": ["./node_modules/tslib/tslib.d.ts"] - } + }, + "forceConsistentCasingInFileNames": true, + "strict": true, + "useUnknownInCatchVariables": false } }