diff --git a/package-lock.json b/package-lock.json index 02aa9843a..eb8c68a2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@finos/fdc3", - "version": "2.1.0-beta.4", + "version": "2.1.0-beta.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@finos/fdc3", - "version": "2.1.0-beta.4", + "version": "2.1.0-beta.6", "license": "Apache-2.0", "devDependencies": { "husky": "^4.3.0", diff --git a/src/api/DesktopAgent.ts b/src/api/DesktopAgent.ts index f9b989c2b..a6a5328cb 100644 --- a/src/api/DesktopAgent.ts +++ b/src/api/DesktopAgent.ts @@ -13,6 +13,8 @@ import { ImplementationMetadata } from './ImplementationMetadata'; import { PrivateChannel } from './PrivateChannel'; import { AppIdentifier } from './AppIdentifier'; import { AppMetadata } from './AppMetadata'; +import { Intent } from '../intents/Intents'; +import { ContextType } from '../context/ContextType'; /** * A Desktop Agent is a desktop component (or aggregate of components) that serves as a @@ -125,7 +127,7 @@ export interface DesktopAgent { * // } * ``` */ - findIntent(intent: string, context?: Context, resultType?: string): Promise; + findIntent(intent: Intent, context?: Context, resultType?: string): Promise; /** * Find all the available intents for a particular context, and optionally a desired result context type. @@ -234,7 +236,7 @@ export interface DesktopAgent { * * If you wish to raise an Intent without a context, use the `fdc3.nothing` context type. This type exists so that apps can explicitly declare support for raising an intent without context. * - * Returns an `IntentResolution` object with details of the app instance that was selected (or started) to respond to the intent. + * Returns an `IntentResolution` object with details of the app instance that was selected (or started) to respond to the intent. * * Issuing apps may optionally wait on the promise that is returned by the `getResult()` member of the `IntentResolution`. This promise will resolve when the _receiving app's_ intent handler function returns and resolves a promise. The Desktop Agent resolves the issuing app's promise with the Context object, Channel object or void that is provided as resolution within the receiving app. The Desktop Agent MUST reject the issuing app's promise, with a string from the `ResultError` enumeration, if: (1) the intent handling function's returned promise rejects, (2) the intent handling function doesn't return a valid response (a promise or void), or (3) the returned promise resolves to an invalid type. * @@ -268,7 +270,7 @@ export interface DesktopAgent { * } * ``` */ - raiseIntent(intent: string, context: Context, app?: AppIdentifier): Promise; + raiseIntent(intent: Intent, context: Context, app?: AppIdentifier): Promise; /** * Finds and raises an intent against apps registered with the desktop agent based on the type of the specified context data example. @@ -346,7 +348,7 @@ export interface DesktopAgent { * }); * ``` */ - addIntentListener(intent: string, handler: IntentHandler): Promise; + addIntentListener(intent: Intent, handler: IntentHandler): Promise; /** * Adds a listener for incoming context broadcasts from the Desktop Agent (via a User channel or `fdc3.open`API call. If the consumer is only interested in a context of a particular type, they can they can specify that type. If the consumer is able to receive context of any type or will inspect types received, then they can pass `null` as the `contextType` parameter to receive all context types. @@ -371,7 +373,7 @@ export interface DesktopAgent { * }); * ``` */ - addContextListener(contextType: string | null, handler: ContextHandler): Promise; + addContextListener(contextType: ContextType | null, handler: ContextHandler): Promise; /** * Retrieves a list of the User channels available for the app to join. @@ -561,7 +563,7 @@ export interface DesktopAgent { * await fdc3.raiseIntent("StartChat", context, appIntent.apps[0].name); * ``` */ - raiseIntent(intent: string, context: Context, name: string): Promise; + raiseIntent(intent: Intent, context: Context, name: string): Promise; /** * @deprecated version of `raiseIntentForContext` that targets an app by by name rather than `AppIdentifier`. Provided for backwards compatibility with versions FDC3 standard <2.0. diff --git a/src/api/IntentMetadata.ts b/src/api/IntentMetadata.ts index e5ca94474..7a55a41a1 100644 --- a/src/api/IntentMetadata.ts +++ b/src/api/IntentMetadata.ts @@ -3,12 +3,14 @@ * Copyright FINOS FDC3 contributors - see NOTICE file */ +import { Intent } from '../intents/Intents'; + /** * Intent descriptor */ export interface IntentMetadata { /** The unique name of the intent that can be invoked by the raiseIntent call */ - readonly name: string; + readonly name: Intent; /** Display name for the intent. * @deprecated Use the intent name for display as display name may vary for diff --git a/src/api/IntentResolution.ts b/src/api/IntentResolution.ts index 82a899d64..ad4f0e374 100644 --- a/src/api/IntentResolution.ts +++ b/src/api/IntentResolution.ts @@ -5,6 +5,7 @@ import { IntentResult } from './Types'; import { AppIdentifier } from './AppIdentifier'; +import { Intent } from '../intents/Intents'; /** * IntentResolution provides a standard format for data returned upon resolving an intent. @@ -43,7 +44,7 @@ export interface IntentResolution { * The intent that was raised. May be used to determine which intent the user * chose in response to `fdc3.raiseIntentForContext()`. */ - readonly intent: string; + readonly intent: Intent; /** * The version number of the Intents schema being used. */ diff --git a/src/api/Methods.ts b/src/api/Methods.ts index 14288d4fd..dcc9eef57 100644 --- a/src/api/Methods.ts +++ b/src/api/Methods.ts @@ -14,7 +14,13 @@ import { ImplementationMetadata, AppMetadata, PrivateChannel, + Intent, + StandardContextType, + StandardIntent, + ContextType, } from '..'; +import { StandardContextsSet } from '../internal/contextConfiguration'; +import { StandardIntentsSet } from '../internal/intentConfiguration'; const DEFAULT_TIMEOUT = 5000; @@ -74,7 +80,7 @@ export function open(app: AppIdentifier | string, context?: Context): Promise { +export function findIntent(intent: Intent, context?: Context, resultType?: string): Promise { return rejectIfNoGlobal(() => window.fdc3.findIntent(intent, context, resultType)); } @@ -86,7 +92,7 @@ export function broadcast(context: Context): Promise { return rejectIfNoGlobal(() => window.fdc3.broadcast(context)); } -export function raiseIntent(intent: string, context: Context, app?: AppIdentifier | string): Promise { +export function raiseIntent(intent: Intent, context: Context, app?: AppIdentifier | string): Promise { if (isString(app)) { return rejectIfNoGlobal(() => window.fdc3.raiseIntent(intent, context, app)); } else { @@ -102,12 +108,12 @@ export function raiseIntentForContext(context: Context, app?: AppIdentifier | st } } -export function addIntentListener(intent: string, handler: IntentHandler): Promise { +export function addIntentListener(intent: Intent, handler: IntentHandler): Promise { return rejectIfNoGlobal(() => window.fdc3.addIntentListener(intent, handler)); } export function addContextListener( - contextTypeOrHandler: string | null | ContextHandler, + contextTypeOrHandler: ContextType | null | ContextHandler, handler?: ContextHandler ): Promise { //Handle (deprecated) function signature that allowed contextType argument to be omitted @@ -178,6 +184,22 @@ export function findInstances(app: AppIdentifier): Promise { return rejectIfNoGlobal(() => window.fdc3.findInstances(app)); } +/** + * Check if the given context is a standard context type. + * @param contextType + */ +export function isStandardContextType(contextType: ContextType): contextType is StandardContextType { + return StandardContextsSet.has(contextType as StandardContextType); +} + +/** + * Check if the given intent is a standard intent. + * @param intent + */ +export function isStandardIntent(intent: Intent): intent is StandardIntent { + return StandardIntentsSet.has(intent as StandardIntent); +} + /** * Compare numeric semver version number strings (in the form `1.2.3`). * @@ -222,5 +244,5 @@ export const versionIsAtLeast: (metadata: ImplementationMetadata, version: strin version ) => { let comparison = compareVersionNumbers(metadata.fdc3Version, version); - return comparison === null ? null : comparison >= 0 ? true : false; + return comparison === null ? null : comparison >= 0; }; diff --git a/src/context/ContextType.ts b/src/context/ContextType.ts index 4771a9dc8..be3cf5016 100644 --- a/src/context/ContextType.ts +++ b/src/context/ContextType.ts @@ -2,6 +2,52 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright FINOS FDC3 contributors - see NOTICE file */ + +/** + * @see https://fdc3.finos.org/docs/context/spec#standard-context-types + */ +export type StandardContextType = + | 'fdc3.action' + | 'fdc3.chart' + | 'fdc3.chat.initSettings' + | 'fdc3.chat.message' + | 'fdc3.chat.room' + | 'fdc3.chat.searchCriteria' + | 'fdc3.contact' + | 'fdc3.contactList' + | 'fdc3.country' + | 'fdc3.currency' + | 'fdc3.email' + | 'fdc3.instrument' + | 'fdc3.instrumentList' + | 'fdc3.interaction' + | 'fdc3.message' + | 'fdc3.organization' + | 'fdc3.portfolio' + | 'fdc3.position' + | 'fdc3.nothing' + | 'fdc3.timerange' + | 'fdc3.transactionResult' + | 'fdc3.valuation'; + +/** + * @see https://fdc3.finos.org/docs/context/spec#standard-context-types + */ +export type ExperimentalContextType = + | 'fdc3.order' + | 'fdc3.orderList' + | 'fdc3.product' + | 'fdc3.trade' + | 'fdc3.tradeList'; + +/** + * @see https://fdc3.finos.org/docs/context/spec + */ +export type ContextType = StandardContextType | ExperimentalContextType | (string & {}); + +/** + * @deprecated Use {@link StandardContextType} instead + */ export enum ContextTypes { Chart = 'fdc3.chart', ChatInitSettings = 'fdc3.chat.initSettings', @@ -23,5 +69,3 @@ export enum ContextTypes { TransactionResult = 'fdc3.transactionResult', Valuation = 'fdc3.valuation', } - -export type ContextType = ContextTypes | string; diff --git a/src/intents/Intents.ts b/src/intents/Intents.ts index ed96abcce..caa83969c 100644 --- a/src/intents/Intents.ts +++ b/src/intents/Intents.ts @@ -2,6 +2,38 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright FINOS FDC3 contributors - see NOTICE file */ + +/** + * @see https://fdc3.finos.org/docs/intents/spec#standard-intents + */ +export type StandardIntent = + | 'CreateInteraction' + | 'SendChatMessage' + | 'StartCall' + | 'StartChat' + | 'StartEmail' + | 'ViewAnalysis' + | 'ViewChat' + | 'ViewChart' + | 'ViewContact' + | 'ViewHoldings' + | 'ViewInstrument' + | 'ViewInteractions' + | 'ViewMessages' + | 'ViewNews' + | 'ViewOrders' + | 'ViewProfile' + | 'ViewQuote' + | 'ViewResearch'; + +/** + * @see https://fdc3.finos.org/docs/intents/spec + */ +export type Intent = StandardIntent | (string & {}); + +/** + * @deprecated Use {@link StandardIntent} instead + */ export enum Intents { CreateInteraction = 'CreateInteraction', SendChatMessage = 'SendChatMessage', diff --git a/src/internal/contextConfiguration.ts b/src/internal/contextConfiguration.ts new file mode 100644 index 000000000..6aed131e8 --- /dev/null +++ b/src/internal/contextConfiguration.ts @@ -0,0 +1,30 @@ +import { StandardContextType } from '../context/ContextType'; +import { exhaustiveStringTuple } from './typeHelpers'; + +const STANDARD_CONTEXT_TYPES = exhaustiveStringTuple()( + 'fdc3.action', + 'fdc3.chart', + 'fdc3.chat.initSettings', + 'fdc3.chat.message', + 'fdc3.chat.room', + 'fdc3.chat.searchCriteria', + 'fdc3.contact', + 'fdc3.contactList', + 'fdc3.country', + 'fdc3.currency', + 'fdc3.email', + 'fdc3.instrument', + 'fdc3.instrumentList', + 'fdc3.interaction', + 'fdc3.message', + 'fdc3.organization', + 'fdc3.portfolio', + 'fdc3.position', + 'fdc3.nothing', + 'fdc3.timerange', + 'fdc3.transactionResult', + 'fdc3.valuation' +); + +// used internally to check if a given intent/context is a standard one +export const StandardContextsSet = new Set(STANDARD_CONTEXT_TYPES); diff --git a/src/internal/intentConfiguration.ts b/src/internal/intentConfiguration.ts new file mode 100644 index 000000000..1e94c0173 --- /dev/null +++ b/src/internal/intentConfiguration.ts @@ -0,0 +1,26 @@ +import { StandardIntent } from '../intents/Intents'; +import { exhaustiveStringTuple } from './typeHelpers'; + +const STANDARD_INTENTS = exhaustiveStringTuple()( + 'CreateInteraction', + 'SendChatMessage', + 'StartCall', + 'StartChat', + 'StartEmail', + 'ViewAnalysis', + 'ViewChat', + 'ViewChart', + 'ViewContact', + 'ViewHoldings', + 'ViewInstrument', + 'ViewInteractions', + 'ViewMessages', + 'ViewNews', + 'ViewOrders', + 'ViewProfile', + 'ViewQuote', + 'ViewResearch' +); + +// used internally to check if a given intent/context is a standard one +export const StandardIntentsSet = new Set(STANDARD_INTENTS); diff --git a/src/internal/typeHelpers.ts b/src/internal/typeHelpers.ts new file mode 100644 index 000000000..bc3583497 --- /dev/null +++ b/src/internal/typeHelpers.ts @@ -0,0 +1,12 @@ +type AtLeastOne = [T, ...T[]]; + +/** + * Ensures at compile time that the given string tuple is exhaustive on a given union type, i.e. contains ALL possible values of the given UNION_TYPE. + */ +export const exhaustiveStringTuple = () => >( + ...tuple: L extends any + ? Exclude extends never + ? L + : Exclude[] + : never +) => tuple; diff --git a/test/Methods.test.ts b/test/Methods.test.ts index 7927270ae..14c036cc1 100644 --- a/test/Methods.test.ts +++ b/test/Methods.test.ts @@ -5,7 +5,6 @@ import { broadcast, compareVersionNumbers, ContextHandler, - ContextTypes, DesktopAgent, fdc3Ready, findIntent, @@ -26,6 +25,8 @@ import { versionIsAtLeast, createPrivateChannel, findInstances, + isStandardContextType, + isStandardIntent, } from '../src'; const UnavailableError = new Error('FDC3 DesktopAgent not available at `window.fdc3`.'); @@ -33,7 +34,7 @@ const TimeoutError = new Error('Timed out waiting for `fdc3Ready` event.'); const UnexpectedError = new Error('`fdc3Ready` event fired, but `window.fdc3` not set to DesktopAgent.'); const ContactContext = { - type: ContextTypes.Contact, + type: 'fdc3.contact', id: { email: 'test@example.com' }, }; @@ -163,10 +164,10 @@ describe('test ES6 module', () => { test('findIntent should delegate to window.fdc3.findIntent (with additional output type argument)', async () => { const intent = 'ViewChart'; - await findIntent(intent, ContactContext, ContextTypes.Contact); + await findIntent(intent, ContactContext, 'fdc3.contact'); expect(window.fdc3.findIntent).toHaveBeenCalledTimes(1); - expect(window.fdc3.findIntent).toHaveBeenLastCalledWith(intent, ContactContext, ContextTypes.Contact); + expect(window.fdc3.findIntent).toHaveBeenLastCalledWith(intent, ContactContext, 'fdc3.contact'); }); test('findIntentsByContext should delegate to window.fdc3.findIntentsByContext', async () => { @@ -177,10 +178,10 @@ describe('test ES6 module', () => { }); test('findIntentsByContext should delegate to window.fdc3.findIntentsByContext (with additional output type argument)', async () => { - await findIntentsByContext(ContactContext, ContextTypes.Contact); + await findIntentsByContext(ContactContext, 'fdc3.contact'); expect(window.fdc3.findIntentsByContext).toHaveBeenCalledTimes(1); - expect(window.fdc3.findIntentsByContext).toHaveBeenLastCalledWith(ContactContext, ContextTypes.Contact); + expect(window.fdc3.findIntentsByContext).toHaveBeenLastCalledWith(ContactContext, 'fdc3.contact'); }); test('broadcast should delegate to window.fdc3.broadcast', async () => { @@ -444,4 +445,58 @@ describe('test version comparison functions', () => { expect(versionIsAtLeast(metaOneTwoOne, '1.2.1')).toBe(true); expect(versionIsAtLeast(metaOneTwoOne, '2.0')).toBe(false); }); + + test('isStandardContextType should return TRUE for standard context types', () => { + expect(isStandardContextType('fdc3.action')).toBe(true); + expect(isStandardContextType('fdc3.chart')).toBe(true); + expect(isStandardContextType('fdc3.chat.initSettings')).toBe(true); + expect(isStandardContextType('fdc3.chat.message')).toBe(true); + expect(isStandardContextType('fdc3.chat.room')).toBe(true); + expect(isStandardContextType('fdc3.chat.searchCriteria')).toBe(true); + expect(isStandardContextType('fdc3.contact')).toBe(true); + expect(isStandardContextType('fdc3.contactList')).toBe(true); + expect(isStandardContextType('fdc3.country')).toBe(true); + expect(isStandardContextType('fdc3.currency')).toBe(true); + expect(isStandardContextType('fdc3.email')).toBe(true); + expect(isStandardContextType('fdc3.instrument')).toBe(true); + expect(isStandardContextType('fdc3.instrumentList')).toBe(true); + expect(isStandardContextType('fdc3.interaction')).toBe(true); + expect(isStandardContextType('fdc3.message')).toBe(true); + expect(isStandardContextType('fdc3.organization')).toBe(true); + expect(isStandardContextType('fdc3.portfolio')).toBe(true); + expect(isStandardContextType('fdc3.position')).toBe(true); + expect(isStandardContextType('fdc3.nothing')).toBe(true); + expect(isStandardContextType('fdc3.timerange')).toBe(true); + expect(isStandardContextType('fdc3.transactionResult')).toBe(true); + expect(isStandardContextType('fdc3.valuation')).toBe(true); + }); + + test('isStandardContextType should return FALSE for custom context types', () => { + expect(isStandardContextType('myApp.customContext')).toBe(false); + }); + + test('isStandardIntent should return TRUE for standard intents', () => { + expect(isStandardIntent('CreateInteraction')).toBe(true); + expect(isStandardIntent('SendChatMessage')).toBe(true); + expect(isStandardIntent('StartCall')).toBe(true); + expect(isStandardIntent('StartChat')).toBe(true); + expect(isStandardIntent('StartEmail')).toBe(true); + expect(isStandardIntent('ViewAnalysis')).toBe(true); + expect(isStandardIntent('ViewChat')).toBe(true); + expect(isStandardIntent('ViewChart')).toBe(true); + expect(isStandardIntent('ViewContact')).toBe(true); + expect(isStandardIntent('ViewHoldings')).toBe(true); + expect(isStandardIntent('ViewInstrument')).toBe(true); + expect(isStandardIntent('ViewInteractions')).toBe(true); + expect(isStandardIntent('ViewMessages')).toBe(true); + expect(isStandardIntent('ViewNews')).toBe(true); + expect(isStandardIntent('ViewOrders')).toBe(true); + expect(isStandardIntent('ViewProfile')).toBe(true); + expect(isStandardIntent('ViewQuote')).toBe(true); + expect(isStandardIntent('ViewResearch')).toBe(true); + }); + + test('isStandardIntent should return FALSE for custom intents', () => { + expect(isStandardIntent('myApp.CustomIntent')).toBe(false); + }); });