From 9eb61cf885e33e7833ae62801ed90c30a7d562dd Mon Sep 17 00:00:00 2001 From: Davide Faconti Date: Wed, 6 Mar 2024 21:27:33 +0100 Subject: [PATCH] added JSON conversion to default values --- include/behaviortree_cpp/basic_types.h | 65 +++++++++++++++++--------- include/behaviortree_cpp/tree_node.h | 11 ++++- src/basic_types.cpp | 16 +++++++ tests/gtest_ports.cpp | 49 +++++++++++++++---- tests/gtest_preconditions.cpp | 44 +++++++++++++++++ 5 files changed, 151 insertions(+), 34 deletions(-) diff --git a/include/behaviortree_cpp/basic_types.h b/include/behaviortree_cpp/basic_types.h index de6dbd6ec..c8e099e19 100644 --- a/include/behaviortree_cpp/basic_types.h +++ b/include/behaviortree_cpp/basic_types.h @@ -58,20 +58,40 @@ enum class PortDirection using StringView = std::string_view; +bool StartWith(StringView str, StringView prefix); + // vector of key/value pairs using KeyValueVector = std::vector>; +/** Usage: given a function/method like this: + * + * Expected getAnswer(); + * + * User code can check result and error message like this: + * + * auto res = getAnswer(); + * if( res ) + * { + * std::cout << "answer was: " << res.value() << std::endl; + * } + * else{ + * std::cerr << "failed to get the answer: " << res.error() << std::endl; + * } + * + * */ +template +using Expected = nonstd::expected; struct AnyTypeAllowed {}; /** * @brief convertFromJSON will parse a json string and use JsonExporter - * to convert its content to a given type. it will work only if + * to convert its content to a given type. It will work only if * the type was previously registered. May throw if it fails. * * @param json_text a valid JSON string - * @param type you must specify the typeid() + * @param type you must specify the typeid() * @return the object, wrapped in Any. */ [[nodiscard]] Any convertFromJSON(StringView json_text, std::type_index type); @@ -89,13 +109,17 @@ inline T convertFromJSON(StringView str) * This function is invoked under the hood by TreeNode::getInput(), but only when the * input port contains a string. * - * If you have a custom type, you need to implement the corresponding template specialization. + * If you have a custom type, you need to implement the corresponding + * template specialization. + * + * If the string starts with the prefix "json:", it will + * fall back to convertFromJSON() */ template [[nodiscard]] inline T convertFromString(StringView str) { // if string starts with "json:{", try to parse it as json - if(str.size() > 6 && std::strncmp("json:{", str.data(), 6) == 0) + if(StartWith(str, "json:")) { str.remove_prefix(5); return convertFromJSON(str); @@ -196,6 +220,15 @@ constexpr bool IsConvertibleToString() std::is_convertible_v; } +Expected toJsonString(const Any &value); + + +/** + * @brief toStr is the reverse operation of convertFromString. + * + * If T is a custom type and there is no template specialization, + * it will try to fall back to toJsonString() + */ template [[nodiscard]] std::string toStr(const T& value) { @@ -205,6 +238,11 @@ std::string toStr(const T& value) } else if constexpr(!std::is_arithmetic_v) { + if(auto str = toJsonString(Any(value))) + { + return *str; + } + throw LogicError( StrCat("Function BT::toStr() not specialized for type [", BT::demangle(typeid(T)), "]") @@ -252,25 +290,6 @@ using enable_if = typename std::enable_if::type*; template using enable_if_not = typename std::enable_if::type*; -/** Usage: given a function/method like this: - * - * Expected getAnswer(); - * - * User code can check result and error message like this: - * - * auto res = getAnswer(); - * if( res ) - * { - * std::cout << "answer was: " << res.value() << std::endl; - * } - * else{ - * std::cerr << "failed to get the answer: " << res.error() << std::endl; - * } - * - * */ -template -using Expected = nonstd::expected; - #ifdef USE_BTCPP3_OLD_NAMES // note: we also use the name Optional instead of expected because it is more intuitive // for users that are not up to date with "modern" C++ diff --git a/include/behaviortree_cpp/tree_node.h b/include/behaviortree_cpp/tree_node.h index dc86f76b0..a8eed5bcf 100644 --- a/include/behaviortree_cpp/tree_node.h +++ b/include/behaviortree_cpp/tree_node.h @@ -440,14 +440,21 @@ inline Result TreeNode::getInput(const std::string& key, T& destination) const // pure string, not a blackboard key if (!remapped_res) { - destination = ParseString(port_value_str); + try { + destination = ParseString(port_value_str); + } + catch(std::exception& ex) + { + return nonstd::make_unexpected(StrCat("getInput(): ", ex.what())); + } return {}; } const auto& remapped_key = remapped_res.value(); if (!config().blackboard) { - return nonstd::make_unexpected("getInput(): trying to access an invalid Blackboard"); + return nonstd::make_unexpected("getInput(): trying to access " + "an invalid Blackboard"); } if (auto any_ref = config().blackboard->getAnyLocked(std::string(remapped_key))) diff --git a/src/basic_types.cpp b/src/basic_types.cpp index e7662467e..ccdffcadb 100644 --- a/src/basic_types.cpp +++ b/src/basic_types.cpp @@ -413,4 +413,20 @@ Any convertFromJSON(StringView json_text, std::type_index type) return res->first; } +Expected toJsonString(const Any& value) +{ + nlohmann::json json; + if(JsonExporter::get().toJson(value, json)) + { + return StrCat("json:", json.dump()); + } + return nonstd::make_unexpected("toJsonString failed"); +} + +bool StartWith(StringView str, StringView prefix) +{ + return str.size() >= prefix.size() && + strncmp(str.data(), prefix.data(), prefix.size()) == 0; +} + } // namespace BT diff --git a/tests/gtest_ports.cpp b/tests/gtest_ports.cpp index 7a8a14f8a..80c361f4c 100644 --- a/tests/gtest_ports.cpp +++ b/tests/gtest_ports.cpp @@ -1,5 +1,7 @@ #include #include "behaviortree_cpp/bt_factory.h" +#include "behaviortree_cpp/xml_parsing.h" +#include "behaviortree_cpp/json_export.h" using namespace BT; @@ -274,6 +276,11 @@ struct Point2D { template <> [[nodiscard]] Point2D BT::convertFromString(StringView str) { + if(StartWith(str, "json:")) + { + str.remove_prefix(5); + return convertFromJSON(str); + } const auto parts = BT::splitString(str, ','); if (parts.size() != 2) { @@ -284,6 +291,18 @@ Point2D BT::convertFromString(StringView str) return {x, y}; } +template <> [[nodiscard]] +std::string BT::toStr(const Point2D& point) +{ + return std::to_string(point.x) + "," + std::to_string(point.y); +} + +BT_JSON_CONVERTER(Point2D, point) +{ + add_field("x", &point.x); + add_field("y", &point.y); +} + class DefaultTestAction : public SyncActionNode { @@ -428,20 +447,24 @@ class NodeWithDefaultPoints : public SyncActionNode NodeStatus tick() override { - Point2D vectA, vectB, vectC, vectD, input; - if (!getInput("pointA", vectA) || vectA != Point2D{1, 2}) { + Point2D pointA, pointB, pointC, pointD, pointE, input; + + if (!getInput("pointA", pointA) || pointA != Point2D{1, 2}) { throw std::runtime_error("failed pointA"); } - if (!getInput("pointB", vectB) || vectB != Point2D{3, 4}) { + if (!getInput("pointB", pointB) || pointB != Point2D{3, 4}) { throw std::runtime_error("failed pointB"); } - if (!getInput("pointC", vectC) || vectC != Point2D{5, 6}) { + if (!getInput("pointC", pointC) || pointC != Point2D{5, 6}) { throw std::runtime_error("failed pointC"); } - if (!getInput("pointD", vectD) || vectD != Point2D{7, 8}) { + if (!getInput("pointD", pointD) || pointD != Point2D{7, 8}) { + throw std::runtime_error("failed pointD"); + } + if (!getInput("pointE", pointE) || pointE != Point2D{9, 10}) { throw std::runtime_error("failed pointD"); } - if (!getInput("input", input) || input != Point2D{9, 10}) { + if (!getInput("input", input) || input != Point2D{-1, -2}) { throw std::runtime_error("failed input"); } return NodeStatus::SUCCESS; @@ -453,20 +476,24 @@ class NodeWithDefaultPoints : public SyncActionNode BT::InputPort("pointA", Point2D{1, 2}, "default value is [1,2]"), BT::InputPort("pointB", "{point}", "default value inside blackboard {point}"), BT::InputPort("pointC", "5,6", "default value is [5,6]"), - BT::InputPort("pointD", "{=}", "default value inside blackboard {pointD}")}; + BT::InputPort("pointD", "{=}", "default value inside blackboard {pointD}"), + BT::InputPort("pointE", R"(json:{"x":9,"y":10})", + "default value is [9,10]")}; } }; -TEST(PortTest, DefaultInputVectors) +TEST(PortTest, DefaultInputPoint2D) { std::string xml_txt = R"( - + )"; + JsonExporter::get().addConverter(); + BehaviorTreeFactory factory; factory.registerNodeType("NodeWithDefaultPoints"); auto tree = factory.createTreeFromText(xml_txt); @@ -477,6 +504,8 @@ TEST(PortTest, DefaultInputVectors) BT::NodeStatus status; ASSERT_NO_THROW(status = tree.tickOnce()); ASSERT_EQ(status, NodeStatus::SUCCESS); + + std::cout << writeTreeNodesModelXML(factory) << std::endl; } class NodeWithDefaultStrings : public SyncActionNode @@ -531,6 +560,8 @@ TEST(PortTest, DefaultInputStrings) BT::NodeStatus status; ASSERT_NO_THROW(status = tree.tickOnce()); ASSERT_EQ(status, NodeStatus::SUCCESS); + + std::cout << writeTreeNodesModelXML(factory) << std::endl; } struct TestStruct diff --git a/tests/gtest_preconditions.cpp b/tests/gtest_preconditions.cpp index 26d55cba8..ed95ea115 100644 --- a/tests/gtest_preconditions.cpp +++ b/tests/gtest_preconditions.cpp @@ -298,3 +298,47 @@ TEST(Preconditions, Issue615_NoSkipWhenRunning_B) tree.rootBlackboard()->set("check", false); ASSERT_EQ( tree.tickOnce(), NodeStatus::RUNNING ); } + + + +TEST(Preconditions, Remapping) +{ + static constexpr auto xml_text = R"( + + + + +