/src/glaze/include/glaze/json/lazy.hpp
Line | Count | Source |
1 | | // Glaze Library |
2 | | // For the license information refer to glaze.hpp |
3 | | |
4 | | #pragma once |
5 | | |
6 | | #include <array> |
7 | | #include <string_view> |
8 | | #include <type_traits> |
9 | | #include <utility> |
10 | | |
11 | | #include "glaze/json/read.hpp" |
12 | | #include "glaze/json/skip.hpp" |
13 | | #include "glaze/json/write.hpp" |
14 | | #include "glaze/tuplet/tuple.hpp" // GLZ_NO_UNIQUE_ADDRESS |
15 | | #include "glaze/util/expected.hpp" |
16 | | |
17 | | namespace glz |
18 | | { |
19 | | // Forward declarations (needed for circular references) |
20 | | template <auto Opts> |
21 | | struct lazy_document; |
22 | | template <auto Opts> |
23 | | class lazy_iterator; |
24 | | template <auto Opts> |
25 | | struct indexed_lazy_view; |
26 | | template <auto Opts> |
27 | | class indexed_lazy_iterator; |
28 | | |
29 | | // ============================================================================ |
30 | | // Truly Lazy JSON Parser - No upfront processing |
31 | | // ============================================================================ |
32 | | |
33 | | namespace detail |
34 | | { |
35 | | // lazy_json_view::get<std::string> unescapes its raw span by re-reading it through the JSON |
36 | | // reader. That read is independent of the document's options except for UTF-8 validation, |
37 | | // which the caller can turn off and must not be silently reinstated here. |
38 | | struct lazy_string_opts : opts |
39 | | { |
40 | | bool validate_utf8 = true; |
41 | | }; |
42 | | |
43 | | template <auto Opts> |
44 | | consteval lazy_string_opts lazy_string_opts_for() |
45 | | { |
46 | | lazy_string_opts o{}; |
47 | | o.validate_utf8 = check_validate_utf8(Opts); |
48 | | return o; |
49 | | } |
50 | | |
51 | | // Skip string - returns position after closing quote |
52 | | template <auto Opts> |
53 | | GLZ_ALWAYS_INLINE const char* skip_string_fast(const char* p, const char* end) noexcept |
54 | | { |
55 | | const char* const start = p; |
56 | | ++p; // skip opening quote |
57 | | |
58 | | // Find closing quote using memchr (SIMD-optimized in libc) |
59 | | while (p < end) { |
60 | | const char* q = static_cast<const char*>(std::memchr(p, '"', static_cast<size_t>(end - p))); |
61 | | if (!q) [[unlikely]] { |
62 | | return end; // unclosed string |
63 | | } |
64 | | |
65 | | // Count preceding backslashes to check if escaped |
66 | | size_t backslashes = 0; |
67 | | const char* check = q - 1; |
68 | | while (check > start && *check == '\\') { |
69 | | ++backslashes; |
70 | | --check; |
71 | | } |
72 | | |
73 | | p = q + 1; |
74 | | if ((backslashes & 1) == 0) { |
75 | | return p; // even backslashes = real quote |
76 | | } |
77 | | // odd backslashes = escaped quote, continue searching |
78 | | } |
79 | | return p; |
80 | | } |
81 | | |
82 | | // The escalated scan below jumps to the next byte the depth loops actually act on, so its |
83 | | // character set must be exactly the bytes lazy_char_class does not classify as `other`. |
84 | | // The two live in different headers, and that equality IS the correctness argument for |
85 | | // escalating, so pin it here rather than leaving it to a comment. |
86 | | inline constexpr std::array<char, 5> lazy_structural_chars{'"', '[', ']', '{', '}'}; |
87 | | |
88 | | template <size_t... I> |
89 | | GLZ_ALWAYS_INLINE const char* find_next_structural(const char* p, const char* end, |
90 | | std::index_sequence<I...>) noexcept |
91 | 0 | { |
92 | 0 | return glz::find_first_of<lazy_structural_chars[I]...>(p, end); |
93 | 0 | } |
94 | | |
95 | | // Scans to the next byte the depth loops act on. Expanded from lazy_structural_chars so the |
96 | | // scan and the assertion below cannot describe different sets. |
97 | | GLZ_ALWAYS_INLINE const char* find_next_structural(const char* p, const char* end) noexcept |
98 | 0 | { |
99 | 0 | return find_next_structural(p, end, std::make_index_sequence<lazy_structural_chars.size()>{}); |
100 | 0 | } |
101 | | |
102 | | static_assert( |
103 | | [] { |
104 | | for (size_t c = 0; c < 256; ++c) { |
105 | | const bool structural = lazy_char_class[c] != lazy_char_type::other; |
106 | | bool listed = false; |
107 | | for (const char l : lazy_structural_chars) { |
108 | | if (uint8_t(l) == c) listed = true; |
109 | | } |
110 | | // `number` bytes are non-`other` but are consumed by the run walk, not the scan. |
111 | | if (lazy_char_class[c] == lazy_char_type::number) continue; |
112 | | if (structural != listed) return false; |
113 | | } |
114 | | return true; |
115 | | }(), |
116 | | "the wide-number escalation set must match lazy_char_class's structural bytes"); |
117 | | |
118 | | // Skip a number inside a container, where the only bytes that matter are the structural |
119 | | // ones. Returns the position just past the numeric run. |
120 | | // |
121 | | // The default walk is a byte-at-a-time table lookup, which is what most JSON wants: typical |
122 | | // numbers are a few bytes long and a wide scan cannot amortize its setup over them. The |
123 | | // lazy_wide_number_skip option targets documents built from long numeric runs, where it |
124 | | // escalates to the SWAR scan once a run is still going after 8 bytes. Escalating is always |
125 | | // semantically safe - inside a container every non-structural byte is ignorable either way - |
126 | | // so the option changes only how fast the same position is reached. |
127 | | template <auto Opts> |
128 | | GLZ_ALWAYS_INLINE const char* skip_number_lazy(const char* p, const char* end) noexcept |
129 | | { |
130 | | ++p; |
131 | | if constexpr (Opts.null_terminated) { |
132 | | // A null-terminated buffer stops on its own sentinel, and end may be absent entirely |
133 | | // for a detached view, so the wide scan does not apply here. |
134 | | while (numeric_table[uint8_t(*p)]) ++p; |
135 | | return p; |
136 | | } |
137 | | else if constexpr (check_lazy_wide_number_skip(Opts)) { |
138 | | const char* const short_run_end = (end - p) > 8 ? p + 8 : end; |
139 | | while (p < short_run_end && numeric_table[uint8_t(*p)]) ++p; |
140 | | if (p == short_run_end && p < end) { |
141 | | return find_next_structural(p, end); |
142 | | } |
143 | | return p; |
144 | | } |
145 | | else { |
146 | | while (p < end && numeric_table[uint8_t(*p)]) ++p; |
147 | | return p; |
148 | | } |
149 | | } |
150 | | |
151 | | // Skip from current position to depth zero (end of enclosing container) |
152 | | // Used after partial scanning to find container end efficiently |
153 | | template <auto Opts> |
154 | | GLZ_ALWAYS_INLINE const char* skip_to_depth_zero(const char* p, const char* end, int depth) noexcept |
155 | | { |
156 | | using enum lazy_char_type; |
157 | | while (depth > 0) { |
158 | | if constexpr (Opts.null_terminated) { |
159 | | if (*p == '\0') break; |
160 | | } |
161 | | else { |
162 | | if (p >= end) break; |
163 | | } |
164 | | switch (lazy_char_class[uint8_t(*p)]) { |
165 | | case quote: |
166 | | p = skip_string_fast<Opts>(p, end); |
167 | | break; |
168 | | case open: |
169 | | ++depth; |
170 | | ++p; |
171 | | break; |
172 | | case close: |
173 | | --depth; |
174 | | ++p; |
175 | | break; |
176 | | case number: |
177 | | p = skip_number_lazy<Opts>(p, end); |
178 | | break; |
179 | | default: |
180 | | ++p; |
181 | | break; |
182 | | } |
183 | | } |
184 | | return p; |
185 | | } |
186 | | |
187 | | // skip_value stops a scalar on the byte that follows it, so a scalar occupying the tail of |
188 | | // a document reports unexpected_end: it runs out of buffer (bounded) or reaches the '\0' |
189 | | // sentinel (null-terminated) with nothing left to look at. Nothing is truncated at the |
190 | | // document edge, so that report is spurious and the value is complete -- the same |
191 | | // situation finalize_read_context settles for readers that land there. |
192 | | // |
193 | | // Containers and strings are deliberately excluded. Each carries its own terminator, so |
194 | | // reaching the edge without one is genuine truncation and must stay an error. |
195 | | [[nodiscard]] inline bool lazy_scalar_reaches_end(const char lead, const error_code ec, const char* it, |
196 | | const char* end) noexcept |
197 | 0 | { |
198 | 0 | return ec == error_code::unexpected_end && it == end && lead != '{' && lead != '[' && lead != '"'; |
199 | 0 | } |
200 | | |
201 | | // Skip any JSON value - optimized container skip |
202 | | // Returns pointer after the value |
203 | | template <auto Opts> |
204 | | GLZ_ALWAYS_INLINE const char* skip_value_lazy(const char* p, const char* end) noexcept |
205 | | { |
206 | | using enum lazy_char_type; |
207 | | if constexpr (!Opts.null_terminated) { |
208 | | // Non-null-terminated buffers have no '\0' sentinel, so a caller can reach here |
209 | | // with p == end (e.g. a key whose value is missing). Guard the tag dereference. |
210 | | if (p >= end) return p; |
211 | | } |
212 | | switch (*p) { |
213 | | case '"': |
214 | | return skip_string_fast<Opts>(p, end); |
215 | | case 't': |
216 | | return p + 4 <= end ? p + 4 : end; |
217 | | case 'f': |
218 | | return p + 5 <= end ? p + 5 : end; |
219 | | case 'n': |
220 | | return p + 4 <= end ? p + 4 : end; |
221 | | case '[': |
222 | | case '{': { |
223 | | int depth = 1; |
224 | | ++p; |
225 | | |
226 | | while (depth > 0) { |
227 | | if constexpr (Opts.null_terminated) { |
228 | | if (*p == '\0') break; |
229 | | } |
230 | | else { |
231 | | if (p >= end) break; |
232 | | } |
233 | | switch (lazy_char_class[uint8_t(*p)]) { |
234 | | case quote: |
235 | | p = skip_string_fast<Opts>(p, end); |
236 | | break; |
237 | | case open: |
238 | | ++depth; |
239 | | ++p; |
240 | | break; |
241 | | case close: |
242 | | --depth; |
243 | | ++p; |
244 | | break; |
245 | | case number: |
246 | | p = skip_number_lazy<Opts>(p, end); |
247 | | break; |
248 | | default: |
249 | | ++p; |
250 | | break; |
251 | | } |
252 | | } |
253 | | return p; |
254 | | } |
255 | | default: { |
256 | | // Number, or an unrecognized byte. |
257 | | const char* const start = p; |
258 | | if constexpr (Opts.null_terminated) { |
259 | | while (numeric_table[uint8_t(*p)]) ++p; |
260 | | } |
261 | | else { |
262 | | while (p < end && numeric_table[uint8_t(*p)]) ++p; |
263 | | } |
264 | | // Guarantee forward progress. A byte that begins no valid value (not a string, |
265 | | // literal, container, or number - impossible in well-formed JSON) leaves p unmoved, |
266 | | // which would spin every caller that skips values in a loop (size(), index(), |
267 | | // operator[], iteration) forever on malformed input. Treat it as a one-byte scalar so |
268 | | // the scan always advances whenever p < end. Bounded by end because json_end_ is the |
269 | | // authoritative message end: a byte within it is safe to step over, while at or past |
270 | | // end we must not move (the caller's own p >= end guard then stops the loop). On |
271 | | // valid input p is always a real value start, so this branch never fires - no |
272 | | // behavior change. |
273 | | if (p == start && p < end) ++p; |
274 | | return p; |
275 | | } |
276 | | } |
277 | | } |
278 | | |
279 | | // ============================================================================ |
280 | | // Streaming cursor storage (lazy_streaming_cursor option) |
281 | | // ============================================================================ |
282 | | // |
283 | | // A single slot on lazy_document remembering the byte extent of the most recently |
284 | | // consumed value. lazy_iterator::operator++ jumps to the recorded end instead of |
285 | | // re-scanning, but only when the recorded extent starts exactly at the element the |
286 | | // iterator is sitting on. That pointer-identity check is what makes a stale slot |
287 | | // harmless: a byte offset in the buffer identifies exactly one value, so an extent |
288 | | // that starts where the iterator stands always describes the value it is about to |
289 | | // skip. Anything else falls back to a normal scan. |
290 | | |
291 | | // Disabled: an empty type, so lazy_document pays nothing under GLZ_NO_UNIQUE_ADDRESS. |
292 | | struct lazy_extent_disabled |
293 | | { |
294 | 0 | static constexpr void clear() noexcept {} |
295 | 0 | static constexpr void set(const char*, const char*, const char*) noexcept {} |
296 | 0 | [[nodiscard]] static constexpr bool try_jump(const char*, const char*, const char*&) noexcept { return false; } |
297 | | }; |
298 | | |
299 | | // Offsets rather than pointers so a copied/moved document rebases onto its own buffer. |
300 | | // end_off == 0 means "no extent recorded" - a real value can never end at offset 0. |
301 | | struct lazy_extent |
302 | | { |
303 | | size_t start_off{}; |
304 | | size_t end_off{}; |
305 | | |
306 | 0 | void clear() noexcept { end_off = 0; } |
307 | | |
308 | | void set(const char* base, const char* start, const char* end) noexcept |
309 | 0 | { |
310 | 0 | if (!base || start < base || end < start) [[unlikely]] { |
311 | 0 | clear(); |
312 | 0 | return; |
313 | 0 | } |
314 | 0 | start_off = size_t(start - base); |
315 | 0 | end_off = size_t(end - base); |
316 | 0 | } |
317 | | |
318 | | [[nodiscard]] bool try_jump(const char* base, const char* pos, const char*& out) noexcept |
319 | 0 | { |
320 | 0 | if (end_off == 0 || !base) { |
321 | 0 | return false; |
322 | 0 | } |
323 | 0 | if (pos != base + start_off) { |
324 | 0 | // The extent describes some other value; leave it in place, it may still match a |
325 | 0 | // later advance (an inner iterator can record before an outer one asks). |
326 | 0 | return false; |
327 | 0 | } |
328 | 0 | out = base + end_off; |
329 | 0 | clear(); // consumed exactly once |
330 | 0 | return true; |
331 | 0 | } |
332 | | }; |
333 | | |
334 | | template <auto Opts> |
335 | | using lazy_extent_for = std::conditional_t<check_lazy_streaming_cursor(Opts), lazy_extent, lazy_extent_disabled>; |
336 | | } // namespace detail |
337 | | |
338 | | // ============================================================================ |
339 | | // lazy_json_view - Truly lazy view with on-demand scanning |
340 | | // ============================================================================ |
341 | | |
342 | | /** |
343 | | * @brief A truly lazy view into JSON data. |
344 | | * |
345 | | * No upfront processing - all navigation uses SWAR-accelerated scanning. |
346 | | * |
347 | | * For objects, parse_pos_ tracks the current scan position to enable |
348 | | * efficient sequential key access (O(n) total instead of O(n²)). |
349 | | * |
350 | | * Memory layout (48 bytes on 64-bit, 24 bytes on 32-bit): |
351 | | * - doc_: pointer to document (8/4 bytes) |
352 | | * - data_: pointer to value start in JSON (8/4 bytes) |
353 | | * - parse_pos_: current scan position for progressive parsing (8/4 bytes) |
354 | | * - key_: stored key for iteration (16/8 bytes - string_view) |
355 | | * - error_: error code (4 bytes) |
356 | | * - padding: 4/0 bytes |
357 | | */ |
358 | | template <auto Opts = opts{}> |
359 | | struct lazy_json_view |
360 | | { |
361 | | private: |
362 | | const lazy_document<Opts>* doc_{}; |
363 | | const char* data_{}; |
364 | | mutable const char* parse_pos_{}; // Current scan position (advances on key access) |
365 | | std::string_view key_{}; |
366 | | error_code error_{error_code::none}; |
367 | | |
368 | | lazy_json_view(error_code ec) noexcept : error_(ec) {} |
369 | | |
370 | | public: |
371 | | lazy_json_view() = default; |
372 | | lazy_json_view(const lazy_document<Opts>* doc, const char* data) noexcept : doc_(doc), data_(data) {} |
373 | | |
374 | | [[nodiscard]] static lazy_json_view make_error(error_code ec) noexcept { return lazy_json_view{ec}; } |
375 | | |
376 | | [[nodiscard]] bool has_error() const noexcept { return error_ != error_code::none; } |
377 | | [[nodiscard]] error_code error() const noexcept { return error_; } |
378 | | |
379 | | // Type checking - direct from JSON byte |
380 | | [[nodiscard]] bool is_null() const noexcept { return has_error() || !data_ || *data_ == 'n'; } |
381 | | [[nodiscard]] bool is_boolean() const noexcept |
382 | | { |
383 | | return !has_error() && data_ && (*data_ == 't' || *data_ == 'f'); |
384 | | } |
385 | | [[nodiscard]] bool is_number() const noexcept |
386 | | { |
387 | | return !has_error() && data_ && (is_digit(uint8_t(*data_)) || *data_ == '-'); |
388 | | } |
389 | | [[nodiscard]] bool is_string() const noexcept { return !has_error() && data_ && *data_ == '"'; } |
390 | | [[nodiscard]] bool is_array() const noexcept { return !has_error() && data_ && *data_ == '['; } |
391 | | [[nodiscard]] bool is_object() const noexcept { return !has_error() && data_ && *data_ == '{'; } |
392 | | |
393 | | explicit operator bool() const noexcept { return !has_error() && data_ && *data_ != 'n'; } |
394 | | |
395 | | [[nodiscard]] const char* data() const noexcept { return data_; } |
396 | | [[nodiscard]] const char* json_end() const noexcept; |
397 | | |
398 | | /// @brief Get the raw JSON bytes for this value |
399 | | /// @return string_view of the raw JSON, or empty if error |
400 | | /// @note Useful for passing to glz::read_json to deserialize into a struct |
401 | | /// @note Consider using read_into<T>() instead for better performance |
402 | | [[nodiscard]] std::string_view raw_json() const noexcept |
403 | | { |
404 | | if (has_error() || !data_) return {}; |
405 | | const char* end = detail::skip_value_lazy<Opts>(data_, json_end()); |
406 | | return {data_, static_cast<size_t>(end - data_)}; |
407 | | } |
408 | | |
409 | | /// @brief Parse this value directly into a C++ type (single-pass, no double scanning) |
410 | | /// @tparam T The type to parse into |
411 | | /// @param value The object to populate |
412 | | /// @return Error context (check error_ctx.error for success/failure) |
413 | | /// @note This is more efficient than raw_json() + read_json() as it avoids scanning twice |
414 | | template <class T> |
415 | | [[nodiscard]] error_ctx read_into(T& value) const |
416 | | { |
417 | | if (has_error()) { |
418 | | return error_ctx{0, error_}; |
419 | | } |
420 | | if (!data_) { |
421 | | return error_ctx{0, error_code::unexpected_end}; |
422 | | } |
423 | | // Use parse<JSON>::op which naturally stops at value end |
424 | | // This avoids the need to pre-scan to find the value's extent |
425 | | context ctx{}; |
426 | | auto it = data_; |
427 | | auto end = json_end(); |
428 | | parse<JSON>::op<Opts>(value, ctx, it, end); |
429 | | // Capture completeness before finalize_read_context, which downgrades |
430 | | // partial_read_complete and a depth-zero end_reached to success. Those leave `it` |
431 | | // short of the value's true end, so `it` is only the real extent when the parse |
432 | | // reported nothing at all. |
433 | | const bool consumed_whole_value = ctx.error == error_code::none; |
434 | | finalize_read_context<Opts>(ctx); |
435 | | if (bool(ctx.error)) { |
436 | | return error_ctx{static_cast<size_t>(it - data_), ctx.error}; |
437 | | } |
438 | | // Streaming cursor: the value spans [data_, it). Record it so a subsequent iterator |
439 | | // advance over this element can jump rather than re-scan. Under partial_read the |
440 | | // parse deliberately stops early, so nothing is recorded and iteration falls back to |
441 | | // scanning - correct, just not accelerated. |
442 | | if constexpr (check_lazy_streaming_cursor(Opts)) { |
443 | | // doc_ is null only for detached subviews; lazy_json() views always carry it. |
444 | | if (doc_ && consumed_whole_value) [[likely]] { |
445 | | record_consumed_extent(it); |
446 | | } |
447 | | } |
448 | | return {}; |
449 | | } |
450 | | |
451 | | // Record [data_, it) as consumed, but only when we can actually tell that `it` is this |
452 | | // value's end. |
453 | | // |
454 | | // read_into hands the iterator to parse<JSON>::op, and where that leaves it is up to the |
455 | | // from<JSON, T> specialization. Glaze's own readers stop at the value's end, but that is a |
456 | | // convention rather than something checkable here: glz::text deliberately swallows the |
457 | | // rest of the buffer, and a custom reader may consume only a prefix. Trusting `it` blindly |
458 | | // is how the cursor turns a well-formed document into a short or mis-valued element |
459 | | // stream, with no error anywhere. |
460 | | // |
461 | | // So only containers are recorded, and only when the parse finished exactly on the |
462 | | // element's own closing bracket. Scalars are skipped by the cheap paths anyway (a numeric |
463 | | // table walk or a memchr for strings), so declining to record them costs almost nothing |
464 | | // and removes the whole class of doubt. |
465 | | void record_consumed_extent(const char* it) const noexcept |
466 | | { |
467 | | const char open = *data_; |
468 | | const char close = (open == '{') ? '}' : ((open == '[') ? ']' : '\0'); |
469 | | if (close == '\0') { |
470 | | return; // scalar: not worth recording |
471 | | } |
472 | | if (it <= data_ || it[-1] != close) { |
473 | | return; // the reader stopped somewhere other than this container's end |
474 | | } |
475 | | doc_->consumed_.set(doc_->json_data(), data_, it); |
476 | | } |
477 | | |
478 | | template <class T> |
479 | | [[nodiscard]] expected<T, error_ctx> get() const; |
480 | | |
481 | | [[nodiscard]] lazy_json_view operator[](size_t index) const; |
482 | | [[nodiscard]] lazy_json_view operator[](std::string_view key) const; |
483 | | [[nodiscard]] bool contains(std::string_view key) const; |
484 | | [[nodiscard]] size_t size() const; |
485 | | [[nodiscard]] bool empty() const noexcept; |
486 | | |
487 | | // Key access for object iteration |
488 | | [[nodiscard]] std::string_view key() const noexcept { return key_; } |
489 | | |
490 | | [[nodiscard]] lazy_iterator<Opts> begin() const; |
491 | | [[nodiscard]] lazy_iterator<Opts> end() const; |
492 | | |
493 | | /// @brief Build an index for O(1) iteration and random access |
494 | | /// @return indexed_lazy_view with pre-computed element positions |
495 | | [[nodiscard]] indexed_lazy_view<Opts> index() const; |
496 | | |
497 | | private: |
498 | | friend struct lazy_document<Opts>; |
499 | | friend class lazy_iterator<Opts>; |
500 | | friend struct indexed_lazy_view<Opts>; |
501 | | friend class indexed_lazy_iterator<Opts>; |
502 | | |
503 | | // Constructor with key for iteration |
504 | | lazy_json_view(const lazy_document<Opts>* doc, const char* data, std::string_view key) noexcept |
505 | | : doc_(doc), data_(data), key_(key) |
506 | | {} |
507 | | |
508 | | // Skip whitespace helper |
509 | | // Note: The minified option is not used here because branch prediction |
510 | | // makes the whitespace loop essentially free when there's no whitespace. |
511 | | // Benchmarks show no benefit from skipping this check. |
512 | | static void skip_ws(const char*& p, [[maybe_unused]] const char* end) noexcept |
513 | | { |
514 | | if constexpr (Opts.null_terminated) { |
515 | | while (whitespace_table[uint8_t(*p)]) ++p; |
516 | | } |
517 | | else { |
518 | | while (p < end && whitespace_table[uint8_t(*p)]) ++p; |
519 | | } |
520 | | } |
521 | | |
522 | | // Parse a key from current position, return key and advance p |
523 | | static std::string_view parse_key(const char*& p, const char* end) noexcept; |
524 | | }; |
525 | | |
526 | | // ============================================================================ |
527 | | // lazy_document - Minimal container, no upfront processing |
528 | | // ============================================================================ |
529 | | |
530 | | template <auto Opts = opts{}> |
531 | | struct lazy_document |
532 | | { |
533 | | private: |
534 | | const char* json_{}; |
535 | | size_t len_{}; |
536 | | const char* root_data_{}; |
537 | | mutable lazy_json_view<Opts> root_view_{}; // Cached root view with parse_pos_ |
538 | | |
539 | | // Streaming cursor slot (lazy_streaming_cursor option). Empty, and therefore free, when |
540 | | // the option is off. Mutable because recording an extent is a caching side effect of |
541 | | // otherwise const traversal. |
542 | | GLZ_NO_UNIQUE_ADDRESS mutable detail::lazy_extent_for<Opts> consumed_{}; |
543 | | |
544 | | friend struct lazy_json_view<Opts>; |
545 | | friend class lazy_iterator<Opts>; |
546 | | |
547 | | // Factory method for lazy_json |
548 | | template <auto O, class Buffer> |
549 | | friend expected<lazy_document<O>, error_ctx> lazy_json(Buffer&&); |
550 | | |
551 | | // Helper to initialize root_view_ with correct doc_ pointer |
552 | | void init_root_view() noexcept { root_view_ = lazy_json_view<Opts>{this, root_data_}; } |
553 | | |
554 | | public: |
555 | | lazy_document() = default; |
556 | | |
557 | | // Copy constructor - fix doc_ pointer and preserve parse_pos_ |
558 | | lazy_document(const lazy_document& other) |
559 | | : json_(other.json_), len_(other.len_), root_data_(other.root_data_), consumed_(other.consumed_) |
560 | | { |
561 | | init_root_view(); |
562 | | root_view_.parse_pos_ = other.root_view_.parse_pos_; |
563 | | } |
564 | | |
565 | | lazy_document& operator=(const lazy_document& other) |
566 | | { |
567 | | if (this != &other) { |
568 | | json_ = other.json_; |
569 | | len_ = other.len_; |
570 | | root_data_ = other.root_data_; |
571 | | consumed_ = other.consumed_; |
572 | | init_root_view(); |
573 | | root_view_.parse_pos_ = other.root_view_.parse_pos_; |
574 | | } |
575 | | return *this; |
576 | | } |
577 | | |
578 | | // Move constructor - fix doc_ pointer and transfer parse_pos_ |
579 | | lazy_document(lazy_document&& other) noexcept |
580 | | : json_(other.json_), len_(other.len_), root_data_(other.root_data_), consumed_(other.consumed_) |
581 | | { |
582 | | init_root_view(); |
583 | | root_view_.parse_pos_ = other.root_view_.parse_pos_; |
584 | | } |
585 | | |
586 | | lazy_document& operator=(lazy_document&& other) noexcept |
587 | | { |
588 | | if (this != &other) { |
589 | | json_ = other.json_; |
590 | | len_ = other.len_; |
591 | | root_data_ = other.root_data_; |
592 | | consumed_ = other.consumed_; |
593 | | init_root_view(); |
594 | | root_view_.parse_pos_ = other.root_view_.parse_pos_; |
595 | | } |
596 | | return *this; |
597 | | } |
598 | | |
599 | | /// @brief Get the root view (cached, enables progressive key scanning) |
600 | | [[nodiscard]] lazy_json_view<Opts>& root() noexcept { return root_view_; } |
601 | | |
602 | | [[nodiscard]] const lazy_json_view<Opts>& root() const noexcept { return root_view_; } |
603 | | |
604 | | [[nodiscard]] lazy_json_view<Opts> operator[](std::string_view key) const { return root_view_[key]; } |
605 | | [[nodiscard]] lazy_json_view<Opts> operator[](size_t index) const { return root_view_[index]; } |
606 | | |
607 | | [[nodiscard]] bool is_null() const noexcept { return !root_data_ || *root_data_ == 'n'; } |
608 | | [[nodiscard]] bool is_array() const noexcept { return root_data_ && *root_data_ == '['; } |
609 | | [[nodiscard]] bool is_object() const noexcept { return root_data_ && *root_data_ == '{'; } |
610 | | |
611 | | explicit operator bool() const noexcept { return !is_null(); } |
612 | | |
613 | | [[nodiscard]] const char* json_data() const noexcept { return json_; } |
614 | | [[nodiscard]] size_t json_size() const noexcept { return len_; } |
615 | | |
616 | | /// @brief Reset parse position to beginning (for re-scanning from start) |
617 | | void reset_parse_pos() noexcept { root_view_.parse_pos_ = nullptr; } |
618 | | }; |
619 | | |
620 | | // ============================================================================ |
621 | | // lazy_iterator - Forward iterator with lazy scanning |
622 | | // ============================================================================ |
623 | | |
624 | | template <auto Opts> |
625 | | class lazy_iterator |
626 | | { |
627 | | private: |
628 | | const lazy_document<Opts>* doc_{}; |
629 | | const char* json_end_{}; |
630 | | const char* container_start_{}; // '[' or '{' of the container being iterated |
631 | | char close_char_{}; |
632 | | bool is_object_{}; |
633 | | bool at_end_{true}; |
634 | | lazy_json_view<Opts> current_view_{}; // Stored view for parse_pos_ optimization |
635 | | |
636 | | // Streaming cursor: on reaching the close, publish this container's own extent so an |
637 | | // outer iterator advancing over it can jump too. That is what makes the optimization |
638 | | // compose through nesting - an inner loop running to completion pays for the outer |
639 | | // advance. No-op when the option is off. |
640 | | void record_container_extent(const char* close_pos) noexcept |
641 | | { |
642 | | if constexpr (check_lazy_streaming_cursor(Opts)) { |
643 | | if (doc_ && container_start_) [[likely]] { |
644 | | // close_pos is the close character, except on a truncated buffer where it is |
645 | | // json_end_ and there is nothing past it to include. |
646 | | const char* const extent_end = (close_pos < json_end_) ? close_pos + 1 : close_pos; |
647 | | doc_->consumed_.set(doc_->json_data(), container_start_, extent_end); |
648 | | } |
649 | | } |
650 | | } |
651 | | |
652 | | // Scan past the element beginning at elem_start, using the parse_pos_ shortcut when keyed |
653 | | // access already walked part of it. |
654 | | [[nodiscard]] const char* scan_past_element(const char* elem_start) const noexcept |
655 | | { |
656 | | if (current_view_.is_object() && current_view_.parse_pos_ && current_view_.parse_pos_ > elem_start) { |
657 | | // Skip the last accessed value, then scan to the end of this object (depth 1 -> 0). |
658 | | const char* p = detail::skip_value_lazy<Opts>(current_view_.parse_pos_, json_end_); |
659 | | return detail::skip_to_depth_zero<Opts>(p, json_end_, 1); |
660 | | } |
661 | | return detail::skip_value_lazy<Opts>(elem_start, json_end_); |
662 | | } |
663 | | |
664 | | public: |
665 | | using iterator_category = std::forward_iterator_tag; |
666 | | using value_type = lazy_json_view<Opts>; |
667 | | using difference_type = std::ptrdiff_t; |
668 | | using pointer = void; |
669 | | using reference = lazy_json_view<Opts>&; |
670 | | |
671 | | lazy_iterator() = default; |
672 | | |
673 | | lazy_iterator(const lazy_document<Opts>* doc, const char* container_start, const char* end, bool is_object); |
674 | | |
675 | | // Return reference to stored view - allows parse_pos_ optimization when user uses auto& |
676 | | reference operator*() { return current_view_; } |
677 | | const lazy_json_view<Opts>& operator*() const { return current_view_; } |
678 | | |
679 | | lazy_iterator& operator++(); |
680 | | |
681 | | lazy_iterator operator++(int) |
682 | | { |
683 | | auto tmp = *this; |
684 | | ++*this; |
685 | | return tmp; |
686 | | } |
687 | | |
688 | | bool operator==(const lazy_iterator& other) const { return at_end_ == other.at_end_; } |
689 | | bool operator!=(const lazy_iterator& other) const { return !(*this == other); } |
690 | | |
691 | | private: |
692 | | void advance_to_next_element(const char*& pos); |
693 | | void skip_ws(const char*& p) noexcept |
694 | | { |
695 | | if constexpr (Opts.null_terminated) { |
696 | | while (whitespace_table[uint8_t(*p)]) ++p; |
697 | | } |
698 | | else { |
699 | | while (p < json_end_ && whitespace_table[uint8_t(*p)]) ++p; |
700 | | } |
701 | | } |
702 | | }; |
703 | | |
704 | | // ============================================================================ |
705 | | // indexed_lazy_view - Lazy view with pre-built element index for O(1) access |
706 | | // ============================================================================ |
707 | | |
708 | | /** |
709 | | * @brief A lazy view with a pre-built index for O(1) iteration and random access. |
710 | | * |
711 | | * Created by calling `index()` on a lazy_json_view. Scans the container once |
712 | | * to build an index of element positions, then provides: |
713 | | * - O(1) iteration advancement (vs O(element_size) for lazy_json_view) |
714 | | * - O(1) random access by index |
715 | | * - O(1) size query |
716 | | * |
717 | | * Elements returned are still lazy_json_view objects, so nested access remains lazy. |
718 | | * |
719 | | * Memory: Base view + 8 bytes per element (+ 16 bytes per element for objects with keys) |
720 | | */ |
721 | | template <auto Opts> |
722 | | struct indexed_lazy_view |
723 | | { |
724 | | private: |
725 | | const lazy_document<Opts>* doc_{}; |
726 | | const char* json_end_{}; |
727 | | std::vector<const char*> value_starts_; |
728 | | std::vector<std::string_view> keys_; // Only populated for objects |
729 | | bool is_object_{}; |
730 | | |
731 | | friend class indexed_lazy_iterator<Opts>; |
732 | | |
733 | | public: |
734 | | indexed_lazy_view() = default; |
735 | | |
736 | | /// @brief Number of elements - O(1) |
737 | | [[nodiscard]] size_t size() const noexcept { return value_starts_.size(); } |
738 | | |
739 | | /// @brief Check if empty - O(1) |
740 | | [[nodiscard]] bool empty() const noexcept { return value_starts_.empty(); } |
741 | | |
742 | | /// @brief Check if this is an indexed object |
743 | | [[nodiscard]] bool is_object() const noexcept { return is_object_; } |
744 | | |
745 | | /// @brief Check if this is an indexed array |
746 | | [[nodiscard]] bool is_array() const noexcept { return !is_object_; } |
747 | | |
748 | | /// @brief O(1) random access by index |
749 | | [[nodiscard]] lazy_json_view<Opts> operator[](size_t index) const |
750 | | { |
751 | | if (index >= value_starts_.size()) { |
752 | | return lazy_json_view<Opts>::make_error(error_code::exceeded_static_array_size); |
753 | | } |
754 | | std::string_view key = is_object_ ? keys_[index] : std::string_view{}; |
755 | | return lazy_json_view<Opts>{doc_, value_starts_[index], key}; |
756 | | } |
757 | | |
758 | | /// @brief O(n) key lookup for objects (linear search in index) |
759 | | [[nodiscard]] lazy_json_view<Opts> operator[](std::string_view key) const |
760 | | { |
761 | | if (!is_object_) { |
762 | | return lazy_json_view<Opts>::make_error(error_code::get_wrong_type); |
763 | | } |
764 | | for (size_t i = 0; i < keys_.size(); ++i) { |
765 | | if (keys_[i] == key) { |
766 | | return lazy_json_view<Opts>{doc_, value_starts_[i], keys_[i]}; |
767 | | } |
768 | | } |
769 | | return lazy_json_view<Opts>::make_error(error_code::key_not_found); |
770 | | } |
771 | | |
772 | | /// @brief Check if object contains key - O(n) linear search |
773 | | [[nodiscard]] bool contains(std::string_view key) const |
774 | | { |
775 | | if (!is_object_) return false; |
776 | | for (const auto& k : keys_) { |
777 | | if (k == key) return true; |
778 | | } |
779 | | return false; |
780 | | } |
781 | | |
782 | | [[nodiscard]] indexed_lazy_iterator<Opts> begin() const; |
783 | | [[nodiscard]] indexed_lazy_iterator<Opts> end() const; |
784 | | |
785 | | private: |
786 | | friend struct lazy_json_view<Opts>; |
787 | | |
788 | | // Private constructor used by lazy_json_view::index() |
789 | | indexed_lazy_view(const lazy_document<Opts>* doc, const char* json_end, bool is_object) |
790 | | : doc_(doc), json_end_(json_end), is_object_(is_object) |
791 | | {} |
792 | | |
793 | | void reserve(size_t n) |
794 | | { |
795 | | value_starts_.reserve(n); |
796 | | if (is_object_) { |
797 | | keys_.reserve(n); |
798 | | } |
799 | | } |
800 | | |
801 | | void add_element(const char* value_start, std::string_view key = {}) |
802 | | { |
803 | | value_starts_.push_back(value_start); |
804 | | if (is_object_) { |
805 | | keys_.push_back(key); |
806 | | } |
807 | | } |
808 | | }; |
809 | | |
810 | | // ============================================================================ |
811 | | // indexed_lazy_iterator - O(1) advancement using pre-built index |
812 | | // ============================================================================ |
813 | | |
814 | | template <auto Opts> |
815 | | class indexed_lazy_iterator |
816 | | { |
817 | | private: |
818 | | const indexed_lazy_view<Opts>* parent_{}; |
819 | | size_t index_{}; |
820 | | mutable lazy_json_view<Opts> current_view_{}; // Cached for reference return |
821 | | |
822 | | public: |
823 | | using iterator_category = std::random_access_iterator_tag; |
824 | | using value_type = lazy_json_view<Opts>; |
825 | | using difference_type = std::ptrdiff_t; |
826 | | using pointer = void; |
827 | | using reference = lazy_json_view<Opts>&; |
828 | | |
829 | | indexed_lazy_iterator() = default; |
830 | | indexed_lazy_iterator(const indexed_lazy_view<Opts>* parent, size_t index) : parent_(parent), index_(index) {} |
831 | | |
832 | | reference operator*() const |
833 | | { |
834 | | std::string_view key = parent_->is_object_ ? parent_->keys_[index_] : std::string_view{}; |
835 | | current_view_ = lazy_json_view<Opts>{parent_->doc_, parent_->value_starts_[index_], key}; |
836 | | return current_view_; |
837 | | } |
838 | | |
839 | | lazy_json_view<Opts>* operator->() const |
840 | | { |
841 | | operator*(); // Update current_view_ |
842 | | return ¤t_view_; |
843 | | } |
844 | | |
845 | | indexed_lazy_iterator& operator++() |
846 | | { |
847 | | ++index_; |
848 | | return *this; |
849 | | } |
850 | | |
851 | | indexed_lazy_iterator operator++(int) |
852 | | { |
853 | | auto tmp = *this; |
854 | | ++index_; |
855 | | return tmp; |
856 | | } |
857 | | |
858 | | indexed_lazy_iterator& operator--() |
859 | | { |
860 | | --index_; |
861 | | return *this; |
862 | | } |
863 | | |
864 | | indexed_lazy_iterator operator--(int) |
865 | | { |
866 | | auto tmp = *this; |
867 | | --index_; |
868 | | return tmp; |
869 | | } |
870 | | |
871 | | indexed_lazy_iterator& operator+=(difference_type n) |
872 | | { |
873 | | index_ = static_cast<size_t>(static_cast<difference_type>(index_) + n); |
874 | | return *this; |
875 | | } |
876 | | |
877 | | indexed_lazy_iterator& operator-=(difference_type n) |
878 | | { |
879 | | index_ = static_cast<size_t>(static_cast<difference_type>(index_) - n); |
880 | | return *this; |
881 | | } |
882 | | |
883 | | indexed_lazy_iterator operator+(difference_type n) const |
884 | | { |
885 | | return {parent_, static_cast<size_t>(static_cast<difference_type>(index_) + n)}; |
886 | | } |
887 | | |
888 | | indexed_lazy_iterator operator-(difference_type n) const |
889 | | { |
890 | | return {parent_, static_cast<size_t>(static_cast<difference_type>(index_) - n)}; |
891 | | } |
892 | | |
893 | | difference_type operator-(const indexed_lazy_iterator& other) const |
894 | | { |
895 | | return static_cast<difference_type>(index_) - static_cast<difference_type>(other.index_); |
896 | | } |
897 | | |
898 | | reference operator[](difference_type n) const |
899 | | { |
900 | | std::string_view key = |
901 | | parent_->is_object_ ? parent_->keys_[index_ + static_cast<size_t>(n)] : std::string_view{}; |
902 | | current_view_ = |
903 | | lazy_json_view<Opts>{parent_->doc_, parent_->value_starts_[index_ + static_cast<size_t>(n)], key}; |
904 | | return current_view_; |
905 | | } |
906 | | |
907 | | bool operator==(const indexed_lazy_iterator& other) const { return index_ == other.index_; } |
908 | | bool operator!=(const indexed_lazy_iterator& other) const { return index_ != other.index_; } |
909 | | bool operator<(const indexed_lazy_iterator& other) const { return index_ < other.index_; } |
910 | | bool operator<=(const indexed_lazy_iterator& other) const { return index_ <= other.index_; } |
911 | | bool operator>(const indexed_lazy_iterator& other) const { return index_ > other.index_; } |
912 | | bool operator>=(const indexed_lazy_iterator& other) const { return index_ >= other.index_; } |
913 | | |
914 | | friend indexed_lazy_iterator operator+(difference_type n, const indexed_lazy_iterator& it) { return it + n; } |
915 | | }; |
916 | | |
917 | | // ============================================================================ |
918 | | // Implementation: lazy_json_view methods (truly lazy - no structural index) |
919 | | // ============================================================================ |
920 | | |
921 | | template <auto Opts> |
922 | | inline const char* lazy_json_view<Opts>::json_end() const noexcept |
923 | | { |
924 | | return doc_ ? doc_->json_ + doc_->len_ : nullptr; |
925 | | } |
926 | | |
927 | | template <auto Opts> |
928 | | inline std::string_view lazy_json_view<Opts>::parse_key(const char*& p, const char* end) noexcept |
929 | | { |
930 | | // A non-null-terminated buffer has no '\0' sentinel, so a scan loop can reach the next |
931 | | // key position with p == end (e.g. after a trailing comma). Guard the opening dereference. |
932 | | if constexpr (!Opts.null_terminated) { |
933 | | if (p >= end) return {}; |
934 | | } |
935 | | if (*p != '"') return {}; |
936 | | const char* const start = p; |
937 | | ++p; // skip opening quote |
938 | | const char* key_start = p; |
939 | | |
940 | | while (p < end) { |
941 | | const char* quote = static_cast<const char*>(std::memchr(p, '"', static_cast<size_t>(end - p))); |
942 | | if (!quote) [[unlikely]] { |
943 | | p = end; |
944 | | return std::string_view{key_start, static_cast<size_t>(end - key_start)}; |
945 | | } |
946 | | |
947 | | // Count preceding backslashes to check if escaped |
948 | | size_t backslashes = 0; |
949 | | const char* check = quote - 1; |
950 | | while (check > start && *check == '\\') { |
951 | | ++backslashes; |
952 | | --check; |
953 | | } |
954 | | |
955 | | if ((backslashes & 1) == 0) { |
956 | | // Even backslashes = real quote |
957 | | std::string_view key{key_start, static_cast<size_t>(quote - key_start)}; |
958 | | p = quote + 1; // skip closing quote |
959 | | return key; |
960 | | } |
961 | | // Odd backslashes = escaped quote, continue searching |
962 | | p = quote + 1; |
963 | | } |
964 | | return std::string_view{key_start, static_cast<size_t>(p - key_start)}; |
965 | | } |
966 | | |
967 | | template <auto Opts> |
968 | | inline lazy_json_view<Opts> lazy_json_view<Opts>::operator[](size_t index) const |
969 | | { |
970 | | if (has_error()) return *this; |
971 | | if (!is_array()) return make_error(error_code::get_wrong_type); |
972 | | |
973 | | const char* end = json_end(); |
974 | | const char* p = data_ + 1; // skip '[' |
975 | | skip_ws(p, end); |
976 | | |
977 | | // Check for empty array. json_end_ bounds the message in both modes: null_terminated does |
978 | | // not guarantee the '\0' sits at json_end_, so fall back to the bound here too. |
979 | | if (p >= end || *p == ']') return make_error(error_code::exceeded_static_array_size); |
980 | | |
981 | | // Skip 'index' elements using lazy scanning |
982 | | for (size_t i = 0; i < index; ++i) { |
983 | | p = detail::skip_value_lazy<Opts>(p, end); |
984 | | skip_ws(p, end); |
985 | | |
986 | | if (p >= end || *p == ']') return make_error(error_code::exceeded_static_array_size); |
987 | | |
988 | | if (*p == ',') { |
989 | | ++p; |
990 | | skip_ws(p, end); |
991 | | } |
992 | | } |
993 | | |
994 | | // The requested index landed at or past end-of-input (a trailing comma with no following |
995 | | // element, or an unclosed array); a view positioned at end would read past the buffer, or, |
996 | | // when null_terminated, off the message into trailing bytes. |
997 | | if (p >= end) return make_error(error_code::exceeded_static_array_size); |
998 | | return {doc_, p}; |
999 | | } |
1000 | | |
1001 | | template <auto Opts> |
1002 | | inline lazy_json_view<Opts> lazy_json_view<Opts>::operator[](std::string_view key) const |
1003 | | { |
1004 | | if (has_error()) return *this; |
1005 | | if (!is_object()) return make_error(error_code::get_wrong_type); |
1006 | | |
1007 | | const char* end = json_end(); |
1008 | | const char* obj_start = data_ + 1; // skip '{' |
1009 | | |
1010 | | // Determine search start position |
1011 | | // parse_pos_ points to the VALUE of the last found key (lazy skip) |
1012 | | // We need to skip past it before continuing the search |
1013 | | const char* search_start = obj_start; |
1014 | | if (parse_pos_ && parse_pos_ > data_) { |
1015 | | // Lazily skip past the last found value now |
1016 | | const char* p = parse_pos_; |
1017 | | p = detail::skip_value_lazy<Opts>(p, end); |
1018 | | skip_ws(p, end); |
1019 | | if (p < end && *p == ',') { |
1020 | | ++p; |
1021 | | skip_ws(p, end); |
1022 | | } |
1023 | | search_start = p; |
1024 | | } |
1025 | | |
1026 | | const char* p = search_start; |
1027 | | skip_ws(p, end); |
1028 | | |
1029 | | // Forward pass: search from current position to end of object |
1030 | | while (true) { |
1031 | | // Check for end of object. json_end_ bounds the message in both modes (null_terminated |
1032 | | // does not guarantee the '\0' sits at json_end_); this is also the loop's backstop |
1033 | | // against an unclosed object spinning forever on a key parse that never advances. |
1034 | | if (p >= end || *p == '}') break; |
1035 | | |
1036 | | // Parse key |
1037 | | auto k = parse_key(p, end); |
1038 | | skip_ws(p, end); |
1039 | | |
1040 | | // Skip ':' |
1041 | | if (p < end && *p == ':') { |
1042 | | ++p; |
1043 | | skip_ws(p, end); |
1044 | | } |
1045 | | |
1046 | | // Check if key matches |
1047 | | if (k == key) { |
1048 | | // A matched key whose value begins at or past json_end_ is truncated; a view |
1049 | | // positioned at end would read past the buffer (or, when null_terminated, off the |
1050 | | // message into trailing bytes). Return the error before storing parse_pos_ so a |
1051 | | // truncated match leaves no state. |
1052 | | if (p >= end) return make_error(error_code::unexpected_end); |
1053 | | parse_pos_ = p; // Store value position (lazy - don't skip yet) |
1054 | | return {doc_, p}; |
1055 | | } |
1056 | | |
1057 | | // Skip value using lazy scanning |
1058 | | p = detail::skip_value_lazy<Opts>(p, end); |
1059 | | skip_ws(p, end); |
1060 | | |
1061 | | // Skip comma |
1062 | | if (p < end && *p == ',') { |
1063 | | ++p; |
1064 | | skip_ws(p, end); |
1065 | | } |
1066 | | } |
1067 | | |
1068 | | // Wrap-around pass: search from beginning to where we started |
1069 | | if (search_start != obj_start) { |
1070 | | p = obj_start; |
1071 | | skip_ws(p, end); |
1072 | | |
1073 | | while (p < search_start) { |
1074 | | // Check for end of object (see forward pass). |
1075 | | if (p >= end || *p == '}') break; |
1076 | | |
1077 | | // Parse key |
1078 | | auto k = parse_key(p, end); |
1079 | | skip_ws(p, end); |
1080 | | |
1081 | | // Skip ':' |
1082 | | if (p < end && *p == ':') { |
1083 | | ++p; |
1084 | | skip_ws(p, end); |
1085 | | } |
1086 | | |
1087 | | // Check if key matches |
1088 | | if (k == key) { |
1089 | | // A matched key whose value begins at or past json_end_ is truncated; a view |
1090 | | // positioned at end would read past the buffer (or, when null_terminated, off the |
1091 | | // message into trailing bytes). Return the error before storing parse_pos_ so a |
1092 | | // truncated match leaves no state. |
1093 | | if (p >= end) return make_error(error_code::unexpected_end); |
1094 | | parse_pos_ = p; // Store value position (lazy - don't skip yet) |
1095 | | return {doc_, p}; |
1096 | | } |
1097 | | |
1098 | | // Skip value using lazy scanning |
1099 | | p = detail::skip_value_lazy<Opts>(p, end); |
1100 | | skip_ws(p, end); |
1101 | | |
1102 | | // Skip comma |
1103 | | if (p < end && *p == ',') { |
1104 | | ++p; |
1105 | | skip_ws(p, end); |
1106 | | } |
1107 | | } |
1108 | | } |
1109 | | |
1110 | | return make_error(error_code::key_not_found); |
1111 | | } |
1112 | | |
1113 | | template <auto Opts> |
1114 | | inline bool lazy_json_view<Opts>::contains(std::string_view key) const |
1115 | | { |
1116 | | auto result = (*this)[key]; |
1117 | | return !result.has_error(); |
1118 | | } |
1119 | | |
1120 | | template <auto Opts> |
1121 | | inline size_t lazy_json_view<Opts>::size() const |
1122 | | { |
1123 | | if (has_error() || !data_) return 0; |
1124 | | if (!is_array() && !is_object()) return 0; |
1125 | | |
1126 | | const char* end = json_end(); |
1127 | | const char* p = data_ + 1; |
1128 | | skip_ws(p, end); |
1129 | | |
1130 | | const char close_char = is_array() ? ']' : '}'; |
1131 | | |
1132 | | // json_end_ bounds the message in both modes: null_terminated does not guarantee the '\0' |
1133 | | // sits at json_end_, so fall back to the bound here and throughout the loop below. |
1134 | | if (p >= end || *p == close_char) return 0; |
1135 | | |
1136 | | size_t count = 0; |
1137 | | const bool is_obj = is_object(); |
1138 | | |
1139 | | while (true) { |
1140 | | // Stop before scanning a key/value at or past end. This keeps skip_string_fast and |
1141 | | // skip_value_lazy from dereferencing off the buffer, and is the loop's backstop against |
1142 | | // an unclosed container running off the end. |
1143 | | if (p >= end) return count; |
1144 | | |
1145 | | if (is_obj) { |
1146 | | // Skip key |
1147 | | p = detail::skip_string_fast<Opts>(p, end); |
1148 | | skip_ws(p, end); |
1149 | | if (p < end && *p == ':') { |
1150 | | ++p; |
1151 | | skip_ws(p, end); |
1152 | | } |
1153 | | // The value begins at or past end (truncated pair); don't count it, so size() agrees |
1154 | | // with index() and iteration. |
1155 | | if (p >= end) return count; |
1156 | | } |
1157 | | |
1158 | | // Skip value |
1159 | | p = detail::skip_value_lazy<Opts>(p, end); |
1160 | | ++count; |
1161 | | skip_ws(p, end); |
1162 | | |
1163 | | if (p >= end || *p == close_char) return count; |
1164 | | |
1165 | | if (*p == ',') { |
1166 | | ++p; |
1167 | | skip_ws(p, end); |
1168 | | } |
1169 | | } |
1170 | | } |
1171 | | |
1172 | | template <auto Opts> |
1173 | | inline bool lazy_json_view<Opts>::empty() const noexcept |
1174 | | { |
1175 | | if (has_error() || !data_) return true; |
1176 | | if (is_null()) return true; |
1177 | | if (!is_array() && !is_object()) return false; |
1178 | | |
1179 | | const char* end = json_end(); |
1180 | | const char* p = data_ + 1; |
1181 | | skip_ws(p, end); |
1182 | | |
1183 | | const char close_char = is_array() ? ']' : '}'; |
1184 | | // json_end_ bounds the message in both modes (null_terminated does not guarantee the '\0' |
1185 | | // sits at json_end_); a container with nothing before end is empty. This keeps empty() |
1186 | | // consistent with size()/index()/iteration reporting 0 on a bare, unclosed container. |
1187 | | return p >= end || *p == close_char; |
1188 | | } |
1189 | | |
1190 | | // ============================================================================ |
1191 | | // lazy_iterator implementation (truly lazy) |
1192 | | // ============================================================================ |
1193 | | |
1194 | | template <auto Opts> |
1195 | | inline lazy_iterator<Opts>::lazy_iterator(const lazy_document<Opts>* doc, const char* container_start, |
1196 | | const char* end, bool is_object) |
1197 | | : doc_(doc), json_end_(end), container_start_(container_start), is_object_(is_object), at_end_(false) |
1198 | | { |
1199 | | close_char_ = is_object ? '}' : ']'; |
1200 | | const char* pos = container_start + 1; // skip '[' or '{' |
1201 | | skip_ws(pos); |
1202 | | |
1203 | | // Check for empty container |
1204 | | if constexpr (Opts.null_terminated) { |
1205 | | if (*pos == close_char_) { |
1206 | | at_end_ = true; |
1207 | | record_container_extent(pos); |
1208 | | return; |
1209 | | } |
1210 | | } |
1211 | | else { |
1212 | | if (pos >= json_end_ || *pos == close_char_) { |
1213 | | at_end_ = true; |
1214 | | record_container_extent(pos); |
1215 | | return; |
1216 | | } |
1217 | | } |
1218 | | |
1219 | | // Position at first element and initialize current_view_ |
1220 | | advance_to_next_element(pos); |
1221 | | } |
1222 | | |
1223 | | template <auto Opts> |
1224 | | inline void lazy_iterator<Opts>::advance_to_next_element(const char*& pos) |
1225 | | { |
1226 | | std::string_view key{}; |
1227 | | if (is_object_) { |
1228 | | // Parse key |
1229 | | key = lazy_json_view<Opts>::parse_key(pos, json_end_); |
1230 | | skip_ws(pos); |
1231 | | |
1232 | | // Skip ':' |
1233 | | if (pos < json_end_ && *pos == ':') { |
1234 | | ++pos; |
1235 | | skip_ws(pos); |
1236 | | } |
1237 | | } |
1238 | | // A value that begins at or past json_end_ is a truncated element; stop iterating instead |
1239 | | // of handing back a view whose data pointer sits at end (every view op reads *data_, an |
1240 | | // out-of-bounds read for a non-null-terminated buffer). This bound is unconditional on |
1241 | | // purpose: json_end_ is the authoritative end of the message, whereas null_terminated only |
1242 | | // promises a '\0' sentinel exists, not that it sits at json_end_ (it may be trailing buffer |
1243 | | // content further on), so '\0' cannot substitute here. It is also the iterator's only |
1244 | | // backstop against spinning forever on unclosed null-terminated input. |
1245 | | if (pos >= json_end_) { |
1246 | | at_end_ = true; |
1247 | | return; |
1248 | | } |
1249 | | // Store key in current_view_ (will be set properly after this call) |
1250 | | current_view_ = lazy_json_view<Opts>{doc_, pos, key}; |
1251 | | } |
1252 | | |
1253 | | template <auto Opts> |
1254 | | inline lazy_iterator<Opts>& lazy_iterator<Opts>::operator++() |
1255 | | { |
1256 | | if (at_end_) return *this; |
1257 | | |
1258 | | const char* const elem_start = current_view_.data(); |
1259 | | const char* pos = elem_start; |
1260 | | |
1261 | | // Streaming cursor: if this element was already consumed end-to-end (read_into, or a |
1262 | | // nested iterator that ran to its close) the recorded extent says where it finishes, so |
1263 | | // skip the scan entirely. |
1264 | | bool jumped = false; |
1265 | | if constexpr (check_lazy_streaming_cursor(Opts)) { |
1266 | | if (doc_) [[likely]] { |
1267 | | jumped = doc_->consumed_.try_jump(doc_->json_data(), pos, pos); |
1268 | | } |
1269 | | } |
1270 | | if (not jumped) { |
1271 | | pos = scan_past_element(elem_start); |
1272 | | } |
1273 | | |
1274 | | skip_ws(pos); |
1275 | | |
1276 | | // Check for end |
1277 | | if constexpr (Opts.null_terminated) { |
1278 | | if (*pos == close_char_) { |
1279 | | at_end_ = true; |
1280 | | record_container_extent(pos); |
1281 | | return *this; |
1282 | | } |
1283 | | } |
1284 | | else { |
1285 | | if (pos >= json_end_ || *pos == close_char_) { |
1286 | | at_end_ = true; |
1287 | | record_container_extent(pos); |
1288 | | return *this; |
1289 | | } |
1290 | | } |
1291 | | |
1292 | | // Skip comma |
1293 | | if (*pos == ',') { |
1294 | | ++pos; |
1295 | | skip_ws(pos); |
1296 | | } |
1297 | | |
1298 | | // Advance to next element (updates current_view_) |
1299 | | advance_to_next_element(pos); |
1300 | | |
1301 | | return *this; |
1302 | | } |
1303 | | |
1304 | | template <auto Opts> |
1305 | | inline lazy_iterator<Opts> lazy_json_view<Opts>::begin() const |
1306 | | { |
1307 | | if (has_error() || !data_) return end(); |
1308 | | if (!is_array() && !is_object()) return end(); |
1309 | | return lazy_iterator<Opts>{doc_, data_, json_end(), is_object()}; |
1310 | | } |
1311 | | |
1312 | | template <auto Opts> |
1313 | | inline lazy_iterator<Opts> lazy_json_view<Opts>::end() const |
1314 | | { |
1315 | | return lazy_iterator<Opts>{}; |
1316 | | } |
1317 | | |
1318 | | // ============================================================================ |
1319 | | // indexed_lazy_view implementation |
1320 | | // ============================================================================ |
1321 | | |
1322 | | template <auto Opts> |
1323 | | inline indexed_lazy_iterator<Opts> indexed_lazy_view<Opts>::begin() const |
1324 | | { |
1325 | | return indexed_lazy_iterator<Opts>{this, 0}; |
1326 | | } |
1327 | | |
1328 | | template <auto Opts> |
1329 | | inline indexed_lazy_iterator<Opts> indexed_lazy_view<Opts>::end() const |
1330 | | { |
1331 | | return indexed_lazy_iterator<Opts>{this, value_starts_.size()}; |
1332 | | } |
1333 | | |
1334 | | // ============================================================================ |
1335 | | // lazy_json_view::index() implementation |
1336 | | // ============================================================================ |
1337 | | |
1338 | | template <auto Opts> |
1339 | | inline indexed_lazy_view<Opts> lazy_json_view<Opts>::index() const |
1340 | | { |
1341 | | // Return empty indexed view for non-containers or errors |
1342 | | if (has_error() || !data_ || (!is_array() && !is_object())) { |
1343 | | return indexed_lazy_view<Opts>{}; |
1344 | | } |
1345 | | |
1346 | | const char* end = json_end(); |
1347 | | indexed_lazy_view<Opts> result{doc_, end, is_object()}; |
1348 | | |
1349 | | const char* p = data_ + 1; // skip '[' or '{' |
1350 | | skip_ws(p, end); |
1351 | | |
1352 | | const char close_char = is_array() ? ']' : '}'; |
1353 | | const bool is_obj = is_object(); |
1354 | | |
1355 | | // Check for empty container. json_end_ bounds the message in both modes: null_terminated |
1356 | | // does not guarantee the '\0' sits at json_end_, so fall back to the bound throughout. |
1357 | | if (p >= end || *p == close_char) return result; |
1358 | | |
1359 | | // Scan and record all element positions |
1360 | | while (true) { |
1361 | | // Stop before parsing a key/value at or past end: this keeps the scans from reading off |
1362 | | // the buffer and is the loop's backstop against an unclosed container looping forever |
1363 | | // (repeatedly recording an element positioned at end). |
1364 | | if (p >= end) break; |
1365 | | |
1366 | | std::string_view key{}; |
1367 | | if (is_obj) { |
1368 | | // Parse and record key |
1369 | | key = parse_key(p, end); |
1370 | | skip_ws(p, end); |
1371 | | |
1372 | | // Skip ':' |
1373 | | if (p < end && *p == ':') { |
1374 | | ++p; |
1375 | | skip_ws(p, end); |
1376 | | } |
1377 | | } |
1378 | | |
1379 | | // A truncated element whose value begins at or past json_end_ must not be recorded: |
1380 | | // an indexed view would later hand back a view positioned at end. |
1381 | | if (p >= end) break; |
1382 | | |
1383 | | // Record element start position |
1384 | | result.add_element(p, key); |
1385 | | |
1386 | | // Skip value |
1387 | | p = detail::skip_value_lazy<Opts>(p, end); |
1388 | | skip_ws(p, end); |
1389 | | |
1390 | | // Check for end of container |
1391 | | if (p >= end || *p == close_char) break; |
1392 | | |
1393 | | // Skip comma |
1394 | | if (*p == ',') { |
1395 | | ++p; |
1396 | | skip_ws(p, end); |
1397 | | } |
1398 | | } |
1399 | | |
1400 | | return result; |
1401 | | } |
1402 | | |
1403 | | // ============================================================================ |
1404 | | // lazy_json_view::get<T>() implementation |
1405 | | // ============================================================================ |
1406 | | |
1407 | | template <auto Opts> |
1408 | | template <class T> |
1409 | | [[nodiscard]] inline expected<T, error_ctx> lazy_json_view<Opts>::get() const |
1410 | | { |
1411 | | if (has_error()) { |
1412 | | return unexpected(error_ctx{0, error_}); |
1413 | | } |
1414 | | |
1415 | | const char* end = json_end(); |
1416 | | |
1417 | | if constexpr (std::is_same_v<T, bool>) { |
1418 | | if (!is_boolean()) { |
1419 | | return unexpected(error_ctx{0, error_code::get_wrong_type}); |
1420 | | } |
1421 | | return *data_ == 't'; |
1422 | | } |
1423 | | else if constexpr (std::is_same_v<T, std::nullptr_t>) { |
1424 | | if (!is_null()) { |
1425 | | return unexpected(error_ctx{0, error_code::get_wrong_type}); |
1426 | | } |
1427 | | return nullptr; |
1428 | | } |
1429 | | else if constexpr (std::is_same_v<T, std::string>) { |
1430 | | if (!is_string()) { |
1431 | | return unexpected(error_ctx{0, error_code::get_wrong_type}); |
1432 | | } |
1433 | | auto it = data_; |
1434 | | context ctx{}; |
1435 | | skip_value<JSON>::op<Opts>(ctx, it, end); |
1436 | | if (bool(ctx.error)) { |
1437 | | return unexpected(error_ctx{0, ctx.error}); |
1438 | | } |
1439 | | std::string_view raw{data_, static_cast<size_t>(it - data_)}; |
1440 | | // The read validates UTF-8 unless the document's options turned it off, so the returned |
1441 | | // string is well formed whenever validation is enabled. |
1442 | | static constexpr auto string_opts = detail::lazy_string_opts_for<Opts>(); |
1443 | | std::string unescaped{}; |
1444 | | context read_ctx{}; |
1445 | | if (const error_ctx ec = glz::read<string_opts>(unescaped, raw, read_ctx)) { |
1446 | | return unexpected(ec); |
1447 | | } |
1448 | | return unescaped; |
1449 | | } |
1450 | | else if constexpr (std::is_same_v<T, std::string_view>) { |
1451 | | if (!is_string()) { |
1452 | | return unexpected(error_ctx{0, error_code::get_wrong_type}); |
1453 | | } |
1454 | | auto it = data_ + 1; |
1455 | | context ctx{}; |
1456 | | skip_string_view(ctx, it, end); |
1457 | | if (bool(ctx.error)) { |
1458 | | return unexpected(error_ctx{0, ctx.error}); |
1459 | | } |
1460 | | return std::string_view{data_ + 1, static_cast<size_t>(it - data_ - 1)}; |
1461 | | } |
1462 | | else if constexpr (std::is_same_v<T, double>) { |
1463 | | if (!is_number()) { |
1464 | | return unexpected(error_ctx{0, error_code::get_wrong_type}); |
1465 | | } |
1466 | | double value{}; |
1467 | | auto [ptr, ec] = glz::from_chars<false>(data_, end, value); |
1468 | | if (ec != std::errc()) { |
1469 | | return unexpected(error_ctx{0, error_code::parse_number_failure}); |
1470 | | } |
1471 | | return value; |
1472 | | } |
1473 | | else if constexpr (std::is_same_v<T, float>) { |
1474 | | if (!is_number()) { |
1475 | | return unexpected(error_ctx{0, error_code::get_wrong_type}); |
1476 | | } |
1477 | | float value{}; |
1478 | | auto [ptr, ec] = glz::from_chars<false>(data_, end, value); |
1479 | | if (ec != std::errc()) { |
1480 | | return unexpected(error_ctx{0, error_code::parse_number_failure}); |
1481 | | } |
1482 | | return value; |
1483 | | } |
1484 | | else if constexpr (std::is_same_v<T, int64_t>) { |
1485 | | if (!is_number()) { |
1486 | | return unexpected(error_ctx{0, error_code::get_wrong_type}); |
1487 | | } |
1488 | | int64_t value{}; |
1489 | | auto it = data_; |
1490 | | if (!glz::atoi(value, it, end)) { |
1491 | | return unexpected(error_ctx{0, error_code::parse_number_failure}); |
1492 | | } |
1493 | | return value; |
1494 | | } |
1495 | | else if constexpr (std::is_same_v<T, uint64_t>) { |
1496 | | if (!is_number()) { |
1497 | | return unexpected(error_ctx{0, error_code::get_wrong_type}); |
1498 | | } |
1499 | | uint64_t value{}; |
1500 | | auto it = data_; |
1501 | | if (!glz::atoi(value, it, end)) { |
1502 | | return unexpected(error_ctx{0, error_code::parse_number_failure}); |
1503 | | } |
1504 | | return value; |
1505 | | } |
1506 | | else if constexpr (std::is_same_v<T, int32_t>) { |
1507 | | auto result = get<int64_t>(); |
1508 | | if (!result) return unexpected(result.error()); |
1509 | | return static_cast<int32_t>(*result); |
1510 | | } |
1511 | | else if constexpr (std::is_same_v<T, uint32_t>) { |
1512 | | auto result = get<uint64_t>(); |
1513 | | if (!result) return unexpected(result.error()); |
1514 | | return static_cast<uint32_t>(*result); |
1515 | | } |
1516 | | else { |
1517 | | static_assert(false_v<T>, "Unsupported type for lazy_json_view::get<T>()"); |
1518 | | } |
1519 | | } |
1520 | | |
1521 | | // ============================================================================ |
1522 | | // JSON writer for lazy_json_view |
1523 | | // ============================================================================ |
1524 | | |
1525 | | template <auto Opts> |
1526 | | struct to<JSON, lazy_json_view<Opts>> |
1527 | | { |
1528 | | template <auto WriteOpts, class B> |
1529 | | GLZ_ALWAYS_INLINE static void op(const lazy_json_view<Opts>& view, is_context auto&& ctx, B&& b, auto& ix) |
1530 | | { |
1531 | | if (view.has_error()) { |
1532 | | ctx.error = view.error(); |
1533 | | return; |
1534 | | } |
1535 | | if (!view.data()) { |
1536 | | dump<false>("null", b, ix); |
1537 | | return; |
1538 | | } |
1539 | | |
1540 | | const char* const view_end = view.json_end(); |
1541 | | auto it = view.data(); |
1542 | | context parse_ctx{}; |
1543 | | // Skip under the document's own options, not a default-constructed set. `opts{}` has |
1544 | | // null_terminated = true, so a view over a buffer without a '\0' sentinel would scan |
1545 | | // past json_end() looking for one and copy out-of-window bytes into the output. |
1546 | | skip_value<JSON>::op<Opts>(parse_ctx, it, view_end); |
1547 | | if (bool(parse_ctx.error)) [[unlikely]] { |
1548 | | if (!detail::lazy_scalar_reaches_end(*view.data(), parse_ctx.error, it, view_end)) { |
1549 | | ctx.error = parse_ctx.error; |
1550 | | return; |
1551 | | } |
1552 | | // Complete after all: trim the run of whitespace skip_value walked into so the |
1553 | | // written bytes match raw_json() exactly. |
1554 | | while (it > view.data() && whitespace_table[uint8_t(it[-1])]) { |
1555 | | --it; |
1556 | | } |
1557 | | } |
1558 | | |
1559 | | const size_t n = static_cast<size_t>(it - view.data()); |
1560 | | if (n == 0) [[unlikely]] { |
1561 | | // No JSON value is zero bytes. skip_value consumes nothing when the view is not on a |
1562 | | // value at all, which navigation can produce on malformed input: looking up "a" in |
1563 | | // {"a":} leaves the view on the closing brace. Writing nothing while reporting |
1564 | | // success would splice invalid JSON into the caller's buffer, so refuse. |
1565 | | ctx.error = error_code::syntax_error; |
1566 | | return; |
1567 | | } |
1568 | | |
1569 | | if constexpr (resizable<B>) { |
1570 | | if (ix + n > b.size()) [[unlikely]] { |
1571 | | b.resize((std::max)(b.size() * 2, ix + n)); |
1572 | | } |
1573 | | } |
1574 | | else { |
1575 | | if (ix + n > b.size()) [[unlikely]] { |
1576 | | ctx.error = error_code::buffer_overflow; |
1577 | | return; |
1578 | | } |
1579 | | } |
1580 | | std::memcpy(&b[ix], view.data(), n); |
1581 | | ix += n; |
1582 | | } |
1583 | | }; |
1584 | | |
1585 | | // ============================================================================ |
1586 | | // lazy_json - Main entry point (truly lazy - minimal upfront work) |
1587 | | // ============================================================================ |
1588 | | |
1589 | | /** |
1590 | | * @brief Create a lazy JSON document - no upfront processing. |
1591 | | * |
1592 | | * - Just validates first byte and stores buffer reference - O(1) |
1593 | | * - All work happens on-demand when accessing fields |
1594 | | * |
1595 | | * @tparam Opts Options for parsing |
1596 | | * @param buffer The JSON text buffer (must remain valid for document lifetime) |
1597 | | * @return lazy_document on success, error_ctx on failure |
1598 | | */ |
1599 | | template <auto Opts = opts{}, class Buffer> |
1600 | | [[nodiscard]] inline expected<lazy_document<Opts>, error_ctx> lazy_json(Buffer&& buffer) |
1601 | | { |
1602 | | lazy_document<Opts> doc; |
1603 | | doc.json_ = buffer.data(); |
1604 | | doc.len_ = buffer.size(); |
1605 | | |
1606 | | // Find root value (skip leading whitespace) |
1607 | | const char* p = buffer.data(); |
1608 | | const char* end = buffer.data() + buffer.size(); |
1609 | | |
1610 | | if constexpr (Opts.null_terminated) { |
1611 | | while (whitespace_table[uint8_t(*p)]) ++p; |
1612 | | } |
1613 | | else { |
1614 | | while (p < end && whitespace_table[uint8_t(*p)]) ++p; |
1615 | | } |
1616 | | |
1617 | | if (p >= end) { |
1618 | | return unexpected(error_ctx{0, error_code::unexpected_end}); |
1619 | | } |
1620 | | |
1621 | | // Validate first character is valid JSON start |
1622 | | const char c = *p; |
1623 | | if (c != '{' && c != '[' && c != '"' && c != 't' && c != 'f' && c != 'n' && !is_digit(uint8_t(c)) && c != '-') { |
1624 | | return unexpected(error_ctx{0, error_code::syntax_error}); |
1625 | | } |
1626 | | |
1627 | | doc.root_data_ = p; |
1628 | | doc.init_root_view(); // Initialize cached root view |
1629 | | return doc; |
1630 | | } |
1631 | | |
1632 | | // ============================================================================ |
1633 | | // read_json overload for lazy_json_view (single-pass deserialization) |
1634 | | // ============================================================================ |
1635 | | |
1636 | | /// @brief Read JSON from a lazy_json_view into a C++ type |
1637 | | /// @tparam T The type to parse into |
1638 | | /// @tparam Opts The lazy view options |
1639 | | /// @param value The object to populate |
1640 | | /// @param view The lazy view to read from |
1641 | | /// @return Error context if parsing failed |
1642 | | /// @note This provides the familiar glz::read_json API while using the efficient single-pass read_into internally |
1643 | | template <class T, auto Opts> |
1644 | | [[nodiscard]] inline error_ctx read_json(T& value, const lazy_json_view<Opts>& view) |
1645 | | { |
1646 | | return view.template read_into<T>(value); |
1647 | | } |
1648 | | |
1649 | | /// @brief Read JSON from a lazy_json_view rvalue into a C++ type |
1650 | | /// @note Handles temporaries like glz::read_json(value, doc["field"]) |
1651 | | template <class T, auto Opts> |
1652 | | [[nodiscard]] inline error_ctx read_json(T& value, lazy_json_view<Opts>&& view) |
1653 | | { |
1654 | | return view.template read_into<T>(value); |
1655 | | } |
1656 | | |
1657 | | } // namespace glz |