Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
341 changes: 149 additions & 192 deletions docs/openrpc/the-spec/firebolt-open-rpc.json

Large diffs are not rendered by default.

28 changes: 23 additions & 5 deletions include/firebolt/actions.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,29 +26,47 @@
#include <functional>
#include <optional>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>

namespace Firebolt::Actions
{

struct IntentContext
{
std::optional<std::string> source;
};

struct IntentData
{
std::string action;
std::optional<IntentContext> context;
};

struct Intent
{
IntentData intent;
uint32_t intentId{0};
};

class IActions
{
public:
virtual ~IActions() = default;

virtual Result<std::string> intent() const = 0;
virtual Result<Intent> intent() const = 0;

virtual Result<SubscriptionId> subscribeOnIntent(std::function<void(const std::string&)>&& notification) = 0;
virtual Result<SubscriptionId> subscribeOnIntentChanged(std::function<void(const std::string&)>&& notification)
virtual Result<SubscriptionId> subscribeOnIntent(std::function<void(const Intent&)>&& notification) = 0;
virtual Result<SubscriptionId> subscribeOnIntentChanged(std::function<void(const Intent&)>&& notification)
Comment on lines +56 to +59
{
return subscribeOnIntent(std::move(notification));
}

virtual Result<void> unsubscribe(SubscriptionId id) = 0;
virtual void unsubscribeAll() = 0;

virtual Result<void> start(const IntentData& intent,
std::optional<std::string> handlerAppId = std::nullopt) const = 0;

}; // class IActions

} // namespace Firebolt::Actions
Expand Down
23 changes: 19 additions & 4 deletions src/actions_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,29 @@ ActionsImpl::ActionsImpl(Firebolt::Helpers::IHelper& helper)
{
}

Result<std::string> ActionsImpl::intent() const
Result<Intent> ActionsImpl::intent() const
{
return helper_.get<JsonData::JsonString, std::string>("Actions.intent");
return helper_.get<JsonData::JsonValue, Intent>("Actions.intent");
}

Result<SubscriptionId> ActionsImpl::subscribeOnIntent(std::function<void(const std::string&)>&& notification)
Result<SubscriptionId> ActionsImpl::subscribeOnIntent(std::function<void(const Intent&)>&& notification)
{
return subscriptionManager_.subscribe<JsonData::JsonString>("Actions.onIntent", std::move(notification));
return subscriptionManager_.subscribe<JsonData::JsonValue>("Actions.onIntent", std::move(notification));
}

Result<void> ActionsImpl::start(const IntentData& intent, std::optional<std::string> handlerAppId) const
{
nlohmann::json params;
params["intent"]["action"] = intent.action;
if (intent.context && intent.context->source)
{
params["intent"]["context"]["source"] = *intent.context->source;
}
if (handlerAppId)
{
params["handlerAppId"] = *handlerAppId;
}
return helper_.invoke("Actions.start", params);
}

Result<void> ActionsImpl::unsubscribe(SubscriptionId id)
Expand Down
6 changes: 4 additions & 2 deletions src/actions_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ class ActionsImpl : public IActions
ActionsImpl& operator=(const ActionsImpl&) = delete;
~ActionsImpl() override = default;

Result<std::string> intent() const override;
Result<Intent> intent() const override;

Result<SubscriptionId> subscribeOnIntent(std::function<void(const std::string&)>&& notification) override;
Result<SubscriptionId> subscribeOnIntent(std::function<void(const Intent&)>&& notification) override;

Result<void> start(const IntentData& intent, std::optional<std::string> handlerAppId = std::nullopt) const override;

Result<void> unsubscribe(SubscriptionId id) override;
void unsubscribeAll() override;
Expand Down
33 changes: 24 additions & 9 deletions src/json_types/actions.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,41 @@
#include "firebolt/actions.h"
#include <firebolt/json_types.h>
#include <nlohmann/json.hpp>
#include <type_traits>
#include <stdexcept>

