From 01593a34898c1ecc8de3544545efba69fa69d5b8 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 19 Aug 2024 12:23:00 +0100 Subject: [PATCH] feat: http url interpolation SOFIE-3310 (#342) Co-authored-by: Johan Nyman --- .../src/index.ts | 10 ++++++ .../src/integrations/casparcg.ts | 4 +-- .../src/integrations/httpSend.ts | 8 +++-- .../src/integrations/sofieChef.ts | 4 +-- .../src/__tests__/lib.spec.ts | 34 +++++++++++++++++++ .../src/integrations/casparCG/index.ts | 12 +++++-- .../src/integrations/httpSend/index.ts | 25 ++++++++------ .../integrations/sofieChef/stateBuilder.ts | 3 +- packages/timeline-state-resolver/src/lib.ts | 23 +++++++++++++ 9 files changed, 103 insertions(+), 20 deletions(-) create mode 100644 packages/timeline-state-resolver/src/__tests__/lib.spec.ts diff --git a/packages/timeline-state-resolver-types/src/index.ts b/packages/timeline-state-resolver-types/src/index.ts index 6d40b27cb..7d2549c3d 100644 --- a/packages/timeline-state-resolver-types/src/index.ts +++ b/packages/timeline-state-resolver-types/src/index.ts @@ -172,3 +172,13 @@ export enum ActionExecutionResultCode { Error = 'ERROR', Ok = 'OK', } + +/** This resolves to a string, where parts can be defined by the datastore */ +export interface TemplateString { + /** The string template. Example: "http://google.com?q={{searchString}}" */ + key: string + /** Values for the arguments in the key string. Example: { searchString: "TSR" } */ + args?: { + [k: string]: any + } +} diff --git a/packages/timeline-state-resolver-types/src/integrations/casparcg.ts b/packages/timeline-state-resolver-types/src/integrations/casparcg.ts index 0c348d591..050fb31d3 100644 --- a/packages/timeline-state-resolver-types/src/integrations/casparcg.ts +++ b/packages/timeline-state-resolver-types/src/integrations/casparcg.ts @@ -1,4 +1,4 @@ -import { DeviceType } from '..' +import { DeviceType, TemplateString } from '..' export enum TimelineContentTypeCasparCg { // CasparCG-state MEDIA = 'media', @@ -122,7 +122,7 @@ export interface TimelineContentCCGInput extends TimelineContentCasparCGBase, Ti export interface TimelineContentCCGHTMLPage extends TimelineContentCasparCGBase, TimelineContentCCGProducerBase { type: TimelineContentTypeCasparCg.HTMLPAGE /** The URL to load */ - url: string + url: string | TemplateString } export interface TimelineContentCCGTemplate extends TimelineContentCasparCGBase, TimelineContentCCGProducerBase { type: TimelineContentTypeCasparCg.TEMPLATE diff --git a/packages/timeline-state-resolver-types/src/integrations/httpSend.ts b/packages/timeline-state-resolver-types/src/integrations/httpSend.ts index fa61b6307..610e04f24 100644 --- a/packages/timeline-state-resolver-types/src/integrations/httpSend.ts +++ b/packages/timeline-state-resolver-types/src/integrations/httpSend.ts @@ -1,8 +1,12 @@ -import { DeviceType, HTTPSendCommandContent } from '..' +import { DeviceType, HTTPSendCommandContent, TemplateString } from '..' export type TimelineContentHTTPSendAny = TimelineContentHTTPRequest export interface TimelineContentHTTPSendBase { deviceType: DeviceType.HTTPSEND } -export type TimelineContentHTTPRequest = TimelineContentHTTPSendBase & HTTPSendCommandContent +export interface HTTPSendCommandContentExt extends Omit { + url: string | TemplateString +} + +export type TimelineContentHTTPRequest = TimelineContentHTTPSendBase & HTTPSendCommandContentExt diff --git a/packages/timeline-state-resolver-types/src/integrations/sofieChef.ts b/packages/timeline-state-resolver-types/src/integrations/sofieChef.ts index 19f2d0261..1b5af2908 100644 --- a/packages/timeline-state-resolver-types/src/integrations/sofieChef.ts +++ b/packages/timeline-state-resolver-types/src/integrations/sofieChef.ts @@ -1,4 +1,4 @@ -import { DeviceType } from '..' +import { DeviceType, TemplateString } from '..' export enum TimelineContentTypeSofieChef { URL = 'url', @@ -13,5 +13,5 @@ export interface TimelineContentSofieChef { export interface TimelineContentSofieChefScene extends TimelineContentSofieChef { type: TimelineContentTypeSofieChef.URL - url: string + url: string | TemplateString } diff --git a/packages/timeline-state-resolver/src/__tests__/lib.spec.ts b/packages/timeline-state-resolver/src/__tests__/lib.spec.ts new file mode 100644 index 000000000..76d627671 --- /dev/null +++ b/packages/timeline-state-resolver/src/__tests__/lib.spec.ts @@ -0,0 +1,34 @@ +import { interpolateTemplateString, interpolateTemplateStringIfNeeded } from '../lib' + +describe('interpolateTemplateString', () => { + test('basic input', () => { + expect(interpolateTemplateString('Hello there {{name}}', { name: 'Bob' })).toEqual('Hello there Bob') + }) + + test('missing arg', () => { + expect(interpolateTemplateString('Hello there {{name}}', {})).toEqual('Hello there name') + }) + + test('repeated arg', () => { + expect(interpolateTemplateString('Hello there {{name}} {{name}} {{name}}', { name: 'Bob' })).toEqual( + 'Hello there Bob Bob Bob' + ) + }) +}) + +describe('interpolateTemplateStringIfNeeded', () => { + test('string input', () => { + const input = 'Hello there' + + expect(interpolateTemplateStringIfNeeded(input)).toEqual(input) + }) + + test('object input', () => { + expect( + interpolateTemplateStringIfNeeded({ + key: 'Hello there {{name}}', + args: { name: 'Bob' }, + }) + ).toEqual('Hello there Bob') + }) +}) diff --git a/packages/timeline-state-resolver/src/integrations/casparCG/index.ts b/packages/timeline-state-resolver/src/integrations/casparCG/index.ts index f04eb877e..1d7a951f1 100644 --- a/packages/timeline-state-resolver/src/integrations/casparCG/index.ts +++ b/packages/timeline-state-resolver/src/integrations/casparCG/index.ts @@ -57,7 +57,15 @@ import { DoOnTime, SendMode } from '../../devices/doOnTime' import got from 'got' import { InternalTransitionHandler } from '../../devices/transitions/transitionHandler' import Debug from 'debug' -import { actionNotFoundMessage, deepMerge, endTrace, literal, startTrace, t } from '../../lib' +import { + actionNotFoundMessage, + deepMerge, + endTrace, + interpolateTemplateStringIfNeeded, + literal, + startTrace, + t, +} from '../../lib' import { ClsParameters } from 'casparcg-connection/dist/parameters' const debug = Debug('timeline-state-resolver:casparcg') @@ -392,7 +400,7 @@ export class CasparCGDevice extends DeviceWithState export interface HttpSendDeviceCommand extends CommandWithContext { command: { commandName: 'added' | 'changed' | 'removed' | 'retry' | 'manual' - content: HTTPSendCommandContent + content: HTTPSendCommandContentExt layer: string } } @@ -69,7 +70,7 @@ export class HTTPSendDevice extends Device> { if (!cmd) return { @@ -133,7 +134,7 @@ export class HTTPSendDevice extends Device, @@ -23,7 +24,7 @@ export function buildSofieChefState( if (mapping && content.deviceType === DeviceType.SOFIE_CHEF) { sofieChefState.windows[mapping.options.windowId] = { - url: content.url, + url: interpolateTemplateStringIfNeeded(content.url), urlTimelineObjId: layerState.id, } } diff --git a/packages/timeline-state-resolver/src/lib.ts b/packages/timeline-state-resolver/src/lib.ts index ee4da293c..2f907eb7b 100644 --- a/packages/timeline-state-resolver/src/lib.ts +++ b/packages/timeline-state-resolver/src/lib.ts @@ -8,6 +8,7 @@ import { ActionExecutionResultCode, TimelineDatastoreReferences, ActionExecutionResult, + TemplateString, } from 'timeline-state-resolver-types' import * as _ from 'underscore' import { PartialDeep } from 'type-fest' @@ -307,3 +308,25 @@ export function actionNotFoundMessage(id: never): ActionExecutionResult { export function cloneDeep(input: T): T { return klona(input) } + +/** + * Interpolate a translation style string + */ +export function interpolateTemplateString(key: string, args: { [key: string]: any } | undefined): string { + if (!args || typeof key !== 'string') { + return String(key) + } + + let interpolated = String(key) + for (const placeholder of key.match(/[^{}]+(?=})/g) || []) { + const value = args[placeholder] || placeholder + interpolated = interpolated.replace(`{{${placeholder}}}`, value) + } + + return interpolated +} + +export function interpolateTemplateStringIfNeeded(str: string | TemplateString): string { + if (typeof str === 'string') return str + return interpolateTemplateString(str.key, str.args) +}