cppfig 0.1.0
Modern C++20 compile-time type-safe configuration library
Loading...
Searching...
No Matches
value.h
Go to the documentation of this file.
1#pragma once
2
3#include <cmath>
4#include <cstddef>
5#include <cstdint>
6#include <limits>
7#include <map>
8#include <memory>
9#include <optional>
10#include <sstream>
11#include <stdexcept>
12#include <string>
13#include <string_view>
14#include <type_traits>
15#include <variant>
16#include <vector>
17
18#include "cppfig/status.h"
19
20namespace cppfig {
21
32class Value {
33public:
35 using ObjectType = std::map<std::string, Value, std::less<>>;
36
38 using ArrayType = std::vector<Value>;
39
40private:
41 using DataVariant = std::variant<std::nullptr_t, bool, std::int64_t, double, std::string, std::unique_ptr<ObjectType>, std::unique_ptr<ArrayType>>;
42
43 DataVariant data_;
44
45 static constexpr std::size_t idx_null = 0;
46 static constexpr std::size_t idx_bool = 1;
47 static constexpr std::size_t idx_int = 2;
48 static constexpr std::size_t idx_double = 3;
49 static constexpr std::size_t idx_string = 4;
50 static constexpr std::size_t idx_object = 5;
51 static constexpr std::size_t idx_array = 6;
52
53public:
56 : data_(nullptr)
57 {
58 }
59
61 Value(std::nullptr_t) // NOLINT(google-explicit-constructor)
62 : data_(nullptr)
63 {
64 }
65
67 Value(bool b) // NOLINT(google-explicit-constructor)
68 : data_(b)
69 {
70 }
71
73 Value(int i) // NOLINT(google-explicit-constructor)
74 : data_(static_cast<std::int64_t>(i))
75 {
76 }
77
79 Value(std::int64_t i) // NOLINT(google-explicit-constructor)
80 : data_(i)
81 {
82 }
83
85 Value(double d) // NOLINT(google-explicit-constructor)
86 : data_(d)
87 {
88 }
89
91 Value(float f) // NOLINT(google-explicit-constructor)
92 : data_(static_cast<double>(f))
93 {
94 }
95
97 Value(const char* s) // NOLINT(google-explicit-constructor)
98 : data_(std::string(s))
99 {
100 }
101
103 Value(std::string s) // NOLINT(google-explicit-constructor)
104 : data_(std::move(s))
105 {
106 }
107
109 Value(std::string_view s) // NOLINT(google-explicit-constructor)
110 : data_(std::string(s))
111 {
112 }
113
115 Value(const Value& other)
116 : data_(DeepCopy(other.data_))
117 {
118 }
119
121 auto operator=(const Value& other) -> Value&
122 {
123 if (this != &other) {
124 data_ = DeepCopy(other.data_);
125 }
126 return *this;
127 }
128
130 Value(Value&&) noexcept = default;
131
133 auto operator=(Value&&) noexcept -> Value& = default;
134
136 ~Value() = default;
137
139 [[nodiscard]] static auto Object() -> Value
140 {
141 Value v;
142 v.data_ = std::make_unique<ObjectType>();
143 return v;
144 }
145
147 [[nodiscard]] static auto Array() -> Value
148 {
149 Value v;
150 v.data_ = std::make_unique<ArrayType>();
151 return v;
152 }
153
155 [[nodiscard]] auto IsNull() const -> bool { return data_.index() == idx_null; }
156
158 [[nodiscard]] auto IsBoolean() const -> bool { return data_.index() == idx_bool; }
159
161 [[nodiscard]] auto IsInteger() const -> bool { return data_.index() == idx_int; }
162
164 [[nodiscard]] auto IsDouble() const -> bool { return data_.index() == idx_double; }
165
167 [[nodiscard]] auto IsNumber() const -> bool { return IsInteger() || IsDouble(); }
168
170 [[nodiscard]] auto IsString() const -> bool { return data_.index() == idx_string; }
171
173 [[nodiscard]] auto IsObject() const -> bool { return data_.index() == idx_object; }
174
176 [[nodiscard]] auto IsArray() const -> bool { return data_.index() == idx_array; }
177
183 template <typename T>
184 [[nodiscard]] auto Fits() const -> bool
185 {
186 if constexpr (std::is_same_v<T, bool>) {
187 return IsBoolean();
188 }
189 else if constexpr (std::is_same_v<T, int>) {
190 if (!IsInteger()) {
191 return false;
192 }
193 const auto stored = std::get<std::int64_t>(data_);
194 return stored >= std::numeric_limits<int>::min() && stored <= std::numeric_limits<int>::max();
195 }
196 else if constexpr (std::is_same_v<T, std::int64_t>) {
197 return IsInteger();
198 }
199 else if constexpr (std::is_same_v<T, double>) {
200 return IsNumber();
201 }
202 else if constexpr (std::is_same_v<T, float>) {
203 if (IsInteger()) {
204 return true;
205 }
206 if (!IsDouble()) {
207 return false;
208 }
209 const auto stored = std::get<double>(data_);
210 // Non-finite doubles convert to the matching float exactly.
211 return !std::isfinite(stored) || (stored >= -static_cast<double>(std::numeric_limits<float>::max()) && stored <= static_cast<double>(std::numeric_limits<float>::max()));
212 }
213 else if constexpr (std::is_same_v<T, std::string>) {
214 return IsString();
215 }
216 else {
217 static_assert(sizeof(T) == 0, "Unsupported type for Value::Fits<T>()");
218 }
219 }
220
233 template <typename T>
234 [[nodiscard]] auto Get() const -> T
235 {
236 if constexpr (std::is_same_v<T, bool>) {
237 return std::get<bool>(data_);
238 }
239 else if constexpr (std::is_same_v<T, int>) {
240 const auto stored = std::get<std::int64_t>(data_);
241 if (!Fits<int>()) {
242 throw std::out_of_range("Value " + std::to_string(stored) + " does not fit in int");
243 }
244 return static_cast<int>(stored);
245 }
246 else if constexpr (std::is_same_v<T, std::int64_t>) {
247 return std::get<std::int64_t>(data_);
248 }
249 else if constexpr (std::is_same_v<T, double>) {
250 if (IsInteger()) {
251 return static_cast<double>(std::get<std::int64_t>(data_));
252 }
253 return std::get<double>(data_);
254 }
255 else if constexpr (std::is_same_v<T, float>) {
256 if (IsInteger()) {
257 return static_cast<float>(std::get<std::int64_t>(data_));
258 }
259 const auto stored = std::get<double>(data_);
260 if (!Fits<float>()) {
261 throw std::out_of_range("Value " + std::to_string(stored) + " does not fit in float");
262 }
263 return static_cast<float>(stored);
264 }
265 else if constexpr (std::is_same_v<T, std::string>) {
266 return std::get<std::string>(data_);
267 }
268 else {
269 static_assert(sizeof(T) == 0, "Unsupported type for Value::Get<T>()");
270 }
271 }
272
278 [[nodiscard]] auto TryToText() const -> std::optional<std::string>
279 {
280 if (IsBoolean()) {
281 return std::get<bool>(data_) ? "true" : "false";
282 }
283 if (IsInteger()) {
284 return std::to_string(std::get<std::int64_t>(data_));
285 }
286 if (IsDouble()) {
287 std::ostringstream stream;
288 stream << std::get<double>(data_);
289 return stream.str();
290 }
291 if (IsString()) {
292 return std::get<std::string>(data_);
293 }
294 return std::nullopt;
295 }
296
303 template <typename T>
304 [[nodiscard]] auto TryGet() const -> std::optional<T>
305 {
306 if constexpr (std::is_same_v<T, bool>) {
307 if (!IsBoolean()) {
308 return std::nullopt;
309 }
310 }
311 else if constexpr (std::is_same_v<T, int> || std::is_same_v<T, std::int64_t>) {
312 if (!IsInteger()) {
313 return std::nullopt;
314 }
315 }
316 else if constexpr (std::is_same_v<T, double> || std::is_same_v<T, float>) {
317 if (!IsNumber()) {
318 return std::nullopt;
319 }
320 }
321 else if constexpr (std::is_same_v<T, std::string>) {
322 if (!IsString()) {
323 return std::nullopt;
324 }
325 }
326 else {
327 static_assert(sizeof(T) == 0, "Unsupported type for Value::TryGet<T>()");
328 }
329 return Get<T>();
330 }
331
333 [[nodiscard]] auto Contains(std::string_view key) const -> bool
334 {
335 if (!IsObject()) {
336 return false;
337 }
338 const auto& obj = *std::get<std::unique_ptr<ObjectType>>(data_);
339 return obj.find(key) != obj.end();
340 }
341
343 auto operator[](const std::string& key) -> Value&
344 {
345 if (!IsObject()) {
346 data_ = std::make_unique<ObjectType>();
347 }
348 auto& obj = *std::get<std::unique_ptr<ObjectType>>(data_);
349 return obj[key];
350 }
351
353 auto operator[](const std::string& key) const -> const Value&
354 {
355 static const Value null_value;
356 if (!IsObject()) {
357 return null_value;
358 }
359 const auto& obj = *std::get<std::unique_ptr<ObjectType>>(data_);
360 auto iter = obj.find(key);
361 if (iter == obj.end()) {
362 return null_value;
363 }
364 return iter->second;
365 }
366
368 [[nodiscard]] auto Items() const -> const ObjectType&
369 {
370 static const ObjectType empty;
371 if (!IsObject()) {
372 return empty;
373 }
374 return *std::get<std::unique_ptr<ObjectType>>(data_);
375 }
376
379 auto Items() -> ObjectType&
380 {
381 if (!IsObject()) {
382 data_ = std::make_unique<ObjectType>();
383 }
384 return *std::get<std::unique_ptr<ObjectType>>(data_);
385 }
386
388 [[nodiscard]] auto Elements() const -> const ArrayType&
389 {
390 static const ArrayType empty;
391 if (!IsArray()) {
392 return empty;
393 }
394 return *std::get<std::unique_ptr<ArrayType>>(data_);
395 }
396
400 {
401 if (!IsArray()) {
402 data_ = std::make_unique<ArrayType>();
403 }
404 return *std::get<std::unique_ptr<ArrayType>>(data_);
405 }
406
413 [[nodiscard]] static auto SplitPath(std::string_view path) -> std::vector<std::string>
414 {
415 std::vector<std::string> segments;
416 if (path.empty()) {
417 return segments;
418 }
419
420 std::string path_str(path);
421 std::istringstream stream(path_str);
422 std::string segment;
423
424 while (std::getline(stream, segment, '.')) {
425 if (segment.empty()) {
426 return {};
427 }
428 segments.push_back(segment);
429 }
430
431 // A trailing dot leaves no final segment for getline to produce.
432 if (path.back() == '.') {
433 return {};
434 }
435 return segments;
436 }
437
442 [[nodiscard]] auto GetAtPath(std::string_view path) const -> StatusOr<Value>
443 {
444 auto segments = SplitPath(path);
445 if (segments.empty()) {
446 return NotFoundError("Ill-formed configuration path: '" + std::string(path) + "'");
447 }
448
449 const Value* current = this;
450 for (const auto& segment : segments) {
451 if (!current->IsObject()) {
452 return NotFoundError("Path segment '" + segment + "' not found: parent is not an object");
453 }
454 if (!current->Contains(segment)) {
455 return NotFoundError("Path segment '" + segment + "' not found");
456 }
457 current = &(*current)[segment];
458 }
459 return *current;
460 }
461
466 void SetAtPath(std::string_view path, const Value& value)
467 {
468 auto segments = SplitPath(path);
469 if (segments.empty()) {
470 return;
471 }
472
473 Value* current = this;
474 for (std::size_t i = 0; i + 1 < segments.size(); ++i) {
475 if (!current->Contains(segments[i]) || !(*current)[segments[i]].IsObject()) {
476 (*current)[segments[i]] = Value::Object();
477 }
478 current = &(*current)[segments[i]];
479 }
480
481 (*current)[segments.back()] = value;
482 }
483
485 [[nodiscard]] auto HasPath(std::string_view path) const -> bool { return GetAtPath(path).ok(); }
486
491 [[nodiscard]] static auto Merge(const Value& base, const Value& overlay) -> Value
492 {
493 if (!base.IsObject() || !overlay.IsObject()) {
494 return overlay;
495 }
496
497 Value result = base;
498 for (const auto& [key, value] : overlay.Items()) {
499 if (result.Contains(key) && result[key].IsObject() && value.IsObject()) {
500 result[key] = Merge(result[key], value);
501 }
502 else {
503 result[key] = value;
504 }
505 }
506 return result;
507 }
508
512 [[nodiscard]] auto Dump(int indent = 0) const -> std::string
513 {
514 std::ostringstream stream;
515 DumpImpl(stream, indent, 0);
516 return stream.str();
517 }
518
520 auto operator==(const Value& other) const -> bool
521 {
522 if (data_.index() != other.data_.index()) {
523 return false;
524 }
525 if (IsNull()) {
526 return true;
527 }
528 if (IsBoolean()) {
529 return Get<bool>() == other.Get<bool>();
530 }
531 if (IsInteger()) {
532 return Get<std::int64_t>() == other.Get<std::int64_t>();
533 }
534 if (IsDouble()) {
535#pragma GCC diagnostic push
536#pragma GCC diagnostic ignored "-Wfloat-equal"
537 return Get<double>() == other.Get<double>();
538#pragma GCC diagnostic pop
539 }
540 if (IsString()) {
541 return Get<std::string>() == other.Get<std::string>();
542 }
543 if (IsObject()) {
544 return Items() == other.Items();
545 }
546 if (IsArray()) {
547 return *std::get<std::unique_ptr<ArrayType>>(data_) == *std::get<std::unique_ptr<ArrayType>>(other.data_);
548 }
549 return false; // LCOV_EXCL_LINE
550 }
551
553 auto operator!=(const Value& other) const -> bool { return !(*this == other); }
554
555private:
556 [[nodiscard]] static auto DeepCopy(const DataVariant& src) -> DataVariant
557 {
558 return std::visit(
559 [](const auto& held) -> DataVariant {
560 using Held = std::decay_t<decltype(held)>;
561 if constexpr (std::is_same_v<Held, std::unique_ptr<ObjectType>>) {
562 // A moved-from Value holds a null pointer.
563 return held ? std::make_unique<ObjectType>(*held) : std::make_unique<ObjectType>();
564 }
565 else if constexpr (std::is_same_v<Held, std::unique_ptr<ArrayType>>) {
566 return held ? std::make_unique<ArrayType>(*held) : std::make_unique<ArrayType>();
567 }
568 else {
569 return held;
570 }
571 },
572 src);
573 }
574
575 static void EscapeString(std::ostringstream& stream, const std::string& str)
576 {
577 stream << '"';
578 for (char ch : str) {
579 switch (ch) {
580 case '"':
581 stream << "\\\"";
582 break;
583 case '\\':
584 stream << "\\\\";
585 break;
586 case '\n':
587 stream << "\\n";
588 break;
589 case '\r':
590 stream << "\\r";
591 break;
592 case '\t':
593 stream << "\\t";
594 break;
595 default:
596 stream << ch;
597 }
598 }
599 stream << '"';
600 }
601
602 static void WriteIndent(std::ostringstream& stream, int indent, int depth)
603 {
604 if (indent > 0) {
605 stream << '\n';
606 for (int i = 0; i < indent * depth; ++i) {
607 stream << ' ';
608 }
609 }
610 }
611
612 void DumpImpl(std::ostringstream& stream, int indent, int depth) const
613 {
614 if (IsNull()) {
615 stream << "null";
616 }
617 else if (IsBoolean()) {
618 stream << (Get<bool>() ? "true" : "false");
619 }
620 else if (IsInteger()) {
621 stream << Get<std::int64_t>();
622 }
623 else if (IsDouble()) {
624 std::ostringstream double_stream;
625 double_stream << Get<double>();
626 auto str = double_stream.str();
627 stream << str;
628 // Ensure there is always a decimal point for clarity
629 if (str.find('.') == std::string::npos && str.find('e') == std::string::npos && str.find('E') == std::string::npos) {
630 stream << ".0";
631 }
632 }
633 else if (IsString()) {
634 EscapeString(stream, Get<std::string>());
635 }
636 else if (IsObject()) {
637 const auto& obj = Items();
638 stream << '{';
639 bool first = true;
640 for (const auto& [key, val] : obj) {
641 if (!first) {
642 stream << ',';
643 }
644 first = false;
645 WriteIndent(stream, indent, depth + 1);
646 if (indent > 0) {
647 // no space needed after newline+indent
648 }
649 stream << '"' << key << '"' << ':';
650 if (indent > 0) {
651 stream << ' ';
652 }
653 val.DumpImpl(stream, indent, depth + 1);
654 }
655 if (!obj.empty()) {
656 WriteIndent(stream, indent, depth);
657 }
658 stream << '}';
659 }
660 else if (IsArray()) {
661 const auto& arr = *std::get<std::unique_ptr<ArrayType>>(data_);
662 stream << '[';
663 bool first = true;
664 for (const auto& val : arr) {
665 if (!first) {
666 stream << ',';
667 }
668 first = false;
669 WriteIndent(stream, indent, depth + 1);
670 val.DumpImpl(stream, indent, depth + 1);
671 }
672 if (!arr.empty()) {
673 WriteIndent(stream, indent, depth);
674 }
675 stream << ']';
676 }
677 }
678};
679
680} // namespace cppfig
A value-or-error type, similar to std::expected (C++23).
Definition status.h:102
A self-contained, recursive value type for configuration data.
Definition value.h:32
auto operator=(const Value &other) -> Value &
Deep-copy assignment.
Definition value.h:121
Value(std::nullptr_t)
Constructs a null value.
Definition value.h:61
auto GetAtPath(std::string_view path) const -> StatusOr< Value >
Gets a value at a dot-separated path.
Definition value.h:442
auto operator==(const Value &other) const -> bool
Value equality (deep comparison for objects/arrays).
Definition value.h:520
auto IsBoolean() const -> bool
Returns true if this value is a boolean.
Definition value.h:158
auto Items() const -> const ObjectType &
Returns const reference to the object entries.
Definition value.h:368
Value()
Constructs a null value.
Definition value.h:55
static auto Merge(const Value &base, const Value &overlay) -> Value
Deep-merges two object values; overlay takes precedence.
Definition value.h:491
Value(const Value &other)
Deep-copies the value (recursive for objects / arrays).
Definition value.h:115
auto operator!=(const Value &other) const -> bool
Value inequality.
Definition value.h:553
auto Items() -> ObjectType &
Returns mutable reference to the object entries, promoting null → object.
Definition value.h:379
auto Contains(std::string_view key) const -> bool
Checks whether the given key exists in an object value.
Definition value.h:333
Value(int i)
Constructs an integer value from int.
Definition value.h:73
auto Fits() const -> bool
Returns true if the stored value is representable as T.
Definition value.h:184
auto IsNumber() const -> bool
Returns true if this value is any numeric type (integer or double).
Definition value.h:167
Value(std::string_view s)
Constructs a string value from string_view.
Definition value.h:109
Value(std::int64_t i)
Constructs an integer value.
Definition value.h:79
auto IsObject() const -> bool
Returns true if this value is an object (key-value map).
Definition value.h:173
std::map< std::string, Value, std::less<> > ObjectType
Ordered map of string keys to Value children.
Definition value.h:35
auto Elements() const -> const ArrayType &
Returns const reference to the array elements.
Definition value.h:388
static auto Array() -> Value
Creates an empty array value.
Definition value.h:147
auto IsNull() const -> bool
Returns true if this value is null.
Definition value.h:155
auto IsDouble() const -> bool
Returns true if this value is a double.
Definition value.h:164
Value(bool b)
Constructs a boolean value.
Definition value.h:67
void SetAtPath(std::string_view path, const Value &value)
Sets a value at a dot-separated path, creating intermediate objects.
Definition value.h:466
auto HasPath(std::string_view path) const -> bool
Checks if a path exists in the data.
Definition value.h:485
Value(Value &&) noexcept=default
Move constructor (default, transfers ownership).
auto TryToText() const -> std::optional< std::string >
Renders a scalar value as text, or nullopt for objects/arrays.
Definition value.h:278
Value(std::string s)
Constructs a string value.
Definition value.h:103
std::vector< Value > ArrayType
Ordered sequence of Value elements.
Definition value.h:38
static auto Object() -> Value
Creates an empty object value.
Definition value.h:139
auto IsInteger() const -> bool
Returns true if this value is an integer.
Definition value.h:161
auto IsString() const -> bool
Returns true if this value is a string.
Definition value.h:170
auto TryGet() const -> std::optional< T >
Extracts the stored value as T, or nullopt if it is not one.
Definition value.h:304
Value(const char *s)
Constructs a string value from a C string.
Definition value.h:97
auto IsArray() const -> bool
Returns true if this value is an array.
Definition value.h:176
auto Dump(int indent=0) const -> std::string
Produces a JSON-like string representation.
Definition value.h:512
Value(float f)
Constructs a double value from float.
Definition value.h:91
auto operator[](const std::string &key) -> Value &
Accesses or creates a child by key, promoting null → object.
Definition value.h:343
Value(double d)
Constructs a double value.
Definition value.h:85
static auto SplitPath(std::string_view path) -> std::vector< std::string >
Splits a dot-separated path into its segments.
Definition value.h:413
auto operator[](const std::string &key) const -> const Value &
Read-only access to a child by key (returns static null for missing keys).
Definition value.h:353
auto Get() const -> T
Extracts the stored value as the requested type.
Definition value.h:234
auto Elements() -> ArrayType &
Returns mutable reference to the array elements, promoting null → array.
Definition value.h:399
C++20 compile-time type-safe configuration library.
Definition conf.h:17
auto NotFoundError(std::string message) -> Status
Returns a NotFound error status.
Definition status.h:57