plugin: implement replaceRedfishHandler

This commit implements the Redfish Plugin macro:
  REDFISH_HANDLER_REPLACE

This plugin macro gives plugin user ability to replace an existing route
with a platform specific route. The plugin takes URL, Verb, and the new
callback.

Change-Id: I13d20b81635e64e9c74d9131c6e553daca6a77e8
Signed-off-by: Nan Zhou <nanzhou@google.com>
diff --git a/http/app.hpp b/http/app.hpp
index bd30007..019b5e7 100644
--- a/http/app.hpp
+++ b/http/app.hpp
@@ -126,6 +126,13 @@
         router.newRuleTagged<Tag>(url).methods(verb).privileges(p)(func);
     }
 
+    template <uint64_t Tag, typename Func>
+    void replaceHandler(std::string_view url, boost::beast::http::verb verb,
+                        Func&& func)
+    {
+        router.replaceHandler<Tag>(url, verb, func);
+    }
+
 #ifdef BMCWEB_ENABLE_SSL
     App& ssl(std::shared_ptr<boost::asio::ssl::context>&& ctx)
     {
diff --git a/http/routing.cpp b/http/routing.cpp
index dce69f6..c04f051 100644
--- a/http/routing.cpp
+++ b/http/routing.cpp
@@ -679,4 +679,15 @@
     return ret;
 }
 
+void Router::throwHandlerNotFound(std::string_view url,
+                                  boost::beast::http::verb boostVerb)
+{
+
+    std::string error = "handler doesn't exist for ";
+    error += url;
+    error += ", method ";
+    error += boostVerbToString(boostVerb);
+    throw std::runtime_error(error);
+}
+
 } // namespace crow
\ No newline at end of file
diff --git a/http/routing.hpp b/http/routing.hpp
index c60721c..92bbce8 100644
--- a/http/routing.hpp
+++ b/http/routing.hpp
@@ -694,6 +694,12 @@
         (*this).template operator()<Func>(std::forward(f));
     }
 
+    template <typename Func>
+    void replaceHandler(Func&& f)
+    {
+        this->operator()(f);
+    }
+
     void handle(const Request& req,
                 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
                 const RoutingParams& params) override
@@ -961,6 +967,28 @@
         });
     }
 
+    template <uint64_t Tag>
+    size_t findRuleByUrlAndVerb(std::string_view url,
+                                boost::beast::http::verb boostVerb)
+    {
+        std::optional<HttpVerb> verb = httpVerbFromBoost(boostVerb);
+        if (!verb)
+        {
+            return allRules.size();
+        }
+
+        for (size_t i = 0; i < allRules.size(); ++i)
+        {
+            if (allRules[i]->rule == url &&
+                (allRules[i]->methodsBitfield &
+                 (1 << static_cast<size_t>(*verb))) > 0)
+            {
+                return i;
+            }
+        }
+        return allRules.size();
+    }
+
     void handle(Request& req,
                 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp);
 
@@ -968,6 +996,24 @@
 
     std::vector<const std::string*> getRoutes(const std::string& parent);
 
+    void throwHandlerNotFound(std::string_view url,
+                              boost::beast::http::verb boostVerb);
+
+    template <uint64_t Tag, typename Func>
+    void replaceHandler(std::string_view url,
+                        boost::beast::http::verb boostVerb, Func&& func)
+    {
+        size_t index = findRuleByUrlAndVerb<Tag>(url, boostVerb);
+        if (index == allRules.size())
+        {
+            throwHandlerNotFound(url, boostVerb);
+        }
+        using RuleT =
+            black_magic::Arguments<Tag>::type::template rebind<TaggedRule>;
+        RuleT* rule = dynamic_cast<RuleT*>(allRules[index].get());
+        rule->replaceHandler(func);
+    }
+
   private:
     bool isUserPrivileged(Request& req,
                           const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
diff --git a/http/verb.hpp b/http/verb.hpp
index 4213604..bae3ec7 100644
--- a/http/verb.hpp
+++ b/http/verb.hpp
@@ -76,3 +76,13 @@
     // Should never reach here
     return "";
 }
+
+inline std::string_view boostVerbToString(boost::beast::http::verb bv)
+{
+    std::optional<HttpVerb> verb = httpVerbFromBoost(bv);
+    if (!verb)
+    {
+        return "";
+    }
+    return httpVerbToString(*verb);
+}
\ No newline at end of file
diff --git a/meson.build b/meson.build
index 0245754..911ad93 100644
--- a/meson.build
+++ b/meson.build
@@ -7,7 +7,6 @@
             'b_lto=true',
             'b_ndebug=if-release',
             'buildtype=debugoptimized',
