From abf74bbac40190f6eededdafaa97c68e1b67b706 Mon Sep 17 00:00:00 2001 From: Levi Armstrong Date: Fri, 13 Jun 2025 15:27:50 -0500 Subject: [PATCH 01/15] Add property tree class to support yaml schema and validation --- tesseract_common/CMakeLists.txt | 10 +- .../include/tesseract_common/property_tree.h | 80 ++++++ tesseract_common/src/property_tree.cpp | 238 ++++++++++++++++++ tesseract_common/src/property_tree_demo.cpp | 123 +++++++++ 4 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 tesseract_common/include/tesseract_common/property_tree.h create mode 100644 tesseract_common/src/property_tree.cpp create mode 100644 tesseract_common/src/property_tree_demo.cpp diff --git a/tesseract_common/CMakeLists.txt b/tesseract_common/CMakeLists.txt index e6a2a05d8a3..8fd5a7452fb 100644 --- a/tesseract_common/CMakeLists.txt +++ b/tesseract_common/CMakeLists.txt @@ -94,6 +94,7 @@ add_library( src/plugin_info.cpp src/profile_dictionary.cpp src/profile.cpp + src/property_tree.cpp src/resource_locator.cpp src/types.cpp src/stopwatch.cpp @@ -124,7 +125,14 @@ target_code_coverage( target_include_directories(${PROJECT_NAME} PUBLIC "$" "$") -configure_package(NAMESPACE tesseract TARGETS ${PROJECT_NAME}) +add_executable(${PROJECT_NAME}_property_tree_demo src/property_tree_demo.cpp) +target_link_libraries(${PROJECT_NAME}_property_tree_demo PRIVATE ${PROJECT_NAME}) +target_compile_options(${PROJECT_NAME}_property_tree_demo PRIVATE ${TESSERACT_COMPILE_OPTIONS_PRIVATE} + ${TESSERACT_COMPILE_OPTIONS_PUBLIC}) +target_compile_definitions(${PROJECT_NAME}_property_tree_demo PRIVATE ${TESSERACT_COMPILE_DEFINITIONS}) +target_cxx_version(${PROJECT_NAME}_property_tree_demo PUBLIC VERSION ${TESSERACT_CXX_VERSION}) + +configure_package(NAMESPACE tesseract TARGETS ${PROJECT_NAME} ${PROJECT_NAME}_property_tree_demo) if(WIN32) target_link_libraries(${PROJECT_NAME} PUBLIC Bcrypt) endif() diff --git a/tesseract_common/include/tesseract_common/property_tree.h b/tesseract_common/include/tesseract_common/property_tree.h new file mode 100644 index 00000000000..9e3c2f78e07 --- /dev/null +++ b/tesseract_common/include/tesseract_common/property_tree.h @@ -0,0 +1,80 @@ +#ifndef TESSERACT_COMMON_PROPERTY_TREE_H +#define TESSERACT_COMMON_PROPERTY_TREE_H + +#include +#include +#include +#include +#include + +namespace tesseract_common +{ +namespace property_type +{ +constexpr std::string_view BOOL{ "bool" }; +constexpr std::string_view STRING{ "string" }; +constexpr std::string_view INT{ "int" }; +constexpr std::string_view FLAOT{ "float" }; +} // namespace property_type + +namespace property_attribute +{ +constexpr std::string_view REQUIRED{ "required" }; +constexpr std::string_view DEFAULT{ "default" }; +constexpr std::string_view ENUM{ "enum" }; +constexpr std::string_view MINIMUM{ "minimum" }; +constexpr std::string_view MAXIMUM{ "maximum" }; +} // namespace property_attribute + +class PropertyTree +{ +public: + using ValidatorFn = std::function&, const std::string&)>; + + PropertyTree() = default; + + void mergeSchema(const PropertyTree& schema); + + bool validate(std::vector& errors, const std::string& path = "") const; + + void addValidator(ValidatorFn fn); + + PropertyTree& get(std::string_view key); + const PropertyTree& get(std::string_view key) const; + const PropertyTree* find(std::string_view key) const; + + void setValue(const YAML::Node& v); + const YAML::Node& getValue() const; + + std::vector keys() const; + + void setAttribute(std::string_view name, const YAML::Node& attr); + void setAttribute(std::string_view name, std::string_view attr); + void setAttribute(std::string_view name, const char* attr); + void setAttribute(std::string_view name, bool attr); + void setAttribute(std::string_view name, int attr); + void setAttribute(std::string_view name, double attr); + + std::optional getAttribute(std::string_view name) const; + bool hasAttribute(std::string_view name) const; + + static PropertyTree fromYAML(const YAML::Node& node); + + YAML::Node toYAML() const; + +private: + YAML::Node value_; + std::map attributes_; + std::map children_; + std::vector validators_; +}; + +bool validateRequired(const PropertyTree& node, std::vector& errors, const std::string& path); + +bool validateRange(const PropertyTree& node, std::vector& errors, const std::string& path); + +bool validateEnum(const PropertyTree& node, std::vector& errors, const std::string& path); + +} // namespace tesseract_common + +#endif // PROPERTY_TREE_H diff --git a/tesseract_common/src/property_tree.cpp b/tesseract_common/src/property_tree.cpp new file mode 100644 index 00000000000..03dd4222334 --- /dev/null +++ b/tesseract_common/src/property_tree.cpp @@ -0,0 +1,238 @@ +#include + +const static std::string ATTRIBUTES_KEY{ "attributes" }; +const static std::string VALUE_KEY{ "value" }; + +namespace tesseract_common +{ +void PropertyTree::mergeSchema(const PropertyTree& schema) +{ + // merge schema attributes + for (const auto& pair : schema.attributes_) + { + if (!hasAttribute(pair.first)) + attributes_[pair.first] = pair.second; + } + + // apply default value only when property is not required and no explicit value present + auto default_it = attributes_.find(std::string(property_attribute::DEFAULT)); + if (default_it != attributes_.end()) + { + // determine if property is marked required + auto it = attributes_.find(std::string(property_attribute::REQUIRED)); + const bool required = (it != attributes_.end() && it->second.as()); + if (!required) + { + if (!value_ || value_.IsNull()) + value_ = YAML::Clone(default_it->second); + } + } + + // merge schema validators + for (const auto& fn : schema.validators_) + validators_.push_back(fn); + + // merge children recursively + for (const auto& pair : schema.children_) + { + if (children_.find(pair.first) == children_.end()) + children_[pair.first] = PropertyTree(); + + children_[pair.first].mergeSchema(pair.second); + } +} + +bool PropertyTree::validate(std::vector& errors, const std::string& path) const +{ + bool ok = true; + // run custom validators + for (const auto& vfn : validators_) + { + if (!vfn(*this, errors, path)) + ok = false; + } + // recurse children + for (const auto& kv : children_) + { + const std::string nextPath = path.empty() ? kv.first : path + "." + kv.first; + if (!kv.second.validate(errors, nextPath)) + ok = false; + } + return ok; +} + +/// Add a custom validator for this node +void PropertyTree::addValidator(ValidatorFn fn) { validators_.push_back(std::move(fn)); } + +PropertyTree& PropertyTree::get(std::string_view key) { return children_[std::string(key)]; } +const PropertyTree& PropertyTree::get(std::string_view key) const { return children_.at(std::string(key)); } + +const PropertyTree* PropertyTree::find(std::string_view key) const +{ + auto it = children_.find(std::string(key)); + return it != children_.end() ? &it->second : nullptr; +} + +void PropertyTree::setValue(const YAML::Node& v) { value_ = v; } +const YAML::Node& PropertyTree::getValue() const { return value_; } + +std::vector PropertyTree::keys() const +{ + std::vector res; + res.reserve(children_.size()); + for (const auto& pair : children_) + res.push_back(pair.first); + return res; +} + +void PropertyTree::setAttribute(std::string_view name, const YAML::Node& attr) +{ + attributes_[std::string(name)] = attr; +} + +void PropertyTree::setAttribute(std::string_view name, std::string_view attr) +{ + setAttribute(name, YAML::Node(std::string(attr))); +} + +void PropertyTree::setAttribute(std::string_view name, const char* attr) +{ + setAttribute(name, YAML::Node(std::string(attr))); +} + +void PropertyTree::setAttribute(std::string_view name, bool attr) { setAttribute(name, YAML::Node(attr)); } + +void PropertyTree::setAttribute(std::string_view name, int attr) { setAttribute(name, YAML::Node(attr)); } + +void PropertyTree::setAttribute(std::string_view name, double attr) { setAttribute(name, YAML::Node(attr)); } + +std::optional PropertyTree::getAttribute(std::string_view name) const +{ + auto it = attributes_.find(std::string(name)); + if (it != attributes_.end()) + return it->second; + return std::nullopt; +} + +bool PropertyTree::hasAttribute(std::string_view name) const +{ + auto it = attributes_.find(std::string(name)); + return (it != attributes_.end() && it->second && !it->second.IsNull()); +} + +PropertyTree PropertyTree::fromYAML(const YAML::Node& node) +{ + PropertyTree tree; + tree.value_ = node; + if (node.IsMap()) + { + // extract attributes map + if (node[ATTRIBUTES_KEY] && node[ATTRIBUTES_KEY].IsMap()) + { + for (const auto& it : node[ATTRIBUTES_KEY]) + { + const auto key = it.first.as(); + tree.attributes_[key] = it.second; + } + } + // extract children + for (const auto& it : node) + { + const auto key = it.first.as(); + if (key == ATTRIBUTES_KEY) + continue; + + tree.children_[key] = fromYAML(it.second); + } + } + else if (node.IsSequence()) + { + int idx = 0; + for (const auto& it : node) + tree.children_[std::to_string(idx++)] = fromYAML(it); + } + return tree; +} + +YAML::Node PropertyTree::toYAML() const +{ + // Always emit a mapping if attributes exist or children exist + if (!attributes_.empty() || !children_.empty()) + { + YAML::Node node(YAML::NodeType::Map); + // emit attributes first + if (!attributes_.empty()) + { + YAML::Node attr_node(YAML::NodeType::Map); + for (const auto& pair : attributes_) + attr_node[pair.first] = pair.second; + + node[ATTRIBUTES_KEY] = attr_node; + } + // emit children + for (const auto& pair : children_) + node[pair.first] = pair.second.toYAML(); + + // if leaf (no children) but value present, emit under 'value' + if (children_.empty() && value_) + node[VALUE_KEY] = value_; + + return node; + } + + // pure leaf without attributes: emit scalar/sequence directly + return value_; +} + +bool validateRequired(const PropertyTree& node, std::vector& errors, const std::string& path) +{ + auto req_attr = node.getAttribute(property_attribute::REQUIRED); + if (req_attr && req_attr->as()) + { + // if leaf node with no value or null + if (!node.getValue() || node.getValue().IsNull()) + { + errors.push_back(path + ": required property missing or null"); + return false; + } + } + return true; +} + +bool validateRange(const PropertyTree& node, std::vector& errors, const std::string& path) +{ + auto min_attr = node.getAttribute(property_attribute::MINIMUM); + auto max_attr = node.getAttribute(property_attribute::MAXIMUM); + if (min_attr && max_attr) + { + const auto minv = min_attr->as(); + const auto maxv = max_attr->as(); + const auto val = node.getValue().as(); + if (val < minv || val > maxv) + { + errors.push_back(path + ": value " + std::to_string(val) + " out of range [" + std::to_string(minv) + "," + + std::to_string(maxv) + "]"); + return false; + } + } + return true; +} + +bool validateEnum(const PropertyTree& node, std::vector& errors, const std::string& path) +{ + auto enum_attr = node.getAttribute(property_attribute::ENUM); + if (enum_attr.has_value() && enum_attr->IsSequence()) + { + const auto val = node.getValue().as(); + for (const auto& v : enum_attr.value()) + { + if (v.as() == val) + return true; + } + errors.push_back(path + ": value '" + val + "' not in enum list"); + return false; + } + return true; +} + +} // namespace tesseract_common diff --git a/tesseract_common/src/property_tree_demo.cpp b/tesseract_common/src/property_tree_demo.cpp new file mode 100644 index 00000000000..9fc7242da3e --- /dev/null +++ b/tesseract_common/src/property_tree_demo.cpp @@ -0,0 +1,123 @@ +#include +#include +#include + +using namespace tesseract_common; + +/// Build a detailed schema for 'config', attaching free-function validators. +PropertyTree buildConfigSchema() +{ + PropertyTree schema; + auto& cfg = schema.get("config"); + cfg.setAttribute("description", "Main config for plugin"); + cfg.setAttribute("required", true); + + // conditional + { + auto& prop = cfg.get("conditional"); + prop.setAttribute("type", property_type::BOOL); + prop.setAttribute("default", true); + prop.setAttribute("description", "Enable conditional execution"); + prop.setAttribute("required", true); + // node.addValidator(validateRange); + } + // inputs + { + auto& inputs = cfg.get("inputs"); + inputs.setAttribute("description", "Input sources"); + inputs.setAttribute("required", true); + // program + { + auto& prop = inputs.get("program"); + prop.setAttribute("type", property_type::STRING); + prop.setAttribute("description", "The composite instruction"); + prop.setAttribute("required", true); + prop.addValidator(validateRequired); + } + // environment + { + auto& prop = inputs.get("environment"); + // env.setAttribute("enum", YAML::Load(R"(["dev","stag","prod"])")); + prop.setAttribute("type", property_type::STRING); + prop.setAttribute("description", "The tesseract environment"); + prop.setAttribute("required", true); + prop.addValidator(validateRequired); + } + // profiles + { + auto& prop = inputs.get("profiles"); + prop.setAttribute("type", property_type::STRING); + prop.setAttribute("description", "The tesseract profiles"); + prop.setAttribute("required", true); + prop.addValidator(validateRequired); + // prof.setAttribute("enum", YAML::Load(R"(["A","B"])")); + // prof.addValidator(validateEnum); + // prof.addValidator([](auto const& node, auto& errs, const auto& path){ + // auto s = node.getValue().template as(); + // if (s.find(',') == std::string::npos) { + // errs.push_back(path + ": profiles should be comma-separated"); + // return false; + // } + // return true; + // }); + } + } + // outputs + { + auto& outs = cfg.get("outputs"); + auto& prop = outs.get("program"); + prop.setAttribute("type", property_type::STRING); + prop.setAttribute("required", true); + } + // format_result_as_input + { + auto& prop = cfg.get("format_result_as_input"); + prop.setAttribute("type", property_type::BOOL); + prop.setAttribute("default", false); + } + return schema; +} + +std::string str = R"(config: + conditional: true + inputs: + program: input_data + environment: environment + profiles: profiles + outputs: + program: output_data + format_result_as_input: false)"; + +std::string str2 = R"(config: + conditional: true + inputs: + program: input_data + environment: environment + profiles: profiles + outputs: + program: output_data)"; + +int main() +{ + // Load configuration from YAML + PropertyTree prop = PropertyTree::fromYAML(YAML::Load(str2)); + + // Parse schema from external file (config_schema.yaml) + PropertyTree schema = buildConfigSchema(); + + // Merge schema metadata into config tree + prop.mergeSchema(schema); + std::cout << prop.toYAML() << "\n"; + + std::vector errors; + if (!prop.validate(errors, "config")) + { + std::cerr << "Validation errors:\n"; + for (auto const& e : errors) + std::cerr << " - " << e << "\n"; + return 1; + } + bool cond = prop.get("config").get("conditional").getValue().as(); + std::cout << "conditional = " << std::boolalpha << cond << "\n"; + return 0; +} From 87dd71a93bc3ab39c2255347b57fd341e7492e28 Mon Sep 17 00:00:00 2001 From: Levi Armstrong Date: Sun, 15 Jun 2025 09:07:00 -0500 Subject: [PATCH 02/15] Update property tree to leverage nested exceptions during validation --- .../include/tesseract_common/property_tree.h | 10 ++--- tesseract_common/src/property_tree.cpp | 45 ++++++++----------- tesseract_common/src/property_tree_demo.cpp | 32 ++++++++++--- 3 files changed, 50 insertions(+), 37 deletions(-) diff --git a/tesseract_common/include/tesseract_common/property_tree.h b/tesseract_common/include/tesseract_common/property_tree.h index 9e3c2f78e07..2bb99c5aeef 100644 --- a/tesseract_common/include/tesseract_common/property_tree.h +++ b/tesseract_common/include/tesseract_common/property_tree.h @@ -29,13 +29,13 @@ constexpr std::string_view MAXIMUM{ "maximum" }; class PropertyTree { public: - using ValidatorFn = std::function&, const std::string&)>; + using ValidatorFn = std::function; PropertyTree() = default; void mergeSchema(const PropertyTree& schema); - bool validate(std::vector& errors, const std::string& path = "") const; + void validate() const; void addValidator(ValidatorFn fn); @@ -69,11 +69,11 @@ class PropertyTree std::vector validators_; }; -bool validateRequired(const PropertyTree& node, std::vector& errors, const std::string& path); +void validateRequired(const PropertyTree& node); -bool validateRange(const PropertyTree& node, std::vector& errors, const std::string& path); +void validateRange(const PropertyTree& node); -bool validateEnum(const PropertyTree& node, std::vector& errors, const std::string& path); +void validateEnum(const PropertyTree& node); } // namespace tesseract_common diff --git a/tesseract_common/src/property_tree.cpp b/tesseract_common/src/property_tree.cpp index 03dd4222334..07b7b764299 100644 --- a/tesseract_common/src/property_tree.cpp +++ b/tesseract_common/src/property_tree.cpp @@ -42,23 +42,24 @@ void PropertyTree::mergeSchema(const PropertyTree& schema) } } -bool PropertyTree::validate(std::vector& errors, const std::string& path) const +void PropertyTree::validate() const { - bool ok = true; // run custom validators for (const auto& vfn : validators_) - { - if (!vfn(*this, errors, path)) - ok = false; - } + vfn(*this); + // recurse children for (const auto& kv : children_) { - const std::string nextPath = path.empty() ? kv.first : path + "." + kv.first; - if (!kv.second.validate(errors, nextPath)) - ok = false; + try + { + kv.second.validate(); + } + catch (...) + { + std::throw_with_nested(std::runtime_error("Validation failed for property: " + kv.first)); + } } - return ok; } /// Add a custom validator for this node @@ -184,22 +185,18 @@ YAML::Node PropertyTree::toYAML() const return value_; } -bool validateRequired(const PropertyTree& node, std::vector& errors, const std::string& path) +void validateRequired(const PropertyTree& node) { auto req_attr = node.getAttribute(property_attribute::REQUIRED); if (req_attr && req_attr->as()) { // if leaf node with no value or null if (!node.getValue() || node.getValue().IsNull()) - { - errors.push_back(path + ": required property missing or null"); - return false; - } + std::throw_with_nested(std::runtime_error("Required property missing or null")); } - return true; } -bool validateRange(const PropertyTree& node, std::vector& errors, const std::string& path) +void validateRange(const PropertyTree& node) { auto min_attr = node.getAttribute(property_attribute::MINIMUM); auto max_attr = node.getAttribute(property_attribute::MAXIMUM); @@ -210,15 +207,13 @@ bool validateRange(const PropertyTree& node, std::vector& errors, c const auto val = node.getValue().as(); if (val < minv || val > maxv) { - errors.push_back(path + ": value " + std::to_string(val) + " out of range [" + std::to_string(minv) + "," + - std::to_string(maxv) + "]"); - return false; + std::throw_with_nested(std::runtime_error("Property value " + std::to_string(val) + " out of range [" + + std::to_string(minv) + "," + std::to_string(maxv) + "]")); } } - return true; } -bool validateEnum(const PropertyTree& node, std::vector& errors, const std::string& path) +void validateEnum(const PropertyTree& node) { auto enum_attr = node.getAttribute(property_attribute::ENUM); if (enum_attr.has_value() && enum_attr->IsSequence()) @@ -227,12 +222,10 @@ bool validateEnum(const PropertyTree& node, std::vector& errors, co for (const auto& v : enum_attr.value()) { if (v.as() == val) - return true; + return; } - errors.push_back(path + ": value '" + val + "' not in enum list"); - return false; + std::throw_with_nested(std::runtime_error("Property value '" + val + "' not in enum list")); } - return true; } } // namespace tesseract_common diff --git a/tesseract_common/src/property_tree_demo.cpp b/tesseract_common/src/property_tree_demo.cpp index 9fc7242da3e..912dacb1463 100644 --- a/tesseract_common/src/property_tree_demo.cpp +++ b/tesseract_common/src/property_tree_demo.cpp @@ -1,6 +1,7 @@ #include #include #include +#include using namespace tesseract_common; @@ -68,6 +69,7 @@ PropertyTree buildConfigSchema() auto& prop = outs.get("program"); prop.setAttribute("type", property_type::STRING); prop.setAttribute("required", true); + prop.addValidator(validateRequired); } // format_result_as_input { @@ -97,10 +99,19 @@ std::string str2 = R"(config: outputs: program: output_data)"; +std::string str3 = R"(config: + conditional: true + inputs: + program: input_data + environment: environment + profiles: profiles + outputs: + programs: output_data)"; + int main() { // Load configuration from YAML - PropertyTree prop = PropertyTree::fromYAML(YAML::Load(str2)); + PropertyTree prop = PropertyTree::fromYAML(YAML::Load(str3)); // Parse schema from external file (config_schema.yaml) PropertyTree schema = buildConfigSchema(); @@ -109,14 +120,23 @@ int main() prop.mergeSchema(schema); std::cout << prop.toYAML() << "\n"; - std::vector errors; - if (!prop.validate(errors, "config")) + try { - std::cerr << "Validation errors:\n"; - for (auto const& e : errors) - std::cerr << " - " << e << "\n"; + prop.validate(); + } + catch (const std::exception& e) + { + tesseract_common::printNestedException(e); return 1; } + + // if (!prop.validate(errors, "config")) + // { + // std::cerr << "Validation errors:\n"; + // for (auto const& e : errors) + // std::cerr << " - " << e << "\n"; + // return 1; + // } bool cond = prop.get("config").get("conditional").getValue().as(); std::cout << "conditional = " << std::boolalpha << cond << "\n"; return 0; From ed323e5ee35bc1a3fe3605bdddb50dc0762b9465 Mon Sep 17 00:00:00 2001 From: Levi Armstrong Date: Sun, 15 Jun 2025 09:22:27 -0500 Subject: [PATCH 03/15] Update property tree toYaml to support excluding attributes --- .../include/tesseract_common/property_tree.h | 2 +- tesseract_common/src/property_tree.cpp | 46 ++++++++++--------- tesseract_common/src/property_tree_demo.cpp | 12 ++--- 3 files changed, 28 insertions(+), 32 deletions(-) diff --git a/tesseract_common/include/tesseract_common/property_tree.h b/tesseract_common/include/tesseract_common/property_tree.h index 2bb99c5aeef..544bafc0721 100644 --- a/tesseract_common/include/tesseract_common/property_tree.h +++ b/tesseract_common/include/tesseract_common/property_tree.h @@ -60,7 +60,7 @@ class PropertyTree static PropertyTree fromYAML(const YAML::Node& node); - YAML::Node toYAML() const; + YAML::Node toYAML(bool exclude_attributes = true) const; private: YAML::Node value_; diff --git a/tesseract_common/src/property_tree.cpp b/tesseract_common/src/property_tree.cpp index 07b7b764299..213d22d563e 100644 --- a/tesseract_common/src/property_tree.cpp +++ b/tesseract_common/src/property_tree.cpp @@ -155,34 +155,36 @@ PropertyTree PropertyTree::fromYAML(const YAML::Node& node) return tree; } -YAML::Node PropertyTree::toYAML() const +YAML::Node PropertyTree::toYAML(bool exclude_attributes) const { - // Always emit a mapping if attributes exist or children exist - if (!attributes_.empty() || !children_.empty()) - { - YAML::Node node(YAML::NodeType::Map); - // emit attributes first - if (!attributes_.empty()) - { - YAML::Node attr_node(YAML::NodeType::Map); - for (const auto& pair : attributes_) - attr_node[pair.first] = pair.second; + // pure leaf without attributes or children: emit scalar/sequence directly + if (attributes_.empty() && children_.empty()) + return value_; - node[ATTRIBUTES_KEY] = attr_node; - } - // emit children - for (const auto& pair : children_) - node[pair.first] = pair.second.toYAML(); + // pure leaf with attributes excluded and no children: emit scalar/sequence directly + if (exclude_attributes && children_.empty()) + return value_; - // if leaf (no children) but value present, emit under 'value' - if (children_.empty() && value_) - node[VALUE_KEY] = value_; + // Always emit a mapping if attributes exist or children exist + YAML::Node node(YAML::NodeType::Map); + // emit attributes first + if (!exclude_attributes && !attributes_.empty()) + { + YAML::Node attr_node(YAML::NodeType::Map); + for (const auto& pair : attributes_) + attr_node[pair.first] = pair.second; - return node; + node[ATTRIBUTES_KEY] = attr_node; } + // emit children + for (const auto& pair : children_) + node[pair.first] = pair.second.toYAML(exclude_attributes); + + // if leaf (no children) but value present, emit under 'value' + if (children_.empty() && value_) + node[VALUE_KEY] = value_; - // pure leaf without attributes: emit scalar/sequence directly - return value_; + return node; } void validateRequired(const PropertyTree& node) diff --git a/tesseract_common/src/property_tree_demo.cpp b/tesseract_common/src/property_tree_demo.cpp index 912dacb1463..7efb429acf9 100644 --- a/tesseract_common/src/property_tree_demo.cpp +++ b/tesseract_common/src/property_tree_demo.cpp @@ -111,14 +111,15 @@ std::string str3 = R"(config: int main() { // Load configuration from YAML - PropertyTree prop = PropertyTree::fromYAML(YAML::Load(str3)); + PropertyTree prop = PropertyTree::fromYAML(YAML::Load(str2)); // Parse schema from external file (config_schema.yaml) PropertyTree schema = buildConfigSchema(); // Merge schema metadata into config tree prop.mergeSchema(schema); - std::cout << prop.toYAML() << "\n"; + std::cout << "Exclude attrubutes:\n" << prop.toYAML() << "\n\n"; + std::cout << "Include attrubutes:\n" << prop.toYAML(false) << "\n\n"; try { @@ -130,13 +131,6 @@ int main() return 1; } - // if (!prop.validate(errors, "config")) - // { - // std::cerr << "Validation errors:\n"; - // for (auto const& e : errors) - // std::cerr << " - " << e << "\n"; - // return 1; - // } bool cond = prop.get("config").get("conditional").getValue().as(); std::cout << "conditional = " << std::boolalpha << cond << "\n"; return 0; From 3fe2933454d5b5e175320037e24a8936e71cfc47 Mon Sep 17 00:00:00 2001 From: Levi Armstrong Date: Sun, 15 Jun 2025 09:37:57 -0500 Subject: [PATCH 04/15] Add property tree documentation --- .../include/tesseract_common/property_tree.h | 137 +++++++++++++++++- tesseract_common/src/property_tree.cpp | 15 +- 2 files changed, 144 insertions(+), 8 deletions(-) diff --git a/tesseract_common/include/tesseract_common/property_tree.h b/tesseract_common/include/tesseract_common/property_tree.h index 544bafc0721..1060310573c 100644 --- a/tesseract_common/include/tesseract_common/property_tree.h +++ b/tesseract_common/include/tesseract_common/property_tree.h @@ -26,53 +26,180 @@ constexpr std::string_view MINIMUM{ "minimum" }; constexpr std::string_view MAXIMUM{ "maximum" }; } // namespace property_attribute +/** + * @brief A hierarchical property tree that stores values, metadata attributes, + * and supports schema merging, default handling, and custom validation. + * + * Each node may contain: + * - A YAML::Node value (scalar, sequence, or map) + * - Metadata attributes (as a map of YAML::Node) + * - Child PropertyTree nodes for nested structures + * - Custom validator functions to enforce constraints + */ class PropertyTree { public: + /** + * @brief Signature for custom validator functions. + * @param node The PropertyTree node being validated. + */ using ValidatorFn = std::function; + /** + * @brief Default constructor. + */ PropertyTree() = default; + /** + * @brief Merge a schema tree into this node. + * * Copies schema attributes, applies non-required defaults, + * merges child structure, and registers schema validators. + * @param schema The schema node to merge from. + */ void mergeSchema(const PropertyTree& schema); + /** + * @brief Validate this tree using registered validators. + * * Traverses the tree, invoking each node's validators. + */ void validate() const; + /** + * @brief Register a custom validator for this node. + * @param fn Validator function to invoke during validate(). + */ void addValidator(ValidatorFn fn); + /** + * @brief Access or create a child node by key. + * @param key Child node identifier. + * @return Reference to the child node. + */ PropertyTree& get(std::string_view key); + + /** + * @brief Access a child node by key (const). + * @param key Child node identifier. + * @return Const reference to the child node. + */ const PropertyTree& get(std::string_view key) const; + + /** + * @brief Find a child node without creating it. + * @param key Child node identifier. + * @return Pointer to the child or nullptr if missing. + */ const PropertyTree* find(std::string_view key) const; + /** + * @brief Set the YAML value for this node. + * @param v YAML::Node representing the value. + */ void setValue(const YAML::Node& v); + + /** + * @brief Get the YAML value stored in this node. + * @return Const reference to YAML::Node. + */ const YAML::Node& getValue() const; + /** + * @brief List all immediate child keys. + * @return Vector of child key strings. + */ std::vector keys() const; + /** + * @brief Set a metadata attribute (YAML::Node form). + * @param name Attribute name. + * @param attr YAML::Node value. + */ void setAttribute(std::string_view name, const YAML::Node& attr); + + /** + * @brief Set a string attribute. + * @param name Attribute name. + * @param attr String value. + */ void setAttribute(std::string_view name, std::string_view attr); + + /** + * @brief Set a C-string attribute. + */ void setAttribute(std::string_view name, const char* attr); + + /** + * @brief Set a boolean attribute. + */ void setAttribute(std::string_view name, bool attr); + + /** + * @brief Set an integer attribute. + */ void setAttribute(std::string_view name, int attr); + + /** + * @brief Set a double attribute. + */ void setAttribute(std::string_view name, double attr); - std::optional getAttribute(std::string_view name) const; + /** + * @brief Check if an attribute exists and is non-null. + * @param name Attribute name. + * @return True if present and non-null. + */ bool hasAttribute(std::string_view name) const; + /** + * @brief Retrieve an attribute value. + * @param name Attribute name. + * @return Optional if attribute exists. + */ + std::optional getAttribute(std::string_view name) const; + + /** + * @brief List all metadata attribute keys. + * @return Vector of attribute key strings. + */ + std::vector getAttributeKeys() const; + + /** + * @brief Build a PropertyTree from a YAML::Node. + * @param node Root YAML::Node. + * @return Populated PropertyTree hierarchy. + */ static PropertyTree fromYAML(const YAML::Node& node); + /** + * @brief Serialize this tree to a YAML::Node. + * @param exclude_attributes If true, omit the attributes map. + * @return YAML::Node representation of the tree. + */ YAML::Node toYAML(bool exclude_attributes = true) const; private: - YAML::Node value_; - std::map attributes_; - std::map children_; - std::vector validators_; + YAML::Node value_; /**< Stored YAML value */ + std::map attributes_; /**< Metadata attributes */ + std::map children_; /**< Nested child nodes */ + std::vector validators_; /**< Registered validators */ }; +/** + * @brief Ensure a required attribute exists and is non-null. + * @param node PropertyTree node to validate. + */ void validateRequired(const PropertyTree& node); +/** + * @brief Enforce numeric range if 'minimum'/'maximum' attributes are set. + * @param node PropertyTree node to validate. + */ void validateRange(const PropertyTree& node); +/** + * @brief Enforce that the node's value is one of the 'enum' attribute list. + * @param node PropertyTree node to validate. + */ void validateEnum(const PropertyTree& node); } // namespace tesseract_common diff --git a/tesseract_common/src/property_tree.cpp b/tesseract_common/src/property_tree.cpp index 213d22d563e..8a91a69402b 100644 --- a/tesseract_common/src/property_tree.cpp +++ b/tesseract_common/src/property_tree.cpp @@ -107,6 +107,12 @@ void PropertyTree::setAttribute(std::string_view name, int attr) { setAttribute( void PropertyTree::setAttribute(std::string_view name, double attr) { setAttribute(name, YAML::Node(attr)); } +bool PropertyTree::hasAttribute(std::string_view name) const +{ + auto it = attributes_.find(std::string(name)); + return (it != attributes_.end() && it->second && !it->second.IsNull()); +} + std::optional PropertyTree::getAttribute(std::string_view name) const { auto it = attributes_.find(std::string(name)); @@ -115,10 +121,13 @@ std::optional PropertyTree::getAttribute(std::string_view name) cons return std::nullopt; } -bool PropertyTree::hasAttribute(std::string_view name) const +std::vector PropertyTree::getAttributeKeys() const { - auto it = attributes_.find(std::string(name)); - return (it != attributes_.end() && it->second && !it->second.IsNull()); + std::vector res; + res.reserve(attributes_.size()); + for (const auto& pair : attributes_) + res.push_back(pair.first); + return res; } PropertyTree PropertyTree::fromYAML(const YAML::Node& node) From 34ee4a4f955a46e1fc0de4181995b85eac93eb0f Mon Sep 17 00:00:00 2001 From: Levi Armstrong Date: Sun, 15 Jun 2025 14:08:46 -0500 Subject: [PATCH 05/15] Add schema registry singleton class --- tesseract_common/CMakeLists.txt | 3 +- .../include/tesseract_common/property_tree.h | 137 +++++++++--------- .../tesseract_common/schema_registry.h | 77 ++++++++++ tesseract_common/src/property_tree.cpp | 13 ++ tesseract_common/src/schema_registry.cpp | 72 +++++++++ 5 files changed, 232 insertions(+), 70 deletions(-) create mode 100644 tesseract_common/include/tesseract_common/schema_registry.h create mode 100644 tesseract_common/src/schema_registry.cpp diff --git a/tesseract_common/CMakeLists.txt b/tesseract_common/CMakeLists.txt index 8fd5a7452fb..2415578a3bc 100644 --- a/tesseract_common/CMakeLists.txt +++ b/tesseract_common/CMakeLists.txt @@ -96,8 +96,9 @@ add_library( src/profile.cpp src/property_tree.cpp src/resource_locator.cpp - src/types.cpp + src/schema_registry.cpp src/stopwatch.cpp + src/types.cpp src/timer.cpp src/yaml_utils.cpp) target_link_libraries( diff --git a/tesseract_common/include/tesseract_common/property_tree.h b/tesseract_common/include/tesseract_common/property_tree.h index 1060310573c..6b6ebcc7979 100644 --- a/tesseract_common/include/tesseract_common/property_tree.h +++ b/tesseract_common/include/tesseract_common/property_tree.h @@ -2,9 +2,11 @@ #define TESSERACT_COMMON_PROPERTY_TREE_H #include +#include #include -#include #include +#include +#include #include namespace tesseract_common @@ -14,7 +16,7 @@ namespace property_type constexpr std::string_view BOOL{ "bool" }; constexpr std::string_view STRING{ "string" }; constexpr std::string_view INT{ "int" }; -constexpr std::string_view FLAOT{ "float" }; +constexpr std::string_view FLOAT{ "float" }; } // namespace property_type namespace property_attribute @@ -27,79 +29,90 @@ constexpr std::string_view MAXIMUM{ "maximum" }; } // namespace property_attribute /** - * @brief A hierarchical property tree that stores values, metadata attributes, - * and supports schema merging, default handling, and custom validation. + * @file property_tree.h + * @brief Defines PropertyTree, a hierarchical structure for YAML-based + * configuration with metadata and validation support. + */ + +/** + * @class PropertyTree + * @brief Represents a node in a hierarchical property tree. * - * Each node may contain: + * Each PropertyTree node may contain: * - A YAML::Node value (scalar, sequence, or map) - * - Metadata attributes (as a map of YAML::Node) - * - Child PropertyTree nodes for nested structures - * - Custom validator functions to enforce constraints + * - A map of metadata attributes (YAML::Node) keyed by string + * - Nested child PropertyTree nodes + * - A list of custom validator functions */ class PropertyTree { public: /** - * @brief Signature for custom validator functions. + * @brief Signature for custom validator callbacks. * @param node The PropertyTree node being validated. + * @throws std::runtime_error on validation failure. */ using ValidatorFn = std::function; /** - * @brief Default constructor. + * @brief Default constructor. Creates an empty tree node. */ PropertyTree() = default; /** - * @brief Merge a schema tree into this node. - * * Copies schema attributes, applies non-required defaults, - * merges child structure, and registers schema validators. - * @param schema The schema node to merge from. + * @brief Merge schema metadata into this node. + * + * Copies attributes from the schema, applies defaults for non-required + * properties, registers schema validators, and recursively merges children. + * @param schema PropertyTree containing schema definitions. */ void mergeSchema(const PropertyTree& schema); /** - * @brief Validate this tree using registered validators. - * * Traverses the tree, invoking each node's validators. + * @brief Validate the tree using registered validators. + * + * Recursively invokes all validators on this node and its children. + * Throws on the first error encountered. */ void validate() const; /** * @brief Register a custom validator for this node. - * @param fn Validator function to invoke during validate(). + * @param fn Callback to invoke during validation. */ void addValidator(ValidatorFn fn); /** * @brief Access or create a child node by key. - * @param key Child node identifier. - * @return Reference to the child node. + * @param key Child identifier (string key). + * @return Reference to the child PropertyTree. */ PropertyTree& get(std::string_view key); /** * @brief Access a child node by key (const). - * @param key Child node identifier. - * @return Const reference to the child node. + * @param key Child identifier. + * @return Const reference to the child. + * @throws std::out_of_range if key not found. */ const PropertyTree& get(std::string_view key) const; /** * @brief Find a child node without creating it. - * @param key Child node identifier. - * @return Pointer to the child or nullptr if missing. + * @param key Child identifier. + * @return Pointer to child or nullptr if not present. */ const PropertyTree* find(std::string_view key) const; /** - * @brief Set the YAML value for this node. - * @param v YAML::Node representing the value. + * @brief Set the YAML value of this node. + * @param v YAML::Node representing the new value. */ void setValue(const YAML::Node& v); /** - * @brief Get the YAML value stored in this node. - * @return Const reference to YAML::Node. + * @brief Retrieve the YAML value stored at this node. + * @return Const reference to a YAML::Node. */ const YAML::Node& getValue() const; @@ -110,98 +123,84 @@ class PropertyTree std::vector keys() const; /** - * @brief Set a metadata attribute (YAML::Node form). + * @brief Set a metadata attribute (YAML node form). * @param name Attribute name. * @param attr YAML::Node value. */ void setAttribute(std::string_view name, const YAML::Node& attr); - /** - * @brief Set a string attribute. - * @param name Attribute name. - * @param attr String value. - */ + /** @brief Convenience overload to set a string attribute. */ void setAttribute(std::string_view name, std::string_view attr); - - /** - * @brief Set a C-string attribute. - */ void setAttribute(std::string_view name, const char* attr); - - /** - * @brief Set a boolean attribute. - */ + /** @brief Set a boolean attribute. */ void setAttribute(std::string_view name, bool attr); - - /** - * @brief Set an integer attribute. - */ + /** @brief Set an integer attribute. */ void setAttribute(std::string_view name, int attr); - - /** - * @brief Set a double attribute. - */ + /** @brief Set a double attribute. */ void setAttribute(std::string_view name, double attr); /** - * @brief Check if an attribute exists and is non-null. - * @param name Attribute name. - * @return True if present and non-null. + * @brief Check if an attribute exists and is not null. + * @param name Attribute name. + * @return True if present and non-null, false otherwise. */ bool hasAttribute(std::string_view name) const; /** - * @brief Retrieve an attribute value. - * @param name Attribute name. - * @return Optional if attribute exists. + * @brief Retrieve an attribute value by name. + * @param name Attribute name. + * @return Optional containing YAML::Node if found. */ std::optional getAttribute(std::string_view name) const; /** * @brief List all metadata attribute keys. - * @return Vector of attribute key strings. + * @return Vector of attribute names. */ std::vector getAttributeKeys() const; /** - * @brief Build a PropertyTree from a YAML::Node. - * @param node Root YAML::Node. + * @brief Create a PropertyTree from a YAML::Node. + * @param node Root YAML::Node to convert. * @return Populated PropertyTree hierarchy. */ static PropertyTree fromYAML(const YAML::Node& node); /** - * @brief Serialize this tree to a YAML::Node. - * @param exclude_attributes If true, omit the attributes map. + * @brief Serialize this tree back into a YAML::Node. + * @param exclude_attributes If true, omit the attributes map. * @return YAML::Node representation of the tree. */ YAML::Node toYAML(bool exclude_attributes = true) const; private: - YAML::Node value_; /**< Stored YAML value */ + YAML::Node value_; /**< Value stored at this node */ std::map attributes_; /**< Metadata attributes */ std::map children_; /**< Nested child nodes */ - std::vector validators_; /**< Registered validators */ + std::vector validators_; /**< Validators to invoke */ }; /** - * @brief Ensure a required attribute exists and is non-null. - * @param node PropertyTree node to validate. + * @brief Validator: ensure 'required' attribute is present and non-null. + * @param node Node to validate. + * @throws runtime_error if missing. */ void validateRequired(const PropertyTree& node); /** - * @brief Enforce numeric range if 'minimum'/'maximum' attributes are set. - * @param node PropertyTree node to validate. + * @brief Validator: enforce 'minimum'/'maximum' range constraints. + * @param node Node to validate. + * @throws runtime_error if out of range. */ void validateRange(const PropertyTree& node); /** - * @brief Enforce that the node's value is one of the 'enum' attribute list. - * @param node PropertyTree node to validate. + * @brief Validator: enforce that node's value is in 'enum' list. + * @param node Node to validate. + * @throws runtime_error if not found. */ void validateEnum(const PropertyTree& node); } // namespace tesseract_common -#endif // PROPERTY_TREE_H +#endif // TESSERACT_COMMON_PROPERTY_TREE_H diff --git a/tesseract_common/include/tesseract_common/schema_registry.h b/tesseract_common/include/tesseract_common/schema_registry.h new file mode 100644 index 00000000000..67cc2ff674f --- /dev/null +++ b/tesseract_common/include/tesseract_common/schema_registry.h @@ -0,0 +1,77 @@ +#ifndef TESSERACT_COMMON_SCHEMA_REGISTRY_H +#define TESSERACT_COMMON_SCHEMA_REGISTRY_H + +#include +#include +#include +#include + +namespace tesseract_common +{ +class PropertyTree; + +/** + * @brief A global registry of named schemas for PropertyTree. + * + * Use SchemaRegistry::instance() to access the singleton. + */ +class SchemaRegistry +{ +public: + ~SchemaRegistry() = default; + SchemaRegistry(const SchemaRegistry&) = delete; + SchemaRegistry& operator=(const SchemaRegistry&) = delete; + SchemaRegistry(const SchemaRegistry&&) = delete; + SchemaRegistry& operator=(const SchemaRegistry&&) = delete; + + /** @brief Access the singleton instance. */ + static std::shared_ptr instance(); + + /** + * @brief Register an already-parsed schema under a logical key. + * @param key Unique identifier for this schema. + * @param schema Parsed PropertyTree schema. + */ + void registerSchema(const std::string& key, const PropertyTree& schema); + + /** + * @brief Register a schema from a YAML file path. + * @param key Unique identifier for this schema. + * @param path Path to a .yaml schema file. + */ + void registerSchemaFromFile(const std::string& key, const std::string& path); + + /** + * @brief Check whether a schema is registered under this key. + * @param key Logical identifier. + * @return True if present. + */ + bool contains(const std::string& key) const; + + /** + * @brief Retrieve a registered schema by key. + * @param key Logical identifier. + * @return Const reference to the schema PropertyTree. + * @throws std::out_of_range if the key is not found. + */ + const PropertyTree& get(const std::string& key) const; + + /** + * @brief Load and parse an arbitrary YAML file into a PropertyTree. + * @param path Path to a .yaml file. + * @return Parsed PropertyTree. + * @throws YAML::Exception on parse errors. + */ + static PropertyTree loadFile(const std::string& path); + +private: + SchemaRegistry() = default; + + mutable std::mutex mutex_; + mutable std::map schemas_; + mutable std::map paths_; +}; + +} // namespace tesseract_common + +#endif // TESSERACT_COMMON_SCHEMA_REGISTRY_H diff --git a/tesseract_common/src/property_tree.cpp b/tesseract_common/src/property_tree.cpp index 8a91a69402b..c59c8244413 100644 --- a/tesseract_common/src/property_tree.cpp +++ b/tesseract_common/src/property_tree.cpp @@ -1,7 +1,9 @@ #include +#include const static std::string ATTRIBUTES_KEY{ "attributes" }; const static std::string VALUE_KEY{ "value" }; +const static std::string FOLLOW_KEY{ "follow" }; namespace tesseract_common { @@ -132,6 +134,17 @@ std::vector PropertyTree::getAttributeKeys() const PropertyTree PropertyTree::fromYAML(const YAML::Node& node) { + // Handle 'follow' directive: load external YAML or schema file + if (node.IsMap() && node[FOLLOW_KEY] && node[FOLLOW_KEY].IsScalar()) + { + if (node.size() > 1) + throw std::runtime_error("'follow' cannot be mixed with other entries"); + + auto key = node[FOLLOW_KEY].as(); + auto registry = SchemaRegistry::instance(); + return registry->contains(key) ? registry->get(key) : SchemaRegistry::loadFile(key); + } + PropertyTree tree; tree.value_ = node; if (node.IsMap()) diff --git a/tesseract_common/src/schema_registry.cpp b/tesseract_common/src/schema_registry.cpp new file mode 100644 index 00000000000..1b13934f4ad --- /dev/null +++ b/tesseract_common/src/schema_registry.cpp @@ -0,0 +1,72 @@ +#include +#include + +#include + +#include + +namespace tesseract_common +{ +std::shared_ptr SchemaRegistry::instance() +{ + // local statics, not global variables + static std::once_flag flag; + static std::shared_ptr singleton; + + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + std::call_once(flag, []() { singleton.reset(new SchemaRegistry()); }); + return singleton; +} + +void SchemaRegistry::registerSchema(const std::string& key, const PropertyTree& schema) +{ + std::lock_guard lock(mutex_); + schemas_[key] = schema; +} + +void SchemaRegistry::registerSchemaFromFile(const std::string& key, const std::string& path) +{ + std::lock_guard lock(mutex_); + paths_[key] = path; + // Parse immediately and store + YAML::Node node = YAML::LoadFile(path); + schemas_[key] = PropertyTree::fromYAML(node); +} + +bool SchemaRegistry::contains(const std::string& key) const +{ + std::lock_guard lock(mutex_); + return schemas_.count(key) > 0 || paths_.count(key) > 0; +} + +const PropertyTree& SchemaRegistry::get(const std::string& key) const +{ + std::lock_guard lock(mutex_); + auto sit = schemas_.find(key); + if (sit != schemas_.end()) + return sit->second; + auto pit = paths_.find(key); + if (pit != paths_.end()) + { + // Lazy-load if path only was registered + YAML::Node node = YAML::LoadFile(pit->second); + schemas_[key] = PropertyTree::fromYAML(node); + return schemas_.at(key); + } + throw std::out_of_range("SchemaRegistry: key not found -> " + key); +} + +PropertyTree SchemaRegistry::loadFile(const std::string& path) +{ + // Allow relative or absolute paths + std::filesystem::path p(path); + if (!p.is_absolute()) + { + // Optionally resolve against current working directory + p = std::filesystem::current_path() / p; + } + YAML::Node node = YAML::LoadFile(p.string()); + return PropertyTree::fromYAML(node); +} + +} // namespace tesseract_common From 8d7d9f12e0713f93859982b728198e3dbe2dd30e Mon Sep 17 00:00:00 2001 From: Levi Armstrong Date: Mon, 16 Jun 2025 13:18:17 -0500 Subject: [PATCH 06/15] fixup --- tesseract_common/src/property_tree_demo.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tesseract_common/src/property_tree_demo.cpp b/tesseract_common/src/property_tree_demo.cpp index 7efb429acf9..5cfc5435c92 100644 --- a/tesseract_common/src/property_tree_demo.cpp +++ b/tesseract_common/src/property_tree_demo.cpp @@ -12,6 +12,11 @@ PropertyTree buildConfigSchema() auto& cfg = schema.get("config"); cfg.setAttribute("description", "Main config for plugin"); cfg.setAttribute("required", true); + std::map return_options; + return_options[0] = "Error"; + return_options[1] = "Successful"; + cfg.setAttribute("return_options", YAML::Node(return_options)); + cfg.addValidator(validateRequired); // conditional { @@ -20,7 +25,7 @@ PropertyTree buildConfigSchema() prop.setAttribute("default", true); prop.setAttribute("description", "Enable conditional execution"); prop.setAttribute("required", true); - // node.addValidator(validateRange); + prop.addValidator(validateRequired); } // inputs { From d41446aefb4d87d83b08a94fad612cd53f008845 Mon Sep 17 00:00:00 2001 From: Levi Armstrong Date: Mon, 16 Jun 2025 14:23:45 -0500 Subject: [PATCH 07/15] fixup --- .../include/tesseract_common/property_tree.h | 31 +++++++++++++- tesseract_common/src/property_tree.cpp | 17 ++++++++ tesseract_common/src/property_tree_demo.cpp | 42 +++++++++---------- 3 files changed, 68 insertions(+), 22 deletions(-) diff --git a/tesseract_common/include/tesseract_common/property_tree.h b/tesseract_common/include/tesseract_common/property_tree.h index 6b6ebcc7979..84711a3dea0 100644 --- a/tesseract_common/include/tesseract_common/property_tree.h +++ b/tesseract_common/include/tesseract_common/property_tree.h @@ -13,14 +13,31 @@ namespace tesseract_common { namespace property_type { +// Integral Types constexpr std::string_view BOOL{ "bool" }; -constexpr std::string_view STRING{ "string" }; +constexpr std::string_view CHAR{ "char" }; +constexpr std::string_view STRING{ "std::string" }; constexpr std::string_view INT{ "int" }; +constexpr std::string_view UNSIGNED_INT{ "unsigned int" }; +constexpr std::string_view LONG_INT{ "long int" }; +constexpr std::string_view LONG_UNSIGNED_INT{ "long unsigned int" }; constexpr std::string_view FLOAT{ "float" }; +constexpr std::string_view DOUBLE{ "double" }; + +// Eigen Types +constexpr std::string_view EIGEN_ISOMETRY_3D{ "Eigen::Isometry3d" }; +constexpr std::string_view EIGEN_MATRIX_XD{ "Eigen::MatrixXd" }; +constexpr std::string_view EIGEN_VECTOR_XD{ "Eigen::VectorXd" }; +constexpr std::string_view EIGEN_MATRIX_2D{ "Eigen::Matrix2d" }; +constexpr std::string_view EIGEN_VECTOR_2D{ "Eigen::Vector2d" }; +constexpr std::string_view EIGEN_MATRIX_3D{ "Eigen::Matrix3d" }; +constexpr std::string_view EIGEN_VECTOR_3D{ "Eigen::Vector3d" }; } // namespace property_type namespace property_attribute { +constexpr std::string_view TYPE{ "type" }; +constexpr std::string_view DOC{ "doc" }; constexpr std::string_view REQUIRED{ "required" }; constexpr std::string_view DEFAULT{ "default" }; constexpr std::string_view ENUM{ "enum" }; @@ -116,6 +133,12 @@ class PropertyTree */ const YAML::Node& getValue() const; + /** + * @brief Check if property value is null + * @return True if required, otherwise false + */ + bool isNull() const; + /** * @brief List all immediate child keys. * @return Vector of child key strings. @@ -159,6 +182,12 @@ class PropertyTree */ std::vector getAttributeKeys() const; + /** + * @brief Check if property is requried by checking for attribute and its value + * @return True if required, otherwise false + */ + bool isRequired() const; + /** * @brief Create a PropertyTree from a YAML::Node. * @param node Root YAML::Node to convert. diff --git a/tesseract_common/src/property_tree.cpp b/tesseract_common/src/property_tree.cpp index c59c8244413..636771e3fe7 100644 --- a/tesseract_common/src/property_tree.cpp +++ b/tesseract_common/src/property_tree.cpp @@ -79,6 +79,8 @@ const PropertyTree* PropertyTree::find(std::string_view key) const void PropertyTree::setValue(const YAML::Node& v) { value_ = v; } const YAML::Node& PropertyTree::getValue() const { return value_; } +bool PropertyTree::isNull() const { return value_.IsNull(); } + std::vector PropertyTree::keys() const { std::vector res; @@ -132,6 +134,15 @@ std::vector PropertyTree::getAttributeKeys() const return res; } +bool PropertyTree::isRequired() const +{ + std::optional required = getAttribute(property_attribute::REQUIRED); + if (!required.has_value()) + return false; + + return required.value().as(); +} + PropertyTree PropertyTree::fromYAML(const YAML::Node& node) { // Handle 'follow' directive: load external YAML or schema file @@ -200,7 +211,13 @@ YAML::Node PropertyTree::toYAML(bool exclude_attributes) const } // emit children for (const auto& pair : children_) + { + // If the property is not required and is null then skip when excluding attributes + if (exclude_attributes && !pair.second.isRequired() && pair.second.isNull()) + continue; + node[pair.first] = pair.second.toYAML(exclude_attributes); + } // if leaf (no children) but value present, emit under 'value' if (children_.empty() && value_) diff --git a/tesseract_common/src/property_tree_demo.cpp b/tesseract_common/src/property_tree_demo.cpp index 5cfc5435c92..b8e5f84f09f 100644 --- a/tesseract_common/src/property_tree_demo.cpp +++ b/tesseract_common/src/property_tree_demo.cpp @@ -10,8 +10,8 @@ PropertyTree buildConfigSchema() { PropertyTree schema; auto& cfg = schema.get("config"); - cfg.setAttribute("description", "Main config for plugin"); - cfg.setAttribute("required", true); + cfg.setAttribute(property_attribute::DOC, "Main config for plugin"); + cfg.setAttribute(property_attribute::REQUIRED, true); std::map return_options; return_options[0] = "Error"; return_options[1] = "Successful"; @@ -21,40 +21,40 @@ PropertyTree buildConfigSchema() // conditional { auto& prop = cfg.get("conditional"); - prop.setAttribute("type", property_type::BOOL); - prop.setAttribute("default", true); - prop.setAttribute("description", "Enable conditional execution"); - prop.setAttribute("required", true); + prop.setAttribute(property_attribute::TYPE, property_type::BOOL); + prop.setAttribute(property_attribute::DEFAULT, true); + prop.setAttribute(property_attribute::DOC, "Enable conditional execution"); + prop.setAttribute(property_attribute::REQUIRED, true); prop.addValidator(validateRequired); } // inputs { auto& inputs = cfg.get("inputs"); - inputs.setAttribute("description", "Input sources"); - inputs.setAttribute("required", true); + inputs.setAttribute(property_attribute::DOC, "Input sources"); + inputs.setAttribute(property_attribute::REQUIRED, true); // program { auto& prop = inputs.get("program"); - prop.setAttribute("type", property_type::STRING); - prop.setAttribute("description", "The composite instruction"); - prop.setAttribute("required", true); + prop.setAttribute(property_attribute::TYPE, property_type::STRING); + prop.setAttribute(property_attribute::DOC, "The composite instruction"); + prop.setAttribute(property_attribute::REQUIRED, true); prop.addValidator(validateRequired); } // environment { auto& prop = inputs.get("environment"); // env.setAttribute("enum", YAML::Load(R"(["dev","stag","prod"])")); - prop.setAttribute("type", property_type::STRING); - prop.setAttribute("description", "The tesseract environment"); - prop.setAttribute("required", true); + prop.setAttribute(property_attribute::TYPE, property_type::STRING); + prop.setAttribute(property_attribute::DOC, "The tesseract environment"); + prop.setAttribute(property_attribute::REQUIRED, true); prop.addValidator(validateRequired); } // profiles { auto& prop = inputs.get("profiles"); - prop.setAttribute("type", property_type::STRING); - prop.setAttribute("description", "The tesseract profiles"); - prop.setAttribute("required", true); + prop.setAttribute(property_attribute::TYPE, property_type::STRING); + prop.setAttribute(property_attribute::DOC, "The tesseract profiles"); + prop.setAttribute(property_attribute::REQUIRED, true); prop.addValidator(validateRequired); // prof.setAttribute("enum", YAML::Load(R"(["A","B"])")); // prof.addValidator(validateEnum); @@ -72,15 +72,15 @@ PropertyTree buildConfigSchema() { auto& outs = cfg.get("outputs"); auto& prop = outs.get("program"); - prop.setAttribute("type", property_type::STRING); - prop.setAttribute("required", true); + prop.setAttribute(property_attribute::TYPE, property_type::STRING); + prop.setAttribute(property_attribute::REQUIRED, true); prop.addValidator(validateRequired); } // format_result_as_input { auto& prop = cfg.get("format_result_as_input"); - prop.setAttribute("type", property_type::BOOL); - prop.setAttribute("default", false); + prop.setAttribute(property_attribute::TYPE, property_type::BOOL); + prop.setAttribute(property_attribute::DEFAULT, false); } return schema; } From 462013f9245ec8f792f1767396b7b9329cc2f8c9 Mon Sep 17 00:00:00 2001 From: Levi Armstrong Date: Tue, 17 Jun 2025 18:22:20 -0500 Subject: [PATCH 08/15] fixup --- tesseract_common/CMakeLists.txt | 1 + .../include/tesseract_common/fwd.h | 6 + .../include/tesseract_common/property_tree.h | 130 ++++++++-- .../tesseract_common/schema_registration.h | 33 +++ tesseract_common/src/property_tree.cpp | 245 ++++++++++++++---- tesseract_common/src/property_tree_demo.cpp | 167 ++++++++++-- tesseract_common/src/schema_registration.cpp | 19 ++ 7 files changed, 518 insertions(+), 83 deletions(-) create mode 100644 tesseract_common/include/tesseract_common/schema_registration.h create mode 100644 tesseract_common/src/schema_registration.cpp diff --git a/tesseract_common/CMakeLists.txt b/tesseract_common/CMakeLists.txt index 2415578a3bc..561ff94449b 100644 --- a/tesseract_common/CMakeLists.txt +++ b/tesseract_common/CMakeLists.txt @@ -96,6 +96,7 @@ add_library( src/profile.cpp src/property_tree.cpp src/resource_locator.cpp + src/schema_registration.cpp src/schema_registry.cpp src/stopwatch.cpp src/types.cpp diff --git a/tesseract_common/include/tesseract_common/fwd.h b/tesseract_common/include/tesseract_common/fwd.h index c5f6bfd4f53..b5681e62adb 100644 --- a/tesseract_common/include/tesseract_common/fwd.h +++ b/tesseract_common/include/tesseract_common/fwd.h @@ -80,6 +80,12 @@ class Timer; // Profile Dictionary class Profile; class ProfileDictionary; + +// property_tree.h +class PropertyTree; + +// schema_registry.h +class SchemaRegistry; } // namespace tesseract_common #endif // TESSERACT_COMMON_TESSERACT_COMMON_FWD_H diff --git a/tesseract_common/include/tesseract_common/property_tree.h b/tesseract_common/include/tesseract_common/property_tree.h index 84711a3dea0..55b2d0e47a8 100644 --- a/tesseract_common/include/tesseract_common/property_tree.h +++ b/tesseract_common/include/tesseract_common/property_tree.h @@ -13,6 +13,21 @@ namespace tesseract_common { namespace property_type { +/** + * @brief A utility for constructing the list type + * @param type The type assoicated with the list + * @return The string representation of the std::vector, aka. type[] + */ +std::string createList(std::string_view type); + +/** + * @brief A utility for constructing the map type + * @param key_type The type assoicated with the map key + * @param value_type The type assoicated with the map value + * @return The string representation of the std::make, aka. {key_type : value_type} + */ +std::string createMap(std::string_view key_type, std::string_view value_type); + // Integral Types constexpr std::string_view BOOL{ "bool" }; constexpr std::string_view CHAR{ "char" }; @@ -26,11 +41,11 @@ constexpr std::string_view DOUBLE{ "double" }; // Eigen Types constexpr std::string_view EIGEN_ISOMETRY_3D{ "Eigen::Isometry3d" }; -constexpr std::string_view EIGEN_MATRIX_XD{ "Eigen::MatrixXd" }; +// constexpr std::string_view EIGEN_MATRIX_XD{ "Eigen::MatrixXd" }; constexpr std::string_view EIGEN_VECTOR_XD{ "Eigen::VectorXd" }; -constexpr std::string_view EIGEN_MATRIX_2D{ "Eigen::Matrix2d" }; +// constexpr std::string_view EIGEN_MATRIX_2D{ "Eigen::Matrix2d" }; constexpr std::string_view EIGEN_VECTOR_2D{ "Eigen::Vector2d" }; -constexpr std::string_view EIGEN_MATRIX_3D{ "Eigen::Matrix3d" }; +// constexpr std::string_view EIGEN_MATRIX_3D{ "Eigen::Matrix3d" }; constexpr std::string_view EIGEN_VECTOR_3D{ "Eigen::Vector3d" }; } // namespace property_type @@ -77,21 +92,22 @@ class PropertyTree PropertyTree() = default; /** - * @brief Merge schema metadata into this node. - * - * Copies attributes from the schema, applies defaults for non-required - * properties, registers schema validators, and recursively merges children. - * @param schema PropertyTree containing schema definitions. + * @brief Given that *this* is purely a schema tree, merge in the + * user’s YAML config to populate all values (and apply defaults). + * @param config The user-supplied YAML::Node (possibly null). + * @param allow_extra_properties If false, “extra” keys in config will be flagged. */ - void mergeSchema(const PropertyTree& schema); + void mergeConfig(const YAML::Node& config, bool allow_extra_properties = false); /** * @brief Validate the tree using registered validators. * * Recursively invokes all validators on this node and its children. * Throws on the first error encountered. + * + * @param allow_extra_properties Indicate if extra properties are allowed */ - void validate() const; + void validate(bool allow_extra_properties = false) const; /** * @brief Register a custom validator for this node. @@ -139,6 +155,12 @@ class PropertyTree */ bool isNull() const; + /** + * @brief Check if property is a container of child properties + * @return True if container, otherwise false + */ + bool isContainer() const; + /** * @brief List all immediate child keys. * @return Vector of child key strings. @@ -204,11 +226,19 @@ class PropertyTree private: YAML::Node value_; /**< Value stored at this node */ + YAML::Node follow_; /**< Follow stored at this node */ std::map attributes_; /**< Metadata attributes */ std::map children_; /**< Nested child nodes */ std::vector validators_; /**< Validators to invoke */ }; +/** + * @brief Check if type is a sequence + * @param type The type to check + * @return If it is a sequence the underlying type is returned + */ +std::optional isSequenceType(std::string_view type); + /** * @brief Validator: ensure 'required' attribute is present and non-null. * @param node Node to validate. @@ -217,18 +247,86 @@ class PropertyTree void validateRequired(const PropertyTree& node); /** - * @brief Validator: enforce 'minimum'/'maximum' range constraints. + * @brief Validator: enforce that node's value is in 'enum' list. * @param node Node to validate. - * @throws runtime_error if out of range. + * @throws runtime_error if not found. + */ +void validateEnum(const PropertyTree& node); + +/** + * @brief Validtor: ensure node value is of type YAML::NodeType::Map + * @param node Node to validate. + * @throws runtime_error if not correct type. */ -void validateRange(const PropertyTree& node); +void validateMap(const PropertyTree& node); /** - * @brief Validator: enforce that node's value is in 'enum' list. + * @brief Validtor: ensure node value is of type YAML::NodeType::Sequence + * @param node Node to validate. + * @throws runtime_error if not correct type. + */ +void validateSequence(const PropertyTree& node); + +void validateCustomType(const PropertyTree& node); + +/** + * @brief Validate that the node’s value can be interpreted as the provided type. + * @param node PropertyTree node whose value to validate. + * @throws std::runtime_error or YAML::BadConversion if the value is not a valid type. + */ +template +void validateTypeCast(const PropertyTree& node) +{ + try + { + node.getValue().as(); + } + catch (const std::exception& e) + { + std::throw_with_nested(e); + } +} + +/** + * @brief Validator: type cast and enforce 'minimum'/'maximum' range constraints. * @param node Node to validate. - * @throws runtime_error if not found. + * @throws runtime_error if out of range. */ -void validateEnum(const PropertyTree& node); +template +void validateTypeCastWithRange(const PropertyTree& node) +{ + // Get value + const T val = [&]() { + try + { + return node.getValue().as(); + } + catch (const std::exception& e) + { + std::throw_with_nested(e); + } + }(); + + // If minimum attribute exist, validate + auto min_attr = node.getAttribute(property_attribute::MINIMUM); + if (min_attr) + { + const auto minv = min_attr->as(); + if (val < minv) + std::throw_with_nested(std::runtime_error("Property value " + std::to_string(val) + " is less than minimum " + + std::to_string(minv))); + } + + // If maximum attribute exist, validate + auto max_attr = node.getAttribute(property_attribute::MAXIMUM); + if (max_attr) + { + const auto maxv = max_attr->as(); + if (val > maxv) + std::throw_with_nested(std::runtime_error("Property value " + std::to_string(val) + " is greater than maximum " + + std::to_string(maxv))); + } +} } // namespace tesseract_common diff --git a/tesseract_common/include/tesseract_common/schema_registration.h b/tesseract_common/include/tesseract_common/schema_registration.h new file mode 100644 index 00000000000..d02063ac1f9 --- /dev/null +++ b/tesseract_common/include/tesseract_common/schema_registration.h @@ -0,0 +1,33 @@ +#ifndef TESSERACT_COMMON_SCHEMA_REGISTRATION_H +#define TESSERACT_COMMON_SCHEMA_REGISTRATION_H + +#include +#include + +namespace tesseract_common +{ +class PropertyTree; + +struct SchemaRegistrar +{ + SchemaRegistrar(const std::string& key, const std::string& path); + + SchemaRegistrar(const std::string& key, const std::function& fn); +}; + +} // namespace tesseract_common + +/// Macro to register either a file‐based schema or a function‐built schema +#define TESSERACT_REGISTER_SCHEMA(KEY, SCHEMA_SOURCE) \ + namespace \ + { \ + /* now a const POD, linter is happy */ \ + static const int _reg_##KEY = []() -> int { \ + using namespace tesseract_common; \ + /* calls the appropriate SchemaRegistrar constructor */ \ + SchemaRegistrar(#KEY, SCHEMA_SOURCE); \ + return 0; \ + }(); \ + } + +#endif // TESSERACT_COMMON_SCHEMA_REGISTRATION_H diff --git a/tesseract_common/src/property_tree.cpp b/tesseract_common/src/property_tree.cpp index 636771e3fe7..25b0c22df75 100644 --- a/tesseract_common/src/property_tree.cpp +++ b/tesseract_common/src/property_tree.cpp @@ -1,54 +1,95 @@ #include #include +#include +#include const static std::string ATTRIBUTES_KEY{ "attributes" }; const static std::string VALUE_KEY{ "value" }; const static std::string FOLLOW_KEY{ "follow" }; +const static std::string EXTRA_KEY{ "_extra" }; namespace tesseract_common { -void PropertyTree::mergeSchema(const PropertyTree& schema) +namespace property_type { - // merge schema attributes - for (const auto& pair : schema.attributes_) +std::string createList(std::string_view type) { return std::string(type) + "[]"; }; +std::string createMap(std::string_view key_type, std::string_view value_type) +{ + return "{" + std::string(key_type) + " : " + std::string(value_type) + "}"; +}; +} // namespace property_type + +void PropertyTree::mergeConfig(const YAML::Node& config, bool allow_extra_properties) +{ + // 0) Leaf-schema override for both maps and sequences: + // If this schema node has no children, but the user provided + // either a map or a sequence, just store it wholesale. + if (children_.empty() && config && (config.IsMap() || config.IsSequence())) { - if (!hasAttribute(pair.first)) - attributes_[pair.first] = pair.second; + value_ = config; + return; } - // apply default value only when property is not required and no explicit value present - auto default_it = attributes_.find(std::string(property_attribute::DEFAULT)); - if (default_it != attributes_.end()) + // 1) Apply default if no config & not required + auto def_it = attributes_.find(std::string(property_attribute::DEFAULT)); + bool required = isRequired(); + if ((!config || config.IsNull()) && def_it != attributes_.end() && !required) + value_ = YAML::Clone(def_it->second); + + // 2) Scalar or (now only non‐leaf) sequence override + if (config && config.IsScalar()) + value_ = config; + + // 3) Map: recurse into declared children + if (config && config.IsMap()) { - // determine if property is marked required - auto it = attributes_.find(std::string(property_attribute::REQUIRED)); - const bool required = (it != attributes_.end() && it->second.as()); - if (!required) + // 3a) Fill declared children + for (auto& [key, child_schema] : children_) { - if (!value_ || value_.IsNull()) - value_ = YAML::Clone(default_it->second); + auto sub = config[key]; + child_schema.mergeConfig(sub, allow_extra_properties); } - } - - // merge schema validators - for (const auto& fn : schema.validators_) - validators_.push_back(fn); - // merge children recursively - for (const auto& pair : schema.children_) + // 3b) Handle extras + if (!allow_extra_properties) + { + for (auto it = config.begin(); it != config.end(); ++it) + { + const auto& key = it->first.as(); + if (children_.count(key) == 0) + { + auto& extra_node = children_[key]; + extra_node.setAttribute(EXTRA_KEY, YAML::Node(true)); + extra_node.mergeConfig(it->second, allow_extra_properties); + } + } + } + } + // 4) Sequence (non-leaf): apply wildcard or numeric schema + else if (config && config.IsSequence()) { - if (children_.find(pair.first) == children_.end()) - children_[pair.first] = PropertyTree(); + PropertyTree elem_schema; + auto w = children_.find("*"); + if (w != children_.end()) + elem_schema = w->second; - children_[pair.first].mergeSchema(pair.second); + children_.clear(); + int idx = 0; + for (const auto& elt : config) + { + std::string key = std::to_string(idx++); + children_[key] = elem_schema; + children_[key].mergeConfig(elt, allow_extra_properties); + } } + // 5) Otherwise leave value_ (possibly set by default) as-is. } -void PropertyTree::validate() const +void PropertyTree::validate(bool allow_extra_properties) const { - // run custom validators - for (const auto& vfn : validators_) - vfn(*this); + // check if it is an extra property not found in schema + if (!allow_extra_properties && hasAttribute(EXTRA_KEY)) + std::throw_with_nested(std::runtime_error("Property does not exist in schema")); // recurse children for (const auto& kv : children_) @@ -62,6 +103,14 @@ void PropertyTree::validate() const std::throw_with_nested(std::runtime_error("Validation failed for property: " + kv.first)); } } + + // if not required and null skip validators + if (!isRequired() && isNull()) + return; + + // run custom validators + for (const auto& vfn : validators_) + vfn(*this); } /// Add a custom validator for this node @@ -81,6 +130,8 @@ const YAML::Node& PropertyTree::getValue() const { return value_; } bool PropertyTree::isNull() const { return value_.IsNull(); } +bool PropertyTree::isContainer() const { return !children_.empty(); } + std::vector PropertyTree::keys() const { std::vector res; @@ -93,6 +144,54 @@ std::vector PropertyTree::keys() const void PropertyTree::setAttribute(std::string_view name, const YAML::Node& attr) { attributes_[std::string(name)] = attr; + + if (name == property_attribute::REQUIRED) + validators_.emplace_back(validateRequired); + + if (name == property_attribute::ENUM) + validators_.emplace_back(validateEnum); + + if (name == property_attribute::TYPE) + { + const auto str_type = attr.as(); + + std::optional is_sequence = isSequenceType(str_type); + if (is_sequence.has_value()) + validators_.emplace_back(validateSequence); + + if (str_type == property_type::STRING) + validators_.emplace_back(validateTypeCast); + else if (str_type == property_type::BOOL) + validators_.emplace_back(validateTypeCast); + else if (str_type == property_type::CHAR) + validators_.emplace_back(validateTypeCast); + else if (str_type == property_type::FLOAT) + validators_.emplace_back(validateTypeCastWithRange); + else if (str_type == property_type::DOUBLE) + validators_.emplace_back(validateTypeCastWithRange); + else if (str_type == property_type::INT) + validators_.emplace_back(validateTypeCastWithRange); + else if (str_type == property_type::UNSIGNED_INT) + validators_.emplace_back(validateTypeCastWithRange); + else if (str_type == property_type::LONG_INT) + validators_.emplace_back(validateTypeCastWithRange); + else if (str_type == property_type::LONG_UNSIGNED_INT) + validators_.emplace_back(validateTypeCastWithRange); + else if (str_type == property_type::EIGEN_ISOMETRY_3D) + validators_.emplace_back(validateTypeCast); + // else if (str_type == property_type::EIGEN_MATRIX_2D) + // validators_.emplace_back(validateTypeCast); + else if (str_type == property_type::EIGEN_VECTOR_XD) + validators_.emplace_back(validateTypeCast); + // else if (str_type == property_type::EIGEN_MATRIX_2D) + // validators_.emplace_back(validateTypeCast); + else if (str_type == property_type::EIGEN_VECTOR_2D) + validators_.emplace_back(validateTypeCast); + // else if (str_type == property_type::EIGEN_MATRIX_3D) + // validators_.emplace_back(validateTypeCast); + else if (str_type == property_type::EIGEN_VECTOR_3D) + validators_.emplace_back(validateTypeCast); + } } void PropertyTree::setAttribute(std::string_view name, std::string_view attr) @@ -151,9 +250,16 @@ PropertyTree PropertyTree::fromYAML(const YAML::Node& node) if (node.size() > 1) throw std::runtime_error("'follow' cannot be mixed with other entries"); - auto key = node[FOLLOW_KEY].as(); - auto registry = SchemaRegistry::instance(); - return registry->contains(key) ? registry->get(key) : SchemaRegistry::loadFile(key); + try + { + auto key = node[FOLLOW_KEY].as(); + auto registry = SchemaRegistry::instance(); + return registry->contains(key) ? registry->get(key) : SchemaRegistry::loadFile(key); + } + catch (const std::exception& e) + { + std::throw_with_nested(e); + } } PropertyTree tree; @@ -226,34 +332,28 @@ YAML::Node PropertyTree::toYAML(bool exclude_attributes) const return node; } +std::optional isSequenceType(std::string_view type) +{ + if (type.size() < 2) + return std::nullopt; + + if (type.substr(type.size() - 2) != "[]") + return std::nullopt; + + return std::string(type.substr(0, type.size() - 2)); +} + void validateRequired(const PropertyTree& node) { auto req_attr = node.getAttribute(property_attribute::REQUIRED); if (req_attr && req_attr->as()) { // if leaf node with no value or null - if (!node.getValue() || node.getValue().IsNull()) + if (!node.isContainer() && node.isNull()) std::throw_with_nested(std::runtime_error("Required property missing or null")); } } -void validateRange(const PropertyTree& node) -{ - auto min_attr = node.getAttribute(property_attribute::MINIMUM); - auto max_attr = node.getAttribute(property_attribute::MAXIMUM); - if (min_attr && max_attr) - { - const auto minv = min_attr->as(); - const auto maxv = max_attr->as(); - const auto val = node.getValue().as(); - if (val < minv || val > maxv) - { - std::throw_with_nested(std::runtime_error("Property value " + std::to_string(val) + " out of range [" + - std::to_string(minv) + "," + std::to_string(maxv) + "]")); - } - } -} - void validateEnum(const PropertyTree& node) { auto enum_attr = node.getAttribute(property_attribute::ENUM); @@ -269,4 +369,51 @@ void validateEnum(const PropertyTree& node) } } +void validateMap(const PropertyTree& node) +{ + if (node.getValue().IsNull() && !node.getValue().IsMap()) + std::throw_with_nested(std::runtime_error("Property value is not of type YAML::NodeType::Map")); +} + +void validateSequence(const PropertyTree& node) +{ + if (!node.getValue().IsSequence()) + std::throw_with_nested(std::runtime_error("Property value is not of type YAML::NodeType::Sequence")); +} + +void validateCustomType(const PropertyTree& node) +{ + const auto type_attr = node.getAttribute(property_attribute::TYPE); + if (!type_attr.has_value()) + std::throw_with_nested(std::runtime_error("Custom type validator was added buy type attribute does not exist")); + + const auto type_str = type_attr.value().as(); + std::optional is_sequence = isSequenceType(type_str); + + auto registry = SchemaRegistry::instance(); + if (!is_sequence.has_value()) + { + if (!registry->contains(type_str)) + std::throw_with_nested(std::runtime_error("No scheme registry entry found for key: " + type_str)); + + PropertyTree schema = registry->get(type_str); + schema.mergeConfig(node.getValue()); + schema.validate(); + } + else + { + if (!registry->contains(is_sequence.value())) + std::throw_with_nested(std::runtime_error("No scheme registry entry found for key: " + is_sequence.value())); + + const YAML::Node& sequence = node.getValue(); + PropertyTree schema = registry->get(is_sequence.value()); + for (auto it = sequence.begin(); it != sequence.end(); ++it) + { + PropertyTree copy_schema(schema); + copy_schema.mergeConfig(*it); + copy_schema.validate(); + } + } +} + } // namespace tesseract_common diff --git a/tesseract_common/src/property_tree_demo.cpp b/tesseract_common/src/property_tree_demo.cpp index b8e5f84f09f..5f26908bca6 100644 --- a/tesseract_common/src/property_tree_demo.cpp +++ b/tesseract_common/src/property_tree_demo.cpp @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include using namespace tesseract_common; @@ -16,7 +18,6 @@ PropertyTree buildConfigSchema() return_options[0] = "Error"; return_options[1] = "Successful"; cfg.setAttribute("return_options", YAML::Node(return_options)); - cfg.addValidator(validateRequired); // conditional { @@ -25,7 +26,6 @@ PropertyTree buildConfigSchema() prop.setAttribute(property_attribute::DEFAULT, true); prop.setAttribute(property_attribute::DOC, "Enable conditional execution"); prop.setAttribute(property_attribute::REQUIRED, true); - prop.addValidator(validateRequired); } // inputs { @@ -38,7 +38,6 @@ PropertyTree buildConfigSchema() prop.setAttribute(property_attribute::TYPE, property_type::STRING); prop.setAttribute(property_attribute::DOC, "The composite instruction"); prop.setAttribute(property_attribute::REQUIRED, true); - prop.addValidator(validateRequired); } // environment { @@ -47,7 +46,6 @@ PropertyTree buildConfigSchema() prop.setAttribute(property_attribute::TYPE, property_type::STRING); prop.setAttribute(property_attribute::DOC, "The tesseract environment"); prop.setAttribute(property_attribute::REQUIRED, true); - prop.addValidator(validateRequired); } // profiles { @@ -55,7 +53,6 @@ PropertyTree buildConfigSchema() prop.setAttribute(property_attribute::TYPE, property_type::STRING); prop.setAttribute(property_attribute::DOC, "The tesseract profiles"); prop.setAttribute(property_attribute::REQUIRED, true); - prop.addValidator(validateRequired); // prof.setAttribute("enum", YAML::Load(R"(["A","B"])")); // prof.addValidator(validateEnum); // prof.addValidator([](auto const& node, auto& errs, const auto& path){ @@ -74,7 +71,6 @@ PropertyTree buildConfigSchema() auto& prop = outs.get("program"); prop.setAttribute(property_attribute::TYPE, property_type::STRING); prop.setAttribute(property_attribute::REQUIRED, true); - prop.addValidator(validateRequired); } // format_result_as_input { @@ -85,7 +81,91 @@ PropertyTree buildConfigSchema() return schema; } -std::string str = R"(config: +tesseract_common::PropertyTree getTaskComposerGraphSchema() +{ + using namespace tesseract_common; + PropertyTree schema; + schema.setAttribute(property_attribute::DOC, "TaskComposerGraph"); + { + auto& prop = schema.get("class"); + prop.setAttribute(property_attribute::TYPE, property_type::STRING); + prop.setAttribute(property_attribute::DOC, "The class factory name"); + prop.setAttribute(property_attribute::REQUIRED, true); + } + + auto& config_schema = schema.get("config"); + config_schema.setAttribute(property_attribute::REQUIRED, true); + { + auto& prop = config_schema.get("conditional"); + prop.setAttribute(property_attribute::TYPE, property_type::BOOL); + prop.setAttribute(property_attribute::DEFAULT, true); + prop.setAttribute(property_attribute::DOC, "Enable conditional execution"); + prop.setAttribute(property_attribute::REQUIRED, true); + } + + { + auto& inputs = config_schema.get("inputs"); + inputs.setAttribute(property_attribute::DOC, "Input sources"); + inputs.addValidator(validateMap); + } + + { + auto& outputs = config_schema.get("outputs"); + outputs.setAttribute(property_attribute::DOC, "Output sources"); + outputs.addValidator(validateMap); + } + + { + auto& prop = config_schema.get("nodes"); + prop.setAttribute(property_attribute::DOC, "Map of all task nodes"); + prop.setAttribute(property_attribute::REQUIRED, true); + prop.addValidator(validateMap); + } + + { + auto& prop = config_schema.get("edges"); + prop.setAttribute(property_attribute::TYPE, "TaskComposerGraphEdge[]"); + prop.setAttribute(property_attribute::DOC, "List of graph edges"); + prop.setAttribute(property_attribute::REQUIRED, true); + prop.addValidator(validateCustomType); + } + + { + auto& prop = config_schema.get("terminals"); + prop.setAttribute(property_attribute::TYPE, property_type::createList(property_type::STRING)); + prop.setAttribute(property_attribute::DOC, "List of terminal tasks"); + prop.setAttribute(property_attribute::REQUIRED, true); + prop.addValidator(validateTypeCast>); + } + + return schema; +} + +tesseract_common::PropertyTree getTaskComposerGraphEdgeSchema() +{ + using namespace tesseract_common; + PropertyTree schema; + schema.setAttribute(property_attribute::DOC, "TaskComposerGraphEdge"); + { + auto& prop = schema.get("source"); + prop.setAttribute(property_attribute::TYPE, property_type::STRING); + prop.setAttribute(property_attribute::DOC, "The source task name"); + prop.setAttribute(property_attribute::REQUIRED, true); + } + + { + auto& prop = schema.get("destinations"); + prop.setAttribute(property_attribute::TYPE, property_type::createList(property_type::STRING)); + prop.setAttribute(property_attribute::DOC, "The list of destination task name"); + prop.setAttribute(property_attribute::REQUIRED, true); + } + + return schema; +} + +TESSERACT_REGISTER_SCHEMA(TaskComposerGraphEdge, getTaskComposerGraphEdgeSchema) + +const std::string str = R"(config: conditional: true inputs: program: input_data @@ -95,7 +175,7 @@ std::string str = R"(config: program: output_data format_result_as_input: false)"; -std::string str2 = R"(config: +const std::string str2 = R"(config: conditional: true inputs: program: input_data @@ -104,7 +184,7 @@ std::string str2 = R"(config: outputs: program: output_data)"; -std::string str3 = R"(config: +const std::string str3 = R"(config: conditional: true inputs: program: input_data @@ -113,22 +193,47 @@ std::string str3 = R"(config: outputs: programs: output_data)"; -int main() -{ - // Load configuration from YAML - PropertyTree prop = PropertyTree::fromYAML(YAML::Load(str2)); +const std::string str4 = R"(config: + conditional: true + should_not_exist: true + inputs: + program: input_data + environment: environment + profiles: profiles + outputs: + program: output_data)"; + +const std::string graph_str = R"( +class: PipelineTaskFactory +config: + conditional: true + nodes: + StartTask: + class: StartTaskFactory + config: + conditional: false + DoneTask: + class: DoneTaskFactory + config: + conditional: false + edges: + - source: StartTask + destinations: [DoneTask] + terminals: [DoneTask])"; +int testBasic() +{ // Parse schema from external file (config_schema.yaml) PropertyTree schema = buildConfigSchema(); // Merge schema metadata into config tree - prop.mergeSchema(schema); - std::cout << "Exclude attrubutes:\n" << prop.toYAML() << "\n\n"; - std::cout << "Include attrubutes:\n" << prop.toYAML(false) << "\n\n"; + schema.mergeConfig(YAML::Load(str4)); + std::cout << "Exclude attrubutes:\n" << schema.toYAML() << "\n\n"; + std::cout << "Include attrubutes:\n" << schema.toYAML(false) << "\n\n"; try { - prop.validate(); + schema.validate(); } catch (const std::exception& e) { @@ -136,7 +241,33 @@ int main() return 1; } - bool cond = prop.get("config").get("conditional").getValue().as(); + bool cond = schema.get("config").get("conditional").getValue().as(); std::cout << "conditional = " << std::boolalpha << cond << "\n"; return 0; } + +int testCustomType() +{ + // Parse schema from external file (config_schema.yaml) + PropertyTree schema = getTaskComposerGraphSchema(); + std::cout << "Schema:\n" << schema.toYAML(false) << "\n\n"; + + // Merge schema metadata into config tree + schema.mergeConfig(YAML::Load(graph_str)); + std::cout << "Exclude attrubutes:\n" << schema.toYAML() << "\n\n"; + std::cout << "Include attrubutes:\n" << schema.toYAML(false) << "\n\n"; + + try + { + schema.validate(); + } + catch (const std::exception& e) + { + tesseract_common::printNestedException(e); + return 1; + } + + return 0; +} + +int main() { return testCustomType(); } diff --git a/tesseract_common/src/schema_registration.cpp b/tesseract_common/src/schema_registration.cpp new file mode 100644 index 00000000000..9f3950dbe57 --- /dev/null +++ b/tesseract_common/src/schema_registration.cpp @@ -0,0 +1,19 @@ +#include +#include +#include + +namespace tesseract_common +{ +SchemaRegistrar::SchemaRegistrar(const std::string& key, const std::string& path) +{ + auto reg = SchemaRegistry::instance(); + reg->registerSchemaFromFile(key, path); +} + +SchemaRegistrar::SchemaRegistrar(const std::string& key, const std::function& fn) +{ + auto reg = SchemaRegistry::instance(); + PropertyTree tree = fn(); + reg->registerSchema(key, tree); +} +} // namespace tesseract_common From 1341e199976fc246f9dbf29c15c24c7959f08101 Mon Sep 17 00:00:00 2001 From: Levi Armstrong Date: Wed, 18 Jun 2025 11:02:41 -0500 Subject: [PATCH 09/15] fixup --- .../include/tesseract_common/property_tree.h | 20 +- tesseract_common/src/property_tree.cpp | 30 ++- tesseract_common/src/property_tree_demo.cpp | 204 +++++++++++++++++- 3 files changed, 243 insertions(+), 11 deletions(-) diff --git a/tesseract_common/include/tesseract_common/property_tree.h b/tesseract_common/include/tesseract_common/property_tree.h index 55b2d0e47a8..4ac7cc5e5dc 100644 --- a/tesseract_common/include/tesseract_common/property_tree.h +++ b/tesseract_common/include/tesseract_common/property_tree.h @@ -39,6 +39,9 @@ constexpr std::string_view LONG_UNSIGNED_INT{ "long unsigned int" }; constexpr std::string_view FLOAT{ "float" }; constexpr std::string_view DOUBLE{ "double" }; +// Container of properties +constexpr std::string_view CONTAINER{ "YAML::NodeType::Map" }; + // Eigen Types constexpr std::string_view EIGEN_ISOMETRY_3D{ "Eigen::Isometry3d" }; // constexpr std::string_view EIGEN_MATRIX_XD{ "Eigen::MatrixXd" }; @@ -235,10 +238,17 @@ class PropertyTree /** * @brief Check if type is a sequence * @param type The type to check - * @return If it is a sequence the underlying type is returned + * @return If it is a sequence, the underlying type is returned */ std::optional isSequenceType(std::string_view type); +/** + * @brief Check if type is a map + * @param type The type to check + * @return If it is a map, the underlying type is returned + */ +std::optional isMapType(std::string_view type); + /** * @brief Validator: ensure 'required' attribute is present and non-null. * @param node Node to validate. @@ -267,6 +277,14 @@ void validateMap(const PropertyTree& node); */ void validateSequence(const PropertyTree& node); +/** + * @brief Validator: ensure property is a container of child properties + * The property should have children and the value should be null + * @param node Node to validate. + * @throws runtime_error if not correct. + */ +void validateContainer(const PropertyTree& node); + void validateCustomType(const PropertyTree& node); /** diff --git a/tesseract_common/src/property_tree.cpp b/tesseract_common/src/property_tree.cpp index 25b0c22df75..4bf3753e8ce 100644 --- a/tesseract_common/src/property_tree.cpp +++ b/tesseract_common/src/property_tree.cpp @@ -159,7 +159,13 @@ void PropertyTree::setAttribute(std::string_view name, const YAML::Node& attr) if (is_sequence.has_value()) validators_.emplace_back(validateSequence); - if (str_type == property_type::STRING) + std::optional is_map = isMapType(str_type); + if (is_map.has_value()) + validators_.emplace_back(validateMap); + + if (str_type == property_type::CONTAINER) + validators_.emplace_back(validateContainer); + else if (str_type == property_type::STRING) validators_.emplace_back(validateTypeCast); else if (str_type == property_type::BOOL) validators_.emplace_back(validateTypeCast); @@ -343,6 +349,17 @@ std::optional isSequenceType(std::string_view type) return std::string(type.substr(0, type.size() - 2)); } +std::optional isMapType(std::string_view type) +{ + if (type.size() < 2) + return std::nullopt; + + if (type.substr(type.size() - 2) != "{}") + return std::nullopt; + + return std::string(type.substr(0, type.size() - 2)); +} + void validateRequired(const PropertyTree& node) { auto req_attr = node.getAttribute(property_attribute::REQUIRED); @@ -371,7 +388,7 @@ void validateEnum(const PropertyTree& node) void validateMap(const PropertyTree& node) { - if (node.getValue().IsNull() && !node.getValue().IsMap()) + if (!node.getValue().IsMap()) std::throw_with_nested(std::runtime_error("Property value is not of type YAML::NodeType::Map")); } @@ -381,6 +398,15 @@ void validateSequence(const PropertyTree& node) std::throw_with_nested(std::runtime_error("Property value is not of type YAML::NodeType::Sequence")); } +void validateContainer(const PropertyTree& node) +{ + if (!node.isContainer()) + std::throw_with_nested(std::runtime_error("Property is not a container")); + + if (!node.isNull()) + std::throw_with_nested(std::runtime_error("Property is a container but value is not null")); +} + void validateCustomType(const PropertyTree& node) { const auto type_attr = node.getAttribute(property_attribute::TYPE); diff --git a/tesseract_common/src/property_tree_demo.cpp b/tesseract_common/src/property_tree_demo.cpp index 5f26908bca6..5790eed28cb 100644 --- a/tesseract_common/src/property_tree_demo.cpp +++ b/tesseract_common/src/property_tree_demo.cpp @@ -12,6 +12,7 @@ PropertyTree buildConfigSchema() { PropertyTree schema; auto& cfg = schema.get("config"); + cfg.setAttribute(property_attribute::TYPE, property_type::CONTAINER); cfg.setAttribute(property_attribute::DOC, "Main config for plugin"); cfg.setAttribute(property_attribute::REQUIRED, true); std::map return_options; @@ -30,6 +31,7 @@ PropertyTree buildConfigSchema() // inputs { auto& inputs = cfg.get("inputs"); + inputs.setAttribute(property_attribute::TYPE, property_type::CONTAINER); inputs.setAttribute(property_attribute::DOC, "Input sources"); inputs.setAttribute(property_attribute::REQUIRED, true); // program @@ -67,10 +69,15 @@ PropertyTree buildConfigSchema() } // outputs { - auto& outs = cfg.get("outputs"); - auto& prop = outs.get("program"); - prop.setAttribute(property_attribute::TYPE, property_type::STRING); - prop.setAttribute(property_attribute::REQUIRED, true); + auto& outputs = cfg.get("outputs"); + outputs.setAttribute(property_attribute::TYPE, property_type::CONTAINER); + outputs.setAttribute(property_attribute::DOC, "Output sources"); + outputs.setAttribute(property_attribute::REQUIRED, true); + { + auto& prop = outputs.get("program"); + prop.setAttribute(property_attribute::TYPE, property_type::STRING); + prop.setAttribute(property_attribute::REQUIRED, true); + } } // format_result_as_input { @@ -85,6 +92,7 @@ tesseract_common::PropertyTree getTaskComposerGraphSchema() { using namespace tesseract_common; PropertyTree schema; + schema.setAttribute(property_attribute::TYPE, property_type::CONTAINER); schema.setAttribute(property_attribute::DOC, "TaskComposerGraph"); { auto& prop = schema.get("class"); @@ -94,6 +102,7 @@ tesseract_common::PropertyTree getTaskComposerGraphSchema() } auto& config_schema = schema.get("config"); + config_schema.setAttribute(property_attribute::TYPE, property_type::CONTAINER); config_schema.setAttribute(property_attribute::REQUIRED, true); { auto& prop = config_schema.get("conditional"); @@ -105,21 +114,21 @@ tesseract_common::PropertyTree getTaskComposerGraphSchema() { auto& inputs = config_schema.get("inputs"); + inputs.setAttribute(property_attribute::TYPE, property_type::CONTAINER); inputs.setAttribute(property_attribute::DOC, "Input sources"); - inputs.addValidator(validateMap); } { auto& outputs = config_schema.get("outputs"); + outputs.setAttribute(property_attribute::TYPE, property_type::CONTAINER); outputs.setAttribute(property_attribute::DOC, "Output sources"); - outputs.addValidator(validateMap); } { auto& prop = config_schema.get("nodes"); + prop.setAttribute(property_attribute::TYPE, "TaskComposerGraphNode{}"); prop.setAttribute(property_attribute::DOC, "Map of all task nodes"); prop.setAttribute(property_attribute::REQUIRED, true); - prop.addValidator(validateMap); } { @@ -145,6 +154,7 @@ tesseract_common::PropertyTree getTaskComposerGraphEdgeSchema() { using namespace tesseract_common; PropertyTree schema; + schema.setAttribute(property_attribute::TYPE, property_type::CONTAINER); schema.setAttribute(property_attribute::DOC, "TaskComposerGraphEdge"); { auto& prop = schema.get("source"); @@ -158,6 +168,138 @@ tesseract_common::PropertyTree getTaskComposerGraphEdgeSchema() prop.setAttribute(property_attribute::TYPE, property_type::createList(property_type::STRING)); prop.setAttribute(property_attribute::DOC, "The list of destination task name"); prop.setAttribute(property_attribute::REQUIRED, true); + prop.addValidator(validateTypeCast>); + } + + return schema; +} + +tesseract_common::PropertyTree getTaskComposerRasterOnlySchema() +{ + using namespace tesseract_common; + PropertyTree schema; + schema.setAttribute(property_attribute::TYPE, property_type::CONTAINER); + schema.setAttribute(property_attribute::DOC, "TaskComposerGraph"); + std::map return_options; + return_options[0] = "Error"; + return_options[1] = "Successful"; + schema.setAttribute("return_options", YAML::Node(return_options)); + { + auto& prop = schema.get("class"); + prop.setAttribute(property_attribute::TYPE, property_type::STRING); + prop.setAttribute(property_attribute::DOC, "The class factory name"); + prop.setAttribute(property_attribute::REQUIRED, true); + } + + auto& config_schema = schema.get("config"); + config_schema.setAttribute(property_attribute::TYPE, property_type::CONTAINER); + config_schema.setAttribute(property_attribute::REQUIRED, true); + { + auto& prop = config_schema.get("conditional"); + prop.setAttribute(property_attribute::TYPE, property_type::BOOL); + prop.setAttribute(property_attribute::DEFAULT, true); + prop.setAttribute(property_attribute::DOC, "Enable conditional execution"); + prop.setAttribute(property_attribute::REQUIRED, true); + } + + { + auto& inputs = config_schema.get("inputs"); + inputs.setAttribute(property_attribute::TYPE, property_type::CONTAINER); + inputs.setAttribute(property_attribute::DOC, "Input sources"); + + // program + { + auto& prop = inputs.get("program"); + prop.setAttribute(property_attribute::TYPE, property_type::STRING); + prop.setAttribute(property_attribute::DOC, "The composite instruction"); + prop.setAttribute(property_attribute::REQUIRED, true); + } + // environment + { + auto& prop = inputs.get("environment"); + prop.setAttribute(property_attribute::TYPE, property_type::STRING); + prop.setAttribute(property_attribute::DOC, "The tesseract environment"); + prop.setAttribute(property_attribute::REQUIRED, true); + } + } + + { + auto& outputs = config_schema.get("outputs"); + outputs.setAttribute(property_attribute::TYPE, property_type::CONTAINER); + outputs.setAttribute(property_attribute::DOC, "Output sources"); + // program + { + auto& prop = outputs.get("program"); + prop.setAttribute(property_attribute::TYPE, property_type::STRING); + prop.setAttribute(property_attribute::DOC, "The composite instruction"); + prop.setAttribute(property_attribute::REQUIRED, true); + } + } + + { + auto& raster = config_schema.get("raster"); + raster.setAttribute(property_attribute::TYPE, property_type::CONTAINER); + raster.setAttribute(property_attribute::DOC, "The raster task"); + raster.setAttribute(property_attribute::REQUIRED, true); + { + auto& prop = raster.get("task"); + prop.setAttribute(property_attribute::TYPE, property_type::STRING); + prop.setAttribute(property_attribute::DOC, "The task name"); + prop.setAttribute(property_attribute::REQUIRED, true); + } + auto& raster_config = raster.get("config"); + raster_config.setAttribute(property_attribute::TYPE, property_type::CONTAINER); + raster_config.setAttribute(property_attribute::REQUIRED, true); + { + auto& prop = raster_config.get("abort_terminal"); + prop.setAttribute(property_attribute::TYPE, property_type::INT); + prop.setAttribute(property_attribute::MINIMUM, 0); + prop.setAttribute(property_attribute::DOC, "The abort terminal"); + } + { + auto& prop = raster_config.get("remapping"); + prop.setAttribute(property_attribute::TYPE, + property_type::createMap(property_type::STRING, property_type::STRING)); + prop.setAttribute(property_attribute::DOC, "The remapping of input and output keys"); + } + { + auto& prop = raster_config.get("indexing"); + prop.setAttribute(property_attribute::TYPE, property_type::createList(property_type::STRING)); + prop.setAttribute(property_attribute::DOC, "The input and output keys to index"); + } + } + + { + auto& transition = config_schema.get("transition"); + transition.setAttribute(property_attribute::TYPE, property_type::CONTAINER); + transition.setAttribute(property_attribute::DOC, "The transition task"); + transition.setAttribute(property_attribute::REQUIRED, true); + { + auto& prop = transition.get("task"); + prop.setAttribute(property_attribute::TYPE, property_type::STRING); + prop.setAttribute(property_attribute::DOC, "The task name"); + prop.setAttribute(property_attribute::REQUIRED, true); + } + auto& transition_config = transition.get("config"); + transition_config.setAttribute(property_attribute::TYPE, property_type::CONTAINER); + transition_config.setAttribute(property_attribute::REQUIRED, true); + { + auto& prop = transition_config.get("abort_terminal"); + prop.setAttribute(property_attribute::TYPE, property_type::INT); + prop.setAttribute(property_attribute::MINIMUM, 0); + prop.setAttribute(property_attribute::DOC, "The abort terminal"); + } + { + auto& prop = transition_config.get("remapping"); + prop.setAttribute(property_attribute::TYPE, + property_type::createMap(property_type::STRING, property_type::STRING)); + prop.setAttribute(property_attribute::DOC, "The remapping of input and output keys"); + } + { + auto& prop = transition_config.get("indexing"); + prop.setAttribute(property_attribute::TYPE, property_type::createList(property_type::STRING)); + prop.setAttribute(property_attribute::DOC, "The input and output keys to index"); + } } return schema; @@ -221,6 +363,24 @@ class: PipelineTaskFactory destinations: [DoneTask] terminals: [DoneTask])"; +std::string raster_only_str = R"( +class: RasterOnlyTaskFactory +config: + conditional: true + inputs: + program: input_data + environment: environment + outputs: + program: output_data + raster: + task: CartesianPipeline + config: + indexing: [input_data, output_data] + transition: + task: CartesianPipeline + config: + indexing: [input_data, output_data])"; + int testBasic() { // Parse schema from external file (config_schema.yaml) @@ -270,4 +430,32 @@ int testCustomType() return 0; } -int main() { return testCustomType(); } +int testRasterOnlyType() +{ + // Parse schema from external file (config_schema.yaml) + PropertyTree schema = getTaskComposerRasterOnlySchema(); + std::cout << "Schema:\n" << schema.toYAML(false) << "\n\n"; + + // Merge schema metadata into config tree + schema.mergeConfig(YAML::Load(raster_only_str)); + std::cout << "Exclude attrubutes:\n" << schema.toYAML() << "\n\n"; + std::cout << "Include attrubutes:\n" << schema.toYAML(false) << "\n\n"; + + try + { + schema.validate(); + } + catch (const std::exception& e) + { + tesseract_common::printNestedException(e); + return 1; + } + + return 0; +} + +int main() +{ + // return testCustomType(); + return testRasterOnlyType(); +} From 8eb096712c3b85649333a430d2e5e6d00441f187 Mon Sep 17 00:00:00 2001 From: Levi Armstrong Date: Wed, 18 Jun 2025 11:13:51 -0500 Subject: [PATCH 10/15] fixup --- .../include/tesseract_common/property_tree.h | 11 +++++------ tesseract_common/src/property_tree.cpp | 5 +---- tesseract_common/src/property_tree_demo.cpp | 6 ++---- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/tesseract_common/include/tesseract_common/property_tree.h b/tesseract_common/include/tesseract_common/property_tree.h index 4ac7cc5e5dc..22c8873cfe0 100644 --- a/tesseract_common/include/tesseract_common/property_tree.h +++ b/tesseract_common/include/tesseract_common/property_tree.h @@ -14,19 +14,18 @@ namespace tesseract_common namespace property_type { /** - * @brief A utility for constructing the list type + * @brief A utility for constructing the std::vector * @param type The type assoicated with the list * @return The string representation of the std::vector, aka. type[] */ std::string createList(std::string_view type); /** - * @brief A utility for constructing the map type - * @param key_type The type assoicated with the map key - * @param value_type The type assoicated with the map value - * @return The string representation of the std::make, aka. {key_type : value_type} + * @brief A utility for constructing the std::map + * @param type The value type assoicated with the map + * @return The string representation of the std::map, aka. type{} */ -std::string createMap(std::string_view key_type, std::string_view value_type); +std::string createMap(std::string_view type); // Integral Types constexpr std::string_view BOOL{ "bool" }; diff --git a/tesseract_common/src/property_tree.cpp b/tesseract_common/src/property_tree.cpp index 4bf3753e8ce..53b78a596ea 100644 --- a/tesseract_common/src/property_tree.cpp +++ b/tesseract_common/src/property_tree.cpp @@ -13,10 +13,7 @@ namespace tesseract_common namespace property_type { std::string createList(std::string_view type) { return std::string(type) + "[]"; }; -std::string createMap(std::string_view key_type, std::string_view value_type) -{ - return "{" + std::string(key_type) + " : " + std::string(value_type) + "}"; -}; +std::string createMap(std::string_view type) { return std::string(type) + "{}"; }; } // namespace property_type void PropertyTree::mergeConfig(const YAML::Node& config, bool allow_extra_properties) diff --git a/tesseract_common/src/property_tree_demo.cpp b/tesseract_common/src/property_tree_demo.cpp index 5790eed28cb..3d538a77c35 100644 --- a/tesseract_common/src/property_tree_demo.cpp +++ b/tesseract_common/src/property_tree_demo.cpp @@ -258,8 +258,7 @@ tesseract_common::PropertyTree getTaskComposerRasterOnlySchema() } { auto& prop = raster_config.get("remapping"); - prop.setAttribute(property_attribute::TYPE, - property_type::createMap(property_type::STRING, property_type::STRING)); + prop.setAttribute(property_attribute::TYPE, property_type::createMap(property_type::STRING)); prop.setAttribute(property_attribute::DOC, "The remapping of input and output keys"); } { @@ -291,8 +290,7 @@ tesseract_common::PropertyTree getTaskComposerRasterOnlySchema() } { auto& prop = transition_config.get("remapping"); - prop.setAttribute(property_attribute::TYPE, - property_type::createMap(property_type::STRING, property_type::STRING)); + prop.setAttribute(property_attribute::TYPE, property_type::createMap(property_type::STRING)); prop.setAttribute(property_attribute::DOC, "The remapping of input and output keys"); } { From 44e76fc87e3d5162e371e0bc7deef439f41b1509 Mon Sep 17 00:00:00 2001 From: Levi Armstrong Date: Wed, 18 Jun 2025 17:38:23 -0500 Subject: [PATCH 11/15] fixup --- tesseract_common/src/property_tree.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tesseract_common/src/property_tree.cpp b/tesseract_common/src/property_tree.cpp index 53b78a596ea..8269a9a02d7 100644 --- a/tesseract_common/src/property_tree.cpp +++ b/tesseract_common/src/property_tree.cpp @@ -3,10 +3,10 @@ #include #include -const static std::string ATTRIBUTES_KEY{ "attributes" }; -const static std::string VALUE_KEY{ "value" }; -const static std::string FOLLOW_KEY{ "follow" }; +const static std::string ATTRIBUTES_KEY{ "_attributes" }; +const static std::string VALUE_KEY{ "_value" }; const static std::string EXTRA_KEY{ "_extra" }; +const static std::string FOLLOW_KEY{ "follow" }; namespace tesseract_common { From 4d1d5766a1ed7772b5a5e55a961055653bd77e92 Mon Sep 17 00:00:00 2001 From: Levi Armstrong Date: Wed, 18 Jun 2025 18:06:45 -0500 Subject: [PATCH 12/15] fixup --- .../include/tesseract_common/property_tree.h | 1 + tesseract_common/src/property_tree.cpp | 40 ++++++++++++------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/tesseract_common/include/tesseract_common/property_tree.h b/tesseract_common/include/tesseract_common/property_tree.h index 22c8873cfe0..a41bf68cd14 100644 --- a/tesseract_common/include/tesseract_common/property_tree.h +++ b/tesseract_common/include/tesseract_common/property_tree.h @@ -231,6 +231,7 @@ class PropertyTree YAML::Node follow_; /**< Follow stored at this node */ std::map attributes_; /**< Metadata attributes */ std::map children_; /**< Nested child nodes */ + std::vector keys_; /**< Nested child keys */ std::vector validators_; /**< Validators to invoke */ }; diff --git a/tesseract_common/src/property_tree.cpp b/tesseract_common/src/property_tree.cpp index 8269a9a02d7..541e9e4ac98 100644 --- a/tesseract_common/src/property_tree.cpp +++ b/tesseract_common/src/property_tree.cpp @@ -55,7 +55,7 @@ void PropertyTree::mergeConfig(const YAML::Node& config, bool allow_extra_proper const auto& key = it->first.as(); if (children_.count(key) == 0) { - auto& extra_node = children_[key]; + auto& extra_node = get(key); extra_node.setAttribute(EXTRA_KEY, YAML::Node(true)); extra_node.mergeConfig(it->second, allow_extra_properties); } @@ -71,10 +71,12 @@ void PropertyTree::mergeConfig(const YAML::Node& config, bool allow_extra_proper elem_schema = w->second; children_.clear(); + keys_.clear(); int idx = 0; for (const auto& elt : config) { std::string key = std::to_string(idx++); + keys_.push_back(key); children_[key] = elem_schema; children_[key].mergeConfig(elt, allow_extra_properties); } @@ -110,10 +112,22 @@ void PropertyTree::validate(bool allow_extra_properties) const vfn(*this); } -/// Add a custom validator for this node void PropertyTree::addValidator(ValidatorFn fn) { validators_.push_back(std::move(fn)); } -PropertyTree& PropertyTree::get(std::string_view key) { return children_[std::string(key)]; } +PropertyTree& PropertyTree::get(std::string_view key) +{ + auto k = std::string(key); + auto it = children_.find(k); + if (it == children_.end()) + { + // first time insertion + keys_.push_back(k); + // default-construct in map + it = children_.emplace(k, PropertyTree{}).first; + } + return it->second; +} + const PropertyTree& PropertyTree::get(std::string_view key) const { return children_.at(std::string(key)); } const PropertyTree* PropertyTree::find(std::string_view key) const @@ -129,14 +143,7 @@ bool PropertyTree::isNull() const { return value_.IsNull(); } bool PropertyTree::isContainer() const { return !children_.empty(); } -std::vector PropertyTree::keys() const -{ - std::vector res; - res.reserve(children_.size()); - for (const auto& pair : children_) - res.push_back(pair.first); - return res; -} +std::vector PropertyTree::keys() const { return keys_; } void PropertyTree::setAttribute(std::string_view name, const YAML::Node& attr) { @@ -318,14 +325,19 @@ YAML::Node PropertyTree::toYAML(bool exclude_attributes) const node[ATTRIBUTES_KEY] = attr_node; } + + if (keys_.size() != children_.size()) + throw std::runtime_error("PropertyTree, keys_ and children_ are not the same size"); + // emit children - for (const auto& pair : children_) + for (const auto& key : keys_) { + const PropertyTree& child = children_.at(key); // If the property is not required and is null then skip when excluding attributes - if (exclude_attributes && !pair.second.isRequired() && pair.second.isNull()) + if (exclude_attributes && !child.isRequired() && child.isNull()) continue; - node[pair.first] = pair.second.toYAML(exclude_attributes); + node[key] = child.toYAML(exclude_attributes); } // if leaf (no children) but value present, emit under 'value' From 9ed68c51af64680779188cc43de018c93116eb63 Mon Sep 17 00:00:00 2001 From: Levi Armstrong Date: Sat, 21 Jun 2025 14:16:54 -0500 Subject: [PATCH 13/15] fixup --- .../include/tesseract_common/property_tree.h | 12 ++- tesseract_common/src/property_tree.cpp | 9 +- tesseract_common/src/property_tree_demo.cpp | 82 +++++++++---------- 3 files changed, 56 insertions(+), 47 deletions(-) diff --git a/tesseract_common/include/tesseract_common/property_tree.h b/tesseract_common/include/tesseract_common/property_tree.h index a41bf68cd14..2e609a58df3 100644 --- a/tesseract_common/include/tesseract_common/property_tree.h +++ b/tesseract_common/include/tesseract_common/property_tree.h @@ -122,7 +122,15 @@ class PropertyTree * @param key Child identifier (string key). * @return Reference to the child PropertyTree. */ - PropertyTree& get(std::string_view key); + PropertyTree& operator[](std::string_view key); + + /** + * @brief Access a child node by key. + * @param key Child identifier. + * @return Reference to the child. + * @throws std::out_of_range if key not found. + */ + PropertyTree& at(std::string_view key); /** * @brief Access a child node by key (const). @@ -130,7 +138,7 @@ class PropertyTree * @return Const reference to the child. * @throws std::out_of_range if key not found. */ - const PropertyTree& get(std::string_view key) const; + const PropertyTree& at(std::string_view key) const; /** * @brief Find a child node without creating it. diff --git a/tesseract_common/src/property_tree.cpp b/tesseract_common/src/property_tree.cpp index 541e9e4ac98..4a81dadb067 100644 --- a/tesseract_common/src/property_tree.cpp +++ b/tesseract_common/src/property_tree.cpp @@ -55,7 +55,7 @@ void PropertyTree::mergeConfig(const YAML::Node& config, bool allow_extra_proper const auto& key = it->first.as(); if (children_.count(key) == 0) { - auto& extra_node = get(key); + auto& extra_node = (*this)[key]; extra_node.setAttribute(EXTRA_KEY, YAML::Node(true)); extra_node.mergeConfig(it->second, allow_extra_properties); } @@ -114,7 +114,7 @@ void PropertyTree::validate(bool allow_extra_properties) const void PropertyTree::addValidator(ValidatorFn fn) { validators_.push_back(std::move(fn)); } -PropertyTree& PropertyTree::get(std::string_view key) +PropertyTree& PropertyTree::operator[](std::string_view key) { auto k = std::string(key); auto it = children_.find(k); @@ -123,12 +123,13 @@ PropertyTree& PropertyTree::get(std::string_view key) // first time insertion keys_.push_back(k); // default-construct in map - it = children_.emplace(k, PropertyTree{}).first; + it = children_.emplace(std::move(k), PropertyTree{}).first; } return it->second; } -const PropertyTree& PropertyTree::get(std::string_view key) const { return children_.at(std::string(key)); } +PropertyTree& PropertyTree::at(std::string_view key) { return children_.at(std::string(key)); } +const PropertyTree& PropertyTree::at(std::string_view key) const { return children_.at(std::string(key)); } const PropertyTree* PropertyTree::find(std::string_view key) const { diff --git a/tesseract_common/src/property_tree_demo.cpp b/tesseract_common/src/property_tree_demo.cpp index 3d538a77c35..32a6b614a0e 100644 --- a/tesseract_common/src/property_tree_demo.cpp +++ b/tesseract_common/src/property_tree_demo.cpp @@ -11,7 +11,7 @@ using namespace tesseract_common; PropertyTree buildConfigSchema() { PropertyTree schema; - auto& cfg = schema.get("config"); + auto& cfg = schema["config"]; cfg.setAttribute(property_attribute::TYPE, property_type::CONTAINER); cfg.setAttribute(property_attribute::DOC, "Main config for plugin"); cfg.setAttribute(property_attribute::REQUIRED, true); @@ -22,7 +22,7 @@ PropertyTree buildConfigSchema() // conditional { - auto& prop = cfg.get("conditional"); + auto& prop = cfg["conditional"]; prop.setAttribute(property_attribute::TYPE, property_type::BOOL); prop.setAttribute(property_attribute::DEFAULT, true); prop.setAttribute(property_attribute::DOC, "Enable conditional execution"); @@ -30,20 +30,20 @@ PropertyTree buildConfigSchema() } // inputs { - auto& inputs = cfg.get("inputs"); + auto& inputs = cfg["inputs"]; inputs.setAttribute(property_attribute::TYPE, property_type::CONTAINER); inputs.setAttribute(property_attribute::DOC, "Input sources"); inputs.setAttribute(property_attribute::REQUIRED, true); // program { - auto& prop = inputs.get("program"); + auto& prop = inputs["program"]; prop.setAttribute(property_attribute::TYPE, property_type::STRING); prop.setAttribute(property_attribute::DOC, "The composite instruction"); prop.setAttribute(property_attribute::REQUIRED, true); } // environment { - auto& prop = inputs.get("environment"); + auto& prop = inputs["environment"]; // env.setAttribute("enum", YAML::Load(R"(["dev","stag","prod"])")); prop.setAttribute(property_attribute::TYPE, property_type::STRING); prop.setAttribute(property_attribute::DOC, "The tesseract environment"); @@ -51,14 +51,14 @@ PropertyTree buildConfigSchema() } // profiles { - auto& prop = inputs.get("profiles"); + auto& prop = inputs["profiles"]; prop.setAttribute(property_attribute::TYPE, property_type::STRING); prop.setAttribute(property_attribute::DOC, "The tesseract profiles"); prop.setAttribute(property_attribute::REQUIRED, true); // prof.setAttribute("enum", YAML::Load(R"(["A","B"])")); // prof.addValidator(validateEnum); // prof.addValidator([](auto const& node, auto& errs, const auto& path){ - // auto s = node.getValue().template as(); + // auto s = node[Value().template as(); // if (s.find(',') == std::string::npos) { // errs.push_back(path + ": profiles should be comma-separated"); // return false; @@ -69,19 +69,19 @@ PropertyTree buildConfigSchema() } // outputs { - auto& outputs = cfg.get("outputs"); + auto& outputs = cfg["outputs"]; outputs.setAttribute(property_attribute::TYPE, property_type::CONTAINER); outputs.setAttribute(property_attribute::DOC, "Output sources"); outputs.setAttribute(property_attribute::REQUIRED, true); { - auto& prop = outputs.get("program"); + auto& prop = outputs["program"]; prop.setAttribute(property_attribute::TYPE, property_type::STRING); prop.setAttribute(property_attribute::REQUIRED, true); } } // format_result_as_input { - auto& prop = cfg.get("format_result_as_input"); + auto& prop = cfg["format_result_as_input"]; prop.setAttribute(property_attribute::TYPE, property_type::BOOL); prop.setAttribute(property_attribute::DEFAULT, false); } @@ -95,17 +95,17 @@ tesseract_common::PropertyTree getTaskComposerGraphSchema() schema.setAttribute(property_attribute::TYPE, property_type::CONTAINER); schema.setAttribute(property_attribute::DOC, "TaskComposerGraph"); { - auto& prop = schema.get("class"); + auto& prop = schema["class"]; prop.setAttribute(property_attribute::TYPE, property_type::STRING); prop.setAttribute(property_attribute::DOC, "The class factory name"); prop.setAttribute(property_attribute::REQUIRED, true); } - auto& config_schema = schema.get("config"); + auto& config_schema = schema["config"]; config_schema.setAttribute(property_attribute::TYPE, property_type::CONTAINER); config_schema.setAttribute(property_attribute::REQUIRED, true); { - auto& prop = config_schema.get("conditional"); + auto& prop = config_schema["conditional"]; prop.setAttribute(property_attribute::TYPE, property_type::BOOL); prop.setAttribute(property_attribute::DEFAULT, true); prop.setAttribute(property_attribute::DOC, "Enable conditional execution"); @@ -113,26 +113,26 @@ tesseract_common::PropertyTree getTaskComposerGraphSchema() } { - auto& inputs = config_schema.get("inputs"); + auto& inputs = config_schema["inputs"]; inputs.setAttribute(property_attribute::TYPE, property_type::CONTAINER); inputs.setAttribute(property_attribute::DOC, "Input sources"); } { - auto& outputs = config_schema.get("outputs"); + auto& outputs = config_schema["outputs"]; outputs.setAttribute(property_attribute::TYPE, property_type::CONTAINER); outputs.setAttribute(property_attribute::DOC, "Output sources"); } { - auto& prop = config_schema.get("nodes"); + auto& prop = config_schema["nodes"]; prop.setAttribute(property_attribute::TYPE, "TaskComposerGraphNode{}"); prop.setAttribute(property_attribute::DOC, "Map of all task nodes"); prop.setAttribute(property_attribute::REQUIRED, true); } { - auto& prop = config_schema.get("edges"); + auto& prop = config_schema["edges"]; prop.setAttribute(property_attribute::TYPE, "TaskComposerGraphEdge[]"); prop.setAttribute(property_attribute::DOC, "List of graph edges"); prop.setAttribute(property_attribute::REQUIRED, true); @@ -140,7 +140,7 @@ tesseract_common::PropertyTree getTaskComposerGraphSchema() } { - auto& prop = config_schema.get("terminals"); + auto& prop = config_schema["terminals"]; prop.setAttribute(property_attribute::TYPE, property_type::createList(property_type::STRING)); prop.setAttribute(property_attribute::DOC, "List of terminal tasks"); prop.setAttribute(property_attribute::REQUIRED, true); @@ -157,14 +157,14 @@ tesseract_common::PropertyTree getTaskComposerGraphEdgeSchema() schema.setAttribute(property_attribute::TYPE, property_type::CONTAINER); schema.setAttribute(property_attribute::DOC, "TaskComposerGraphEdge"); { - auto& prop = schema.get("source"); + auto& prop = schema["source"]; prop.setAttribute(property_attribute::TYPE, property_type::STRING); prop.setAttribute(property_attribute::DOC, "The source task name"); prop.setAttribute(property_attribute::REQUIRED, true); } { - auto& prop = schema.get("destinations"); + auto& prop = schema["destinations"]; prop.setAttribute(property_attribute::TYPE, property_type::createList(property_type::STRING)); prop.setAttribute(property_attribute::DOC, "The list of destination task name"); prop.setAttribute(property_attribute::REQUIRED, true); @@ -185,17 +185,17 @@ tesseract_common::PropertyTree getTaskComposerRasterOnlySchema() return_options[1] = "Successful"; schema.setAttribute("return_options", YAML::Node(return_options)); { - auto& prop = schema.get("class"); + auto& prop = schema["class"]; prop.setAttribute(property_attribute::TYPE, property_type::STRING); prop.setAttribute(property_attribute::DOC, "The class factory name"); prop.setAttribute(property_attribute::REQUIRED, true); } - auto& config_schema = schema.get("config"); + auto& config_schema = schema["config"]; config_schema.setAttribute(property_attribute::TYPE, property_type::CONTAINER); config_schema.setAttribute(property_attribute::REQUIRED, true); { - auto& prop = config_schema.get("conditional"); + auto& prop = config_schema["conditional"]; prop.setAttribute(property_attribute::TYPE, property_type::BOOL); prop.setAttribute(property_attribute::DEFAULT, true); prop.setAttribute(property_attribute::DOC, "Enable conditional execution"); @@ -203,20 +203,20 @@ tesseract_common::PropertyTree getTaskComposerRasterOnlySchema() } { - auto& inputs = config_schema.get("inputs"); + auto& inputs = config_schema["inputs"]; inputs.setAttribute(property_attribute::TYPE, property_type::CONTAINER); inputs.setAttribute(property_attribute::DOC, "Input sources"); // program { - auto& prop = inputs.get("program"); + auto& prop = inputs["program"]; prop.setAttribute(property_attribute::TYPE, property_type::STRING); prop.setAttribute(property_attribute::DOC, "The composite instruction"); prop.setAttribute(property_attribute::REQUIRED, true); } // environment { - auto& prop = inputs.get("environment"); + auto& prop = inputs["environment"]; prop.setAttribute(property_attribute::TYPE, property_type::STRING); prop.setAttribute(property_attribute::DOC, "The tesseract environment"); prop.setAttribute(property_attribute::REQUIRED, true); @@ -224,12 +224,12 @@ tesseract_common::PropertyTree getTaskComposerRasterOnlySchema() } { - auto& outputs = config_schema.get("outputs"); + auto& outputs = config_schema["outputs"]; outputs.setAttribute(property_attribute::TYPE, property_type::CONTAINER); outputs.setAttribute(property_attribute::DOC, "Output sources"); // program { - auto& prop = outputs.get("program"); + auto& prop = outputs["program"]; prop.setAttribute(property_attribute::TYPE, property_type::STRING); prop.setAttribute(property_attribute::DOC, "The composite instruction"); prop.setAttribute(property_attribute::REQUIRED, true); @@ -237,64 +237,64 @@ tesseract_common::PropertyTree getTaskComposerRasterOnlySchema() } { - auto& raster = config_schema.get("raster"); + auto& raster = config_schema["raster"]; raster.setAttribute(property_attribute::TYPE, property_type::CONTAINER); raster.setAttribute(property_attribute::DOC, "The raster task"); raster.setAttribute(property_attribute::REQUIRED, true); { - auto& prop = raster.get("task"); + auto& prop = raster["task"]; prop.setAttribute(property_attribute::TYPE, property_type::STRING); prop.setAttribute(property_attribute::DOC, "The task name"); prop.setAttribute(property_attribute::REQUIRED, true); } - auto& raster_config = raster.get("config"); + auto& raster_config = raster["config"]; raster_config.setAttribute(property_attribute::TYPE, property_type::CONTAINER); raster_config.setAttribute(property_attribute::REQUIRED, true); { - auto& prop = raster_config.get("abort_terminal"); + auto& prop = raster_config["abort_terminal"]; prop.setAttribute(property_attribute::TYPE, property_type::INT); prop.setAttribute(property_attribute::MINIMUM, 0); prop.setAttribute(property_attribute::DOC, "The abort terminal"); } { - auto& prop = raster_config.get("remapping"); + auto& prop = raster_config["remapping"]; prop.setAttribute(property_attribute::TYPE, property_type::createMap(property_type::STRING)); prop.setAttribute(property_attribute::DOC, "The remapping of input and output keys"); } { - auto& prop = raster_config.get("indexing"); + auto& prop = raster_config["indexing"]; prop.setAttribute(property_attribute::TYPE, property_type::createList(property_type::STRING)); prop.setAttribute(property_attribute::DOC, "The input and output keys to index"); } } { - auto& transition = config_schema.get("transition"); + auto& transition = config_schema["transition"]; transition.setAttribute(property_attribute::TYPE, property_type::CONTAINER); transition.setAttribute(property_attribute::DOC, "The transition task"); transition.setAttribute(property_attribute::REQUIRED, true); { - auto& prop = transition.get("task"); + auto& prop = transition["task"]; prop.setAttribute(property_attribute::TYPE, property_type::STRING); prop.setAttribute(property_attribute::DOC, "The task name"); prop.setAttribute(property_attribute::REQUIRED, true); } - auto& transition_config = transition.get("config"); + auto& transition_config = transition["config"]; transition_config.setAttribute(property_attribute::TYPE, property_type::CONTAINER); transition_config.setAttribute(property_attribute::REQUIRED, true); { - auto& prop = transition_config.get("abort_terminal"); + auto& prop = transition_config["abort_terminal"]; prop.setAttribute(property_attribute::TYPE, property_type::INT); prop.setAttribute(property_attribute::MINIMUM, 0); prop.setAttribute(property_attribute::DOC, "The abort terminal"); } { - auto& prop = transition_config.get("remapping"); + auto& prop = transition_config["remapping"]; prop.setAttribute(property_attribute::TYPE, property_type::createMap(property_type::STRING)); prop.setAttribute(property_attribute::DOC, "The remapping of input and output keys"); } { - auto& prop = transition_config.get("indexing"); + auto& prop = transition_config["indexing"]; prop.setAttribute(property_attribute::TYPE, property_type::createList(property_type::STRING)); prop.setAttribute(property_attribute::DOC, "The input and output keys to index"); } @@ -399,7 +399,7 @@ int testBasic() return 1; } - bool cond = schema.get("config").get("conditional").getValue().as(); + bool cond = schema["config"]["conditional"].getValue().as(); std::cout << "conditional = " << std::boolalpha << cond << "\n"; return 0; } From cb719c266f0e7a504f248ed1c787febfd5df2514 Mon Sep 17 00:00:00 2001 From: Levi Armstrong Date: Thu, 3 Jul 2025 22:35:18 -0500 Subject: [PATCH 14/15] fixup --- .../bullet/bullet_factories.h | 8 + .../bullet/src/bullet_factories.cpp | 56 +++- .../core/contact_managers_plugin_factory.h | 18 ++ .../src/contact_managers_plugin_factory.cpp | 8 + tesseract_collision/core/src/types.cpp | 9 + .../tesseract_collision/fcl/fcl_factories.h | 2 + tesseract_collision/fcl/src/fcl_factories.cpp | 3 + .../test/collision_core_unit.cpp | 12 + .../test/contact_manager_plugins.yaml | 4 + .../include/tesseract_common/plugin_info.h | 4 + .../include/tesseract_common/property_tree.h | 31 ++ .../tesseract_common/yaml_extenstions.h | 295 ++++++++++++++++-- tesseract_common/src/plugin_info.cpp | 8 + tesseract_common/src/property_tree.cpp | 141 ++++++++- tesseract_support/CMakeLists.txt | 3 +- 15 files changed, 549 insertions(+), 53 deletions(-) diff --git a/tesseract_collision/bullet/include/tesseract_collision/bullet/bullet_factories.h b/tesseract_collision/bullet/include/tesseract_collision/bullet/bullet_factories.h index c29272fa40c..4087771bdd0 100644 --- a/tesseract_collision/bullet/include/tesseract_collision/bullet/bullet_factories.h +++ b/tesseract_collision/bullet/include/tesseract_collision/bullet/bullet_factories.h @@ -55,6 +55,8 @@ class BulletDiscreteBVHManagerFactory : public DiscreteContactManagerFactory public: std::unique_ptr create(const std::string& name, const YAML::Node& config) const override final; + + tesseract_common::PropertyTree schema() const override final; }; class BulletDiscreteSimpleManagerFactory : public DiscreteContactManagerFactory @@ -62,6 +64,8 @@ class BulletDiscreteSimpleManagerFactory : public DiscreteContactManagerFactory public: std::unique_ptr create(const std::string& name, const YAML::Node& config) const override final; + + tesseract_common::PropertyTree schema() const override final; }; class BulletCastBVHManagerFactory : public ContinuousContactManagerFactory @@ -69,6 +73,8 @@ class BulletCastBVHManagerFactory : public ContinuousContactManagerFactory public: std::unique_ptr create(const std::string& name, const YAML::Node& config) const override final; + + tesseract_common::PropertyTree schema() const override final; }; class BulletCastSimpleManagerFactory : public ContinuousContactManagerFactory @@ -76,6 +82,8 @@ class BulletCastSimpleManagerFactory : public ContinuousContactManagerFactory public: std::unique_ptr create(const std::string& name, const YAML::Node& config) const override final; + + tesseract_common::PropertyTree schema() const override final; }; PLUGIN_ANCHOR_DECL(BulletFactoriesAnchor) diff --git a/tesseract_collision/bullet/src/bullet_factories.cpp b/tesseract_collision/bullet/src/bullet_factories.cpp index 2d992251fad..9f182f90c0c 100644 --- a/tesseract_collision/bullet/src/bullet_factories.cpp +++ b/tesseract_collision/bullet/src/bullet_factories.cpp @@ -39,24 +39,60 @@ TESSERACT_COMMON_IGNORE_WARNINGS_POP #include #include +#include + namespace tesseract_collision::tesseract_collision_bullet { +tesseract_common::PropertyTree getConfigSchema() +{ + using namespace tesseract_common; + PropertyTree schema; + { + auto& prop = schema["share_pool_allocators"]; + prop.setAttribute(property_attribute::TYPE, property_type::BOOL); + prop.setAttribute(property_attribute::DOC, "Indicate if shared pool allocators should be leverage."); + prop.setAttribute(property_attribute::REQUIRED, false); + prop.setAttribute(property_attribute::DEFAULT, false); + } + + { + auto& prop = schema["max_persistent_manifold_pool_size"]; + prop.setAttribute(property_attribute::TYPE, property_type::INT); + prop.setAttribute(property_attribute::DOC, "The max size for the persistent manifold pool size."); + prop.setAttribute(property_attribute::MINIMUM, 0); + } + + { + auto& prop = schema["max_collision_algorithm_pool_size"]; + prop.setAttribute(property_attribute::TYPE, property_type::INT); + prop.setAttribute(property_attribute::DOC, "The max size for the persistent algorithm pool size."); + prop.setAttribute(property_attribute::MINIMUM, 0); + } + + return schema; +} + TesseractCollisionConfigurationInfo getConfigInfo(const YAML::Node& config) { if (config.IsNull()) return {}; + // Validate config + tesseract_common::PropertyTree schema = getConfigSchema(); + schema.mergeConfig(config); + schema.validate(); + bool share_pool_allocators{ false }; - if (YAML::Node n = config["share_pool_allocators"]) - share_pool_allocators = n.as(); + if (const tesseract_common::PropertyTree& prop = schema.at("share_pool_allocators")) + share_pool_allocators = prop.as(); TesseractCollisionConfigurationInfo config_info(false, share_pool_allocators); - if (YAML::Node n = config["max_persistent_manifold_pool_size"]) - config_info.m_defaultMaxPersistentManifoldPoolSize = n.as(); + if (const tesseract_common::PropertyTree& prop = schema.at("max_persistent_manifold_pool_size")) + config_info.m_defaultMaxPersistentManifoldPoolSize = prop.as(); - if (YAML::Node n = config["max_collision_algorithm_pool_size"]) - config_info.m_defaultMaxCollisionAlgorithmPoolSize = n.as(); + if (const tesseract_common::PropertyTree& prop = schema.at("max_collision_algorithm_pool_size")) + config_info.m_defaultMaxCollisionAlgorithmPoolSize = prop.as(); config_info.createPoolAllocators(); return config_info; @@ -68,24 +104,32 @@ BulletDiscreteBVHManagerFactory::create(const std::string& name, const YAML::Nod return std::make_unique(name, getConfigInfo(config)); } +tesseract_common::PropertyTree BulletDiscreteBVHManagerFactory::schema() const { return getConfigSchema(); } + std::unique_ptr BulletDiscreteSimpleManagerFactory::create(const std::string& name, const YAML::Node& config) const { return std::make_unique(name, getConfigInfo(config)); } +tesseract_common::PropertyTree BulletDiscreteSimpleManagerFactory::schema() const { return getConfigSchema(); } + std::unique_ptr BulletCastBVHManagerFactory::create(const std::string& name, const YAML::Node& config) const { return std::make_unique(name, getConfigInfo(config)); } +tesseract_common::PropertyTree BulletCastBVHManagerFactory::schema() const { return getConfigSchema(); } + std::unique_ptr BulletCastSimpleManagerFactory::create(const std::string& name, const YAML::Node& config) const { return std::make_unique(name, getConfigInfo(config)); } +tesseract_common::PropertyTree BulletCastSimpleManagerFactory::schema() const { return getConfigSchema(); } + PLUGIN_ANCHOR_IMPL(BulletFactoriesAnchor) } // namespace tesseract_collision::tesseract_collision_bullet diff --git a/tesseract_collision/core/include/tesseract_collision/core/contact_managers_plugin_factory.h b/tesseract_collision/core/include/tesseract_collision/core/contact_managers_plugin_factory.h index 1b01033b8c9..a22e30ea869 100644 --- a/tesseract_collision/core/include/tesseract_collision/core/contact_managers_plugin_factory.h +++ b/tesseract_collision/core/include/tesseract_collision/core/contact_managers_plugin_factory.h @@ -70,6 +70,12 @@ class DiscreteContactManagerFactory */ virtual std::unique_ptr create(const std::string& name, const YAML::Node& config) const = 0; + /** + * @brief Get the config yaml schema + * @return The schema + */ + virtual tesseract_common::PropertyTree schema() const = 0; + protected: static std::string getSection(); friend class boost_plugin_loader::PluginLoader; @@ -92,6 +98,12 @@ class ContinuousContactManagerFactory virtual std::unique_ptr create(const std::string& solver_name, const YAML::Node& config) const = 0; + /** + * @brief Get the config yaml schema + * @return The schema + */ + virtual tesseract_common::PropertyTree schema() const = 0; + protected: static std::string getSection(); friend class boost_plugin_loader::PluginLoader; @@ -125,6 +137,12 @@ class ContactManagersPluginFactory */ ContactManagersPluginFactory(const std::string& config, const tesseract_common::ResourceLocator& locator); + /** + * @brief Get the schema + * @return The schema + */ + tesseract_common::PropertyTree getSchema() const; + /** * @brief Add location for the plugin loader to search * @param path The full path to the directory diff --git a/tesseract_collision/core/src/contact_managers_plugin_factory.cpp b/tesseract_collision/core/src/contact_managers_plugin_factory.cpp index bc27e8c28d8..ece8b31fc54 100644 --- a/tesseract_collision/core/src/contact_managers_plugin_factory.cpp +++ b/tesseract_collision/core/src/contact_managers_plugin_factory.cpp @@ -34,6 +34,7 @@ TESSERACT_COMMON_IGNORE_WARNINGS_POP #include #include #include +#include #include #include #include @@ -61,6 +62,13 @@ ContactManagersPluginFactory::ContactManagersPluginFactory() boost::token_compress_on); } +tesseract_common::PropertyTree ContactManagersPluginFactory::getSchema() const +{ + tesseract_common::PropertyTree schema; + + return schema; +} + void ContactManagersPluginFactory::loadConfig(const YAML::Node& config) { if (const YAML::Node& plugin_info = config[ContactManagersPluginInfo::CONFIG_KEY]) diff --git a/tesseract_collision/core/src/types.cpp b/tesseract_collision/core/src/types.cpp index ac264ad2686..00d805acb97 100644 --- a/tesseract_collision/core/src/types.cpp +++ b/tesseract_collision/core/src/types.cpp @@ -399,8 +399,17 @@ void ContactManagerConfig::validate() const throw std::runtime_error("ContactManagerConfig, pair margin override type is NONE but pair collision margins " "exist!"); + if (pair_margin_override_type != CollisionMarginPairOverrideType::NONE && + pair_margin_data.getCollisionMargins().empty()) + throw std::runtime_error("ContactManagerConfig, pair collision margins are empty, but pair margin override type is " + "not NONE!"); + if (acm_override_type == ACMOverrideType::NONE && !acm.getAllAllowedCollisions().empty()) throw std::runtime_error("ContactManagerConfig, acm override type is NONE but allowed collision entries exist!"); + + if (acm_override_type != ACMOverrideType::NONE && acm.getAllAllowedCollisions().empty()) + throw std::runtime_error("ContactManagerConfig, allowed collision entries are empty, but acm override type is not " + "NONE!"); } bool ContactManagerConfig::operator==(const ContactManagerConfig& rhs) const diff --git a/tesseract_collision/fcl/include/tesseract_collision/fcl/fcl_factories.h b/tesseract_collision/fcl/include/tesseract_collision/fcl/fcl_factories.h index d2806227600..0f49c1ca71d 100644 --- a/tesseract_collision/fcl/include/tesseract_collision/fcl/fcl_factories.h +++ b/tesseract_collision/fcl/include/tesseract_collision/fcl/fcl_factories.h @@ -36,6 +36,8 @@ class FCLDiscreteBVHManagerFactory : public DiscreteContactManagerFactory public: std::unique_ptr create(const std::string& name, const YAML::Node& config) const override final; + + tesseract_common::PropertyTree schema() const override final; }; PLUGIN_ANCHOR_DECL(FCLFactoriesAnchor) diff --git a/tesseract_collision/fcl/src/fcl_factories.cpp b/tesseract_collision/fcl/src/fcl_factories.cpp index 51c764a8989..5e7a643426e 100644 --- a/tesseract_collision/fcl/src/fcl_factories.cpp +++ b/tesseract_collision/fcl/src/fcl_factories.cpp @@ -27,6 +27,7 @@ #include #include #include +#include namespace tesseract_collision::tesseract_collision_fcl { @@ -36,6 +37,8 @@ FCLDiscreteBVHManagerFactory::create(const std::string& name, const YAML::Node& return std::make_unique(name); } +tesseract_common::PropertyTree FCLDiscreteBVHManagerFactory::schema() const { return {}; } + PLUGIN_ANCHOR_IMPL(FCLFactoriesAnchor) // LCOV_EXCL_LINE } // namespace tesseract_collision::tesseract_collision_fcl diff --git a/tesseract_collision/test/collision_core_unit.cpp b/tesseract_collision/test/collision_core_unit.cpp index 3c27fcf4cf7..d641563a3cc 100644 --- a/tesseract_collision/test/collision_core_unit.cpp +++ b/tesseract_collision/test/collision_core_unit.cpp @@ -125,6 +125,12 @@ TEST(TesseractCoreUnit, ContactManagerConfigTest) // NOLINT EXPECT_ANY_THROW(config.validate()); // NOLINT } + { + tesseract_collision::ContactManagerConfig config; + config.pair_margin_override_type = tesseract_collision::CollisionMarginPairOverrideType::MODIFY; + EXPECT_ANY_THROW(config.validate()); // NOLINT + } + { tesseract_collision::ContactManagerConfig config; config.pair_margin_override_type = tesseract_collision::CollisionMarginPairOverrideType::MODIFY; @@ -138,6 +144,12 @@ TEST(TesseractCoreUnit, ContactManagerConfigTest) // NOLINT EXPECT_ANY_THROW(config.validate()); // NOLINT } + { + tesseract_collision::ContactManagerConfig config; + config.acm_override_type = tesseract_collision::ACMOverrideType::OR; + EXPECT_ANY_THROW(config.validate()); // NOLINT + } + { tesseract_collision::ContactManagerConfig config; config.acm_override_type = tesseract_collision::ACMOverrideType::OR; diff --git a/tesseract_collision/test/contact_manager_plugins.yaml b/tesseract_collision/test/contact_manager_plugins.yaml index 4209235a988..cd829f85c92 100644 --- a/tesseract_collision/test/contact_manager_plugins.yaml +++ b/tesseract_collision/test/contact_manager_plugins.yaml @@ -11,6 +11,8 @@ contact_manager_plugins: class: BulletDiscreteBVHManagerFactory BulletDiscreteSimpleManager: class: BulletDiscreteSimpleManagerFactory + config: + share_pool_allocators: false FCLDiscreteBVHManager: class: FCLDiscreteBVHManagerFactory continuous_plugins: @@ -20,4 +22,6 @@ contact_manager_plugins: class: BulletCastBVHManagerFactory BulletCastSimpleManager: class: BulletCastSimpleManagerFactory + config: + share_pool_allocators: false diff --git a/tesseract_common/include/tesseract_common/plugin_info.h b/tesseract_common/include/tesseract_common/plugin_info.h index cef50daceda..e9d1bdfed19 100644 --- a/tesseract_common/include/tesseract_common/plugin_info.h +++ b/tesseract_common/include/tesseract_common/plugin_info.h @@ -45,6 +45,7 @@ class access; namespace tesseract_common { struct Serialization; +class PropertyTree; /** @brief The Plugin Information struct */ // NOLINTNEXTLINE @@ -155,6 +156,9 @@ struct ContactManagersPluginInfo /** @brief Check if structure is empty */ bool empty() const; + /** @brief Get the schema */ + static PropertyTree schema(); + // Yaml Config key static inline const std::string CONFIG_KEY{ "contact_manager_plugins" }; diff --git a/tesseract_common/include/tesseract_common/property_tree.h b/tesseract_common/include/tesseract_common/property_tree.h index 2e609a58df3..ddba52c719a 100644 --- a/tesseract_common/include/tesseract_common/property_tree.h +++ b/tesseract_common/include/tesseract_common/property_tree.h @@ -40,6 +40,7 @@ constexpr std::string_view DOUBLE{ "double" }; // Container of properties constexpr std::string_view CONTAINER{ "YAML::NodeType::Map" }; +constexpr std::string_view ONEOF{ "oneOf" }; // Eigen Types constexpr std::string_view EIGEN_ISOMETRY_3D{ "Eigen::Isometry3d" }; @@ -93,6 +94,15 @@ class PropertyTree */ PropertyTree() = default; + /** @brief Deep-copy constructor — clones all YAML::Nodes and subtrees */ + PropertyTree(const PropertyTree& other); + /** @brief Deep-copy assignment operator */ + PropertyTree& operator=(const PropertyTree& other); + + /** @brief Default move constructor/assignment are fine */ + PropertyTree(PropertyTree&&) noexcept = default; + PropertyTree& operator=(PropertyTree&&) noexcept = default; + /** * @brief Given that *this* is purely a schema tree, merge in the * user’s YAML config to populate all values (and apply defaults). @@ -159,6 +169,16 @@ class PropertyTree */ const YAML::Node& getValue() const; + /** + * @brief Retrieve the YAML value casted to the provided type + * @return The casted value + */ + template + inline T as() const + { + return value_.as(); + } + /** * @brief Check if property value is null * @return True if required, otherwise false @@ -193,6 +213,8 @@ class PropertyTree void setAttribute(std::string_view name, int attr); /** @brief Set a double attribute. */ void setAttribute(std::string_view name, double attr); + /** @brief Set a vector of strings attribute. */ + void setAttribute(std::string_view name, const std::vector& attr); /** * @brief Check if an attribute exists and is not null. @@ -234,6 +256,9 @@ class PropertyTree */ YAML::Node toYAML(bool exclude_attributes = true) const; + /** @brief Indicate if defined, meaning it has children or a value */ + explicit operator bool() const noexcept; + private: YAML::Node value_; /**< Value stored at this node */ YAML::Node follow_; /**< Follow stored at this node */ @@ -241,6 +266,7 @@ class PropertyTree std::map children_; /**< Nested child nodes */ std::vector keys_; /**< Nested child keys */ std::vector validators_; /**< Validators to invoke */ + std::unique_ptr oneof_; /**< Store the property content on merge */ }; /** @@ -293,6 +319,11 @@ void validateSequence(const PropertyTree& node); */ void validateContainer(const PropertyTree& node); +/** + * @brief Validator: Retrieve schema for the custom type and runs it validtor + * @param node Node to validate + * @throws runtime_error if not correct. + */ void validateCustomType(const PropertyTree& node); /** diff --git a/tesseract_common/include/tesseract_common/yaml_extenstions.h b/tesseract_common/include/tesseract_common/yaml_extenstions.h index cdfc652df08..97bcabf49bf 100644 --- a/tesseract_common/include/tesseract_common/yaml_extenstions.h +++ b/tesseract_common/include/tesseract_common/yaml_extenstions.h @@ -35,6 +35,7 @@ TESSERACT_COMMON_IGNORE_WARNINGS_POP #include #include #include +#include namespace YAML { @@ -199,6 +200,55 @@ struct convert rhs = out; return true; } + + static tesseract_common::PropertyTree schema() + { + using namespace tesseract_common; + using namespace property_attribute; + using namespace property_type; + + // Top-level must be a map container + PropertyTree sch; + sch.setAttribute(TYPE, CONTAINER); + + // position: map{x:double,y:double,z:double} + { + auto& prop = sch["position"]; + prop.setAttribute(TYPE, CONTAINER); + prop.setAttribute(REQUIRED, true); + + // x, y, z all required doubles + for (auto key : { "x", "y", "z" }) + { + auto& c = prop[key]; + c.setAttribute(TYPE, DOUBLE); + c.setAttribute(REQUIRED, true); + } + } + + // orientation: either quaternion (x,y,z,w) or rpy (r,p,y) + { + auto& prop = sch["orientation"]; + prop.setAttribute(TYPE, ONEOF); + prop.setAttribute(REQUIRED, true); + + auto& option0 = prop["xyzw"]; + for (auto key : { "x", "y", "z", "w" }) + { + auto& c = option0[key]; + c.setAttribute(TYPE, DOUBLE); + } + + auto& option1 = prop["rpy"]; + for (auto key : { "r", "p", "y" }) + { + auto& c = option1[key]; + c.setAttribute(TYPE, DOUBLE); + } + } + + return sch; + } }; template <> @@ -224,6 +274,14 @@ struct convert return true; } + + static tesseract_common::PropertyTree schema() + { + using namespace tesseract_common; + PropertyTree sch; + sch.setAttribute(property_attribute::TYPE, property_type::EIGEN_VECTOR_XD); + return sch; + } }; template <> @@ -248,6 +306,14 @@ struct convert return true; } + + static tesseract_common::PropertyTree schema() + { + using namespace tesseract_common; + PropertyTree sch; + sch.setAttribute(property_attribute::TYPE, property_type::EIGEN_VECTOR_2D); + return sch; + } }; template <> @@ -272,18 +338,25 @@ struct convert return true; } + + static tesseract_common::PropertyTree schema() + { + using namespace tesseract_common; + PropertyTree sch; + sch.setAttribute(property_attribute::TYPE, property_type::EIGEN_VECTOR_3D); + return sch; + } }; template <> struct convert { + inline static const std::string SEARCH_PATHS_KEY{ "search_paths" }; + inline static const std::string SEARCH_LIBRARIES_KEY{ "search_libraries" }; + inline static const std::string FWD_KIN_PLUGINS_KEY{ "fwd_kin_plugins" }; + inline static const std::string INV_KIN_PLUGINS_KEY{ "inv_kin_plugins" }; static Node encode(const tesseract_common::KinematicsPluginInfo& rhs) { - const std::string SEARCH_PATHS_KEY{ "search_paths" }; - const std::string SEARCH_LIBRARIES_KEY{ "search_libraries" }; - const std::string FWD_KIN_PLUGINS_KEY{ "fwd_kin_plugins" }; - const std::string INV_KIN_PLUGINS_KEY{ "inv_kin_plugins" }; - YAML::Node kinematic_plugins; if (!rhs.search_paths.empty()) kinematic_plugins[SEARCH_PATHS_KEY] = rhs.search_paths; @@ -302,11 +375,6 @@ struct convert static bool decode(const Node& node, tesseract_common::KinematicsPluginInfo& rhs) { - const std::string SEARCH_PATHS_KEY{ "search_paths" }; - const std::string SEARCH_LIBRARIES_KEY{ "search_libraries" }; - const std::string FWD_KIN_PLUGINS_KEY{ "fwd_kin_plugins" }; - const std::string INV_KIN_PLUGINS_KEY{ "inv_kin_plugins" }; - if (const YAML::Node& search_paths = node[SEARCH_PATHS_KEY]) { std::set sp; @@ -379,18 +447,54 @@ struct convert return true; } + + static tesseract_common::PropertyTree schema() + { + using namespace tesseract_common; + using namespace property_attribute; + using namespace property_type; + + PropertyTree sch; + + // This is a map of named fields + sch.setAttribute(TYPE, CONTAINER); + + { // search_paths: optional list + auto& prop = sch[SEARCH_PATHS_KEY]; + prop.setAttribute(TYPE, createList(STRING)); + } + + { // search_libraries: optional list + auto& prop = sch[SEARCH_LIBRARIES_KEY]; + prop.setAttribute(TYPE, createList(STRING)); + } + + { // fwd_kin_plugins: optional map + auto& prop = sch[FWD_KIN_PLUGINS_KEY]; + prop.setAttribute(TYPE, createMap("tesseract_common::PluginInfoContainer")); + // contents are custom type → use validateCustomType + prop.addValidator(validateCustomType); + } + + { // inv_kin_plugins: optional map + auto& prop = sch[INV_KIN_PLUGINS_KEY]; + prop.setAttribute(TYPE, createMap("tesseract_common::PluginInfoContainer")); + prop.addValidator(validateCustomType); + } + + return sch; + } }; template <> struct convert { + inline static const std::string SEARCH_PATHS_KEY{ "search_paths" }; + inline static const std::string SEARCH_LIBRARIES_KEY{ "search_libraries" }; + inline static const std::string DISCRETE_PLUGINS_KEY{ "discrete_plugins" }; + inline static const std::string CONTINUOUS_PLUGINS_KEY{ "continuous_plugins" }; static Node encode(const tesseract_common::ContactManagersPluginInfo& rhs) { - const std::string SEARCH_PATHS_KEY{ "search_paths" }; - const std::string SEARCH_LIBRARIES_KEY{ "search_libraries" }; - const std::string DISCRETE_PLUGINS_KEY{ "discrete_plugins" }; - const std::string CONTINUOUS_PLUGINS_KEY{ "continuous_plugins" }; - YAML::Node contact_manager_plugins; if (!rhs.search_paths.empty()) contact_manager_plugins[SEARCH_PATHS_KEY] = rhs.search_paths; @@ -409,11 +513,6 @@ struct convert static bool decode(const Node& node, tesseract_common::ContactManagersPluginInfo& rhs) { - const std::string SEARCH_PATHS_KEY{ "search_paths" }; - const std::string SEARCH_LIBRARIES_KEY{ "search_libraries" }; - const std::string DISCRETE_PLUGINS_KEY{ "discrete_plugins" }; - const std::string CONTINUOUS_PLUGINS_KEY{ "continuous_plugins" }; - if (const YAML::Node& search_paths = node[SEARCH_PATHS_KEY]) { std::set sp; @@ -482,18 +581,57 @@ struct convert return true; } + + static tesseract_common::PropertyTree schema() + { + using namespace tesseract_common; + using namespace property_attribute; + using namespace property_type; + + PropertyTree sch; + // top-level is a map container + sch.setAttribute(TYPE, CONTAINER); + + // search_paths: optional list + { + auto& prop = sch[SEARCH_PATHS_KEY]; + prop.setAttribute(TYPE, createList(STRING)); + } + + // search_libraries: optional list + { + auto& prop = sch[SEARCH_LIBRARIES_KEY]; + prop.setAttribute(TYPE, createList(STRING)); + } + + // discrete_plugins: optional map + { + auto& prop = sch[DISCRETE_PLUGINS_KEY]; + prop.setAttribute(TYPE, createMap("tesseract_common::PluginInfoContainer")); + prop.addValidator(validateCustomType); + } + + // continuous_plugins: optional map + { + auto& prop = sch[CONTINUOUS_PLUGINS_KEY]; + prop.setAttribute(TYPE, createMap("tesseract_common::PluginInfoContainer")); + prop.addValidator(validateCustomType); + } + + return sch; + } }; template <> struct convert { + inline static const std::string SEARCH_PATHS_KEY{ "search_paths" }; + inline static const std::string SEARCH_LIBRARIES_KEY{ "search_libraries" }; + inline static const std::string EXECUTOR_PLUGINS_KEY{ "executors" }; + inline static const std::string NODE_PLUGINS_KEY{ "tasks" }; + static Node encode(const tesseract_common::TaskComposerPluginInfo& rhs) { - const std::string SEARCH_PATHS_KEY{ "search_paths" }; - const std::string SEARCH_LIBRARIES_KEY{ "search_libraries" }; - const std::string EXECUTOR_PLUGINS_KEY{ "executors" }; - const std::string NODE_PLUGINS_KEY{ "tasks" }; - YAML::Node task_composer_plugins; if (!rhs.search_paths.empty()) task_composer_plugins[SEARCH_PATHS_KEY] = rhs.search_paths; @@ -512,11 +650,6 @@ struct convert static bool decode(const Node& node, tesseract_common::TaskComposerPluginInfo& rhs) { - const std::string SEARCH_PATHS_KEY{ "search_paths" }; - const std::string SEARCH_LIBRARIES_KEY{ "search_libraries" }; - const std::string EXECUTOR_PLUGINS_KEY{ "executors" }; - const std::string NODE_PLUGINS_KEY{ "tasks" }; - if (const YAML::Node& search_paths = node[SEARCH_PATHS_KEY]) { std::set sp; @@ -586,6 +719,46 @@ struct convert return true; } + + static tesseract_common::PropertyTree schema() + { + using namespace tesseract_common; + using namespace property_attribute; + using namespace property_type; + + PropertyTree sch; + + // top‐level must be a map container + sch.setAttribute(TYPE, CONTAINER); + + // search_paths: optional list + { + auto& node = sch[SEARCH_PATHS_KEY]; + node.setAttribute(TYPE, createList(STRING)); + } + + // search_libraries: optional list + { + auto& node = sch[SEARCH_LIBRARIES_KEY]; + node.setAttribute(TYPE, createList(STRING)); + } + + // executors: optional map + { + auto& node = sch[EXECUTOR_PLUGINS_KEY]; + node.setAttribute(TYPE, createMap("tesseract_common::PluginInfoContainer")); + node.addValidator(validateCustomType); + } + + // tasks: optional map + { + auto& node = sch[NODE_PLUGINS_KEY]; + node.setAttribute(TYPE, createMap("tesseract_common::PluginInfoContainer")); + node.addValidator(validateCustomType); + } + + return sch; + } }; template <> @@ -610,27 +783,58 @@ struct convert return true; } + + static tesseract_common::PropertyTree schema() + { + using namespace tesseract_common; + using namespace property_attribute; + using namespace property_type; + + // top‐level must be a map container + PropertyTree sch; + sch.setAttribute(TYPE, createMap(EIGEN_ISOMETRY_3D)); + + return sch; + } }; template <> struct convert { + inline static const std::string JOINTS_KEY{ "joints" }; static Node encode(const tesseract_common::CalibrationInfo& rhs) { Node node; - node["joints"] = rhs.joints; + node[JOINTS_KEY] = rhs.joints; return node; } static bool decode(const Node& node, tesseract_common::CalibrationInfo& rhs) { - const YAML::Node& joints_node = node["joints"]; + const YAML::Node& joints_node = node[JOINTS_KEY]; rhs.joints = joints_node.as(); return true; } + + static tesseract_common::PropertyTree schema() + { + using namespace tesseract_common; + using namespace property_attribute; + using namespace property_type; + + // top‐level must be a map container + PropertyTree sch; + sch.setAttribute(TYPE, CONTAINER); + + auto& prop = sch[JOINTS_KEY]; + prop.setAttribute(TYPE, "tesseract_common::TransformMap"); + prop.setAttribute(REQUIRED, true); + + return sch; + } }; template <> @@ -669,6 +873,19 @@ struct convert return true; } + + static tesseract_common::PropertyTree schema() + { + using namespace tesseract_common; + using namespace property_attribute; + using namespace property_type; + + // top‐level must be a map container + PropertyTree sch; + sch.setAttribute(TYPE, createList(EIGEN_ISOMETRY_3D)); + + return sch; + } }; //=========================== CollisionMarginPairOverrideType Enum =========================== @@ -707,6 +924,20 @@ struct convert rhs = it->second; return true; } + + static tesseract_common::PropertyTree schema() + { + using namespace tesseract_common; + using namespace property_attribute; + using namespace property_type; + + // top‐level must be a map container + PropertyTree sch; + sch.setAttribute(TYPE, STRING); + sch.setAttribute(ENUM, { "NONE", "MODIFY", "REPLACE" }); + + return sch; + } }; //============================ PairsCollisionMarginData ============================ diff --git a/tesseract_common/src/plugin_info.cpp b/tesseract_common/src/plugin_info.cpp index ce4337757ea..6136bb5c05c 100644 --- a/tesseract_common/src/plugin_info.cpp +++ b/tesseract_common/src/plugin_info.cpp @@ -41,6 +41,7 @@ TESSERACT_COMMON_IGNORE_WARNINGS_POP #include #include #include +#include namespace tesseract_common { @@ -215,6 +216,13 @@ bool ContactManagersPluginInfo::empty() const continuous_plugin_infos.plugins.empty()); } +PropertyTree ContactManagersPluginInfo::schema() +{ + PropertyTree schema; + + return schema; +} + bool ContactManagersPluginInfo::operator==(const ContactManagersPluginInfo& rhs) const { bool equal = true; diff --git a/tesseract_common/src/property_tree.cpp b/tesseract_common/src/property_tree.cpp index 4a81dadb067..2d6fab2f070 100644 --- a/tesseract_common/src/property_tree.cpp +++ b/tesseract_common/src/property_tree.cpp @@ -6,6 +6,7 @@ const static std::string ATTRIBUTES_KEY{ "_attributes" }; const static std::string VALUE_KEY{ "_value" }; const static std::string EXTRA_KEY{ "_extra" }; +const static std::string ONEOF_KEY{ "_oneof" }; const static std::string FOLLOW_KEY{ "follow" }; namespace tesseract_common @@ -16,38 +17,124 @@ std::string createList(std::string_view type) { return std::string(type) + "[]"; std::string createMap(std::string_view type) { return std::string(type) + "{}"; }; } // namespace property_type +PropertyTree::PropertyTree(const PropertyTree& other) + : value_(YAML::Clone(other.value_)) + , follow_(YAML::Clone(other.follow_)) + , children_(other.children_) + , keys_(other.keys_) + , validators_(other.validators_) +{ + if (other.oneof_ != nullptr) + oneof_ = std::make_unique(*other.oneof_); + + // Deep-clone all attributes + for (auto const& [k, node] : other.attributes_) + attributes_[k] = YAML::Clone(node); +} + +PropertyTree& PropertyTree::operator=(const PropertyTree& other) +{ + if (this == &other) + return *this; + + // Clone the YAML values + value_ = YAML::Clone(other.value_); + follow_ = YAML::Clone(other.follow_); + + // Copy and clone attributes + attributes_.clear(); + for (const auto& [k, node] : other.attributes_) + attributes_[k] = YAML::Clone(node); + + // Copy children, keys and validators + children_ = other.children_; + keys_ = other.keys_; + validators_ = other.validators_; + + // Copy oneOf + if (other.oneof_ != nullptr) + oneof_ = std::make_unique(*other.oneof_); + + return *this; +} + void PropertyTree::mergeConfig(const YAML::Node& config, bool allow_extra_properties) { - // 0) Leaf-schema override for both maps and sequences: - // If this schema node has no children, but the user provided - // either a map or a sequence, just store it wholesale. + // Handle oneOf nodes up front + auto t = getAttribute(property_attribute::TYPE); + if (t.has_value() && t->as() == property_type::ONEOF) + { + if (!config || !config.IsMap()) + std::throw_with_nested(std::runtime_error("oneOf schema expects a YAML map")); + + // Find exactly one branch whose schema-keys all appear in config + std::string chosen; + for (const auto& [branch_name, branch_schema] : children_) + { + bool matches = true; + for (const auto& key : branch_schema.keys()) + { + if (branch_schema.at(key).isRequired() && !config[key]) + { + matches = false; + break; + } + } + + if (matches) + { + if (!chosen.empty()) + std::throw_with_nested(std::runtime_error("oneOf: multiple branches match")); + + chosen = branch_name; + } + } + + if (chosen.empty()) + std::throw_with_nested(std::runtime_error("oneOf: no branch matches the provided keys")); + + // Store schema + oneof_ = std::make_unique(*this); + + // Flatten: replace this node's children with the chosen branch's children + PropertyTree schema_copy = children_.at(chosen); + *this = schema_copy; + + // Now call merge again + mergeConfig(config, allow_extra_properties); + return; + } + + // Leaf-schema override for both maps and sequences: + // If this schema node has no children, but the user provided + // either a map or a sequence, just store it wholesale. if (children_.empty() && config && (config.IsMap() || config.IsSequence())) { value_ = config; return; } - // 1) Apply default if no config & not required + // Apply default if no config & not required auto def_it = attributes_.find(std::string(property_attribute::DEFAULT)); bool required = isRequired(); if ((!config || config.IsNull()) && def_it != attributes_.end() && !required) value_ = YAML::Clone(def_it->second); - // 2) Scalar or (now only non‐leaf) sequence override + // Scalar or (now only non‐leaf) sequence override if (config && config.IsScalar()) value_ = config; - // 3) Map: recurse into declared children + // Map: recurse into declared children if (config && config.IsMap()) { - // 3a) Fill declared children + // Fill declared children for (auto& [key, child_schema] : children_) { auto sub = config[key]; child_schema.mergeConfig(sub, allow_extra_properties); } - // 3b) Handle extras + // Handle extras if (!allow_extra_properties) { for (auto it = config.begin(); it != config.end(); ++it) @@ -62,7 +149,8 @@ void PropertyTree::mergeConfig(const YAML::Node& config, bool allow_extra_proper } } } - // 4) Sequence (non-leaf): apply wildcard or numeric schema + + // Sequence (non-leaf): apply wildcard or numeric schema else if (config && config.IsSequence()) { PropertyTree elem_schema; @@ -81,7 +169,7 @@ void PropertyTree::mergeConfig(const YAML::Node& config, bool allow_extra_proper children_[key].mergeConfig(elt, allow_extra_properties); } } - // 5) Otherwise leave value_ (possibly set by default) as-is. + // Otherwise leave value_ (possibly set by default) as-is. } void PropertyTree::validate(bool allow_extra_properties) const @@ -193,15 +281,24 @@ void PropertyTree::setAttribute(std::string_view name, const YAML::Node& attr) // else if (str_type == property_type::EIGEN_MATRIX_2D) // validators_.emplace_back(validateTypeCast); else if (str_type == property_type::EIGEN_VECTOR_XD) + { + validators_.emplace_back(validateSequence); validators_.emplace_back(validateTypeCast); + } // else if (str_type == property_type::EIGEN_MATRIX_2D) // validators_.emplace_back(validateTypeCast); else if (str_type == property_type::EIGEN_VECTOR_2D) + { + validators_.emplace_back(validateSequence); validators_.emplace_back(validateTypeCast); + } // else if (str_type == property_type::EIGEN_MATRIX_3D) // validators_.emplace_back(validateTypeCast); else if (str_type == property_type::EIGEN_VECTOR_3D) + { + validators_.emplace_back(validateSequence); validators_.emplace_back(validateTypeCast); + } } } @@ -221,6 +318,11 @@ void PropertyTree::setAttribute(std::string_view name, int attr) { setAttribute( void PropertyTree::setAttribute(std::string_view name, double attr) { setAttribute(name, YAML::Node(attr)); } +void PropertyTree::setAttribute(std::string_view name, const std::vector& attr) +{ + setAttribute(name, YAML::Node(attr)); +} + bool PropertyTree::hasAttribute(std::string_view name) const { auto it = attributes_.find(std::string(name)); @@ -259,7 +361,7 @@ PropertyTree PropertyTree::fromYAML(const YAML::Node& node) if (node.IsMap() && node[FOLLOW_KEY] && node[FOLLOW_KEY].IsScalar()) { if (node.size() > 1) - throw std::runtime_error("'follow' cannot be mixed with other entries"); + std::throw_with_nested(std::runtime_error("'follow' cannot be mixed with other entries")); try { @@ -277,7 +379,7 @@ PropertyTree PropertyTree::fromYAML(const YAML::Node& node) tree.value_ = node; if (node.IsMap()) { - // extract attributes map + // extract attributes if it exists if (node[ATTRIBUTES_KEY] && node[ATTRIBUTES_KEY].IsMap()) { for (const auto& it : node[ATTRIBUTES_KEY]) @@ -286,11 +388,16 @@ PropertyTree PropertyTree::fromYAML(const YAML::Node& node) tree.attributes_[key] = it.second; } } + + // extract oneof if it exist + if (node[ONEOF_KEY] && node[ONEOF_KEY].IsMap()) + tree.oneof_ = std::make_unique(fromYAML(node[ONEOF_KEY])); + // extract children for (const auto& it : node) { const auto key = it.first.as(); - if (key == ATTRIBUTES_KEY) + if (key == ATTRIBUTES_KEY || key == ONEOF_KEY) continue; tree.children_[key] = fromYAML(it.second); @@ -328,7 +435,7 @@ YAML::Node PropertyTree::toYAML(bool exclude_attributes) const } if (keys_.size() != children_.size()) - throw std::runtime_error("PropertyTree, keys_ and children_ are not the same size"); + std::throw_with_nested(std::runtime_error("PropertyTree, keys_ and children_ are not the same size")); // emit children for (const auto& key : keys_) @@ -341,6 +448,10 @@ YAML::Node PropertyTree::toYAML(bool exclude_attributes) const node[key] = child.toYAML(exclude_attributes); } + // emit oneof + if (!exclude_attributes && oneof_ != nullptr) + node[ONEOF_KEY] = oneof_->toYAML(exclude_attributes); + // if leaf (no children) but value present, emit under 'value' if (children_.empty() && value_) node[VALUE_KEY] = value_; @@ -348,6 +459,8 @@ YAML::Node PropertyTree::toYAML(bool exclude_attributes) const return node; } +PropertyTree::operator bool() const noexcept { return (!children_.empty() || !value_.IsNull()); } + std::optional isSequenceType(std::string_view type) { if (type.size() < 2) diff --git a/tesseract_support/CMakeLists.txt b/tesseract_support/CMakeLists.txt index 50f0aa7b73b..0bfbed2e230 100644 --- a/tesseract_support/CMakeLists.txt +++ b/tesseract_support/CMakeLists.txt @@ -12,7 +12,8 @@ tesseract_variables() # Configure Package configure_package() -foreach(dir urdf meshes) +# Install resources +foreach(dir meshes urdf) install(DIRECTORY ${dir}/ DESTINATION share/${PROJECT_NAME}/${dir}) endforeach() From 7e5297a39e7e10a3f09bdcec2ee12e96fe938a23 Mon Sep 17 00:00:00 2001 From: Levi Armstrong Date: Fri, 4 Jul 2025 12:28:56 -0500 Subject: [PATCH 15/15] fixup --- .../src/contact_managers_plugin_factory.cpp | 2 +- tesseract_common/CMakeLists.txt | 1 + .../include/tesseract_common/property_tree.h | 29 ++++-- .../tesseract_common/schema_registration.h | 8 +- .../{yaml_extenstions.h => yaml_extensions.h} | 96 ++++++++++++++++++- tesseract_common/src/plugin_info.cpp | 2 +- tesseract_common/src/property_tree.cpp | 86 ++++++++++++----- tesseract_common/src/yaml_extensions.cpp | 55 +++++++++++ .../test/tesseract_common_unit.cpp | 2 +- .../core/src/kinematics_plugin_factory.cpp | 2 +- tesseract_srdf/src/configs.cpp | 2 +- tesseract_srdf/src/srdf_model.cpp | 2 +- tesseract_srdf/test/tesseract_srdf_unit.cpp | 2 +- 13 files changed, 244 insertions(+), 45 deletions(-) rename tesseract_common/include/tesseract_common/{yaml_extenstions.h => yaml_extensions.h} (92%) create mode 100644 tesseract_common/src/yaml_extensions.cpp diff --git a/tesseract_collision/core/src/contact_managers_plugin_factory.cpp b/tesseract_collision/core/src/contact_managers_plugin_factory.cpp index ece8b31fc54..87d7c5c6f22 100644 --- a/tesseract_collision/core/src/contact_managers_plugin_factory.cpp +++ b/tesseract_collision/core/src/contact_managers_plugin_factory.cpp @@ -33,7 +33,7 @@ TESSERACT_COMMON_IGNORE_WARNINGS_POP #include #include #include -#include +#include #include #include #include diff --git a/tesseract_common/CMakeLists.txt b/tesseract_common/CMakeLists.txt index 561ff94449b..063e5ab3a6b 100644 --- a/tesseract_common/CMakeLists.txt +++ b/tesseract_common/CMakeLists.txt @@ -101,6 +101,7 @@ add_library( src/stopwatch.cpp src/types.cpp src/timer.cpp + src/yaml_extensions.cpp src/yaml_utils.cpp) target_link_libraries( ${PROJECT_NAME} diff --git a/tesseract_common/include/tesseract_common/property_tree.h b/tesseract_common/include/tesseract_common/property_tree.h index ddba52c719a..decc1c10eeb 100644 --- a/tesseract_common/include/tesseract_common/property_tree.h +++ b/tesseract_common/include/tesseract_common/property_tree.h @@ -14,19 +14,27 @@ namespace tesseract_common namespace property_type { /** - * @brief A utility for constructing the std::vector + * @brief A utility for constructing the vector * @param type The type assoicated with the list - * @return The string representation of the std::vector, aka. type[] + * @param length The length if fixed size + * @return The string representation of the vector, aka. type[] and type[length] for fixed size */ -std::string createList(std::string_view type); +std::string createList(std::string_view type, std::size_t length = 0); /** - * @brief A utility for constructing the std::map + * @brief A utility for constructing the map * @param type The value type assoicated with the map - * @return The string representation of the std::map, aka. type{} + * @return The string representation of the map, aka. {string,string} */ std::string createMap(std::string_view type); +/** + * @brief A utility for constructing the map + * @param type The value type assoicated with the map + * @return The string representation of the map, aka. {string, string} or {string[2], string} + */ +std::string createMap(std::string_view key, std::string_view type); + // Integral Types constexpr std::string_view BOOL{ "bool" }; constexpr std::string_view CHAR{ "char" }; @@ -272,16 +280,16 @@ class PropertyTree /** * @brief Check if type is a sequence * @param type The type to check - * @return If it is a sequence, the underlying type is returned + * @return If it is a sequence, the underlying type is returned and size */ -std::optional isSequenceType(std::string_view type); +std::optional> isSequenceType(std::string_view type); /** * @brief Check if type is a map * @param type The type to check - * @return If it is a map, the underlying type is returned + * @return If it is a map, the underlying pair is returned */ -std::optional isMapType(std::string_view type); +std::optional> isMapType(std::string_view type); /** * @brief Validator: ensure 'required' attribute is present and non-null. @@ -307,9 +315,10 @@ void validateMap(const PropertyTree& node); /** * @brief Validtor: ensure node value is of type YAML::NodeType::Sequence * @param node Node to validate. + * @param length The length if fixed size. If zero, it is considered dynamic size sequence * @throws runtime_error if not correct type. */ -void validateSequence(const PropertyTree& node); +void validateSequence(const PropertyTree& node, std::size_t length = 0); /** * @brief Validator: ensure property is a container of child properties diff --git a/tesseract_common/include/tesseract_common/schema_registration.h b/tesseract_common/include/tesseract_common/schema_registration.h index d02063ac1f9..95a32e555ba 100644 --- a/tesseract_common/include/tesseract_common/schema_registration.h +++ b/tesseract_common/include/tesseract_common/schema_registration.h @@ -17,12 +17,18 @@ struct SchemaRegistrar } // namespace tesseract_common +// first level: does the actual pasting +#define _SCHEMA_REG_PASTE(a, b) a##b + +// second level: expands its arguments before passing to the first +#define _SCHEMA_REG_MAKE_NAME(a, b) _SCHEMA_REG_PASTE(a, b) + /// Macro to register either a file‐based schema or a function‐built schema #define TESSERACT_REGISTER_SCHEMA(KEY, SCHEMA_SOURCE) \ namespace \ { \ /* now a const POD, linter is happy */ \ - static const int _reg_##KEY = []() -> int { \ + static const int _SCHEMA_REG_MAKE_NAME(_schema_reg_, __COUNTER__) = []() -> int { \ using namespace tesseract_common; \ /* calls the appropriate SchemaRegistrar constructor */ \ SchemaRegistrar(#KEY, SCHEMA_SOURCE); \ diff --git a/tesseract_common/include/tesseract_common/yaml_extenstions.h b/tesseract_common/include/tesseract_common/yaml_extensions.h similarity index 92% rename from tesseract_common/include/tesseract_common/yaml_extenstions.h rename to tesseract_common/include/tesseract_common/yaml_extensions.h index 97bcabf49bf..3bb6fe39b1a 100644 --- a/tesseract_common/include/tesseract_common/yaml_extenstions.h +++ b/tesseract_common/include/tesseract_common/yaml_extensions.h @@ -1,5 +1,5 @@ /** - * @file yaml_extenstions.h + * @file yaml_extensions.h * @brief YAML Type conversions * * @author Levi Armstrong @@ -982,6 +982,40 @@ struct convert } return true; } + + static tesseract_common::PropertyTree schema() + { + using namespace tesseract_common; + using namespace property_attribute; + using namespace property_type; + + // top‐level must be a map container + PropertyTree sch; + sch.setAttribute(TYPE, "tesseract_common::PairsCollisionMarginData"); + sch.addValidator([](const PropertyTree& node) { + const YAML::Node& yn = node.getValue(); + if (!yn.IsMap()) + std::throw_with_nested(std::runtime_error("PairsCollisionMarginData, must be a map")); + + for (auto it = yn.begin(); it != yn.end(); ++it) + { + Node key_node = it->first; + if (!key_node.IsSequence() || key_node.size() != 2) + std::throw_with_nested(std::runtime_error("PairsCollisionMarginData, key must be a sequenc of size 2")); + + try + { + it->second.as(); + } + catch (const std::exception& e) + { + std::throw_with_nested(e); + } + } + }); + + return sch; + } }; //================================== CollisionMarginPairData ================================= @@ -1000,6 +1034,19 @@ struct convert rhs = tesseract_common::CollisionMarginPairData(data); return true; } + + static tesseract_common::PropertyTree schema() + { + using namespace tesseract_common; + using namespace property_attribute; + using namespace property_type; + + // top‐level must be a map container + PropertyTree sch; + sch.setAttribute(TYPE, "tesseract_common::PairsCollisionMarginData"); + + return sch; + } }; //============================ AllowedCollisionEntries ============================ @@ -1044,6 +1091,40 @@ struct convert } return true; } + + static tesseract_common::PropertyTree schema() + { + using namespace tesseract_common; + using namespace property_attribute; + using namespace property_type; + + // top‐level must be a map container + PropertyTree sch; + sch.setAttribute(TYPE, "tesseract_common::AllowedCollisionEntries"); + sch.addValidator([](const PropertyTree& node) { + const YAML::Node& yn = node.getValue(); + if (!yn.IsMap()) + std::throw_with_nested(std::runtime_error("AllowedCollisionEntries, must be a map")); + + for (auto it = yn.begin(); it != yn.end(); ++it) + { + Node key_node = it->first; + if (!key_node.IsSequence() || key_node.size() != 2) + std::throw_with_nested(std::runtime_error("AllowedCollisionEntries, key must be a sequenc of size 2")); + + try + { + it->second.as(); + } + catch (const std::exception& e) + { + std::throw_with_nested(e); + } + } + }); + + return sch; + } }; //================================== AllowedCollisionMatrix ================================= @@ -1062,6 +1143,19 @@ struct convert rhs = tesseract_common::AllowedCollisionMatrix(data); return true; } + + static tesseract_common::PropertyTree schema() + { + using namespace tesseract_common; + using namespace property_attribute; + using namespace property_type; + + // top‐level must be a map container + PropertyTree sch; + sch.setAttribute(TYPE, "tesseract_common::AllowedCollisionEntries"); + + return sch; + } }; //============================== std::unordered_map ============================= diff --git a/tesseract_common/src/plugin_info.cpp b/tesseract_common/src/plugin_info.cpp index 6136bb5c05c..170e1d57063 100644 --- a/tesseract_common/src/plugin_info.cpp +++ b/tesseract_common/src/plugin_info.cpp @@ -40,7 +40,7 @@ TESSERACT_COMMON_IGNORE_WARNINGS_POP #include #include #include -#include +#include #include namespace tesseract_common diff --git a/tesseract_common/src/property_tree.cpp b/tesseract_common/src/property_tree.cpp index 2d6fab2f070..a06c3feb2b2 100644 --- a/tesseract_common/src/property_tree.cpp +++ b/tesseract_common/src/property_tree.cpp @@ -1,7 +1,8 @@ #include #include -#include +#include #include +#include const static std::string ATTRIBUTES_KEY{ "_attributes" }; const static std::string VALUE_KEY{ "_value" }; @@ -13,8 +14,19 @@ namespace tesseract_common { namespace property_type { -std::string createList(std::string_view type) { return std::string(type) + "[]"; }; -std::string createMap(std::string_view type) { return std::string(type) + "{}"; }; +std::string createList(std::string_view type, std::size_t length) +{ + if (length == 0) + return std::string(type) + "[]"; + + return std::string(type) + "[" + std::to_string(length) + "]"; +} + +std::string createMap(std::string_view key, std::string_view type) +{ + return "{" + std::string(key) + "," + std::string(type) + "}"; +} +std::string createMap(std::string_view type) { return createMap(STRING, type); } } // namespace property_type PropertyTree::PropertyTree(const PropertyTree& other) @@ -248,11 +260,12 @@ void PropertyTree::setAttribute(std::string_view name, const YAML::Node& attr) { const auto str_type = attr.as(); - std::optional is_sequence = isSequenceType(str_type); + std::optional> is_sequence = isSequenceType(str_type); if (is_sequence.has_value()) - validators_.emplace_back(validateSequence); + validators_.emplace_back( + [length = is_sequence.value().second](const PropertyTree& node) { validateSequence(node, length); }); - std::optional is_map = isMapType(str_type); + std::optional> is_map = isMapType(str_type); if (is_map.has_value()) validators_.emplace_back(validateMap); @@ -282,21 +295,21 @@ void PropertyTree::setAttribute(std::string_view name, const YAML::Node& attr) // validators_.emplace_back(validateTypeCast); else if (str_type == property_type::EIGEN_VECTOR_XD) { - validators_.emplace_back(validateSequence); + validators_.emplace_back([](const PropertyTree& node) { validateSequence(node); }); validators_.emplace_back(validateTypeCast); } // else if (str_type == property_type::EIGEN_MATRIX_2D) // validators_.emplace_back(validateTypeCast); else if (str_type == property_type::EIGEN_VECTOR_2D) { - validators_.emplace_back(validateSequence); + validators_.emplace_back([](const PropertyTree& node) { validateSequence(node, 2); }); validators_.emplace_back(validateTypeCast); } // else if (str_type == property_type::EIGEN_MATRIX_3D) // validators_.emplace_back(validateTypeCast); else if (str_type == property_type::EIGEN_VECTOR_3D) { - validators_.emplace_back(validateSequence); + validators_.emplace_back([](const PropertyTree& node) { validateSequence(node, 3); }); validators_.emplace_back(validateTypeCast); } } @@ -461,26 +474,40 @@ YAML::Node PropertyTree::toYAML(bool exclude_attributes) const PropertyTree::operator bool() const noexcept { return (!children_.empty() || !value_.IsNull()); } -std::optional isSequenceType(std::string_view type) +std::optional> isSequenceType(std::string_view type) { - if (type.size() < 2) - return std::nullopt; + static const std::regex re(R"(^([^\[\]]+)\[(\d*)\]$)"); + std::string s{ type }; + + std::smatch m; + if (std::regex_match(s, m, re)) + { + std::string base_type = m[1].str(); + std::string length_str = m[2].str(); + std::size_t length{ 0 }; + if (!length_str.empty()) + { + if (!toNumeric(length_str, length)) + std::throw_with_nested(std::runtime_error("Invalid fixed size sequence definition")); + } - if (type.substr(type.size() - 2) != "[]") - return std::nullopt; + return std::make_pair(base_type, length); + } - return std::string(type.substr(0, type.size() - 2)); + return std::nullopt; } -std::optional isMapType(std::string_view type) +std::optional> isMapType(std::string_view type) { - if (type.size() < 2) - return std::nullopt; + static const std::regex re(R"(^\{([^,]+),([^}]+)\}$)"); + std::string s{ type }; + std::smatch m; - if (type.substr(type.size() - 2) != "{}") - return std::nullopt; + // m[0] is the full match, m[1] is the first capture, m[2] the second + if (std::regex_match(s, m, re)) + return std::make_pair(m[1].str(), m[2].str()); - return std::string(type.substr(0, type.size() - 2)); + return std::nullopt; } void validateRequired(const PropertyTree& node) @@ -515,10 +542,16 @@ void validateMap(const PropertyTree& node) std::throw_with_nested(std::runtime_error("Property value is not of type YAML::NodeType::Map")); } -void validateSequence(const PropertyTree& node) +void validateSequence(const PropertyTree& node, std::size_t length) { if (!node.getValue().IsSequence()) std::throw_with_nested(std::runtime_error("Property value is not of type YAML::NodeType::Sequence")); + + if (length == 0) + return; + + if (node.getValue().size() != length) + std::throw_with_nested(std::runtime_error("Property value YAML::NodeType::Sequence is not correct length")); } void validateContainer(const PropertyTree& node) @@ -537,7 +570,7 @@ void validateCustomType(const PropertyTree& node) std::throw_with_nested(std::runtime_error("Custom type validator was added buy type attribute does not exist")); const auto type_str = type_attr.value().as(); - std::optional is_sequence = isSequenceType(type_str); + std::optional> is_sequence = isSequenceType(type_str); auto registry = SchemaRegistry::instance(); if (!is_sequence.has_value()) @@ -551,11 +584,12 @@ void validateCustomType(const PropertyTree& node) } else { - if (!registry->contains(is_sequence.value())) - std::throw_with_nested(std::runtime_error("No scheme registry entry found for key: " + is_sequence.value())); + if (!registry->contains(is_sequence.value().first)) + std::throw_with_nested( + std::runtime_error("No scheme registry entry found for key: " + is_sequence.value().first)); const YAML::Node& sequence = node.getValue(); - PropertyTree schema = registry->get(is_sequence.value()); + PropertyTree schema = registry->get(is_sequence.value().first); for (auto it = sequence.begin(); it != sequence.end(); ++it) { PropertyTree copy_schema(schema); diff --git a/tesseract_common/src/yaml_extensions.cpp b/tesseract_common/src/yaml_extensions.cpp new file mode 100644 index 00000000000..f87e99454df --- /dev/null +++ b/tesseract_common/src/yaml_extensions.cpp @@ -0,0 +1,55 @@ +/** + * @file yaml_extensions.h + * @brief YAML Type conversions + * + * @author Levi Armstrong + * @date September 5, 2021 + * @version TODO + * @bug No known bugs + * + * @copyright Copyright (c) 2021, Southwest Research Institute + * + * @par License + * Software License Agreement (Apache License) + * @par + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * @par + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +TESSERACT_REGISTER_SCHEMA(Eigen::Isometry3d, YAML::convert::schema) +TESSERACT_REGISTER_SCHEMA(Eigen::VectorXd, YAML::convert::schema) +TESSERACT_REGISTER_SCHEMA(Eigen::Vector2d, YAML::convert::schema) +TESSERACT_REGISTER_SCHEMA(Eigen::Vector3d, YAML::convert::schema) +// TESSERACT_REGISTER_SCHEMA(tesseract_common::PluginInfo, YAML::convert::schema) +// TESSERACT_REGISTER_SCHEMA(tesseract_common::PluginInfoContainer, +// YAML::convert::schema) +TESSERACT_REGISTER_SCHEMA(tesseract_common::KinematicsPluginInfo, + YAML::convert::schema) +TESSERACT_REGISTER_SCHEMA(tesseract_common::ContactManagersPluginInfo, + YAML::convert::schema) +TESSERACT_REGISTER_SCHEMA(tesseract_common::TaskComposerPluginInfo, + YAML::convert::schema) +TESSERACT_REGISTER_SCHEMA(tesseract_common::CalibrationInfo, YAML::convert::schema) +TESSERACT_REGISTER_SCHEMA(tesseract_common::TransformMap, YAML::convert::schema) +TESSERACT_REGISTER_SCHEMA(tesseract_common::Toolpath, YAML::convert::schema) +TESSERACT_REGISTER_SCHEMA(tesseract_common::CollisionMarginPairOverrideType, + YAML::convert::schema) +TESSERACT_REGISTER_SCHEMA(tesseract_common::PairsCollisionMarginData, + YAML::convert::schema) +TESSERACT_REGISTER_SCHEMA(tesseract_common::CollisionMarginPairData, + YAML::convert::schema) +TESSERACT_REGISTER_SCHEMA(tesseract_common::AllowedCollisionEntries, + YAML::convert::schema) +TESSERACT_REGISTER_SCHEMA(tesseract_common::AllowedCollisionMatrix, + YAML::convert::schema) diff --git a/tesseract_common/test/tesseract_common_unit.cpp b/tesseract_common/test/tesseract_common_unit.cpp index ab7d8f78d48..0ae0f9179c6 100644 --- a/tesseract_common/test/tesseract_common_unit.cpp +++ b/tesseract_common/test/tesseract_common_unit.cpp @@ -18,7 +18,7 @@ TESSERACT_COMMON_IGNORE_WARNINGS_POP #include #include #include -#include +#include #include #include #include diff --git a/tesseract_kinematics/core/src/kinematics_plugin_factory.cpp b/tesseract_kinematics/core/src/kinematics_plugin_factory.cpp index a160e14d4c1..4c8bca60e1b 100644 --- a/tesseract_kinematics/core/src/kinematics_plugin_factory.cpp +++ b/tesseract_kinematics/core/src/kinematics_plugin_factory.cpp @@ -34,7 +34,7 @@ TESSERACT_COMMON_IGNORE_WARNINGS_POP #include #include #include -#include +#include #include #include #include diff --git a/tesseract_srdf/src/configs.cpp b/tesseract_srdf/src/configs.cpp index 1d366afccc1..6bcc3168925 100644 --- a/tesseract_srdf/src/configs.cpp +++ b/tesseract_srdf/src/configs.cpp @@ -33,7 +33,7 @@ TESSERACT_COMMON_IGNORE_WARNINGS_POP #include #include #include -#include +#include #include namespace tesseract_srdf diff --git a/tesseract_srdf/src/srdf_model.cpp b/tesseract_srdf/src/srdf_model.cpp index 6944e8ec7c1..548e8428564 100644 --- a/tesseract_srdf/src/srdf_model.cpp +++ b/tesseract_srdf/src/srdf_model.cpp @@ -51,7 +51,7 @@ TESSERACT_COMMON_IGNORE_WARNINGS_POP #include #include #include -#include +#include #include #include #include diff --git a/tesseract_srdf/test/tesseract_srdf_unit.cpp b/tesseract_srdf/test/tesseract_srdf_unit.cpp index f8018d036da..59c72c0bebc 100644 --- a/tesseract_srdf/test/tesseract_srdf_unit.cpp +++ b/tesseract_srdf/test/tesseract_srdf_unit.cpp @@ -11,7 +11,7 @@ TESSERACT_COMMON_IGNORE_WARNINGS_POP #include #include #include -#include +#include #include #include #include