Skip to content
Open
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
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,15 @@ if (WIN32)
list(APPEND BT_SOURCE src/shared_library_WIN.cpp )
endif()

# Embed src/btcpp4_schematron.sch as a C++ string constant.
# configure_file tracks the input file: CMake reruns automatically when it changes.
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/src/btcpp4_schematron.sch" BTCPP_SCHEMATRON_TEMPLATE)
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/src/schematron_template.h.in"
"${CMAKE_CURRENT_BINARY_DIR}/generated/schematron_template.gen.h"
@ONLY
)

if (BTCPP_SHARED_LIBS)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
add_library(${BTCPP_LIBRARY} SHARED ${BT_SOURCE})
Expand Down Expand Up @@ -252,6 +261,7 @@ target_include_directories(${BTCPP_LIBRARY}
$<INSTALL_INTERFACE:include>
PRIVATE
${BTCPP_EXTRA_INCLUDE_DIRS}
${CMAKE_CURRENT_BINARY_DIR}/generated
)

target_compile_definitions(${BTCPP_LIBRARY} PUBLIC BTCPP_LIBRARY_VERSION="${CMAKE_PROJECT_VERSION}")
Expand Down
38 changes: 34 additions & 4 deletions include/behaviortree_cpp/xml_parsing.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,43 @@ void VerifyXML(const std::string& xml_text,
bool include_builtin = false);

/**
* @brief writeTreeXSD generates an XSD for the nodes defined in the factory
* @brief writeTreeXSD generates an XSD for the nodes defined in the factory.
*
* @param factory the factory with the registered types
* @param factory the factory with the registered types
* @param generic if true, oneNodeGroup uses xs:any processContents="lax"
* instead of a closed xs:choice, allowing unknown custom
* node elements to pass validation. Top-level xs:element
* declarations are also emitted so that lax processing can
* still resolve and validate the known built-in node types.
* Defaults to false (strict, closed-vocabulary schema).
*
* @return string containing the XML.
* @return string containing the XSD XML.
*/
[[nodiscard]] std::string writeTreeXSD(const BehaviorTreeFactory& factory,
bool generic = false);

/**
* @brief writeTreeSchematron generates an ISO Schematron schema for BehaviorTree.CPP XML files.
*
* XSD alone cannot express cross-reference constraints. This Schematron
* complements writeTreeXSD() with three rule patterns:
* - treeNodesModel: every custom (non-built-in) node element appearing in a
* BehaviorTree body must have a matching TreeNodesModel entry (required by
* Groot2 for port display and editing).
* - subtreeResolution: every <SubTree ID="X"/> must resolve to a
* <BehaviorTree ID="X"> in the same file (relaxed when <include> is present).
* - rootStructure: main_tree_to_execute must name an existing BehaviorTree.
*
* The built-in node list is derived from factory.builtinNodes() so that the
* schema stays in sync as new built-ins are added.
*
* @param factory factory from which the built-in node names are taken;
* a default-constructed BehaviorTreeFactory suffices for most uses.
*
* @return string containing the Schematron XML (queryBinding="xslt",
* compatible with xsltproc and lxml.isoschematron).
*/
[[nodiscard]] std::string writeTreeXSD(const BehaviorTreeFactory& factory);
[[nodiscard]] std::string writeTreeSchematron(const BehaviorTreeFactory& factory);