-            'cpp_rtti=false',
             'cpp_std=c++20',
             'warning_level=3',
             'werror=true',
diff --git a/plugins/macros.hpp b/plugins/macros.hpp
index 0d90605..8d1f6c4 100644
--- a/plugins/macros.hpp
+++ b/plugins/macros.hpp
@@ -12,7 +12,12 @@
 // NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
 #define REDFISH_HANDLER_ADD(url, verb, privileges, func)                       \
     plugins::addRedfishHandler<crow::black_magic::getParameterTag(url)>(       \
-        url, verb, privileges, func);
+        url, verb, privileges, func)
+
+// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
+#define REDFISH_HANDLER_REPLACE(url, verb, func)                               \
+    plugins::replaceRedfishHandler<crow::black_magic::getParameterTag(url)>(   \
+        url, verb, func)
 
 namespace plugins
 {
@@ -23,4 +28,12 @@
 {
     crow::globalBmcWebApp->addHandler<tag>(url, verb, p, func);
 }
+
+template <uint64_t tag, typename RedfishHandler>
+void replaceRedfishHandler(std::string_view url, boost::beast::http::verb verb,
+                           RedfishHandler&& func)
+{
+    crow::globalBmcWebApp->replaceHandler<tag>(url, verb, func);
+}
+
 } // namespace plugins
\ No newline at end of file
diff --git a/plugins/macros_test.cpp b/plugins/macros_test.cpp
index ca15030..5133d2d 100644
--- a/plugins/macros_test.cpp
+++ b/plugins/macros_test.cpp
@@ -35,7 +35,7 @@
     std::unique_ptr<crow::App> app;
 };
 
-TEST_F(PluginMacrosTest, AddHandlerCanBeInvoked)
+TEST_F(PluginMacrosTest, AddedHandlerCanBeInvoked)
 {
     bool called = false;
     auto callback = [&called](const Request&,
@@ -60,8 +60,8 @@
 
 TEST_F(PluginMacrosTest, DuplicateHandlerThrows)
 {
-    auto callback =
-        [](const Request&, const std::shared_ptr<bmcweb::AsyncResp>&) {};
+    auto callback = [](const Request&,
+                       const std::shared_ptr<bmcweb::AsyncResp>&) {};
 
     REDFISH_HANDLER_ADD("/hello/", boost::beast::http::verb::get,
                         redfish::privileges::getChassis, std::move(callback));
@@ -71,5 +71,48 @@
     EXPECT_ANY_THROW(crow::globalBmcWebApp->validate());
 }
 
+TEST_F(PluginMacrosTest, ReplacedNonExistHandlerThrows)
+{
+    auto callback = [](const Request&,
+                       const std::shared_ptr<bmcweb::AsyncResp>&) {};
+    EXPECT_ANY_THROW(REDFISH_HANDLER_REPLACE("/hello/",
+                                             boost::beast::http::verb::get,
+                                             std::move(callback)););
+}
+
+TEST_F(PluginMacrosTest, ReplacedHandlerCanBeInvoked)
+{
+    bool originalCalled = false;
+    auto originalCallback =
+        [&originalCalled](const Request&,
+                          const std::shared_ptr<bmcweb::AsyncResp>&) {
+        originalCalled = true;
+    };
+
+    bool replacedCalled = false;
+    auto replacedCallback =
+        [&replacedCalled](const Request&,
+                          const std::shared_ptr<bmcweb::AsyncResp>&) {
+        replacedCalled = true;
+    };
+
+    REDFISH_HANDLER_ADD("/hello/", boost::beast::http::verb::get,
+                        redfish::privileges::getChassis,
+                        std::move(originalCallback));
+    REDFISH_HANDLER_REPLACE("/hello/", boost::beast::http::verb::get,
+                            std::move(replacedCallback));
+    crow::globalBmcWebApp->validate();
+
+    constexpr std::string_view url = "/hello/";
+    std::error_code ec;
+    Request req{{boost::beast::http::verb::get, url, 11}, ec};
+
+    auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
+    crow::globalBmcWebApp->handle(req, asyncResp);
+
+    EXPECT_FALSE(originalCalled);
+    EXPECT_TRUE(replacedCalled);
+}
+
 } // namespace
 } // namespace crow
\ No newline at end of file