diff --git a/core/main/src/bootstrap/start_fbgateway_step.rs b/core/main/src/bootstrap/start_fbgateway_step.rs index 4d6323587..e8fc25556 100644 --- a/core/main/src/bootstrap/start_fbgateway_step.rs +++ b/core/main/src/bootstrap/start_fbgateway_step.rs @@ -48,9 +48,9 @@ impl FireboltGatewayStep { async fn init_handlers(&self, state: PlatformState, extn_methods: Methods) -> Methods { let mut methods = Methods::new(); - // TODO: Ultimately this should be able to register all provider below, for now just does - // AcknowledgeChallenge and PinChallenge. - ProviderRegistrar::register(&state, &mut methods); + // TODO: Ultimately this may be able to register all providers below, for now just does + // those included by build_provider_relation_sets(). + ProviderRegistrar::register_methods(&state, &mut methods); let _ = methods.merge(DeviceRPCProvider::provide_with_alias(state.clone())); let _ = methods.merge(WifiRPCProvider::provide_with_alias(state.clone())); diff --git a/core/main/src/firebolt/handlers/discovery_rpc.rs b/core/main/src/firebolt/handlers/discovery_rpc.rs index ee062eef5..9ed265f4b 100644 --- a/core/main/src/firebolt/handlers/discovery_rpc.rs +++ b/core/main/src/firebolt/handlers/discovery_rpc.rs @@ -625,7 +625,7 @@ impl DiscoveryServer for DiscoveryImpl { &self.state, FireboltCap::Short(ENTITY_INFO_CAPABILITY.into()).as_str(), String::from("entityInfo"), - ENTITY_INFO_EVENT, + String::from(ENTITY_INFO_EVENT), ctx, request, ) @@ -697,7 +697,7 @@ impl DiscoveryServer for DiscoveryImpl { &self.state, FireboltCap::Short(PURCHASED_CONTENT_CAPABILITY.into()).as_str(), String::from("purchasedContent"), - PURCHASED_CONTENT_EVENT, + String::from(PURCHASED_CONTENT_EVENT), ctx, request, ) diff --git a/core/main/src/firebolt/handlers/keyboard_rpc.rs b/core/main/src/firebolt/handlers/keyboard_rpc.rs index 58526a96a..a5717968a 100644 --- a/core/main/src/firebolt/handlers/keyboard_rpc.rs +++ b/core/main/src/firebolt/handlers/keyboard_rpc.rs @@ -302,7 +302,7 @@ impl KeyboardImpl { &self.platform_state, String::from(KEYBOARD_PROVIDER_CAPABILITY), method, - event_name, + String::from(event_name), ctx, request, ) diff --git a/core/main/src/firebolt/handlers/lcm_rpc.rs b/core/main/src/firebolt/handlers/lcm_rpc.rs index b77f5a0a0..bb0e50f85 100644 --- a/core/main/src/firebolt/handlers/lcm_rpc.rs +++ b/core/main/src/firebolt/handlers/lcm_rpc.rs @@ -113,7 +113,7 @@ impl LifecycleManagementImpl { &self.state, FireboltCap::short("app:lifecycle").as_str(), method.into(), - event_name, + String::from(event_name), ctx, request, ) @@ -121,7 +121,7 @@ impl LifecycleManagementImpl { Ok(ListenerResponse { listening: listen, - event: event_name.into(), + event: String::from(event_name), }) } } diff --git a/core/main/src/firebolt/handlers/localization_rpc.rs b/core/main/src/firebolt/handlers/localization_rpc.rs index 6f0b36dea..4661e9406 100644 --- a/core/main/src/firebolt/handlers/localization_rpc.rs +++ b/core/main/src/firebolt/handlers/localization_rpc.rs @@ -221,7 +221,7 @@ impl LocalizationImpl { // TODO update with Firebolt Cap in later effort "xrn:firebolt:capability:localization:locale".into(), method.into(), - event_name, + String::from(event_name), ctx, request, ) diff --git a/core/main/src/firebolt/handlers/provider_registrar.rs b/core/main/src/firebolt/handlers/provider_registrar.rs index bf6c5192b..4cfb06e7e 100644 --- a/core/main/src/firebolt/handlers/provider_registrar.rs +++ b/core/main/src/firebolt/handlers/provider_registrar.rs @@ -15,13 +15,19 @@ // SPDX-License-Identifier: Apache-2.0 // +use std::{sync::Arc, time::Duration}; + use crate::{ - firebolt::rpc::register_aliases, service::apps::provider_broker::ProviderBroker, - state::platform_state::PlatformState, + firebolt::rpc::register_aliases, + service::apps::{ + app_events::AppEvents, + provider_broker::{ProviderBroker, ProviderBrokerRequest}, + }, + state::{openrpc_state::ProviderRelationSet, platform_state::PlatformState}, }; use jsonrpsee::{ - core::{server::rpc_module::Methods, RpcResult}, - types::{error::CallError, ParamsSequence}, + core::{server::rpc_module::Methods, Error, RpcResult}, + types::{error::CallError, Params, ParamsSequence}, RpcModule, }; use ripple_sdk::{ @@ -32,22 +38,49 @@ use ripple_sdk::{ fb_pin::PinChallengeResponse, provider::{ ChallengeResponse, ExternalProviderError, ExternalProviderResponse, FocusRequest, - ProviderResponse, ProviderResponsePayload, ProviderResponsePayloadType, + ProviderRequestPayload, ProviderResponse, ProviderResponsePayload, + ProviderResponsePayloadType, }, }, - gateway::rpc_gateway_api::CallContext, + gateway::rpc_gateway_api::{CallContext, CallerSession}, }, - log::error, + log::{error, info}, + tokio::{sync::oneshot, time::timeout}, }; +use serde_json::Value; + +// TODO: Add to config +const DEFAULT_PROVIDER_RESPONSE_TIMEOUT_MS: u64 = 15000; + +#[derive(Debug)] +enum MethodType { + AppEventListener, + Provider, + AppEventEmitter, + Error, + ProviderInvoker, + Focus, + Response, +} #[derive(Clone)] struct RpcModuleContext { platform_state: PlatformState, + method: String, + provider_relation_set: ProviderRelationSet, } impl RpcModuleContext { - fn new(platform_state: PlatformState) -> Self { - RpcModuleContext { platform_state } + fn new( + platform_state: PlatformState, + method: String, + provider_relation_set: ProviderRelationSet, + ) -> Self { + RpcModuleContext { + method, + platform_state, + provider_relation_set, + } } } @@ -97,144 +130,647 @@ impl ProviderRegistrar { }); } } + ProviderResponsePayloadType::Generic => { + let external_provider_response: Result, CallError> = + params_sequence.next(); + + if let Ok(r) = external_provider_response { + return Some(ProviderResponse { + correlation_id: r.correlation_id, + result: ProviderResponsePayload::Generic(r.result), + }); + } + } _ => error!("get_provider_response: Unsupported payload type"), } None } - pub fn register(platform_state: &PlatformState, methods: &mut Methods) { - let provider_map = platform_state.open_rpc_state.get_provider_map(); - for provides in provider_map.clone().keys() { - if let Some(provider_set) = provider_map.get(provides) { - if let Some(attributes) = provider_set.attributes { - let rpc_module_context = RpcModuleContext::new(platform_state.clone()); - let mut rpc_module = RpcModule::new(rpc_module_context.clone()); - - // Register request function - if let Some(method) = provider_set.request.clone() { - let request_method = - FireboltOpenRpcMethod::name_with_lowercase_module(&method.name).leak(); - - rpc_module - .register_async_method( - request_method, - move |params, context| async move { - let mut params_sequence = params.sequence(); - let call_context: CallContext = params_sequence.next().unwrap(); - let request: ListenRequest = params_sequence.next().unwrap(); - let listening = request.listen; - - ProviderBroker::register_or_unregister_provider( - &context.platform_state, - attributes.capability.into(), - attributes.method.into(), - attributes.event, - call_context, - request, - ) - .await; - - Ok(ListenerResponse { - listening, - event: attributes.event.into(), - }) - }, - ) - .unwrap(); - } + fn register_method( + method_name: &'static str, + method_type: MethodType, + rpc_module: &mut RpcModule, + ) -> bool { + info!( + "register_method: method_name={}, method_type={:?}", + method_name, method_type + ); + + let result = match method_type { + MethodType::AppEventEmitter => { + rpc_module.register_async_method(method_name, Self::callback_app_event_emitter) + } + MethodType::AppEventListener => { + rpc_module.register_async_method(method_name, Self::callback_app_event_listener) + } + MethodType::Error => { + rpc_module.register_async_method(method_name, Self::callback_error) + } + MethodType::Focus => { + rpc_module.register_async_method(method_name, Self::callback_focus) + } + MethodType::Provider => { + rpc_module.register_async_method(method_name, Self::callback_register_provider) + } + MethodType::ProviderInvoker => { + rpc_module.register_async_method(method_name, Self::callback_provider_invoker) + } + MethodType::Response => { + rpc_module.register_async_method(method_name, Self::callback_response) + } + }; + + match result { + Ok(_) => true, + Err(e) => { + error!("register_method: Error registering method: {:?}", e); + false + } + } + } + + async fn callback_app_event_listener( + params: Params<'static>, + context: Arc, + ) -> Result { + info!("callback_app_event_listener: method={}", context.method); + info!( + "*** DEBUG: callback_app_event_listener: method/event={}", + context.method + ); + + let mut params_sequence = params.sequence(); + let call_context: CallContext = params_sequence.next().unwrap(); + let request: ListenRequest = params_sequence.next().unwrap(); + let listen = request.listen; + + AppEvents::add_listener( + &context.platform_state, + context.method.clone(), + call_context, + request, + ); + Ok(ListenerResponse { + listening: listen, + event: context.method.clone(), + }) + } + + async fn callback_register_provider( + params: Params<'static>, + context: Arc, + ) -> Result { + info!("callback_register_provider: method={}", context.method); + + if let Some(capability) = &context.provider_relation_set.capability { + let mut params_sequence = params.sequence(); + let call_context: CallContext = params_sequence.next().unwrap(); + let request: ListenRequest = params_sequence.next().unwrap(); + let listening = request.listen; + + ProviderBroker::register_or_unregister_provider( + &context.platform_state, + capability.clone(), + context.method.clone(), + context.method.clone(), + call_context, + request, + ) + .await; + + Ok(ListenerResponse { + listening, + event: context.method.clone(), + }) + } else { + Err(Error::Custom("Missing provides attribute".to_string())) + } + } + + async fn callback_app_event_emitter( + params: Params<'static>, + context: Arc, + ) -> Result, Error> { + info!( + "callback_app_event_emitter: method={}, event={:?}", + context.method, &context.provider_relation_set.provides_to + ); + if let Some(event) = &context.provider_relation_set.provides_to { + let mut params_sequence = params.sequence(); + let _call_context: CallContext = params_sequence.next().unwrap(); + let result: Value = params_sequence.next().unwrap(); + + AppEvents::emit( + &context.platform_state, + &FireboltOpenRpcMethod::name_with_lowercase_module(event), + &result, + ) + .await; + } - // Register focus function - if let Some(method) = provider_set.focus.clone() { - let focus_method = - FireboltOpenRpcMethod::name_with_lowercase_module(&method.name).leak(); - - rpc_module - .register_async_method( - focus_method, - move |params, context| async move { - let mut params_sequence = params.sequence(); - let call_context: CallContext = params_sequence.next().unwrap(); - let request: FocusRequest = params_sequence.next().unwrap(); - - ProviderBroker::focus( - &context.platform_state, - call_context, - attributes.capability.into(), - request, - ) - .await; - Ok(None) as RpcResult> - }, - ) - .unwrap(); + Ok(None) + } + + async fn callback_error( + params: Params<'static>, + context: Arc, + ) -> Result, Error> { + info!("callback_error: method={}", context.method); + let params_sequence = params.sequence(); + + if let Some(attributes) = context.provider_relation_set.attributes { + if let Some(provider_response) = ProviderRegistrar::get_provider_response( + attributes.error_payload_type.clone(), + params_sequence, + ) { + ProviderBroker::provider_response(&context.platform_state, provider_response).await; + } + } else { + error!( + "callback_error: NO ATTRIBUTES: context.method={}", + context.method + ); + return Err(Error::Custom(String::from("Missing provider attributes"))); + } + + Ok(None) as RpcResult> + } + + async fn callback_provider_invoker( + params: Params<'static>, + context: Arc, + ) -> Result { + let mut params_sequence = params.sequence(); + let call_context: CallContext = params_sequence.next().unwrap(); + let params: Value = params_sequence.next().unwrap(); + + info!("callback_provider_invoker: method={}", context.method); + + if let Some(provided_by) = &context.provider_relation_set.provided_by { + let provider_relation_map = context + .platform_state + .open_rpc_state + .get_provider_relation_map(); + + if let Some(provided_by_set) = provider_relation_map.get( + &FireboltOpenRpcMethod::name_with_lowercase_module(provided_by), + ) { + if let Some(capability) = &provided_by_set.capability { + let (provider_response_payload_tx, provider_response_payload_rx) = + oneshot::channel::(); + + let caller = CallerSession { + session_id: Some(call_context.session_id.clone()), + app_id: Some(call_context.app_id.clone()), + }; + + let provider_broker_request = ProviderBrokerRequest { + capability: capability.clone(), + method: provided_by.clone(), + caller, + request: ProviderRequestPayload::Generic(params), + tx: provider_response_payload_tx, + app_id: None, + }; + + ProviderBroker::invoke_method(&context.platform_state, provider_broker_request) + .await; + + match timeout( + Duration::from_millis(DEFAULT_PROVIDER_RESPONSE_TIMEOUT_MS), + provider_response_payload_rx, + ) + .await + { + Ok(result) => match result { + Ok(provider_response_payload) => { + return Ok(provider_response_payload.as_value()); + } + Err(_) => { + return Err(Error::Custom(String::from( + "Error returning from provider", + ))); + } + }, + Err(_) => { + return Err(Error::Custom(String::from("Provider response timeout"))); + } } + } + } + } + + Err(Error::Custom(String::from( + "Unexpected schema configuration", + ))) + } - // Register response function - if let Some(method) = provider_set.response.clone() { - let response_method = - FireboltOpenRpcMethod::name_with_lowercase_module(&method.name).leak(); - - rpc_module - .register_async_method( - response_method, - move |params, context| async move { - let params_sequence = params.sequence(); - - if let Some(provider_response) = - ProviderRegistrar::get_provider_response( - attributes.response_payload_type.clone(), - params_sequence, - ) - { - ProviderBroker::provider_response( - &context.platform_state, - provider_response, - ) - .await; - } - - Ok(None) as RpcResult> - }, - ) - .unwrap(); + async fn callback_focus( + params: Params<'static>, + context: Arc, + ) -> Result, Error> { + info!("callback_focus: method={}", context.method); + + if let Some(capability) = &context.provider_relation_set.capability { + let mut params_sequence = params.sequence(); + let call_context: CallContext = params_sequence.next().unwrap(); + let request: FocusRequest = params_sequence.next().unwrap(); + + ProviderBroker::focus( + &context.platform_state, + call_context, + capability.clone(), + request, + ) + .await; + + Ok(None) as RpcResult> + } else { + Err(Error::Custom("Missing provides attribute".to_string())) + } + } + + async fn callback_response( + params: Params<'static>, + context: Arc, + ) -> Result, Error> { + info!("callback_response: method={}", context.method); + + let params_sequence = params.sequence(); + + let response_payload_type = match &context.provider_relation_set.attributes { + Some(attributes) => attributes.response_payload_type.clone(), + None => ProviderResponsePayloadType::Generic, + }; + + if let Some(provider_response) = + ProviderRegistrar::get_provider_response(response_payload_type, params_sequence) + { + ProviderBroker::provider_response(&context.platform_state, provider_response).await; + } else { + error!( + "callback_response: Could not resolve response payload type: context.method={}", + context.method + ); + return Err(Error::Custom(String::from( + "Couldn't resolve response payload type", + ))); + } + + Ok(None) + } + + pub fn register_methods(platform_state: &PlatformState, methods: &mut Methods) -> u32 { + let provider_relation_map = platform_state.open_rpc_state.get_provider_relation_map(); + let mut registered_methods = 0; + + for method_name in provider_relation_map.clone().keys() { + if let Some(provider_relation_set) = provider_relation_map.get(method_name) { + let mut registered = false; + + let method_name_lcm = + FireboltOpenRpcMethod::name_with_lowercase_module(method_name).leak(); + + let rpc_module_context = RpcModuleContext::new( + platform_state.clone(), + method_name_lcm.into(), + provider_relation_set.clone(), + ); + + let mut rpc_module = RpcModule::new(rpc_module_context.clone()); + + if provider_relation_set.event { + if provider_relation_set.provided_by.is_some() { + registered = Self::register_method( + method_name_lcm, + MethodType::AppEventListener, + &mut rpc_module, + ); + } else if provider_relation_set.capability.is_some() + || provider_relation_set.provides_to.is_some() + { + registered = Self::register_method( + method_name_lcm, + MethodType::Provider, + &mut rpc_module, + ); } + } else if provider_relation_set.provides_to.is_some() { + registered = Self::register_method( + method_name_lcm, + MethodType::AppEventEmitter, + &mut rpc_module, + ); + } else if provider_relation_set.error_for.is_some() { + registered = + Self::register_method(method_name_lcm, MethodType::Error, &mut rpc_module); + } else if provider_relation_set.provided_by.is_some() { + registered = Self::register_method( + method_name_lcm, + MethodType::ProviderInvoker, + &mut rpc_module, + ); + } - // Register error function - if let Some(method) = provider_set.error.clone() { - let error_method = - FireboltOpenRpcMethod::name_with_lowercase_module(&method.name).leak(); - rpc_module - .register_async_method( - error_method, - move |params, context| async move { - let params_sequence = params.sequence(); - - if let Some(provider_response) = - ProviderRegistrar::get_provider_response( - attributes.error_payload_type.clone(), - params_sequence, - ) - { - ProviderBroker::provider_response( - &context.platform_state, - provider_response, - ) - .await; - } - - Ok(None) as RpcResult> - }, - ) - .unwrap(); + if !registered { + if provider_relation_set.allow_focus_for.is_some() { + registered = Self::register_method( + method_name_lcm, + MethodType::Focus, + &mut rpc_module, + ); + } else if provider_relation_set.response_for.is_some() { + registered = Self::register_method( + method_name_lcm, + MethodType::Response, + &mut rpc_module, + ); } + } - //methods.merge(rpc_module.clone()).ok(); + if registered { methods .merge(register_aliases(platform_state, rpc_module)) .ok(); + + registered_methods += 1; } } } + + registered_methods + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use crate::{state::openrpc_state::OpenRpcState, utils::test_utils}; + + use super::*; + use jsonrpsee::core::server::rpc_module::Methods; + use ripple_sdk::tokio; + + #[tokio::test] + async fn test_register_methods() { + let mut methods = Methods::new(); + let mut runtime = test_utils::MockRuntime::new(); + runtime.platform_state.open_rpc_state = OpenRpcState::new(None); + + let mut provider_relation_map: HashMap = HashMap::new(); + provider_relation_map.insert("some.method".to_string(), ProviderRelationSet::new()); + + runtime + .platform_state + .open_rpc_state + .set_provider_relation_map(provider_relation_map); + + let registered_methods = + ProviderRegistrar::register_methods(&runtime.platform_state, &mut methods); + + assert!(registered_methods == 0); + } + + #[tokio::test] + async fn test_register_method_event_provided_by() { + let mut methods = Methods::new(); + let mut runtime = test_utils::MockRuntime::new(); + runtime.platform_state.open_rpc_state = OpenRpcState::new(None); + + let provider_relation_set = ProviderRelationSet { + event: true, + provided_by: Some("some.other_method".to_string()), + ..Default::default() + }; + + let mut provider_relation_map: HashMap = HashMap::new(); + provider_relation_map.insert("some.method".to_string(), provider_relation_set); + + runtime + .platform_state + .open_rpc_state + .set_provider_relation_map(provider_relation_map); + + let registered_methods = + ProviderRegistrar::register_methods(&runtime.platform_state, &mut methods); + + assert!(registered_methods == 1); + } + + #[tokio::test] + async fn test_register_method_event_provides() { + let mut methods = Methods::new(); + let mut runtime = test_utils::MockRuntime::new(); + runtime.platform_state.open_rpc_state = OpenRpcState::new(None); + + let provider_relation_set = ProviderRelationSet { + event: true, + capability: Some("some.capability".to_string()), + ..Default::default() + }; + + let mut provider_relation_map: HashMap = HashMap::new(); + provider_relation_map.insert("some.method".to_string(), provider_relation_set); + + runtime + .platform_state + .open_rpc_state + .set_provider_relation_map(provider_relation_map); + + let registered_methods = + ProviderRegistrar::register_methods(&runtime.platform_state, &mut methods); + + assert!(registered_methods == 1); + } + + #[tokio::test] + async fn test_register_method_event_provides_to() { + let mut methods = Methods::new(); + let mut runtime = test_utils::MockRuntime::new(); + runtime.platform_state.open_rpc_state = OpenRpcState::new(None); + + let provider_relation_set = ProviderRelationSet { + event: true, + provides_to: Some("some.other.method".to_string()), + ..Default::default() + }; + + let mut provider_relation_map: HashMap = HashMap::new(); + provider_relation_map.insert("some.method".to_string(), provider_relation_set); + + runtime + .platform_state + .open_rpc_state + .set_provider_relation_map(provider_relation_map); + + let registered_methods = + ProviderRegistrar::register_methods(&runtime.platform_state, &mut methods); + + assert!(registered_methods == 1); + } + + #[tokio::test] + async fn test_register_method_provides_to() { + let mut methods = Methods::new(); + let mut runtime = test_utils::MockRuntime::new(); + runtime.platform_state.open_rpc_state = OpenRpcState::new(None); + + let provider_relation_set = ProviderRelationSet { + event: true, + provides_to: Some("some.other.method".to_string()), + ..Default::default() + }; + + let mut provider_relation_map: HashMap = HashMap::new(); + provider_relation_map.insert("some.method".to_string(), provider_relation_set); + + runtime + .platform_state + .open_rpc_state + .set_provider_relation_map(provider_relation_map); + + let registered_methods = + ProviderRegistrar::register_methods(&runtime.platform_state, &mut methods); + + assert!(registered_methods == 1); + } + + #[tokio::test] + async fn test_register_method_error_for() { + let mut methods = Methods::new(); + let mut runtime = test_utils::MockRuntime::new(); + runtime.platform_state.open_rpc_state = OpenRpcState::new(None); + + let provider_relation_set = ProviderRelationSet { + error_for: Some("some.other.method".to_string()), + ..Default::default() + }; + + let mut provider_relation_map: HashMap = HashMap::new(); + provider_relation_map.insert("some.method".to_string(), provider_relation_set); + + runtime + .platform_state + .open_rpc_state + .set_provider_relation_map(provider_relation_map); + + let registered_methods = + ProviderRegistrar::register_methods(&runtime.platform_state, &mut methods); + + assert!(registered_methods == 1); + } + + #[tokio::test] + async fn test_register_method_provided_by() { + let mut methods = Methods::new(); + let mut runtime = test_utils::MockRuntime::new(); + runtime.platform_state.open_rpc_state = OpenRpcState::new(None); + + let provider_relation_set = ProviderRelationSet { + provided_by: Some("some.other.method".to_string()), + ..Default::default() + }; + + let mut provider_relation_map: HashMap = HashMap::new(); + provider_relation_map.insert("some.method".to_string(), provider_relation_set); + + runtime + .platform_state + .open_rpc_state + .set_provider_relation_map(provider_relation_map); + + let registered_methods = + ProviderRegistrar::register_methods(&runtime.platform_state, &mut methods); + + assert!(registered_methods == 1); + } + + #[tokio::test] + async fn test_register_method_allow_focus_for() { + let mut methods = Methods::new(); + let mut runtime = test_utils::MockRuntime::new(); + runtime.platform_state.open_rpc_state = OpenRpcState::new(None); + + let provider_relation_set = ProviderRelationSet { + allow_focus_for: Some("some.other.method".to_string()), + ..Default::default() + }; + + let mut provider_relation_map: HashMap = HashMap::new(); + provider_relation_map.insert("some.method".to_string(), provider_relation_set); + + runtime + .platform_state + .open_rpc_state + .set_provider_relation_map(provider_relation_map); + + let registered_methods = + ProviderRegistrar::register_methods(&runtime.platform_state, &mut methods); + + assert!(registered_methods == 1); + } + + #[tokio::test] + async fn test_register_method_response_for() { + let mut methods = Methods::new(); + let mut runtime = test_utils::MockRuntime::new(); + runtime.platform_state.open_rpc_state = OpenRpcState::new(None); + + let provider_relation_set = ProviderRelationSet { + response_for: Some("some.other.method".to_string()), + ..Default::default() + }; + + let mut provider_relation_map: HashMap = HashMap::new(); + provider_relation_map.insert("some.method".to_string(), provider_relation_set); + + runtime + .platform_state + .open_rpc_state + .set_provider_relation_map(provider_relation_map); + + let registered_methods = + ProviderRegistrar::register_methods(&runtime.platform_state, &mut methods); + + assert!(registered_methods == 1); + } + + #[tokio::test] + async fn test_register_method_duplicate() { + const METHOD_NAME: &str = "some.method"; + + let mut runtime = test_utils::MockRuntime::new(); + runtime.platform_state.open_rpc_state = OpenRpcState::new(None); + + let provider_relation_set = ProviderRelationSet { + response_for: Some("some.other.method".to_string()), + ..Default::default() + }; + + let rpc_module_context = RpcModuleContext::new( + runtime.platform_state.clone(), + String::from(METHOD_NAME), + provider_relation_set.clone(), + ); + + let mut rpc_module = RpcModule::new(rpc_module_context.clone()); + + let result = ProviderRegistrar::register_method( + METHOD_NAME, + MethodType::AppEventEmitter, + &mut rpc_module, + ); + + assert!(result); + + let result = ProviderRegistrar::register_method( + METHOD_NAME, + MethodType::ProviderInvoker, + &mut rpc_module, + ); + + assert!(!result); } } diff --git a/core/main/src/processor/pin_processor.rs b/core/main/src/processor/pin_processor.rs index c7eadafe4..2e5f4e84a 100644 --- a/core/main/src/processor/pin_processor.rs +++ b/core/main/src/processor/pin_processor.rs @@ -87,7 +87,7 @@ impl ExtnRequestProcessor for PinProcessor { let (session_tx, session_rx) = oneshot::channel::(); let pr_msg = ProviderBrokerRequest { capability: String::from(PIN_CHALLENGE_CAPABILITY), - method: String::from("challenge"), + method: String::from("pinchallenge.onRequestChallenge"), caller: pin_request.call_ctx.clone().into(), request: ProviderRequestPayload::PinChallenge(pin_request.into()), tx: session_tx, diff --git a/core/main/src/service/apps/provider_broker.rs b/core/main/src/service/apps/provider_broker.rs index 0ce5f0a57..b3728ff96 100644 --- a/core/main/src/service/apps/provider_broker.rs +++ b/core/main/src/service/apps/provider_broker.rs @@ -24,6 +24,7 @@ use ripple_sdk::{ fb_lifecycle_management::{ LifecycleManagementEventRequest, LifecycleManagementProviderEvent, }, + fb_openrpc::FireboltOpenRpcMethod, provider::{ FocusRequest, ProviderRequest, ProviderRequestPayload, ProviderResponse, ProviderResponsePayload, @@ -76,7 +77,7 @@ pub struct ProviderBroker {} #[derive(Clone, Debug)] struct ProviderMethod { - event_name: &'static str, + event_name: String, provider: CallContext, } @@ -121,7 +122,7 @@ impl ProviderBroker { pst: &PlatformState, capability: String, method: String, - event_name: &'static str, + event_name: String, provider: CallContext, listen_request: ListenRequest, ) { @@ -164,7 +165,7 @@ impl ProviderBroker { pst: &PlatformState, capability: String, method: String, - event_name: &'static str, + event_name: String, provider: CallContext, listen_request: ListenRequest, ) { @@ -173,12 +174,7 @@ impl ProviderBroker { capability, method, event_name ); let cap_method = format!("{}:{}", capability, method); - AppEvents::add_listener( - pst, - event_name.to_string(), - provider.clone(), - listen_request, - ); + AppEvents::add_listener(pst, event_name.clone(), provider.clone(), listen_request); { let mut provider_methods = pst.provider_broker_state.provider_methods.write().unwrap(); provider_methods.insert( @@ -212,11 +208,11 @@ impl ProviderBroker { for cap in all_caps { if let Some(provider) = provider_methods.get(&cap) { if let Some(list) = result.get_mut(&provider.provider.app_id) { - list.push(String::from(provider.event_name)); + list.push(provider.event_name.clone()); } else { result.insert( provider.provider.app_id.clone(), - vec![String::from(provider.event_name)], + vec![provider.event_name.clone()], ); } } @@ -225,7 +221,11 @@ impl ProviderBroker { } pub async fn invoke_method(pst: &PlatformState, request: ProviderBrokerRequest) { - let cap_method = format!("{}:{}", request.capability, request.method); + let cap_method = format!( + "{}:{}", + request.capability, + FireboltOpenRpcMethod::name_with_lowercase_module(&request.method) + ); debug!("invoking provider for {}", cap_method); let provider_opt = { @@ -233,7 +233,7 @@ impl ProviderBroker { provider_methods.get(&cap_method).cloned() }; if let Some(provider) = provider_opt { - let event_name = provider.event_name; + let event_name = provider.event_name.clone(); let req_params = request.request.clone(); let app_id_opt = request.app_id.clone(); let c_id = ProviderBroker::start_provider_session(pst, request, provider); @@ -242,7 +242,7 @@ impl ProviderBroker { AppEvents::emit_to_app( pst, app_id, - event_name, + &event_name, &serde_json::to_value(ProviderRequest { correlation_id: c_id, parameters: req_params, @@ -254,7 +254,7 @@ impl ProviderBroker { debug!("Broadcasting request to all the apps!!"); AppEvents::emit( pst, - event_name, + &event_name, &serde_json::to_value(ProviderRequest { correlation_id: c_id, parameters: req_params, diff --git a/core/main/src/service/user_grants.rs b/core/main/src/service/user_grants.rs index 702b67c76..ed0f9f7f3 100644 --- a/core/main/src/service/user_grants.rs +++ b/core/main/src/service/user_grants.rs @@ -1813,7 +1813,6 @@ impl GrantStepExecutor { pub async fn invoke_capability( platform_state: &PlatformState, - // call_ctx: &CallContext, caller_session: &CallerSession, app_requested_for: &AppIdentification, cap: &FireboltCap, @@ -1822,11 +1821,34 @@ impl GrantStepExecutor { ) -> Result<(), DenyReasonWithCap> { let (session_tx, session_rx) = oneshot::channel::(); let p_cap = cap.clone(); - /* - * We have a concrete struct defined for ack challenge and pin challenge hence handling them separately. If any new - * caps are introduced in future, the assumption is that capability provider has a method "challenge" and it can - * deduce its params from a string. - */ + + let mut method_key: Option = None; + + for (key, provider_relation_set) in platform_state + .open_rpc_state + .get_provider_relation_map() + .iter() + { + if let Some(provides) = &provider_relation_set.provides { + if provides.eq(&cap.as_str()) { + method_key = Some(key.clone()); + break; + } + } + } + + if method_key.is_none() { + error!( + "invoke_capability: Could not find provider for capability {}", + p_cap.as_str() + ); + return Err(DenyReasonWithCap { + reason: DenyReason::Ungranted, + caps: vec![permission.cap.clone()], + }); + } + + let method = method_key.unwrap(); /* * this might be weird looking as_str().as_str(), FireboltCap returns String but has a function named as_str. @@ -1845,7 +1867,7 @@ impl GrantStepExecutor { }; Some(ProviderBrokerRequest { capability: p_cap.as_str(), - method: String::from("challenge"), + method, caller: caller_session.to_owned(), request: ProviderRequestPayload::AckChallenge(challenge), tx: session_tx, @@ -1870,7 +1892,7 @@ impl GrantStepExecutor { }; Some(ProviderBrokerRequest { capability: p_cap.as_str(), - method: "challenge".to_owned(), + method, caller: caller_session.clone(), request: ProviderRequestPayload::PinChallenge(challenge), tx: session_tx, @@ -1883,15 +1905,11 @@ impl GrantStepExecutor { * This is for any other capability, hoping it to deduce its necessary params from a json string * and has a challenge method. */ - let param_str = match param { - None => "".to_owned(), - Some(val) => val.to_string(), - }; Some(ProviderBrokerRequest { capability: p_cap.as_str(), - method: String::from("challenge"), + method, caller: caller_session.clone(), - request: ProviderRequestPayload::Generic(param_str), + request: ProviderRequestPayload::Generic(param.clone().unwrap_or(Value::Null)), tx: session_tx, app_id: None, }) @@ -2001,8 +2019,8 @@ mod tests { ProviderBroker::register_or_unregister_provider( &state_c, String::from(PIN_CHALLENGE_CAPABILITY), - String::from("challenge"), - PIN_CHALLENGE_EVENT, + String::from(PIN_CHALLENGE_EVENT), + String::from(PIN_CHALLENGE_EVENT), ctx_c.clone(), ListenRequest { listen: true }, ) @@ -2011,8 +2029,8 @@ mod tests { ProviderBroker::register_or_unregister_provider( &state_c, String::from(ACK_CHALLENGE_CAPABILITY), - String::from("challenge"), - ACK_CHALLENGE_EVENT, + String::from(ACK_CHALLENGE_EVENT), + String::from(ACK_CHALLENGE_EVENT), ctx_c.clone(), ListenRequest { listen: true }, ) diff --git a/core/main/src/state/firebolt-open-rpc.json b/core/main/src/state/firebolt-open-rpc.json index 6105e0309..7743a2c51 100644 --- a/core/main/src/state/firebolt-open-rpc.json +++ b/core/main/src/state/firebolt-open-rpc.json @@ -696,6 +696,21 @@ "negotiable": true } }, + "xrn:firebolt:capability:discovery:interest": { + "level": "must", + "use": { + "public": true, + "negotiable": true + }, + "manage": { + "public": false, + "negotiable": false + }, + "provide": { + "public": true, + "negotiable": true + } + }, "xrn:firebolt:capability:discovery:navigate-to": { "level": "must", "use": { @@ -912,7 +927,7 @@ "openrpc": "1.2.4", "info": { "title": "Firebolt JSON-RPC API", - "version": "1.1.0", + "version": "1.2.0", "x-module-descriptions": { "Internal": "Internal methods for SDK / FEE integration", "Accessibility": "The `Accessibility` module provides access to the user/device settings for closed captioning and voice guidance.\n\nApps **SHOULD** attempt o respect these settings, rather than manage and persist seprate settings, which would be different per-app.", @@ -1273,7 +1288,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "closedCaptionsSettings" + "x-subscriber-for": "Accessibility.closedCaptionsSettings" }, { "name": "event", @@ -1351,7 +1366,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "voiceGuidanceSettings" + "x-subscriber-for": "Accessibility.voiceGuidanceSettings" }, { "name": "event", @@ -1412,7 +1427,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "audioDescriptionSettings" + "x-subscriber-for": "Accessibility.audioDescriptionSettings" }, { "name": "event", @@ -2212,7 +2227,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "skipRestriction" + "x-subscriber-for": "Advertising.skipRestriction" }, { "name": "event", @@ -2291,7 +2306,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "policy" + "x-subscriber-for": "Advertising.policy" }, { "name": "event", @@ -2442,7 +2457,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "enabled" + "x-subscriber-for": "AudioDescriptions.enabled" }, { "name": "event", @@ -4404,7 +4419,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "enabled" + "x-subscriber-for": "ClosedCaptions.enabled" }, { "name": "event", @@ -4474,7 +4489,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "fontFamily" + "x-subscriber-for": "ClosedCaptions.fontFamily" }, { "name": "event", @@ -4557,7 +4572,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "fontSize" + "x-subscriber-for": "ClosedCaptions.fontSize" }, { "name": "event", @@ -4640,7 +4655,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "fontColor" + "x-subscriber-for": "ClosedCaptions.fontColor" }, { "name": "event", @@ -4723,7 +4738,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "fontEdge" + "x-subscriber-for": "ClosedCaptions.fontEdge" }, { "name": "event", @@ -4806,7 +4821,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "fontEdgeColor" + "x-subscriber-for": "ClosedCaptions.fontEdgeColor" }, { "name": "event", @@ -4889,7 +4904,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "fontOpacity" + "x-subscriber-for": "ClosedCaptions.fontOpacity" }, { "name": "event", @@ -4972,7 +4987,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "backgroundColor" + "x-subscriber-for": "ClosedCaptions.backgroundColor" }, { "name": "event", @@ -5055,7 +5070,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "backgroundOpacity" + "x-subscriber-for": "ClosedCaptions.backgroundOpacity" }, { "name": "event", @@ -5138,7 +5153,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "textAlign" + "x-subscriber-for": "ClosedCaptions.textAlign" }, { "name": "event", @@ -5221,7 +5236,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "textAlignVertical" + "x-subscriber-for": "ClosedCaptions.textAlignVertical" }, { "name": "event", @@ -5304,7 +5319,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "windowColor" + "x-subscriber-for": "ClosedCaptions.windowColor" }, { "name": "event", @@ -5387,7 +5402,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "windowOpacity" + "x-subscriber-for": "ClosedCaptions.windowOpacity" }, { "name": "event", @@ -5480,7 +5495,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "preferredLanguages" + "x-subscriber-for": "ClosedCaptions.preferredLanguages" }, { "name": "event", @@ -6537,6 +6552,167 @@ } ] }, + { + "name": "Content.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": "#/x-schemas/Discovery/InterestType" + } + }, + { + "name": "reason", + "required": true, + "schema": { + "$ref": "#/x-schemas/Discovery/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": "Content.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": [ + { + "name": "listen", + "required": true, + "schema": { + "type": "boolean" + } + } + ], + "result": { + "name": "interest", + "schema": { + "anyOf": [ + { + "$ref": "#/x-schemas/Types/ListenResponse" + }, + { + "$ref": "#/components/schemas/InterestEvent" + } + ] + }, + "summary": "The EntityDetails data." + }, + "examples": [ + { + "name": "Default Example", + "params": [ + { + "name": "listen", + "value": true + } + ], + "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" + } + ] + } + } + } + } + } + ] + }, { "name": "Device.id", "summary": "Get the platform back-office device identifier", @@ -6847,7 +7023,6 @@ } }, "required": [ - "sdk", "api", "firmware", "os" @@ -7322,7 +7497,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "name" + "x-subscriber-for": "Device.name" }, { "name": "event", @@ -7393,7 +7568,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "hdcp" + "x-subscriber-for": "Device.hdcp" }, { "name": "event", @@ -7454,7 +7629,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "hdr" + "x-subscriber-for": "Device.hdr" }, { "name": "event", @@ -7517,7 +7692,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "audio" + "x-subscriber-for": "Device.audio" }, { "name": "event", @@ -7580,7 +7755,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "screenResolution" + "x-subscriber-for": "Device.screenResolution" }, { "name": "event", @@ -7641,7 +7816,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "videoResolution" + "x-subscriber-for": "Device.videoResolution" }, { "name": "event", @@ -7702,7 +7877,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "network" + "x-subscriber-for": "Device.network" }, { "name": "event", @@ -7866,6 +8041,9 @@ { "name": "capabilities", "x-provides": "xrn:firebolt:capability:discovery:entity-info" + }, + { + "name": "deprecated" } ], "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.", @@ -8223,7 +8401,7 @@ } ], "result": { - "name": "success", + "name": "result", "value": true } } @@ -8238,6 +8416,9 @@ { "name": "capabilities", "x-provides": "xrn:firebolt:capability:discovery:purchased-content" + }, + { + "name": "deprecated" } ], "summary": "Provide a list of purchased content for the authenticated account, such as rentals and electronic sell through purchases.", @@ -8450,7 +8631,7 @@ "name": "identifiers", "summary": "A set of content identifiers for this call to action", "schema": { - "$ref": "#/x-schemas/Entertainment/ContentIdentifiers" + "$ref": "#/x-schemas/Entity/Entity" }, "required": true }, @@ -9343,118 +9524,315 @@ ] }, { - "name": "Discovery.onPolicyChanged", - "summary": "get the discovery policy", + "name": "Discovery.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": "listen", + "name": "type", "required": true, "schema": { - "type": "boolean" + "$ref": "#/x-schemas/Discovery/InterestType" } - } - ], - "tags": [ - { - "name": "subscriber", - "x-subscriber-for": "policy" }, { - "name": "event", - "x-alternative": "policy" + "name": "reason", + "required": true, + "schema": { + "$ref": "#/x-schemas/Discovery/InterestReason" + } }, { - "name": "capabilities", - "x-uses": [ - "xrn:firebolt:capability:discovery:policy" - ] + "name": "entity", + "required": true, + "schema": { + "$ref": "#/x-schemas/Entity/EntityDetails" + } } ], "result": { - "name": "policy", - "summary": "discovery policy opt-in/outs", + "name": "result", "schema": { - "anyOf": [ - { - "$ref": "#/x-schemas/Types/ListenResponse" - }, - { - "$ref": "#/components/schemas/DiscoveryPolicy" - } - ] + "type": "null" } }, "examples": [ { - "name": "Getting the discovery policy", + "name": "Default Example", "params": [ { - "name": "listen", - "value": true + "name": "type", + "value": "interest" + }, + { + "name": "reason", + "value": "playlist" + }, + { + "name": "entity", + "value": { + "identifiers": { + "entityId": "345", + "entityType": "program", + "programType": "movie" + }, + "info": {} + } } ], "result": { - "name": "Default Result", - "value": { - "enableRecommendations": true, - "shareWatchHistory": true, - "rememberWatchedPrograms": true - } + "name": "result", + "value": null } } ] }, { - "name": "Discovery.onPullEntityInfo", + "name": "Discovery.onRequestUserInterest", "tags": [ { - "name": "polymorphic-pull-event" + "name": "rpc-only" }, { "name": "event", - "x-pulls-for": "entityInfo" - }, - { - "name": "capabilities", - "x-provides": "xrn:firebolt:capability:discovery:entity-info" - } - ], - "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.", - "description": "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. Includes information about the program entity and its relevant associated entities, such as extras, previews, and, in the case of TV series, seasons and episodes.\n\nSee the `EntityInfo` and `WayToWatch` data structures below for more information.\n\nThe app only needs to implement Pull support for `entityInfo` at this time.", - "params": [ - { - "name": "listen", - "required": true, - "schema": { - "type": "boolean" - } - } - ], - "result": { - "name": "request", - "summary": "A EntityInfoFederatedRequest object.", - "schema": { - "anyOf": [ - { - "$ref": "#/x-schemas/Types/ListenResponse" - }, - { - "$ref": "#/components/schemas/EntityInfoFederatedRequest" - } - ] - } - }, - "examples": [ - { - "name": "Send entity info for a movie to the platform.", - "params": [ - { - "name": "listen", - "value": true - } - ], - "result": { - "name": "result", + "x-response-name": "entity", + "x-response": { + "$ref": "#/x-schemas/Entity/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" + } + ] + } + } + ] + }, + "x-error": { + "type": "object", + "additionalProperties": false, + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "title": "errorObjectCode", + "description": "A Number that indicates the error type that occurred. This MUST be an integer. The error codes from and including -32768 to -32000 are reserved for pre-defined errors. These pre-defined errors SHOULD be assumed to be returned from any JSON-RPC api.", + "type": "integer" + }, + "message": { + "title": "errorObjectMessage", + "description": "A String providing a short description of the error. The message SHOULD be limited to a concise single sentence.", + "type": "string" + }, + "data": { + "title": "errorObjectData", + "description": "A Primitive or Structured value that contains additional information about the error. This may be omitted. The value of this member is defined by the Server (e.g. detailed error information, nested errors etc.)." + } + } + } + }, + { + "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": [ + { + "name": "listen", + "required": true, + "schema": { + "type": "boolean" + } + } + ], + "result": { + "name": "request", + "schema": { + "anyOf": [ + { + "$ref": "#/x-schemas/Types/ListenResponse" + }, + { + "type": "object", + "required": [ + "correlationId", + "parameters" + ], + "properties": { + "correlationId": { + "type": "string" + }, + "parameters": { + "$ref": "#/components/schemas/UserInterestProviderParameters" + } + }, + "additionalProperties": false + } + ] + } + }, + "examples": [ + { + "name": "Default Example", + "params": [ + { + "name": "listen", + "value": true + } + ], + "result": { + "name": "request", + "value": { + "correlationId": "xyz", + "parameters": { + "type": "interest", + "reason": "playlist" + } + } + } + } + ] + }, + { + "name": "Discovery.onPolicyChanged", + "summary": "get the discovery policy", + "params": [ + { + "name": "listen", + "required": true, + "schema": { + "type": "boolean" + } + } + ], + "tags": [ + { + "name": "subscriber", + "x-subscriber-for": "Discovery.policy" + }, + { + "name": "event", + "x-alternative": "policy" + }, + { + "name": "capabilities", + "x-uses": [ + "xrn:firebolt:capability:discovery:policy" + ] + } + ], + "result": { + "name": "policy", + "summary": "discovery policy opt-in/outs", + "schema": { + "anyOf": [ + { + "$ref": "#/x-schemas/Types/ListenResponse" + }, + { + "$ref": "#/components/schemas/DiscoveryPolicy" + } + ] + } + }, + "examples": [ + { + "name": "Getting the discovery policy", + "params": [ + { + "name": "listen", + "value": true + } + ], + "result": { + "name": "Default Result", + "value": { + "enableRecommendations": true, + "shareWatchHistory": true, + "rememberWatchedPrograms": true + } + } + } + ] + }, + { + "name": "Discovery.onPullEntityInfo", + "tags": [ + { + "name": "polymorphic-pull-event" + }, + { + "name": "event", + "x-pulls-for": "entityInfo" + }, + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capability:discovery:entity-info" + }, + { + "name": "deprecated" + } + ], + "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.", + "description": "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. Includes information about the program entity and its relevant associated entities, such as extras, previews, and, in the case of TV series, seasons and episodes.\n\nSee the `EntityInfo` and `WayToWatch` data structures below for more information.\n\nThe app only needs to implement Pull support for `entityInfo` at this time.", + "params": [ + { + "name": "listen", + "required": true, + "schema": { + "type": "boolean" + } + } + ], + "result": { + "name": "request", + "summary": "A EntityInfoFederatedRequest object.", + "schema": { + "anyOf": [ + { + "$ref": "#/x-schemas/Types/ListenResponse" + }, + { + "$ref": "#/components/schemas/EntityInfoFederatedRequest" + } + ] + } + }, + "examples": [ + { + "name": "Send entity info for a movie to the platform.", + "params": [ + { + "name": "listen", + "value": true + } + ], + "result": { + "name": "result", "value": { "correlationId": "xyz", "parameters": { @@ -9514,6 +9892,9 @@ { "name": "capabilities", "x-provides": "xrn:firebolt:capability:discovery:purchased-content" + }, + { + "name": "deprecated" } ], "summary": "Provide a list of purchased content for the authenticated account, such as rentals and electronic sell through purchases.", @@ -9563,41 +9944,222 @@ "description": "Return content purchased by the user, such as rentals and electronic sell through purchases.\n\nThe app should return the user's 100 most recent purchases in `entries`. The total count of purchases must be provided in `count`. If `count` is greater than the total number of `entries`, the UI may provide a link into the app to see the complete purchase list.\n\nThe `EntityInfo` object returned is not required to have `waysToWatch` populated, but it is recommended that it do so in case the UI wants to surface additional information on the purchases screen.\n\nThe app should implement both Push and Pull methods for `purchasedContent`.\n\nThe app should actively push `purchasedContent` when:\n\n* The app becomes Active.\n* When the state of the purchasedContent set has changed.\n* The app goes into Inactive or Background state, if there is a chance a change event has been missed." }, { - "name": "HDMIInput.ports", + "name": "Discovery.userInterestResponse", "tags": [ + { + "name": "rpc-only" + }, { "name": "capabilities", - "x-uses": [ - "xrn:firebolt:capability:inputs:hdmi" - ] + "x-provides": "xrn:firebolt:capability:discovery:interest", + "x-response-for": "Discovery.onRequestUserInterest" } ], - "summary": "Retrieve a list of HDMI input ports.", - "params": [], - "result": { - "name": "ports", - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/HDMIInputPort" - } - } - }, - "examples": [ + "summary": "Internal API for .onRequestUserInterest Provider to send back response.", + "description": "Provide information about the entity currently displayed or selected on the screen.", + "params": [ { - "name": "Default Example", - "params": [], - "result": { - "name": "ports", - "value": [ + "name": "correlationId", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "result", + "schema": { + "$ref": "#/x-schemas/Entity/EntityDetails", + "examples": [ { - "port": "HDMI1", - "connected": true, - "signal": "stable", - "arcCapable": true, - "arcConnected": true, - "edidVersion": "2.0", - "autoLowLatencyModeCapable": true, + "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" + } + ] + } + } + ] + }, + "required": true + } + ], + "result": { + "name": "result", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Example", + "params": [ + { + "name": "correlationId", + "value": "123" + }, + { + "name": "result", + "value": { + "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" + } + ] + } + } + } + ], + "result": { + "name": "result", + "value": null + } + } + ] + }, + { + "name": "Discovery.userInterestError", + "tags": [ + { + "name": "rpc-only" + }, + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capability:discovery:interest", + "x-error-for": "Discovery.onRequestUserInterest" + } + ], + "summary": "Internal API for .onRequestUserInterest Provider to send back error.", + "description": "Provide information about the entity currently displayed or selected on the screen.", + "params": [ + { + "name": "correlationId", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "error", + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "title": "errorObjectCode", + "description": "A Number that indicates the error type that occurred. This MUST be an integer. The error codes from and including -32768 to -32000 are reserved for pre-defined errors. These pre-defined errors SHOULD be assumed to be returned from any JSON-RPC api.", + "type": "integer" + }, + "message": { + "title": "errorObjectMessage", + "description": "A String providing a short description of the error. The message SHOULD be limited to a concise single sentence.", + "type": "string" + }, + "data": { + "title": "errorObjectData", + "description": "A Primitive or Structured value that contains additional information about the error. This may be omitted. The value of this member is defined by the Server (e.g. detailed error information, nested errors etc.)." + } + } + }, + "required": true + } + ], + "result": { + "name": "result", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Example 1", + "params": [ + { + "name": "correlationId", + "value": "123" + }, + { + "name": "error", + "value": { + "code": 1, + "message": "Error" + } + } + ], + "result": { + "name": "result", + "value": null + } + } + ] + }, + { + "name": "HDMIInput.ports", + "tags": [ + { + "name": "capabilities", + "x-uses": [ + "xrn:firebolt:capability:inputs:hdmi" + ] + } + ], + "summary": "Retrieve a list of HDMI input ports.", + "params": [], + "result": { + "name": "ports", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HDMIInputPort" + } + } + }, + "examples": [ + { + "name": "Default Example", + "params": [], + "result": { + "name": "ports", + "value": [ + { + "port": "HDMI1", + "connected": true, + "signal": "stable", + "arcCapable": true, + "arcConnected": true, + "edidVersion": "2.0", + "autoLowLatencyModeCapable": true, "autoLowLatencyModeSignalled": true } ] @@ -10055,7 +10617,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "lowLatencyMode" + "x-subscriber-for": "HDMIInput.lowLatencyMode" }, { "name": "event", @@ -10125,7 +10687,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "autoLowLatencyModeCapable" + "x-subscriber-for": "HDMIInput.autoLowLatencyModeCapable" }, { "name": "event", @@ -10201,7 +10763,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "edidVersion" + "x-subscriber-for": "HDMIInput.edidVersion" }, { "name": "event", @@ -10492,9 +11054,11 @@ "tags": [ { "name": "capabilities", + "x-provided-by": "Keyboard.onRequestEmail", "x-uses": [ "xrn:firebolt:capability:input:keyboard" - ] + ], + "x-allow-focus": true } ], "summary": "Prompt the user for their email address with a simplified list of choices.", @@ -10565,9 +11129,11 @@ "tags": [ { "name": "capabilities", + "x-provided-by": "Keyboard.onRequestPassword", "x-uses": [ "xrn:firebolt:capability:input:keyboard" - ] + ], + "x-allow-focus": true } ], "summary": "Show the password entry keyboard, with typing obfuscated from visibility", @@ -10609,9 +11175,11 @@ "tags": [ { "name": "capabilities", + "x-provided-by": "Keyboard.onRequestStandard", "x-uses": [ "xrn:firebolt:capability:input:keyboard" - ] + ], + "x-allow-focus": true } ], "summary": "Show the standard platform keyboard, and return the submitted value", @@ -10667,11 +11235,9 @@ { "name": "event", "x-response": { - "$ref": "#/components/schemas/KeyboardResult", + "type": "string", "examples": [ - { - "text": "username" - } + "username" ] }, "x-error": { @@ -10759,11 +11325,9 @@ { "name": "event", "x-response": { - "$ref": "#/components/schemas/KeyboardResult", + "type": "string", "examples": [ - { - "text": "password" - } + "password" ] }, "x-error": { @@ -10851,11 +11415,9 @@ { "name": "event", "x-response": { - "$ref": "#/components/schemas/KeyboardResult", + "type": "string", "examples": [ - { - "text": "email@address.com" - } + "email@address.com" ] }, "x-error": { @@ -11034,11 +11596,9 @@ { "name": "result", "schema": { - "$ref": "#/components/schemas/KeyboardResult", + "type": "string", "examples": [ - { - "text": "username" - } + "username" ] }, "required": true @@ -11071,9 +11631,7 @@ }, { "name": "result", - "value": { - "text": "username" - } + "value": "username" } ], "result": { @@ -11177,11 +11735,9 @@ { "name": "result", "schema": { - "$ref": "#/components/schemas/KeyboardResult", + "type": "string", "examples": [ - { - "text": "password" - } + "password" ] }, "required": true @@ -11214,9 +11770,7 @@ }, { "name": "result", - "value": { - "text": "password" - } + "value": "password" } ], "result": { @@ -11320,11 +11874,9 @@ { "name": "result", "schema": { - "$ref": "#/components/schemas/KeyboardResult", + "type": "string", "examples": [ - { - "text": "email@address.com" - } + "email@address.com" ] }, "required": true @@ -11357,9 +11909,7 @@ }, { "name": "result", - "value": { - "text": "email@address.com" - } + "value": "email@address.com" } ], "result": { @@ -12371,7 +12921,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "locality" + "x-subscriber-for": "Localization.locality" }, { "name": "event", @@ -12442,7 +12992,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "postalCode" + "x-subscriber-for": "Localization.postalCode" }, { "name": "event", @@ -12513,7 +13063,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "countryCode" + "x-subscriber-for": "Localization.countryCode" }, { "name": "event", @@ -12594,7 +13144,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "language" + "x-subscriber-for": "Localization.language" }, { "name": "event", @@ -12670,7 +13220,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "preferredAudioLanguages" + "x-subscriber-for": "Localization.preferredAudioLanguages" }, { "name": "event", @@ -12740,7 +13290,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "locale" + "x-subscriber-for": "Localization.locale" }, { "name": "event", @@ -12811,7 +13361,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "timeZone" + "x-subscriber-for": "Localization.timeZone" }, { "name": "event", @@ -15279,7 +15829,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "allowResumePoints" + "x-subscriber-for": "Privacy.allowResumePoints" }, { "name": "event", @@ -15349,7 +15899,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "allowUnentitledResumePoints" + "x-subscriber-for": "Privacy.allowUnentitledResumePoints" }, { "name": "event", @@ -15419,7 +15969,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "allowWatchHistory" + "x-subscriber-for": "Privacy.allowWatchHistory" }, { "name": "event", @@ -15489,7 +16039,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "allowProductAnalytics" + "x-subscriber-for": "Privacy.allowProductAnalytics" }, { "name": "event", @@ -15559,7 +16109,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "allowPersonalization" + "x-subscriber-for": "Privacy.allowPersonalization" }, { "name": "event", @@ -15629,7 +16179,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "allowUnentitledPersonalization" + "x-subscriber-for": "Privacy.allowUnentitledPersonalization" }, { "name": "event", @@ -15699,7 +16249,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "allowRemoteDiagnostics" + "x-subscriber-for": "Privacy.allowRemoteDiagnostics" }, { "name": "event", @@ -15769,7 +16319,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "allowPrimaryContentAdTargeting" + "x-subscriber-for": "Privacy.allowPrimaryContentAdTargeting" }, { "name": "event", @@ -15839,7 +16389,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "allowPrimaryBrowseAdTargeting" + "x-subscriber-for": "Privacy.allowPrimaryBrowseAdTargeting" }, { "name": "event", @@ -15909,7 +16459,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "allowAppContentAdTargeting" + "x-subscriber-for": "Privacy.allowAppContentAdTargeting" }, { "name": "event", @@ -15979,7 +16529,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "allowACRCollection" + "x-subscriber-for": "Privacy.allowACRCollection" }, { "name": "event", @@ -16049,7 +16599,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "allowCameraAnalytics" + "x-subscriber-for": "Privacy.allowCameraAnalytics" }, { "name": "event", @@ -17145,7 +17695,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "friendlyName" + "x-subscriber-for": "SecondScreen.friendlyName" }, { "name": "event", @@ -18294,7 +18844,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "enabled" + "x-subscriber-for": "VoiceGuidance.enabled" }, { "name": "event", @@ -18364,7 +18914,7 @@ "tags": [ { "name": "subscriber", - "x-subscriber-for": "speed" + "x-subscriber-for": "VoiceGuidance.speed" }, { "name": "event", @@ -18999,6 +19549,46 @@ } ] }, + "InterestResult": { + "title": "InterestResult", + "type": "object", + "properties": { + "appId": { + "type": "string" + }, + "entity": { + "$ref": "#/x-schemas/Entity/EntityDetails" + } + }, + "required": [ + "appId", + "entity" + ] + }, + "InterestEvent": { + "title": "InterestEvent", + "type": "object", + "properties": { + "appId": { + "type": "string" + }, + "type": { + "$ref": "#/x-schemas/Discovery/InterestType" + }, + "reason": { + "$ref": "#/x-schemas/Discovery/InterestReason" + }, + "entity": { + "$ref": "#/x-schemas/Entity/EntityDetails" + } + }, + "required": [ + "appId", + "entity", + "type", + "reason" + ] + }, "Resolution": { "type": "array", "items": [ @@ -19361,6 +19951,22 @@ "xrn:firebolt:channel:any" ] }, + "UserInterestProviderParameters": { + "title": "UserInterestProviderParameters", + "type": "object", + "required": [ + "type", + "reason" + ], + "properties": { + "type": { + "$ref": "#/x-schemas/Discovery/InterestType" + }, + "reason": { + "$ref": "#/x-schemas/Discovery/InterestReason" + } + } + }, "HDMIPortId": { "type": "string", "pattern": "^HDMI[0-9]+$" @@ -19557,30 +20163,13 @@ } } }, - "KeyboardResult": { - "title": "KeyboardResult", + "LifecycleEvent": { + "title": "LifecycleEvent", + "description": "A an object describing the previous and current states", "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" - } - } - }, - "LifecycleEvent": { - "title": "LifecycleEvent", - "description": "A an object describing the previous and current states", - "type": "object", - "required": [ - "state", - "previous" + "state", + "previous" ], "properties": { "state": { @@ -20190,13 +20779,6 @@ } } }, - "BooleanMap": { - "title": "BooleanMap", - "type": "object", - "additionalProperties": { - "type": "boolean" - } - }, "AudioProfile": { "title": "AudioProfile", "type": "string", @@ -20209,6 +20791,13 @@ "dolbyAtmos" ] }, + "BooleanMap": { + "title": "BooleanMap", + "type": "object", + "additionalProperties": { + "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.", @@ -20616,6 +21205,23 @@ }, "Discovery": { "uri": "https://meta.comcast.com/firebolt/discovery", + "InterestType": { + "title": "InterestType", + "type": "string", + "enum": [ + "interest", + "disinterest" + ] + }, + "InterestReason": { + "title": "InterestReason", + "type": "string", + "enum": [ + "playlist", + "reaction", + "recording" + ] + }, "EntityInfoResult": { "title": "EntityInfoResult", "description": "The result for an `entityInfo()` push or pull.", @@ -20668,88 +21274,57 @@ "additionalProperties": false } }, - "Entertainment": { - "uri": "https://meta.comcast.com/firebolt/entertainment", - "ContentIdentifiers": { - "title": "ContentIdentifiers", + "Entity": { + "uri": "https://meta.comcast.com/firebolt/entity", + "EntityDetails": { + "title": "EntityDetails", "type": "object", + "required": [ + "identifiers" + ], "properties": { - "assetId": { - "type": "string", - "description": "Identifies a particular playable asset. For example, the HD version of a particular movie separate from the UHD version." - }, - "entityId": { - "type": "string", - "description": "Identifies an entity, such as a Movie, TV Series or TV Episode." - }, - "seasonId": { - "type": "string", - "description": "The TV Season for a TV Episode." + "identifiers": { + "$ref": "#/x-schemas/Entity/Entity" }, - "seriesId": { - "type": "string", - "description": "The TV Series for a TV Episode or TV Season." + "info": { + "$ref": "#/x-schemas/Entity/Metadata" }, - "appContentData": { - "type": "string", - "description": "App-specific content identifiers.", - "maxLength": 1024 + "waysToWatch": { + "type": "array", + "items": { + "$ref": "#/x-schemas/Entertainment/WayToWatch" + }, + "description": "An array of ways a user is might watch this entity, regardless of entitlements." } - }, - "description": "The ContentIdentifiers object is how the app identifies an entity or asset to\nthe Firebolt platform. These ids are used to look up metadata and deep link into\nthe app.\n\nApps do not need to provide all ids. They only need to provide the minimum\nrequired to target a playable stream or an entity detail screen via a deep link.\nIf an id isn't needed to get to those pages, it doesn't need to be included." + } }, - "Entitlement": { - "title": "Entitlement", - "type": "object", - "properties": { - "entitlementId": { - "type": "string" + "Entity": { + "oneOf": [ + { + "$ref": "#/x-schemas/Entity/ProgramEntity" }, - "startTime": { - "type": "string", - "format": "date-time" + { + "$ref": "#/x-schemas/Entity/MusicEntity" }, - "endTime": { - "type": "string", - "format": "date-time" + { + "$ref": "#/x-schemas/Entity/ChannelEntity" + }, + { + "$ref": "#/x-schemas/Entity/UntypedEntity" + }, + { + "$ref": "#/x-schemas/Entity/PlaylistEntity" } - }, - "required": [ - "entitlementId" ] }, - "EntityInfo": { - "title": "EntityInfo", - "description": "An EntityInfo object represents an \"entity\" on the platform. Currently, only entities of type `program` are supported. `programType` must be supplied to identify the program type.\n\nAdditionally, EntityInfo objects must specify a properly formed\nContentIdentifiers object, `entityType`, and `title`. The app should provide\nthe `synopsis` property for a good user experience if the content\nmetadata is not available another way.\n\nThe ContentIdentifiers must be sufficient for navigating the user to the\nappropriate entity or detail screen via a `detail` intent or deep link.\n\nEntityInfo objects must provide at least one WayToWatch object when returned as\npart of an `entityInfo` method and a streamable asset is available to the user.\nIt is optional for the `purchasedContent` method, but recommended because the UI\nmay use those data.", + "Metadata": { + "title": "Metadata", "type": "object", - "required": [ - "identifiers", - "entityType", - "programType", - "title" - ], "properties": { - "identifiers": { - "$ref": "#/x-schemas/Entertainment/ContentIdentifiers" - }, "title": { "type": "string", "description": "Title of the entity." }, - "entityType": { - "type": "string", - "enum": [ - "program", - "music" - ], - "description": "The type of the entity, e.g. `program` or `music`." - }, - "programType": { - "$ref": "#/x-schemas/Entertainment/ProgramType" - }, - "musicType": { - "$ref": "#/x-schemas/Entertainment/MusicType" - }, "synopsis": { "type": "string", "description": "Short description of the entity." @@ -20781,116 +21356,368 @@ "$ref": "#/x-schemas/Entertainment/ContentRating" }, "description": "A list of ContentRating objects, describing the entity's ratings in various rating schemes." - }, - "waysToWatch": { - "type": "array", - "items": { - "$ref": "#/x-schemas/Entertainment/WayToWatch" - }, - "description": "An array of ways a user is might watch this entity, regardless of entitlements." - } - }, - "if": { - "properties": { - "entityType": { - "const": "program" - } - } - }, - "then": { - "required": [ - "programType" - ], - "not": { - "required": [ - "musicType" - ] - } - }, - "else": { - "required": [ - "musicType" - ], - "not": { - "required": [ - "programType" - ] } } }, - "OfferingType": { - "title": "OfferingType", - "type": "string", - "enum": [ - "free", - "subscribe", - "buy", - "rent" - ], - "description": "The offering type of the WayToWatch." - }, - "ProgramType": { - "title": "ProgramType", - "type": "string", - "description": "In the case of a program `entityType`, specifies the program type.", - "enum": [ - "movie", - "episode", - "season", - "series", - "other", - "preview", - "extra", - "concert", - "sportingEvent", - "advertisement", - "musicVideo", - "minisode" + "ProgramEntity": { + "title": "ProgramEntity", + "oneOf": [ + { + "$ref": "#/x-schemas/Entity/MovieEntity" + }, + { + "$ref": "#/x-schemas/Entity/TVEpisodeEntity" + }, + { + "$ref": "#/x-schemas/Entity/TVSeasonEntity" + }, + { + "$ref": "#/x-schemas/Entity/TVSeriesEntity" + }, + { + "$ref": "#/x-schemas/Entity/AdditionalEntity" + } ] }, - "MusicType": { - "title": "MusicType", - "type": "string", - "description": "In the case of a music `entityType`, specifies the type of music entity.", - "enum": [ - "song", - "album" + "MusicEntity": { + "title": "MusicEntity", + "type": "object", + "properties": { + "entityType": { + "const": "music" + }, + "musicType": { + "$ref": "#/x-schemas/Entertainment/MusicType" + }, + "entityId": { + "type": "string" + } + }, + "required": [ + "entityType", + "musicType", + "entityId" ] }, - "ContentRating": { - "title": "ContentRating", + "ChannelEntity": { + "title": "ChannelEntity", "type": "object", - "required": [ - "scheme", - "rating" - ], "properties": { - "scheme": { + "entityType": { + "const": "channel" + }, + "channelType": { "type": "string", "enum": [ - "CA-Movie", - "CA-TV", - "CA-Movie-Fr", - "CA-TV-Fr", - "US-Movie", - "US-TV" - ], - "description": "The rating scheme." + "streaming", + "overTheAir" + ] }, - "rating": { + "entityId": { "type": "string", - "description": "The content rating." + "description": "ID of the channel, in the target App's scope." }, - "advisories": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Optional list of subratings or content advisories." + "appContentData": { + "type": "string", + "maxLength": 256 } }, - "description": "A ContentRating represents an age or content based of an entity. Supported rating schemes and associated types are below.\n\n## United States\n\n`US-Movie` (MPAA):\n\nRatings: `NR`, `G`, `PG`, `PG13`, `R`, `NC17`\n\nAdvisories: `AT`, `BN`, `SL`, `SS`, `N`, `V`\n\n`US-TV` (Vchip):\n\nRatings: `TVY`, `TVY7`, `TVG`, `TVPG`, `TV14`, `TVMA`\n\nAdvisories: `FV`, `D`, `L`, `S`, `V`\n\n## Canada\n\n`CA-Movie` (OFRB):\n\nRatings: `G`, `PG`, `14A`, `18A`, `R`, `E`\n\n`CA-TV` (AGVOT)\n\nRatings: `E`, `C`, `C8`, `G`, `PG`, `14+`, `18+`\n\nAdvisories: `C`, `C8`, `G`, `PG`, `14+`, `18+`\n\n`CA-Movie-Fr` (Canadian French language movies):\n\nRatings: `G`, `8+`, `13+`, `16+`, `18+`\n\n`CA-TV-Fr` (Canadian French language TV):\n\nRatings: `G`, `8+`, `13+`, `16+`, `18+`\n" + "required": [ + "entityType", + "channelType", + "entityId" + ], + "additionalProperties": false }, + "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" + } + ] + }, + "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" + } + ] + }, + "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" + } + ] + }, + "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" + } + ] + }, + "PlayableEntity": { + "title": "PlayableEntity", + "anyOf": [ + { + "$ref": "#/x-schemas/Entity/MovieEntity" + }, + { + "$ref": "#/x-schemas/Entity/TVEpisodeEntity" + }, + { + "$ref": "#/x-schemas/Entity/PlaylistEntity" + }, + { + "$ref": "#/x-schemas/Entity/MusicEntity" + }, + { + "$ref": "#/x-schemas/Entity/AdditionalEntity" + } + ] + } + }, + "Entertainment": { + "uri": "https://meta.comcast.com/firebolt/entertainment", "WayToWatch": { "title": "WayToWatch", "type": "object", @@ -20976,6 +21803,226 @@ } }, "description": "A WayToWatch describes a way to watch a video program. It may describe a single\nstreamable asset or a set of streamable assets. For example, an app provider may\ndescribe HD, SD, and UHD assets as individual WayToWatch objects or rolled into\na single WayToWatch.\n\nIf the WayToWatch represents a single streamable asset, the provided\nContentIdentifiers must be sufficient to play back the specific asset when sent\nvia a playback intent or deep link. If the WayToWatch represents multiple\nstreamable assets, the provided ContentIdentifiers must be sufficient to\nplayback one of the assets represented with no user action. In this scenario,\nthe app SHOULD choose the best asset for the user based on their device and\nsettings. The ContentIdentifiers MUST also be sufficient for navigating the user\nto the appropriate entity or detail screen via an entity intent.\n\nThe app should set the `entitled` property to indicate if the user can watch, or\nnot watch, the asset without making a purchase. If the entitlement is known to\nexpire at a certain time (e.g., a rental), the app should also provide the\n`entitledExpires` property. If the entitlement is not expired, the UI will use\nthe `entitled` property to display watchable assets to the user, adjust how\nassets are presented to the user, and how intents into the app are generated.\nFor example, the the Aggregated Experience could render a \"Watch\" button for an\nentitled asset versus a \"Subscribe\" button for an non-entitled asset.\n\nThe app should set the `offeringType` to define how the content may be\nauthorized. The UI will use this to adjust how content is presented to the user.\n\nA single WayToWatch cannot represent streamable assets available via multiple\npurchase paths. If, for example, an asset has both Buy, Rent and Subscription\navailability, the three different entitlement paths MUST be represented as\nmultiple WayToWatch objects.\n\n`price` should be populated for WayToWatch objects with `buy` or `rent`\n`offeringType`. If the WayToWatch represents a set of assets with various price\npoints, the `price` provided must be the lowest available price." + }, + "OfferingType": { + "title": "OfferingType", + "type": "string", + "enum": [ + "free", + "subscribe", + "buy", + "rent" + ], + "description": "The offering type of the WayToWatch." + }, + "ContentIdentifiers": { + "title": "ContentIdentifiers", + "type": "object", + "properties": { + "assetId": { + "type": "string", + "description": "Identifies a particular playable asset. For example, the HD version of a particular movie separate from the UHD version." + }, + "entityId": { + "type": "string", + "description": "Identifies an entity, such as a Movie, TV Series or TV Episode." + }, + "seasonId": { + "type": "string", + "description": "The TV Season for a TV Episode." + }, + "seriesId": { + "type": "string", + "description": "The TV Series for a TV Episode or TV Season." + }, + "appContentData": { + "type": "string", + "description": "App-specific content identifiers.", + "maxLength": 1024 + } + }, + "description": "The ContentIdentifiers object is how the app identifies an entity or asset to\nthe Firebolt platform. These ids are used to look up metadata and deep link into\nthe app.\n\nApps do not need to provide all ids. They only need to provide the minimum\nrequired to target a playable stream or an entity detail screen via a deep link.\nIf an id isn't needed to get to those pages, it doesn't need to be included." + }, + "ContentRating": { + "title": "ContentRating", + "type": "object", + "required": [ + "scheme", + "rating" + ], + "properties": { + "scheme": { + "type": "string", + "enum": [ + "CA-Movie", + "CA-TV", + "CA-Movie-Fr", + "CA-TV-Fr", + "US-Movie", + "US-TV" + ], + "description": "The rating scheme." + }, + "rating": { + "type": "string", + "description": "The content rating." + }, + "advisories": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional list of subratings or content advisories." + } + }, + "description": "A ContentRating represents an age or content based of an entity. Supported rating schemes and associated types are below.\n\n## United States\n\n`US-Movie` (MPAA):\n\nRatings: `NR`, `G`, `PG`, `PG13`, `R`, `NC17`\n\nAdvisories: `AT`, `BN`, `SL`, `SS`, `N`, `V`\n\n`US-TV` (Vchip):\n\nRatings: `TVY`, `TVY7`, `TVG`, `TVPG`, `TV14`, `TVMA`\n\nAdvisories: `FV`, `D`, `L`, `S`, `V`\n\n## Canada\n\n`CA-Movie` (OFRB):\n\nRatings: `G`, `PG`, `14A`, `18A`, `R`, `E`\n\n`CA-TV` (AGVOT)\n\nRatings: `E`, `C`, `C8`, `G`, `PG`, `14+`, `18+`\n\nAdvisories: `C`, `C8`, `G`, `PG`, `14+`, `18+`\n\n`CA-Movie-Fr` (Canadian French language movies):\n\nRatings: `G`, `8+`, `13+`, `16+`, `18+`\n\n`CA-TV-Fr` (Canadian French language TV):\n\nRatings: `G`, `8+`, `13+`, `16+`, `18+`\n" + }, + "MusicType": { + "title": "MusicType", + "type": "string", + "description": "In the case of a music `entityType`, specifies the type of music entity.", + "enum": [ + "song", + "album" + ] + }, + "Entitlement": { + "title": "Entitlement", + "type": "object", + "properties": { + "entitlementId": { + "type": "string" + }, + "startTime": { + "type": "string", + "format": "date-time" + }, + "endTime": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "entitlementId" + ] + }, + "EntityInfo": { + "title": "EntityInfo", + "description": "An EntityInfo object represents an \"entity\" on the platform. Currently, only entities of type `program` are supported. `programType` must be supplied to identify the program type.\n\nAdditionally, EntityInfo objects must specify a properly formed\nContentIdentifiers object, `entityType`, and `title`. The app should provide\nthe `synopsis` property for a good user experience if the content\nmetadata is not available another way.\n\nThe ContentIdentifiers must be sufficient for navigating the user to the\nappropriate entity or detail screen via a `detail` intent or deep link.\n\nEntityInfo objects must provide at least one WayToWatch object when returned as\npart of an `entityInfo` method and a streamable asset is available to the user.\nIt is optional for the `purchasedContent` method, but recommended because the UI\nmay use those data.", + "type": "object", + "required": [ + "identifiers", + "entityType", + "title" + ], + "properties": { + "identifiers": { + "$ref": "#/x-schemas/Entertainment/ContentIdentifiers" + }, + "title": { + "type": "string", + "description": "Title of the entity." + }, + "entityType": { + "type": "string", + "enum": [ + "program", + "music" + ], + "description": "The type of the entity, e.g. `program` or `music`." + }, + "programType": { + "$ref": "#/x-schemas/Entertainment/ProgramType" + }, + "musicType": { + "$ref": "#/x-schemas/Entertainment/MusicType" + }, + "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": "#/x-schemas/Entertainment/ContentRating" + }, + "description": "A list of ContentRating objects, describing the entity's ratings in various rating schemes." + }, + "waysToWatch": { + "type": "array", + "items": { + "$ref": "#/x-schemas/Entertainment/WayToWatch" + }, + "description": "An array of ways a user is might watch this entity, regardless of entitlements." + } + }, + "if": { + "properties": { + "entityType": { + "const": "program" + } + } + }, + "then": { + "required": [ + "programType" + ], + "not": { + "required": [ + "musicType" + ] + } + }, + "else": { + "required": [ + "musicType" + ], + "not": { + "required": [ + "programType" + ] + } + } + }, + "ProgramType": { + "title": "ProgramType", + "type": "string", + "description": "In the case of a program `entityType`, specifies the program type.", + "enum": [ + "movie", + "episode", + "season", + "series", + "other", + "preview", + "extra", + "concert", + "sportingEvent", + "advertisement", + "musicVideo", + "minisode" + ] } }, "Intents": { @@ -21032,6 +22079,11 @@ "action": { "const": "home" } + }, + "not": { + "required": [ + "data" + ] } } ], @@ -21060,285 +22112,26 @@ "action": { "const": "launch" } - } - } - ], - "examples": [ - { - "action": "launch", - "context": { - "source": "voice" - } - } - ] - }, - "EntityIntent": { - "description": "A Firebolt compliant representation of a user intention to navigate an app to a specific entity page, and bring that app to the foreground if needed.", - "title": "EntityIntent", - "allOf": [ - { - "$ref": "#/x-schemas/Intents/Intent" - }, - { - "$ref": "#/x-schemas/Intents/IntentProperties" - }, - { - "type": "object", - "required": [ - "data" - ], - "properties": { - "action": { - "const": "entity" - }, - "data": { - "anyOf": [ - { - "$ref": "#/x-schemas/Intents/MovieEntity" - }, - { - "$ref": "#/x-schemas/Intents/TVEpisodeEntity" - }, - { - "$ref": "#/x-schemas/Intents/TVSeriesEntity" - }, - { - "$ref": "#/x-schemas/Intents/TVSeasonEntity" - }, - { - "$ref": "#/x-schemas/Intents/MusicEntity" - }, - { - "$ref": "#/x-schemas/Intents/AdditionalEntity" - }, - { - "$ref": "#/x-schemas/Intents/UntypedEntity" - } - ] - } - } - } - ], - "examples": [ - { - "action": "entity", - "context": { - "source": "voice" - }, - "data": { - "entityType": "program", - "programType": "movie", - "entityId": "el-camino" - } - } - ] - }, - "PlaybackIntent": { - "description": "A Firebolt compliant representation of a user intention to navigate an app to a the video player for a specific, playable entity, and bring that app to the foreground if needed.", - "title": "PlaybackIntent", - "allOf": [ - { - "$ref": "#/x-schemas/Intents/Intent" - }, - { - "$ref": "#/x-schemas/Intents/IntentProperties" - }, - { - "type": "object", - "required": [ - "data" - ], - "properties": { - "action": { - "const": "playback" - }, - "data": { - "$ref": "#/x-schemas/Intents/PlayableEntity" - } - } - } - ], - "examples": [ - { - "action": "playback", - "data": { - "entityType": "program", - "programType": "episode", - "entityId": "breaking-bad-pilot", - "seriesId": "breaking-bad", - "seasonId": "breaking-bad-season-1" - }, - "context": { - "source": "voice" - } - } - ] - }, - "SearchIntent": { - "description": "A Firebolt compliant representation of a user intention to navigate an app to it's search UI with a search term populated, and bring that app to the foreground if needed.", - "title": "SearchIntent", - "allOf": [ - { - "$ref": "#/x-schemas/Intents/Intent" - }, - { - "$ref": "#/x-schemas/Intents/IntentProperties" - }, - { - "type": "object", - "properties": { - "action": { - "const": "search" - }, - "data": { - "type": "object", - "required": [ - "query" - ], - "properties": { - "query": { - "type": "string" - } - }, - "additionalProperties": false - } - } - } - ], - "examples": [ - { - "action": "search", - "data": { - "query": "walter white" - }, - "context": { - "source": "voice" - } - } - ] - }, - "SectionIntent": { - "description": "A Firebolt compliant representation of a user intention to navigate an app to a section not covered by `home`, `entity`, `player`, or `search`, and bring that app to the foreground if needed.", - "title": "SectionIntent", - "allOf": [ - { - "$ref": "#/x-schemas/Intents/Intent" - }, - { - "$ref": "#/x-schemas/Intents/IntentProperties" - }, - { - "type": "object", - "properties": { - "action": { - "const": "section" - }, - "data": { - "type": "object", - "required": [ - "sectionName" - ], - "properties": { - "sectionName": { - "type": "string" - } - }, - "additionalProperties": false - } - } - } - ], - "examples": [ - { - "action": "section", - "data": { - "sectionName": "settings" }, - "context": { - "source": "voice" - } - } - ] - }, - "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", - "allOf": [ - { - "$ref": "#/x-schemas/Intents/Intent" - }, - { - "$ref": "#/x-schemas/Intents/IntentProperties" - }, - { - "type": "object", - "required": [ - "data" - ], - "properties": { - "action": { - "const": "tune" - }, - "data": { - "type": "object", - "required": [ - "entity" - ], - "additionalProperties": false, - "properties": { - "entity": { - "$ref": "#/x-schemas/Intents/ChannelEntity" - }, - "options": { - "description": "The options property of the data property MUST have only one of the following fields.", - "type": "object", - "required": [], - "additionalProperties": false, - "minProperties": 1, - "maxProperties": 1, - "properties": { - "assetId": { - "type": "string", - "description": "The ID of a specific 'listing', as scoped by the target App's ID-space, which the App should begin playback from." - }, - "restartCurrentProgram": { - "type": "boolean", - "description": "Denotes that the App should start playback at the most recent program boundary, rather than 'live.'" - }, - "time": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 Date/Time where the App should begin playback from." - } - } - } - } - } + "not": { + "required": [ + "data" + ] } } ], "examples": [ { - "action": "tune", - "data": { - "entity": { - "entityType": "channel", - "channelType": "streaming", - "entityId": "an-ott-channel" - }, - "options": { - "restartCurrentProgram": true - } - }, + "action": "launch", "context": { "source": "voice" } } ] }, - "PlayEntityIntent": { - "description": "A Firebolt compliant representation of a user intention to navigate an app to a the video player for a specific, playable entity, and bring that app to the foreground if needed.", - "title": "PlayEntityIntent", + "EntityIntent": { + "description": "A Firebolt compliant representation of a user intention to navigate an app to a specific entity page, and bring that app to the foreground if needed.", + "title": "EntityIntent", "allOf": [ { "$ref": "#/x-schemas/Intents/Intent" @@ -21353,108 +22146,31 @@ ], "properties": { "action": { - "const": "play-entity" + "const": "entity" }, "data": { - "type": "object", - "properties": { - "entity": { - "$ref": "#/x-schemas/Intents/PlayableEntity" - }, - "options": { - "type": "object", - "properties": { - "playFirstId": { - "type": "string" - }, - "playFirstTrack": { - "type": "integer", - "minimum": 1 - } - }, - "additionalProperties": false - } - }, - "required": [ - "entity" - ], - "propertyNames": { - "enum": [ - "entity", - "options" - ] - }, - "if": { - "properties": { - "entity": { - "type": "object", - "required": [ - "entityType" - ], - "properties": { - "entityType": { - "const": "playlist" - } - } - } - } - }, - "then": { - "type": "object", - "properties": { - "options": { - "maxProperties": 1 - } - } - }, - "else": { - "type": "object", - "properties": { - "options": { - "maxProperties": 0 - } - } - } + "$ref": "#/x-schemas/Entity/Entity" } } } ], "examples": [ { - "action": "play-entity", - "data": { - "entity": { - "entityType": "playlist", - "entityId": "playlist/xyz" - }, - "options": { - "playFirstId": "song/xyz" - } - }, + "action": "entity", "context": { "source": "voice" - } - }, - { - "action": "play-entity", - "data": { - "entity": { - "entityType": "playlist", - "entityId": "playlist/xyz" - }, - "options": { - "playFirstTrack": 3 - } }, - "context": { - "source": "voice" + "data": { + "entityType": "program", + "programType": "movie", + "entityId": "el-camino" } } ] }, - "PlayQueryIntent": { - "description": "A Firebolt compliant representation of a user intention to navigate an app to a the video player for an abstract query to be searched for and played by the app.", - "title": "PlayQueryIntent", + "PlaybackIntent": { + "description": "A Firebolt compliant representation of a user intention to navigate an app to a the video player for a specific, playable entity, and bring that app to the foreground if needed.", + "title": "PlaybackIntent", "allOf": [ { "$ref": "#/x-schemas/Intents/Intent" @@ -21469,82 +22185,23 @@ ], "properties": { "action": { - "const": "play-query" + "const": "playback" }, "data": { - "type": "object", - "properties": { - "query": { - "type": "string" - }, - "options": { - "type": "object", - "properties": { - "programTypes": { - "type": "array", - "items": { - "$ref": "#/x-schemas/Entertainment/ProgramType" - } - }, - "musicTypes": { - "type": "array", - "items": { - "$ref": "#/x-schemas/Entertainment/MusicType" - } - } - }, - "additionalProperties": false - } - }, - "required": [ - "query" - ], - "propertyNames": { - "enum": [ - "query", - "options" - ] - } + "$ref": "#/x-schemas/Entity/PlayableEntity" } } } ], "examples": [ { - "action": "play-query", - "data": { - "query": "Ed Sheeran" - }, - "context": { - "source": "voice" - } - }, - { - "action": "play-query", - "data": { - "query": "Ed Sheeran", - "options": { - "programTypes": [ - "movie" - ] - } - }, - "context": { - "source": "voice" - } - }, - { - "action": "play-query", + "action": "playback", "data": { - "query": "Ed Sheeran", - "options": { - "programTypes": [ - "movie" - ], - "musicTypes": [ - "song" - ] - } + "entityType": "program", + "programType": "episode", + "entityId": "breaking-bad-pilot", + "seriesId": "breaking-bad", + "seasonId": "breaking-bad-season-1" }, "context": { "source": "voice" @@ -21552,434 +22209,422 @@ } ] }, - "Intent": { - "description": "A Firebolt compliant representation of a user intention.", - "type": "object", - "required": [ - "action", - "context" - ], - "properties": { - "action": { - "type": "string" - }, - "context": { - "type": "object", - "required": [ - "source" - ], - "properties": { - "source": { - "type": "string" - } - } - } - } - }, - "IntentProperties": { - "type": "object", - "propertyNames": { - "enum": [ - "action", - "data", - "context" - ] - } - }, - "MovieEntity": { - "title": "MovieEntity", + "SearchIntent": { + "description": "A Firebolt compliant representation of a user intention to navigate an app to it's search UI with a search term populated, and bring that app to the foreground if needed.", + "title": "SearchIntent", "allOf": [ { - "$ref": "#/x-schemas/Intents/ProgramEntity" + "$ref": "#/x-schemas/Intents/Intent" + }, + { + "$ref": "#/x-schemas/Intents/IntentProperties" }, { - "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": "#/x-schemas/Intents/Identifier" - }, - "assetId": { - "$ref": "#/x-schemas/Intents/Identifier" + "action": { + "const": "search" }, - "appContentData": { - "type": "string", - "maxLength": 256 + "data": { + "type": "object", + "required": [ + "query" + ], + "properties": { + "query": { + "type": "string" + } + }, + "additionalProperties": false } - }, - "additionalProperties": false + } } ], "examples": [ { - "entityType": "program", - "programType": "movie", - "entityId": "el-camino" + "action": "search", + "data": { + "query": "walter white" + }, + "context": { + "source": "voice" + } } ] }, - "TVEpisodeEntity": { - "title": "TVEpisodeEntity", + "SectionIntent": { + "description": "A Firebolt compliant representation of a user intention to navigate an app to a section not covered by `home`, `entity`, `player`, or `search`, and bring that app to the foreground if needed.", + "title": "SectionIntent", "allOf": [ { - "$ref": "#/x-schemas/Intents/ProgramEntity" + "$ref": "#/x-schemas/Intents/Intent" + }, + { + "$ref": "#/x-schemas/Intents/IntentProperties" }, { - "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": "#/x-schemas/Intents/Identifier" - }, - "seriesId": { - "$ref": "#/x-schemas/Intents/Identifier" - }, - "seasonId": { - "$ref": "#/x-schemas/Intents/Identifier" - }, - "assetId": { - "$ref": "#/x-schemas/Intents/Identifier" + "action": { + "const": "section" }, - "appContentData": { - "type": "string", - "maxLength": 256 + "data": { + "type": "object", + "required": [ + "sectionName" + ], + "properties": { + "sectionName": { + "type": "string" + } + }, + "additionalProperties": false } }, - "additionalProperties": false + "required": [ + "data" + ] } ], "examples": [ { - "entityType": "program", - "programType": "episode", - "entityId": "breaking-bad-pilot", - "seriesId": "breaking-bad", - "seasonId": "breaking-bad-season-1" + "action": "section", + "data": { + "sectionName": "settings" + }, + "context": { + "source": "voice" + } } ] }, - "TVSeriesEntity": { - "title": "TVSeriesEntity", + "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", "allOf": [ { - "$ref": "#/x-schemas/Intents/ProgramEntity" + "$ref": "#/x-schemas/Intents/Intent" + }, + { + "$ref": "#/x-schemas/Intents/IntentProperties" }, { - "description": "A Firebolt compliant representation of a TV Series entity.", "type": "object", "required": [ - "entityType", - "programType", - "entityId" + "data" ], "properties": { - "entityType": { - "const": "program" - }, - "programType": { - "const": "series" - }, - "entityId": { - "$ref": "#/x-schemas/Intents/Identifier" - }, - "assetId": { - "$ref": "#/x-schemas/Intents/Identifier" + "action": { + "const": "tune" }, - "appContentData": { - "type": "string", - "maxLength": 256 + "data": { + "type": "object", + "required": [ + "entity" + ], + "additionalProperties": false, + "properties": { + "entity": { + "$ref": "#/x-schemas/Entity/ChannelEntity" + }, + "options": { + "description": "The options property of the data property MUST have only one of the following fields.", + "type": "object", + "required": [], + "additionalProperties": false, + "minProperties": 1, + "maxProperties": 1, + "properties": { + "assetId": { + "type": "string", + "description": "The ID of a specific 'listing', as scoped by the target App's ID-space, which the App should begin playback from." + }, + "restartCurrentProgram": { + "type": "boolean", + "description": "Denotes that the App should start playback at the most recent program boundary, rather than 'live.'" + }, + "time": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 Date/Time where the App should begin playback from." + } + } + } + } } - }, - "additionalProperties": false + } } ], "examples": [ { - "entityType": "program", - "programType": "series", - "entityId": "breaking-bad" + "action": "tune", + "data": { + "entity": { + "entityType": "channel", + "channelType": "streaming", + "entityId": "an-ott-channel" + }, + "options": { + "restartCurrentProgram": true + } + }, + "context": { + "source": "voice" + } } ] }, - "TVSeasonEntity": { - "title": "TVSeasonEntity", - "description": "A Firebolt compliant representation of a TV Season entity.", + "PlayEntityIntent": { + "description": "A Firebolt compliant representation of a user intention to navigate an app to a the video player for a specific, playable entity, and bring that app to the foreground if needed.", + "title": "PlayEntityIntent", "allOf": [ { - "$ref": "#/x-schemas/Intents/ProgramEntity" + "$ref": "#/x-schemas/Intents/Intent" + }, + { + "$ref": "#/x-schemas/Intents/IntentProperties" }, { "type": "object", "required": [ - "entityType", - "programType", - "entityId", - "seriesId" + "data" ], "properties": { - "entityType": { - "const": "program" - }, - "programType": { - "const": "season" - }, - "entityId": { - "$ref": "#/x-schemas/Intents/Identifier" - }, - "seriesId": { - "$ref": "#/x-schemas/Intents/Identifier" - }, - "assetId": { - "$ref": "#/x-schemas/Intents/Identifier" + "action": { + "const": "play-entity" }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false - } - ], - "examples": [ - { - "entityType": "program", - "programType": "season", - "entityId": "breaking-bad-season-1", - "seriesId": "breaking-bad" - } - ] - }, - "MusicEntity": { - "title": "MusicEntity", - "type": "object", - "properties": { - "entityType": { - "const": "music" - }, - "musicType": { - "$ref": "#/x-schemas/Entertainment/MusicType" - }, - "entityId": { - "type": "string" - } - }, - "required": [ - "entityType", - "musicType", - "entityId" - ] - }, - "AdditionalEntity": { - "title": "AdditionalEntity", - "allOf": [ + "data": { + "type": "object", + "properties": { + "entity": { + "$ref": "#/x-schemas/Entity/PlayableEntity" + }, + "options": { + "type": "object", + "properties": { + "playFirstId": { + "type": "string" + }, + "playFirstTrack": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false + } + }, + "required": [ + "entity" + ], + "propertyNames": { + "enum": [ + "entity", + "options" + ] + }, + "if": { + "properties": { + "entity": { + "type": "object", + "required": [ + "entityType" + ], + "properties": { + "entityType": { + "const": "playlist" + } + } + } + } + }, + "then": { + "type": "object", + "properties": { + "options": { + "type": "object", + "maxProperties": 1 + } + } + }, + "else": { + "type": "object", + "properties": { + "options": { + "type": "object", + "maxProperties": 0 + } + } + } + } + } + } + ], + "examples": [ { - "$ref": "#/x-schemas/Intents/ProgramEntity" + "action": "play-entity", + "data": { + "entity": { + "entityType": "playlist", + "entityId": "playlist/xyz" + }, + "options": { + "playFirstId": "song/xyz" + } + }, + "context": { + "source": "voice" + } }, { - "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": "#/x-schemas/Intents/Identifier" - }, - "assetId": { - "$ref": "#/x-schemas/Intents/Identifier" + "action": "play-entity", + "data": { + "entity": { + "entityType": "playlist", + "entityId": "playlist/xyz" }, - "appContentData": { - "type": "string", - "maxLength": 256 + "options": { + "playFirstTrack": 3 } }, - "additionalProperties": false - } - ], - "examples": [ - { - "entityType": "program", - "programType": "concert", - "entityId": "live-aid" + "context": { + "source": "voice" + } } ] }, - "UntypedEntity": { - "title": "UntypedEntity", + "PlayQueryIntent": { + "description": "A Firebolt compliant representation of a user intention to navigate an app to a the video player for an abstract query to be searched for and played by the app.", + "title": "PlayQueryIntent", "allOf": [ { - "description": "A Firebolt compliant representation of the remaining entity types.", + "$ref": "#/x-schemas/Intents/Intent" + }, + { + "$ref": "#/x-schemas/Intents/IntentProperties" + }, + { "type": "object", "required": [ - "entityId" + "data" ], "properties": { - "entityId": { - "$ref": "#/x-schemas/Intents/Identifier" - }, - "assetId": { - "$ref": "#/x-schemas/Intents/Identifier" + "action": { + "const": "play-query" }, - "appContentData": { - "type": "string", - "maxLength": 256 + "data": { + "type": "object", + "properties": { + "query": { + "type": "string" + }, + "options": { + "type": "object", + "properties": { + "programTypes": { + "type": "array", + "items": { + "$ref": "#/x-schemas/Entertainment/ProgramType" + } + }, + "musicTypes": { + "type": "array", + "items": { + "$ref": "#/x-schemas/Entertainment/MusicType" + } + } + }, + "additionalProperties": false + } + }, + "required": [ + "query" + ], + "propertyNames": { + "enum": [ + "query", + "options" + ] + } } - }, - "additionalProperties": false + } } ], "examples": [ { - "entityId": "an-entity" - } - ] - }, - "PlayableEntity": { - "title": "PlayableEntity", - "anyOf": [ - { - "$ref": "#/x-schemas/Intents/MovieEntity" - }, - { - "$ref": "#/x-schemas/Intents/TVEpisodeEntity" - }, - { - "$ref": "#/x-schemas/Intents/PlaylistEntity" + "action": "play-query", + "data": { + "query": "Ed Sheeran" + }, + "context": { + "source": "voice" + } }, { - "$ref": "#/x-schemas/Intents/MusicEntity" + "action": "play-query", + "data": { + "query": "Ed Sheeran", + "options": { + "programTypes": [ + "movie" + ] + } + }, + "context": { + "source": "voice" + } }, { - "$ref": "#/x-schemas/Intents/AdditionalEntity" + "action": "play-query", + "data": { + "query": "Ed Sheeran", + "options": { + "programTypes": [ + "movie" + ], + "musicTypes": [ + "song" + ] + } + }, + "context": { + "source": "voice" + } } ] }, - "ChannelEntity": { - "title": "ChannelEntity", + "Intent": { + "description": "A Firebolt compliant representation of a user intention.", "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" + "action", + "context" ], - "additionalProperties": false - }, - "ProgramEntity": { - "title": "ProgramEntity", - "type": "object", "properties": { - "entityType": { - "const": "program" - }, - "programType": { - "$ref": "#/x-schemas/Entertainment/ProgramType" - }, - "entityId": { + "action": { "type": "string" + }, + "context": { + "type": "object", + "required": [ + "source" + ], + "properties": { + "source": { + "type": "string" + } + } } - }, - "required": [ - "entityType", - "programType", - "entityId" - ] - }, - "Identifier": { - "type": "string" + } }, - "PlaylistEntity": { - "title": "PlaylistEntity", - "description": "A Firebolt compliant representation of a Playlist entity.", + "IntentProperties": { "type": "object", - "required": [ - "entityType", - "entityId" - ], - "properties": { - "entityType": { - "const": "playlist" - }, - "entityId": { - "$ref": "#/x-schemas/Intents/Identifier" - }, - "assetId": { - "$ref": "#/x-schemas/Intents/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false, - "examples": [ - { - "entityType": "playlist", - "entityId": "playlist/xyz" - } - ] + "propertyNames": { + "enum": [ + "action", + "data", + "context" + ] + } } }, "Lifecycle": { diff --git a/core/main/src/state/openrpc_state.rs b/core/main/src/state/openrpc_state.rs index 039fc5f66..c0eec7680 100644 --- a/core/main/src/state/openrpc_state.rs +++ b/core/main/src/state/openrpc_state.rs @@ -42,81 +42,144 @@ pub enum ApiSurface { } #[derive(Debug, Clone, Default)] -pub struct ProviderSet { - pub request: Option, - pub focus: Option, - pub response: Option, - pub error: Option, +pub struct ProviderRelationSet { + pub capability: Option, pub attributes: Option<&'static ProviderAttributes>, + pub event: bool, + pub provides: Option, + pub provides_to: Option, + pub provided_by: Option, + pub uses: Option>, + pub allow_focus_for: Option, + pub response_for: Option, + pub error_for: Option, } -impl ProviderSet { - pub fn new() -> ProviderSet { - ProviderSet::default() +impl ProviderRelationSet { + pub fn new() -> ProviderRelationSet { + ProviderRelationSet::default() } } -pub fn build_provider_sets( +pub fn build_provider_relation_sets( openrpc_methods: &Vec, -) -> HashMap { - let mut provider_sets = HashMap::default(); +) -> HashMap { + let mut provider_relation_sets = HashMap::default(); for method in openrpc_methods { let mut has_x_provides = None; - // Only build provider sets for AcknowledgeChallenge and PinChallenge methods for now + // Only build provider sets for AcknowledgeChallenge, PinChallenge methods, Discovery, and Content for now if !method.name.starts_with("AcknowledgeChallenge.") && !method.name.starts_with("PinChallenge.") + && !method.name.starts_with("Discovery.userInterest") + && !method.name.starts_with("Discovery.onRequestUserInterest") + && !method.name.starts_with("Discovery.userInterestResponse") + && !method.name.starts_with("Content.requestUserInterest") + && !method.name.starts_with("Content.onUserInterest") { continue; } if let Some(tags) = &method.tags { let mut has_event = false; - let mut has_caps = false; - let mut has_x_allow_focus_for = false; - let mut has_x_response_for = false; - let mut has_x_error_for = false; + let mut x_allow_focus_for = None; + let mut x_response_for = None; + let mut x_error_for = None; + let mut x_provided_by = None; + let mut x_provides = None; + let mut x_uses = None; for tag in tags { if tag.name.eq("event") { has_event = true; } else if tag.name.eq("capabilities") { - has_caps = true; has_x_provides = tag.get_provides(); - has_x_allow_focus_for |= tag.allow_focus_for.is_some(); - has_x_response_for |= tag.response_for.is_some(); - has_x_error_for |= tag.error_for.is_some(); + x_allow_focus_for = tag.allow_focus_for.clone(); + x_response_for = tag.response_for.clone(); + x_error_for = tag.error_for.clone(); + x_provided_by = tag.provided_by.clone(); + x_provides = tag.provides.clone(); + x_uses = tag.uses.clone(); } } - if let Some(p) = has_x_provides { - let mut provider_set = provider_sets - .get(&p.as_str()) - .unwrap_or(&ProviderSet::new()) - .clone(); - - if has_event && has_caps { - provider_set.request = Some(method.clone()); - } - if has_x_allow_focus_for { - provider_set.focus = Some(method.clone()); - } - if has_x_response_for { - provider_set.response = Some(method.clone()); - } - if has_x_error_for { - provider_set.error = Some(method.clone()); + let mut provider_relation_set = provider_relation_sets + .get(&FireboltOpenRpcMethod::name_with_lowercase_module( + &method.name, + )) + .unwrap_or(&ProviderRelationSet::new()) + .clone(); + + if has_x_provides.is_some() { + provider_relation_set.allow_focus_for = x_allow_focus_for; + provider_relation_set.response_for = x_response_for; + provider_relation_set.error_for = x_error_for; + provider_relation_set.capability = x_provides; + } else { + // x-provided-by can only be set if x-provides isn't. + provider_relation_set.provided_by = x_provided_by.clone(); + if let Some(provided_by) = x_provided_by { + let mut provided_by_set = provider_relation_sets + .get(&provided_by) + .unwrap_or(&ProviderRelationSet::new()) + .clone(); + + provided_by_set.provides_to = Some(method.name.clone()); + + provider_relation_sets.insert( + FireboltOpenRpcMethod::name_with_lowercase_module(&provided_by), + provided_by_set.to_owned(), + ); } + } - let module: Vec<&str> = method.name.split('.').collect(); - provider_set.attributes = ProviderAttributes::get(module[0]); + provider_relation_set.uses = x_uses; + provider_relation_set.event = has_event; - provider_sets.insert(p.as_str(), provider_set.to_owned()); + // If this is an event, then it provides the capability. + if provider_relation_set.event { + provider_relation_set.provides = provider_relation_set.capability.clone(); } + + let module: Vec<&str> = method.name.split('.').collect(); + provider_relation_set.attributes = ProviderAttributes::get(module[0]); + + provider_relation_sets.insert( + FireboltOpenRpcMethod::name_with_lowercase_module(&method.name), + provider_relation_set.to_owned(), + ); } } - provider_sets + + // Post-process sets to set 'provides' for methods that provide-to other methods. + + let provides_to_array: Vec<(String, String)> = provider_relation_sets + .iter() + .filter_map(|(method_name, provider_relation_set)| { + provider_relation_set + .provides_to + .as_ref() + .map(|provides_to| (method_name.clone(), provides_to.clone())) + }) + .collect(); + + for (provider_method, provides_to_method) in provides_to_array { + let provided_to_capability = + if let Some(provided_to_set) = provider_relation_sets.get(&provides_to_method) { + provided_to_set.capability.clone() + } else { + None + }; + + if let Some(provider_set) = provider_relation_sets.get_mut(&provider_method) { + if provider_set.provides.is_none() { + provider_set.provides = provided_to_capability.clone(); + } + } + } + + provider_relation_sets } #[derive(Debug, Clone)] @@ -127,7 +190,7 @@ pub struct OpenRpcState { ripple_cap_map: Arc>>, cap_policies: Arc>>, extended_rpc: Arc>>, - provider_map: Arc>>, + provider_relation_map: Arc>>, openrpc_validator: Arc>, } @@ -198,7 +261,9 @@ impl OpenRpcState { cap_policies: Arc::new(RwLock::new(version_manifest.capabilities)), open_rpc: firebolt_open_rpc.clone(), extended_rpc: Arc::new(RwLock::new(Vec::new())), - provider_map: Arc::new(RwLock::new(build_provider_sets(&firebolt_open_rpc.methods))), + provider_relation_map: Arc::new(RwLock::new(build_provider_relation_sets( + &firebolt_open_rpc.methods, + ))), openrpc_validator: Arc::new(RwLock::new(openrpc_validator)), } } @@ -346,8 +411,15 @@ impl OpenRpcState { self.open_rpc.clone() } - pub fn get_provider_map(&self) -> HashMap { - self.provider_map.read().unwrap().clone() + pub fn get_provider_relation_map(&self) -> HashMap { + self.provider_relation_map.read().unwrap().clone() + } + + pub fn set_provider_relation_map( + &self, + provider_relation_map: HashMap, + ) { + *self.provider_relation_map.write().unwrap() = provider_relation_map; } pub fn get_version(&self) -> FireboltSemanticVersion { diff --git a/core/sdk/src/api/firebolt/fb_openrpc.rs b/core/sdk/src/api/firebolt/fb_openrpc.rs index 2169bc8e4..6ee5adfcd 100644 --- a/core/sdk/src/api/firebolt/fb_openrpc.rs +++ b/core/sdk/src/api/firebolt/fb_openrpc.rs @@ -307,6 +307,8 @@ pub struct FireboltOpenRpcTag { pub manages: Option>, #[serde(rename = "x-provides")] pub provides: Option, + #[serde(rename = "x-provided-by")] + pub provided_by: Option, #[serde(rename = "x-alternative")] pub alternative: Option, #[serde(rename = "x-since")] @@ -804,6 +806,7 @@ mod tests { error_for: None, allow_focus: None, allow_focus_for: None, + provided_by: None, }; assert_eq!( @@ -847,6 +850,7 @@ mod tests { error_for: None, allow_focus: None, allow_focus_for: None, + provided_by: None, }]), }; @@ -867,6 +871,7 @@ mod tests { error_for: None, allow_focus: None, allow_focus_for: None, + provided_by: None, }]); assert_eq!(method.get_allow_value(), None); diff --git a/core/sdk/src/api/firebolt/provider.rs b/core/sdk/src/api/firebolt/provider.rs index c9254a4c8..dd25e2517 100644 --- a/core/sdk/src/api/firebolt/provider.rs +++ b/core/sdk/src/api/firebolt/provider.rs @@ -23,9 +23,7 @@ use crate::api::device::entertainment_data::{ use super::{ fb_keyboard::{KeyboardSessionRequest, KeyboardSessionResponse}, - fb_pin::{ - PinChallengeRequest, PinChallengeResponse, PIN_CHALLENGE_CAPABILITY, PIN_CHALLENGE_EVENT, - }, + fb_pin::{PinChallengeRequest, PinChallengeResponse}, }; pub const ACK_CHALLENGE_EVENT: &str = "acknowledgechallenge.onRequestChallenge"; @@ -39,7 +37,7 @@ pub enum ProviderRequestPayload { AckChallenge(Challenge), EntityInfoRequest(EntityInfoParameters), PurchasedContentRequest(PurchasedContentParameters), - Generic(String), + Generic(serde_json::Value), } #[derive(Debug, Clone, Serialize)] @@ -50,6 +48,7 @@ pub enum ProviderResponsePayloadType { KeyboardResult, EntityInfoResponse, PurchasedContentResponse, + Generic, } impl ToString for ProviderResponsePayloadType { @@ -63,6 +62,7 @@ impl ToString for ProviderResponsePayloadType { ProviderResponsePayloadType::PurchasedContentResponse => { "PurchasedContentResponse".into() } + ProviderResponsePayloadType::Generic => "GenericResponse".into(), } } } @@ -77,6 +77,7 @@ pub enum ProviderResponsePayload { KeyboardResult(KeyboardSessionResponse), EntityInfoResponse(Option), PurchasedContentResponse(PurchasedContentResult), + Generic(serde_json::Value), } impl ProviderResponsePayload { @@ -119,6 +120,22 @@ impl ProviderResponsePayload { _ => None, } } + + pub fn as_value(&self) -> serde_json::Value { + match self { + ProviderResponsePayload::ChallengeResponse(res) => serde_json::to_value(res).unwrap(), + ProviderResponsePayload::ChallengeError(res) => serde_json::to_value(res).unwrap(), + ProviderResponsePayload::PinChallengeResponse(res) => { + serde_json::to_value(res).unwrap() + } + ProviderResponsePayload::KeyboardResult(res) => serde_json::to_value(res).unwrap(), + ProviderResponsePayload::EntityInfoResponse(res) => serde_json::to_value(res).unwrap(), + ProviderResponsePayload::PurchasedContentResponse(res) => { + serde_json::to_value(res).unwrap() + } + ProviderResponsePayload::Generic(res) => res.clone(), + } + } } #[derive(Serialize, Deserialize)] @@ -159,37 +176,28 @@ pub struct ExternalProviderError { #[derive(Debug, Clone, Serialize)] pub struct ProviderAttributes { - pub event: &'static str, pub response_payload_type: ProviderResponsePayloadType, pub error_payload_type: ProviderResponsePayloadType, - pub capability: &'static str, - pub method: &'static str, } impl ProviderAttributes { pub fn get(module: &str) -> Option<&'static ProviderAttributes> { match module { "AcknowledgeChallenge" => Some(&ACKNOWLEDGE_CHALLENGE_ATTRIBS), - "PinChallenge" => Some(&ACKNOWLEDGE_CHALLENGE_ATTRIBS), + "PinChallenge" => Some(&PIN_CHALLENGE_ATTRIBS), _ => None, } } } pub const ACKNOWLEDGE_CHALLENGE_ATTRIBS: ProviderAttributes = ProviderAttributes { - event: ACK_CHALLENGE_EVENT, response_payload_type: ProviderResponsePayloadType::ChallengeResponse, error_payload_type: ProviderResponsePayloadType::ChallengeError, - capability: ACK_CHALLENGE_CAPABILITY, - method: "challenge", }; pub const PIN_CHALLENGE_ATTRIBS: ProviderAttributes = ProviderAttributes { - event: PIN_CHALLENGE_EVENT, response_payload_type: ProviderResponsePayloadType::PinChallengeResponse, error_payload_type: ProviderResponsePayloadType::ChallengeError, - capability: PIN_CHALLENGE_CAPABILITY, - method: "challenge", }; #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/core/sdk/src/api/manifest/app_library.rs b/core/sdk/src/api/manifest/app_library.rs index 4e713385e..b4d452c79 100644 --- a/core/sdk/src/api/manifest/app_library.rs +++ b/core/sdk/src/api/manifest/app_library.rs @@ -44,7 +44,7 @@ pub struct AppLibrary {} impl AppLibraryState { pub fn new(default_apps: Vec) -> AppLibraryState { - let providers = AppLibrary::generate_provider_map(&default_apps); + let providers = AppLibrary::generate_provider_relation_map(&default_apps); AppLibraryState { default_apps, providers, @@ -90,7 +90,7 @@ impl AppLibrary { } } - fn generate_provider_map(apps: &[AppLibraryEntry]) -> HashMap { + fn generate_provider_relation_map(apps: &[AppLibraryEntry]) -> HashMap { let mut map = HashMap::new(); for app in apps.iter() { @@ -103,7 +103,10 @@ impl AppLibrary { map.insert(capability.clone(), app.app_id.clone()); } } else { - warn!("generate_provider_map: Not supported: {:?}", app.manifest); + warn!( + "generate_provider_relation_map: Not supported: {:?}", + app.manifest + ); } } @@ -137,7 +140,7 @@ mod tests { let app_library_state = AppLibraryState::new(default_apps.clone()); assert_eq!(app_library_state.default_apps, default_apps); - let providers = AppLibrary::generate_provider_map(&default_apps); + let providers = AppLibrary::generate_provider_relation_map(&default_apps); assert_eq!(app_library_state.providers, providers); }