dasmig-core 1.2.0
Shared random engine, distributions and data tiers for the dasmig generator libraries
Loading...
Searching...
No Matches
json.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2020-2026 Diego Dasso Migotto
3
4#ifndef DASMIG_CORE_JSON_HPP
5#define DASMIG_CORE_JSON_HPP
6
7#include "utf8.hpp"
8
9#include <array>
10#include <charconv>
11#include <cmath>
12#include <concepts>
13#include <cstddef>
14#include <cstdint>
15#include <memory>
16#include <stdexcept>
17#include <string>
18#include <string_view>
19#include <system_error>
20#include <vector>
21
22/// @file json.hpp
23/// @brief A minimal, dependency-free JSON writer for `to_json()`.
24/// @author Diego Dasso Migotto (diegomigotto at hotmail dot com)
25///
26/// Writes compact JSON (RFC 8259). Commas are placed automatically, misuse
27/// (a value without a key inside an object, unbalanced ends) throws, and
28/// numbers are written with `std::to_chars`, the shortest form that reads
29/// back to the same double, so output is identical on every platform.
30///
31/// JavaScript numbers are doubles: integers above 2^53 lose precision there.
32/// Write 64-bit seeds as strings (seed_signature does).
33
34namespace dasmig::core
35{
36
37/// @brief Builds one JSON document.
38///
39/// @code
40/// dasmig::core::json_writer w;
41/// w.begin_object()
42/// .field("name", "Ana")
43/// .field("age", 34)
44/// .key("tags").begin_array().value("calm").value("curious").end_array()
45/// .end_object();
46/// std::string text = w.str(); // {"name":"Ana","age":34,"tags":["calm","curious"]}
47/// @endcode
49{
50 public:
51 /// @brief An empty writer, with room for a typical document.
53 {
54 _out.reserve(initial_capacity_);
55 _stack.reserve(initial_depth_);
56 }
57
58 /// @brief Open an object.
59 /// @return `*this`.
60 /// @throws std::logic_error Where a value is not allowed.
62 {
63 before_value();
64 _out += '{';
65 _stack.push_back({.type = kind::object});
66 return *this;
67 }
68
69 /// @brief Close the innermost object.
70 /// @return `*this`.
71 /// @throws std::logic_error If the innermost container is not an object,
72 /// or a key has no value.
74 {
75 if (_stack.empty() || _stack.back().type != kind::object || _stack.back().key_pending)
76 {
77 throw std::logic_error("json_writer: end_object without a matching open object");
78 }
79 _stack.pop_back();
80 _out += '}';
81 after_value();
82 return *this;
83 }
84
85 /// @brief Open an array.
86 /// @return `*this`.
87 /// @throws std::logic_error Where a value is not allowed.
89 {
90 before_value();
91 _out += '[';
92 _stack.push_back({.type = kind::array});
93 return *this;
94 }
95
96 /// @brief Close the innermost array.
97 /// @return `*this`.
98 /// @throws std::logic_error If the innermost container is not an array.
100 {
101 if (_stack.empty() || _stack.back().type != kind::array)
102 {
103 throw std::logic_error("json_writer: end_array without a matching open array");
104 }
105 _stack.pop_back();
106 _out += ']';
107 after_value();
108 return *this;
109 }
110
111 /// @brief Write an object key; the next call writes its value.
112 /// @param name UTF-8 key.
113 /// @return `*this`.
114 /// @throws std::logic_error Outside an object or after another key.
115 /// @throws std::invalid_argument If @p name is not valid UTF-8.
116 json_writer& key(std::string_view name)
117 {
118 if (_stack.empty() || _stack.back().type != kind::object || _stack.back().key_pending)
119 {
120 throw std::logic_error("json_writer: key outside an object or after another key");
121 }
122 if (_stack.back().has_items) _out += ',';
123 write_string(name);
124 _out += ':';
125 _stack.back().key_pending = true;
126 return *this;
127 }
128
129 /// @brief Write a string value.
130 /// @param text UTF-8 text.
131 /// @return `*this`.
132 /// @throws std::invalid_argument If @p text is not valid UTF-8.
133 json_writer& value(std::string_view text)
134 {
135 before_value();
136 write_string(text);
137 after_value();
138 return *this;
139 }
140
141 /// @brief Write a string value (keeps string literals from binding to bool).
142 /// @param text Null-terminated UTF-8 text.
143 /// @return `*this`.
144 json_writer& value(const char* text) { return value(std::string_view{text}); }
145
146 /// @brief Write a boolean.
147 /// @param b The value.
148 /// @return `*this`.
150 {
151 before_value();
152 _out += b ? "true" : "false";
153 after_value();
154 return *this;
155 }
156
157 /// @brief Write null.
158 /// @return `*this`.
159 json_writer& value(std::nullptr_t)
160 {
161 before_value();
162 _out += "null";
163 after_value();
164 return *this;
165 }
166
167 /// @brief Write an integer.
168 /// @tparam T Integral type other than bool and the character types.
169 /// @param n The value.
170 /// @return `*this`.
171 template <std::integral T>
172 requires(!std::same_as<T, bool> && !std::same_as<T, char> &&
173 !std::same_as<T, wchar_t> && !std::same_as<T, char8_t> &&
174 !std::same_as<T, char16_t> && !std::same_as<T, char32_t>)
176 {
177 before_value();
178 std::array<char, 24> buf{};
179 const auto res = std::to_chars(buf.data(), std::to_address(buf.end()), n);
180 _out.append(buf.data(), res.ptr);
181 after_value();
182 return *this;
183 }
184
185 /// @brief Write a number, in the shortest form that reads back exactly.
186 /// @param d The value.
187 /// @return `*this`.
188 /// @throws std::invalid_argument If @p d is NaN or infinite (JSON has no
189 /// representation for them).
191 {
192 if (!std::isfinite(d))
193 {
194 throw std::invalid_argument("json_writer: NaN and infinity are not JSON");
195 }
196 before_value();
197 std::array<char, 32> buf{};
198 const auto res = std::to_chars(buf.data(), std::to_address(buf.end()), d);
199 _out.append(buf.data(), res.ptr);
200 after_value();
201 return *this;
202 }
203
204 /// @brief Write a key and its value.
205 /// @param name UTF-8 key.
206 /// @param v The value (any type value() accepts).
207 /// @return `*this`.
208 template <typename V>
209 json_writer& field(std::string_view name, const V& v)
210 {
211 key(name);
212 return value(v);
213 }
214
215 /// @brief Whether exactly one complete value has been written.
216 /// @return True when str() can be called.
217 [[nodiscard]] bool complete() const noexcept { return _done && _stack.empty(); }
218
219 /// @brief The document.
220 /// @return The JSON text.
221 /// @throws std::logic_error If the document is incomplete.
222 [[nodiscard]] const std::string& str() const
223 {
224 if (!complete())
225 {
226 throw std::logic_error("json_writer: document is incomplete");
227 }
228 return _out;
229 }
230
231 /// @brief Append a JSON string literal for @p text to @p out.
232 /// @param out Destination.
233 /// @param text UTF-8 text.
234 /// @throws std::invalid_argument If @p text is not valid UTF-8.
235 static void escape(std::string& out, std::string_view text)
236 {
237 // Most text is printable ASCII without quotes or backslashes: nothing
238 // to check or escape. An ASCII prefix is valid UTF-8 on its own, so
239 // only the rest needs the check.
240 std::size_t plain = 0;
241 while (plain < text.size() && needs_no_escape(text[plain]) && static_cast<std::uint8_t>(text[plain]) < 0x80U)
242 {
243 ++plain;
244 }
245 if (plain < text.size() && !is_utf8(text.substr(plain)))
246 {
247 throw std::invalid_argument("json_writer: string is not valid UTF-8");
248 }
249 constexpr std::string_view hex = "0123456789abcdef";
250 out += '"';
251 // Bytes that need no escape are appended a run at a time.
252 std::size_t run = 0;
253 for (std::size_t i = plain; i < text.size(); ++i)
254 {
255 const char c = text[i];
256 if (needs_no_escape(c)) continue;
257 const auto b = static_cast<std::uint8_t>(c);
258 out.append(text.substr(run, i - run));
259 run = i + 1;
260 switch (c)
261 {
262 case '"': out += "\\\""; break;
263 case '\\': out += "\\\\"; break;
264 case '\b': out += "\\b"; break;
265 case '\f': out += "\\f"; break;
266 case '\n': out += "\\n"; break;
267 case '\r': out += "\\r"; break;
268 case '\t': out += "\\t"; break;
269 default:
270 out += "\\u00";
271 out += hex[b >> 4U];
272 out += hex[b & 0x0FU];
273 }
274 }
275 out.append(text.substr(run));
276 out += '"';
277 }
278
279 private:
280 enum class kind : std::uint8_t
281 {
282 object,
283 array
284 };
285
286 // Reserved up front: growing a string from empty reallocates it about ten
287 // times on the way to a few kilobytes.
288 static constexpr std::size_t initial_capacity_ = 1024;
289 static constexpr std::size_t initial_depth_ = 8;
290
291 // A byte written as it is: not a control character, quote or backslash.
292 [[nodiscard]] static constexpr bool needs_no_escape(char c) noexcept
293 {
294 return static_cast<std::uint8_t>(c) >= 0x20U && c != '"' && c != '\\';
295 }
296
297 struct frame
298 {
299 kind type;
300 bool has_items{false};
301 bool key_pending{false};
302 };
303
304 void before_value()
305 {
306 if (_stack.empty())
307 {
308 if (_done) throw std::logic_error("json_writer: a document holds one value");
309 return;
310 }
311 auto& top = _stack.back();
312 if (top.type == kind::object)
313 {
314 if (!top.key_pending)
315 {
316 throw std::logic_error("json_writer: object value without a key");
317 }
318 }
319 else if (top.has_items)
320 {
321 _out += ',';
322 }
323 }
324
325 void after_value()
326 {
327 if (_stack.empty())
328 {
329 _done = true;
330 return;
331 }
332 auto& top = _stack.back();
333 top.has_items = true;
334 top.key_pending = false;
335 }
336
337 void write_string(std::string_view text) { escape(_out, text); }
338
339 std::string _out;
340 std::vector<frame> _stack;
341 bool _done{false};
342};
343
344} // namespace dasmig::core
345
346#endif // DASMIG_CORE_JSON_HPP
Builds one JSON document.
Definition json.hpp:49
json_writer & begin_object()
Open an object.
Definition json.hpp:61
bool complete() const noexcept
Whether exactly one complete value has been written.
Definition json.hpp:217
json_writer()
An empty writer, with room for a typical document.
Definition json.hpp:52
json_writer & key(std::string_view name)
Write an object key; the next call writes its value.
Definition json.hpp:116
json_writer & field(std::string_view name, const V &v)
Write a key and its value.
Definition json.hpp:209
json_writer & value(T n)
Write an integer.
Definition json.hpp:175
json_writer & begin_array()
Open an array.
Definition json.hpp:88
json_writer & value(std::nullptr_t)
Write null.
Definition json.hpp:159
static void escape(std::string &out, std::string_view text)
Append a JSON string literal for text to out.
Definition json.hpp:235
json_writer & value(const char *text)
Write a string value (keeps string literals from binding to bool).
Definition json.hpp:144
json_writer & value(std::string_view text)
Write a string value.
Definition json.hpp:133
json_writer & value(double d)
Write a number, in the shortest form that reads back exactly.
Definition json.hpp:190
json_writer & end_object()
Close the innermost object.
Definition json.hpp:73
const std::string & str() const
The document.
Definition json.hpp:222
json_writer & value(bool b)
Write a boolean.
Definition json.hpp:149
json_writer & end_array()
Close the innermost array.
Definition json.hpp:99
Conversion between UTF-8 and std::wstring.
constexpr bool is_utf8(std::string_view text) noexcept
Whether a byte string is well-formed UTF-8.
Definition utf8.hpp:67