namespace Firebolt::Actions
{

namespace JsonData
{

// Serialises any JSON value (object, string, …) to its compact JSON text
// representation. Used for Actions.intent / Actions.onIntent whose wire format
// is the object {"intent":"...","intentId":N} but whose public C++ API surface
// exposes the whole document as a std::string, per the Firebolt 9 spec.
class JsonString : public Firebolt::JSON::NL_Json_Basic<std::string>
// Deserialises the wire object {"intent":{"action":"...","context":{"source":"..."}},"intentId":N}
// into Firebolt::Actions::Intent. nlohmann stays hidden in this impl-layer header.
class JsonValue : public Firebolt::JSON::NL_Json_Basic<Intent>
{
public:
void fromJson(const nlohmann::json& json) override { value_ = json.dump(); }
std::string value() const override { return value_; }
void fromJson(const nlohmann::json& json) override
{
value_ = {};
if (!checkRequiredFields(json, {"intent", "intentId"}) || !json["intent"].is_object() ||
!checkRequiredFields(json["intent"], {"action"}))
{
throw std::invalid_argument("Missing required fields in JSON");
}
value_.intent.action = json["intent"]["action"].get<std::string>();
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<std::string>();
value_.intent.context = ctx;
}
value_.intentId = json["intentId"].get<uint32_t>();
}
Intent value() const override { return value_; }

private:
std::string value_;
Intent value_;
};

} // namespace JsonData
Expand Down
108 changes: 108 additions & 0 deletions test/api_test_app/apis/actionsDemo.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* Copyright 2026 Comcast Cable Communications Management, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/

#include "actionsDemo.h"
#include <firebolt/firebolt.h>
#include <iostream>
#include <string>
#include <utility>
Comment on lines +19 to +23

using namespace Firebolt;
using namespace Firebolt::Actions;

ActionsDemo::ActionsDemo()
: DemoBase("Actions")
{
methods_.push_back("Actions.intent");
methods_.push_back("Actions.start");
methods_.push_back("Actions.onIntent");
methods_.push_back("Actions.unsubscribe");
methods_.push_back("Actions.unsubscribeAll");
}

void ActionsDemo::runOption(const std::string& method)
{
std::cout << "Running Actions method: " << method << std::endl;

if (method == "Actions.intent")
{
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;
}
}
else if (method == "Actions.start")
{
std::string actionStr = paramFromConsole("action", "pre-load");
std::string sourceStr = paramFromConsole("context.source (leave empty to skip)", "system");
std::string handlerAppIdStr = paramFromConsole("handlerAppId (leave empty to skip)", "");
std::optional<std::string> handlerAppId;
if (!handlerAppIdStr.empty())
handlerAppId = handlerAppIdStr;
Firebolt::Actions::IntentData intentData{actionStr};
if (!sourceStr.empty())
intentData.context = Firebolt::Actions::IntentContext{sourceStr};
auto r = Firebolt::IFireboltAccessor::Instance().ActionsInterface().start(intentData, handlerAppId);
if (succeed(r))
{
std::cout << "Actions.start: Success" << std::endl;
}
}
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;
};
auto r = Firebolt::IFireboltAccessor::Instance().ActionsInterface().subscribeOnIntent(std::move(callback));
if (succeed(r))
{
std::cout << "Subscribed to Actions.onIntent with Subscription ID: " << *r << std::endl;
}
}
else if (method == "Actions.unsubscribe")
{
std::string idStr = paramFromConsole("subscription ID", "0");
SubscriptionId id = 0;
try
{
id = static_cast<SubscriptionId>(std::stoul(idStr));
}
catch (const std::exception&)
{
}
auto r = Firebolt::IFireboltAccessor::Instance().ActionsInterface().unsubscribe(id);
if (succeed(r))
{
std::cout << "Unsubscribed from Actions subscription " << id << std::endl;
}
}
else if (method == "Actions.unsubscribeAll")
{
Firebolt::IFireboltAccessor::Instance().ActionsInterface().unsubscribeAll();
std::cout << "Unsubscribed from all Actions subscriptions" << std::endl;
}
}
30 changes: 30 additions & 0 deletions test/api_test_app/apis/actionsDemo.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Copyright 2026 Comcast Cable Communications Management, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/

#pragma once

#include "utils.h"
#include <string>

