Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactors ContextTypes&Intent to union type instead of enum #1139

Merged
merged 9 commits into from
Apr 11, 2024
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 8 additions & 6 deletions src/api/DesktopAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -125,7 +127,7 @@ export interface DesktopAgent {
* // }
* ```
*/
findIntent(intent: string, context?: Context, resultType?: string): Promise<AppIntent>;
findIntent(intent: Intent, context?: Context, resultType?: string): Promise<AppIntent>;

/**
* Find all the available intents for a particular context, and optionally a desired result context type.
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -268,7 +270,7 @@ export interface DesktopAgent {
* }
* ```
*/
raiseIntent(intent: string, context: Context, app?: AppIdentifier): Promise<IntentResolution>;
raiseIntent(intent: Intent, context: Context, app?: AppIdentifier): Promise<IntentResolution>;

/**
* Finds and raises an intent against apps registered with the desktop agent based on the type of the specified context data example.
Expand Down Expand Up @@ -346,7 +348,7 @@ export interface DesktopAgent {
* });
* ```
*/
addIntentListener(intent: string, handler: IntentHandler): Promise<Listener>;
addIntentListener(intent: Intent, handler: IntentHandler): Promise<Listener>;

/**
* 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.
Expand All @@ -371,7 +373,7 @@ export interface DesktopAgent {
* });
* ```
*/
addContextListener(contextType: string | null, handler: ContextHandler): Promise<Listener>;
addContextListener(contextType: ContextType | null, handler: ContextHandler): Promise<Listener>;

/**
* Retrieves a list of the User channels available for the app to join.
Expand Down Expand Up @@ -561,7 +563,7 @@ export interface DesktopAgent {
* await fdc3.raiseIntent("StartChat", context, appIntent.apps[0].name);
* ```
*/
raiseIntent(intent: string, context: Context, name: string): Promise<IntentResolution>;
raiseIntent(intent: Intent, context: Context, name: string): Promise<IntentResolution>;

/**
* @deprecated version of `raiseIntentForContext` that targets an app by by name rather than `AppIdentifier`. Provided for backwards compatibility with versions FDC3 standard <2.0.
Expand Down
4 changes: 3 additions & 1 deletion src/api/IntentMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/api/IntentResolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*/
Expand Down
32 changes: 27 additions & 5 deletions src/api/Methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -74,7 +80,7 @@ export function open(app: AppIdentifier | string, context?: Context): Promise<Ap
}
}

export function findIntent(intent: string, context?: Context, resultType?: string): Promise<AppIntent> {
export function findIntent(intent: Intent, context?: Context, resultType?: string): Promise<AppIntent> {
return rejectIfNoGlobal(() => window.fdc3.findIntent(intent, context, resultType));
}

Expand All @@ -86,7 +92,7 @@ export function broadcast(context: Context): Promise<void> {
return rejectIfNoGlobal(() => window.fdc3.broadcast(context));
}

export function raiseIntent(intent: string, context: Context, app?: AppIdentifier | string): Promise<IntentResolution> {
export function raiseIntent(intent: Intent, context: Context, app?: AppIdentifier | string): Promise<IntentResolution> {
if (isString(app)) {
return rejectIfNoGlobal(() => window.fdc3.raiseIntent(intent, context, app));
} else {
Expand All @@ -102,12 +108,12 @@ export function raiseIntentForContext(context: Context, app?: AppIdentifier | st
}
}

export function addIntentListener(intent: string, handler: IntentHandler): Promise<Listener> {
export function addIntentListener(intent: Intent, handler: IntentHandler): Promise<Listener> {
return rejectIfNoGlobal(() => window.fdc3.addIntentListener(intent, handler));
}

export function addContextListener(
contextTypeOrHandler: string | null | ContextHandler,
contextTypeOrHandler: ContextType | null | ContextHandler,
handler?: ContextHandler
): Promise<Listener> {
//Handle (deprecated) function signature that allowed contextType argument to be omitted
Expand Down Expand Up @@ -178,6 +184,22 @@ export function findInstances(app: AppIdentifier): Promise<AppIdentifier[]> {
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`).
*
Expand Down Expand Up @@ -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;
};
48 changes: 46 additions & 2 deletions src/context/ContextType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -23,5 +69,3 @@ export enum ContextTypes {
TransactionResult = 'fdc3.transactionResult',
Valuation = 'fdc3.valuation',
}

export type ContextType = ContextTypes | string;
32 changes: 32 additions & 0 deletions src/intents/Intents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
30 changes: 30 additions & 0 deletions src/internal/contextConfiguration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { StandardContextType } from '../context/ContextType';
import { exhaustiveStringTuple } from './typeHelpers';

const STANDARD_CONTEXT_TYPES = exhaustiveStringTuple<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'
);

// used internally to check if a given intent/context is a standard one
export const StandardContextsSet = new Set(STANDARD_CONTEXT_TYPES);
26 changes: 26 additions & 0 deletions src/internal/intentConfiguration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { StandardIntent } from '../intents/Intents';
import { exhaustiveStringTuple } from './typeHelpers';

const STANDARD_INTENTS = exhaustiveStringTuple<StandardIntent>()(
'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);
12 changes: 12 additions & 0 deletions src/internal/typeHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
type AtLeastOne<T> = [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 = <UNION_TYPE extends string>() => <L extends AtLeastOne<UNION_TYPE>>(
...tuple: L extends any
? Exclude<UNION_TYPE, L[number]> extends never
? L
: Exclude<UNION_TYPE, L[number]>[]
: never
) => tuple;
Loading
Loading