JSON Voorhees
Killer JSON for C++
Loading...
Searching...
No Matches
container_adapter.hpp
Go to the documentation of this file.
1/// \file jsonv/serialization/container_adapter.hpp
2///
3/// Copyright (c) 2015-2026 by Travis Gockel. All rights reserved.
4///
5/// This program is free software: you can redistribute it and/or modify it under the terms of the Apache License
6/// as published by the Apache Software Foundation, either version 2 of the License, or (at your option) any later
7/// version.
8///
9/// \author Travis Gockel (travis@gockelhut.com)
10#pragma once
11
12#include <jsonv/ast.hpp>
13#include <jsonv/config.hpp>
15#include <jsonv/reader.hpp>
17
18#include <cstddef>
19#include <expected>
20#include <iterator>
21#include <utility>
22
23#include "adapter_for.hpp"
24
25namespace jsonv
26{
27
28/// \addtogroup Serialization
29/// \{
30
31/// An adapter for container types. This is for convenience of creating an \c adapter for things like \c std::vector,
32/// \c std::set and such.
33///
34/// \tparam TContainer is the container to create and encode. It must have a member type \c value_type, support
35/// iteration and an \c insert operation.
36template <typename TContainer>
38 public adapter_for<TContainer>
39{
40 using element_type = typename TContainer::value_type;
41
42protected:
44 virtual std::expected<TContainer, ast_node_type> create(extraction_context& context, reader& from) const override
45 {
46 using std::end;
47
48 auto opened = context.current_as<ast_node::array_begin>(from);
49 if (!opened)
50 return std::unexpected(opened.error());
51
52 TContainer out;
53
54 // The opening token carries how many elements the array holds, so the container is sized once rather than
55 // grown. A failed parse leaves that count determinate and bounded by what is actually on the tape, which is
56 // what keeps a malformed document from driving an arbitrary reservation.
57 detail::reserve_if_possible(out, opened->element_count());
58
59 bool recovered = false;
60 bool closed = false;
61
62 // Step off the `[` and onto the first element, or onto the `]` of an empty array. The loop never advances
63 // itself: extracting an element leaves the cursor one past it, which is what every extractor owes its caller.
64 (void) from.next_token();
65
66 try
67 {
68 for (std::size_t idx = 0U; from.good(); ++idx)
69 {
70 // A parse which failed part-way through an array still hands back a usable tape; it just ends with
71 // an `error` describing what cut it short, where the rest of the elements should have been. Saying
72 // the array never closed is more use than letting the extraction below report it as a mismatch
73 // against a node type no element can have, and there is nothing after it to recover into.
74 if (auto type = from.current_type();
75 type == ast_node_type::document_end || type == ast_node_type::error)
76 {
77 return context.problem(context.problem_path(from), "Unterminated array");
78 }
79
80 if (from.current_type() == ast_node_type::array_end)
81 {
82 (void) from.next_token();
83 closed = true;
84
85 // Moving `out` into the result is the last thing which can fail, and `TContainer` may be a
86 // user's; `closed` is what tells the handler below that there is nothing left to walk.
87 if (!recovered)
88 return out;
89
90 // Recovering collected the rest of the problems; it did not make the container valid. The whole
91 // array has been read either way, so the cursor lands in the same place whether this succeeds
92 // or fails -- which only works because the failure says the value is behind it.
93 context.note_value_consumed(from);
94 return std::unexpected(ast_node_type::error);
95 }
96
97 // Naming the element is what puts `[3]` into a problem raised inside it. The scope lives on this
98 // frame and is two stores to push; no `jsonv::path` is built unless a problem is actually recorded,
99 // which is why a wholly successful extraction of a large array allocates nothing to track where it
100 // is.
101 //
102 // It covers the extraction and not the insertion, which is `TContainer`'s code and may be a user's.
103 auto element = [&] () -> std::expected<element_type, ast_node_type>
104 {
106
107 return context.extract<element_type>(from);
108 }();
109
110 if (!element)
111 {
112 // The next element starts at a known place, so a bad one does not have to hide every problem
113 // after it. Only in `collect_all`, and only while the budget lasts -- otherwise the failure is
114 // reported as it stands, with the cursor still naming the element which caused it.
115 if (!context.recover())
116 return std::unexpected(element.error());
117
118 recovered = true;
119 context.skip_failed_value(from);
120 continue;
121 }
122
123 out.insert(end(out), *std::move(element));
124 }
125 }
126 catch (...)
127 {
128 // Something left the walk -- inserting into `TContainer`, which is often a user's code, or moving an
129 // element into place. Unless the array was already closed, the cursor is somewhere inside it: a
130 // position only this adapter can make sense of, so finish the walk before letting the failure out.
131 //
132 // Stepping over whole child values is what finds this array's own end. `reader::next_structure` cannot:
133 // on a child which is itself a structure it leaves *that* child, landing back inside this array, and on
134 // a scalar child it leaves this array without consuming the `]` consistently. `reader::next_value`
135 // crosses a child of either shape, so the only closing token this loop can stop on is its own.
136 if (!closed)
137 {
138 while (from.good())
139 {
140 auto type = from.current_type();
141 if (type == ast_node_type::array_end)
142 {
143 (void) from.next_token();
144 break;
145 }
146 else if (type == ast_node_type::document_end || type == ast_node_type::error)
147 {
148 // A truncated tape has no `]` to find. Stopping here leaves the cursor on the end of the
149 // document, which the loop above reports as an unterminated array anyway.
150 break;
151 }
152
153 (void) from.next_value();
154 }
155 }
156
157 context.note_value_consumed(from);
158 throw;
159 }
160
161 // Only reachable by recovering off the end of a truncated tape; the check at the top of the loop is what an
162 // unterminated array normally arrives as.
163 return context.problem(context.problem_path(from), "Unterminated array");
164 }
165
167 virtual value to_json(const serialization_context& context, const TContainer& from) const override
168 {
169 value out = array();
170 for (const element_type& x : from)
171 out.push_back(context.to_json(x));
172 return out;
173 }
174};
175
176/// \}
177
178}
Copyright (c) 2015-2026 by Travis Gockel.
Utilities for directly dealing with a JSON AST.
An adapter for the type T.
The beginning of an kind::array ([).
Definition ast.hpp:280
An adapter for container types.
virtual std::expected< TContainer, ast_node_type > create(extraction_context &context, reader &from) const override
Create an instance of T by reading from.
Provides extra information to routines used for extraction and serialization.
Definition context.hpp:26
An RAII guard naming one step of the extraction path while it is alive.
Definition extract.hpp:747
Provides extra information to routines used for extraction, collects the problems they encounter,...
Definition extract.hpp:391
A reader instance reads from some form of JSON source (probably a string) and converts it into a JSON...
Definition reader.hpp:104
bool next_value() noexcept
Go to one past the value this reader is on.
ast_node_type current_type() const
Get the type of the current AST node, which is always current().type().
bool next_token() noexcept
Go to the next token.
bool good() const
Check if this reader is still good to read from.
Represents a single JSON value, which can be any one of a potential kind, each behaving slightly diff...
Definition value.hpp:113
Copyright (c) 2014-2020 by Travis Gockel.
#define JSONV_NODISCARD
Warn if the caller discards the result of this function.
Definition config.hpp:132
JSONV_PUBLIC value array()
Create an empty array value.
Read a JSON AST.
Definition of the reserve_if_possible utility.
Conversion between C++ types and JSON values.