class ActionsDemo : public DemoBase
{
public:
ActionsDemo();
~ActionsDemo() = default;
void runOption(const std::string& method) override;
};
6 changes: 4 additions & 2 deletions test/api_test_app/apis/lifecycleDemo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,17 @@ void LifecycleDemo::runOption(const std::string& method)
Firebolt::IFireboltAccessor::Instance().LifecycleInterface().subscribeOnStateChanged(std::move(callback));
if (succeed(r))
{
lastSubscriptionId_ = *r;
std::cout << "Subscribed to Lifecycle state changes with Subscription ID: " << *r << std::endl;
}
}
else if (method == "Lifecycle2.unsubscribe")
{
SubscriptionId id = 0;
SubscriptionId id = lastSubscriptionId_;
try
{
id = static_cast<SubscriptionId>(std::stoul(paramFromConsole("Subscription ID to unsubscribe", "0")));
id = static_cast<SubscriptionId>(
std::stoul(paramFromConsole("Subscription ID to unsubscribe", std::to_string(lastSubscriptionId_))));
}
catch (const std::exception&)
{
Expand Down
1 change: 1 addition & 0 deletions test/api_test_app/apis/lifecycleDemo.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ class LifecycleDemo : public DemoBase

private:
Firebolt::Lifecycle::LifecycleState currentState_;
Firebolt::SubscriptionId lastSubscriptionId_{0};
};
2 changes: 2 additions & 0 deletions test/api_test_app/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/

#include "accessibilityDemo.h"
#include "actionsDemo.h"
#include "advertisingDemo.h"
#include "deviceDemo.h"
#include "discoveryDemo.h"
Expand Down Expand Up @@ -157,6 +158,7 @@ int main(int argc, char** argv)
std::vector<std::unique_ptr<DemoBase>> interfaces;

interfaces.emplace_back(std::make_unique<AccessibilityDemo>());
interfaces.emplace_back(std::make_unique<ActionsDemo>());
interfaces.emplace_back(std::make_unique<AdvertisingDemo>());
interfaces.emplace_back(std::make_unique<DeviceDemo>());
interfaces.emplace_back(std::make_unique<DiscoveryDemo>());
Expand Down
27 changes: 19 additions & 8 deletions test/component/actionsGeneratedTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,23 @@ TEST_F(ActionsGeneratedCTest, Intent)
{
auto result = Firebolt::IFireboltAccessor::Instance().ActionsInterface().intent();
ASSERT_TRUE(result) << toError(result);
auto parsed = nlohmann::json::parse(*result);
EXPECT_EQ(parsed.at("intent").get<std::string>(), "launch");
EXPECT_EQ(parsed.at("intentId").get<int>(), 1);
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->intentId, 0u);
}

TEST_F(ActionsGeneratedCTest, SubscribeOnIntent)
{
auto id = Firebolt::IFireboltAccessor::Instance().ActionsInterface().subscribeOnIntent(
[&](const std::string& intent)
[&](const Firebolt::Actions::Intent& payload)
{
auto parsed = nlohmann::json::parse(intent);
EXPECT_EQ(parsed.at("intent").get<std::string>(), "launch");
EXPECT_EQ(parsed.at("intentId").get<int>(), 1);
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(payload.intentId, 0u);
{
std::lock_guard<std::mutex> lock(mtx);
eventReceived = true;
Expand All @@ -59,9 +63,16 @@ TEST_F(ActionsGeneratedCTest, SubscribeOnIntent)
ASSERT_TRUE(id) << toError(id);
verifyEventSubscription(id);

triggerEvent("Actions.onIntent", R"({"intent":"launch","intentId":1})");
triggerEvent("Actions.onIntent", R"({"intent":{"action":"pre-load","context":{"source":"system"}},"intentId":0})");
verifyEventReceived(mtx, cv, eventReceived);

auto result = Firebolt::IFireboltAccessor::Instance().ActionsInterface().unsubscribe(id.value());
verifyUnsubscribeResult(result);
}

TEST_F(ActionsGeneratedCTest, Start)
{
auto result = Firebolt::IFireboltAccessor::Instance().ActionsInterface().start(
Firebolt::Actions::IntentData{"pre-load", Firebolt::Actions::IntentContext{{"system"}}});
ASSERT_TRUE(result) << toError(result);
}
Loading
Loading