Skip to content

Commit

Permalink
added JSON conversion to default values
Browse files Browse the repository at this point in the history
  • Loading branch information
facontidavide committed Mar 6, 2024
1 parent 851c7a0 commit 9eb61cf
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 34 deletions.
65 changes: 42 additions & 23 deletions include/behaviortree_cpp/basic_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::pair<std::string, std::string>>;

/** Usage: given a function/method like this:
*
* Expected<double> 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 <typename T>
using Expected = nonstd::expected<T, std::string>;

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);
Expand All @@ -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 <typename T> [[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<T>(str);
Expand Down Expand Up @@ -196,6 +220,15 @@ constexpr bool IsConvertibleToString()
std::is_convertible_v<T, std::string_view>;
}

Expected<std::string> 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<typename T> [[nodiscard]]
std::string toStr(const T& value)
{
Expand All @@ -205,6 +238,11 @@ std::string toStr(const T& value)
}
else if constexpr(!std::is_arithmetic_v<T>)
{
if(auto str = toJsonString(Any(value)))
{
return *str;
}

throw LogicError(
StrCat("Function BT::toStr<T>() not specialized for type [",
BT::demangle(typeid(T)), "]")
Expand Down Expand Up @@ -252,25 +290,6 @@ using enable_if = typename std::enable_if<Predicate::value>::type*;
template <typename Predicate>
using enable_if_not = typename std::enable_if<!Predicate::value>::type*;

/** Usage: given a function/method like this:
*
* Expected<double> 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 <typename T>
using Expected = nonstd::expected<T, std::string>;

#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++
Expand Down
11 changes: 9 additions & 2 deletions include/behaviortree_cpp/tree_node.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down
16 changes: 16 additions & 0 deletions src/basic_types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -413,4 +413,20 @@ Any convertFromJSON(StringView json_text, std::type_index type)
return res->first;
}

Expected<std::string> 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
49 changes: 40 additions & 9 deletions tests/gtest_ports.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include <gtest/gtest.h>
#include "behaviortree_cpp/bt_factory.h"
#include "behaviortree_cpp/xml_parsing.h"
#include "behaviortree_cpp/json_export.h"

using namespace BT;

Expand Down Expand Up @@ -274,6 +276,11 @@ struct Point2D {
template <> [[nodiscard]]
Point2D BT::convertFromString<Point2D>(StringView str)
{
if(StartWith(str, "json:"))
{
str.remove_prefix(5);
return convertFromJSON<Point2D>(str);
}
const auto parts = BT::splitString(str, ',');
if (parts.size() != 2)
{
Expand All @@ -284,6 +291,18 @@ Point2D BT::convertFromString<Point2D>(StringView str)
return {x, y};
}

template <> [[nodiscard]]
std::string BT::toStr<Point2D>(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
{
Expand Down Expand Up @@ -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;
Expand All @@ -453,20 +476,24 @@ class NodeWithDefaultPoints : public SyncActionNode
BT::InputPort<Point2D>("pointA", Point2D{1, 2}, "default value is [1,2]"),
BT::InputPort<Point2D>("pointB", "{point}", "default value inside blackboard {point}"),
BT::InputPort<Point2D>("pointC", "5,6", "default value is [5,6]"),
BT::InputPort<Point2D>("pointD", "{=}", "default value inside blackboard {pointD}")};
BT::InputPort<Point2D>("pointD", "{=}", "default value inside blackboard {pointD}"),
BT::InputPort<Point2D>("pointE", R"(json:{"x":9,"y":10})",
"default value is [9,10]")};
}
};


TEST(PortTest, DefaultInputVectors)
TEST(PortTest, DefaultInputPoint2D)
{
std::string xml_txt = R"(
<root BTCPP_format="4" >
<BehaviorTree>
<NodeWithDefaultPoints input="9,10"/>
<NodeWithDefaultPoints input="-1,-2"/>
</BehaviorTree>
</root>)";

JsonExporter::get().addConverter<Point2D>();

BehaviorTreeFactory factory;
factory.registerNodeType<NodeWithDefaultPoints>("NodeWithDefaultPoints");
auto tree = factory.createTreeFromText(xml_txt);
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions tests/gtest_preconditions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"(
<root BTCPP_format="4">
<BehaviorTree ID="Main">
<Sequence>
<Script code="value:=1" />
<SubTree ID="Sub1" param="{value}"/>
<TestA _skipIf="value!=1" />
</Sequence>
</BehaviorTree>
<BehaviorTree ID="Sub1">
<Sequence>
<SubTree ID="Sub2" _skipIf="param!=1" />
</Sequence>
</BehaviorTree>
<BehaviorTree ID="Sub2">
<Sequence>
<TestB/>
</Sequence>
</BehaviorTree>
</root>
)";

BehaviorTreeFactory factory;

std::array<int, 2> counters;
RegisterTestTick(factory, "Test", counters);

factory.registerBehaviorTreeFromText(xml_text);
auto tree = factory.createTree("Main");

auto status = tree.tickWhileRunning();

ASSERT_EQ(status, BT::NodeStatus::SUCCESS);
ASSERT_EQ( counters[0], 1 );
ASSERT_EQ( counters[1], 1 );
}

0 comments on commit 9eb61cf

Please sign in to comment.