Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
409e3a8
Add 'generic' parameter to writeTreeXSD()
vivoeiva May 13, 2026
3a3c9e1
Add generate_xsd tool
vivoeiva May 13, 2026
ee6699d
Fix non-deterministic port attribute order in writeTreeXSD()
vivoeiva May 13, 2026
0ae3219
Add writeTreeSchematron() for cross-reference validation of BT XML files
vivoeiva May 26, 2026
3506f11
Add generate_schematron tool
vivoeiva May 26, 2026
ccaf48e
Add generated XSD and Schematron schemas
vivoeiva May 26, 2026
f4412ef
Add validate_xml.py - XSD and Schematron validation tool
vivoeiva May 26, 2026
5fabd28
Only built-ins should go into generic xsd file
vivoeiva May 27, 2026
e1d967c
Add tests for writeTreeXSD() and writeTreeSchematron()
vivoeiva May 27, 2026
4f60ec4
Fix implicit conversion
vivoeiva May 27, 2026
89b3a25
Also handle inout_port in XSD
vivoeiva May 27, 2026
621d133
Merge branch 'master' into feature/xml-validation
facontidavide Jul 6, 2026
e575e39
Merge branch 'master' into feature/xml-validation
facontidavide Jul 15, 2026
b2289d6
Fix reconfiguration after editing src/btcpp4_schematron.sch
vivoeiva Jul 20, 2026
dc21d9d
Fix writeTreeXSD() signature not to break ABI
vivoeiva Jul 20, 2026
84f4a73
Change placeholder replacement logic for Schematron
vivoeiva Jul 20, 2026
ad1c70c
Add more test cases for InsideXmlComment()
vivoeiva Jul 20, 2026
809c691
Add xsd and schematron checks to CI (intentionally broken)
vivoeiva Jul 20, 2026
d4b31a1
Fix clang-format issues
vivoeiva Jul 20, 2026
5a0e510
Regenerate schemas to pass CI
vivoeiva Jul 20, 2026
36abdc3
Add guard for --no-xsd --no-sch together
vivoeiva Jul 20, 2026
d5af5db
Remove rule checking explicit notation against TreeNodeModel
vivoeiva Jul 24, 2026
cf93f98
Regenerate Schematron file
vivoeiva Jul 24, 2026
9780868
Add Schematron Rule B: explicit-notation node structure constraints
vivoeiva Jul 24, 2026
c185730
Regenerate Schematron file
vivoeiva Jul 24, 2026
11cb813
Merge branch 'master' into feature/xml-validation
vivoeiva Jul 24, 2026
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
11 changes: 11 additions & 0 deletions .github/workflows/cmake_ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ jobs:
- name: run test (Linux)
run: ctest --test-dir build/${{env.BUILD_TYPE}}

- name: Check generated schemas are up to date
run: |
./build/${{env.BUILD_TYPE}}/tools/bt4_generate_xsd > /tmp/bt4.xsd
./build/${{env.BUILD_TYPE}}/tools/bt4_generate_schematron > /tmp/bt4.sch
fail=0
diff tools/generated/bt4.xsd /tmp/bt4.xsd || \
{ echo "XSD is stale - regenerate: ./build/Release/tools/bt4_generate_xsd > tools/generated/bt4.xsd"; fail=1; }
diff tools/generated/bt4.sch /tmp/bt4.sch || \
{ echo "Schematron is stale - regenerate: ./build/Release/tools/bt4_generate_schematron > tools/generated/bt4.sch"; fail=1; }
exit $fail

coverage:
runs-on: ubuntu-24.04

Expand Down
13 changes: 13 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,18 @@ if (WIN32)
list(APPEND BT_SOURCE src/shared_library_WIN.cpp )
endif()

# Embed src/btcpp4_schematron.sch as a C++ string constant.
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/src/btcpp4_schematron.sch"
)

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 @@ -255,6 +267,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
49 changes: 46 additions & 3 deletions include/behaviortree_cpp/xml_parsing.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,51 @@ 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
*
* @return string containing the XML.
* @return string containing the XSD XML (strict, closed-vocabulary schema).
*/
[[nodiscard]] std::string writeTreeXSD(const BehaviorTreeFactory& factory);

/**
* @brief writeTreeXSD generates an XSD for the nodes defined in the factory.
*
* @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.
*
* @return string containing the XSD XML.
*/
[[nodiscard]] std::string writeTreeXSD(const BehaviorTreeFactory& factory, bool generic);

/**
* @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 writeTreeSchematron(const BehaviorTreeFactory& factory);

/**
* @brief WriteTreeToXML create a string that contains the XML that corresponds to a given tree.
* When using this function with a logger, you should probably set both add_metadata and
Expand All @@ -81,6 +118,12 @@ void VerifyXML(const std::string& xml_text,
[[nodiscard]] std::string WriteTreeToXML(const Tree& tree, bool add_metadata,
bool add_builtin_models);

namespace detail
{
/// Returns true when @p pos inside @p s falls within an XML comment (<!-- ... -->).
[[nodiscard]] bool insideXmlComment(const std::string& s, std::size_t pos);
} // namespace detail

} // namespace BT

#endif // XML_PARSING_BT_H
105 changes: 105 additions & 0 deletions src/btcpp4_schematron.sch
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?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>

<!--
Rule A checks that every compact-notation custom node appearing in a BehaviorTree
body has a corresponding entry in TreeNodesModel. Groot2 and other graphical
editors require this because compact notation alone gives no port information.
Explicit notation (<Action ID="X"/>) is already sufficient for Groot2 and does
not require a TreeNodesModel entry (see behaviortree.dev/docs/learn-the-basics/xml_format).
Built-in node types are excluded - they are always known to the library.

##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>
</sch:pattern>

<!--
Rule B checks structural constraints for explicit-notation nodes.
The BT.CPP XML format mandates:
- Action and Condition nodes are leaves (no children).
- Decorator nodes have exactly one child.
- Control nodes have at least one child.
These constraints apply to the generic wrapper elements used in explicit notation
(<Action ID="..."/>, <Condition ID="..."/>, <Decorator ID="..."/>, <Control ID="..."/>).
Built-in nodes written in compact notation are already constrained by the XSD.
-->
<sch:pattern id="explicitNodeStructure">
<sch:title>Explicit-notation node structure</sch:title>

<!-- Action and Condition are leaves -->
<sch:rule context="/root/BehaviorTree//*[
@ID and (local-name() = 'Action' or local-name() = 'Condition')
]">
<sch:assert test="count(*) = 0">
<sch:value-of select="local-name()"/> ID='<sch:value-of select="@ID"/>'
must have no children (has <sch:value-of select="count(*)"/>).
</sch:assert>
</sch:rule>

<!-- Decorator has exactly one child -->
<sch:rule context="/root/BehaviorTree//*[local-name() = 'Decorator' and @ID]">
<sch:assert test="count(*) = 1">
Decorator ID='<sch:value-of select="@ID"/>'
must have exactly one child (has <sch:value-of select="count(*)"/>).
</sch:assert>
</sch:rule>

<!-- Control has at least one child -->
<sch:rule context="/root/BehaviorTree//*[local-name() = 'Control' and @ID]">
<sch:assert test="count(*) >= 1">
Control ID='<sch:value-of select="@ID"/>' must have at least one child.
</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
Loading
Loading