CppZmqZoltanExt 0.0.1
Loading...
Searching...
No Matches
zpl_config.cpp
Go to the documentation of this file.
1/*
2MIT License
3
4Copyright (c) 2025 Luan Young
5
6Permission is hereby granted, free of charge, to any person obtaining a copy
7of this software and associated documentation files (the "Software"), to deal
8in the Software without restriction, including without limitation the rights
9to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10copies of the Software, and to permit persons to whom the Software is
11furnished to do so, subject to the following conditions:
12
13The above copyright notice and this permission notice shall be included in all
14copies or substantial portions of the Software.
15
16THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22SOFTWARE.
23*/
38
39#include <cctype>
40#include <fstream>
41#include <sstream>
42#include <unordered_map>
43#include <utility>
44#include <vector>
45
46namespace zmqzext {
47
48namespace {
50std::string const k_empty_string;
51
53struct zpl_node_t {
54 std::string name;
55 std::string value;
56 bool explicitly_defined{false};
59 std::vector<std::shared_ptr<zpl_node_t>> ordered_children;
60 std::unordered_map<std::string, std::shared_ptr<zpl_node_t>> children_by_name;
61};
62
64struct parsed_line_t {
65 std::size_t indent_level;
66 std::string name;
67 std::string value;
68};
69
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]))) {
74 ++start;
75 }
76 return text.substr(start);
77}
78
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]))) {
83 --end;
84 }
85 return text.substr(0, end);
86}
87
89std::string trim_copy(const std::string& text) { return rtrim_copy(ltrim_copy(text)); }
90
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 == '/';
95}
96
98std::vector<std::string> split_segments(const std::string& path, bool& valid) {
99 valid = true;
100 std::vector<std::string> segments;
101
102 std::size_t pos = 0;
103 while (pos < path.size() && path[pos] == '/') {
104 ++pos;
105 }
106
107 if (pos == path.size()) {
108 return segments;
109 }
110
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;
114 if (end == pos) {
115 valid = false;
116 return {};
117 }
118
119 segments.emplace_back(path.substr(pos, end - pos));
120 if (slash == std::string::npos) {
121 break;
122 }
123 pos = slash + 1;
124 if (pos == path.size()) {
125 valid = false;
126 return {};
127 }
128 }
129
130 return segments;
131}
132
134std::vector<std::string> split_lines(const std::string& content) {
135 std::vector<std::string> lines;
136
137 std::size_t start = 0;
138 std::size_t i = 0;
139 while (i < content.size()) {
140 if (content[i] == '\n') {
141 lines.emplace_back(content.substr(start, i - start));
142 ++i;
143 start = i;
144 continue;
145 }
146 if (content[i] == '\r') {
147 lines.emplace_back(content.substr(start, i - start));
148 ++i;
149 if (i < content.size() && content[i] == '\n') {
150 ++i;
151 }
152 start = i;
153 continue;
154 }
155 ++i;
156 }
157
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("");
162 }
163
164 return lines;
165}
166
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()) {
171 return "";
172 }
173
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);
179 }
180
181 const std::string value = without_leading.substr(1, closing_quote - 1);
182 const std::string trailing = without_leading.substr(closing_quote + 1);
183
184 std::size_t pos = 0;
185 while (pos < trailing.size() && std::isblank(static_cast<unsigned char>(trailing[pos]))) {
186 ++pos;
187 }
188
189 if (pos == trailing.size()) {
190 return value;
191 }
192
193 if (trailing[pos] == '#') {
194 return value;
195 }
196
197 throw zpl_parse_error("invalid characters after quoted value", line_number);
198 }
199
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);
204}
205
207parsed_line_t parse_line(const std::string& line, std::size_t line_number) {
208 std::size_t pos = 0;
209 std::size_t leading_spaces = 0;
210
211 while (pos < line.size()) {
212 if (line[pos] == ' ') {
213 ++leading_spaces;
214 ++pos;
215 continue;
216 }
217 if (line[pos] == '\t') {
218 throw zpl_parse_error("tab indentation is not allowed", line_number);
219 }
220 break;
221 }
222
223 if ((leading_spaces % 4U) != 0U) {
224 throw zpl_parse_error("indentation must be divisible by 4 spaces", line_number);
225 }
226
227 const std::string content = line.substr(pos);
228 if (content.empty() || content.front() == '#') {
229 return {leading_spaces / 4U, "", ""};
230 }
231
232 const std::size_t equal_pos = content.find('=');
233 const std::size_t hash_pos = content.find('#');
234
235 std::string name;
236 std::string value;
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);
244 } else {
245 name = trim_copy(content.substr(0, equal_pos));
246 value = parse_value_fragment(content.substr(equal_pos + 1), line_number);
247 }
248
249 if (name.empty()) {
250 throw zpl_parse_error("property name is empty", line_number);
251 }
252
253 if (name.front() == '/' || name.back() == '/') {
254 throw zpl_parse_error("property name must not start or end with '/'", line_number);
255 }
256
257 for (char ch : name) {
258 if (!is_valid_name_char(ch)) {
259 throw zpl_parse_error("invalid character in property name", line_number);
260 }
261 }
262
263 return {leading_spaces / 4U, std::move(name), std::move(value)};
264}
265
267std::shared_ptr<zpl_node_t> parse_stream(std::istream& input) {
268 std::ostringstream buffer;
269 buffer << input.rdbuf();
270 if (input.bad()) {
271 throw std::ios_base::failure("failed while reading stream");
272 }
273 if (input.fail() && !input.eof()) {
274 throw std::ios_base::failure("failed while reading stream");
275 }
276
277 auto root = std::make_shared<zpl_node_t>();
278 std::vector<std::shared_ptr<zpl_node_t>> stack;
279 stack.push_back(root);
280
281 const auto lines = split_lines(buffer.str());
282
283 std::size_t previous_indent = 0;
284 bool has_previous_property = false;
285
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()) {
290 continue;
291 }
292
293 if (has_previous_property && parsed.indent_level > previous_indent + 1) {
294 throw zpl_parse_error("invalid indentation transition", line_number);
295 }
296 if (parsed.indent_level + 1 > stack.size()) {
297 throw zpl_parse_error("invalid indentation transition", line_number);
298 }
299
300 stack.resize(parsed.indent_level + 1);
301 auto current_parent = stack.back();
302
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);
307 }
308
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);
318 current = created;
319 } else {
320 current = found->second;
321 }
322
323 if (is_last) {
324 if (current->explicitly_defined) {
325 throw zpl_parse_error("duplicate property name in same level", line_number);
326 }
327 current->explicitly_defined = true;
328 current->value = parsed.value;
329 }
330 }
331
332 stack.push_back(current);
333 previous_indent = parsed.indent_level;
334 has_previous_property = true;
335 }
336
337 return root;
338}
339
341std::shared_ptr<zpl_node_t> find_node(const std::shared_ptr<zpl_node_t>& start, const std::string& path) {
342 if (!start) {
343 return nullptr;
344 }
345
346 bool valid = false;
347 const auto segments = split_segments(path, valid);
348 if (!valid) {
349 return nullptr;
350 }
351
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()) {
356 return nullptr;
357 }
358 current = found->second;
359 }
360
361 return current;
362}
363
364} // namespace
365
367 std::shared_ptr<zpl_node_t> node;
368};
369
370zpl_parse_error::zpl_parse_error(std::string message, std::size_t line) : zpl_error(std::move(message)), _line(line) {}
371
372std::size_t zpl_parse_error::line() const noexcept { return _line; }
373
374zpl_config_t::zpl_config_t() noexcept : _impl(std::make_shared<impl_t>()) {
375 _impl->node = std::make_shared<zpl_node_t>();
376}
377
378zpl_config_t::zpl_config_t(std::istream& input) : _impl(std::make_shared<impl_t>()) { load(input); }
379
381 zpl_config_t config;
382 config.load(input);
383 return config;
384}
385
386zpl_config_t zpl_config_t::from_file(const std::string& file_path) {
387 zpl_config_t config;
388 config.load_from_file(file_path);
389 return config;
390}
391
392void zpl_config_t::load(std::istream& input) { _impl->node = parse_stream(input); }
393
394void zpl_config_t::load_from_file(const std::string& file_path) {
395 std::ifstream file(file_path);
396 if (!file.is_open()) {
397 throw std::ios_base::failure("unable to open file: " + file_path);
398 }
399 load(file);
400}
401
402bool zpl_config_t::empty() const noexcept { return !_impl || !_impl->node || _impl->node->ordered_children.empty(); }
403
404const std::string& zpl_config_t::name() const noexcept {
405 if (!_impl || !_impl->node) {
406 return k_empty_string;
407 }
408 return _impl->node->name;
409}
410
411const std::string& zpl_config_t::value() const noexcept {
412 if (!_impl || !_impl->node) {
413 return k_empty_string;
414 }
415 return _impl->node->value;
416}
417
418bool zpl_config_t::contains(const std::string& path) const noexcept {
419 return static_cast<bool>(find_node(_impl ? _impl->node : nullptr, path));
420}
421
422const std::string& zpl_config_t::get(const std::string& path) const {
423 auto node = find_node(_impl ? _impl->node : nullptr, path);
424 if (!node) {
425 throw zpl_property_not_found("property not found: " + path);
426 }
427 return node->value;
428}
429
430std::optional<std::string> zpl_config_t::try_get(const std::string& path) const noexcept {
431 auto node = find_node(_impl ? _impl->node : nullptr, path);
432 if (!node) {
433 return std::nullopt;
434 }
435 return node->value;
436}
437
438std::string zpl_config_t::get_or(const std::string& path, std::string default_value) const noexcept {
439 auto node = find_node(_impl ? _impl->node : nullptr, path);
440 if (!node) {
441 return default_value;
442 }
443 return node->value;
444}
445
446zpl_config_t zpl_config_t::child(const std::string& path) const {
447 auto node = find_node(_impl ? _impl->node : nullptr, path);
448 if (!node) {
449 throw zpl_property_not_found("property not found: " + path);
450 }
451
452 zpl_config_t result;
453 result._impl->node = std::move(node);
454 return result;
455}
456
457std::optional<zpl_config_t> zpl_config_t::try_child(const std::string& path) const noexcept {
458 auto node = find_node(_impl ? _impl->node : nullptr, path);
459 if (!node) {
460 return std::nullopt;
461 }
462
463 zpl_config_t result;
464 result._impl->node = std::move(node);
465 return result;
466}
467
468std::vector<zpl_config_t> zpl_config_t::children() const noexcept {
469 if (!_impl || !_impl->node) {
470 return {};
471 }
472
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));
479 }
480 return result;
481}
482
483} // namespace zmqzext
ZPL configuration tree loaded from text or file.
Definition: zpl_config.h:137
bool contains(const std::string &path) const noexcept
Check if a path exists.
Definition: zpl_config.cpp:418
const std::string & name() const noexcept
Get the node name.
Definition: zpl_config.cpp:404
std::string get_or(const std::string &path, std::string default_value) const noexcept
Get a property value or return a default.
Definition: zpl_config.cpp:438
zpl_config_t() noexcept
Construct an empty configuration.
Definition: zpl_config.cpp:374
bool empty() const noexcept
Check whether the configuration is empty.
Definition: zpl_config.cpp:402
void load_from_file(const std::string &file_path)
Load configuration data from a file.
Definition: zpl_config.cpp:394
std::optional< std::string > try_get(const std::string &path) const noexcept
Try to get a property value by path.
Definition: zpl_config.cpp:430
std::optional< zpl_config_t > try_child(const std::string &path) const noexcept
Try to get a child node by path.
Definition: zpl_config.cpp:457
const std::string & get(const std::string &path) const
Get a property value by path.
Definition: zpl_config.cpp:422
std::vector< zpl_config_t > children() const noexcept
Get direct children of this node.
Definition: zpl_config.cpp:468
const std::string & value() const noexcept
Get the node value.
Definition: zpl_config.cpp:411
static zpl_config_t from_file(const std::string &file_path)
Parse and return a configuration from a file.
Definition: zpl_config.cpp:386
void load(std::istream &input)
Load configuration data from a stream.
Definition: zpl_config.cpp:392
zpl_config_t child(const std::string &path) const
Get a child node by path.
Definition: zpl_config.cpp:446
static zpl_config_t from_stream(std::istream &input)
Parse and return a configuration from a stream.
Definition: zpl_config.cpp:380
Base exception for ZPL configuration errors.
Definition: zpl_config.h:84
zpl_parse_error(std::string message, std::size_t line)
Construct a parse error with message and line.
Definition: zpl_config.cpp:370
std::size_t line() const noexcept
Get the line number where the parse error occurred.
Definition: zpl_config.cpp:372
Exception thrown when a requested property is missing.
Definition: zpl_config.h:116
std::shared_ptr< zpl_node_t > node
Root node for the parsed tree.
Definition: zpl_config.cpp:367
std::vector< std::shared_ptr< zpl_node_t > > ordered_children
Children in source order.
Definition: zpl_config.cpp:59
std::unordered_map< std::string, std::shared_ptr< zpl_node_t > > children_by_name
Fast lookup.
Definition: zpl_config.cpp:60
std::string value
Raw string value.
Definition: zpl_config.cpp:55
std::string name
Node name segment.
Definition: zpl_config.cpp:54
std::size_t indent_level
Indentation level (4-space units)
Definition: zpl_config.cpp:65
bool explicitly_defined
Definition: zpl_config.cpp:56
ZPL (ZeroMQ Property Language) parser.