metadata 0.2.0
Modern C++23 header-only metadata container (JSON-like)
Loading...
Searching...
No Matches
path.hpp
1#pragma once
2
3#include <md/error.hpp>
4#include <md/object.hpp>
5#include <md/value.hpp>
6
7#include <cctype>
8#include <cstddef>
9#include <string>
10#include <string_view>
11
12namespace md {
13
14namespace detail {
15
16enum class PathSegmentKind {
17 Key,
18 Index,
19 End,
20 Malformed,
21};
22
23struct PathSegment {
24 PathSegmentKind kind = PathSegmentKind::End;
25 std::string_view key;
26 std::size_t index = 0;
27};
28
29// Pulls one segment from `path` starting at `pos` and updates `pos` past it.
30// On success returns Key or Index. Returns End when the path is exhausted.
31// Returns Malformed for unparsable input (e.g. unmatched bracket).
32inline PathSegment next_segment(const std::string_view path, std::size_t& pos) {
33 PathSegment out;
34 if (pos >= path.size()) {
35 out.kind = PathSegmentKind::End;
36 return out;
37 }
38
39 const char c = path[pos];
40
41 if (c == '[') {
42 ++pos;
43 const std::size_t start = pos;
44 while (pos < path.size() && path[pos] != ']') {
45 if (path[pos] < '0' || path[pos] > '9') {
46 out.kind = PathSegmentKind::Malformed;
47 return out;
48 }
49 ++pos;
50 }
51 if (pos >= path.size() || pos == start) {
52 // Either missing ']' or empty []
53 out.kind = PathSegmentKind::Malformed;
54 return out;
55 }
56 std::size_t idx = 0;
57 for (std::size_t i = start; i < pos; ++i) {
58 idx = (idx * 10) + static_cast<std::size_t>(path[i] - '0');
59 }
60 ++pos; // skip ']'
61 out.kind = PathSegmentKind::Index;
62 out.index = idx;
63 return out;
64 }
65
66 if (c == '.') {
67 // A leading '.' is malformed; a trailing '.' before next segment is also
68 // unusual. We consume the dot and expect a key to follow.
69 ++pos;
70 if (pos >= path.size() || path[pos] == '.' || path[pos] == '[') {
71 out.kind = PathSegmentKind::Malformed;
72 return out;
73 }
74 return next_segment(path, pos);
75 }
76
77 // Read an identifier segment until '.' or '['.
78 const std::size_t start = pos;
79 while (pos < path.size() && path[pos] != '.' && path[pos] != '[') {
80 ++pos;
81 }
82 out.kind = PathSegmentKind::Key;
83 out.key = path.substr(start, pos - start);
84 return out;
85}
86
87// Returns nullptr on miss, sets `malformed` to true on bad syntax.
88inline const Value* walk_path(const Object& root, const std::string_view path, bool& malformed) {
89 malformed = false;
90 if (path.empty()) {
91 // The root itself isn't a Value — but for symmetry, callers can
92 // interpret an empty path as "the object". We return nullptr to
93 // signal "no Value to return" and let callers handle root specially.
94 return nullptr;
95 }
96
97 std::size_t pos = 0;
98 PathSegment seg = next_segment(path, pos);
99 if (seg.kind == PathSegmentKind::Malformed) {
100 malformed = true;
101 return nullptr;
102 }
103 if (seg.kind != PathSegmentKind::Key) {
104 // Path must start with an identifier (root is an Object).
105 malformed = true;
106 return nullptr;
107 }
108
109 const Value* cur = root.find_ptr(seg.key);
110 if (cur == nullptr) {
111 return nullptr;
112 }
113
114 while (true) {
115 seg = next_segment(path, pos);
116 if (seg.kind == PathSegmentKind::End) {
117 return cur;
118 }
119 if (seg.kind == PathSegmentKind::Malformed) {
120 malformed = true;
121 return nullptr;
122 }
123 if (seg.kind == PathSegmentKind::Key) {
124 const Object* obj = cur->as_object_if();
125 if (obj == nullptr) {
126 malformed = true; // type mismatch — require_path() will throw type_error
127 return nullptr;
128 }
129 cur = obj->find_ptr(seg.key);
130 if (cur == nullptr) {
131 return nullptr;
132 }
133 } else { // Index
134 const Array* arr = cur->as_array_if();
135 if (arr == nullptr) {
136 malformed = true;
137 return nullptr;
138 }
139 if (seg.index >= arr->size()) {
140 return nullptr;
141 }
142 cur = &(*arr)[seg.index];
143 }
144 }
145}
146
147inline Value* walk_path_mut(Object& root, const std::string_view path, bool& malformed) {
148 // Reuse the const walker by stripping const off the result — the underlying
149 // storage is non-const (we have a non-const Object&), so this is safe.
150 const Object& croot = root;
151 const Value* p = walk_path(croot, path, malformed);
152 return const_cast<Value*>(p); // NOLINT(cppcoreguidelines-pro-type-const-cast)
153}
154
155} // namespace detail
156
157inline const Value* Object::find_path(const std::string_view path) const {
158 bool malformed = false;
159 return detail::walk_path(*this, path, malformed);
160}
161
162inline Value* Object::find_path(const std::string_view path) {
163 bool malformed = false;
164 return detail::walk_path_mut(*this, path, malformed);
165}
166
167inline const Value& Object::require_path(const std::string_view path) const {
168 bool malformed = false;
169 if (const Value* p = detail::walk_path(*this, path, malformed); p != nullptr) {
170 return *p;
171 }
172 if (malformed) {
173 throw type_error("Object::require_path: malformed path or type mismatch: '" +
174 std::string(path) + "'");
175 }
176 throw missing_key_error("Object::require_path: not found: '" + std::string(path) + "'");
177}
178
179inline Value& Object::require_path(const std::string_view path) {
180 bool malformed = false;
181 if (Value* p = detail::walk_path_mut(*this, path, malformed); p != nullptr) {
182 return *p;
183 }
184 if (malformed) {
185 throw type_error("Object::require_path: malformed path or type mismatch: '" +
186 std::string(path) + "'");
187 }
188 throw missing_key_error("Object::require_path: not found: '" + std::string(path) + "'");
189}
190
191inline bool Object::contains_path(const std::string_view path) const {
192 bool malformed = false;
193 return detail::walk_path(*this, path, malformed) != nullptr;
194}
195
196} // namespace md
Value * find_path(std::string_view path)
Find a value by dotted path (e.g. "a.b[0].c"), or nullptr on miss.
Definition path.hpp:162
bool contains_path(std::string_view path) const
True if path resolves to a value in the object.
Definition path.hpp:191
const Value & require_path(std::string_view path) const
Return the value at path; throws on miss or malformed path.
Definition path.hpp:167
Discriminated union holding one of the JSON-like alternatives (null, bool, signed/unsigned integer,...
Definition value.hpp:67
Thrown when a required key or path is not present in an Object.
Definition error.hpp:14
Thrown when a Value holds a different alternative than the caller demanded.
Definition error.hpp:19