diff --git a/CHANGELOG.md b/CHANGELOG.md index 2db38e8..dee69da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## [0.7.1](https://github.com/rdkcentral/firebolt-cpp-client/compare/v0.7.0...v0.7.1) + +### Added +- `Device.name` getter and `onNameChanged` event + +### Changed +- **Breaking**: `Actions.start()` now takes the intent as a JSON string (`const std::string&`) instead of `const IntentData&`; malformed JSON returns `Error::InvalidParams` instead of throwing +- **Breaking**: `Firebolt::Actions::IntentData` and `IntentContext` have been removed; `Intent::intent` is now a `std::string` holding a JSON-serialized representation of the payload + +### Fixed +- `TextToSpeech` event subscriptions (`onWillSpeak`, `onSpeechStart`, `onSpeechPause`, `onSpeechResume`, `onSpeechComplete`, `onSpeechInterrupted`, `onNetworkError`, etc.) used incorrect lowercase wire method names and never received platform events + ## [0.7.0](https://github.com/rdkcentral/firebolt-cpp-client/compare/v0.6.4...v0.7.0) ### Added diff --git a/docs/openrpc/the-spec/firebolt-open-rpc.json b/docs/openrpc/the-spec/firebolt-open-rpc.json index 8ded70c..16e6e03 100644 --- a/docs/openrpc/the-spec/firebolt-open-rpc.json +++ b/docs/openrpc/the-spec/firebolt-open-rpc.json @@ -75,25 +75,7 @@ "intentId" ], "properties": { - "intent": { - "type": "object", - "required": [ - "action" - ], - "properties": { - "action": { - "type": "string" - }, - "context": { - "type": "object", - "properties": { - "source": { - "type": "string" - } - } - } - } - }, + "intent": {}, "intentId": { "type": "integer", "minimum": 0 @@ -111,6 +93,10 @@ "action": "pre-load", "context": { "source": "system" + }, + "data": { + "ids": [1, "two", true, null], + "future": {"enabled": false} } }, "intentId": 0 @@ -153,25 +139,7 @@ "intentId" ], "properties": { - "intent": { - "type": "object", - "required": [ - "action" - ], - "properties": { - "action": { - "type": "string" - }, - "context": { - "type": "object", - "properties": { - "source": { - "type": "string" - } - } - } - } - }, + "intent": {}, "intentId": { "type": "integer", "minimum": 0 @@ -195,6 +163,10 @@ "action": "pre-load", "context": { "source": "system" + }, + "data": { + "ids": [1, "two", true, null], + "future": {"enabled": false} } }, "intentId": 0 @@ -219,25 +191,7 @@ "name": "intent", "summary": "The intent to send, as a JSON document.", "required": true, - "schema": { - "type": "object", - "required": [ - "action" - ], - "properties": { - "action": { - "type": "string" - }, - "context": { - "type": "object", - "properties": { - "source": { - "type": "string" - } - } - } - } - } + "schema": {} }, { "name": "handlerAppId", @@ -264,6 +218,10 @@ "action": "pre-load", "context": { "source": "system" + }, + "data": { + "ids": [1, "two", true, null], + "future": {"enabled": false} } } } @@ -2685,7 +2643,7 @@ { "name": "TextToSpeech.speak", "summary": "Speak the utterance immediately. Any ongoing speech is interrupted.", - "description": "Text argument is either plain text or a well-formed SSML document TTS_status, not success attribute, to be used by caller to indicate success of call 0 OK, 1 Fail, 2 not enabled, 3 invalid configuration Raises onSpeechinterrupted if speaking is interrupted", + "description": "Text argument is either plain text or a well-formed SSML document TTS_status, not success attribute, to be used by caller to indicate success of call 0 OK, 1 Fail, 2 not enabled, 3 invalid configuration Raises onSpeechInterrupted if speaking is interrupted", "params": [ { "name": "text", @@ -2803,7 +2761,7 @@ { "name": "TextToSpeech.pause", "summary": "Pauses the speech for given speech id", - "description": "Pauses the utterance. Raises onSpeechpause if ongoing speech is paused. Does nothing if utterance is already paused", + "description": "Pauses the utterance. Raises onSpeechPause if ongoing speech is paused. Does nothing if utterance is already paused", "params": [ { "name": "speechid", @@ -2854,7 +2812,7 @@ { "name": "TextToSpeech.resume", "summary": "Resumes the speech for given speech id", - "description": "Continue the paused utterance. Raises onSpeechresume if paused speech is resumed. Does nothing if the utterance is not paused", + "description": "Continue the paused utterance. Raises onSpeechResume if paused speech is resumed. Does nothing if the utterance is not paused", "params": [ { "name": "speechid", @@ -2905,7 +2863,7 @@ { "name": "TextToSpeech.cancel", "summary": "Cancels the speech for given speech id", - "description": "Stop speaking if utterance is currently being spoken. Raises onSpeechinterrupted if speaking was interrupted.", + "description": "Stop speaking if utterance is currently being spoken. Raises onSpeechInterrupted if speaking was interrupted.", "params": [ { "name": "speechid", @@ -3526,14 +3484,16 @@ ], "result": { "name": "result", - "value": null + "value": { + "speechid": 1 + } } } ], "result": { "name": "result", "schema": { - "type": "null" + "$ref": "#/components/schemas/SpeechIdEvent" } } }, @@ -3574,14 +3534,16 @@ ], "result": { "name": "result", - "value": null + "value": { + "speechid": 1 + } } } ], "result": { "name": "result", "schema": { - "type": "null" + "$ref": "#/components/schemas/SpeechIdEvent" } } }, @@ -3622,14 +3584,16 @@ ], "result": { "name": "result", - "value": null + "value": { + "speechid": 1 + } } } ], "result": { "name": "result", "schema": { - "type": "null" + "$ref": "#/components/schemas/SpeechIdEvent" } } }, @@ -3670,14 +3634,16 @@ ], "result": { "name": "result", - "value": null + "value": { + "speechid": 1 + } } } ], "result": { "name": "result", "schema": { - "type": "null" + "$ref": "#/components/schemas/SpeechIdEvent" } } }, @@ -3718,14 +3684,16 @@ ], "result": { "name": "result", - "value": null + "value": { + "speechid": 1 + } } } ], "result": { "name": "result", "schema": { - "type": "null" + "$ref": "#/components/schemas/SpeechIdEvent" } } }, @@ -3766,14 +3734,16 @@ ], "result": { "name": "result", - "value": null + "value": { + "speechid": 1 + } } } ], "result": { "name": "result", "schema": { - "type": "null" + "$ref": "#/components/schemas/SpeechIdEvent" } } }, @@ -3814,14 +3784,16 @@ ], "result": { "name": "result", - "value": null + "value": { + "speechid": 1 + } } } ], "result": { "name": "result", "schema": { - "type": "null" + "$ref": "#/components/schemas/SpeechIdEvent" } } }, @@ -3862,14 +3834,16 @@ ], "result": { "name": "result", - "value": null + "value": { + "speechid": 1 + } } } ], "result": { "name": "result", "schema": { - "type": "null" + "$ref": "#/components/schemas/SpeechIdEvent" } } }, diff --git a/include/firebolt/actions.h b/include/firebolt/actions.h index 543320c..3f42132 100644 --- a/include/firebolt/actions.h +++ b/include/firebolt/actions.h @@ -15,10 +15,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -// -// ============================================================================ -// AUTO-GENERATED by fb-gen — DO NOT EDIT -// ============================================================================ + #ifndef FIREBOLT_ACTIONS_H #define FIREBOLT_ACTIONS_H @@ -31,20 +28,10 @@ namespace Firebolt::Actions { -struct IntentContext -{ - std::optional source; -}; - -struct IntentData -{ - std::string action; - std::optional context; -}; - +// intent holds a JSON-serialized representation of the payload received from or sent to the platform. struct Intent { - IntentData intent; + std::string intent; uint32_t intentId{0}; }; @@ -64,7 +51,7 @@ class IActions virtual Result unsubscribe(SubscriptionId id) = 0; virtual void unsubscribeAll() = 0; - [[nodiscard]] virtual Result start(const IntentData& intent, + [[nodiscard]] virtual Result start(const std::string& intent, std::optional handlerAppId = std::nullopt) const = 0; }; // class IActions diff --git a/src/actions_impl.cpp b/src/actions_impl.cpp index 887fb55..e74073d 100644 --- a/src/actions_impl.cpp +++ b/src/actions_impl.cpp @@ -15,10 +15,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -// -// ============================================================================ -// AUTO-GENERATED by fb-gen — DO NOT EDIT -// ============================================================================ + #include "actions_impl.h" #include "json_types/actions.h" #include @@ -42,14 +39,20 @@ Result ActionsImpl::subscribeOnIntent(std::function("Actions.onIntent", std::move(notification)); } -Result ActionsImpl::start(const IntentData& intent, std::optional handlerAppId) const +Result ActionsImpl::start(const std::string& intent, std::optional handlerAppId) const { - nlohmann::json params; - params["intent"]["action"] = intent.action; - if (intent.context && intent.context->source) + nlohmann::json intentJson; + try + { + intentJson = nlohmann::json::parse(intent); + } + catch (const nlohmann::json::parse_error&) { - params["intent"]["context"]["source"] = *intent.context->source; + return Result{Error::InvalidParams}; } + + nlohmann::json params; + params["intent"] = std::move(intentJson); if (handlerAppId) { params["handlerAppId"] = *handlerAppId; diff --git a/src/actions_impl.h b/src/actions_impl.h index a61d03b..c3b8160 100644 --- a/src/actions_impl.h +++ b/src/actions_impl.h @@ -15,10 +15,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -// -// ============================================================================ -// AUTO-GENERATED by fb-gen — DO NOT EDIT -// ============================================================================ + #ifndef FIREBOLT_ACTIONS_IMPL_H #define FIREBOLT_ACTIONS_IMPL_H @@ -32,6 +29,7 @@ class ActionsImpl : public IActions { public: explicit ActionsImpl(Firebolt::Helpers::IHelper& helper); + ActionsImpl(const ActionsImpl&) = delete; ActionsImpl& operator=(const ActionsImpl&) = delete; ~ActionsImpl() override = default; @@ -40,7 +38,7 @@ class ActionsImpl : public IActions Result subscribeOnIntent(std::function&& notification) override; - [[nodiscard]] Result start(const IntentData& intent, + [[nodiscard]] Result start(const std::string& intent, std::optional handlerAppId = std::nullopt) const override; Result unsubscribe(SubscriptionId id) override; diff --git a/src/json_types/actions.h b/src/json_types/actions.h index 0310384..a49902b 100644 --- a/src/json_types/actions.h +++ b/src/json_types/actions.h @@ -15,10 +15,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -// -// ============================================================================ -// AUTO-GENERATED by fb-gen — DO NOT EDIT -// ============================================================================ + #ifndef FIREBOLT_ACTIONS_JSON_H #define FIREBOLT_ACTIONS_JSON_H @@ -31,29 +28,15 @@ namespace Firebolt::Actions::JsonData { -// Deserialises the wire object {"intent":{"action":"...","context":{"source":"..."}},"intentId":N} -// into Firebolt::Actions::Intent. nlohmann stays hidden in this impl-layer header. +// Deserialises the intent envelope while preserving the intent payload as raw JSON text. class JsonValue : public Firebolt::JSON::NL_Json_Basic { public: void fromJson(const nlohmann::json& json) override { - value_ = {}; - if (!checkRequiredFields(json, {"intent", "intentId"}) || !json["intent"].is_object() || - !checkRequiredFields(json["intent"], {"action"})) - { + if (!checkRequiredFields(json, {"intent", "intentId"})) throw std::invalid_argument("Missing required fields in JSON"); - } - value_.intent.action = json["intent"]["action"].get(); - if (json["intent"].contains("context") && json["intent"]["context"].is_object()) - { - IntentContext ctx; - if (json["intent"]["context"].contains("source")) - { - ctx.source = json["intent"]["context"]["source"].get(); - } - value_.intent.context = ctx; - } + value_.intent = json["intent"].dump(); value_.intentId = json["intentId"].get(); } [[nodiscard]] Intent value() const override { return value_; } diff --git a/test/api_test_app/apis/actionsDemo.cpp b/test/api_test_app/apis/actionsDemo.cpp index 31b982f..22ac05c 100644 --- a/test/api_test_app/apis/actionsDemo.cpp +++ b/test/api_test_app/apis/actionsDemo.cpp @@ -19,6 +19,7 @@ #include "actionsDemo.h" #include #include +#include #include #include @@ -44,9 +45,7 @@ void ActionsDemo::runOption(const std::string& method) auto r = Firebolt::IFireboltAccessor::Instance().ActionsInterface().intent(); if (succeed(r)) { - std::cout << "Current Intent - action: " << r->intent.action << ", source: " - << (r->intent.context && r->intent.context->source ? *r->intent.context->source : "(none)") - << ", intentId: " << r->intentId << std::endl; + std::cout << "Current Intent: " << r->intent << ", intentId: " << r->intentId << std::endl; } } else if (method == "Actions.start") @@ -57,10 +56,13 @@ void ActionsDemo::runOption(const std::string& method) std::optional handlerAppId; if (!handlerAppIdStr.empty()) handlerAppId = handlerAppIdStr; - Firebolt::Actions::IntentData intentData{actionStr}; + nlohmann::json intentData = {{"action", actionStr}, + {"data", + {{"route", nlohmann::json::array({"home", "featured"})}, + {"metadata", {{"source", "demo"}}}}}}; if (!sourceStr.empty()) - intentData.context = Firebolt::Actions::IntentContext{sourceStr}; - auto r = Firebolt::IFireboltAccessor::Instance().ActionsInterface().start(intentData, handlerAppId); + intentData["context"]["source"] = sourceStr; + auto r = Firebolt::IFireboltAccessor::Instance().ActionsInterface().start(intentData.dump(), handlerAppId); if (succeed(r)) { std::cout << "Actions.start: Success" << std::endl; @@ -69,12 +71,7 @@ void ActionsDemo::runOption(const std::string& method) else if (method == "Actions.onIntent") { auto callback = [&](const Intent& payload) - { - std::cout << "Intent received - action: " << payload.intent.action << ", source: " - << (payload.intent.context && payload.intent.context->source ? *payload.intent.context->source - : "(none)") - << ", intentId: " << payload.intentId << std::endl; - }; + { std::cout << "Intent received: " << payload.intent << ", intentId: " << payload.intentId << std::endl; }; auto r = Firebolt::IFireboltAccessor::Instance().ActionsInterface().subscribeOnIntent(std::move(callback)); if (succeed(r)) { diff --git a/test/component/actionsGeneratedTest.cpp b/test/component/actionsGeneratedTest.cpp index ceb55a7..9a8bc46 100644 --- a/test/component/actionsGeneratedTest.cpp +++ b/test/component/actionsGeneratedTest.cpp @@ -21,6 +21,7 @@ #include #include #include +#include class ActionsGeneratedCTest : public ::testing::Test { @@ -34,24 +35,22 @@ class ActionsGeneratedCTest : public ::testing::Test TEST_F(ActionsGeneratedCTest, Intent) { + const auto expectedIntent = nlohmann::json::parse( + R"({"action":"pre-load","context":{"source":"system"},"data":{"ids":[1,"two",true,null],"future":{"enabled":false}}})"); auto result = Firebolt::IFireboltAccessor::Instance().ActionsInterface().intent(); ASSERT_TRUE(result) << toError(result); - EXPECT_EQ(result->intent.action, "pre-load"); - ASSERT_TRUE(result->intent.context); - ASSERT_TRUE(result->intent.context->source); - EXPECT_EQ(*result->intent.context->source, "system"); + EXPECT_EQ(nlohmann::json::parse(result->intent), expectedIntent); EXPECT_EQ(result->intentId, 0U); } TEST_F(ActionsGeneratedCTest, SubscribeOnIntent) { + const auto expectedIntent = nlohmann::json::parse( + R"({"action":"pre-load","context":{"source":"system"},"data":{"ids":[1,"two",true,null],"future":{"enabled":false}}})"); auto id = Firebolt::IFireboltAccessor::Instance().ActionsInterface().subscribeOnIntent( [&](const Firebolt::Actions::Intent& payload) { - EXPECT_EQ(payload.intent.action, "pre-load"); - ASSERT_TRUE(payload.intent.context); - ASSERT_TRUE(payload.intent.context->source); - EXPECT_EQ(*payload.intent.context->source, "system"); + EXPECT_EQ(nlohmann::json::parse(payload.intent), expectedIntent); EXPECT_EQ(payload.intentId, 0U); { std::lock_guard lock(mtx); @@ -63,7 +62,8 @@ TEST_F(ActionsGeneratedCTest, SubscribeOnIntent) ASSERT_TRUE(id) << toError(id); verifyEventSubscription(id); - triggerEvent("Actions.onIntent", R"({"intent":{"action":"pre-load","context":{"source":"system"}},"intentId":0})"); + triggerEvent("Actions.onIntent", + R"({"intent":{"action":"pre-load","context":{"source":"system"},"data":{"ids":[1,"two",true,null],"future":{"enabled":false}}},"intentId":0})"); verifyEventReceived(mtx, cv, eventReceived); auto result = Firebolt::IFireboltAccessor::Instance().ActionsInterface().unsubscribe(id.value()); @@ -72,7 +72,8 @@ TEST_F(ActionsGeneratedCTest, SubscribeOnIntent) TEST_F(ActionsGeneratedCTest, Start) { - auto result = Firebolt::IFireboltAccessor::Instance().ActionsInterface().start( - Firebolt::Actions::IntentData{"pre-load", Firebolt::Actions::IntentContext{{"system"}}}); + const auto intentPayload = + R"({"action":"pre-load","context":{"source":"system"},"data":{"ids":[1,"two",true,null],"future":{"enabled":false}}})"; + auto result = Firebolt::IFireboltAccessor::Instance().ActionsInterface().start(intentPayload); ASSERT_TRUE(result) << toError(result); } diff --git a/test/unit/actionsGeneratedTest.cpp b/test/unit/actionsGeneratedTest.cpp index a9f8ba7..4730244 100644 --- a/test/unit/actionsGeneratedTest.cpp +++ b/test/unit/actionsGeneratedTest.cpp @@ -54,3 +54,32 @@ TEST_F(ActionsGeneratedUTest, ForwardsIntentTransportErrors) auto result = impl.intent(); EXPECT_FALSE(result) << "Expected error propagation when helper getJson fails"; } + +TEST_F(ActionsGeneratedUTest, SubscribeOnIntentDispatchesGenericPayload) +{ + bool notified = false; + const auto expectedIntent = + nlohmann::json::parse(R"({"action":"pre-load","data":{"ids":[1,"two",true,null],"future":{"enabled":false}}})"); + + EXPECT_CALL(mockHelper, subscribe(&impl, "Actions.onIntent", ::testing::_, ::testing::_)) + .WillOnce(::testing::Invoke( + [&](void* owner, const std::string& eventName, std::any&& notification, + void (*callback)(void*, const nlohmann::json&)) + { + Firebolt::Helpers::SubscriptionData data{owner, eventName, std::move(notification)}; + callback(&data, {{"intent", expectedIntent}, {"intentId", 11U}}); + return Firebolt::Result{99}; + })); + + auto result = impl.subscribeOnIntent( + [&](const Firebolt::Actions::Intent& intent) + { + notified = true; + EXPECT_EQ(intent.intent, expectedIntent.dump()); + EXPECT_EQ(intent.intentId, 11U); + }); + + ASSERT_TRUE(result); + EXPECT_EQ(*result, 99U); + EXPECT_TRUE(notified); +} diff --git a/test/unit/actionsTest.cpp b/test/unit/actionsTest.cpp index 98936f7..28a78b1 100644 --- a/test/unit/actionsTest.cpp +++ b/test/unit/actionsTest.cpp @@ -30,19 +30,27 @@ class ActionsUTest : public ::testing::Test, protected MockBase TEST_F(ActionsUTest, Intent) { - mock_with_response("Actions.intent", - nlohmann::json({{"intent", {{"action", "pre-load"}, {"context", {{"source", "system"}}}}}, - {"intentId", 0U}})); + constexpr auto intentJsonText = + R"({"action":"pre-load","context":{"source":"system"},"data":{"ids":[1,"two",true,null],"future":{"enabled":false}}})"; + const auto expectedIntent = nlohmann::json::parse(intentJsonText); + mock_with_response("Actions.intent", {{"intent", expectedIntent}, {"intentId", 0U}}); auto result = actionsImpl_.intent(); ASSERT_TRUE(result) << "ActionsImpl::intent() returned an error"; - EXPECT_EQ(result->intent.action, "pre-load"); - ASSERT_TRUE(result->intent.context); - ASSERT_TRUE(result->intent.context->source); - EXPECT_EQ(*result->intent.context->source, "system"); + EXPECT_EQ(result->intent, expectedIntent.dump()); EXPECT_EQ(result->intentId, 0U); } +TEST_F(ActionsUTest, IntentPreservesNonObjectPayload) +{ + const auto expectedIntent = nlohmann::json::array({"future-intent", 42, true, nullptr, {{"nested", {1, 2}}}}); + mock_with_response("Actions.intent", {{"intent", expectedIntent}, {"intentId", 1U}}); + + auto result = actionsImpl_.intent(); + ASSERT_TRUE(result) << "ActionsImpl::intent() returned an error"; + EXPECT_EQ(result->intent, expectedIntent.dump()); +} + TEST_F(ActionsUTest, SubscribeOnIntent) { nlohmann::json expectedValue = 1; @@ -59,13 +67,35 @@ TEST_F(ActionsUTest, SubscribeOnIntent) TEST_F(ActionsUTest, Start) { + constexpr auto intentJsonText = + R"({"action":"pre-load","data":{"ids":[1,"two",true,null],"future":{"enabled":false}}})"; + nlohmann::json expectedParams; + expectedParams["intent"] = nlohmann::json::parse(intentJsonText); + expectedParams["handlerAppId"] = "com.example.handler"; + EXPECT_CALL(mockHelper, invoke("Actions.start", expectedParams)) + .WillOnce(Invoke([&](const std::string& /*methodName*/, const nlohmann::json& /*parameters*/) + { return Firebolt::Result{Firebolt::Error::None}; })); + + auto result = actionsImpl_.start(intentJsonText, "com.example.handler"); + ASSERT_TRUE(result) << "ActionsImpl::start() returned an error"; +} + +TEST_F(ActionsUTest, StartPreservesNonObjectPayload) +{ + const auto intentJson = nlohmann::json::array({"future-intent", 42, true, nullptr, {{"nested", {1, 2}}}}); nlohmann::json expectedParams; - expectedParams["intent"] = {{"action", "pre-load"}, {"context", {{"source", "system"}}}}; + expectedParams["intent"] = intentJson; EXPECT_CALL(mockHelper, invoke("Actions.start", expectedParams)) .WillOnce(Invoke([&](const std::string& /*methodName*/, const nlohmann::json& /*parameters*/) { return Firebolt::Result{Firebolt::Error::None}; })); - auto result = - actionsImpl_.start(Firebolt::Actions::IntentData{"pre-load", Firebolt::Actions::IntentContext{{"system"}}}); + auto result = actionsImpl_.start(intentJson.dump()); ASSERT_TRUE(result) << "ActionsImpl::start() returned an error"; } + +TEST_F(ActionsUTest, StartRejectsMalformedJsonWithoutThrowing) +{ + auto result = actionsImpl_.start("{not valid json"); + ASSERT_FALSE(result) << "ActionsImpl::start() should reject malformed JSON"; + EXPECT_EQ(result.error(), Firebolt::Error::InvalidParams); +}