From 48a1094aaab6418f09db662dbc81f090a34f32ed Mon Sep 17 00:00:00 2001 From: Jeremy LaCivita Date: Thu, 6 Jun 2024 16:12:48 -0400 Subject: [PATCH] feat: User Interest (#170) In additional to traditional discovery APIs such as Watch History and Watch Next, Firebolt provides a more abstract API that facilitates impromptu content discovery connections between first-party Aggregated Experiences and third-party Apps. The User Interest Capability enables Apps to provide meta-data on content that the user has expressed an interest in to Aggregated Experience Apps that have been given access to use this Capability. This allows for open ended design of Aggregated Experience App features that present App-specific content to re-engage the user with the content inside the originating App. While the functionality and UX is left to the Aggregated Experience App, typically designed by each Firebolt Distributor, the Firebolt API enables events to register user interest and pass entity meta-data. Which generally enables Aggregated Experiences to present that entity meta-data in some way that leads to re-launching the original App at a later point, using a `navigateTo` notification. This is just one example of what an Aggregated Experience App might do with the User Interest API. Note that this API **SHOULD NOT** be used to implement Watch History or Watch Next features. These concepts are much more fundamental to Firebolt and have explicit APIs so that Firebolt Distributors can keep track of which apps are using them separately. --- package-lock.json | 46 +- package.json | 11 +- .../intents/user-interest/media/image1.png | Bin 0 -> 14294 bytes .../intents/user-interest/media/image2.png | Bin 0 -> 14615 bytes .../intents/user-interest/media/image3.png | Bin 0 -> 16673 bytes .../intents/user-interest/media/image4.png | Bin 0 -> 15496 bytes .../specifications/discovery/user-interest.md | 361 +++++++++++++ .../general/context-parameters.md | 303 +++++++++++ .../specifications/intents/user-interest.md | 15 + .../app-passthrough-apis.md | 477 ++++++++++++++++++ src/openrpc/content.json | 195 +++++++ src/openrpc/discovery.json | 178 ++++++- src/openrpc/keyboard.json | 38 +- src/schemas/advertising.json | 23 + src/schemas/discovery.json | 79 +++ src/schemas/entertainment.json | 1 - src/schemas/entity.json | 456 +++++++++++++++++ src/schemas/intents.json | 458 +---------------- src/schemas/lifecycle.json | 38 ++ src/schemas/types.json | 194 +++++++ src/sdks/core/sdk.config.json | 3 +- src/sdks/core/test/suite/discovery.test.ts | 9 + src/sdks/discovery/.npmignore | 6 + src/sdks/discovery/CHANGELOG.md | 0 src/sdks/discovery/CONTRIBUTING.md | 1 + src/sdks/discovery/LICENSE | 1 + src/sdks/discovery/NOTICE | 1 + src/sdks/discovery/README.md | 26 + src/sdks/discovery/jest.config.json | 8 + src/sdks/discovery/package.json | 53 ++ src/sdks/discovery/sdk.config.json | 15 + src/sdks/discovery/src/js/sdk/index.mjs | 30 ++ src/sdks/discovery/test/suite/content.test.ts | 43 ++ src/sdks/discovery/tsconfig.json | 5 + src/sdks/manage/test/suite/keyboard.test.ts | 24 +- 35 files changed, 2595 insertions(+), 503 deletions(-) create mode 100644 requirements/images/specifications/intents/user-interest/media/image1.png create mode 100644 requirements/images/specifications/intents/user-interest/media/image2.png create mode 100644 requirements/images/specifications/intents/user-interest/media/image3.png create mode 100644 requirements/images/specifications/intents/user-interest/media/image4.png create mode 100644 requirements/specifications/discovery/user-interest.md create mode 100644 requirements/specifications/general/context-parameters.md create mode 100644 requirements/specifications/intents/user-interest.md create mode 100644 requirements/specifications/openrpc-extensions/app-passthrough-apis.md create mode 100644 src/openrpc/content.json create mode 100644 src/schemas/advertising.json create mode 100644 src/schemas/discovery.json create mode 100644 src/schemas/entity.json create mode 100644 src/schemas/lifecycle.json create mode 100644 src/schemas/types.json create mode 100644 src/sdks/discovery/.npmignore create mode 100644 src/sdks/discovery/CHANGELOG.md create mode 120000 src/sdks/discovery/CONTRIBUTING.md create mode 120000 src/sdks/discovery/LICENSE create mode 120000 src/sdks/discovery/NOTICE create mode 100644 src/sdks/discovery/README.md create mode 100644 src/sdks/discovery/jest.config.json create mode 100644 src/sdks/discovery/package.json create mode 100644 src/sdks/discovery/sdk.config.json create mode 100644 src/sdks/discovery/src/js/sdk/index.mjs create mode 100644 src/sdks/discovery/test/suite/content.test.ts create mode 100644 src/sdks/discovery/tsconfig.json diff --git a/package-lock.json b/package-lock.json index 173940cab..f9fe68d78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "Apache-2.0", "workspaces": [ "src/sdks/core", - "src/sdks/manage" + "src/sdks/manage", + "src/sdks/discovery" ], "bin": { "firebolt-version": "src/js/version.mjs" @@ -18,7 +19,7 @@ "devDependencies": { "@commitlint/cli": "^17.0.3", "@commitlint/config-conventional": "^17.0.3", - "@firebolt-js/openrpc": "3.0.0-next.3", + "@firebolt-js/openrpc": "3.0.0-next.4", "@firebolt-js/schemas": "2.0.0", "@saithodev/semantic-release-backmerge": "^3.2.0", "@semantic-release/changelog": "^6.0.1", @@ -1065,18 +1066,22 @@ "node": ">=8" } }, + "node_modules/@firebolt-js/discovery-sdk": { + "resolved": "src/sdks/discovery", + "link": true + }, "node_modules/@firebolt-js/manage-sdk": { "resolved": "src/sdks/manage", "link": true }, "node_modules/@firebolt-js/openrpc": { - "version": "3.0.0-next.3", - "resolved": "https://registry.npmjs.org/@firebolt-js/openrpc/-/openrpc-3.0.0-next.3.tgz", - "integrity": "sha512-hhNtHIpNwwN99Zd4ZABnQ3MIEMPYDN5rSZPNad4ah4R5rmLypWv79vfHSPYeCrTTmoQCwwCFhpObjhJk0EVX/g==", + "version": "3.0.0-next.4", + "resolved": "https://registry.npmjs.org/@firebolt-js/openrpc/-/openrpc-3.0.0-next.4.tgz", + "integrity": "sha512-Q3GZpaA2Gmhx5UOh7cOMpDjZZtbutVfTUUis0FrOKceVMVllRhHz/XeqZrpmKo4EX0hn4OPWOmVek34WO68jQg==", "dev": true, "dependencies": { - "ajv": "^8.3.0", - "ajv-formats": "^2.1.0", + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", "array.prototype.groupby": "^1.1.0", "crocks": "^0.12.4", "deepmerge": "^4.2.2", @@ -1098,15 +1103,15 @@ "dev": true }, "node_modules/@firebolt-js/openrpc/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", + "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "uri-js": "^4.4.1" }, "funding": { "type": "github", @@ -16669,9 +16674,9 @@ } }, "node_modules/web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", "dev": true, "engines": { "node": ">= 8" @@ -16960,6 +16965,17 @@ "typescript": "^4.6.4" } }, + "src/sdks/discovery": { + "name": "@firebolt-js/discovery-sdk", + "version": "1.2.0-next.2", + "license": "Apache-2.0", + "devDependencies": { + "jest": "^28.1.0", + "jest-environment-jsdom": "^28.1.3", + "prettier": "^3.1.0", + "typescript": "^4.6.4" + } + }, "src/sdks/manage": { "name": "@firebolt-js/manage-sdk", "version": "1.2.0-next.2", diff --git a/package.json b/package.json index f019e95da..d27b2efd0 100644 --- a/package.json +++ b/package.json @@ -8,14 +8,15 @@ }, "workspaces": [ "src/sdks/core", - "src/sdks/manage" + "src/sdks/manage", + "src/sdks/discovery" ], "scripts": { "fs:setup": "npm run clean && mkdir -p dist", - "validate:each": "npx firebolt-openrpc validate --input src/openrpc --schemas node_modules/@firebolt-js/schemas/src/schemas --schemas src/schemas --transformations", - "validate:compiled": "npx firebolt-openrpc validate --input dist/firebolt-open-rpc.json && npm run validate --workspaces", + "validate:each": "npx firebolt-openrpc validate --input src/openrpc --schemas src/schemas --transformations", + "validate:compiled": "npx firebolt-openrpc validate --input dist/firebolt-open-rpc.json --pass-throughs && npm run validate --workspaces", "validate": "npm run validate:each && npm run validate:compiled ", - "compile": "npx firebolt-openrpc openrpc --input src --template src/template/openrpc/template.json --output ./dist/firebolt-open-rpc.json --schemas node_modules/@firebolt-js/schemas/src/schemas --schemas src/schemas", + "compile": "npx firebolt-openrpc openrpc --input src --template src/template/openrpc/template.json --output ./dist/firebolt-open-rpc.json --schemas src/schemas", "slice": "npm run slice --workspaces", "sdks": "npm run sdk --workspaces", "docs": "npm run docs --workspaces", @@ -44,7 +45,7 @@ "devDependencies": { "@commitlint/cli": "^17.0.3", "@commitlint/config-conventional": "^17.0.3", - "@firebolt-js/openrpc": "3.0.0-next.3", + "@firebolt-js/openrpc": "3.0.0-next.4", "@firebolt-js/schemas": "2.0.0", "@saithodev/semantic-release-backmerge": "^3.2.0", "@semantic-release/changelog": "^6.0.1", diff --git a/requirements/images/specifications/intents/user-interest/media/image1.png b/requirements/images/specifications/intents/user-interest/media/image1.png new file mode 100644 index 0000000000000000000000000000000000000000..b0171a9a4ca0e95d45314ce3ce84ff6669623e19 GIT binary patch literal 14294 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 literal 0 HcmV?d00001 diff --git a/requirements/images/specifications/intents/user-interest/media/image2.png b/requirements/images/specifications/intents/user-interest/media/image2.png new file mode 100644 index 0000000000000000000000000000000000000000..94102a60fa1d79eab4760418b07643dae07bd5b0 GIT binary patch literal 14615 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 literal 0 HcmV?d00001 diff --git a/requirements/images/specifications/intents/user-interest/media/image4.png b/requirements/images/specifications/intents/user-interest/media/image4.png new file mode 100644 index 0000000000000000000000000000000000000000..6a15fe0c612806675309696195d47143ed946315 GIT binary patch literal 15496 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 literal 0 HcmV?d00001 diff --git a/requirements/specifications/discovery/user-interest.md b/requirements/specifications/discovery/user-interest.md new file mode 100644 index 000000000..f53e239ba --- /dev/null +++ b/requirements/specifications/discovery/user-interest.md @@ -0,0 +1,361 @@ +# User Interest + +Document Status: Candidate Specification + +See [Firebolt Requirements Governance](../../governance.md) for more info. + +| Contributor | Organization | +| -------------- | -------------- | +| Eugene Chung | Comcast | +| Tim Dibben | Sky | +| Mike Horwitz | Comcast | +| Jeremy LaCivita | Comcast | + +## 1. Overview + +In additional to traditional discovery APIs such as Watch History and +Watch Next, Firebolt provides a more abstract API that facilitates +impromptu content discovery connections between first-party Aggregated +Experiences and third-party Apps. + +The User Interest Capability enables Apps to provide meta-data on +content that the user has expressed an interest in to Aggregated +Experience Apps that have been given access to use this Capability. + +This allows for open ended design of Aggregated Experience App features +that present App-specific content to re-engage the user with the content +inside the originating App. + +While the functionality and UX is left to the Aggregated Experience App, +typically designed by each Firebolt Distributor, the Firebolt API +enables events to register user interest and pass entity meta-data: + +![Diagram Description automatically +generated](../../../requirements/images/specifications/intents/user-interest/media/image1.png) + +Which generally enables Aggregated Experiences to present that entity +meta-data in some way that leads to re-launching the original App at a +later point, using a `navigateTo` notification: + +![Diagram Description automatically +generated](../../../requirements/images/specifications/intents/user-interest/media/image2.png) + +This is just one example of what an Aggregated Experience App might do +with the User Interest API. + +Note that this API **SHOULD NOT** be used to implement Watch History or +Watch Next features. These concepts are much more fundamental to +Firebolt and have explicit APIs so that Firebolt Distributors can keep +track of which apps are using them separately. + +## 2. Table of Contents +- [1. Overview](#1-overview) +- [2. Table of Contents](#2-table-of-contents) +- [3. User Interest Flows](#3-user-interest-flows) + - [3.1. User Interest from an in-app UX](#31-user-interest-from-an-in-app-ux) + - [3.2. User Interest from a platform UX](#32-user-interest-from-a-platform-ux) + - [User Interest Errors](#user-interest-errors) + - [3.3. Upstream User Interest Intent](#33-upstream-user-interest-intent) + - [3.4. User Interest Bulk Updates](#34-user-interest-bulk-updates) +- [4. Core SDK APIs](#4-core-sdk-apis) + - [4.1. InterestType](#41-interesttype) + - [4.2. InterestReason](#42-interestreason) + - [4.3. Discovery.userInterest](#43-discoveryuserinterest) + - [4.4. Discovery Interest Provider](#44-discovery-interest-provider) + - [4.5. InterestIntent](#45-interestintent) +- [5. Discovery SDK APIs](#5-discovery-sdk-apis) + - [5.1. Interest Types](#51-interest-types) + - [5.2. Content.requestUserInterest](#52-contentrequestuserinterest) + - [5.3. Content.onUserInterest](#53-contentonuserinterest) + + +## 3. User Interest Flows +### 3.1. User Interest from an in-app UX + +Some Apps will have a built-in user interface for users to express +interest in content from the App. This could be a "Favorite" button, +an in-app "My List" button, etc. + +If the App wants to leverage any additional exposure from the device's +Aggregated Experience, it can wire up its own UI to the Firebolt User +Interest API, in addition to any in-app features that it's already +invoking. + +By calling the `Discovery.userInterest` method with the relevant entity +meta-data, the device's Aggregated Experience will be notified of the +user's interest in that entity: + +```typescript +Discovery.userInterest(type: InterestType, reason: InterestReason, entity: EntityDetails) +``` + +The `type` parameter denotes the directionality of the interest: + +- `interest` +- `disinterest` + +The `reason` parameter denotes why or how the user has expressed interest: + +| Reason | Description | +| ------ | ----------- | +| `playlist` | Interested in adding to a list | +| `reaction` | Interested in submitting a reaction, e.g. like or dislike | +| `recording` | Interest in scheduling a recording | +| `share` | Interest in sharing the content on social media | + +**NOTE**: We can remove some of these (not `playlist`) these are here for now to illustrate the purpose for the reason paramater. + +An app **MUST** `provide` the `xrn:firebolt:capability:discovery:interest` +capability in order to call `Discovery.userInterest`. + +When this method is called with a valid `EntityDetails`, the platform +**MUST** dispatch a `Content.onUserInterest` notification to all Apps +that have registered for it (typically Aggregated Experience Apps) with +information about the app, interest type, and the entity. + +The `Content.onUserInterest` event has a result type of `Interest`: + +| property | type | description | +|---------|------|-------------| +| appId | string | The id of the app that pushed the user interest. | +| type | `InterestType` | the type of interest. | +| reason | `InterestReason` | the reason for the interest | +| entity | `EntityDetails` | The entity the user expressed interest in. | + +An Aggregated Experience can register for the `Content.onUserInterest` +notification, and it will receive notifications when an `EntityDetails` is +returned from the active App after a `Discovery.userInterest` call is +fulfilled. + +An app **MUST** have permissions to `use` the +`xrn:firebolt:capability:discovery:interest` capability in order to +listen to the `Content.onUserInterest` notification. + +If the result is not a valid entity, i.e. does not match +the [EntityDetails](../entities/) schema, then no `Content.onUserInterest` +notification will be dispatched. + +The `Discovery.userInterest` method **SHOULD NOT** be used in place of more +specific Discovery methods, e.g. `Discovery.watchNext` or +`Discovery.watched`. These methods facilitate specific UX flows that may +have separate legal opt-outs for each user. + +The `Discovery.userInterest` method **SHOULD NOT** be called unless the user +is activating a UI element in the app, or in a second screen experience +that is communicating with the app, that implies interest of some kind. + +### 3.2. User Interest from a platform UX + +Firebolt platforms may provide a platform UX, e.g. voice or and RCU, to +express user interest in content from an active App. To facilitate this +Apps will need to be told about the user's expressed interest in their +content. + +First, the Aggregated Experience (or some app with this +capability) detects that the user is interested in something. In this +picture the interest is triggered by an RCU button, but how this occurs +is outside the scope of this document. When this happens, the Aggregated +Experience app calls `Content.requestUserInterest()`, which will trigger the +platform to identify the best [Provider Candidate](../openrpc-extensions/app-passthrough-apis.md#5-provider-candidates) +and call that app's `userInterest` method via the Provider RPC method: +`Discovery.onRequestUserInterest`. + +![](../../../requirements/images/specifications/intents/user-interest/media/image3.png) + +Next, the provider app receives and responds to the request with an +EntityDetails, which is returned as the result to the pending +`Content.requestUserInterest` method: + +![](../../../requirements/images/specifications/intents/user-interest/media/image4.png) + +Once an App's callback is invoked, that app will have `interestTimeout` +milliseconds to return a value or throw an error. Values returned after +that time **MUST** be ignored. The timeout value is stored in the +device's configuration manifest. + +To be notified when a user expresses interest in the currently displayed +content, an App **MUST** provide the +`xrn:firebolt:capability:discovery:interest` capability by enabling the +`Discovery.onRequestUserInterest` notification. + +If there is a valid entity to return, then the method registered by the +App **MUST** return the currently displayed entity meta-data. + +If there is no valid entity to return, then the method **MUST** throw an +exception. + +If the provider app returns a valid `EntityDetails` before the timeout, +then, the returned value **MUST** be used. + +If there is no app registered the an error **MUST** be returned. + +#### User Interest Errors +An app is expected return either a valid result or an appriate error. + +If neither happens before `interestTimeout` expires then the platform **MUST** return a Provider timed-out error: + +```json +{ + "id": 1, + "error": { + "code": -50400, + "message": "Provider timed-out", + "data": { + "capability": "xrn:firebolt:capability:discovery:interest", + } + } +} +``` + +If an app recieves a request for user interest when there is nothing appropriate to return, e.g. nothing selected or presented that maps to an Entity, then the app **SHOULD** return an error with a valid JSON-RPC error response, e.g.: + +```json +{ + "id": 1, + "error": { + "code": -40400, + "message": "No entities currently presented." + } +} +``` + +The platform API Gateway **MUST** append, or overwrite, the `data.capability` value to any errors returned by the app, e.g.: + +```json +{ + "id": 1, + "error": { + "code": -40400, + "message": "No entities currently presented.", + "data": { + "capability": "xrn:firebolt:capability:discovery:interest", + } + } +} +``` + +### 3.3. Upstream User Interest Intent +In some cases, e.g. a voice assistant, some upstream component will inform +the platform that the user is interested in whatever is currently being presented. + +To do this, the upstream system **MUST** send a `Interest` intent, which describes the type of and reason for the interest. + +```json +{ + "action": "interest", + "data": { + "type": "interest", + "reason": "playlist" + } +} +``` + +When a Firebolt platform receives this intent, it **SHOULD** initiate the platform's [user interest flow](#4-user-interest-from-a-platform-ux). + +### 3.4. User Interest Bulk Updates + +Sending bulk interest updates, e.g. Entities the user expressed interest +in on a different platform, is not supported. + +## 4. Core SDK APIs + +The following APIs are exposed by the Firebolt Core SDK as part of the +`Discovery` module. + +### 4.1. InterestType +This is an enum with the following values: + +- `"interest"` +- `"disinterest"` + +### 4.2. InterestReason +This is an enum with the following values: + +| Reason | Description | +| ------ | ----------- | +| `playlist` | Interested in adding to a list | +| `reaction` | Interested in submitting a reaction, e.g. like or dislike | +| `recording` | Interest in scheduling a recording | +| `share` | Interest in sharing the content on social media | + +### 4.3. Discovery.userInterest + +This is a push API that allows Apps to push entities that the user has +expressed interest in to the platform. + +To push an entity that the user is interested in pass an `EntityDetails` +object to the method: + +```typescript +Discovery.userInterest(type: InterestType, reason: InterestReason, entity: EntityDetails): Promise +``` + +### 4.4. Discovery Interest Provider +To respond to requests for the current entity, because the user has +expressed interest in some way that the platform manages, register a +provider: + +```typescript +interface IDiscoveryInterestProvider { + function userInterest(type: InterestType, reason: InterestReason): Promise +} + +Discovery.provide("xrn:firbolt:capability:discovery:interest", IDiscoveryInterestProvider) +``` + +### 4.5. InterestIntent + +An `InterestIntent` denotes that the user has expressed interest in the +currently displayed and/or selected content: + +```typescript +type InterestIntent { + action: "interest" + data: { + type: "interest" | "disinterest", + reason: "playlist" | "reaction" | "recording" + }, + context: { + source: "rcu" | "voice" + } +} +``` + +## 5. Discovery SDK APIs + +The following APIs are exposed by the Firebolt Discovery SDK as part of the +`Content` module. + +### 5.1. Interest Types +This type stores the various attributes of an Interest response or event: + +```typescript +type InterestType = "interest" | "disinterest" +type InterestReason = "playlist" | "reaction" | "recording" | "share" + +type Interest { + appId: string + entity: EntityDetails + type?: InterestType + reason?: InterestReason +} +``` + +### 5.2. Content.requestUserInterest +This method triggers the corresponding Discovery provider API for the +provider app. + +```typescript +Content.requestUserInterest(type: InterestType, reason: InterestReason): Promise +``` + +### 5.3. Content.onUserInterest + +This notification allows Aggregated Experience Apps to be informed when +a user expresses interest in some Content, and the content resolves to a +valid Entity from some App. + +`Content.listen('userInterest', Interest => void): Promise` + +The callback will be passed an `Interest` object with +the appId, type, reason, and information about the entity that the user expressed interest in. diff --git a/requirements/specifications/general/context-parameters.md b/requirements/specifications/general/context-parameters.md new file mode 100644 index 000000000..761a03fd7 --- /dev/null +++ b/requirements/specifications/general/context-parameters.md @@ -0,0 +1,303 @@ +# Context Parameters +Document Status: Working Draft + +See [Firebolt Requirements Governance](../../governance.md) for more info. + +| Contributor | Organization | +| -------------- | -------------- | +| Jeremy LaCivita | Comcast | +| Yuri Pasquali | Sky | + +## 1. Overview + +**TODO**: This doc is old and need to be refreshed + +Context Parameters are parameters on an RPC method that provide context +for the call via a set of primitive types (string, number, integer, +boolean). This allows for sharing the context parameters across property +getters, setters, and subscribers, as well as filtering which events to +listen for by context. + +An example of a property method with context could be: + +```javascript +// get a context-driven property (context: appId=hulu) +const huluShare = await Privacy.shareWatchHistory('hulu') +``` + +```javascript +// set a context-driven property (context: appId=hulu) +Privacy.shareWatchHistory('hulu', false) +``` + +In the example above, 'hulu' is the context parameter for both the +shareWatchHistory getter and setter. + +Context parameters can also be applied to property subscribers, as well +as other, non-property events: + +```javascript +// subscribe to a context-driven property +Privacy.shareWatchHistory('hulu', (value) => { + console.log('hulu value changed to: ' + value) +}) + +Privacy.listen('shareWatchHistoryChanged', 'hulu', (value) => { + console.log('hulu value changed to: ' + value) +}) +``` + +For subscribers and events, the context parameters may be omitted, in +which case, all events will be dispatched to the listener: + +```javascript +// subscribe to a context-driven property w/out any context (get all of them) + +Privacy.shareWatchHistory((appId, value) => { + console.log(`App '${appId}' value changed to ${value}`) +}) + +Privacy.listen('shareWatchHistoryChanged', (appId, value) => { + console.log(`App '${appId}' value changed to: ${value}`) +}) +``` + +Context Parameters **MUST** be of a primitive type, to avoid complex +object-tree filtering. + +This document describes an OpenRPC pattern and JavaScript code +generation for a Firebolt method template that uses Context Parameters. + +## 2. Table of Contesnts +- [1. Overview](#1-overview) +- [2. Table of Contesnts](#2-table-of-contesnts) +- [3. Context Parameters Use Cases](#3-context-parameters-use-cases) +- [4. Context Parameters API](#4-context-parameters-api) + - [4.1. JSON-RPC API](#41-json-rpc-api) + - [4.1.1. Setter RPC generation](#411-setter-rpc-generation) + - [4.1.2. onChanged RPC generation](#412-onchanged-rpc-generation) + - [4.1.3. Temporal Set onAvailable / Unavailable RPC generation](#413-temporal-set-onavailable--unavailable-rpc-generation) + - [4.1.4. Event RPC Decoration](#414-event-rpc-decoration) + - [4.2. JavaScript API](#42-javascript-api) + - [4.2.1. Event Listener Signatures](#421-event-listener-signatures) + +## 3. Context Parameters Use Cases + +How each parameter affects it's corresponding API is out of scope for +this document. See each API spec for details on what each context +parameter does. + +Setting a context parameter to null is still setting it to a value. If a +context parameter is passed to the SDK with either a value of null or +undefined, then it **MUST** be explicitly set in the RPC request to the +value null. This is to avoid additional method signature permutations +being required for the SDK. + +An effort should be made to sort the context parameters in order of most +usefulness, since not all languages support undefined. + +## 4. Context Parameters API + +The section describes the RPC and JavaScript APIs. + +### 4.1. JSON-RPC API + +Simple getters and event listeners don't need any parameters. + +If a method is tagged as either a property (any kind) or an event, then +**all** the parameters in the RPC definition **MUST** be context +parameters. + +To facilitate this, the listen parameter that all events currently have +will be removed from the source module and inserted into the generated +RPC by the firebolt-openrpc tooling. + +If any Context Parameters have a type other than: + +- `string` +- `boolean` +- `number` +- `integer` + +Then the RPC method **MUST NOT** pass validation. This is to ensure that +implementing context parameters is not overly complicated. + +#### 4.1.1. Setter RPC generation + +When generating the setter for a property method, all the Context +Parameters **MUST** be copied to the setter. The context parameters +**MUST** be before the value parameter, which itself **MUST** be last. + +#### 4.1.2. onChanged RPC generation + +When generating the onChanged notification for a property +method, all the Context Parameters **MUST** be copied to the event +parameters. The context parameters **MUST** be before the listen +parameter, which itself **MUST** be last. + +#### 4.1.3. Temporal Set onAvailable / Unavailable RPC generation + +Generated Temporal Set events will treat the entire parameter list from +the original temporal-set method as Context Parameters and copy them to +the event parameters. The context parameters **MUST** be before the +listen parameter, which itself **MUST** be last. + +#### 4.1.4. Event RPC Decoration + +All RPC methods tagged as event **MUST** have the listen parameter, of +type boolean, automatically added when generating the final RPC. This +parameter will be added at the end of the parameters list. + +All RPC methods tagged as event that have context parameters **MUST** +have the result schema wrapped in an object. The original result schema +**MUST** be moved to a property called data. Each of the context +parameters **MUST** be copied to a property called context. + +So the following RPC event: + +```json +{ + "name": "onContextualEvent", + "tags": [ + { + "name": "event" + } + ], + "params": [ + { + "name": "a", + "required": true, + "schema": { + "type": "boolean" + } + }, + { + "name": "b", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "c", + "required": true, + "schema": { + "type": "number" + } + } + ], + "result": { + "name": "result", + "schema": { + "type": "object", + "properties": { + "foo": { + "type": "boolean" + } + } + } + } +} +``` + +Would have its result transformed to: + +```json +{ + "name": "result", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "foo": { + "type": "boolean" + } + } + }, + "context": { + "type": "object", + "properties": { + "a": { + "type": "boolean" + }, + "b": { + "type": "string" + }, + "c": { + "type": "number" + } + }, + "required": [ + "a", "b", "c" + ] + } + } + } +} +``` + +### 4.2. JavaScript API + +TBD + +#### 4.2.1. Event Listener Signatures + +If any of the context parameters are optional, then a callback signature +must be generated for each left-to-right combination of the parameters. + +For example, the method: + +```json +{ + "name": "onFoo", + "tags": [ + { + "name": "event" + } + ], + "params": [ + { + "name": "a", + "required": true, + "schema": { + "type": "boolean" + } + }, + { + "name": "b", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "c", + "required": false, + "schema": { + "type": "number" + } + } + ] +} +``` + +Would result in the following method signatures: + +```typescript +listen(event: 'foo', a: boolean, callback: (b: string, c: number, data: any) => {}) + +listen(event: 'foo', a: boolean, b: string, callback: (c: number, data: any) => {}) + +listen(event: 'foo', a: boolean, b: string, c: number, callback: (data: any) => {}) +``` + +Which allows parameters to be omitted, from right-to-left, and included +as part of the result, instead. + +When invoking the callback, the SDK **MUST** pass the data portion of +the result to the data parameter of the callback, and each context +property to the corresponding callback parameter. + +This pattern also applies to property subscribers. diff --git a/requirements/specifications/intents/user-interest.md b/requirements/specifications/intents/user-interest.md new file mode 100644 index 000000000..eae60ecf7 --- /dev/null +++ b/requirements/specifications/intents/user-interest.md @@ -0,0 +1,15 @@ +# User Interest + +Document Status: Candidate Specification + +See [Firebolt Requirements Governance](../../governance.md) for more info. + +| Contributor | Organization | +| -------------- | -------------- | +| Eugene Chung | Comcast | +| Tim Dibben | Sky | +| Mike Horwitz | Comcast | +| Jeremy LaCivita | Comcast | + +## 1. Overview +This document describes the intent to initiate a [User Interest](../discovery/user-interest.md) flow from an upstream system, e.g. a voice assistant. diff --git a/requirements/specifications/openrpc-extensions/app-passthrough-apis.md b/requirements/specifications/openrpc-extensions/app-passthrough-apis.md new file mode 100644 index 000000000..ec23922e6 --- /dev/null +++ b/requirements/specifications/openrpc-extensions/app-passthrough-apis.md @@ -0,0 +1,477 @@ +# App Pass-through APIs + +Document Status: Working Draft + +See [Firebolt Requirements Governance](../../governance.md) for more info. + +| Contributor | Organization | +|-----------------|----------------| +| Jeremy LaCivita | Comcast | +| Kevin Pearson | Comcast | +| Yuri Pasquali | Sky | + +## 1. Overview +This document describes how one Firebolt App can provide a capability that may be used by another Firebolt App, with the platform as a permission broker that passes the requests and responses to each app without feature-specific logic. + +This document covers the App Pass-through Firebolt OpenRPC extension as well as how Firebolt implementations should detect and execute app provided pass-through APIs. + +Some APIs require an app to fulfill the request on behalf of another app, e.g. to provide a UX or cross-app data sharing. Generally the calling app doesn't care, or have a say in, which other app provides the API, that is up to the Firebolt distributor. + +To facilitate these APIs, Firebolt denotes an OpenRPC tag with OpenRPC extensions to connect the `provide` API to the `use` API. + +This document is written using the [IETF Best Common Practice 14](https://www.rfc-editor.org/rfc/rfc2119.txt) and should include the following summary in the Overview section: + +The key words "**MUST**", "**MUST NOT**", "**REQUIRED**", "**SHALL**", "**SHALL NOT**", "**SHOULD**", "**SHOULD NOT**", "**RECOMMENDED**", "**NOT RECOMMENDED**", "**MAY**", and "**OPTIONAL**" in this document are to be interpreted as described in [BCP 14](https://www.rfc-editor.org/rfc/rfc2119.txt) [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here. + +## 2. Table of Contents +- [1. Overview](#1-overview) +- [2. Table of Contents](#2-table-of-contents) +- [3. Open RPC Extensions](#3-open-rpc-extensions) + - [3.1. Provided By Extension](#31-provided-by-extension) +- [4. Routing App pass-through APIs](#4-routing-app-pass-through-apis) + - [4.1. No available providers](#41-no-available-providers) + - [4.2. Direct pass-through](#42-direct-pass-through) + - [4.4. Pass-through notifications](#44-pass-through-notifications) +- [5. Provider Candidates](#5-provider-candidates) +- [6. Best Candidate](#6-best-candidate) +- [7. Result Transformations](#7-result-transformations) +- [8. Provider Parameter Injection](#8-provider-parameter-injection) +- [9. API Gateway](#9-api-gateway) +- [10. Example: User Interest](#10-example-user-interest) + - [10.1. User Interest Pull](#101-user-interest-pull) + - [10.2. User Interest Push](#102-user-interest-push) + +## 3. Open RPC Extensions + +### 3.1. Provided By Extension +Firebolt OpenRPC **MUST** support a `string` `x-provided-by` extension property on the `capabilities` tag that denotes a method is provided by some app on the device registering for the specified provider API, e.g.: + +```json +{ + "methods": [ + { + "name": "Keyboard.standard", + "tags": [ + { + "name": "capabilities", + "x-provided-by": "Keyboard.onRequestStandard", + "x-uses": [ + "xrn:firebolt:capability:input:keyboard" + ] + } + ] + } + ] +} +``` + +The method denoted by `x-provided-by` is referred to as the "*provider*" or "*provider method*" for the remainder of this document. + +The method with the `x-provided-by` extension is referred to as the "*platform method*" for the remainder of this document. + +To prevent unresolvable chaining of methods the `x-provided-by` extension **MUST NOT** be used on a method with any value in the `x-provides` extension. + +To prevent compound methods a platform method **MUST** `use` a single capability or `manage` a single capability, but not both. + +The provider method **MUST** provide the same capability that the platform method either uses or manages. + +If a platform method has no provider method then it is not a valid Firebolt OpenRPC method schema, and a validation error **MUST** be generated. + +## 4. Routing App pass-through APIs +App pass-through APIs may be routed in one of several ways. + +When an app calls a platform method, i.e. one with an `x-provided-by` extension, the platform **MUST** use one of the routing methods defined in this section based on various properties of the method. + +### 4.1. No available providers +When an app calls a platform method with an `x-provided-by` extension, the platform **MUST** return an unavailable error if there is no [candidate app](#7-provider-candidates) to execute the provider method. + +```json +{ + "id": 1, + "error": { + "code": -50300, + "message": "Capability is unavailable." + } +} +``` + +Where `` is the capability XRN string, e.g. `xrn:firebolt:capabilities:example:foo`. + +### 4.2. Direct pass-through +A direct pass-through is where a single app provides a single response to a single request by another app. + +This section only applies to app provider methods that do not have an `event` tag. + +The platform method result schema **MUST** either: + +> Match the `x-response` schema on the provider method so that the result can be passed through. +> +> or +> +> Have a property that matches the `x-response-name` string and `x-response` schema on the +> provider method so that the result can be composed and passed through. + +The platform **MUST** call the provider method from the [best candidate](#8-best-candidate) app and acquire the result. + +If the platform method result schema matches the `x-response` schema on the provider method then the value **MUST** be used as-is. + +Otherwise if the platform method result schema has a property that matches the `x-response` schema on the provider method then the value **MUST** be composed into an object under the corresponding property name and the platform **MUST** apply any [result transformations](#9-result-transformations) to the composed result. + +### 4.4. Pass-through notifications +Firebolt events have a synchronous subscriber registration method, e.g. `Lifecycle.onInactive(true)`, in addition to asynchronous notifications when the event actually happens. For events powered by an app pass-through, only the asynchronous notifications are passed in by the providing app. The initial event registration is handled by the platform, and the success response is not handled by the providing app. + +This section only applies to platform methods that have an `event` tag. + +App provided event registration **MUST** not return an availability error due to a lack of providers, since one may be launched at a future point. + +To ensure that event provider methods all behave the same the provider method **MUST** have a `result` schema with `"type"` set to `"null"`, since it will not expect any data in the response from the platform after pushing the notification. + +The platform method result schema **MUST** either: + +> Match the *last* parameter schema on the provider method so that the result can be passed through. +> +> Have a property that matches the *last* parameter name and schema on the provider method so that the result can be passed through. + +Example platform method with context: +```json +{ + "name": "onFoo", + "tags": [ + { + "name": "capabilities", + "x-uses": [ + "xrn:firebolt:capabilities:example:foo" + ], + "x-provided-by": "foo" + }, + { + "name": "event" + } + ], + "params": [ + { + "name": "context1", + "schema":{ + "type": "string" + } + }, + { + "name": "context2", + "schema": { + "type": "number" + } + } + ], + "result": { + "name": "value", + "schema": { + "type": "boolean" + } + } +} +``` + +Matching provider method: + +```json +{ + "name": "foo", + "tags": [ + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capabilities:example:foo" + } + ], + "params": [ + { + "name": "context1", + "schema":{ + "type": "string" + } + }, + { + "name": "context2", + "schema": { + "type": "number" + } + }, + { + "name": "value", + "schema": { + "type": "boolean" + } + } + ] +} +``` + +When a provider app calls a provider method mapped to an event the platform **MUST** ignore the notification if the app is not a [candidate app](#7-provider-candidates) for this capability. + +If the platform method result schema matches the *last* parameter schema on the provider method then the value **MUST** be used as-is. + +Otherwise if the platform method result schema has a property that matches the *last* parameter schema on the provider method then the value **MUST** be composed into an object under the corresponding property name and the platform **MUST** apply any [result transformations](#9-result-transformations) to the composed result. + +If the value was composed into the platform method result under a matching property, then any context parameter values from the provider method that correspond to a property name and schema in the platform method result **MUST** also be composed into the platform method result under those properties. + +Finally the platform **MUST** dispatch the notification to the app that registered for the event via the original platform method, using all but the last parameter as context. + +## 5. Provider Candidates +The Firebolt Device Manifist **MUST** have a list of `ProviderPolicy` configurations that map capabilities to policies for determining candidate providers: + +```json +{ + "providerPolicies": [ + { + "inFocus": true, + "capabilities": [ + "xrn:firebolt:capability:foo:bar" + ] + } + ] +} +``` +The policy **MUST** have a list of capabilities that it is applied to. + +A capability **MUST NOT** be included in more than one policy. + +The policy **MAY** have an `inFocus` boolean. + +If the policy has `inFocus` set to `true` then any app without RCU input focus when the capability is invoked **MUST NOT** be considered a candidate. + +## 6. Best Candidate +If there is only one candidate then it **MUST** be the best candidate. + +If there is more than one candidate, then the candidate app that most recently had RCU input focus **MUST** be the best candidate. + +If none of the candidates have had focus yet, then the candidate app that was most recently launched **MUST** be the best candidate. + +## 7. Result Transformations +A platform method may be configured to insert the providing app id into composite values. This is not allowed in non-composite results to avoid collisions with the provder method sending an appId and Firebolt overriding it. + +If a "composite result" was used to wrap the provider method value and the platform method's schema has an `appId` `string` property at the top level then the property's value **MUST** be set to the the appId of the providing app for that result. + +## 8. Provider Parameter Injection +If the provider method has a parameter named `appId` and the platform method *does not*, then the appId of the app calling the platform method **MUST** be sent to the provider in the `appId` parameter. + +If the platform method is an `event` and the provider method has context parameters then each context parameter from the provider that has a matching context parameter in the event **MUST** have it's value passed to that parameter. + +If the platform method is an `event` with a "composite result" and the provider method has context parameters then each context parameter from the provider that has a matching property on the `result` object **MUST** have it's value copied into that property. + +## 9. API Gateway +The Firebolt API Gateway **MUST** detect app-passthrough APIs and map the `use`/`manage` APIs to the corresponding `provide` APIs by parsing the Firebolt OpenRPC Specification and following the logic outline in this document. + +## 10. Example: User Interest + +The following schemas are referenced by these examples: + +```json +{ + "components": { + "schemas": { + "InterestType": { + "type": "string", + "enum": [ + "interest", + "disinterest" + ] + }, + "InterestReason": { + "type": "string", + "enum": [ + "playlist" + ] + }, + "EntityDetailsFromApp": { + "type": "object", + "properties": { + "appId": { + "type": "string" + }, + "entity": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails" + } + }, + "required": [ + "appId", + "entity" + ] + } + } + } +} +``` + +### 10.1. User Interest Pull + +Platform method: + +```json +{ + "methods": [ + { + "name": "requestUserInterest", + "tags": [ + { + "name": "capabilities", + "x-provided-by": "Discovery.onRequestUserInterest", + "x-uses": [ + "xrn:firebolt:capability:discovery:interest" + ] + } + ], + "params": [ + { + "name": "type", + "required": true, + "schema": { + "$ref": "#/components/schemas/InterestType" + } + }, + { + "name": "reason", + "required": true, + "schema": { + "$ref": "#/components/schemas/InterestReason" + } + } + ], + "result": { + "name": "interest", + "schema": { + "$ref": "#/components/schemas/EntityDetailsFromApp", + } + } + } + ] +} +``` + +Provider method: + +```json +{ + "methods": [ + { + "name": "onRequestUserInterest", + "tags": [ + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capability:discovery:interest" + }, + { + "name": "event", + "x-response": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails" + } + } + ], + "result": { + "name": "request", + "schema": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/InterestType", + }, + "reason": { + "$ref": "#/components/schemas/InterestReason", + } + } + } + } + } + ] +} +``` + +### 10.2. User Interest Push + +Provider method: + +```json +{ + "methods": [ + { + "name": "userInterest", + "tags": [ + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capability:discovery:interest" + } + ], + "params": [ + { + "name": "type", + "schema": { + "$ref": "#/components/schemas/InterestType", + } + }, + { + "name": "reason", + "schema": { + "$ref": "#/components/schemas/InterestReason", + } + }, + { + "name": "entity", + "schema": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails" + } + } + ], + "result": { + "name": "result", + "schema": { + "type": "null" + } + } + } + ] +} +``` + +Platform Method: + +```json +{ + "methods": [ + { + "name": "onUserInterest", + "tags": [ + { + "name": "capabilities", + "x-provided-by": "Discovery.userInterest", + "x-uses": [ + "xrn:firebolt:capability:discovery:interest" + ] + }, + { + "name": "event" + } + ], + "params": [], + "result": { + "name": "interest", + "schema": { + "type": "object", + "properties": { + "appId": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/InterestType" + }, + "reason": { + "$ref": "#/components/schemas/InterestReason" + }, + "entity": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails" + } + } + + } + } + } + ] +} +``` diff --git a/src/openrpc/content.json b/src/openrpc/content.json new file mode 100644 index 000000000..0ffbeadbf --- /dev/null +++ b/src/openrpc/content.json @@ -0,0 +1,195 @@ +{ + "openrpc": "1.2.4", + "info": { + "title": "Content", + "version": "0.0.0", + "description": "" + }, + "methods": [ + { + "name": "requestUserInterest", + "tags": [ + { + "name": "capabilities", + "x-provided-by": "Discovery.onRequestUserInterest", + "x-uses": [ + "xrn:firebolt:capability:discovery:interest" + ] + } + ], + "summary": "Provide information about the entity currently displayed or selected on the screen.", + "description": "Provide information about the entity currently displayed or selected on the screen.", + "params": [ + { + "name": "type", + "required": true, + "schema": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestType" + } + }, + { + "name": "reason", + "required": true, + "schema": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestReason" + } + } + ], + "result": { + "name": "interest", + "schema": { + "$ref": "#/components/schemas/InterestResult" + }, + "summary": "The EntityDetails data." + }, + "examples": [ + { + "name": "Default Example", + "params": [ + { + "name": "type", + "value": "interest" + }, + { + "name": "reason", + "value": "playlist" + } + ], + "result": { + "name": "interest", + "value": { + "appId": "cool-app", + "entity": { + "identifiers": { + "entityId": "345", + "entityType": "program", + "programType": "movie" + }, + "info": { + "title": "Cool Runnings", + "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Pulvinar sapien et ligula ullamcorper malesuada proin libero nunc.", + "releaseDate": "1993-01-01T00:00:00.000Z", + "contentRatings": [ + { + "scheme": "US-Movie", + "rating": "PG" + }, + { + "scheme": "CA-Movie", + "rating": "G" + } + ] + } + } + } + } + } + ] + }, + { + "name": "onUserInterest", + "tags": [ + { + "name": "event" + }, + { + "name": "capabilities", + "x-provided-by": "Discovery.userInterest", + "x-uses": [ + "xrn:firebolt:capability:discovery:interest" + ] + } + ], + "summary": "Provide information about the entity currently displayed or selected on the screen.", + "description": "Provide information about the entity currently displayed or selected on the screen.", + "params": [], + "result": { + "name": "interest", + "schema": { + "$ref": "#/components/schemas/InterestEvent" + }, + "summary": "The EntityDetails data." + }, + "examples": [ + { + "name": "Default Example", + "params": [], + "result": { + "name": "interest", + "value": { + "appId": "cool-app", + "type": "interest", + "reason": "playlist", + "entity": { + "identifiers": { + "entityId": "345", + "entityType": "program", + "programType": "movie" + }, + "info": { + "title": "Cool Runnings", + "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Pulvinar sapien et ligula ullamcorper malesuada proin libero nunc.", + "releaseDate": "1993-01-01T00:00:00.000Z", + "contentRatings": [ + { + "scheme": "US-Movie", + "rating": "PG" + }, + { + "scheme": "CA-Movie", + "rating": "G" + } + ] + } + } + } + } + } + ] + } + ], + "components": { + "schemas": { + "InterestResult": { + "title": "InterestResult", + "type": "object", + "properties": { + "appId": { + "type": "string" + }, + "entity": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails" + } + }, + "required": [ + "appId", + "entity" + ] + }, + "InterestEvent": { + "title": "InterestEvent", + "type": "object", + "properties": { + "appId": { + "type": "string" + }, + "type": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestType" + }, + "reason": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestReason" + }, + "entity": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails" + } + }, + "required": [ + "appId", + "entity", + "type", + "reason" + ] + } + } + } +} \ No newline at end of file diff --git a/src/openrpc/discovery.json b/src/openrpc/discovery.json index eb7530a35..49b7e20aa 100644 --- a/src/openrpc/discovery.json +++ b/src/openrpc/discovery.json @@ -52,6 +52,10 @@ { "name": "capabilities", "x-provides": "xrn:firebolt:capability:discovery:entity-info" + }, + { + "name": "deprecated", + "x-alternative": "Discovery.details" } ], "summary": "Provide information about a program entity and its available watchable assets, such as entitlement status and price, via either a push or pull call flow.", @@ -409,7 +413,7 @@ } ], "result": { - "name": "success", + "name": "result", "value": true } } @@ -424,6 +428,10 @@ { "name": "capabilities", "x-provides": "xrn:firebolt:capability:discovery:purchased-content" + }, + { + "name": "deprecated", + "x-alternative": "Discovery.purchases" } ], "summary": "Provide a list of purchased content for the authenticated account, such as rentals and electronic sell through purchases.", @@ -636,7 +644,7 @@ "name": "identifiers", "summary": "A set of content identifiers for this call to action", "schema": { - "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/ContentIdentifiers" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/Entity" }, "required": true }, @@ -1467,6 +1475,156 @@ } } ] + }, + { + "name": "userInterest", + "summary": "Send an entity that the user has expressed interest in to the platform.", + "tags": [ + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capability:discovery:interest" + } + ], + "params": [ + { + "name": "type", + "required": true, + "schema": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestType" + } + }, + { + "name": "reason", + "required": true, + "schema": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestReason" + } + }, + { + "name": "entity", + "required": true, + "schema": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails" + } + } + ], + "result": { + "name": "result", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Default Example", + "params": [ + { + "name": "type", + "value": "interest" + }, + { + "name": "reason", + "value": "playlist" + }, + { + "name": "entity", + "value": { + "identifiers": { + "entityId": "345", + "entityType": "program", + "programType": "movie" + }, + "info": {} + } + } + ], + "result": { + "name": "result", + "value": null + } + } + ] + }, + { + "name": "Discovery.onRequestUserInterest", + "tags": [ + { + "name": "rpc-only" + }, + { + "name": "event", + "x-response-name": "entity", + "x-response": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails", + "examples": [ + { + "identifiers": { + "entityId": "345", + "entityType": "program", + "programType": "movie" + }, + "info": { + "title": "Cool Runnings", + "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Pulvinar sapien et ligula ullamcorper malesuada proin libero nunc.", + "releaseDate": "1993-01-01T00:00:00.000Z", + "contentRatings": [ + { + "scheme": "US-Movie", + "rating": "PG" + }, + { + "scheme": "CA-Movie", + "rating": "G" + } + ] + } + } + ] + } + }, + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capability:discovery:interest" + } + ], + "summary": "Provide information about the entity currently displayed or selected on the screen.", + "description": "Provide information about the entity currently displayed or selected on the screen.", + "params": [], + "result": { + "name": "request", + "schema": { + "type": "object", + "required": [ + "correlationId", + "parameters" + ], + "properties": { + "correlationId": { + "type": "string" + }, + "parameters": { + "$ref": "#/components/schemas/UserInterestProviderParameters" + } + }, + "additionalProperties": false + } + }, + "examples": [ + { + "name": "Default Example", + "params": [], + "result": { + "name": "request", + "value": { + "correlationId": "xyz", + "parameters": { + "type": "interest", + "reason": "playlist" + } + } + } + } + ] } ], "components": { @@ -1785,6 +1943,22 @@ "enum": [ "xrn:firebolt:channel:any" ] + }, + "UserInterestProviderParameters": { + "title": "UserInterestProviderParameters", + "type": "object", + "required": [ + "type", + "reason" + ], + "properties": { + "type": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestType" + }, + "reason": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestReason" + } + } } } } diff --git a/src/openrpc/keyboard.json b/src/openrpc/keyboard.json index 0ef1f725b..9d8e39412 100644 --- a/src/openrpc/keyboard.json +++ b/src/openrpc/keyboard.json @@ -11,6 +11,7 @@ "tags": [ { "name": "capabilities", + "x-provided-by": "Keyboard.onRequestEmail", "x-uses": [ "xrn:firebolt:capability:input:keyboard" ] @@ -84,6 +85,7 @@ "tags": [ { "name": "capabilities", + "x-provided-by": "Keyboard.onRequestPassword", "x-uses": [ "xrn:firebolt:capability:input:keyboard" ] @@ -128,6 +130,7 @@ "tags": [ { "name": "capabilities", + "x-provided-by": "Keyboard.onRequestStandard", "x-uses": [ "xrn:firebolt:capability:input:keyboard" ] @@ -175,11 +178,9 @@ { "name": "event", "x-response": { - "$ref": "#/components/schemas/KeyboardResult", + "type": "string", "examples": [ - { - "text": "username" - } + "username" ] } }, @@ -220,11 +221,9 @@ { "name": "event", "x-response": { - "$ref": "#/components/schemas/KeyboardResult", + "type": "string", "examples": [ - { - "text": "password" - } + "password" ] } }, @@ -265,11 +264,9 @@ { "name": "event", "x-response": { - "$ref": "#/components/schemas/KeyboardResult", + "type": "string", "examples": [ - { - "text": "email@address.com" - } + "email@address.com" ] } }, @@ -359,23 +356,6 @@ "$ref": "#/components/schemas/KeyboardParameters" } } - }, - "KeyboardResult": { - "title": "KeyboardResult", - "type": "object", - "required": [ - "text" - ], - "properties": { - "text": { - "type": "string", - "description": "The text the user entered into the keyboard" - }, - "canceled": { - "type": "boolean", - "description": "Whether the user canceled entering text before they were finished typing on the keyboard" - } - } } } } diff --git a/src/schemas/advertising.json b/src/schemas/advertising.json new file mode 100644 index 000000000..e3aca3ede --- /dev/null +++ b/src/schemas/advertising.json @@ -0,0 +1,23 @@ +{ + "$id": "https://meta.comcast.com/firebolt/advertising", + "title": "Advertising", + "oneOf": [ + { + "$ref": "#/definitions/SkipRestriction" + } + ], + "definitions": { + "SkipRestriction": { + "title": "SkipRestriction", + "$comment": "xrn:advertising:policy:skipRestriction:", + "type": "string", + "enum": [ + "none", + "adsUnwatched", + "adsAll", + "all" + ], + "description": "The advertisement skip restriction.\n\nApplies to fast-forward/rewind (e.g. trick mode), seeking over an entire opportunity (e.g. jump), seeking out of what's currently playing, and \"Skip this ad...\" features. Seeking over multiple ad opportunities only requires playback of the _last_ opportunity, not all opportunities, preceding the seek destination.\n\n| Value | Description |\n|--------------|--------------------------------------------------------------------------------|\n| none |No fast-forward, jump, or skip restrictions |\n| adsUnwatched | Restrict fast-forward, jump, and skip for unwatched ad opportunities only. |\n| adsAll | Restrict fast-forward, jump, and skip for all ad opportunities |\n| all | Restrict fast-forward, jump, and skip for all ad opportunities and all content |\n\nNamespace: `xrn:advertising:policy:skipRestriction:`\n\n" + } + } +} \ No newline at end of file diff --git a/src/schemas/discovery.json b/src/schemas/discovery.json new file mode 100644 index 000000000..1c13435a7 --- /dev/null +++ b/src/schemas/discovery.json @@ -0,0 +1,79 @@ +{ + "$id": "https://meta.comcast.com/firebolt/discovery", + "title": "Discovery", + "anyOf": [ + { + "$ref": "#/definitions/PurchasedContentResult" + } + ], + "definitions": { + "PurchasedContentResult": { + "title": "PurchasedContentResult", + "type": "object", + "properties": { + "expires": { + "type": "string", + "format": "date-time" + }, + "totalCount": { + "type": "integer", + "minimum": 0 + }, + "entries": { + "type": "array", + "items": { + "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/EntityInfo" + } + } + }, + "required": [ + "expires", + "totalCount", + "entries" + ], + "additionalProperties": false + }, + "EntityInfoResult": { + "title": "EntityInfoResult", + "description": "The result for an `entityInfo()` push or pull.", + "type": "object", + "properties": { + "expires": { + "type": "string", + "format": "date-time" + }, + "entity": { + "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/EntityInfo" + }, + "related": { + "type": "array", + "items": { + "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/EntityInfo" + } + } + }, + "required": [ + "expires", + "entity" + ], + "additionalProperties": false + }, + "InterestType": { + "title": "InterestType", + "type": "string", + "enum": [ + "interest", + "disinterest" + ] + }, + "InterestReason": { + "title": "InterestReason", + "type": "string", + "enum": [ + "playlist", + "reaction", + "recording" + ] + } + } + } diff --git a/src/schemas/entertainment.json b/src/schemas/entertainment.json index 8fd14ee38..8f94854e8 100644 --- a/src/schemas/entertainment.json +++ b/src/schemas/entertainment.json @@ -80,7 +80,6 @@ "required": [ "identifiers", "entityType", - "programType", "title" ], "properties": { diff --git a/src/schemas/entity.json b/src/schemas/entity.json new file mode 100644 index 000000000..2e486e865 --- /dev/null +++ b/src/schemas/entity.json @@ -0,0 +1,456 @@ +{ + "$id": "https://meta.comcast.com/firebolt/entity", + "title": "Entity", + "oneOf": [ + { + "$ref": "#/definitions/Entity" + }, + { + "$ref": "#/definitions/PlayableEntity" + }, + { + "$ref": "#/definitions/EntityDetails" + } + ], + "definitions": { + "Entity": { + "oneOf": [ + { + "$ref": "#/definitions/ProgramEntity" + }, + { + "$ref": "#/definitions/MusicEntity" + }, + { + "$ref": "#/definitions/ChannelEntity" + }, + { + "$ref": "#/definitions/UntypedEntity" + }, + { + "$ref": "#/definitions/PlaylistEntity" + } + ] + }, + "EntityDetails": { + "title": "EntityDetails", + "type": "object", + "required": [ + "identifiers" + ], + "properties": { + "identifiers": { + "$ref": "#/definitions/Entity" + }, + "info": { + "$ref": "#/definitions/Metadata" + }, + "waysToWatch": { + "type": "array", + "items": { + "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/WayToWatch" + }, + "description": "An array of ways a user is might watch this entity, regardless of entitlements." + } + } + }, + "ChannelEntity": { + "title": "ChannelEntity", + "type": "object", + "properties": { + "entityType": { + "const": "channel" + }, + "channelType": { + "type": "string", + "enum": [ + "streaming", + "overTheAir" + ] + }, + "entityId": { + "type": "string", + "description": "ID of the channel, in the target App's scope." + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "required": [ + "entityType", + "channelType", + "entityId" + ], + "additionalProperties": false + }, + "ProgramEntity": { + "title": "ProgramEntity", + "oneOf": [ + { + "$ref": "#/definitions/MovieEntity" + }, + { + "$ref": "#/definitions/TVEpisodeEntity" + }, + { + "$ref": "#/definitions/TVSeasonEntity" + }, + { + "$ref": "#/definitions/TVSeriesEntity" + }, + { + "$ref": "#/definitions/AdditionalEntity" + } + ] + }, + "MusicEntity": { + "title": "MusicEntity", + "type": "object", + "properties": { + "entityType": { + "const": "music" + }, + "musicType": { + "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/MusicType" + }, + "entityId": { + "type": "string" + } + }, + "required": [ + "entityType", + "musicType", + "entityId" + ] + }, + "MovieEntity": { + "title": "MovieEntity", + "description": "A Firebolt compliant representation of a Movie entity.", + "type": "object", + "required": [ + "entityType", + "programType", + "entityId" + ], + "properties": { + "entityType": { + "const": "program" + }, + "programType": { + "const": "movie" + }, + "entityId": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "additionalProperties": false, + "examples": [ + { + "entityType": "program", + "programType": "movie", + "entityId": "el-camino" + } + ] + }, + "TVEpisodeEntity": { + "title": "TVEpisodeEntity", + "description": "A Firebolt compliant representation of a TV Episode entity.", + "type": "object", + "required": [ + "entityType", + "programType", + "entityId", + "seriesId", + "seasonId" + ], + "properties": { + "entityType": { + "const": "program" + }, + "programType": { + "const": "episode" + }, + "entityId": { + "type": "string" + }, + "seriesId": { + "type": "string" + }, + "seasonId": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "additionalProperties": false, + "examples": [ + { + "entityType": "program", + "programType": "episode", + "entityId": "breaking-bad-pilot", + "seriesId": "breaking-bad", + "seasonId": "breaking-bad-season-1" + } + ] + }, + "TVSeasonEntity": { + "title": "TVSeasonEntity", + "description": "A Firebolt compliant representation of a TV Season entity.", + "type": "object", + "required": [ + "entityType", + "programType", + "entityId", + "seriesId" + ], + "properties": { + "entityType": { + "const": "program" + }, + "programType": { + "const": "season" + }, + "entityId": { + "type": "string" + }, + "seriesId": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "additionalProperties": false, + "examples": [ + { + "entityType": "program", + "programType": "season", + "entityId": "breaking-bad-season-1", + "seriesId": "breaking-bad" + } + ] + }, + "TVSeriesEntity": { + "title": "TVSeriesEntity", + "description": "A Firebolt compliant representation of a TV Series entity.", + "type": "object", + "required": [ + "entityType", + "programType", + "entityId" + ], + "properties": { + "entityType": { + "const": "program" + }, + "programType": { + "const": "series" + }, + "entityId": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "additionalProperties": false, + "examples": [ + { + "entityType": "program", + "programType": "series", + "entityId": "breaking-bad" + } + ] + }, + "PlaylistEntity": { + "title": "PlaylistEntity", + "description": "A Firebolt compliant representation of a Playlist entity.", + "type": "object", + "required": [ + "entityType", + "entityId" + ], + "properties": { + "entityType": { + "const": "playlist" + }, + "entityId": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "additionalProperties": false, + "examples": [ + { + "entityType": "playlist", + "entityId": "playlist/xyz" + } + ] + }, + "PlayableEntity": { + "title": "PlayableEntity", + "anyOf": [ + { + "$ref": "#/definitions/MovieEntity" + }, + { + "$ref": "#/definitions/TVEpisodeEntity" + }, + { + "$ref": "#/definitions/PlaylistEntity" + }, + { + "$ref": "#/definitions/MusicEntity" + }, + { + "$ref": "#/definitions/AdditionalEntity" + } + ] + }, + "AdditionalEntity": { + "title": "AdditionalEntity", + "description": "A Firebolt compliant representation of the remaining program entity types.", + "type": "object", + "required": [ + "entityType", + "programType", + "entityId" + ], + "properties": { + "entityType": { + "const": "program" + }, + "programType": { + "type": "string", + "enum": [ + "concert", + "sportingEvent", + "preview", + "other", + "advertisement", + "musicVideo", + "minisode", + "extra" + ] + }, + "entityId": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "additionalProperties": false, + "examples": [ + { + "entityType": "program", + "programType": "concert", + "entityId": "live-aid" + } + ] + }, + "UntypedEntity": { + "title": "UntypedEntity", + "allOf": [ + { + "description": "A Firebolt compliant representation of the remaining entity types.", + "type": "object", + "required": [ + "entityId" + ], + "properties": { + "entityId": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "additionalProperties": false + } + ], + "examples": [ + { + "entityId": "an-entity" + } + ] + }, + "Metadata": { + "title": "Metadata", + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Title of the entity." + }, + "synopsis": { + "type": "string", + "description": "Short description of the entity." + }, + "seasonNumber": { + "type": "number", + "description": "For TV seasons, the season number. For TV episodes, the season that the episode belongs to." + }, + "seasonCount": { + "type": "number", + "description": "For TV series, seasons, and episodes, the total number of seasons." + }, + "episodeNumber": { + "type": "number", + "description": "For TV episodes, the episode number." + }, + "episodeCount": { + "type": "number", + "description": "For TV seasons and episodes, the total number of episodes in the current season." + }, + "releaseDate": { + "type": "string", + "format": "date-time", + "description": "The date that the program or entity was released or first aired." + }, + "contentRatings": { + "type": "array", + "items": { + "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/ContentRating" + }, + "description": "A list of ContentRating objects, describing the entity's ratings in various rating schemes." + } + } + } + } +} \ No newline at end of file diff --git a/src/schemas/intents.json b/src/schemas/intents.json index 6fa2a5807..3ce3d1ca8 100644 --- a/src/schemas/intents.json +++ b/src/schemas/intents.json @@ -370,32 +370,7 @@ "const": "entity" }, "data": { - "anyOf": [ - { - "$ref": "#/definitions/MovieEntity" - }, - { - "$ref": "#/definitions/TVEpisodeEntity" - }, - { - "$ref": "#/definitions/TVSeriesEntity" - }, - { - "$ref": "#/definitions/TVSeasonEntity" - }, - { - "$ref": "#/definitions/MusicEntity" - }, - { - "$ref": "#/definitions/PlaylistEntity" - }, - { - "$ref": "#/definitions/AdditionalEntity" - }, - { - "$ref": "#/definitions/UntypedEntity" - } - ] + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/Entity" } } } @@ -414,398 +389,6 @@ } ] }, - "ChannelEntity": { - "title": "ChannelEntity", - "type": "object", - "properties": { - "entityType": { - "const": "channel" - }, - "channelType": { - "type": "string", - "enum": [ - "streaming", - "overTheAir" - ] - }, - "entityId": { - "type": "string", - "description": "ID of the channel, in the target App's scope." - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "required": [ - "entityType", - "channelType", - "entityId" - ], - "additionalProperties": false - }, - "ProgramEntity": { - "title": "ProgramEntity", - "type": "object", - "properties": { - "entityType": { - "const": "program" - }, - "programType": { - "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/ProgramType" - }, - "entityId": { - "type": "string" - } - }, - "required": [ - "entityType", - "programType", - "entityId" - ] - }, - "MusicEntity": { - "title": "MusicEntity", - "type": "object", - "properties": { - "entityType": { - "const": "music" - }, - "musicType": { - "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/MusicType" - }, - "entityId": { - "type": "string" - } - }, - "required": [ - "entityType", - "musicType", - "entityId" - ] - }, - "MovieEntity": { - "title": "MovieEntity", - "allOf": [ - { - "$ref": "#/definitions/ProgramEntity" - }, - { - "description": "A Firebolt compliant representation of a Movie entity.", - "title": "MovieEntity", - "type": "object", - "required": [ - "entityType", - "programType", - "entityId" - ], - "properties": { - "entityType": { - "const": "program" - }, - "programType": { - "const": "movie" - }, - "entityId": { - "$ref": "#/definitions/Identifier" - }, - "assetId": { - "$ref": "#/definitions/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false - } - ], - "examples": [ - { - "entityType": "program", - "programType": "movie", - "entityId": "el-camino" - } - ] - }, - "TVEpisodeEntity": { - "title": "TVEpisodeEntity", - "allOf": [ - { - "$ref": "#/definitions/ProgramEntity" - }, - { - "description": "A Firebolt compliant representation of a TV Episode entity.", - "title": "TVEpisodeEntity", - "type": "object", - "required": [ - "entityType", - "programType", - "entityId", - "seriesId", - "seasonId" - ], - "properties": { - "entityType": { - "const": "program" - }, - "programType": { - "const": "episode" - }, - "entityId": { - "$ref": "#/definitions/Identifier" - }, - "seriesId": { - "$ref": "#/definitions/Identifier" - }, - "seasonId": { - "$ref": "#/definitions/Identifier" - }, - "assetId": { - "$ref": "#/definitions/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false - } - ], - "examples": [ - { - "entityType": "program", - "programType": "episode", - "entityId": "breaking-bad-pilot", - "seriesId": "breaking-bad", - "seasonId": "breaking-bad-season-1" - } - ] - }, - "TVSeasonEntity": { - "title": "TVSeasonEntity", - "description": "A Firebolt compliant representation of a TV Season entity.", - "allOf": [ - { - "$ref": "#/definitions/ProgramEntity" - }, - { - "type": "object", - "required": [ - "entityType", - "programType", - "entityId", - "seriesId" - ], - "properties": { - "entityType": { - "const": "program" - }, - "programType": { - "const": "season" - }, - "entityId": { - "$ref": "#/definitions/Identifier" - }, - "seriesId": { - "$ref": "#/definitions/Identifier" - }, - "assetId": { - "$ref": "#/definitions/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false - } - ], - "examples": [ - { - "entityType": "program", - "programType": "season", - "entityId": "breaking-bad-season-1", - "seriesId": "breaking-bad" - } - ] - }, - "TVSeriesEntity": { - "title": "TVSeriesEntity", - "allOf": [ - { - "$ref": "#/definitions/ProgramEntity" - }, - { - "description": "A Firebolt compliant representation of a TV Series entity.", - "type": "object", - "required": [ - "entityType", - "programType", - "entityId" - ], - "properties": { - "entityType": { - "const": "program" - }, - "programType": { - "const": "series" - }, - "entityId": { - "$ref": "#/definitions/Identifier" - }, - "assetId": { - "$ref": "#/definitions/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false - } - ], - "examples": [ - { - "entityType": "program", - "programType": "series", - "entityId": "breaking-bad" - } - ] - }, - "PlaylistEntity": { - "title": "PlaylistEntity", - "description": "A Firebolt compliant representation of a Playlist entity.", - "type": "object", - "required": [ - "entityType", - "entityId" - ], - "properties": { - "entityType": { - "const": "playlist" - }, - "entityId": { - "$ref": "#/definitions/Identifier" - }, - "assetId": { - "$ref": "#/definitions/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false, - "examples": [ - { - "entityType": "playlist", - "entityId": "playlist/xyz" - } - ] - }, - "PlayableEntity": { - "title": "PlayableEntity", - "anyOf": [ - { - "$ref": "#/definitions/MovieEntity" - }, - { - "$ref": "#/definitions/TVEpisodeEntity" - }, - { - "$ref": "#/definitions/PlaylistEntity" - }, - { - "$ref": "#/definitions/MusicEntity" - }, - { - "$ref": "#/definitions/AdditionalEntity" - } - ] - }, - "AdditionalEntity": { - "title": "AdditionalEntity", - "allOf": [ - { - "$ref": "#/definitions/ProgramEntity" - }, - { - "description": "A Firebolt compliant representation of the remaining entity types.", - "type": "object", - "required": [ - "entityType", - "entityId" - ], - "properties": { - "entityType": { - "const": "program" - }, - "programType": { - "type": "string", - "enum": [ - "concert", - "sportingEvent", - "preview", - "other", - "advertisement", - "musicVideo", - "minisode", - "extra" - ] - }, - "entityId": { - "$ref": "#/definitions/Identifier" - }, - "assetId": { - "$ref": "#/definitions/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false - } - ], - "examples": [ - { - "entityType": "program", - "programType": "concert", - "entityId": "live-aid" - } - ] - }, - "UntypedEntity": { - "title": "UntypedEntity", - "allOf": [ - { - "description": "A Firebolt compliant representation of the remaining entity types.", - "type": "object", - "required": [ - "entityId" - ], - "properties": { - "entityId": { - "$ref": "#/definitions/Identifier" - }, - "assetId": { - "$ref": "#/definitions/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false - } - ], - "examples": [ - { - "entityId": "an-entity" - } - ] - }, "TuneIntent": { "description": "A Firebolt compliant representation of a user intention to 'tune' to a traditional over-the-air broadcast, or an OTT Stream from an OTT or vMVPD App.", "title": "TuneIntent", @@ -833,7 +416,7 @@ "additionalProperties": false, "properties": { "entity": { - "$ref": "#/definitions/ChannelEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/ChannelEntity" }, "options": { "description": "The options property of the data property MUST have only one of the following fields.", @@ -902,7 +485,7 @@ "const": "playback" }, "data": { - "$ref": "#/definitions/PlayableEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/PlayableEntity" } } } @@ -1032,7 +615,7 @@ "type": "object", "properties": { "entity": { - "$ref": "#/definitions/PlayableEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/PlayableEntity" }, "options": { "type": "object", @@ -1272,7 +855,7 @@ "menus": { "type": "array", "items": { - "$ref": "#/definitions/Identifier" + "type": "string" }, "minItems": 1, "maxItems": 100 @@ -1283,7 +866,7 @@ "type": "object", "properties": { "appId": { - "$ref": "#/definitions/Identifier" + "type": "string" }, "exclude": { "type": "boolean" @@ -1369,19 +952,19 @@ "entity": { "anyOf": [ { - "$ref": "#/definitions/MovieEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/MovieEntity" }, { - "$ref": "#/definitions/TVEpisodeEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/TVEpisodeEntity" }, { - "$ref": "#/definitions/TVSeriesEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/TVSeriesEntity" }, { - "$ref": "#/definitions/TVSeasonEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/TVSeasonEntity" }, { - "$ref": "#/definitions/AdditionalEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/AdditionalEntity" } ] }, @@ -1391,24 +974,24 @@ "type": "object", "properties": { "appId": { - "$ref": "#/definitions/Identifier" + "type": "string" }, "entity": { "anyOf": [ { - "$ref": "#/definitions/MovieEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/MovieEntity" }, { - "$ref": "#/definitions/TVEpisodeEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/TVEpisodeEntity" }, { - "$ref": "#/definitions/TVSeriesEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/TVSeriesEntity" }, { - "$ref": "#/definitions/TVSeasonEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/TVSeasonEntity" }, { - "$ref": "#/definitions/AdditionalEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/AdditionalEntity" } ] } @@ -1419,7 +1002,7 @@ "menus": { "type": "array", "items": { - "$ref": "#/definitions/Identifier" + "type": "string" }, "minItems": 1, "maxItems": 100 @@ -1430,7 +1013,7 @@ "type": "object", "properties": { "appId": { - "$ref": "#/definitions/Identifier" + "type": "string" }, "exclude": { "type": "boolean" @@ -2205,6 +1788,7 @@ } ] }, + "Identifier": { "type": "string" }, @@ -2229,7 +1813,7 @@ "type": "string" }, "appId": { - "$ref": "#/definitions/Identifier" + "type": "string" } } }, diff --git a/src/schemas/lifecycle.json b/src/schemas/lifecycle.json new file mode 100644 index 000000000..906cf41ea --- /dev/null +++ b/src/schemas/lifecycle.json @@ -0,0 +1,38 @@ +{ + "$id": "https://meta.comcast.com/firebolt/lifecycle", + "title": "Lifecycle", + "oneOf": [ + { + "$ref": "#/definitions/LifecycleState" + }, + { + "$ref": "#/definitions/CloseReason" + } + ], + "definitions": { + "LifecycleState": { + "title": "LifecycleState", + "description": "The application lifecycle state", + "type": "string", + "enum": [ + "initializing", + "inactive", + "foreground", + "background", + "unloading", + "suspended" + ] + }, + "CloseReason": { + "title": "CloseReason", + "description": "The application close reason", + "type": "string", + "enum": [ + "remoteButton", + "userExit", + "done", + "error" + ] + } + } +} diff --git a/src/schemas/types.json b/src/schemas/types.json new file mode 100644 index 000000000..0caa49c1f --- /dev/null +++ b/src/schemas/types.json @@ -0,0 +1,194 @@ +{ + "$id": "https://meta.comcast.com/firebolt/types", + "title": "Types", + "definitions": { + "SemanticVersion": { + "title": "SemanticVersion", + "type": "object", + "properties": { + "major": { + "type": "integer", + "minimum": 0 + }, + "minor": { + "type": "integer", + "minimum": 0 + }, + "patch": { + "type": "integer", + "minimum": 0 + }, + "readable": { + "type": "string" + } + }, + "required": [ + "major", + "minor", + "patch", + "readable" + ], + "additionalProperties": false + }, + "AudioProfile": { + "title": "AudioProfile", + "type": "string", + "enum": [ + "stereo", + "dolbyDigital5.1", + "dolbyDigital7.1", + "dolbyDigital5.1+", + "dolbyDigital7.1+", + "dolbyAtmos" + ] + }, + "BooleanMap": { + "title": "BooleanMap", + "type": "object", + "additionalProperties": { + "type": "boolean" + } + }, + "FlatMap": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "LocalizedString": { + "title": "LocalizedString", + "description": "Localized string supports either a simple `string` or a Map of language codes to strings. When using a simple `string`, the current preferred langauge from `Localization.langauge()` is assumed.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + ], + "examples": [ + "A simple string, with no language code", + { + "en": "This is english", + "es": "esto es espaƱol" + } + ] + }, + "ListenResponse": { + "title": "ListenResponse", + "type": "object", + "required": [ + "event", + "listening" + ], + "properties": { + "event": { + "type": "string", + "pattern": "[a-zA-Z]+\\.on[A-Z][a-zA-Z]+" + }, + "listening": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "ProviderRequest": { + "title": "ProviderRequest", + "type": "object", + "required": [ + "correlationId" + ], + "additionalProperties": false, + "properties": { + "correlationId": { + "type": "string", + "description": "The id that was passed in to the event that triggered a provider method to be called" + }, + "parameters": { + "description": "The result of the provider response.", + "type": ["object", "null"] + } + } + }, + "ProviderResponse": { + "title": "ProviderResponse", + "type": "object", + "required": [ + "correlationId" + ], + "additionalProperties": false, + "properties": { + "correlationId": { + "type": "string", + "description": "The id that was passed in to the event that triggered a provider method to be called" + }, + "result": { + "description": "The result of the provider response." + } + } + }, + "Timeout": { + "title": "Timeout", + "description": "Defines the timeout in seconds. If the threshold for timeout is passed for any operation without a result it will throw an error.", + "type": "integer", + "default": 0, + "minimum": 0, + "maximum": 9999 + }, + "Dimensions": { + "type": "object", + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "required": [ "width", "height" ] + }, + "Image": { + "type": "object", + "properties": { + "uri": { + "description": "URI for the image. May be a relative path (e.g. ./foo/image.png) or absolute (e.g. https://foo.com/bar.png) depending on usage.", + "type": "string" + }, + "aspectRatio": { + "description": "The aspect ratio of the image", + "type": "string", + "pattern": "^\\d+x\\d+" + }, + "description": { + "description": "Description of the image.", + "type": "string" + }, + "type": { + "description": "The type of the image.", + "type": "string", + "enum": [ + "icon", "poster", "banner", "splash", "hero" + ] + } + }, + "required": [ + "uri", "aspectRatio", "type" + ] + } + } +} \ No newline at end of file diff --git a/src/sdks/core/sdk.config.json b/src/sdks/core/sdk.config.json index f406f8bea..14ba4d55c 100644 --- a/src/sdks/core/sdk.config.json +++ b/src/sdks/core/sdk.config.json @@ -69,7 +69,8 @@ ], "provide": [ "xrn:firebolt:capability:discovery:entity-info", - "xrn:firebolt:capability:discovery:purchased-content" + "xrn:firebolt:capability:discovery:purchased-content", + "xrn:firebolt:capability:discovery:interest" ] }, { diff --git a/src/sdks/core/test/suite/discovery.test.ts b/src/sdks/core/test/suite/discovery.test.ts index 638c57432..7402dce42 100644 --- a/src/sdks/core/test/suite/discovery.test.ts +++ b/src/sdks/core/test/suite/discovery.test.ts @@ -150,3 +150,12 @@ test("clear()", () => { const result: boolean = Discovery.clear(-1000); expect(result).toBeFalsy(); }); + +test("details() provider", () => { + + class myUserInterestProvider implements Discovery.UserInterestProvider { + userInterest(parameters?: object, session?: Discovery.ProviderSession): Promise { + return null + } + } +}) \ No newline at end of file diff --git a/src/sdks/discovery/.npmignore b/src/sdks/discovery/.npmignore new file mode 100644 index 000000000..0e2ba4d22 --- /dev/null +++ b/src/sdks/discovery/.npmignore @@ -0,0 +1,6 @@ +build/* +src/* +test/* +.DS_Store +jest.config.* +tsconfig.* \ No newline at end of file diff --git a/src/sdks/discovery/CHANGELOG.md b/src/sdks/discovery/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/src/sdks/discovery/CONTRIBUTING.md b/src/sdks/discovery/CONTRIBUTING.md new file mode 120000 index 000000000..c97564d93 --- /dev/null +++ b/src/sdks/discovery/CONTRIBUTING.md @@ -0,0 +1 @@ +../../../CONTRIBUTING.md \ No newline at end of file diff --git a/src/sdks/discovery/LICENSE b/src/sdks/discovery/LICENSE new file mode 120000 index 000000000..5853aaea5 --- /dev/null +++ b/src/sdks/discovery/LICENSE @@ -0,0 +1 @@ +../../../LICENSE \ No newline at end of file diff --git a/src/sdks/discovery/NOTICE b/src/sdks/discovery/NOTICE new file mode 120000 index 000000000..295f6bdb3 --- /dev/null +++ b/src/sdks/discovery/NOTICE @@ -0,0 +1 @@ +../../../NOTICE \ No newline at end of file diff --git a/src/sdks/discovery/README.md b/src/sdks/discovery/README.md new file mode 100644 index 000000000..2ec46b95d --- /dev/null +++ b/src/sdks/discovery/README.md @@ -0,0 +1,26 @@ +--- +title: Firebolt Discovery SDK +--- + +[![semantic-release: conventional](https://img.shields.io/badge/semantic--release-conventional-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release) + +# Firebolt Discovery SDK +For building Firebolt compliant apps for discovering first-party content on Firebolt devices. + +## Usage +To install, run: + +``` +npm install @firebolt-js/discovery-sdk +``` + +To use the package, import one of it's modules, e.g.: + +```js +import { Content } from '@firebolt-js/discovery-sdk' +``` + +## Contributing +The Firebolt SDKs are built using the Firebolt OpenRPC toolset: + +See [Firebolt OpenRPC](https://www.github.com/rdkcentral/firebolt-openrpc/), for more info. diff --git a/src/sdks/discovery/jest.config.json b/src/sdks/discovery/jest.config.json new file mode 100644 index 000000000..745370cfa --- /dev/null +++ b/src/sdks/discovery/jest.config.json @@ -0,0 +1,8 @@ +{ + "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|js?)$", + "transform": {}, + "testPathIgnorePatterns": ["/node_modules/", "/../../../test"], + "moduleFileExtensions": ["js", "jsx", "mjs"], + "verbose": true, + "testEnvironment": "jest-environment-jsdom" + } \ No newline at end of file diff --git a/src/sdks/discovery/package.json b/src/sdks/discovery/package.json new file mode 100644 index 000000000..40e4cd992 --- /dev/null +++ b/src/sdks/discovery/package.json @@ -0,0 +1,53 @@ +{ + "name": "@firebolt-js/discovery-sdk", + "version": "1.2.0-next.2", + "description": "The Firebolt Discovery JS SDK", + "main": "./dist/lib/firebolt-discovery.mjs", + "types": "./dist/lib/firebolt-discovery.d.ts", + "exports": { + ".": "./dist/lib/firebolt-discovery.mjs" + }, + "type": "module", + "scripts": { + "validate": "npx firebolt-openrpc validate --input ./dist/firebolt-discovery-open-rpc.json", + "sdk": "npx firebolt-openrpc sdk --input ./dist/firebolt-discovery-open-rpc.json --template ./src/js --output ./build/javascript/src", + "native": "npx firebolt-openrpc sdk --input ./dist/firebolt-discovery-open-rpc.json --template ./src/js --output ./build/c/src --language ../../../node_modules/@firebolt-js/openrpc/languages/c", + "compile": "cd ../../.. && npm run compile", + "slice": "npx firebolt-openrpc slice -i ../../../dist/firebolt-open-rpc.json --sdk ./sdk.config.json -o ./dist/firebolt-discovery-open-rpc.json", + "docs": "npx firebolt-openrpc docs --input ./dist/firebolt-discovery-open-rpc.json --output build/docs/markdown --as-path", + "wiki": "npx firebolt-openrpc docs --input ./dist/firebolt-discovery-open-rpc.json --output build/docs/markdown", + "dist:notest": "npm run clean && npm run slice && npm run validate && npm run sdk && npm run docs && npm run prettier && npm run dist:copy && echo 'Firebolt Discovery SDK /dist/ is ready.\n'", + "dist:copy": "npm run dist:copy:sdk && npm run dist:copy:docs", + "dist:copy:sdk": "mkdirp ./dist && cp -R build/javascript/src dist/lib && cp ./dist/firebolt-discovery-open-rpc.json ../../../dist/firebolt-discovery-open-rpc.json", + "dist:copy:docs": "mkdirp ./dist && cp -R build/docs/markdown dist/docs", + "dist": "npm run dist:notest && npm run test", + "clean": "rm -rf ./build && rm -rf ./dist", + "test:setup": "rm -rf test/transpiled-suite && npx tsc --target es6 --moduleResolution node --outDir test/transpiled-suite", + "test": "npm run test:setup && NODE_OPTIONS=--experimental-vm-modules npx --config=jest.config.json --detectOpenHandles jest", + "prepack": "node ../../js/version.mjs validate && npm run broilerplate", + "broilerplate": "rm ./CONTRIBUTING.md && cp ../../../CONTRIBUTING.md ./CONTRIBUTING.md && rm ./LICENSE && cp ../../../LICENSE ./LICENSE && rm ./NOTICE && cp ../../../NOTICE ./NOTICE", + "prettier": "prettier build/**/*.mjs --write --parser babel && prettier build/**/*.md --write --parser markdown" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rdkcentral/firebolt-core-sdk", + "directory": "src/sdks/discovery" + }, + "author": "", + "bugs": { + "url": "https://github.com/rdkcentral/firebolt-core-sdk/issues" + }, + "homepage": "https://github.com/rdkcentral/firebolt-core-sdk#readme", + "devDependencies": { + "jest": "^28.1.0", + "jest-environment-jsdom": "^28.1.3", + "prettier": "^3.1.0", + "typescript": "^4.6.4" + }, + "keywords": [ + "firebolt", + "apps", + "sdk" + ], + "license": "Apache-2.0" +} \ No newline at end of file diff --git a/src/sdks/discovery/sdk.config.json b/src/sdks/discovery/sdk.config.json new file mode 100644 index 000000000..535d3bb12 --- /dev/null +++ b/src/sdks/discovery/sdk.config.json @@ -0,0 +1,15 @@ +{ + "info": { + "title": "Firebolt Discovery SDK" + }, + "methods": [ + { + "module": "Content", + "use": [ + "xrn:firebolt:capability:discovery:purchased-content", + "xrn:firebolt:capability:discovery:entity-info", + "xrn:firebolt:capability:discovery:interest" + ] + } + ] +} diff --git a/src/sdks/discovery/src/js/sdk/index.mjs b/src/sdks/discovery/src/js/sdk/index.mjs new file mode 100644 index 000000000..a9f2f5ca6 --- /dev/null +++ b/src/sdks/discovery/src/js/sdk/index.mjs @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { setMockResponses } from './Transport/MockTransport.mjs' + +/* ${MOCK_IMPORTS} */ + +setMockResponses({ + /* ${MOCK_OBJECTS} */ +}) + +/* ${EXPORTS} */ +export { default as Log } from './Log/index.mjs' +export { default as Events } from './Events/index.mjs' +export { default as Settings } from './Settings/index.mjs' diff --git a/src/sdks/discovery/test/suite/content.test.ts b/src/sdks/discovery/test/suite/content.test.ts new file mode 100644 index 000000000..1ab4e98c8 --- /dev/null +++ b/src/sdks/discovery/test/suite/content.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2021 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { test, expect } from "@jest/globals"; +import { Content } from "../../build/javascript/src/firebolt-discovery"; + +test("Content.requestUserInterest()", () => { + return Content.requestUserInterest(Content.InterestType.INTEREST, Content.InterestReason.PLAYLIST).then((interest: Content.InterestResult) => { + const entity = interest.entity + const appId = interest.appId + expect(appId).toBeDefined() + expect(entity).toBeDefined() + expect(entity.info.title).toBe("Cool Runnings") + }) +}); + +test("Content.onUserInterest()", () => { + return Content.listen('userInterest', (interest: Content.InterestEvent) => { + const entity = interest.entity + const appId = interest.appId + const reason = interest.reason + expect(interest['type']).toBeDefined() + expect(reason).toBeDefined() + expect(appId).toBeDefined() + expect(entity).toBeDefined() + expect(entity.info.title).toBe("Cool Runnings") + }) +}); diff --git a/src/sdks/discovery/tsconfig.json b/src/sdks/discovery/tsconfig.json new file mode 100644 index 000000000..56ff58bc8 --- /dev/null +++ b/src/sdks/discovery/tsconfig.json @@ -0,0 +1,5 @@ +{ + "include": [ + "test/suite/*" + ] +} \ No newline at end of file diff --git a/src/sdks/manage/test/suite/keyboard.test.ts b/src/sdks/manage/test/suite/keyboard.test.ts index c40b7d40e..6ee3a5aed 100644 --- a/src/sdks/manage/test/suite/keyboard.test.ts +++ b/src/sdks/manage/test/suite/keyboard.test.ts @@ -82,19 +82,19 @@ class DelegatingKBProvider implements Keyboard.KeyboardInputProvider { standard( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { return this.delegate.standard(parameters, session) } password( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { return this.delegate.password(parameters, session) } email( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { return this.delegate.email(parameters, session) } } @@ -103,21 +103,19 @@ class KBProvider implements Keyboard.KeyboardInputProvider { standard( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { - return Promise.resolve({ - text: 'foo' - }); + ): Promise { + return Promise.resolve('foo'); } password( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { return Promise.resolve(null); } email( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { return Promise.resolve(null); } } @@ -126,19 +124,19 @@ class KBProviderWithError implements Keyboard.KeyboardInputProvider { async standard( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { throw new Error('failed') } async password( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { throw new Error('failed') } async email( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { throw new Error('failed') } } @@ -156,7 +154,7 @@ test("Keyboard.provide() declarations", async () => { let result = await promise console.log(result) expect(result.method).toStrictEqual('keyboard.standardResponse') - expect(result.params.result.text).toStrictEqual('foo') + expect(result.params.result).toStrictEqual('foo') }); test("Keyboard.provide() with blank object", () => {