Skip to content

Commit 9eb61cf

Browse files
committed
added JSON conversion to default values
1 parent 851c7a0 commit 9eb61cf

File tree

5 files changed

+151
-34
lines changed

5 files changed

+151
-34
lines changed

include/behaviortree_cpp/basic_types.h

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -58,20 +58,40 @@ enum class PortDirection
5858

5959
using StringView = std::string_view;
6060

61+
bool StartWith(StringView str, StringView prefix);
62+
6163
// vector of key/value pairs
6264
using KeyValueVector = std::vector<std::pair<std::string, std::string>>;
6365

66+
/** Usage: given a function/method like this:
67+
*
68+
* Expected<double> getAnswer();
69+
*
70+
* User code can check result and error message like this:
71+
*
72+
* auto res = getAnswer();
73+
* if( res )
74+
* {
75+
* std::cout << "answer was: " << res.value() << std::endl;
76+
* }
77+
* else{
78+
* std::cerr << "failed to get the answer: " << res.error() << std::endl;
79+
* }
80+
*
81+
* */
82+
template <typename T>
83+
using Expected = nonstd::expected<T, std::string>;
6484

6585
struct AnyTypeAllowed
6686
{};
6787

6888
/**
6989
* @brief convertFromJSON will parse a json string and use JsonExporter
70-
* to convert its content to a given type. it will work only if
90+
* to convert its content to a given type. It will work only if
7191
* the type was previously registered. May throw if it fails.
7292
*
7393
* @param json_text a valid JSON string
74-
* @param type you must specify the typeid()
94+
* @param type you must specify the typeid()
7595
* @return the object, wrapped in Any.
7696
*/
7797
[[nodiscard]] Any convertFromJSON(StringView json_text, std::type_index type);
@@ -89,13 +109,17 @@ inline T convertFromJSON(StringView str)
89109
* This function is invoked under the hood by TreeNode::getInput(), but only when the
90110
* input port contains a string.
91111
*
92-
* If you have a custom type, you need to implement the corresponding template specialization.
112+
* If you have a custom type, you need to implement the corresponding
113+
* template specialization.
114+
*
115+
* If the string starts with the prefix "json:", it will
116+
* fall back to convertFromJSON()
93117
*/
94118
template <typename T> [[nodiscard]]
95119
inline T convertFromString(StringView str)
96120
{
97121
// if string starts with "json:{", try to parse it as json
98-
if(str.size() > 6 && std::strncmp("json:{", str.data(), 6) == 0)
122+
if(StartWith(str, "json:"))
99123
{
100124
str.remove_prefix(5);
101125
return convertFromJSON<T>(str);
@@ -196,6 +220,15 @@ constexpr bool IsConvertibleToString()
196220
std::is_convertible_v<T, std::string_view>;
197221
}
198222

223+
Expected<std::string> toJsonString(const Any &value);
224+
225+
226+
/**
227+
* @brief toStr is the reverse operation of convertFromString.
228+
*
229+
* If T is a custom type and there is no template specialization,
230+
* it will try to fall back to toJsonString()
231+
*/
199232
template<typename T> [[nodiscard]]
200233
std::string toStr(const T& value)
201234
{
@@ -205,6 +238,11 @@ std::string toStr(const T& value)
205238
}
206239
else if constexpr(!std::is_arithmetic_v<T>)
207240
{
241+
if(auto str = toJsonString(Any(value)))
242+
{
243+
return *str;
244+
}
245+
208246
throw LogicError(
209247
StrCat("Function BT::toStr<T>() not specialized for type [",
210248
BT::demangle(typeid(T)), "]")
@@ -252,25 +290,6 @@ using enable_if = typename std::enable_if<Predicate::value>::type*;
252290
template <typename Predicate>
253291
using enable_if_not = typename std::enable_if<!Predicate::value>::type*;
254292

255-
/** Usage: given a function/method like this:
256-
*
257-
* Expected<double> getAnswer();
258-
*
259-
* User code can check result and error message like this:
260-
*
261-
* auto res = getAnswer();
262-
* if( res )
263-
* {
264-
* std::cout << "answer was: " << res.value() << std::endl;
265-
* }
266-
* else{
267-
* std::cerr << "failed to get the answer: " << res.error() << std::endl;
268-
* }
269-
*
270-
* */
271-
template <typename T>
272-
using Expected = nonstd::expected<T, std::string>;
273-
274293
#ifdef USE_BTCPP3_OLD_NAMES
275294
// note: we also use the name Optional instead of expected because it is more intuitive
276295
// for users that are not up to date with "modern" C++

include/behaviortree_cpp/tree_node.h

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,14 +440,21 @@ inline Result TreeNode::getInput(const std::string& key, T& destination) const
440440
// pure string, not a blackboard key
441441
if (!remapped_res)
442442
{
443-
destination = ParseString(port_value_str);
443+
try {
444+
destination = ParseString(port_value_str);
445+
}
446+
catch(std::exception& ex)
447+
{
448+
return nonstd::make_unexpected(StrCat("getInput(): ", ex.what()));
449+
}
444450
return {};
445451
}
446452
const auto& remapped_key = remapped_res.value();
447453

448454
if (!config().blackboard)
449455
{
450-
return nonstd::make_unexpected("getInput(): trying to access an invalid Blackboard");
456+
return nonstd::make_unexpected("getInput(): trying to access "
457+
"an invalid Blackboard");
451458
}
452459

453460
if (auto any_ref = config().blackboard->getAnyLocked(std::string(remapped_key)))

src/basic_types.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,4 +413,20 @@ Any convertFromJSON(StringView json_text, std::type_index type)
413413
return res->first;
414414
}
415415

416+
Expected<std::string> toJsonString(const Any& value)
417+
{
418+
nlohmann::json json;
419+
if(JsonExporter::get().toJson(value, json))
420+
{
421+
return StrCat("json:", json.dump());
422+
}
423+
return nonstd::make_unexpected("toJsonString failed");
424+
}
425+
426+
bool StartWith(StringView str, StringView prefix)
427+
{
428+
return str.size() >= prefix.size() &&
429+
strncmp(str.data(), prefix.data(), prefix.size()) == 0;
430+
}
431+
416432
} // namespace BT

tests/gtest_ports.cpp

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include <gtest/gtest.h>
22
#include "behaviortree_cpp/bt_factory.h"
3+
#include "behaviortree_cpp/xml_parsing.h"
4+
#include "behaviortree_cpp/json_export.h"
35

46
using namespace BT;
57

@@ -274,6 +276,11 @@ struct Point2D {
274276
template <> [[nodiscard]]
275277
Point2D BT::convertFromString<Point2D>(StringView str)
276278
{
279+
if(StartWith(str, "json:"))
280+
{
281+
str.remove_prefix(5);
282+
return convertFromJSON<Point2D>(str);
283+
}
277284
const auto parts = BT::splitString(str, ',');
278285
if (parts.size() != 2)
279286
{
@@ -284,6 +291,18 @@ Point2D BT::convertFromString<Point2D>(StringView str)
284291
return {x, y};
285292
}
286293

294+
template <> [[nodiscard]]
295+
std::string BT::toStr<Point2D>(const Point2D& point)
296+
{
297+
return std::to_string(point.x) + "," + std::to_string(point.y);
298+
}
299+
300+
BT_JSON_CONVERTER(Point2D, point)
301+
{
302+
add_field("x", &point.x);
303+
add_field("y", &point.y);
304+
}
305+
287306

288307
class DefaultTestAction : public SyncActionNode
289308
{
@@ -428,20 +447,24 @@ class NodeWithDefaultPoints : public SyncActionNode
428447

429448
NodeStatus tick() override
430449
{
431-
Point2D vectA, vectB, vectC, vectD, input;
432-
if (!getInput("pointA", vectA) || vectA != Point2D{1, 2}) {
450+
Point2D pointA, pointB, pointC, pointD, pointE, input;
451+
452+
if (!getInput("pointA", pointA) || pointA != Point2D{1, 2}) {
433453
throw std::runtime_error("failed pointA");
434454
}
435-
if (!getInput("pointB", vectB) || vectB != Point2D{3, 4}) {
455+
if (!getInput("pointB", pointB) || pointB != Point2D{3, 4}) {
436456
throw std::runtime_error("failed pointB");
437457
}
438-
if (!getInput("pointC", vectC) || vectC != Point2D{5, 6}) {
458+
if (!getInput("pointC", pointC) || pointC != Point2D{5, 6}) {
439459
throw std::runtime_error("failed pointC");
440460
}
441-
if (!getInput("pointD", vectD) || vectD != Point2D{7, 8}) {
461+
if (!getInput("pointD", pointD) || pointD != Point2D{7, 8}) {
462+
throw std::runtime_error("failed pointD");
463+
}
464+
if (!getInput("pointE", pointE) || pointE != Point2D{9, 10}) {
442465
throw std::runtime_error("failed pointD");
443466
}
444-
if (!getInput("input", input) || input != Point2D{9, 10}) {
467+
if (!getInput("input", input) || input != Point2D{-1, -2}) {
445468
throw std::runtime_error("failed input");
446469
}
447470
return NodeStatus::SUCCESS;
@@ -453,20 +476,24 @@ class NodeWithDefaultPoints : public SyncActionNode
453476
BT::InputPort<Point2D>("pointA", Point2D{1, 2}, "default value is [1,2]"),
454477
BT::InputPort<Point2D>("pointB", "{point}", "default value inside blackboard {point}"),
455478
BT::InputPort<Point2D>("pointC", "5,6", "default value is [5,6]"),
456-
BT::InputPort<Point2D>("pointD", "{=}", "default value inside blackboard {pointD}")};
479+
BT::InputPort<Point2D>("pointD", "{=}", "default value inside blackboard {pointD}"),
480+
BT::InputPort<Point2D>("pointE", R"(json:{"x":9,"y":10})",
481+
"default value is [9,10]")};
457482
}
458483
};
459484

460485

461-
TEST(PortTest, DefaultInputVectors)
486+
TEST(PortTest, DefaultInputPoint2D)
462487
{
463488
std::string xml_txt = R"(
464489
<root BTCPP_format="4" >
465490
<BehaviorTree>
466-
<NodeWithDefaultPoints input="9,10"/>
491+
<NodeWithDefaultPoints input="-1,-2"/>
467492
</BehaviorTree>
468493
</root>)";
469494

495+
JsonExporter::get().addConverter<Point2D>();
496+
470497
BehaviorTreeFactory factory;
471498
factory.registerNodeType<NodeWithDefaultPoints>("NodeWithDefaultPoints");
472499
auto tree = factory.createTreeFromText(xml_txt);
@@ -477,6 +504,8 @@ TEST(PortTest, DefaultInputVectors)
477504
BT::NodeStatus status;
478505
ASSERT_NO_THROW(status = tree.tickOnce());
479506
ASSERT_EQ(status, NodeStatus::SUCCESS);
507+
508+
std::cout << writeTreeNodesModelXML(factory) << std::endl;
480509
}
481510

482511
class NodeWithDefaultStrings : public SyncActionNode
@@ -531,6 +560,8 @@ TEST(PortTest, DefaultInputStrings)
531560
BT::NodeStatus status;
532561
ASSERT_NO_THROW(status = tree.tickOnce());
533562
ASSERT_EQ(status, NodeStatus::SUCCESS);
563+
564+
std::cout << writeTreeNodesModelXML(factory) << std::endl;
534565
}
535566

536567
struct TestStruct

tests/gtest_preconditions.cpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,3 +298,47 @@ TEST(Preconditions, Issue615_NoSkipWhenRunning_B)
298298
tree.rootBlackboard()->set("check", false);
299299
ASSERT_EQ( tree.tickOnce(), NodeStatus::RUNNING );
300300
}
301+
302+
303+
304+
TEST(Preconditions, Remapping)
305+
{
306+
static constexpr auto xml_text = R"(
307+
<root BTCPP_format="4">
308+
309+
<BehaviorTree ID="Main">
310+
<Sequence>
311+
<Script code="value:=1" />
312+
<SubTree ID="Sub1" param="{value}"/>
313+
<TestA _skipIf="value!=1" />
314+
</Sequence>
315+
</BehaviorTree>
316+
317+
<BehaviorTree ID="Sub1">
318+
<Sequence>
319+
<SubTree ID="Sub2" _skipIf="param!=1" />
320+
</Sequence>
321+
</BehaviorTree>
322+
323+
<BehaviorTree ID="Sub2">
324+
<Sequence>
325+
<TestB/>
326+
</Sequence>
327+
</BehaviorTree>
328+
</root>
329+
)";
330+
331+
BehaviorTreeFactory factory;
332+
333+
std::array<int, 2> counters;
334+
RegisterTestTick(factory, "Test", counters);
335+
336+
factory.registerBehaviorTreeFromText(xml_text);
337+
auto tree = factory.createTree("Main");
338+
339+
auto status = tree.tickWhileRunning();
340+
341+
ASSERT_EQ(status, BT::NodeStatus::SUCCESS);
342+
ASSERT_EQ( counters[0], 1 );
343+
ASSERT_EQ( counters[1], 1 );
344+
}

0 commit comments

Comments
 (0)