/**
* @brief WriteTreeToXML create a string that contains the XML that corresponds to a given tree.
Expand Down
79 changes: 79 additions & 0 deletions src/btcpp4_schematron.sch
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt">
<sch:title>BehaviorTree.CPP XML validation rules</sch:title>

<!--
Rules A and B check that every custom node appearing in a BehaviorTree
body has a corresponding entry in TreeNodesModel. Groot2 and other
graphical editors require this metadata to display nodes and their ports.
Built-in node types are excluded — they are always known to the library
and do not need an explicit declaration.

\#\#BUILTIN_PIPE\#\# is a placeholder expanded at runtime by writeTreeSchematron()
with a pipe-delimited list of all built-in node names derived from the factory,
e.g. |AlwaysFailure|AlwaysSuccess|Fallback|Sequence|...|.

The XPath 1.0 idiom contains('|A|B|C|', concat('|', local-name(), '|'))
tests set membership without requiring XPath 2.0 sequence literals, keeping
this schema compatible with xsltproc and lxml.isoschematron.
-->
<sch:pattern id="treeNodesModel">
<sch:title>TreeNodesModel completeness</sch:title>

<!-- Rule A: compact-notation custom nodes (element name IS the registration ID) -->
<sch:rule context="/root/BehaviorTree//*[
not(contains('##BUILTIN_PIPE##', concat('|', local-name(), '|'))) and
local-name() != 'Action' and local-name() != 'Condition' and
local-name() != 'Control' and local-name() != 'Decorator' and
local-name() != 'SubTree'
]">
<sch:assert test="/root/TreeNodesModel/*[@ID = local-name(current())]">
Custom node '<sch:value-of select="local-name()"/>' used in tree
'<sch:value-of select="ancestor::BehaviorTree/@ID"/>'
has no &lt;TreeNodesModel&gt; entry.
</sch:assert>
</sch:rule>

<!-- Rule B: explicit-notation nodes (<Action ID="..."/>, <Condition ID="..."/>, etc.) -->
<sch:rule context="/root/BehaviorTree//*[
@ID and (
local-name() = 'Action' or local-name() = 'Condition' or
local-name() = 'Control' or local-name() = 'Decorator'
)
]">
<sch:assert test="/root/TreeNodesModel/*[@ID = current()/@ID]">
Node with ID='<sch:value-of select="@ID"/>' used in tree
'<sch:value-of select="ancestor::BehaviorTree/@ID"/>'
has no &lt;TreeNodesModel&gt; entry.
</sch:assert>
</sch:rule>
</sch:pattern>

<!--
Rule C checks that every <SubTree ID="X"/> references a <BehaviorTree ID="X">
defined in the same file. The check is relaxed when an <include> element is
present because the referenced tree may reside in an external file.
-->
<sch:pattern id="subtreeResolution">
<sch:title>SubTree ID cross-references</sch:title>
<sch:rule context="/root/BehaviorTree//SubTree[@ID]">
<sch:assert
test="/root/BehaviorTree[@ID = current()/@ID] or /root/include">
SubTree ID='<sch:value-of select="@ID"/>' is not defined in this file.
(If the definition lives in an included file, add an &lt;include&gt;.)
</sch:assert>
</sch:rule>
</sch:pattern>

<sch:pattern id="rootStructure">
<sch:title>Root element consistency</sch:title>
<sch:rule context="/root[@main_tree_to_execute]">
<sch:assert
test="BehaviorTree[@ID = current()/@main_tree_to_execute]">
main_tree_to_execute='<sch:value-of select="@main_tree_to_execute"/>'
does not match any BehaviorTree ID in this file.
</sch:assert>
</sch:rule>
</sch:pattern>

</sch:schema>
7 changes: 7 additions & 0 deletions src/schematron_template.h.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Auto-generated from src/btcpp4_schematron.sch — do not edit directly.
// To update: edit src/btcpp4_schematron.sch and re-run CMake.
namespace BT { namespace detail {
static const char* const schematron_template =
R"BTCPP4SCH(@BTCPP_SCHEMATRON_TEMPLATE@)BTCPP4SCH";
} // namespace detail
} // namespace BT
81 changes: 72 additions & 9 deletions src/xml_parsing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
#include <ament_index_cpp/get_package_share_directory.hpp>
#endif

#include "schematron_template.gen.h"

#include "behaviortree_cpp/blackboard.h"
#include "behaviortree_cpp/tree_node.h"
#include "behaviortree_cpp/utils/demangle_util.h"
Expand Down Expand Up @@ -1473,18 +1475,20 @@ std::string writeTreeNodesModelXML(const BehaviorTreeFactory& factory,
return std::string(printer.CStr(), size_t(printer.CStrSize() - 1));
}

std::string writeTreeXSD(const BehaviorTreeFactory& factory)
std::string writeTreeXSD(const BehaviorTreeFactory& factory, bool generic)
{
// There are 2 forms of representation for a node:
// compact: <Sequence .../> and explicit: <Control ID="Sequence" ... />
// Only the compact form is supported because the explicit form doesn't
// make sense with XSD since we would need to allow any attribute.
// Prepare the data

const auto& builtin_set = factory.builtinNodes();
std::map<std::string, const TreeNodeManifest*> ordered_models;
for(const auto& [registration_id, model] : factory.manifests())
{
ordered_models.insert({ registration_id, &model });
if(!generic || builtin_set.count(registration_id) != 0)
ordered_models.insert({ registration_id, &model });
}

XMLDocument doc;
Expand Down Expand Up @@ -1565,6 +1569,15 @@ std::string writeTreeXSD(const BehaviorTreeFactory& factory)
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:complexType name="inoutPortType">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="type" type="xs:string" use="optional"/>
<xs:attribute name="default" type="xs:string" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:attributeGroup name="preconditionAttributeGroup">
<xs:attribute name="_failureIf" type="xs:string" use="optional"/>
<xs:attribute name="_skipIf" type="xs:string" use="optional"/>
Expand Down Expand Up @@ -1598,6 +1611,7 @@ std::string writeTreeXSD(const BehaviorTreeFactory& factory)
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="input_port" type="inputPortType"/>
<xs:element name="output_port" type="outputPortType"/>
<xs:element name="inout_port" type="inoutPortType"/>
</xs:choice>
<xs:element name="description" type="descriptionType" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
Expand Down Expand Up @@ -1635,15 +1649,27 @@ std::string writeTreeXSD(const BehaviorTreeFactory& factory)
XMLElement* one_node_group = doc.NewElement("xs:group");
{
one_node_group->SetAttribute("name", "oneNodeGroup");
std::ostringstream xsd;
xsd << "<xs:choice>";
for(const auto& [registration_id, model] : ordered_models)
if(generic)
{
// Generic: accept any element; lax processing validates known built-ins
// via the top-level xs:element declarations emitted at the end of this function.
parse_and_insert(one_node_group, "<xs:choice>"
"<xs:any namespace=\"##any\" "
"processContents=\"lax\"/>"
"</xs:choice>");
}
else
{
xsd << "<xs:element name=\"" << registration_id << "\" type=\"" << registration_id
<< "Type\"/>";
std::ostringstream xsd;
xsd << "<xs:choice>";
for(const auto& [registration_id, model] : ordered_models)
{
xsd << "<xs:element name=\"" << registration_id << "\" type=\"" << registration_id
<< "Type\"/>";
}
xsd << "</xs:choice>";
parse_and_insert(one_node_group, xsd.str().c_str());
}
xsd << "</xs:choice>";
parse_and_insert(one_node_group, xsd.str().c_str());
schema_element->InsertEndChild(one_node_group);
}

Expand Down Expand Up @@ -1710,8 +1736,12 @@ std::string writeTreeXSD(const BehaviorTreeFactory& factory)
XMLElement* common_attr_group = doc.NewElement("xs:attributeGroup");
common_attr_group->SetAttribute("ref", "commonAttributeGroup");
type->InsertEndChild(common_attr_group);
std::map<std::string, const BT::PortInfo*> ordered_ports;
for(const auto& [port_name, port_info] : model->ports)
ordered_ports.insert({ port_name, &port_info });
for(const auto& [port_name, port_info_ptr] : ordered_ports)
{
const auto& port_info = *port_info_ptr;
XMLElement* attr = doc.NewElement("xs:attribute");
attr->SetAttribute("name", port_name.c_str());
const auto xsd_attribute_type = xsdAttributeType(port_info);
Expand Down Expand Up @@ -1739,11 +1769,44 @@ std::string writeTreeXSD(const BehaviorTreeFactory& factory)
schema_element->InsertEndChild(type);
}

// Generic mode: emit top-level xs:element declarations so that
// processContents="lax" in oneNodeGroup can resolve and validate
// known built-in node elements by name.
if(generic)
{
for(const auto& [registration_id, model] : ordered_models)
{
XMLElement* elem = doc.NewElement("xs:element");
elem->SetAttribute("name", registration_id.c_str());
elem->SetAttribute("type", (registration_id + "Type").c_str());
schema_element->InsertEndChild(elem);
}
}

XMLPrinter printer;
doc.Print(&printer);
return std::string(printer.CStr(), size_t(printer.CStrSize() - 1));
}

std::string writeTreeSchematron(const BehaviorTreeFactory& factory)
{
// Build the pipe-delimited builtin node list, e.g. "|AlwaysFailure|Fallback|...|",
// then substitute the ##BUILTIN_PIPE## placeholder in the embedded template.
// The template itself lives in src/btcpp4_schematron.sch (human-readable);
// schematron_template.gen.h is generated from it by CMake.
std::ostringstream builtin_pipe_ss;
builtin_pipe_ss << "|";
for(const auto& name : factory.builtinNodes())
builtin_pipe_ss << name << "|";

std::string result = BT::detail::schematron_template;
const std::string placeholder = "##BUILTIN_PIPE##";
const auto pos = result.find(placeholder);
if(pos != std::string::npos)
result.replace(pos, placeholder.size(), builtin_pipe_ss.str());
return result;
}

std::string WriteTreeToXML(const Tree& tree, bool add_metadata, bool add_builtin_models)
{
XMLDocument doc;
Expand Down
Loading
Loading