From 6c0128fde7a73e1cc11aedc21f1775dbadd5fc38 Mon Sep 17 00:00:00 2001 From: Andrei Floricel Date: Fri, 22 Dec 2023 20:24:12 +0200 Subject: [PATCH 1/8] Refactors ContextTypes to union type instead of enum (#1138) Signed-off-by: Andrei Floricel --- src/context/ContextType.ts | 53 ++++++++++++++++++++++---------------- test/Methods.test.ts | 11 ++++---- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/src/context/ContextType.ts b/src/context/ContextType.ts index 4771a9dc8..27975f2e9 100644 --- a/src/context/ContextType.ts +++ b/src/context/ContextType.ts @@ -2,26 +2,35 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright FINOS FDC3 contributors - see NOTICE file */ -export enum ContextTypes { - Chart = 'fdc3.chart', - ChatInitSettings = 'fdc3.chat.initSettings', - ChatRoom = 'fdc3.chat.room', - Contact = 'fdc3.contact', - ContactList = 'fdc3.contactList', - Country = 'fdc3.country', - Currency = 'fdc3.currency', - Email = 'fdc3.email', - Instrument = 'fdc3.instrument', - InstrumentList = 'fdc3.instrumentList', - Interaction = 'fdc3.interaction', - Nothing = 'fdc3.nothing', - Organization = 'fdc3.organization', - Portfolio = 'fdc3.portfolio', - Position = 'fdc3.position', - ChatSearchCriteria = 'fdc3.chat.searchCriteria', - TimeRange = 'fdc3.timerange', - TransactionResult = 'fdc3.transactionResult', - Valuation = 'fdc3.valuation', -} -export type ContextType = ContextTypes | string; +/** + * @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 + */ +export type ContextType = StandardContextType | string; diff --git a/test/Methods.test.ts b/test/Methods.test.ts index 7927270ae..ca9ab2b82 100644 --- a/test/Methods.test.ts +++ b/test/Methods.test.ts @@ -5,7 +5,6 @@ import { broadcast, compareVersionNumbers, ContextHandler, - ContextTypes, DesktopAgent, fdc3Ready, findIntent, @@ -33,7 +32,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 +162,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 +176,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 () => { From f3e8d084b250b55cf3749ad80ee0deaa46392e75 Mon Sep 17 00:00:00 2001 From: Andrei Floricel Date: Fri, 16 Feb 2024 16:57:52 +0200 Subject: [PATCH 2/8] Add experimental context types Signed-off-by: Andrei Floricel --- src/context/ContextType.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/context/ContextType.ts b/src/context/ContextType.ts index 27975f2e9..1069ba68a 100644 --- a/src/context/ContextType.ts +++ b/src/context/ContextType.ts @@ -30,7 +30,17 @@ export type StandardContextType = | '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 | string; +export type ContextType = StandardContextType | ExperimentalContextType | (string & {}); From a19a6335e03b249eaaab9ce4ea12601dd3e5b362 Mon Sep 17 00:00:00 2001 From: Andrei Floricel Date: Fri, 16 Feb 2024 17:22:59 +0200 Subject: [PATCH 3/8] Refactor standard intents declaration to use union types instead of enums Signed-off-by: Andrei Floricel --- src/api/DesktopAgent.ts | 11 ++++---- src/api/IntentMetadata.ts | 4 ++- src/api/IntentResolution.ts | 3 ++- src/api/Methods.ts | 7 ++--- src/bridging/BridgingTypes.ts | 14 +++++----- src/intents/Intents.ts | 48 ++++++++++++++++++++--------------- 6 files changed, 51 insertions(+), 36 deletions(-) diff --git a/src/api/DesktopAgent.ts b/src/api/DesktopAgent.ts index f9b989c2b..3e72739b0 100644 --- a/src/api/DesktopAgent.ts +++ b/src/api/DesktopAgent.ts @@ -13,6 +13,7 @@ import { ImplementationMetadata } from './ImplementationMetadata'; import { PrivateChannel } from './PrivateChannel'; import { AppIdentifier } from './AppIdentifier'; import { AppMetadata } from './AppMetadata'; +import { Intent } from '../intents/Intents'; /** * A Desktop Agent is a desktop component (or aggregate of components) that serves as a @@ -125,7 +126,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 +235,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 +269,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 +347,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. @@ -561,7 +562,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..b82798ef6 100644 --- a/src/api/Methods.ts +++ b/src/api/Methods.ts @@ -14,6 +14,7 @@ import { ImplementationMetadata, AppMetadata, PrivateChannel, + Intent, } from '..'; const DEFAULT_TIMEOUT = 5000; @@ -74,7 +75,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 +87,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,7 +103,7 @@ 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)); } diff --git a/src/bridging/BridgingTypes.ts b/src/bridging/BridgingTypes.ts index 68f27737f..676f1195e 100644 --- a/src/bridging/BridgingTypes.ts +++ b/src/bridging/BridgingTypes.ts @@ -75,6 +75,8 @@ // These functions will throw an error if the JSON doesn't // match the expected interface, even if the JSON is valid. +import { Intent } from '../intents/Intents'; + /** * Metadata relating to the FDC3 Desktop Agent implementation and its provider. */ @@ -1770,7 +1772,7 @@ export interface FindIntentAgentRequestMeta { */ export interface FindIntentAgentRequestPayload { context?: ContextElement; - intent: string; + intent: Intent; resultType?: string; } @@ -1844,7 +1846,7 @@ export interface IntentMetadata { /** * The unique name of the intent that can be invoked by the raiseIntent call */ - name: string; + name: Intent; } /** @@ -1929,7 +1931,7 @@ export interface FindIntentBridgeRequestMeta { */ export interface FindIntentBridgeRequestPayload { context?: ContextElement; - intent: string; + intent: Intent; resultType?: string; } @@ -3697,7 +3699,7 @@ export interface RaiseIntentAgentRequestMeta { export interface RaiseIntentAgentRequestPayload { app: AppDestinationIdentifier; context: ContextElement; - intent: string; + intent: Intent; } /** @@ -3774,7 +3776,7 @@ export interface IntentResolution { * The intent that was raised. May be used to determine which intent the user * chose in response to `fdc3.raiseIntentForContext()`. */ - intent: string; + intent: Intent; /** * Identifier for the app instance that was selected (or started) to resolve the intent. * `source.instanceId` MUST be set, indicating the specific app instance that @@ -3870,7 +3872,7 @@ export interface RaiseIntentBridgeRequestMeta { export interface RaiseIntentBridgeRequestPayload { app: AppDestinationIdentifier; context: ContextElement; - intent: string; + intent: Intent; } /** diff --git a/src/intents/Intents.ts b/src/intents/Intents.ts index ed96abcce..e1dd2da02 100644 --- a/src/intents/Intents.ts +++ b/src/intents/Intents.ts @@ -2,23 +2,31 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright FINOS FDC3 contributors - see NOTICE file */ -export enum Intents { - CreateInteraction = 'CreateInteraction', - SendChatMessage = 'SendChatMessage', - StartCall = 'StartCall', - StartChat = 'StartChat', - StartEmail = 'StartEmail', - ViewAnalysis = 'ViewAnalysis', - ViewChat = 'ViewChat', - ViewChart = 'ViewChart', - ViewContact = 'ViewContact', - ViewHoldings = 'ViewHoldings', - ViewInstrument = 'ViewInstrument', - ViewInteractions = 'ViewInteractions', - ViewMessages = 'ViewMessages', - ViewNews = 'ViewNews', - ViewOrders = 'ViewOrders', - ViewProfile = 'ViewProfile', - ViewQuote = 'ViewQuote', - ViewResearch = 'ViewResearch', -} + +/** + * @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 & {}); From e5449d74de93d50e931eea7177f0573793435b9f Mon Sep 17 00:00:00 2001 From: Andrei Floricel Date: Fri, 16 Feb 2024 19:02:33 +0200 Subject: [PATCH 4/8] Add various utility functions Signed-off-by: Andrei Floricel --- .prettierignore | 3 ++ package-lock.json | 4 +- src/api/Methods.ts | 28 ++++++++++++ src/intents/Intents.ts | 9 ++++ src/intents/IntentsConfiguration.ts | 66 +++++++++++++++++++++++++++++ test/Methods.test.ts | 56 ++++++++++++++++++++++++ 6 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 src/intents/IntentsConfiguration.ts diff --git a/.prettierignore b/.prettierignore index cd3688288..9268d64db 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,3 +9,6 @@ website dist src/app-directory/*/target + +# temporary, until Prettier is upgraded to at least v2.8, which supports the TS 'satisfies' syntax +src/**/IntentsConfiguration.ts 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/Methods.ts b/src/api/Methods.ts index b82798ef6..77cc4a65c 100644 --- a/src/api/Methods.ts +++ b/src/api/Methods.ts @@ -15,7 +15,11 @@ import { AppMetadata, PrivateChannel, Intent, + StandardContextType, + StandardIntent, + ContextTypeFor, } from '..'; +import { IntentsConfiguration, StandardContextsSet, StandardIntentsSet } from '../intents/IntentsConfiguration'; const DEFAULT_TIMEOUT = 5000; @@ -179,6 +183,30 @@ 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: string): contextType is StandardContextType { + return StandardContextsSet.has(contextType as StandardContextType); +} + +/** + * Check if the given intent is a standard intent. + * @param intent + */ +export function isStandardIntent(intent: string): intent is StandardIntent { + return StandardIntentsSet.has(intent as StandardIntent); +} + +/** + * Get the possible context types for a given intent. + * @param intent + */ +export function getPossibleContextsForIntent(intent: I): ContextTypeFor[] { + return IntentsConfiguration[intent] ?? []; +} + /** * Compare numeric semver version number strings (in the form `1.2.3`). * diff --git a/src/intents/Intents.ts b/src/intents/Intents.ts index e1dd2da02..9335c813b 100644 --- a/src/intents/Intents.ts +++ b/src/intents/Intents.ts @@ -3,6 +3,8 @@ * Copyright FINOS FDC3 contributors - see NOTICE file */ +import { IntentsConfiguration } from './IntentsConfiguration'; + /** * @see https://fdc3.finos.org/docs/intents/spec#standard-intents */ @@ -30,3 +32,10 @@ export type StandardIntent = * @see https://fdc3.finos.org/docs/intents/spec */ export type Intent = StandardIntent | (string & {}); + +/** + * Typed possible context for a given intent + * + * @example `ContextTypeFor<'StartCall'>` is equivalent to `'fdc3.contact' | 'fdc3.contactList' | 'fdc3.nothing'` + */ +export type ContextTypeFor = typeof IntentsConfiguration[I][number]; diff --git a/src/intents/IntentsConfiguration.ts b/src/intents/IntentsConfiguration.ts new file mode 100644 index 000000000..06beabf76 --- /dev/null +++ b/src/intents/IntentsConfiguration.ts @@ -0,0 +1,66 @@ +import { StandardIntent } from './Intents'; +import { StandardContextType } from '../context/ContextType'; + +export const IntentsConfiguration = { + CreateInteraction: ['fdc3.contactList', 'fdc3.interaction', 'fdc3.nothing'], + StartCall: ['fdc3.contact', 'fdc3.contactList', 'fdc3.nothing'], + StartChat: ['fdc3.contact', 'fdc3.contactList', 'fdc3.chat.initSettings', 'fdc3.nothing'], + StartEmail: ['fdc3.email', 'fdc3.nothing'], + SendChatMessage: ['fdc3.chat.message', 'fdc3.nothing'], + ViewAnalysis: ['fdc3.instrument', 'fdc3.organization', 'fdc3.portfolio', 'fdc3.nothing'], + ViewChat: ['fdc3.chat.room', 'fdc3.contact', 'fdc3.contactList', 'fdc3.nothing'], + ViewChart: [ + 'fdc3.chart', + 'fdc3.instrument', + 'fdc3.instrumentList', + 'fdc3.portfolio', + 'fdc3.position', + 'fdc3.nothing', + ], + ViewContact: ['fdc3.contact', 'fdc3.nothing'], + ViewHoldings: ['fdc3.instrument', 'fdc3.instrumentList', 'fdc3.organization', 'fdc3.nothing'], + ViewInstrument: ['fdc3.instrument', 'fdc3.nothing'], + ViewInteractions: ['fdc3.contact', 'fdc3.instrument', 'fdc3.organization', 'fdc3.nothing'], + ViewMessages: ['fdc3.chat.searchCriteria', 'fdc3.nothing'], + ViewNews: [ + 'fdc3.country', + 'fdc3.instrument', + 'fdc3.instrumentList', + 'fdc3.organization', + 'fdc3.portfolio', + 'fdc3.nothing', + ], + ViewOrders: ['fdc3.contact', 'fdc3.instrument', 'fdc3.organization', 'fdc3.nothing'], + ViewProfile: ['fdc3.contact', 'fdc3.organization', 'fdc3.nothing'], + ViewQuote: ['fdc3.instrument', 'fdc3.nothing'], + ViewResearch: ['fdc3.contact', 'fdc3.instrument', 'fdc3.organization', 'fdc3.nothing'], +} satisfies Record; + +const STANDARD_CONTEXT_TYPES: Readonly = [ + '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', +] as const; + +// used internally to check if a given intent/context is a standard one +export const StandardIntentsSet = new Set(Object.keys(IntentsConfiguration) as StandardIntent[]); +export const StandardContextsSet = new Set(STANDARD_CONTEXT_TYPES); diff --git a/test/Methods.test.ts b/test/Methods.test.ts index ca9ab2b82..14c036cc1 100644 --- a/test/Methods.test.ts +++ b/test/Methods.test.ts @@ -25,6 +25,8 @@ import { versionIsAtLeast, createPrivateChannel, findInstances, + isStandardContextType, + isStandardIntent, } from '../src'; const UnavailableError = new Error('FDC3 DesktopAgent not available at `window.fdc3`.'); @@ -443,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); + }); }); From 265eed8dbddc5bfc3b9c90e1b30b6a63fde81144 Mon Sep 17 00:00:00 2001 From: Andrei Floricel Date: Fri, 16 Feb 2024 19:38:27 +0200 Subject: [PATCH 5/8] Remove TS 'satisfies' syntax to fix build Signed-off-by: Andrei Floricel --- .prettierignore | 3 --- src/bridging/BridgingTypes.ts | 14 ++++++-------- src/intents/IntentsConfiguration.ts | 5 ++++- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/.prettierignore b/.prettierignore index 9268d64db..cd3688288 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,6 +9,3 @@ website dist src/app-directory/*/target - -# temporary, until Prettier is upgraded to at least v2.8, which supports the TS 'satisfies' syntax -src/**/IntentsConfiguration.ts diff --git a/src/bridging/BridgingTypes.ts b/src/bridging/BridgingTypes.ts index 676f1195e..68f27737f 100644 --- a/src/bridging/BridgingTypes.ts +++ b/src/bridging/BridgingTypes.ts @@ -75,8 +75,6 @@ // These functions will throw an error if the JSON doesn't // match the expected interface, even if the JSON is valid. -import { Intent } from '../intents/Intents'; - /** * Metadata relating to the FDC3 Desktop Agent implementation and its provider. */ @@ -1772,7 +1770,7 @@ export interface FindIntentAgentRequestMeta { */ export interface FindIntentAgentRequestPayload { context?: ContextElement; - intent: Intent; + intent: string; resultType?: string; } @@ -1846,7 +1844,7 @@ export interface IntentMetadata { /** * The unique name of the intent that can be invoked by the raiseIntent call */ - name: Intent; + name: string; } /** @@ -1931,7 +1929,7 @@ export interface FindIntentBridgeRequestMeta { */ export interface FindIntentBridgeRequestPayload { context?: ContextElement; - intent: Intent; + intent: string; resultType?: string; } @@ -3699,7 +3697,7 @@ export interface RaiseIntentAgentRequestMeta { export interface RaiseIntentAgentRequestPayload { app: AppDestinationIdentifier; context: ContextElement; - intent: Intent; + intent: string; } /** @@ -3776,7 +3774,7 @@ export interface IntentResolution { * The intent that was raised. May be used to determine which intent the user * chose in response to `fdc3.raiseIntentForContext()`. */ - intent: Intent; + intent: string; /** * Identifier for the app instance that was selected (or started) to resolve the intent. * `source.instanceId` MUST be set, indicating the specific app instance that @@ -3872,7 +3870,7 @@ export interface RaiseIntentBridgeRequestMeta { export interface RaiseIntentBridgeRequestPayload { app: AppDestinationIdentifier; context: ContextElement; - intent: Intent; + intent: string; } /** diff --git a/src/intents/IntentsConfiguration.ts b/src/intents/IntentsConfiguration.ts index 06beabf76..deec04be6 100644 --- a/src/intents/IntentsConfiguration.ts +++ b/src/intents/IntentsConfiguration.ts @@ -34,7 +34,10 @@ export const IntentsConfiguration = { ViewProfile: ['fdc3.contact', 'fdc3.organization', 'fdc3.nothing'], ViewQuote: ['fdc3.instrument', 'fdc3.nothing'], ViewResearch: ['fdc3.contact', 'fdc3.instrument', 'fdc3.organization', 'fdc3.nothing'], -} satisfies Record; +}; +// TODO - TSDX's Prettier version does not support 'satisfies' construct +// that's unfortunate because this syntax ensures that the IntentsConfiguration Map is always kep in sync with the StandardIntent type +// satisfies Record; const STANDARD_CONTEXT_TYPES: Readonly = [ 'fdc3.action', From 87b859ee3dae81fff00e56108f8c7f4b2b066e3d Mon Sep 17 00:00:00 2001 From: Andrei Floricel Date: Mon, 19 Feb 2024 11:58:56 +0200 Subject: [PATCH 6/8] Improve typing Signed-off-by: Andrei Floricel --- src/api/DesktopAgent.ts | 3 ++- src/api/Methods.ts | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/api/DesktopAgent.ts b/src/api/DesktopAgent.ts index 3e72739b0..a6a5328cb 100644 --- a/src/api/DesktopAgent.ts +++ b/src/api/DesktopAgent.ts @@ -14,6 +14,7 @@ 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 @@ -372,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. diff --git a/src/api/Methods.ts b/src/api/Methods.ts index 77cc4a65c..251560caf 100644 --- a/src/api/Methods.ts +++ b/src/api/Methods.ts @@ -18,6 +18,7 @@ import { StandardContextType, StandardIntent, ContextTypeFor, + ContextType, } from '..'; import { IntentsConfiguration, StandardContextsSet, StandardIntentsSet } from '../intents/IntentsConfiguration'; @@ -112,7 +113,7 @@ export function addIntentListener(intent: Intent, handler: IntentHandler): Promi } 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 @@ -187,7 +188,7 @@ export function findInstances(app: AppIdentifier): Promise { * Check if the given context is a standard context type. * @param contextType */ -export function isStandardContextType(contextType: string): contextType is StandardContextType { +export function isStandardContextType(contextType: ContextType): contextType is StandardContextType { return StandardContextsSet.has(contextType as StandardContextType); } @@ -195,7 +196,7 @@ export function isStandardContextType(contextType: string): contextType is Stand * Check if the given intent is a standard intent. * @param intent */ -export function isStandardIntent(intent: string): intent is StandardIntent { +export function isStandardIntent(intent: Intent): intent is StandardIntent { return StandardIntentsSet.has(intent as StandardIntent); } From 972ff91bfd97ca8d29d3d1f253349e29729853a5 Mon Sep 17 00:00:00 2001 From: Andrei Floricel Date: Wed, 21 Feb 2024 16:01:26 +0200 Subject: [PATCH 7/8] Add deprecated types to maintain backward compatibility Signed-off-by: Andrei Floricel --- src/context/ContextType.ts | 25 +++++++++++++++++++++++++ src/intents/Intents.ts | 24 ++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/context/ContextType.ts b/src/context/ContextType.ts index 1069ba68a..be3cf5016 100644 --- a/src/context/ContextType.ts +++ b/src/context/ContextType.ts @@ -44,3 +44,28 @@ export type ExperimentalContextType = * @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', + ChatRoom = 'fdc3.chat.room', + Contact = 'fdc3.contact', + ContactList = 'fdc3.contactList', + Country = 'fdc3.country', + Currency = 'fdc3.currency', + Email = 'fdc3.email', + Instrument = 'fdc3.instrument', + InstrumentList = 'fdc3.instrumentList', + Interaction = 'fdc3.interaction', + Nothing = 'fdc3.nothing', + Organization = 'fdc3.organization', + Portfolio = 'fdc3.portfolio', + Position = 'fdc3.position', + ChatSearchCriteria = 'fdc3.chat.searchCriteria', + TimeRange = 'fdc3.timerange', + TransactionResult = 'fdc3.transactionResult', + Valuation = 'fdc3.valuation', +} diff --git a/src/intents/Intents.ts b/src/intents/Intents.ts index 9335c813b..b0f1f7fd1 100644 --- a/src/intents/Intents.ts +++ b/src/intents/Intents.ts @@ -39,3 +39,27 @@ export type Intent = StandardIntent | (string & {}); * @example `ContextTypeFor<'StartCall'>` is equivalent to `'fdc3.contact' | 'fdc3.contactList' | 'fdc3.nothing'` */ export type ContextTypeFor = typeof IntentsConfiguration[I][number]; + +/** + * @deprecated Use {@link StandardIntent} instead + */ +export enum Intents { + CreateInteraction = 'CreateInteraction', + SendChatMessage = 'SendChatMessage', + StartCall = 'StartCall', + StartChat = 'StartChat', + StartEmail = 'StartEmail', + ViewAnalysis = 'ViewAnalysis', + ViewChat = 'ViewChat', + ViewChart = 'ViewChart', + ViewContact = 'ViewContact', + ViewHoldings = 'ViewHoldings', + ViewInstrument = 'ViewInstrument', + ViewInteractions = 'ViewInteractions', + ViewMessages = 'ViewMessages', + ViewNews = 'ViewNews', + ViewOrders = 'ViewOrders', + ViewProfile = 'ViewProfile', + ViewQuote = 'ViewQuote', + ViewResearch = 'ViewResearch', +} From 17cf9e62ad1a87d7faadc993f0cfe4ea67d852ac Mon Sep 17 00:00:00 2001 From: Andrei Floricel Date: Tue, 12 Mar 2024 10:00:03 +0200 Subject: [PATCH 8/8] Implement code review suggestions Signed-off-by: Andrei Floricel --- src/api/Methods.ts | 14 ++---- src/intents/Intents.ts | 9 ---- src/intents/IntentsConfiguration.ts | 69 ---------------------------- src/internal/contextConfiguration.ts | 30 ++++++++++++ src/internal/intentConfiguration.ts | 26 +++++++++++ src/internal/typeHelpers.ts | 12 +++++ 6 files changed, 71 insertions(+), 89 deletions(-) delete mode 100644 src/intents/IntentsConfiguration.ts create mode 100644 src/internal/contextConfiguration.ts create mode 100644 src/internal/intentConfiguration.ts create mode 100644 src/internal/typeHelpers.ts diff --git a/src/api/Methods.ts b/src/api/Methods.ts index 251560caf..dcc9eef57 100644 --- a/src/api/Methods.ts +++ b/src/api/Methods.ts @@ -17,10 +17,10 @@ import { Intent, StandardContextType, StandardIntent, - ContextTypeFor, ContextType, } from '..'; -import { IntentsConfiguration, StandardContextsSet, StandardIntentsSet } from '../intents/IntentsConfiguration'; +import { StandardContextsSet } from '../internal/contextConfiguration'; +import { StandardIntentsSet } from '../internal/intentConfiguration'; const DEFAULT_TIMEOUT = 5000; @@ -200,14 +200,6 @@ export function isStandardIntent(intent: Intent): intent is StandardIntent { return StandardIntentsSet.has(intent as StandardIntent); } -/** - * Get the possible context types for a given intent. - * @param intent - */ -export function getPossibleContextsForIntent(intent: I): ContextTypeFor[] { - return IntentsConfiguration[intent] ?? []; -} - /** * Compare numeric semver version number strings (in the form `1.2.3`). * @@ -252,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/intents/Intents.ts b/src/intents/Intents.ts index b0f1f7fd1..caa83969c 100644 --- a/src/intents/Intents.ts +++ b/src/intents/Intents.ts @@ -3,8 +3,6 @@ * Copyright FINOS FDC3 contributors - see NOTICE file */ -import { IntentsConfiguration } from './IntentsConfiguration'; - /** * @see https://fdc3.finos.org/docs/intents/spec#standard-intents */ @@ -33,13 +31,6 @@ export type StandardIntent = */ export type Intent = StandardIntent | (string & {}); -/** - * Typed possible context for a given intent - * - * @example `ContextTypeFor<'StartCall'>` is equivalent to `'fdc3.contact' | 'fdc3.contactList' | 'fdc3.nothing'` - */ -export type ContextTypeFor = typeof IntentsConfiguration[I][number]; - /** * @deprecated Use {@link StandardIntent} instead */ diff --git a/src/intents/IntentsConfiguration.ts b/src/intents/IntentsConfiguration.ts deleted file mode 100644 index deec04be6..000000000 --- a/src/intents/IntentsConfiguration.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { StandardIntent } from './Intents'; -import { StandardContextType } from '../context/ContextType'; - -export const IntentsConfiguration = { - CreateInteraction: ['fdc3.contactList', 'fdc3.interaction', 'fdc3.nothing'], - StartCall: ['fdc3.contact', 'fdc3.contactList', 'fdc3.nothing'], - StartChat: ['fdc3.contact', 'fdc3.contactList', 'fdc3.chat.initSettings', 'fdc3.nothing'], - StartEmail: ['fdc3.email', 'fdc3.nothing'], - SendChatMessage: ['fdc3.chat.message', 'fdc3.nothing'], - ViewAnalysis: ['fdc3.instrument', 'fdc3.organization', 'fdc3.portfolio', 'fdc3.nothing'], - ViewChat: ['fdc3.chat.room', 'fdc3.contact', 'fdc3.contactList', 'fdc3.nothing'], - ViewChart: [ - 'fdc3.chart', - 'fdc3.instrument', - 'fdc3.instrumentList', - 'fdc3.portfolio', - 'fdc3.position', - 'fdc3.nothing', - ], - ViewContact: ['fdc3.contact', 'fdc3.nothing'], - ViewHoldings: ['fdc3.instrument', 'fdc3.instrumentList', 'fdc3.organization', 'fdc3.nothing'], - ViewInstrument: ['fdc3.instrument', 'fdc3.nothing'], - ViewInteractions: ['fdc3.contact', 'fdc3.instrument', 'fdc3.organization', 'fdc3.nothing'], - ViewMessages: ['fdc3.chat.searchCriteria', 'fdc3.nothing'], - ViewNews: [ - 'fdc3.country', - 'fdc3.instrument', - 'fdc3.instrumentList', - 'fdc3.organization', - 'fdc3.portfolio', - 'fdc3.nothing', - ], - ViewOrders: ['fdc3.contact', 'fdc3.instrument', 'fdc3.organization', 'fdc3.nothing'], - ViewProfile: ['fdc3.contact', 'fdc3.organization', 'fdc3.nothing'], - ViewQuote: ['fdc3.instrument', 'fdc3.nothing'], - ViewResearch: ['fdc3.contact', 'fdc3.instrument', 'fdc3.organization', 'fdc3.nothing'], -}; -// TODO - TSDX's Prettier version does not support 'satisfies' construct -// that's unfortunate because this syntax ensures that the IntentsConfiguration Map is always kep in sync with the StandardIntent type -// satisfies Record; - -const STANDARD_CONTEXT_TYPES: Readonly = [ - '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', -] as const; - -// used internally to check if a given intent/context is a standard one -export const StandardIntentsSet = new Set(Object.keys(IntentsConfiguration) as StandardIntent[]); -export const StandardContextsSet = new Set(STANDARD_CONTEXT_TYPES); 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;