42#include <unordered_map>
50std::string
const k_empty_string;
71std::string ltrim_copy(
const std::string& text) {
72 std::size_t start = 0;
73 while (start < text.size() && std::isblank(
static_cast<unsigned char>(text[start]))) {
76 return text.substr(start);
80std::string rtrim_copy(
const std::string& text) {
81 std::size_t end = text.size();
82 while (end > 0 && std::isblank(
static_cast<unsigned char>(text[end - 1]))) {
85 return text.substr(0, end);
89std::string trim_copy(
const std::string& text) {
return rtrim_copy(ltrim_copy(text)); }
92bool is_valid_name_char(
char ch)
noexcept {
93 return std::isalnum(
static_cast<unsigned char>(ch)) != 0 || ch ==
'$' || ch ==
'-' || ch ==
'_' || ch ==
'@' ||
94 ch ==
'.' || ch ==
'&' || ch ==
'+' || ch ==
'/';
98std::vector<std::string> split_segments(
const std::string& path,
bool& valid) {
100 std::vector<std::string> segments;
103 while (pos < path.size() && path[pos] ==
'/') {
107 if (pos == path.size()) {
111 while (pos <= path.size()) {
112 const std::size_t slash = path.find(
'/', pos);
113 const std::size_t end = (slash == std::string::npos) ? path.size() : slash;
119 segments.emplace_back(path.substr(pos, end - pos));
120 if (slash == std::string::npos) {
124 if (pos == path.size()) {
134std::vector<std::string> split_lines(
const std::string& content) {
135 std::vector<std::string> lines;
137 std::size_t start = 0;
139 while (i < content.size()) {
140 if (content[i] ==
'\n') {
141 lines.emplace_back(content.substr(start, i - start));
146 if (content[i] ==
'\r') {
147 lines.emplace_back(content.substr(start, i - start));
149 if (i < content.size() && content[i] ==
'\n') {
158 if (start < content.size()) {
159 lines.emplace_back(content.substr(start));
160 }
else if (!content.empty() && (content.back() ==
'\n' || content.back() ==
'\r')) {
161 lines.emplace_back(
"");
168std::string parse_value_fragment(
const std::string& fragment, std::size_t line_number) {
169 const std::string without_leading = ltrim_copy(fragment);
170 if (without_leading.empty()) {
174 const char first = without_leading.front();
175 if (first ==
'\'' || first ==
'"') {
176 const std::size_t closing_quote = without_leading.find(first, 1);
177 if (closing_quote == std::string::npos) {
178 throw zpl_parse_error(
"unterminated quoted value", line_number);
181 const std::string
value = without_leading.substr(1, closing_quote - 1);
182 const std::string trailing = without_leading.substr(closing_quote + 1);
185 while (pos < trailing.size() && std::isblank(
static_cast<unsigned char>(trailing[pos]))) {
189 if (pos == trailing.size()) {
193 if (trailing[pos] ==
'#') {
197 throw zpl_parse_error(
"invalid characters after quoted value", line_number);
200 const std::size_t comment_pos = without_leading.find(
'#');
201 const std::string raw_value =
202 (comment_pos == std::string::npos) ? without_leading : without_leading.substr(0, comment_pos);
203 return rtrim_copy(raw_value);
207parsed_line_t parse_line(
const std::string& line, std::size_t line_number) {
209 std::size_t leading_spaces = 0;
211 while (pos < line.size()) {
212 if (line[pos] ==
' ') {
217 if (line[pos] ==
'\t') {
218 throw zpl_parse_error(
"tab indentation is not allowed", line_number);
223 if ((leading_spaces % 4U) != 0U) {
224 throw zpl_parse_error(
"indentation must be divisible by 4 spaces", line_number);
227 const std::string content = line.substr(pos);
228 if (content.empty() || content.front() ==
'#') {
229 return {leading_spaces / 4U,
"",
""};
232 const std::size_t equal_pos = content.find(
'=');
233 const std::size_t hash_pos = content.find(
'#');
237 if (equal_pos == std::string::npos) {
238 const std::size_t comment_pos = content.find(
'#');
239 const std::string active = (comment_pos == std::string::npos) ? content : content.substr(0, comment_pos);
240 name = trim_copy(active);
241 }
else if (hash_pos != std::string::npos && hash_pos < equal_pos) {
242 const std::string active = content.substr(0, hash_pos);
243 name = trim_copy(active);
245 name = trim_copy(content.substr(0, equal_pos));
246 value = parse_value_fragment(content.substr(equal_pos + 1), line_number);
250 throw zpl_parse_error(
"property name is empty", line_number);
253 if (
name.front() ==
'/' ||
name.back() ==
'/') {
254 throw zpl_parse_error(
"property name must not start or end with '/'", line_number);
257 for (
char ch :
name) {
258 if (!is_valid_name_char(ch)) {
259 throw zpl_parse_error(
"invalid character in property name", line_number);
263 return {leading_spaces / 4U, std::move(
name), std::move(
value)};
267std::shared_ptr<zpl_node_t> parse_stream(std::istream& input) {
268 std::ostringstream buffer;
269 buffer << input.rdbuf();
271 throw std::ios_base::failure(
"failed while reading stream");
273 if (input.fail() && !input.eof()) {
274 throw std::ios_base::failure(
"failed while reading stream");
277 auto root = std::make_shared<zpl_node_t>();
278 std::vector<std::shared_ptr<zpl_node_t>> stack;
279 stack.push_back(root);
281 const auto lines = split_lines(buffer.str());
283 std::size_t previous_indent = 0;
284 bool has_previous_property =
false;
286 for (std::size_t i = 0; i < lines.size(); ++i) {
287 const std::size_t line_number = i + 1;
288 const parsed_line_t parsed = parse_line(lines[i], line_number);
289 if (parsed.name.empty()) {
293 if (has_previous_property && parsed.indent_level > previous_indent + 1) {
294 throw zpl_parse_error(
"invalid indentation transition", line_number);
296 if (parsed.indent_level + 1 > stack.size()) {
297 throw zpl_parse_error(
"invalid indentation transition", line_number);
300 stack.resize(parsed.indent_level + 1);
301 auto current_parent = stack.back();
303 bool path_valid =
false;
304 const auto segments = split_segments(parsed.name, path_valid);
305 if (!path_valid || segments.empty()) {
306 throw zpl_parse_error(
"invalid property name", line_number);
309 auto current = current_parent;
310 for (std::size_t segment_idx = 0; segment_idx < segments.size(); ++segment_idx) {
311 const bool is_last = (segment_idx + 1 == segments.size());
312 const auto found = current->children_by_name.find(segments[segment_idx]);
313 if (found == current->children_by_name.end()) {
314 auto created = std::make_shared<zpl_node_t>();
315 created->name = segments[segment_idx];
316 current->ordered_children.push_back(created);
317 current->children_by_name.emplace(segments[segment_idx], created);
320 current = found->second;
324 if (current->explicitly_defined) {
325 throw zpl_parse_error(
"duplicate property name in same level", line_number);
327 current->explicitly_defined =
true;
328 current->value = parsed.value;
332 stack.push_back(current);
333 previous_indent = parsed.indent_level;
334 has_previous_property =
true;
341std::shared_ptr<zpl_node_t> find_node(
const std::shared_ptr<zpl_node_t>& start,
const std::string& path) {
347 const auto segments = split_segments(path, valid);
352 auto current = start;
353 for (
const auto& segment : segments) {
354 const auto found = current->children_by_name.find(segment);
355 if (found == current->children_by_name.end()) {
358 current = found->second;
367 std::shared_ptr<zpl_node_t>
node;
375 _impl->node = std::make_shared<zpl_node_t>();
395 std::ifstream file(file_path);
396 if (!file.is_open()) {
397 throw std::ios_base::failure(
"unable to open file: " + file_path);
402bool zpl_config_t::empty() const noexcept {
return !_impl || !_impl->node || _impl->node->ordered_children.empty(); }
405 if (!_impl || !_impl->node) {
406 return k_empty_string;
408 return _impl->node->name;
412 if (!_impl || !_impl->node) {
413 return k_empty_string;
415 return _impl->node->value;
419 return static_cast<bool>(find_node(_impl ? _impl->node :
nullptr, path));
423 auto node = find_node(_impl ? _impl->node :
nullptr, path);
431 auto node = find_node(_impl ? _impl->node :
nullptr, path);
439 auto node = find_node(_impl ? _impl->node :
nullptr, path);
441 return default_value;
447 auto node = find_node(_impl ? _impl->node :
nullptr, path);
453 result._impl->node = std::move(node);
458 auto node = find_node(_impl ? _impl->node :
nullptr, path);
464 result._impl->node = std::move(node);
469 if (!_impl || !_impl->node) {
473 std::vector<zpl_config_t> result;
474 result.reserve(_impl->node->ordered_children.size());
475 for (
const auto& node : _impl->node->ordered_children) {
477 child._impl->node = node;
478 result.push_back(std::move(
child));
ZPL configuration tree loaded from text or file.
bool contains(const std::string &path) const noexcept
Check if a path exists.
const std::string & name() const noexcept
Get the node name.
std::string get_or(const std::string &path, std::string default_value) const noexcept
Get a property value or return a default.
zpl_config_t() noexcept
Construct an empty configuration.
bool empty() const noexcept
Check whether the configuration is empty.
void load_from_file(const std::string &file_path)
Load configuration data from a file.
std::optional< std::string > try_get(const std::string &path) const noexcept
Try to get a property value by path.
std::optional< zpl_config_t > try_child(const std::string &path) const noexcept
Try to get a child node by path.
const std::string & get(const std::string &path) const
Get a property value by path.
std::vector< zpl_config_t > children() const noexcept
Get direct children of this node.
const std::string & value() const noexcept
Get the node value.
static zpl_config_t from_file(const std::string &file_path)
Parse and return a configuration from a file.
void load(std::istream &input)
Load configuration data from a stream.
zpl_config_t child(const std::string &path) const
Get a child node by path.
static zpl_config_t from_stream(std::istream &input)
Parse and return a configuration from a stream.
Base exception for ZPL configuration errors.
zpl_parse_error(std::string message, std::size_t line)
Construct a parse error with message and line.
std::size_t line() const noexcept
Get the line number where the parse error occurred.
Exception thrown when a requested property is missing.
std::shared_ptr< zpl_node_t > node
Root node for the parsed tree.
std::vector< std::shared_ptr< zpl_node_t > > ordered_children
Children in source order.
std::unordered_map< std::string, std::shared_ptr< zpl_node_t > > children_by_name
Fast lookup.
std::string value
Raw string value.
std::string name
Node name segment.
std::size_t indent_level
Indentation level (4-space units)
ZPL (ZeroMQ Property Language) parser.