Skip to content

Commit ee53e44

Browse files
Add property tree class to support yaml schema and validation
1 parent 6e71a7e commit ee53e44

File tree

4 files changed

+450
-1
lines changed

4 files changed

+450
-1
lines changed

tesseract_common/CMakeLists.txt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ add_library(
9494
src/plugin_info.cpp
9595
src/profile_dictionary.cpp
9696
src/profile.cpp
97+
src/property_tree.cpp
9798
src/resource_locator.cpp
9899
src/types.cpp
99100
src/stopwatch.cpp
@@ -124,7 +125,14 @@ target_code_coverage(
124125
target_include_directories(${PROJECT_NAME} PUBLIC "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
125126
"$<INSTALL_INTERFACE:include>")
126127

127-
configure_package(NAMESPACE tesseract TARGETS ${PROJECT_NAME})
128+
add_executable(${PROJECT_NAME}_property_tree_demo src/property_tree_demo.cpp)
129+
target_link_libraries(${PROJECT_NAME}_property_tree_demo PRIVATE ${PROJECT_NAME})
130+
target_compile_options(${PROJECT_NAME}_property_tree_demo PRIVATE ${TESSERACT_COMPILE_OPTIONS_PRIVATE}
131+
${TESSERACT_COMPILE_OPTIONS_PUBLIC})
132+
target_compile_definitions(${PROJECT_NAME}_property_tree_demo PRIVATE ${TESSERACT_COMPILE_DEFINITIONS})
133+
target_cxx_version(${PROJECT_NAME}_property_tree_demo PUBLIC VERSION ${TESSERACT_CXX_VERSION})
134+
135+
configure_package(NAMESPACE tesseract TARGETS ${PROJECT_NAME} ${PROJECT_NAME}_property_tree_demo)
128136
if(WIN32)
129137
target_link_libraries(${PROJECT_NAME} PUBLIC Bcrypt)
130138
endif()
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#ifndef TESSERACT_COMMON_PROPERTY_TREE_H
2+
#define TESSERACT_COMMON_PROPERTY_TREE_H
3+
4+
#include <string>
5+
#include <map>
6+
#include <optional>
7+
#include <vector>
8+
#include <yaml-cpp/yaml.h>
9+
10+
namespace tesseract_common
11+
{
12+
namespace property_type
13+
{
14+
constexpr std::string_view BOOL{ "bool" };
15+
constexpr std::string_view STRING{ "string" };
16+
constexpr std::string_view INT{ "int" };
17+
constexpr std::string_view FLAOT{ "float" };
18+
} // namespace property_type
19+
20+
namespace property_attribute
21+
{
22+
constexpr std::string_view REQUIRED{ "required" };
23+
constexpr std::string_view DEFAULT{ "default" };
24+
constexpr std::string_view ENUM{ "enum" };
25+
constexpr std::string_view MINIMUM{ "minimum" };
26+
constexpr std::string_view MAXIMUM{ "maximum" };
27+
} // namespace property_attribute
28+
29+
class PropertyTree
30+
{
31+
public:
32+
using ValidatorFn = std::function<bool(const PropertyTree&, std::vector<std::string>&, const std::string&)>;
33+
34+
PropertyTree() = default;
35+
36+
void mergeSchema(const PropertyTree& schema);
37+
38+
bool validate(std::vector<std::string>& errors, const std::string& path = "") const;
39+
40+
void addValidator(ValidatorFn fn);
41+
42+
PropertyTree& get(std::string_view key);
43+
const PropertyTree& get(std::string_view key) const;
44+
const PropertyTree* find(std::string_view key) const;
45+
46+
void setValue(const YAML::Node& v);
47+
const YAML::Node& getValue() const;
48+
49+
std::vector<std::string> keys() const;
50+
51+
void setAttribute(std::string_view name, const YAML::Node& attr);
52+
void setAttribute(std::string_view name, std::string_view attr);
53+
void setAttribute(std::string_view name, const char* attr);
54+
void setAttribute(std::string_view name, bool attr);
55+
void setAttribute(std::string_view name, int attr);
56+
void setAttribute(std::string_view name, double attr);
57+
58+
std::optional<YAML::Node> getAttribute(std::string_view name) const;
59+
bool hasAttribute(std::string_view name) const;
60+
61+
static PropertyTree fromYAML(const YAML::Node& node);
62+
63+
YAML::Node toYAML() const;
64+
65+
private:
66+
YAML::Node value_;
67+
std::map<std::string, YAML::Node> attributes_;
68+
std::map<std::string, PropertyTree> children_;
69+
std::vector<ValidatorFn> validators_;
70+
};
71+
72+
bool validateRequired(const PropertyTree& node, std::vector<std::string>& errors, const std::string& path);
73+
74+
bool validateRange(const PropertyTree& node, std::vector<std::string>& errors, const std::string& path);
75+
76+
bool validateEnum(const PropertyTree& node, std::vector<std::string>& errors, const std::string& path);
77+
78+
} // namespace tesseract_common
79+
80+
#endif // PROPERTY_TREE_H
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
#include <tesseract_common/property_tree.h>
2+
3+
const static std::string ATTRIBUTES_KEY{ "attributes" };
4+
const static std::string VALUE_KEY{ "value" };
5+
6+
namespace tesseract_common
7+
{
8+
void PropertyTree::mergeSchema(const PropertyTree& schema)
9+
{
10+
// merge schema attributes
11+
for (const auto& pair : schema.attributes_)
12+
{
13+
if (!hasAttribute(pair.first))
14+
attributes_[pair.first] = pair.second;
15+
}
16+
17+
// apply default value only when property is not required and no explicit value present
18+
auto default_it = attributes_.find(std::string(property_attribute::DEFAULT));
19+
if (default_it != attributes_.end())
20+
{
21+
// determine if property is marked required
22+
auto it = attributes_.find(std::string(property_attribute::REQUIRED));
23+
const bool required = (it != attributes_.end() && it->second.as<bool>());
24+
if (!required)
25+
{
26+
if (!value_ || value_.IsNull())
27+
value_ = YAML::Clone(default_it->second);
28+
}
29+
}
30+
31+
// merge schema validators
32+
for (const auto& fn : schema.validators_)
33+
validators_.push_back(fn);
34+
35+
// merge children recursively
36+
for (const auto& pair : schema.children_)
37+
{
38+
if (children_.find(pair.first) == children_.end())
39+
children_[pair.first] = PropertyTree();
40+
41+
children_[pair.first].mergeSchema(pair.second);
42+
}
43+
}
44+
45+
bool PropertyTree::validate(std::vector<std::string>& errors, const std::string& path) const
46+
{
47+
bool ok = true;
48+
// run custom validators
49+
for (const auto& vfn : validators_)
50+
{
51+
if (!vfn(*this, errors, path))
52+
ok = false;
53+
}
54+
// recurse children
55+
for (const auto& kv : children_)
56+
{
57+
const std::string nextPath = path.empty() ? kv.first : path + "." + kv.first;
58+
if (!kv.second.validate(errors, nextPath))
59+
ok = false;
60+
}
61+
return ok;
62+
}
63+
64+
/// Add a custom validator for this node
65+
void PropertyTree::addValidator(ValidatorFn fn) { validators_.push_back(std::move(fn)); }
66+
67+
PropertyTree& PropertyTree::get(std::string_view key) { return children_[std::string(key)]; }
68+
const PropertyTree& PropertyTree::get(std::string_view key) const { return children_.at(std::string(key)); }
69+
70+
const PropertyTree* PropertyTree::find(std::string_view key) const
71+
{
72+
auto it = children_.find(std::string(key));
73+
return it != children_.end() ? &it->second : nullptr;
74+
}
75+
76+
void PropertyTree::setValue(const YAML::Node& v) { value_ = v; }
77+
const YAML::Node& PropertyTree::getValue() const { return value_; }
78+
79+
std::vector<std::string> PropertyTree::keys() const
80+
{
81+
std::vector<std::string> res;
82+
res.reserve(children_.size());
83+
for (const auto& pair : children_)
84+
res.push_back(pair.first);
85+
return res;
86+
}
87+
88+
void PropertyTree::setAttribute(std::string_view name, const YAML::Node& attr)
89+
{
90+
attributes_[std::string(name)] = attr;
91+
}
92+
93+
void PropertyTree::setAttribute(std::string_view name, std::string_view attr)
94+
{
95+
setAttribute(name, YAML::Node(std::string(attr)));
96+
}
97+
98+
void PropertyTree::setAttribute(std::string_view name, const char* attr)
99+
{
100+
setAttribute(name, YAML::Node(std::string(attr)));
101+
}
102+
103+
void PropertyTree::setAttribute(std::string_view name, bool attr) { setAttribute(name, YAML::Node(attr)); }
104+
105+
void PropertyTree::setAttribute(std::string_view name, int attr) { setAttribute(name, YAML::Node(attr)); }
106+
107+
void PropertyTree::setAttribute(std::string_view name, double attr) { setAttribute(name, YAML::Node(attr)); }
108+
109+
std::optional<YAML::Node> PropertyTree::getAttribute(std::string_view name) const
110+
{
111+
auto it = attributes_.find(std::string(name));
112+
if (it != attributes_.end())
113+
return it->second;
114+
return std::nullopt;
115+
}
116+
117+
bool PropertyTree::hasAttribute(std::string_view name) const
118+
{
119+
auto it = attributes_.find(std::string(name));
120+
return (it != attributes_.end() && it->second && !it->second.IsNull());
121+
}
122+
123+
PropertyTree PropertyTree::fromYAML(const YAML::Node& node)
124+
{
125+
PropertyTree tree;
126+
tree.value_ = node;
127+
if (node.IsMap())
128+
{
129+
// extract attributes map
130+
if (node[ATTRIBUTES_KEY] && node[ATTRIBUTES_KEY].IsMap())
131+
{
132+
for (const auto& it : node[ATTRIBUTES_KEY])
133+
{
134+
const auto key = it.first.as<std::string>();
135+
tree.attributes_[key] = it.second;
136+
}
137+
}
138+
// extract children
139+
for (const auto& it : node)
140+
{
141+
const auto key = it.first.as<std::string>();
142+
if (key == ATTRIBUTES_KEY)
143+
continue;
144+
145+
tree.children_[key] = fromYAML(it.second);
146+
}
147+
}
148+
else if (node.IsSequence())
149+
{
150+
int idx = 0;
151+
for (const auto& it : node)
152+
tree.children_[std::to_string(idx++)] = fromYAML(it);
153+
}
154+
return tree;
155+
}
156+
157+
YAML::Node PropertyTree::toYAML() const
158+
{
159+
// Always emit a mapping if attributes exist or children exist
160+
if (!attributes_.empty() || !children_.empty())
161+
{
162+
YAML::Node node(YAML::NodeType::Map);
163+
// emit attributes first
164+
if (!attributes_.empty())
165+
{
166+
YAML::Node attr_node(YAML::NodeType::Map);
167+
for (const auto& pair : attributes_)
168+
attr_node[pair.first] = pair.second;
169+
170+
node[ATTRIBUTES_KEY] = attr_node;
171+
}
172+
// emit children
173+
for (const auto& pair : children_)
174+
node[pair.first] = pair.second.toYAML();
175+
176+
// if leaf (no children) but value present, emit under 'value'
177+
if (children_.empty() && value_)
178+
node[VALUE_KEY] = value_;
179+
180+
return node;
181+
}
182+
183+
// pure leaf without attributes: emit scalar/sequence directly
184+
return value_;
185+
}
186+
187+
bool validateRequired(const PropertyTree& node, std::vector<std::string>& errors, const std::string& path)
188+
{
189+
auto req_attr = node.getAttribute(property_attribute::REQUIRED);
190+
if (req_attr && req_attr->as<bool>())
191+
{
192+
// if leaf node with no value or null
193+
if (!node.getValue() || node.getValue().IsNull())
194+
{
195+
errors.push_back(path + ": required property missing or null");
196+
return false;
197+
}
198+
}
199+
return true;
200+
}
201+
202+
bool validateRange(const PropertyTree& node, std::vector<std::string>& errors, const std::string& path)
203+
{
204+
auto min_attr = node.getAttribute(property_attribute::MINIMUM);
205+
auto max_attr = node.getAttribute(property_attribute::MAXIMUM);
206+
if (min_attr && max_attr)
207+
{
208+
const auto minv = min_attr->as<double>();
209+
const auto maxv = max_attr->as<double>();
210+
const auto val = node.getValue().as<double>();
211+
if (val < minv || val > maxv)
212+
{
213+
errors.push_back(path + ": value " + std::to_string(val) + " out of range [" + std::to_string(minv) + "," +
214+
std::to_string(maxv) + "]");
215+
return false;
216+
}
217+
}
218+
return true;
219+
}
220+
221+
bool validateEnum(const PropertyTree& node, std::vector<std::string>& errors, const std::string& path)
222+
{
223+
auto enum_attr = node.getAttribute(property_attribute::ENUM);
224+
if (enum_attr.has_value() && enum_attr->IsSequence())
225+
{
226+
const auto val = node.getValue().as<std::string>();
227+
for (const auto& v : enum_attr.value())
228+
{
229+
if (v.as<std::string>() == val)
230+
return true;
231+
}
232+
errors.push_back(path + ": value '" + val + "' not in enum list");
233+
return false;
234+
}
235+
return true;
236+
}
237+
238+
} // namespace tesseract_common

0 commit comments

Comments
 (0)