From bd41aa5d12c7941ceef0a3de85934bcaa5a50623 Mon Sep 17 00:00:00 2001 From: Josh Gummersall Date: Wed, 7 Jul 2021 11:07:28 -0700 Subject: [PATCH] chore: replace stdlib assertions with zod Fixes #3670 --- libraries/botbuilder-ai/package.json | 1 - libraries/botbuilder-ai/src/qnaMakerDialog.ts | 26 +- .../tests/qnaMakerDialog.test.js | 15 +- .../src/blobsTranscriptStore.ts | 2 +- .../etc/botbuilder-core.api.md | 15 +- libraries/botbuilder-core/src/botComponent.ts | 19 +- libraries/botbuilder-core/src/storage.ts | 26 +- .../src/index.ts | 8 +- ...botbuilder-dialogs-adaptive-testing.api.md | 2 +- .../package.json | 3 +- .../src/testActions/userActivity.ts | 9 +- .../package.json | 3 +- .../src/componentDeclarativeTypes.ts | 9 +- .../src/resources/resourceExplorer.ts | 2 +- libraries/botbuilder-dialogs/package.json | 1 - .../src/memory/componentMemoryScopes.ts | 8 +- .../src/memory/componentPathResolvers.ts | 9 +- libraries/botbuilder-stdlib/src/assertExt.ts | 20 - libraries/botbuilder-stdlib/src/index.ts | 3 +- libraries/botbuilder-stdlib/src/stringExt.ts | 14 + libraries/botbuilder-stdlib/src/types.ts | 743 ------------------ .../botbuilder-stdlib/tests/stringExt.test.js | 25 + .../botbuilder-stdlib/tests/types.test.js | 207 ----- .../botbuilder/src/setSpeakMiddleware.ts | 12 +- .../teams/teamsSSOTokenExchangeMiddleware.ts | 13 +- libraries/botframework-connector/package.json | 3 +- .../auth/botFrameworkAuthenticationFactory.ts | 26 +- .../src/auth/botFrameworkClientImpl.ts | 47 +- .../passwordServiceClientCredentialFactory.ts | 8 +- .../src/auth/userTokenClient.ts | 15 +- .../src/auth/userTokenClientImpl.ts | 72 +- .../tests/auth/userTokenClient.test.js | 15 +- .../etc/botframework-schema.api.md | 147 ++-- libraries/botframework-schema/package.json | 4 +- libraries/botframework-schema/src/index.ts | 416 ++++++---- testing/botbuilder-test-utils/package.json | 4 +- testing/botbuilder-test-utils/src/jwt.ts | 10 +- 37 files changed, 645 insertions(+), 1317 deletions(-) delete mode 100644 libraries/botbuilder-stdlib/src/assertExt.ts create mode 100644 libraries/botbuilder-stdlib/src/stringExt.ts create mode 100644 libraries/botbuilder-stdlib/tests/stringExt.test.js delete mode 100644 libraries/botbuilder-stdlib/tests/types.test.js diff --git a/libraries/botbuilder-ai/package.json b/libraries/botbuilder-ai/package.json index 3ff0a8eeab..994976aa8c 100644 --- a/libraries/botbuilder-ai/package.json +++ b/libraries/botbuilder-ai/package.json @@ -34,7 +34,6 @@ "botbuilder-dialogs": "4.1.6", "botbuilder-dialogs-adaptive-runtime-core": "4.1.6", "botbuilder-dialogs-declarative": "4.1.6", - "botbuilder-stdlib": "4.1.6", "lodash": "^4.17.21", "node-fetch": "^2.6.0", "url-parse": "^1.5.1", diff --git a/libraries/botbuilder-ai/src/qnaMakerDialog.ts b/libraries/botbuilder-ai/src/qnaMakerDialog.ts index dcbda409f6..53807a8287 100644 --- a/libraries/botbuilder-ai/src/qnaMakerDialog.ts +++ b/libraries/botbuilder-ai/src/qnaMakerDialog.ts @@ -5,6 +5,14 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ + +import * as z from 'zod'; +import { ActiveLearningUtils, BindToActivity } from './qnamaker-utils'; +import { Activity, ActivityTypes, MessageFactory } from 'botbuilder-core'; +import { QnACardBuilder } from './qnaCardBuilder'; +import { QnAMaker, QnAMakerResult } from './'; +import { QnAMakerClient, QnAMakerClientKey } from './qnaMaker'; + import { ArrayExpression, ArrayExpressionConverter, @@ -20,7 +28,7 @@ import { StringExpression, StringExpressionConverter, } from 'adaptive-expressions'; -import { Activity, ActivityTypes, MessageFactory } from 'botbuilder-core'; + import { Converter, ConverterFactory, @@ -36,10 +44,7 @@ import { TurnPath, WaterfallStepContext, } from 'botbuilder-dialogs'; -import { assert, Test, tests } from 'botbuilder-stdlib'; -import { QnAMaker, QnAMakerResult } from './'; -import { QnACardBuilder } from './qnaCardBuilder'; -import { QnAMakerClient, QnAMakerClientKey } from './qnaMaker'; + import { FeedbackRecord, FeedbackRecords, @@ -48,7 +53,6 @@ import { QnAMakerOptions, RankerTypes, } from './qnamaker-interfaces'; -import { ActiveLearningUtils, BindToActivity } from './qnamaker-utils'; class QnAMakerDialogActivityConverter implements Converter, DialogStateManager>> { @@ -124,9 +128,9 @@ export interface QnAMakerDialogConfiguration extends DialogConfiguration { */ export type QnASuggestionsActivityFactory = (suggestionsList: string[], noMatchesText: string) => Partial; -const isSuggestionsFactory: Test = (val): val is QnASuggestionsActivityFactory => { - return tests.isFunc(val); -}; +const qnaSuggestionsActivityFactory = z.custom((val) => typeof val === 'function', { + message: 'QnASuggestionsActivityFactory', +}); /** * A dialog that supports multi-step and adaptive-learning QnA Maker services. @@ -375,7 +379,7 @@ export class QnAMakerDialog extends WaterfallDialog implements QnAMakerDialogCon if (top) { this.top = new IntExpression(top); } - if (isSuggestionsFactory(activeLearningTitleOrFactory)) { + if (qnaSuggestionsActivityFactory.check(activeLearningTitleOrFactory)) { if (!cardNoMatchText) { // Without a developer-provided cardNoMatchText, the end user will not be able to tell the convey to the bot and QnA Maker that the suggested alternative questions were not correct. // When the user's reply to a suggested alternatives Activity matches the cardNoMatchText, the QnAMakerDialog sends this information to the QnA Maker service for active learning. @@ -710,7 +714,7 @@ export class QnAMakerDialog extends WaterfallDialog implements QnAMakerDialogCon dialogOptions.qnaDialogResponseOptions.cardNoMatchText ); - assert.object(message, ['suggestionsActivity']); + z.record(z.unknown()).parse(message, { path: ['message'] }); await step.context.sendActivity(message); step.activeDialog.state[this.options] = dialogOptions; diff --git a/libraries/botbuilder-ai/tests/qnaMakerDialog.test.js b/libraries/botbuilder-ai/tests/qnaMakerDialog.test.js index e880633dc0..ccf33e12e2 100644 --- a/libraries/botbuilder-ai/tests/qnaMakerDialog.test.js +++ b/libraries/botbuilder-ai/tests/qnaMakerDialog.test.js @@ -1,9 +1,12 @@ const fs = require('fs'); const nock = require('nock'); const sinon = require('sinon'); -const { ok, rejects, strictEqual, throws } = require('assert'); -const { join } = require('path'); const { BoolExpression } = require('adaptive-expressions'); +const { Dialog, DialogSet, DialogTurnStatus, DialogManager, ScopePath } = require('botbuilder-dialogs'); +const { QnAMakerDialog, QnAMaker, QnACardBuilder } = require('..'); +const { join } = require('path'); +const { ok, rejects, strictEqual, throws } = require('assert'); + const { ActionTypes, ActivityTypes, @@ -13,8 +16,6 @@ const { MessageFactory, TestAdapter, } = require('botbuilder-core'); -const { Dialog, DialogSet, DialogTurnStatus, DialogManager, ScopePath } = require('botbuilder-dialogs'); -const { QnAMakerDialog, QnAMaker, QnACardBuilder } = require('../lib'); const KB_ID = process.env.QNAKNOWLEDGEBASEID; const ENDPOINT_KEY = process.env.QNAENDPOINTKEY; @@ -439,7 +440,9 @@ describe('QnAMakerDialog', function () { await rejects( adapter.send('QnaMaker_TopNAnswer.json').startTest(), - (thrown) => thrown.message === '`suggestionsActivity` must be of type "object"' + (thrown) => + thrown.message.includes('invalid_type at message') && + thrown.message.includes('Expected object, received number') ); }); @@ -477,7 +480,7 @@ describe('QnAMakerDialog', function () { await rejects( adapter.send('QnaMaker_TopNAnswer.json').startTest(), - (thrown) => thrown.message === '`suggestionsActivity` must be defined' + (thrown) => thrown.message.includes('invalid_type at message') && thrown.message.includes('Required') ); sandbox.verify(); diff --git a/libraries/botbuilder-azure-blobs/src/blobsTranscriptStore.ts b/libraries/botbuilder-azure-blobs/src/blobsTranscriptStore.ts index 7f9b0e1a02..fd02407a64 100644 --- a/libraries/botbuilder-azure-blobs/src/blobsTranscriptStore.ts +++ b/libraries/botbuilder-azure-blobs/src/blobsTranscriptStore.ts @@ -5,7 +5,7 @@ import * as z from 'zod'; import getStream from 'get-stream'; import pmap from 'p-map'; import { Activity, PagedResult, TranscriptInfo, TranscriptStore } from 'botbuilder-core'; -import { maybeCast } from 'botbuilder-stdlib/lib/maybeCast'; +import { maybeCast } from 'botbuilder-stdlib'; import { sanitizeBlobKey } from './sanitizeBlobKey'; import { diff --git a/libraries/botbuilder-core/etc/botbuilder-core.api.md b/libraries/botbuilder-core/etc/botbuilder-core.api.md index b23b1478c2..e31e0fa998 100644 --- a/libraries/botbuilder-core/etc/botbuilder-core.api.md +++ b/libraries/botbuilder-core/etc/botbuilder-core.api.md @@ -8,7 +8,6 @@ import { Activity } from 'botframework-schema'; import { AdaptiveCardInvokeResponse } from 'botframework-schema'; import { AdaptiveCardInvokeValue } from 'botframework-schema'; import { AnimationCard } from 'botframework-schema'; -import { Assertion } from 'botbuilder-stdlib'; import { Attachment } from 'botframework-schema'; import { AudioCard } from 'botframework-schema'; import { AuthenticateRequestResult } from 'botframework-connector'; @@ -128,11 +127,15 @@ export class ActivityHandlerBase { run(context: TurnContext): Promise; } -// @public (undocumented) -export const assertBotComponent: Assertion; +// Warning: (ae-internal-missing-underscore) The name "assertBotComponent" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal @deprecated (undocumented) +export function assertBotComponent(val: unknown, ..._args: unknown[]): asserts val is BotComponent; -// @public (undocumented) -export const assertStoreItems: Assertion; +// Warning: (ae-internal-missing-underscore) The name "assertStoreItems" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal @deprecated (undocumented) +export function assertStoreItems(val: unknown, ..._args: unknown[]): asserts val is StoreItem; // @public export class AutoSaveStateMiddleware implements Middleware { @@ -174,6 +177,8 @@ export const BotCallbackHandlerKey = "botCallbackHandler"; export abstract class BotComponent { // (undocumented) abstract configureServices(services: ServiceCollection, configuration: Configuration): void; + // (undocumented) + static z: z.ZodType; } export { BotFrameworkClient } diff --git a/libraries/botbuilder-core/src/botComponent.ts b/libraries/botbuilder-core/src/botComponent.ts index 45d2c40a51..3273bfc184 100644 --- a/libraries/botbuilder-core/src/botComponent.ts +++ b/libraries/botbuilder-core/src/botComponent.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { Assertion, assert } from 'botbuilder-stdlib'; +import * as z from 'zod'; import { Configuration, ServiceCollection } from 'botbuilder-dialogs-adaptive-runtime-core'; /** @@ -12,10 +12,19 @@ import { Configuration, ServiceCollection } from 'botbuilder-dialogs-adaptive-ru * gets called automatically on the components by the bot runtime, as long as the components are registered in the configuration. */ export abstract class BotComponent { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static z = z.custom((val: any) => typeof val.configureServices === 'function', { + message: 'BotComponent', + }); + abstract configureServices(services: ServiceCollection, configuration: Configuration): void; } -export const assertBotComponent: Assertion = (val, path) => { - assert.unsafe.castObjectAs(val, path); - assert.func(val.configureServices, path.concat('configureServices')); -}; +/** + * @internal + * + * @deprecated Use `BotComponent.z.parse()` instead. + */ +export function assertBotComponent(val: unknown, ..._args: unknown[]): asserts val is BotComponent { + BotComponent.z.parse(val); +} diff --git a/libraries/botbuilder-core/src/storage.ts b/libraries/botbuilder-core/src/storage.ts index 8eff47da55..bafa544466 100644 --- a/libraries/botbuilder-core/src/storage.ts +++ b/libraries/botbuilder-core/src/storage.ts @@ -5,8 +5,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ + +import * as z from 'zod'; import { TurnContext } from './turnContext'; -import { Assertion, assert } from 'botbuilder-stdlib'; /** * Callback to calculate a storage key. @@ -14,7 +15,9 @@ import { Assertion, assert } from 'botbuilder-stdlib'; * ```TypeScript * type StorageKeyFactory = (context: TurnContext) => Promise; * ``` + * * @param StorageKeyFactory.context Context for the current turn of conversation with a user. + * @returns A promise resolving to the storage key string */ export type StorageKeyFactory = (context: TurnContext) => Promise; @@ -71,7 +74,7 @@ export interface StoreItem { /** * Key/value pairs. */ - [key: string]: any; + [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any /** * (Optional) eTag field for stores that support optimistic concurrency. @@ -86,12 +89,19 @@ export interface StoreItems { /** * List of store items indexed by key. */ - [key: string]: any; + [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any } -export const assertStoreItems: Assertion = (val, path) => { - assert.object(val, path); -}; +const storeItems = z.record(z.unknown()); + +/** + * @internal + * + * @deprecated Use `zod.record(zod.unknown())` instead. + */ +export function assertStoreItems(val: unknown, ..._args: unknown[]): asserts val is StoreItem { + storeItems.parse(val); +} /** * Utility function to calculate a change hash for a `StoreItem`. @@ -112,10 +122,12 @@ export const assertStoreItems: Assertion = (val, path) => { * await storage.write({ 'botState': state }); * } * ``` + * * @param item Item to calculate the change hash for. + * @returns change hash string */ export function calculateChangeHash(item: StoreItem): string { - const cpy: any = { ...item }; + const cpy = { ...item }; if (cpy.eTag) { delete cpy.eTag; } diff --git a/libraries/botbuilder-dialogs-adaptive-runtime/src/index.ts b/libraries/botbuilder-dialogs-adaptive-runtime/src/index.ts index 790b6bc027..e47b9db13b 100644 --- a/libraries/botbuilder-dialogs-adaptive-runtime/src/index.ts +++ b/libraries/botbuilder-dialogs-adaptive-runtime/src/index.ts @@ -53,7 +53,6 @@ import { TelemetryLoggerMiddleware, TranscriptLoggerMiddleware, UserState, - assertBotComponent, createBotFrameworkAuthenticationFromConfiguration, } from 'botbuilder'; @@ -390,9 +389,12 @@ async function addSettingsBotComponents(services: ServiceCollection, configurati } const instance = new DefaultExport(); - assertBotComponent(instance, [`import(${name})`, 'default']); - return instance; + try { + return BotComponent.z.parse(instance); + } catch (err) { + throw new Error(`${name} does not extend BotComponent: ${err}`); + } }; const components = diff --git a/libraries/botbuilder-dialogs-adaptive-testing/etc/botbuilder-dialogs-adaptive-testing.api.md b/libraries/botbuilder-dialogs-adaptive-testing/etc/botbuilder-dialogs-adaptive-testing.api.md index 5a49d56b53..77d4e25fbe 100644 --- a/libraries/botbuilder-dialogs-adaptive-testing/etc/botbuilder-dialogs-adaptive-testing.api.md +++ b/libraries/botbuilder-dialogs-adaptive-testing/etc/botbuilder-dialogs-adaptive-testing.api.md @@ -313,7 +313,7 @@ export class UserActivity extends TestAction implements UserActivityConfiguratio // (undocumented) static $kind: string; activity: Activity; - execute(testAdapter: TestAdapter, callback: (context: TurnContext) => Promise, inspector?: Inspector): Promise; + execute(testAdapter: TestAdapter, callback: (context: TurnContext) => Promise, _inspector?: Inspector): Promise; user: string; } diff --git a/libraries/botbuilder-dialogs-adaptive-testing/package.json b/libraries/botbuilder-dialogs-adaptive-testing/package.json index d869234290..79db8c0274 100644 --- a/libraries/botbuilder-dialogs-adaptive-testing/package.json +++ b/libraries/botbuilder-dialogs-adaptive-testing/package.json @@ -34,7 +34,8 @@ "botbuilder-stdlib": "4.1.6", "murmurhash-js": "^1.0.0", "nock": "^11.9.1", - "url-parse": "^1.5.1" + "url-parse": "^1.5.1", + "zod": "~1.11.17" }, "devDependencies": { "@microsoft/recognizers-text-suite": "1.1.4", diff --git a/libraries/botbuilder-dialogs-adaptive-testing/src/testActions/userActivity.ts b/libraries/botbuilder-dialogs-adaptive-testing/src/testActions/userActivity.ts index f31a794704..47853b0b14 100644 --- a/libraries/botbuilder-dialogs-adaptive-testing/src/testActions/userActivity.ts +++ b/libraries/botbuilder-dialogs-adaptive-testing/src/testActions/userActivity.ts @@ -6,9 +6,9 @@ * Licensed under the MIT License. */ +import * as z from 'zod'; import { Activity, TurnContext, TestAdapter } from 'botbuilder-core'; import { Inspector, TestAction } from '../testAction'; -import { tests } from 'botbuilder-stdlib'; export interface UserActivityConfiguration { activity?: Activity; @@ -33,15 +33,16 @@ export class UserActivity extends TestAction implements UserActivityConfiguratio /** * Execute the test. + * * @param testAdapter Adapter to execute against. * @param callback Logic for the bot to use. - * @param inspector Inspector for dialog context. + * @param _inspector Inspector for dialog context. * @returns A Promise that represents the work queued to execute. */ public async execute( testAdapter: TestAdapter, callback: (context: TurnContext) => Promise, - inspector?: Inspector + _inspector?: Inspector ): Promise { if (!this.activity) { throw new Error('You must define one of Text of Activity properties'); @@ -62,7 +63,7 @@ export class UserActivity extends TestAction implements UserActivityConfiguratio activity.from = { ...activity.from }; activity.from.id = this.user; activity.from.name = this.user; - } else if (tests.isObject(this.activity?.from)) { + } else if (z.record(z.unknown()).check(this.activity?.from)) { activity.from = { ...this.activity.from }; } diff --git a/libraries/botbuilder-dialogs-declarative/package.json b/libraries/botbuilder-dialogs-declarative/package.json index d6680f9717..3d0f9f41b5 100644 --- a/libraries/botbuilder-dialogs-declarative/package.json +++ b/libraries/botbuilder-dialogs-declarative/package.json @@ -31,7 +31,8 @@ "botbuilder-core": "4.1.6", "botbuilder-dialogs": "4.1.6", "botbuilder-stdlib": "4.1.6", - "chokidar": "^3.4.0" + "chokidar": "^3.4.0", + "zod": "~1.11.17" }, "devDependencies": { "adaptive-expressions": "4.1.6", diff --git a/libraries/botbuilder-dialogs-declarative/src/componentDeclarativeTypes.ts b/libraries/botbuilder-dialogs-declarative/src/componentDeclarativeTypes.ts index cd0d399a99..e7ec0cbec9 100644 --- a/libraries/botbuilder-dialogs-declarative/src/componentDeclarativeTypes.ts +++ b/libraries/botbuilder-dialogs-declarative/src/componentDeclarativeTypes.ts @@ -6,7 +6,7 @@ * Licensed under the MIT License. */ -import { tests } from 'botbuilder-stdlib'; +import * as z from 'zod'; import { DeclarativeType } from './declarativeType'; import { ResourceExplorer } from './resources'; @@ -17,6 +17,11 @@ export interface ComponentDeclarativeTypes { getDeclarativeTypes(resourceExplorer: ResourceExplorer): DeclarativeType[]; } +const componentDeclarativeTypes = z.custom( + (val: any) => typeof val.getDeclarativeTypes === 'function', + { message: 'ComponentDeclarativeTypes' } +); + /** * Check if a [ComponentRegistration](xref:botbuilder-core.ComponentRegistration) is * [ComponentDeclarativeTypes](xref:botbuilder-dialogs-declarative.ComponentDeclarativeTypes) or not. @@ -25,5 +30,5 @@ export interface ComponentDeclarativeTypes { * @returns {boolean} Type check result. */ export function isComponentDeclarativeTypes(component: unknown): component is ComponentDeclarativeTypes { - return tests.unsafe.isObjectAs(component) && tests.isFunc(component.getDeclarativeTypes); + return componentDeclarativeTypes.check(component); } diff --git a/libraries/botbuilder-dialogs-declarative/src/resources/resourceExplorer.ts b/libraries/botbuilder-dialogs-declarative/src/resources/resourceExplorer.ts index 15e39acc71..52987554d6 100644 --- a/libraries/botbuilder-dialogs-declarative/src/resources/resourceExplorer.ts +++ b/libraries/botbuilder-dialogs-declarative/src/resources/resourceExplorer.ts @@ -14,7 +14,7 @@ import { ResourceProvider, ResourceChangeEvent } from './resourceProvider'; import { FolderResourceProvider } from './folderResourceProvider'; import { Resource } from './resource'; import { PathUtil } from '../pathUtil'; -import { ComponentDeclarativeTypes, isComponentDeclarativeTypes } from '../componentDeclarativeTypes'; +import { ComponentDeclarativeTypes } from '../componentDeclarativeTypes'; import { DeclarativeType } from '../declarativeType'; import { CustomDeserializer } from '../customDeserializer'; import { DefaultLoader } from '../defaultLoader'; diff --git a/libraries/botbuilder-dialogs/package.json b/libraries/botbuilder-dialogs/package.json index 8db67cb2ab..814fad7eab 100644 --- a/libraries/botbuilder-dialogs/package.json +++ b/libraries/botbuilder-dialogs/package.json @@ -33,7 +33,6 @@ "@microsoft/recognizers-text-suite": "1.1.4", "botbuilder-core": "4.1.6", "botbuilder-dialogs-adaptive-runtime-core": "4.1.6", - "botbuilder-stdlib": "4.1.6", "botframework-connector": "4.1.6", "globalize": "^1.4.2", "lodash": "^4.17.21", diff --git a/libraries/botbuilder-dialogs/src/memory/componentMemoryScopes.ts b/libraries/botbuilder-dialogs/src/memory/componentMemoryScopes.ts index 87456832a3..a6dd22112f 100644 --- a/libraries/botbuilder-dialogs/src/memory/componentMemoryScopes.ts +++ b/libraries/botbuilder-dialogs/src/memory/componentMemoryScopes.ts @@ -6,7 +6,7 @@ * Licensed under the MIT License. */ -import { tests } from 'botbuilder-stdlib'; +import * as z from 'zod'; import { MemoryScope } from './scopes'; /** @@ -16,6 +16,10 @@ export interface ComponentMemoryScopes { getMemoryScopes(): MemoryScope[]; } +const componentMemoryScopes = z.custom((val: any) => typeof val.getMemoryScopes === 'function', { + message: 'ComponentMemoryScopes', +}); + /** * Check if a [ComponentRegistration](xref:botbuilder-core.ComponentRegistration) is * [ComponentMemoryScopes](xref:botbuilder-dialogs.ComponentMemoryScopes) or not. @@ -24,5 +28,5 @@ export interface ComponentMemoryScopes { * @returns {boolean} Type check result. */ export function isComponentMemoryScopes(component: unknown): component is ComponentMemoryScopes { - return tests.unsafe.isObjectAs(component) && tests.isFunc(component.getMemoryScopes); + return componentMemoryScopes.check(component); } diff --git a/libraries/botbuilder-dialogs/src/memory/componentPathResolvers.ts b/libraries/botbuilder-dialogs/src/memory/componentPathResolvers.ts index 8cb827ec2b..09003cc459 100644 --- a/libraries/botbuilder-dialogs/src/memory/componentPathResolvers.ts +++ b/libraries/botbuilder-dialogs/src/memory/componentPathResolvers.ts @@ -6,7 +6,7 @@ * Licensed under the MIT License. */ -import { tests } from 'botbuilder-stdlib'; +import * as z from 'zod'; import { PathResolver } from './pathResolvers'; /** @@ -16,6 +16,11 @@ export interface ComponentPathResolvers { getPathResolvers(): PathResolver[]; } +const componentPathResolvers = z.custom( + (val: any) => typeof val.getPathResolvers === 'function', + { message: 'ComponentPathResolvers' } +); + /** * Check if a [ComponentRegistration](xref:botbuilder-core.ComponentRegistration) is * [ComponentPathResolvers](xref:botbuilder-dialogs.ComponentPathResolvers) or not. @@ -24,5 +29,5 @@ export interface ComponentPathResolvers { * @returns {boolean} Type check result. */ export function isComponentPathResolvers(component: unknown): component is ComponentPathResolvers { - return tests.unsafe.isObjectAs(component) && tests.isFunc(component.getPathResolvers); + return componentPathResolvers.check(component); } diff --git a/libraries/botbuilder-stdlib/src/assertExt.ts b/libraries/botbuilder-stdlib/src/assertExt.ts deleted file mode 100644 index 93bfad482e..0000000000 --- a/libraries/botbuilder-stdlib/src/assertExt.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { Newable } from './types'; - -// Represents an error constructor -export type NewableError = Newable; - -/** - * Asserts `condition` to the Typescript compiler - * - * @param {any} condition a condition to assert - * @param {string} message error message - * @param {NewableError} ctor an optional constructor that makes Error instances - */ -export function assertCondition(condition: unknown, message: string, ctor: NewableError = Error): asserts condition { - if (!condition) { - throw new ctor(message); - } -} diff --git a/libraries/botbuilder-stdlib/src/index.ts b/libraries/botbuilder-stdlib/src/index.ts index e595b7207a..c2c43dbd8a 100644 --- a/libraries/botbuilder-stdlib/src/index.ts +++ b/libraries/botbuilder-stdlib/src/index.ts @@ -1,8 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -export * as assertExt from './assertExt'; export * from './types'; +export * as stringExt from './stringExt'; + export { delay } from './delay'; export { maybeCast } from './maybeCast'; export { retry } from './retry'; diff --git a/libraries/botbuilder-stdlib/src/stringExt.ts b/libraries/botbuilder-stdlib/src/stringExt.ts new file mode 100644 index 0000000000..5d925fcea5 --- /dev/null +++ b/libraries/botbuilder-stdlib/src/stringExt.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Nil } from './types'; + +/** + * Check if a string is nil or empty. + * + * @param val a value that may be a string + * @returns true if the string is nil or empty + */ +export function isNilOrEmpty(val: string | Nil): boolean { + return val == null || val === ''; +} diff --git a/libraries/botbuilder-stdlib/src/types.ts b/libraries/botbuilder-stdlib/src/types.ts index 1a4824e399..056a782b27 100644 --- a/libraries/botbuilder-stdlib/src/types.ts +++ b/libraries/botbuilder-stdlib/src/types.ts @@ -1,11 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/ban-types */ - -import { NewableError, assertCondition } from './assertExt'; - // Nil describes a null or undefined value; export type Nil = null | undefined; @@ -17,741 +12,3 @@ export type Newable = new (...args: A) => T; // Extends mimics Newable, but works for abstract classes as well export type Extends = Function & { prototype: T }; // eslint-disable-line @typescript-eslint/ban-types - -// A dictionary type describes a common Javascript object -export type Dictionary = Record; - -// A type test function signature -export type Test = (val: unknown) => val is T; - -// A type assertion method signature -export type Assertion = (val: unknown, path: string[]) => asserts val is T; - -// Formats error messages for assertion failures -const formatPathAndMessage = (path: string[], message: string): string => `\`${path.join('.')}\` ${message}`; - -/** - * An error that indicates that the source of the error was an undefined value - */ -export class UndefinedError extends Error {} - -/** - * Asserts `cond` to the typescript compiler - * - * @param {any} cond a condition to assert - * @param {string[]} path the accumulated path for the assertion - * @param {string} message an error message to use - * @param {NewableError} errorCtor an optional error constructor - */ -function condition(cond: unknown, path: string[], message: string, errorCtor: NewableError = TypeError): asserts cond { - assertCondition(cond, formatPathAndMessage(path, message), errorCtor); -} - -/** - * Construct an assertion function - * - * @template T the type to assert - * @param {string} typeName the name of type `T` - * @param {Test} test a method to test if an unknown value is of type `T` - * @param {boolean} acceptNil true if null or undefined values are acceptable - * @returns {Assertion} an assertion that asserts an unknown value is of type `T` - */ -function makeAssertion(typeName: string, test: Test, acceptNil = false): Assertion { - return (val, path) => { - if (!acceptNil) { - condition(!isNil(val), path, 'must be defined', UndefinedError); - } - condition(test(val), path, `must be of type "${typeName}"`); - }; -} - -/** - * Constructs a type assertion that is enforced if the value is not null or undefined - * - * @template T the type to assert - * @param {Assertion} assertion the assertion - * @returns {Assertion>} an assertion that asserts an unknown value is of type `T` or `Nil` - */ -function makeMaybeAssertion(assertion: Assertion): Assertion> { - return (val, path) => { - if (!isNil(val)) { - assertion(val, path); - } - }; -} - -/** - * Takes an assertion for type `T` and returns an assertion for type `Partial`. The implementation - * expects that the assertion throws `UndefinedError` if an expected value is undefined. All the assertions - * exported by this package satisfy that requirement. - * - * @template T a type extending from `Dictionary` - * @param {Assertion} assertion an assertion that asserts an unknown value is of type `T` - * @returns {Assertion>} an assertion that asserts an unknown value is of type `Partial` - */ -function makePartialAssertion(assertion: Assertion): Assertion> { - return (val, path) => { - try { - assertion(val, path); - } catch (err) { - if (!isUndefinedError(err)) { - throw err; - } - } - }; -} - -/** - * Test if `val` is of type `any`. - * - * @param {any} val value to test - * @returns {boolean} true if `val` is of type `any` - */ -function isAny(val: unknown): val is any { - return true; -} - -/** - * Assert that `val` is of type `any`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function any(val: unknown, path: string[]): asserts val is any { - const assertion: Assertion = makeAssertion('any', isAny); - assertion(val, path); -} - -/** - * Assert that `val` is of type `any`, or `Nil`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function maybeAny(val: unknown, path: string[]): asserts val is Maybe { - const assertion: Assertion> = makeMaybeAssertion(any); - assertion(val, path); -} - -/** - * Test if `val` is of type `array`. - * - * @param {any} val value to test - * @returns {boolean} true if `val` is of type `array` - */ -function isArray(val: unknown): val is unknown[] { - return Array.isArray(val); -} - -/** - * Assert that `val` is of type `array`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function array(val: unknown, path: string[]): asserts val is unknown[] { - const assertion: Assertion = makeAssertion('array', isArray); - assertion(val, path); -} - -/** - * Assert that `val` is of type `array`, or `Nil`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function maybeArray(val: unknown, path: string[]): asserts val is Maybe { - const assertion: Assertion> = makeMaybeAssertion(array); - assertion(val, path); -} - -/** - * Test if `val` is of type `boolean`. - * - * @param {any} val value to test - * @returns {boolean} true if `val` is of type `boolean` - */ -function isBoolean(val: unknown): val is boolean { - return typeof val === 'boolean'; -} - -/** - * Assert that `val` is of type `boolean`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function boolean(val: unknown, path: string[]): asserts val is boolean { - const assertion: Assertion = makeAssertion('boolean', isBoolean); - assertion(val, path); -} - -/** - * Assert that `val` is of type `boolean`, or `Nil`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function maybeBoolean(val: unknown, path: string[]): asserts val is Maybe { - const assertion: Assertion> = makeMaybeAssertion(boolean); - assertion(val, path); -} - -/** - * Test if `val` is of type `Date`. - * - * @param {any} val value to test - * @returns {boolean} true if `val` is of type `Date` - */ -function isDate(val: unknown): val is Date { - return val instanceof Date; -} - -/** - * Assert that `val` is of type `Date`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function date(val: unknown, path: string[]): asserts val is Date { - const assertion: Assertion = makeAssertion('Date', isDate); - assertion(val, path); -} - -/** - * Assert that `val` is of type `Date`, or `Nil`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function maybeDate(val: unknown, path: string[]): asserts val is Maybe { - const assertion: Assertion> = makeMaybeAssertion(date); - assertion(val, path); -} - -/** - * Test if `val` is of type `Dictionary`. - * - * @param {any} val value to test - * @returns {boolean} true if `val` is of type `Dictionary` - */ -function isDictionary(val: unknown): val is Dictionary { - return isObject(val); -} - -/** - * Assert that `val` is of type `Dictionary`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function dictionary(val: unknown, path: string[]): asserts val is Dictionary { - const assertion: Assertion = makeAssertion('Dictionary', isDictionary); - assertion(val, path); -} - -/** - * Assert that `val` is of type `Dictionary`, or `Nil`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function maybeDictionary(val: unknown, path: string[]): asserts val is Maybe { - const assertion: Assertion> = makeMaybeAssertion(dictionary); - assertion(val, path); -} - -/** - * Test if `val` is of type `Error`. - * - * @param {any} val value to test - * @returns {boolean} true if `val` is of type `Error` - */ -function isError(val: unknown): val is Error { - return val instanceof Error; -} - -/** - * Assert that `val` is of type `Error`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function error(val: unknown, path: string[]): asserts val is Error { - const assertion: Assertion = makeAssertion('Error', isError); - assertion(val, path); -} - -/** - * Test if `val` is of type `TypeError`. - * - * @param {any} val value to test - * @returns {boolean} true if `val` is of type `TypeError` - */ -function isTypeError(val: unknown): val is TypeError { - return val instanceof TypeError; -} - -/** - * Assert that `val` is of type `TypeError`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function typeError(val: unknown, path: string[]): asserts val is TypeError { - const assertion: Assertion = makeAssertion('TypeError', isTypeError); - assertion(val, path); -} - -/** - * Test if `val` is of type `UndefinedError`. - * - * @param {any} val value to test - * @returns {boolean} true if `val` is of type `UndefinedError` - */ -function isUndefinedError(val: unknown): val is UndefinedError { - return val instanceof UndefinedError; -} - -/** - * Assert that `val` is of type `UndefinedError`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function undefinedError(val: unknown, path: string[]): asserts val is UndefinedError { - const assertion: Assertion = makeAssertion('UndefinedError', isUndefinedError); - assertion(val, path); -} - -// Represents a generic function -export type Func = (...args: T) => R; - -/** - * Test if `val` is of type `Func`. - * - * @param {any} val value to test - * @returns {boolean} true if `val` is of type `Func` - */ -function isFunc(val: unknown): val is Func { - return typeof val === 'function'; -} - -/** - * Assert that `val` is of type `Func`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function func(val: unknown, path: string[]): asserts val is Func { - const assertion: Assertion = makeAssertion('Function', isFunc); - assertion(val, path); -} - -/** - * Assert that `val` is of type `Func`, or `Nil`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function maybeFunc(val: unknown, path: string[]): asserts val is Maybe { - const assertion: Assertion> = makeMaybeAssertion(func); - assertion(val, path); -} - -/** - * Test if `val` is of type `Nil`. - * - * @param {any} val value to test - * @returns {boolean} true if `val` is of type `Func` - */ -function isNil(val: unknown): val is Nil { - return val == null; -} - -/** - * Assert that `val` is of type `Nil`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function nil(val: unknown, path: string[]): asserts val is Nil { - const assertion: Assertion = makeAssertion('nil', isNil, true); - assertion(val, path); -} - -/** - * Test if `val` is of type `string`. - * - * @param {any} val value to test - * @returns {boolean} true if `val` is of type `string` - */ -function isString(val: unknown): val is string { - return typeof val === 'string'; -} - -/** - * Test if `val` is of type `string` with zero length or `Nil`. - * - * @remarks - * Implementation of string.IsNullOrEmpty(): https://docs.microsoft.com/en-us/dotnet/api/system.string.isnullorempty?view=netcore-3.1 - * @param {any} val value to test - * @returns {boolean} true if `val` is of `string` with zero length or `Nil` - */ -function isStringNullOrEmpty(val: unknown): val is Maybe { - return tests.isNil(val) || (tests.isString(val) && !val.length); -} - -/** - * Assert that `val` is of type `string`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function string(val: unknown, path: string[]): asserts val is string { - const assertion: Assertion = makeAssertion('string', isString); - assertion(val, path); -} - -/** - * Assert that `val` is of type `string`, or `Nil`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function maybeString(val: unknown, path: string[]): asserts val is Maybe { - const assertion: Assertion> = makeMaybeAssertion(string); - assertion(val, path); -} - -/** - * Test if `val` is of type `number`. - * - * @param {any} val value to test - * @returns {boolean} true if `val` is of type `number` - */ -function isNumber(val: unknown): val is number { - return typeof val === 'number' && !isNaN(val); -} - -/** - * Assert that `val` is of type `number`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function number(val: unknown, path: string[]): asserts val is number { - const assertion: Assertion = makeAssertion('number', isNumber); - assertion(val, path); -} - -/** - * Assert that `val` is of type `number`, or `Nil`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function maybeNumber(val: unknown, path: string[]): asserts val is Maybe { - const assertion: Assertion> = makeMaybeAssertion(number); - assertion(val, path); -} - -/** - * Test if `val` is of type `object`. - * - * @param {any} val value to test - * @returns {boolean} true if `val` is of type `object` - */ -function isObject(val: unknown): val is object { - return !isNil(val) && typeof val === 'object' && !isArray(val); -} - -/** - * Assert that `val` is of type `object`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function object(val: unknown, path: string[]): asserts val is object { - const assertion: Assertion = makeAssertion('object', isObject); - assertion(val, path); -} - -/** - * Assert that `val` is of type `object`, or `Nil`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function maybeObject(val: unknown, path: string[]): asserts val is Maybe { - const assertion: Assertion> = makeMaybeAssertion(object); - assertion(val, path); -} - -/** - * Test if `val` is of type `unknown`. - * - * @param {any} val value to test - * @returns {boolean} true if `val` is of type `unknown` - */ -function isUnknown(val: unknown): val is unknown { - return true; -} - -/** - * Assert that `val` is of type `unknown`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function unknown(val: unknown, path: string[]): asserts val is unknown { - const assertion: Assertion = makeAssertion('unknown', isUnknown); - assertion(val, path); -} - -/** - * Assert that `val` is of type `unknown`, or `Nil`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function maybeUnknown(val: unknown, path: string[]): asserts val is Maybe { - const assertion: Assertion> = makeMaybeAssertion(unknown); - assertion(val, path); -} - -/** - * Make a type test function out of an assertion - * - * @template T the type to test - * @param {Assertion} assertion an assertion - * @returns {Test} a type test that returns true if an unknown value is of type `T` - */ -function makeTest(assertion: Assertion): Test { - return (val): val is T => { - try { - assertion(val, []); - return true; - } catch (_err) { - return false; - } - }; -} - -/** - * **UNSAFE** - * Test if `val` is of type `object`. - * This test does not actually verify that `val` is of type `T`. It is useful as the first - * line in a nested assertion so that remaining assertion calls can leverage helpful intellisense. - * This method is only exported under the `unsafe` keyword as a constant reminder of this fact. - * - * @template T the type to cast `val` to, should extend `Dictionary`, i.e. be itself an object - * @param {any} val value to test - * @returns {boolean} true if `val` is of type `object` - */ -function isObjectAs(val: unknown): val is T { - castObjectAs(val, []); - return isObject(val); -} - -export const tests = { - isAny, - isArray, - isBoolean, - isDate, - isDictionary, - isFunc, - isNil, - isNumber, - isObject, - isString, - isStringNullOrEmpty, - isUnknown, - - isError, - isTypeError, - isUndefinedError, - - fromAssertion: makeTest, - toAssertion: makeAssertion, - - unsafe: { isObjectAs }, -}; - -/** - * Construct an assertion that an unknown value is an array with items of type `T` - * - * @template T the item type - * @param {Assertion} assertion the assertion - * @returns {Assertion>} an assertion that asserts an unknown value is an array with items of type `T` - */ -function arrayOf(assertion: Assertion): Assertion> { - return (val, path) => { - const assertArray: Assertion = array; - assertArray(val, path); - - val.forEach((val, idx) => assertion(val, path.concat(`[${idx}]`))); - }; -} - -/** - * Assert that `val` is of type `string[]`. - * - * @param {any} val value to assert - * @param {string[]} path path to val (useful for nested assertions) - */ -function arrayOfString(val: unknown, path: string[]): asserts val is string[] { - const assertion: Assertion = arrayOf(string); - assertion(val, path); -} - -/** - * Construct an assertion that an unknown value is an array with items of type `T`, or `Nil` - * - * @template T the item type - * @param {Assertion} assertion the assertion - * @returns {Assertion>>} an assertion that asserts an unknown value is an array with - * items of type `T`, or `Nil` - */ -function maybeArrayOf(assertion: Assertion): Assertion>> { - return (val, path) => { - const assertArrayOf: Assertion> = arrayOf(assertion); - const assertMaybeArrayOf: Assertion | Nil> = makeMaybeAssertion(assertArrayOf); - assertMaybeArrayOf(val, path); - }; -} - -/** - * Construct an assertion that an unknown value is an instance of type `T` - * - * @template T the instance type - * @param {string} typeName the name of type `T` - * @param {Newable | Extends} ctor a constructor reference for type `T` - * @returns {Assertion} an assertion that asserts an unknown value is an instance of type `T` - */ -function instanceOf(typeName: string, ctor: Newable | Extends): Assertion { - return (val, path) => { - condition(!isNil(val), path, 'must be defined', UndefinedError); - condition(val instanceof ctor, path, `must be an instance of "${typeName}"`); - }; -} - -/** - * Construct an assertion that an unknown value is an instance of type `T`, or `Nil` - * - * @template T the instance type - * @param {string} typeName the name of type `T` - * @param {Newable | Extends} ctor a constructor reference for type `T` - * @returns {Assertion>} an assertion that asserts an unknown value is an instance of type `T`, or `Nil` - */ -function maybeInstanceOf(typeName: string, ctor: Newable | Extends): Assertion> { - return (val, path) => { - const assertInstanceOf: Assertion = instanceOf(typeName, ctor); - const assertMaybeInstanceOf: Assertion = makeMaybeAssertion(assertInstanceOf); - assertMaybeInstanceOf(val, path); - }; -} - -/** - * Construct an assertion that an unknown value is of type `T`, likely a union type - * - * @template T the type, likely a union of other types - * @param {Array>} tests a set of tests for type `T` - * @returns {Assertion} an assertion that asserts an unknown value is of type `T` - */ -function oneOf(...tests: Array>): Assertion { - return (val, path) => { - condition(!isNil(val), path, 'must be defined', UndefinedError); - if (!tests.some((test) => test(val))) { - condition(false, path, 'is of wrong type'); - } - }; -} - -/** - * Construct an assertion that an unknown value is of type `T`, likely a union type, or `Nil` - * - * @template T the type, likely a union of other types - * @param {Array>} tests a set of tests for type `T` - * @returns {Assertion>} an assertion that asserts an unknown value is of type `T`, or `Nil` - */ -function maybeOneOf(...tests: Array>): Assertion> { - return (val, path) => { - const assertOneOf: Assertion = oneOf(...tests); - const assertMaybeOneOf: Assertion = makeMaybeAssertion(assertOneOf); - assertMaybeOneOf(val, path); - }; -} - -/** - * **UNSAFE** - * This assertion does not actually verify that `val` is of type `T`. It is useful as the first - * line in a nested assertion so that remaining assertion calls can leverage helpful intellisense. - * This method is only exported under the `unsafe` keyword as a constant reminder of this fact. - * - * @template T the type to cast `val` to, should extend `Dictionary`, i.e. be itself an object - * @param {any} val the unknown value - * @param {string[]} path the accumulated assertion path - */ -function castObjectAs(val: unknown, path: string[]): asserts val is T { - const assertWithCast: Assertion = object; - assertWithCast(val, path); -} - -export const assert = { - condition, - - any, - maybeAny, - - array, - maybeArray, - - boolean, - maybeBoolean, - - date, - maybeDate, - - dictionary, - maybeDictionary, - - error, - undefinedError, - typeError, - - func, - maybeFunc, - - nil, - - number, - maybeNumber, - - object, - maybeObject, - - string, - maybeString, - - unknown, - maybeUnknown, - - arrayOf, - maybeArrayOf, - arrayOfString, - - instanceOf, - maybeInstanceOf, - - oneOf, - maybeOneOf, - - // Some helpful, well, helpers - fromTest: makeAssertion, - makeMaybe: makeMaybeAssertion, - makePartial: makePartialAssertion, - toTest: makeTest, - - unsafe: { castObjectAs }, -}; diff --git a/libraries/botbuilder-stdlib/tests/stringExt.test.js b/libraries/botbuilder-stdlib/tests/stringExt.test.js new file mode 100644 index 0000000000..c7e02edbc5 --- /dev/null +++ b/libraries/botbuilder-stdlib/tests/stringExt.test.js @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +const assert = require('assert'); +const { stringExt } = require('..'); + +describe('stringext', function () { + describe('isNilOrEmpty', function () { + it("works for ''", function () { + assert(stringExt.isNilOrEmpty('')); + }); + + it('works for undefined', function () { + assert(stringExt.isNilOrEmpty(undefined)); + }); + + it('works for null', function () { + assert(stringExt.isNilOrEmpty(null)); + }); + + it('works for numbers', function () { + assert(!stringExt.isNilOrEmpty(2)); + }); + }); +}); diff --git a/libraries/botbuilder-stdlib/tests/types.test.js b/libraries/botbuilder-stdlib/tests/types.test.js deleted file mode 100644 index 918b4d2b81..0000000000 --- a/libraries/botbuilder-stdlib/tests/types.test.js +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -const assert = require('assert'); -const { UndefinedError, assert: typeAssert, tests } = require('../lib/types'); - -describe('assertType', function () { - describe('basic', function () { - const testCases = [ - { label: 'any', assert: typeAssert.any, input: 0, throws: false }, - { label: 'any', assert: typeAssert.any, input: 0, throws: false }, - { label: 'maybeAny', assert: typeAssert.maybeAny, input: null, throws: false }, - - { label: 'unknown', assert: typeAssert.unknown, input: 0, throws: false }, - { label: 'maybeUnknown', assert: typeAssert.maybeUnknown, input: null, throws: false }, - - { label: 'array', assert: typeAssert.array, input: [], throws: false }, - { label: 'array', assert: typeAssert.array, input: {}, throws: true }, - { label: 'array', assert: typeAssert.array, input: false, throws: true }, - { label: 'maybeArray', assert: typeAssert.maybeArray, input: null, throws: false }, - - { label: 'arrayOfString', assert: typeAssert.arrayOfString, input: ['foo'], throws: false }, - { label: 'arrayOfString', assert: typeAssert.arrayOfString, input: [10], throws: true }, - - { label: 'boolean', assert: typeAssert.boolean, input: true, throws: false }, - { label: 'boolean', assert: typeAssert.boolean, input: false, throws: false }, - { label: 'boolean', assert: typeAssert.boolean, input: 10, throws: true }, - { label: 'maybeBoolean', assert: typeAssert.maybeBoolean, input: undefined, throws: false }, - - { label: 'date', assert: typeAssert.date, input: new Date(), throws: false }, - { label: 'date', assert: typeAssert.date, input: {}, throws: true }, - { label: 'maybeDate', assert: typeAssert.maybeDate, input: null, throws: false }, - - { label: 'dictionary', assert: typeAssert.dictionary, input: {}, throws: false }, - { label: 'dictionary', assert: typeAssert.dictionary, input: [], throws: true }, - { label: 'maybeDictionary', assert: typeAssert.maybeDictionary, input: null, throws: false }, - - { label: 'object', assert: typeAssert.object, input: {}, throws: false }, - { label: 'object', assert: typeAssert.object, input: [], throws: true }, - { label: 'maybeObject', assert: typeAssert.maybeObject, input: undefined, throws: false }, - - { label: 'error', assert: typeAssert.error, input: new Error(), throws: false }, - { label: 'error', assert: typeAssert.error, input: new TypeError(), throws: false }, - { label: 'error', assert: typeAssert.error, input: 'error', throws: true }, - - { label: 'typeError', assert: typeAssert.typeError, input: new TypeError(), throws: false }, - { label: 'typeError', assert: typeAssert.typeError, input: new Error(), throws: true }, - { label: 'typeError', assert: typeAssert.typeError, input: false, throws: true }, - - { label: 'undefinedError', assert: typeAssert.undefinedError, input: new UndefinedError(), throws: false }, - { label: 'undefinedError', assert: typeAssert.typeError, input: new Error(), throws: true }, - { label: 'undefinedError', assert: typeAssert.typeError, input: false, throws: true }, - - { label: 'func', assert: typeAssert.func, input: () => null, throws: false }, - { label: 'func', assert: typeAssert.func, input: new Date(), throws: true }, - { label: 'maybeFunc', assert: typeAssert.maybeFunc, input: null, throws: false }, - - { label: 'nil', assert: typeAssert.nil, input: null, throws: false }, - { label: 'nil', assert: typeAssert.nil, input: undefined, throws: false }, - { label: 'nil', assert: typeAssert.nil, input: false, throws: true }, - - { label: 'number', assert: typeAssert.number, input: 10, throws: false }, - { label: 'number', assert: typeAssert.number, input: false, throws: true }, - { label: 'number', assert: typeAssert.number, input: NaN, throws: true }, - { label: 'maybeNumber', assert: typeAssert.maybeNumber, input: null, throws: false }, - - { label: 'string', assert: typeAssert.string, input: 'hello', throws: false }, - { label: 'string', assert: typeAssert.string, input: '', throws: false }, - { label: 'string', assert: typeAssert.string, input: false, throws: true }, - { label: 'maybeString', assert: typeAssert.maybeString, input: undefined, throws: false }, - ]; - - testCases.forEach((testCase) => { - it(`typeAssert.${testCase.label} with ${JSON.stringify(testCase.input)} ${ - testCase.throws ? 'throws' : 'does not throw' - }`, function () { - const test = () => testCase.assert(testCase.input, [testCase.label]); - - if (testCase.throws) { - assert.throws(test); - } else { - assert.doesNotThrow(test); - } - }); - }); - - it('maybeArrayOf works', function () { - assert.doesNotThrow(() => typeAssert.maybeArrayOf(typeAssert.number)([1, 2, 3], [])); - assert.doesNotThrow(() => typeAssert.maybeArrayOf(typeAssert.number)(undefined, [])); - assert.throws( - () => typeAssert.maybeArrayOf(typeAssert.number)([1, 2, '3'], ['arr']), - new TypeError('`arr.[2]` must be of type "number"') - ); - }); - - it('maybeInstanceOf works', function () { - assert.doesNotThrow(() => typeAssert.maybeInstanceOf('Error', Error)(new Error(), [])); - assert.doesNotThrow(() => typeAssert.maybeInstanceOf('Error', Error)(undefined, [])); - assert.throws( - () => typeAssert.maybeInstanceOf('Error', Error)('foo', ['err']), - new TypeError('`err` must be an instance of "Error"') - ); - }); - - it('oneOf works', function () { - assert.doesNotThrow(() => typeAssert.maybeOneOf(tests.isNumber, tests.isString)(10, [])); - assert.doesNotThrow(() => typeAssert.maybeOneOf(tests.isNumber, tests.isString)('11', [])); - assert.throws( - () => typeAssert.maybeOneOf(tests.isNumber, tests.isString)(false, ['val']), - new TypeError('`val` is of wrong type') - ); - }); - - it('isStringNullOrEmpty works', function () { - assert(tests.isStringNullOrEmpty('')); - assert(tests.isStringNullOrEmpty(undefined)); - assert(!tests.isStringNullOrEmpty(2)); - }); - }); - - describe('partial', function () { - const assertBaz = (val, path) => { - typeAssert.unsafe.castObjectAs(val, path); - typeAssert.string(val.nested, path.concat('nested')); - }; - - const assertThing = (val, path) => { - typeAssert.unsafe.castObjectAs(val, path); - typeAssert.string(val.foo, path.concat('foo')); - typeAssert.number(val.bar, path.concat('bar')); - assertBaz(val.baz, path.concat('baz')); - }; - - const testCases = [ - { thing: {}, throws: true, throwsPartial: false }, - { thing: { foo: 'foo' }, throws: true, throwsPartial: false }, - { thing: { foo: 10 }, throws: true, throwsPartial: true }, - { thing: { bar: 10 }, throws: true, throwsPartial: false }, - { thing: { bar: '10' }, throws: true, throwsPartial: false }, - { thing: { foo: 'foo', bar: 10 }, throws: true, throwsPartial: false }, - { thing: { foo: 10, bar: '10' }, throws: true, throwsPartial: true }, - { thing: { foo: 'foo', bar: 10, baz: { nested: 'nested' } }, throws: false, throwsPartial: false }, - { thing: { foo: 'foo', bar: 10, baz: { nested: 10 } }, throws: true, throwsPartial: true }, - { thing: { foo: 'foo', bar: 10, baz: {} }, throws: true, throwsPartial: false }, - ]; - - testCases.forEach((testCase) => { - it(`${testCase.throws ? 'throws' : 'does not throw'} for ${JSON.stringify(testCase.thing)}`, function () { - const test = () => assertThing(testCase.thing, ['thing']); - if (testCase.throws) { - assert.throws(test); - } else { - assert.doesNotThrow(test); - } - }); - - it(`partial ${testCase.throwsPartial ? 'throws' : 'does not throw'} for ${JSON.stringify( - testCase.thing - )}`, function () { - const test = () => typeAssert.makePartial(assertThing)(testCase.thing, ['thing']); - if (testCase.throwsPartial) { - assert.throws(test); - } else { - assert.doesNotThrow(test); - } - }); - }); - }); - - describe('makeTest', function () { - it('works', function () { - const isNumber = typeAssert.toTest(typeAssert.number); - assert(isNumber(10)); - assert(!isNumber('foo')); - }); - }); - - describe('nested assertion', function () { - it('collects labels', function () { - const testAssert = (val, label) => { - typeAssert.object(val, label); - Object.entries(val).forEach(([key, val]) => typeAssert.string(val, label.concat(key))); - }; - - const object = { foo: 'bar', baz: false }; - assert.throws(() => testAssert(object, ['object']), new TypeError('`object.baz` must be of type "string"')); - }); - - it('works for instanceOf', function () { - assert.throws( - () => typeAssert.instanceOf('Error', Error)(null, ['value']), - new UndefinedError('`value` must be defined') - ); - assert.throws( - () => typeAssert.instanceOf('Error', Error)(false, ['value']), - new TypeError('`value` must be an instance of "Error"') - ); - }); - - it('works for arrayOf', function () { - assert.throws( - () => typeAssert.arrayOf(typeAssert.number)([0, 1, '2'], ['arr']), - new TypeError('`arr.[2]` must be of type "number"') - ); - }); - }); -}); diff --git a/libraries/botbuilder/src/setSpeakMiddleware.ts b/libraries/botbuilder/src/setSpeakMiddleware.ts index fcf8e81ffe..95988db0c7 100644 --- a/libraries/botbuilder/src/setSpeakMiddleware.ts +++ b/libraries/botbuilder/src/setSpeakMiddleware.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import * as z from 'zod'; import { ActivityTypes, Channels, Middleware, TurnContext } from 'botbuilder-core'; import { parseDocument } from 'htmlparser2'; -import { tests } from 'botbuilder-stdlib'; const supportedChannels = new Set([Channels.DirectlineSpeech, Channels.Emulator, Channels.Telephony]); @@ -12,12 +12,18 @@ function hasTag(tag: string, nodes: unknown[]): boolean { while (nodes.length) { const item = nodes.shift(); - if (tests.isDictionary(item)) { + if ( + z + .object({ tagName: z.string(), children: z.array(z.unknown()) }) + .partial() + .nonstrict() + .check(item) + ) { if (item.tagName === tag) { return true; } - if (tests.isArray(item.children)) { + if (item.children) { nodes.push(...item.children); } } diff --git a/libraries/botbuilder/src/teams/teamsSSOTokenExchangeMiddleware.ts b/libraries/botbuilder/src/teams/teamsSSOTokenExchangeMiddleware.ts index 42e12f2333..71de6ddb97 100644 --- a/libraries/botbuilder/src/teams/teamsSSOTokenExchangeMiddleware.ts +++ b/libraries/botbuilder/src/teams/teamsSSOTokenExchangeMiddleware.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { Assertion, assert } from 'botbuilder-stdlib'; +import * as z from 'zod'; import { ActivityTypes, @@ -46,10 +46,10 @@ async function sendInvokeResponse(context: TurnContext, body: unknown = null, st }); } -const assertExchangeToken: Assertion> = (val, path) => { - assert.unsafe.castObjectAs(val, path); - assert.func(val.exchangeToken, path.concat('exchangeToken')); -}; +const ExchangeToken = z.custom>( + (val: any) => typeof val.exchangeToken === 'function', + { message: 'ExtendedUserTokenProvider' } +); /** * If the activity name is signin/tokenExchange, this middleware will attempt to @@ -138,8 +138,7 @@ export class TeamsSSOTokenExchangeMiddleware implements Middleware { let tokenExchangeResponse: TokenResponse; const tokenExchangeRequest: TokenExchangeInvokeRequest = context.activity.value; - const tokenProvider = context.adapter; - assertExchangeToken(tokenProvider, ['context', 'adapter']); + const tokenProvider = ExchangeToken.parse(context.adapter); // TODO(jgummersall) convert to new user token client provider when available try { diff --git a/libraries/botframework-connector/package.json b/libraries/botframework-connector/package.json index ae2dca10c8..60077c3684 100644 --- a/libraries/botframework-connector/package.json +++ b/libraries/botframework-connector/package.json @@ -37,7 +37,8 @@ "botframework-schema": "4.1.6", "cross-fetch": "^3.0.5", "jsonwebtoken": "8.0.1", - "rsa-pem-from-mod-exp": "^0.8.4" + "rsa-pem-from-mod-exp": "^0.8.4", + "zod": "~1.11.17" }, "devDependencies": { "botbuilder-test-utils": "0.0.0", diff --git a/libraries/botframework-connector/src/auth/botFrameworkAuthenticationFactory.ts b/libraries/botframework-connector/src/auth/botFrameworkAuthenticationFactory.ts index cb481fa64c..c3c9070f5c 100644 --- a/libraries/botframework-connector/src/auth/botFrameworkAuthenticationFactory.ts +++ b/libraries/botframework-connector/src/auth/botFrameworkAuthenticationFactory.ts @@ -1,15 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { CallerIdConstants } from 'botframework-schema'; -import { ConnectorClientOptions } from '../connectorApi/models'; import type { AuthenticationConfiguration } from './authenticationConfiguration'; -import { AuthenticationConstants } from './authenticationConstants'; import type { BotFrameworkAuthentication } from './botFrameworkAuthentication'; +import type { ServiceClientCredentialsFactory } from './serviceClientCredentialsFactory'; +import { AuthenticationConstants } from './authenticationConstants'; +import { CallerIdConstants } from 'botframework-schema'; +import { ConnectorClientOptions } from '../connectorApi/models'; import { GovernmentConstants } from './governmentConstants'; import { ParameterizedBotFrameworkAuthentication } from './parameterizedBotFrameworkAuthentication'; -import type { ServiceClientCredentialsFactory } from './serviceClientCredentialsFactory'; -import { tests } from 'botbuilder-stdlib'; +import { stringExt } from 'botbuilder-stdlib'; /** * A factory for [BotFrameworkAuthentication](xref:botframework-connector.BotFrameworkAuthentication) which encapsulates the environment specific Bot Framework Protocol auth code. @@ -71,13 +71,13 @@ export class BotFrameworkAuthenticationFactory { maybeConnectorClientOptions: ConnectorClientOptions = {} ): BotFrameworkAuthentication { if ( - !tests.isStringNullOrEmpty(maybeToChannelFromBotLoginUrl) || - !tests.isStringNullOrEmpty(maybeToChannelFromBotOAuthScope) || - !tests.isStringNullOrEmpty(maybeToBotFromChannelTokenIssuer) || - !tests.isStringNullOrEmpty(maybeOAuthUrl) || - !tests.isStringNullOrEmpty(maybeToBotFromChannelOpenIdMetadataUrl) || - !tests.isStringNullOrEmpty(maybeToBotFromEmulatorOpenIdMetadataUrl) || - !tests.isStringNullOrEmpty(maybeCallerId) + !stringExt.isNilOrEmpty(maybeToChannelFromBotLoginUrl) || + !stringExt.isNilOrEmpty(maybeToChannelFromBotOAuthScope) || + !stringExt.isNilOrEmpty(maybeToBotFromChannelTokenIssuer) || + !stringExt.isNilOrEmpty(maybeOAuthUrl) || + !stringExt.isNilOrEmpty(maybeToBotFromChannelOpenIdMetadataUrl) || + !stringExt.isNilOrEmpty(maybeToBotFromEmulatorOpenIdMetadataUrl) || + !stringExt.isNilOrEmpty(maybeCallerId) ) { // If any of the 'parameterized' properties are defined, assume all parameters are intentional. return new ParameterizedBotFrameworkAuthentication( @@ -96,7 +96,7 @@ export class BotFrameworkAuthenticationFactory { ); } else { // else apply the built in default behavior, which is either the public cloud or the gov cloud depending on whether we have a channelService value present - if (tests.isStringNullOrEmpty(maybeChannelService)) { + if (stringExt.isNilOrEmpty(maybeChannelService)) { return new ParameterizedBotFrameworkAuthentication( true, AuthenticationConstants.ToChannelFromBotLoginUrl, diff --git a/libraries/botframework-connector/src/auth/botFrameworkClientImpl.ts b/libraries/botframework-connector/src/auth/botFrameworkClientImpl.ts index fe4c6f8921..c3dd8f7853 100644 --- a/libraries/botframework-connector/src/auth/botFrameworkClientImpl.ts +++ b/libraries/botframework-connector/src/auth/botFrameworkClientImpl.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import * as z from 'zod'; import axios from 'axios'; import { Activity, ChannelAccount, InvokeResponse, RoleTypes } from 'botframework-schema'; import { BotFrameworkClient } from '../skills'; @@ -8,19 +9,17 @@ import { ConversationIdHttpHeaderName } from '../conversationConstants'; import { ServiceClientCredentialsFactory } from './serviceClientCredentialsFactory'; import { USER_AGENT } from './connectorFactoryImpl'; import { WebResource } from '@azure/ms-rest-js'; -import { assert } from 'botbuilder-stdlib'; +import { ok } from 'assert'; -const botFrameworkClientFetchImpl = async (input: RequestInfo, init?: RequestInit): Promise => { - const config = { - headers: init.headers as Record, - validateStatus: (): boolean => true, - }; +const botFrameworkClientFetchImpl: typeof fetch = async (input, init) => { + const url = z.string().parse(input); + const { body } = z.object({ body: z.string() }).parse(init); - assert.string(input, ['input']); - assert.string(init.body, ['init']); - const activity = JSON.parse(init.body) as Activity; + const response = await axios.post(url, JSON.parse(body), { + headers: z.record(z.string()).parse(init.headers ?? {}), + validateStatus: () => true, + }); - const response = await axios.post(input, activity, config); return { status: response.status, json: async () => response.data, @@ -32,12 +31,9 @@ export class BotFrameworkClientImpl implements BotFrameworkClient { constructor( private readonly credentialsFactory: ServiceClientCredentialsFactory, private readonly loginEndpoint: string, - private readonly botFrameworkClientFetch: ( - input: RequestInfo, - init?: RequestInit - ) => Promise = botFrameworkClientFetchImpl + private readonly botFrameworkClientFetch = botFrameworkClientFetchImpl ) { - assert.maybeFunc(botFrameworkClientFetch, ['botFrameworkClientFetch']); + ok(typeof botFrameworkClientFetch === 'function'); } async postActivity( @@ -48,12 +44,21 @@ export class BotFrameworkClientImpl implements BotFrameworkClient { conversationId: string, activity: Activity ): Promise> { - assert.maybeString(fromBotId, ['fromBotId']); - assert.maybeString(toBotId, ['toBotId']); - assert.string(toUrl, ['toUrl']); - assert.string(serviceUrl, ['serviceUrl']); - assert.string(conversationId, ['conversationId']); - assert.object(activity, ['activity']); + z.object({ + fromBotId: z.string().optional(), + toBotId: z.string().optional(), + toUrl: z.string(), + serviceUrl: z.string(), + conversationId: z.string(), + activity: z.record(z.unknown()), + }).parse({ + fromBotId, + toBotId, + toUrl, + serviceUrl, + conversationId, + activity, + }); const credentials = await this.credentialsFactory.createCredentials( fromBotId, diff --git a/libraries/botframework-connector/src/auth/passwordServiceClientCredentialFactory.ts b/libraries/botframework-connector/src/auth/passwordServiceClientCredentialFactory.ts index ebfdbde64c..58ae9caf84 100644 --- a/libraries/botframework-connector/src/auth/passwordServiceClientCredentialFactory.ts +++ b/libraries/botframework-connector/src/auth/passwordServiceClientCredentialFactory.ts @@ -3,11 +3,11 @@ import * as adal from 'adal-node'; import type { ServiceClientCredentials } from '@azure/ms-rest-js'; -import { tests } from 'botbuilder-stdlib'; -import { MicrosoftAppCredentials } from './microsoftAppCredentials'; -import { ServiceClientCredentialsFactory } from './serviceClientCredentialsFactory'; import { AuthenticationConstants } from './authenticationConstants'; import { GovernmentConstants } from './governmentConstants'; +import { MicrosoftAppCredentials } from './microsoftAppCredentials'; +import { ServiceClientCredentialsFactory } from './serviceClientCredentialsFactory'; +import { stringExt } from 'botbuilder-stdlib'; /** * A simple implementation of the [ServiceClientCredentialsFactory](xref:botframework-connector.ServiceClientCredentialsFactory) interface. @@ -41,7 +41,7 @@ export class PasswordServiceClientCredentialFactory implements ServiceClientCred } async isAuthenticationDisabled(): Promise { - return tests.isStringNullOrEmpty(this.appId); + return stringExt.isNilOrEmpty(this.appId); } async createCredentials( diff --git a/libraries/botframework-connector/src/auth/userTokenClient.ts b/libraries/botframework-connector/src/auth/userTokenClient.ts index 934831c752..e967f7cc2c 100644 --- a/libraries/botframework-connector/src/auth/userTokenClient.ts +++ b/libraries/botframework-connector/src/auth/userTokenClient.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import * as z from 'zod'; + import { Activity, ActivityEx, @@ -10,7 +12,6 @@ import { TokenResponse, TokenStatus, } from 'botframework-schema'; -import { assert } from 'botbuilder-stdlib'; /** * Client for access user token service. @@ -106,9 +107,15 @@ export abstract class UserTokenClient { * @returns Base64 encoded token exchange state. */ protected static createTokenExchangeState(appId: string, connectionName: string, activity: Activity): string { - assert.string(appId, ['appId']); - assert.string(connectionName, ['connectionName']); - assert.object(activity, ['activity']); + z.object({ + appId: z.string(), + connectionName: z.string(), + activity: z.record(z.unknown()), + }).parse({ + appId, + connectionName, + activity, + }); const tokenExchangeState: TokenExchangeState = { connectionName, diff --git a/libraries/botframework-connector/src/auth/userTokenClientImpl.ts b/libraries/botframework-connector/src/auth/userTokenClientImpl.ts index c669092be9..882f14928d 100644 --- a/libraries/botframework-connector/src/auth/userTokenClientImpl.ts +++ b/libraries/botframework-connector/src/auth/userTokenClientImpl.ts @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { Activity, SignInUrlResponse, TokenExchangeRequest, TokenResponse, TokenStatus } from 'botframework-schema'; +import * as z from 'zod'; import type { ServiceClientCredentials } from '@azure/ms-rest-js'; +import { Activity, SignInUrlResponse, TokenExchangeRequest, TokenResponse, TokenStatus } from 'botframework-schema'; +import { ConnectorClientOptions } from '../connectorApi/models'; import { TokenApiClient } from '../tokenApi/tokenApiClient'; import { UserTokenClient } from './userTokenClient'; -import { assert } from 'botbuilder-stdlib'; -import { ConnectorClientOptions } from '../connectorApi/models'; // Internal export class UserTokenClientImpl extends UserTokenClient { @@ -30,9 +30,15 @@ export class UserTokenClientImpl extends UserTokenClient { channelId: string, magicCode: string ): Promise { - assert.string(userId, ['userId']); - assert.string(connectionName, ['connectionName']); - assert.string(channelId, ['channelId']); + z.object({ + userId: z.string(), + connectionName: z.string(), + channelId: z.string(), + }).parse({ + userId, + connectionName, + channelId, + }); const result = await this.client.userToken.getToken(userId, connectionName, { channelId, code: magicCode }); return result._response.parsedBody; @@ -43,8 +49,13 @@ export class UserTokenClientImpl extends UserTokenClient { activity: Activity, finalRedirect: string ): Promise { - assert.string(connectionName, ['connectionName']); - assert.object(activity, ['activity']); + z.object({ + activity: z.record(z.unknown()), + connectionName: z.string(), + }).parse({ + activity, + connectionName, + }); const result = await this.client.botSignIn.getSignInResource( UserTokenClient.createTokenExchangeState(this.appId, connectionName, activity), @@ -55,16 +66,27 @@ export class UserTokenClientImpl extends UserTokenClient { } async signOutUser(userId: string, connectionName: string, channelId: string): Promise { - assert.string(userId, ['userId']); - assert.string(connectionName, ['connectionName']); - assert.string(channelId, ['channelId']); + z.object({ + userId: z.string(), + connectionName: z.string(), + channelId: z.string(), + }).parse({ + userId, + connectionName, + channelId, + }); await this.client.userToken.signOut(userId, { channelId, connectionName }); } async getTokenStatus(userId: string, channelId: string, includeFilter: string): Promise { - assert.string(userId, ['userId']); - assert.string(channelId, ['channelId']); + z.object({ + userId: z.string(), + channelId: z.string(), + }).parse({ + userId, + channelId, + }); const result = await this.client.userToken.getTokenStatus(userId, { channelId, @@ -79,9 +101,15 @@ export class UserTokenClientImpl extends UserTokenClient { resourceUrls: string[], channelId: string ): Promise> { - assert.string(userId, ['userId']); - assert.string(connectionName, ['connectionName']); - assert.string(channelId, ['channelId']); + z.object({ + userId: z.string(), + connectionName: z.string(), + channelId: z.string(), + }).parse({ + userId, + connectionName, + channelId, + }); const result = await this.client.userToken.getAadTokens( userId, @@ -98,9 +126,15 @@ export class UserTokenClientImpl extends UserTokenClient { channelId: string, exchangeRequest: TokenExchangeRequest ): Promise { - assert.string(userId, ['userId']); - assert.string(connectionName, ['connectionName']); - assert.string(channelId, ['channelId']); + z.object({ + userId: z.string(), + connectionName: z.string(), + channelId: z.string(), + }).parse({ + userId, + connectionName, + channelId, + }); const result = await this.client.userToken.exchangeAsync(userId, connectionName, channelId, exchangeRequest); return result._response.parsedBody; diff --git a/libraries/botframework-connector/tests/auth/userTokenClient.test.js b/libraries/botframework-connector/tests/auth/userTokenClient.test.js index 5cd59d4cc8..5b6a208235 100644 --- a/libraries/botframework-connector/tests/auth/userTokenClient.test.js +++ b/libraries/botframework-connector/tests/auth/userTokenClient.test.js @@ -4,16 +4,17 @@ const { UserTokenClient } = require('../..'); describe('UserTokenClient', function () { describe('createTokenExchangeState', function () { const paramsValidation = [ - { paramName: 'appId', values: [null, null, null] }, - { paramName: 'connectionName', values: ['appId', null, null] }, + { paramName: 'appId', values: [null, 'connectionName', {}] }, + { paramName: 'connectionName', values: ['appId', null, {}] }, { paramName: 'activity', values: ['appId', 'connectionName', null] }, ]; - paramsValidation.forEach((testConfig) => { - it(`should throw with null ${testConfig.paramName}`, function () { - assert.throws(() => UserTokenClient.createTokenExchangeState(...testConfig.values), { - message: `\`${testConfig.paramName}\` must be defined`, - }); + paramsValidation.forEach(({ paramName, values }) => { + it(`should throw with null ${paramName}`, function () { + assert.throws( + () => UserTokenClient.createTokenExchangeState(...values), + (thrown) => thrown.message.includes(paramName) && thrown.message.includes('received null') + ); }); }); }); diff --git a/libraries/botframework-schema/etc/botframework-schema.api.md b/libraries/botframework-schema/etc/botframework-schema.api.md index 868b647d23..84392000b4 100644 --- a/libraries/botframework-schema/etc/botframework-schema.api.md +++ b/libraries/botframework-schema/etc/botframework-schema.api.md @@ -4,9 +4,6 @@ ```ts -import { Assertion } from 'botbuilder-stdlib'; -import { Test } from 'botbuilder-stdlib'; - // @public export type Action = 'accept' | 'decline'; @@ -243,41 +240,65 @@ export interface AppBasedLinkQuery { // @public export type ApplicationIdentityType = 'aadApplication' | 'bot' | 'tenantBot' | 'office365Connector' | 'webhook'; -// @public (undocumented) -export const assertActivity: Assertion; +// Warning: (ae-internal-missing-underscore) The name "assertActivity" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function assertActivity(val: unknown, ..._args: unknown[]): asserts val is Activity; -// @public (undocumented) -export const assertAttachment: Assertion; +// Warning: (ae-internal-missing-underscore) The name "assertAttachment" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function assertAttachment(val: unknown, ..._args: unknown[]): asserts val is Attachment; -// @public (undocumented) -export const assertAttachmentInfo: Assertion; +// Warning: (ae-internal-missing-underscore) The name "assertAttachmentInfo" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function assertAttachmentInfo(val: unknown, ..._args: unknown[]): asserts val is AttachmentInfo; -// @public (undocumented) -export const assertAttachmentView: Assertion; +// Warning: (ae-internal-missing-underscore) The name "assertAttachmentView" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function assertAttachmentView(val: unknown, ..._args: unknown[]): asserts val is AttachmentView; -// @public (undocumented) -export const assertCardAction: Assertion; +// Warning: (ae-internal-missing-underscore) The name "assertCardAction" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function assertCardAction(val: unknown, ..._args: unknown[]): asserts val is CardAction; -// @public (undocumented) -export const assertChannelAccount: Assertion; +// Warning: (ae-internal-missing-underscore) The name "assertChannelAccount" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function assertChannelAccount(val: unknown, ..._args: unknown[]): asserts val is ChannelAccount; -// @public (undocumented) -export const assertConversationAccount: Assertion; +// Warning: (ae-internal-missing-underscore) The name "assertConversationAccount" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function assertConversationAccount(val: unknown, ..._args: unknown[]): asserts val is ConversationAccount; -// @public (undocumented) -export const assertConversationReference: Assertion; +// Warning: (ae-internal-missing-underscore) The name "assertConversationReference" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function assertConversationReference(val: unknown, ..._args: unknown[]): asserts val is ConversationReference; -// @public (undocumented) -export const assertEntity: Assertion; +// Warning: (ae-internal-missing-underscore) The name "assertEntity" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function assertEntity(val: unknown, ..._args: unknown[]): asserts val is Entity; -// @public (undocumented) -export const assertMessageReaction: Assertion; +// Warning: (ae-internal-missing-underscore) The name "assertMessageReaction" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function assertMessageReaction(val: unknown, ..._args: unknown[]): asserts val is MessageReaction; -// @public (undocumented) -export const assertSemanticAction: Assertion; +// Warning: (ae-internal-missing-underscore) The name "assertSemanticAction" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function assertSemanticAction(val: unknown, ..._args: unknown[]): asserts val is SemanticAction; -// @public (undocumented) -export const assertSuggestedActions: Assertion; +// Warning: (ae-internal-missing-underscore) The name "assertSuggestedActions" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function assertSuggestedActions(val: unknown, ..._args: unknown[]): asserts val is SuggestedActions; // @public export interface Attachment { @@ -782,41 +803,65 @@ export interface InvokeResponse { status: number; } -// @public (undocumented) -export const isActivity: Test; +// Warning: (ae-internal-missing-underscore) The name "isActivity" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function isActivity(val: unknown): val is Activity; -// @public (undocumented) -export const isAttachment: Test; +// Warning: (ae-internal-missing-underscore) The name "isAttachment" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function isAttachment(val: unknown): val is Attachment; -// @public (undocumented) -export const isAttachmentInfo: Test; +// Warning: (ae-internal-missing-underscore) The name "isAttachmentInfo" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function isAttachmentInfo(val: unknown): val is AttachmentInfo; -// @public (undocumented) -export const isAttachmentView: Test; +// Warning: (ae-internal-missing-underscore) The name "isAttachmentView" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function isAttachmentView(val: unknown): val is AttachmentView; -// @public (undocumented) -export const isCardAction: Test; +// Warning: (ae-internal-missing-underscore) The name "isCardAction" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function isCardAction(val: unknown): val is CardAction; -// @public (undocumented) -export const isChannelAccount: Test; +// Warning: (ae-internal-missing-underscore) The name "isChannelAccount" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function isChannelAccount(val: unknown): val is ChannelAccount; -// @public (undocumented) -export const isConversationAccount: Test; +// Warning: (ae-internal-missing-underscore) The name "isConversationAccount" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function isConversationAccount(val: unknown): val is ConversationAccount; -// @public (undocumented) -export const isConversationReference: Test; +// Warning: (ae-internal-missing-underscore) The name "isConversationReference" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function isConversationReference(val: unknown): val is ConversationReference; -// @public (undocumented) -export const isEntity: Test; +// Warning: (ae-internal-missing-underscore) The name "isEntity" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function isEntity(val: unknown): val is Entity; -// @public (undocumented) -export const isMessageReaction: Test; +// Warning: (ae-internal-missing-underscore) The name "isMessageReaction" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function isMessageReaction(val: unknown): val is MessageReaction; -// @public (undocumented) -export const isSemanticAction: Test; +// Warning: (ae-internal-missing-underscore) The name "isSemanticAction" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function isSemanticAction(val: unknown): val is SemanticAction; -// @public (undocumented) -export const isSuggestedActions: Test; +// Warning: (ae-internal-missing-underscore) The name "isSuggestedActions" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function isSuggestedActions(val: unknown): val is SuggestedActions; // @public (undocumented) export interface IStatusCodeError { diff --git a/libraries/botframework-schema/package.json b/libraries/botframework-schema/package.json index b4d8881765..45d6bca1f1 100644 --- a/libraries/botframework-schema/package.json +++ b/libraries/botframework-schema/package.json @@ -27,8 +27,8 @@ } }, "dependencies": { - "botbuilder-stdlib": "4.1.6", - "uuid": "^8.3.2" + "uuid": "^8.3.2", + "zod": "~1.11.17" }, "scripts": { "build": "tsc -b", diff --git a/libraries/botframework-schema/src/index.ts b/libraries/botframework-schema/src/index.ts index 09df1e61e0..5c600cbb0f 100644 --- a/libraries/botframework-schema/src/index.ts +++ b/libraries/botframework-schema/src/index.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. */ -import { Assertion, Nil, assert, Test } from 'botbuilder-stdlib'; +import * as z from 'zod'; import { TokenExchangeInvokeRequest } from './tokenExchangeInvokeRequest'; export * from './activityInterfaces'; export * from './activityEx'; + export { CallerIdConstants } from './callerIdConstants'; export { SpeechConstants } from './speechConstants'; export { TokenExchangeInvokeRequest } from './tokenExchangeInvokeRequest'; @@ -30,15 +31,24 @@ export interface AttachmentView { size: number; } -export const assertAttachmentView: Assertion = (value, path) => { - assert.unsafe.castObjectAs(value, path); - assert.string(value.viewId, path.concat('viewId')); - assert.number(value.size, path.concat('size')); -}; +const attachmentView = z.object({ + viewId: z.string(), + size: z.number(), +}); -const assertAttachmentViewArray: Assertion> = assert.arrayOf(assertAttachmentView); +/** + * @internal + */ +export function assertAttachmentView(val: unknown, ..._args: unknown[]): asserts val is AttachmentView { + attachmentView.parse(val); +} -export const isAttachmentView: Test = assert.toTest(assertAttachmentView); +/** + * @internal + */ +export function isAttachmentView(val: unknown): val is AttachmentView { + return attachmentView.check(val); +} /** * Metadata for an attachment @@ -58,14 +68,25 @@ export interface AttachmentInfo { views: AttachmentView[]; } -export const assertAttachmentInfo: Assertion = (value, path) => { - assert.unsafe.castObjectAs(value, path); - assert.string(value.name, path.concat('name')); - assert.array(value.views, path.concat('views')); - assertAttachmentViewArray(value.views, path.concat('views')); -}; +const attachmentInfo = z.object({ + name: z.string(), + type: z.string(), + views: z.array(attachmentView), +}); + +/** + * @internal + */ +export function assertAttachmentInfo(val: unknown, ..._args: unknown[]): asserts val is AttachmentInfo { + attachmentInfo.parse(val); +} -export const isAttachmentInfo = assert.toTest(assertAttachmentInfo); +/** + * @internal + */ +export function isAttachmentInfo(val: unknown): val is AttachmentInfo { + return attachmentInfo.check(val); +} /** * Object representing inner http error @@ -133,18 +154,26 @@ export interface ChannelAccount { role?: RoleTypes | string; } -export const assertChannelAccount: Assertion = (value, path) => { - assert.unsafe.castObjectAs(value, path); - assert.string(value.id, path.concat('id')); - assert.string(value.name, path.concat('name')); - assert.maybeString(value.aadObjectId, path.concat('aadObjectId')); - assert.maybeString(value.role, path.concat('role')); -}; +const channelAccount = z.object({ + id: z.string(), + name: z.string(), + aadObjectId: z.string().optional(), + role: z.string().optional(), +}); -const assertChannelAccountArray: Assertion> = assert.arrayOf(assertChannelAccount); -const assertMaybeChannelAccount: Assertion = assert.makeMaybe(assertChannelAccount); +/** + * @internal + */ +export function assertChannelAccount(val: unknown, ..._args: unknown[]): asserts val is ChannelAccount { + channelAccount.parse(val); +} -export const isChannelAccount = assert.toTest(assertChannelAccount); +/** + * @internal + */ +export function isChannelAccount(val: unknown): val is ChannelAccount { + return channelAccount.check(val); +} /** * Channel account information for a conversation @@ -187,19 +216,30 @@ export interface ConversationAccount { properties?: any; // eslint-disable-line @typescript-eslint/no-explicit-any } -export const assertConversationAccount: Assertion = (value, path) => { - assert.unsafe.castObjectAs(value, path); - assert.boolean(value.isGroup, path.concat('isGroup')); - assert.string(value.conversationType, path.concat('conversationType')); - assert.maybeString(value.tenantId, path.concat('tenantId')); - assert.string(value.id, path.concat('id')); - assert.string(value.name, path.concat('name')); - assert.maybeString(value.aadObjectId, path.concat('aadObjectId')); - assert.maybeString(value.role, path.concat('role')); - assert.any(value.properties, path.concat('properties')); -}; +const conversationAccount = z.object({ + isGroup: z.boolean(), + conversationType: z.string(), + tenantId: z.string().optional(), + id: z.string(), + name: z.string(), + aadObjectId: z.string().optional(), + role: z.string().optional(), + properties: z.unknown().optional(), +}); -export const isConversationAccount = assert.toTest(assertConversationAccount); +/** + * @internal + */ +export function assertConversationAccount(val: unknown, ..._args: unknown[]): asserts val is ConversationAccount { + conversationAccount.parse(val); +} + +/** + * @internal + */ +export function isConversationAccount(val: unknown): val is ConversationAccount { + return conversationAccount.check(val); +} /** * Message reaction object @@ -211,17 +251,23 @@ export interface MessageReaction { type: MessageReactionTypes | string; } -export const assertMessageReaction: Assertion = (value, path) => { - assert.unsafe.castObjectAs(value, path); - assert.string(value.type, path.concat('type')); -}; +const messageReaction = z.object({ + type: z.string(), +}); -const assertMessageReactionArray: Assertion = (value, path) => { - assert.array(value, path); - value.forEach((item, idx) => assertMessageReaction(item, path.concat(`[${idx}]`))); -}; +/** + * @internal + */ +export function assertMessageReaction(val: unknown, ..._args: unknown[]): asserts val is MessageReaction { + messageReaction.parse(val); +} -export const isMessageReaction = assert.toTest(assertMessageReaction); +/** + * @internal + */ +export function isMessageReaction(val: unknown): val is MessageReaction { + return messageReaction.check(val); +} /** * A clickable action @@ -263,21 +309,30 @@ export interface CardAction { imageAltText?: string; } -export const assertCardAction: Assertion = (value, path) => { - assert.unsafe.castObjectAs(value, path); - assert.string(value.type, path.concat('type')); - assert.string(value.title, path.concat('title')); - assert.maybeString(value.image, path.concat('image')); - assert.maybeString(value.text, path.concat('text')); - assert.maybeString(value.displayText, path.concat('displayText')); - assert.any(value.value, path.concat('value')); - assert.maybeAny(value.channelData, path.concat('channelData')); - assert.maybeString(value.imageAltText, path.concat('imageAltText')); -}; +const cardAction = z.object({ + type: z.string(), + title: z.string(), + image: z.string().optional(), + text: z.string().optional(), + displayText: z.string().optional(), + value: z.unknown(), + channelData: z.unknown(), + imageAltText: z.string().optional(), +}); -const assertCardActionArray: Assertion> = assert.arrayOf(assertCardAction); +/** + * @internal + */ +export function assertCardAction(val: unknown, ..._args: unknown[]): asserts val is CardAction { + cardAction.parse(val); +} -export const isCardAction = assert.toTest(assertCardAction); +/** + * @internal + */ +export function isCardAction(val: unknown): val is CardAction { + return cardAction.check(val); +} /** * SuggestedActions that can be performed @@ -294,13 +349,24 @@ export interface SuggestedActions { actions: CardAction[]; } -export const assertSuggestedActions: Assertion = (value, path) => { - assert.unsafe.castObjectAs(value, path); - assert.arrayOfString(value.to, path.concat('to')); - assertCardActionArray(value.actions, path.concat('actions')); -}; +const suggestedActions = z.object({ + to: z.array(z.string()), + actions: z.array(cardAction), +}); -export const isSuggestedActions = assert.toTest(assertSuggestedActions); +/** + * @internal + */ +export function assertSuggestedActions(val: unknown, ..._args: unknown[]): asserts val is SuggestedActions { + suggestedActions.parse(val); +} + +/** + * @internal + */ +export function isSuggestedActions(val: unknown): val is SuggestedActions { + return suggestedActions.check(val); +} /** * An attachment within an activity @@ -328,19 +394,27 @@ export interface Attachment { thumbnailUrl?: string; } -export const assertAttachment: Assertion = (value, path) => { - assert.unsafe.castObjectAs(value, path); - assert.string(value.contentType, path.concat('contentType')); - assert.maybeString(value.contentUrl, path.concat('contentUrl')); - assert.maybeAny(value.content, path.concat('content')); - assert.maybeString(value.name, path.concat('name')); - assert.maybeString(value.thumbnailUrl, path.concat('thumbnailUrl')); -}; +const attachment = z.object({ + contentType: z.string(), + contentUrl: z.string().optional(), + content: z.unknown().optional(), + name: z.string().optional(), + thumbnailUrl: z.string().optional(), +}); -const assertAttachmentArray: Assertion> = assert.arrayOf(assertAttachment); -const assertMaybeAttachmentArray: Assertion | Nil> = assert.makeMaybe(assertAttachmentArray); +/** + * @internal + */ +export function assertAttachment(val: unknown, ..._args: unknown[]): asserts val is Attachment { + attachment.parse(val); +} -export const isAttachment = assert.toTest(assertAttachment); +/** + * @internal + */ +export function isAttachment(val: unknown): val is Attachment { + return attachment.check(val); +} /** * Metadata object pertaining to an activity @@ -356,15 +430,21 @@ export interface Entity { [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any } -export const assertEntity: Assertion = (value, path) => { - assert.dictionary(value, path); - assert.string(value.type, path.concat('type')); -}; +const entity = z.record(z.unknown()).refine((val) => typeof val.type === 'string'); -const assertEntityArray: Assertion> = assert.arrayOf(assertEntity); -const assertMaybeEntityArray: Assertion | Nil> = assert.makeMaybe(assertEntityArray); +/** + * @internal + */ +export function assertEntity(val: unknown, ..._args: unknown[]): asserts val is Entity { + entity.parse(val); +} -export const isEntity = assert.toTest(assertEntity); +/** + * @internal + */ +export function isEntity(val: unknown): val is Entity { + return entity.check(val); +} /** * An object relating to a particular point in a conversation @@ -404,21 +484,29 @@ export interface ConversationReference { serviceUrl: string; } -export const assertConversationReference: Assertion = (value, path) => { - assert.unsafe.castObjectAs(value, path); - assert.maybeString(value.activityId, path.concat('activityId')); - assertMaybeChannelAccount(value.user, path.concat('user')); - assert.maybeString(value.locale, path.concat('locale')); - assertChannelAccount(value.bot, path.concat('bot')); - assert.string(value.channelId, path.concat('channelId')); - assert.string(value.serviceUrl, path.concat('serviceUrl')); -}; +const conversationReference = z.object({ + ActivityId: z.string().optional(), + user: channelAccount.optional(), + locale: z.string().optional(), + bot: channelAccount, + conversation: conversationAccount, + channelId: z.string(), + serviceUrl: z.string(), +}); -const assertMaybeConversationReference: Assertion = assert.makeMaybe( - assertConversationReference -); +/** + * @internal + */ +export function assertConversationReference(val: unknown, ..._args: unknown[]): asserts val is ConversationReference { + conversationReference.parse(val); +} -export const isConversationReference = assert.toTest(assertConversationReference); +/** + * @internal + */ +export function isConversationReference(val: unknown): val is ConversationReference { + return conversationReference.check(val); +} /** * Refers to a substring of content within another field @@ -434,6 +522,11 @@ export interface TextHighlight { occurrence: number; } +const textHighlight = z.object({ + text: z.string(), + occurrence: z.number(), +}); + /** * Represents a reference to a programmatic action */ @@ -452,16 +545,25 @@ export interface SemanticAction { entities: { [propertyName: string]: Entity }; } -export const assertSemanticAction: Assertion = (value, path) => { - assert.unsafe.castObjectAs(value, path); - assert.string(value.id, path.concat('id')); - assert.string(value.state, path.concat('state')); - assert.dictionary(value.entities, path.concat('entities')); -}; +const semanticAction = z.object({ + id: z.string(), + state: z.string(), + entities: z.record(entity), +}); -const assertMaybeSemanticAction: Assertion = assert.makeMaybe(assertSemanticAction); +/** + * @internal + */ +export function assertSemanticAction(val: unknown, ..._args: unknown[]): asserts val is SemanticAction { + semanticAction.parse(val); +} -export const isSemanticAction = assert.toTest(assertSemanticAction); +/** + * @internal + */ +export function isSemanticAction(val: unknown): val is SemanticAction { + return semanticAction.check(val); +} /** * An Activity is the basic communication type for the Bot Framework 3.0 protocol. @@ -656,55 +758,63 @@ export interface Activity { semanticAction?: SemanticAction; } -export const assertActivity: Assertion = (value, path) => { - assert.unsafe.castObjectAs(value, path); - assert.string(value.type, path.concat('type')); - assert.maybeString(value.id, path.concat('id')); - assert.maybeDate(value.timestamp, path.concat('timestamp')); - assert.maybeDate(value.localTimestamp, path.concat('localTimestamp')); - assert.string(value.localTimezone, path.concat('localTimezone')); - assert.maybeString(value.callerId, path.concat('callerId')); - assert.string(value.serviceUrl, path.concat('serviceUrl')); - assert.string(value.channelId, path.concat('channelId')); - assertChannelAccount(value.from, path.concat('from')); - assertConversationAccount(value.conversation, path.concat('conversation')); - assertChannelAccount(value.recipient, path.concat('recipient')); - assert.maybeString(value.textFormat, path.concat('textFormat')); - assert.maybeString(value.attachmentLayout, path.concat('attachmentLayout')); - assertChannelAccountArray(value.membersAdded, path.concat('membersAdded')); - assertChannelAccountArray(value.membersRemoved, path.concat('membersRemoved')); - assertMessageReactionArray(value.reactionsAdded, path.concat('reactionsAdded')); - assertMessageReactionArray(value.reactionsRemoved, path.concat('reactionsRemoved')); - assert.maybeString(value.topicName, path.concat('topicName')); - assert.maybeBoolean(value.historyDisclosed, path.concat('historyDisclosed')); - assert.maybeString(value.locale, path.concat('locale')); - assert.string(value.text, path.concat('text')); - assert.maybeString(value.speak, path.concat('speak')); - assert.maybeString(value.inputHint, path.concat('inputHint')); - assert.maybeString(value.summary, path.concat('summary')); - assertSuggestedActions(value.suggestedActions, path.concat('suggestedActions')); - assertMaybeAttachmentArray(value.attachments, path.concat('attachments')); - assertMaybeEntityArray(value.entities, path.concat('entities')); - assert.maybeAny(value.channelData, path.concat('channelData')); - assert.maybeString(value.action, path.concat('action')); - assert.maybeString(value.replyToId, path.concat('replyToId')); - assert.string(value.label, path.concat('label')); - assert.string(value.valueType, path.concat('valueType')); - assert.maybeAny(value.value, path.concat('value')); - assert.maybeString(value.name, path.concat('name')); - assertMaybeConversationReference(value.relatesTo, path.concat('relatesTo')); - assert.maybeString(value.code, path.concat('code')); - assert.maybeDate(value.expiration, path.concat('expiration')); - assert.maybeString(value.importance, path.concat('importance')); - assert.maybeString(value.deliveryMode, path.concat('deliveryMode')); - - const assertArrayOfStrings: Assertion> = assert.arrayOf(assert.string); - assertArrayOfStrings(value.listenFor, path.concat('listenFor')); - - assertMaybeSemanticAction(value.semanticAction, path.concat('semanticAction')); -}; - -export const isActivity = assert.toTest(assertActivity); +const activity = z.object({ + type: z.string(), + id: z.string().optional(), + timestamp: z.instanceof(Date).optional(), + localTimestamp: z.instanceof(Date).optional(), + localTimezone: z.string(), + callerId: z.string(), + serviceUrl: z.string(), + channelId: z.string(), + from: channelAccount, + conversation: conversationAccount, + recipient: channelAccount, + textFormat: z.string().optional(), + attachmentLayout: z.string().optional(), + membersAdded: z.array(channelAccount).optional(), + membersRemoved: z.array(channelAccount).optional(), + reactionsAdded: z.array(messageReaction).optional(), + reactionsRemoved: z.array(messageReaction).optional(), + topicName: z.string().optional(), + historyDisclosed: z.boolean().optional(), + locale: z.string().optional(), + text: z.string(), + speak: z.string().optional(), + inputHint: z.string().optional(), + summary: z.string().optional(), + suggestedActions: suggestedActions.optional(), + attachments: z.array(attachment).optional(), + entities: z.array(entity).optional(), + channelData: z.unknown().optional(), + action: z.string().optional(), + replyToId: z.string().optional(), + label: z.string(), + valueType: z.string(), + value: z.unknown().optional(), + name: z.string().optional(), + relatesTo: conversationReference.optional(), + code: z.string().optional(), + importance: z.string().optional(), + deliveryMode: z.string().optional(), + listenFor: z.array(z.string()).optional(), + textHighlights: z.array(textHighlight).optional(), + semanticAction: semanticAction.optional(), +}); + +/** + * @internal + */ +export function assertActivity(val: unknown, ..._args: unknown[]): asserts val is Activity { + activity.parse(val); +} + +/** + * @internal + */ +export function isActivity(val: unknown): val is Activity { + return activity.check(val); +} /** * This interface is used to preserve the original string values of dates on Activities. diff --git a/testing/botbuilder-test-utils/package.json b/testing/botbuilder-test-utils/package.json index cb3a941684..92f50613e9 100644 --- a/testing/botbuilder-test-utils/package.json +++ b/testing/botbuilder-test-utils/package.json @@ -19,8 +19,8 @@ "@types/node-forge": "^0.9.5" }, "dependencies": { - "botbuilder-stdlib": "4.1.6", "nanoid": "^3.1.20", - "node-forge": "^0.10.0" + "node-forge": "^0.10.0", + "zod": "~1.11.17" } } diff --git a/testing/botbuilder-test-utils/src/jwt.ts b/testing/botbuilder-test-utils/src/jwt.ts index ff4f65f644..37a162ee6e 100644 --- a/testing/botbuilder-test-utils/src/jwt.ts +++ b/testing/botbuilder-test-utils/src/jwt.ts @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import * as z from 'zod'; import forge from 'node-forge'; import jwt from 'jsonwebtoken'; // eslint-disable-line import/no-extraneous-dependencies import nock from 'nock'; // eslint-disable-line import/no-extraneous-dependencies import url from 'url'; -import { assert } from 'botbuilder-stdlib'; import { nanoid } from 'nanoid'; import { ok } from 'assert'; @@ -82,20 +82,20 @@ export function stub(options: Partial = {}): Result { const hostURL = url.parse(host); const metadataURL = Object.assign({}, hostURL, metadata); - assert.string(metadataURL.path, ['metadata', 'path']); + const metadataPath = z.string().parse(metadataURL.path); const jwksURL = Object.assign({}, hostURL, jwks); - assert.string(jwksURL.path, ['jwks', 'path']); + const jwksPath = z.string().parse(jwksURL.path); const openIdExpectation = nock(formatHost(metadataURL)) - .get(metadataURL.path) + .get(metadataPath) .reply(200, { issuer, jwks_uri: `${formatHost(jwksURL)}${jwksURL.path}`, }); const jwksExpectation = nock(formatHost(jwksURL)) - .get(jwksURL.path) + .get(jwksPath) .reply(200, { keys: [ {