uzuki2
Recovering R lists faithfully from HDF5 or JSON
Loading...
Searching...
No Matches
parse_json.hpp
Go to the documentation of this file.
1#ifndef UZUKI2_PARSE_JSON_HPP
2#define UZUKI2_PARSE_JSON_HPP
3
4#include <memory>
5#include <vector>
6#include <cctype>
7#include <string>
8#include <stdexcept>
9#include <cmath>
10#include <unordered_map>
11#include <unordered_set>
12#include <type_traits>
13#include <cstddef>
14
15#include "byteme/byteme.hpp"
16#include "millijson/millijson.hpp"
17#include "ritsuko/ritsuko.hpp"
18#include "sanisizer/sanisizer.hpp"
19
20#include "interfaces.hpp"
21#include "Dummy.hpp"
22#include "ExternalTracker.hpp"
23#include "ParsedList.hpp"
24#include "utils.hpp"
25
31namespace uzuki2 {
32
41namespace json {
42
46inline const std::vector<std::shared_ptr<millijson::Base> >& extract_array(
47 const std::unordered_map<std::string, std::shared_ptr<millijson::Base> >& properties,
48 const std::string& name,
49 const std::string& path)
50{
51 auto vIt = properties.find(name);
52 if (vIt == properties.end()) {
53 throw std::runtime_error("expected '" + name + "' property for object at '" + path + "'");
54 }
55
56 const auto& values_ptr = vIt->second;
57 if (values_ptr->type() != millijson::ARRAY) {
58 throw std::runtime_error("expected an array in '" + path + "/" + name + "'");
59 }
60
61 return static_cast<const millijson::Array*>(values_ptr.get())->value();
62}
63
64inline const millijson::Array* has_names(const std::unordered_map<std::string, std::shared_ptr<millijson::Base> >& properties, const std::string& path) {
65 auto nIt = properties.find("names");
66 if (nIt == properties.end()) {
67 return NULL;
68 }
69
70 const auto name_ptr = nIt->second;
71 if (name_ptr->type() != millijson::ARRAY) {
72 throw std::runtime_error("expected an array in '" + path + "/names'");
73 }
74 return static_cast<const millijson::Array*>(name_ptr.get());
75}
76
77template<class Destination_>
78void fill_names(const millijson::Array* names_ptr, Destination_* dest, const std::string& path) {
79 const auto& names = names_ptr->value();
80 if (names.size() != dest->size()) {
81 throw std::runtime_error("length of 'names' and 'values' should be the same in '" + path + "'");
82 }
83
84 const auto nnames = names.size();
85 for (I<decltype(nnames)> i = 0; i < nnames; ++i) {
86 if (names[i]->type() != millijson::STRING) {
87 throw std::runtime_error("expected a string at '" + path + "/names/" + std::to_string(i) + "'");
88 }
89 dest->set_name(i, static_cast<const millijson::String*>(names[i].get())->value());
90 }
91}
92
93template<class Function_>
94auto process_array_or_scalar_values(
95 const std::unordered_map<std::string, std::shared_ptr<millijson::Base> >& properties,
96 const std::string& path,
97 Function_ fun)
98{
99 auto vIt = properties.find("values");
100 if (vIt == properties.end()) {
101 throw std::runtime_error("expected 'values' property for object at '" + path + "'");
102 }
103
104 auto names_ptr = has_names(properties, path);
105 bool has_names = names_ptr != NULL;
106
107 typename std::invoke_result<Function_,std::vector<std::shared_ptr<millijson::Base> >,bool,bool>::type out_ptr;
108
109 const auto& values_ptr = vIt->second;
110 if (values_ptr->type() == millijson::ARRAY) {
111 out_ptr = fun(static_cast<const millijson::Array*>(values_ptr.get())->value(), has_names, false);
112 } else {
113 std::vector<std::shared_ptr<millijson::Base> > temp { values_ptr };
114 out_ptr = fun(temp, has_names, true);
115 }
116
117 if (has_names) {
118 fill_names(names_ptr, out_ptr, path);
119 }
120 return out_ptr;
121}
122
123template<class Destination_, class Function_>
124void extract_integers(const std::vector<std::shared_ptr<millijson::Base> >& values, Destination_* dest, Function_ check, const std::string& path, const Version& version) {
125 const auto n = values.size();
126 for (I<decltype(n)> i = 0; i < n; ++i) {
127 if (values[i]->type() == millijson::NOTHING) {
128 dest->set_missing(i);
129 continue;
130 }
131
132 if (values[i]->type() != millijson::NUMBER) {
133 throw std::runtime_error("expected a number at '" + path + "/values/" + std::to_string(i) + "'");
134 }
135
136 auto val = static_cast<const millijson::Number*>(values[i].get())->value();
137 if (val != std::floor(val)) {
138 throw std::runtime_error("expected an integer at '" + path + "/values/" + std::to_string(i) + "'");
139 }
140
141 // 32-bit integers are always representable by doubles, as the latter have 53 bits of precision.
142 constexpr double upper = std::numeric_limits<std::int32_t>::max();
143 constexpr double lower = std::numeric_limits<std::int32_t>::min();
144 if (val < lower || val > upper) {
145 throw std::runtime_error("value at '" + path + "/values/" + std::to_string(i) + "' cannot be represented by a 32-bit signed integer");
146 }
147
148 const std::int32_t ival = val;
149 if (version.equals(1, 0) && ival == -2147483648) {
150 dest->set_missing(i);
151 continue;
152 }
153
154 check(ival);
155 dest->set(i, ival);
156 }
157}
158
159template<class Destination_, class Function_>
160void extract_strings(const std::vector<std::shared_ptr<millijson::Base> >& values, Destination_* dest, Function_ check, const std::string& path) {
161 const auto n = values.size();
162 for (I<decltype(n)> i = 0; i < n; ++i) {
163 if (values[i]->type() == millijson::NOTHING) {
164 dest->set_missing(i);
165 continue;
166 }
167
168 if (values[i]->type() != millijson::STRING) {
169 throw std::runtime_error("expected a string at '" + path + "/values/" + std::to_string(i) + "'");
170 }
171
172 const auto& str = static_cast<const millijson::String*>(values[i].get())->value();
173 check(str);
174 dest->set(i, str);
175 }
176}
177
178template<class Provisioner_, class Externals_>
179std::shared_ptr<Base> parse_object(const millijson::Base* contents, Externals_& ext, const std::string& path, const Version& version) {
180 if (contents->type() != millijson::OBJECT) {
181 throw std::runtime_error("each R object should be represented by a JSON object at '" + path + "'");
182 }
183 const auto& map = static_cast<const millijson::Object*>(contents)->value();
184
185 auto tIt = map.find("type");
186 if (tIt == map.end()) {
187 throw std::runtime_error("missing 'type' property for JSON object at '" + path + "'");
188 }
189 const auto& type_ptr = tIt->second;
190 if (type_ptr->type() != millijson::STRING) {
191 throw std::runtime_error("expected a string at '" + path + "/type'");
192 }
193 const auto& type = static_cast<const millijson::String*>(type_ptr.get())->value();
194
195 std::shared_ptr<Base> output;
196 if (type == "nothing") {
197 output.reset(Provisioner_::new_Nothing());
198
199 } else if (type == "external") {
200 auto iIt = map.find("index");
201 if (iIt == map.end()) {
202 throw std::runtime_error("expected 'index' property for 'external' type at '" + path + "'");
203 }
204 const auto& index_ptr = iIt->second;
205 if (index_ptr->type() != millijson::NUMBER) {
206 throw std::runtime_error("expected a number at '" + path + "/index'");
207 }
208
209 const auto flt_index = static_cast<const millijson::Number*>(index_ptr.get())->value();
210 if (flt_index != std::floor(flt_index)) {
211 throw std::runtime_error("expected an integer at '" + path + "/index'");
212 } else if (flt_index < 0) {
213 throw std::runtime_error("expected a non-negative integer at '" + path + "/index'");
214 }
215
216 std::int32_t index;
217 try {
218 index = sanisizer::from_float<std::int32_t>(flt_index);
219 } catch (...) {
220 throw std::runtime_error("value at '" + path + "/index' should fit in a 32-bit signed integer");
221 }
222 if (sanisizer::is_greater_than_or_equal(index, ext.size())) {
223 throw std::runtime_error("external index out of range at '" + path + "/index'");
224 }
225 output.reset(Provisioner_::new_External(ext.get(index)));
226
227 } else if (type == "integer") {
228 process_array_or_scalar_values(map, path, [&](const auto& vals, bool named, bool scalar) -> auto {
229 auto ptr = Provisioner_::new_Integer(sanisizer::cast<std::size_t>(vals.size()), named, scalar);
230 output.reset(ptr);
231 extract_integers(vals, ptr, [](std::int32_t) -> void {}, path, version);
232 return ptr;
233 });
234
235 } else if (type == "factor" || (version.equals(1, 0) && type == "ordered")) {
236 bool ordered = false;
237 if (type == "ordered") {
238 ordered = true;
239 } else {
240 auto oIt = map.find("ordered");
241 if (oIt != map.end()) {
242 if (oIt->second->type() != millijson::BOOLEAN) {
243 throw std::runtime_error("expected a boolean at '" + path + "/ordered'");
244 }
245 ordered = static_cast<const millijson::Boolean*>((oIt->second).get())->value();
246 }
247 }
248
249 const std::string levels_name = "levels"; // avoid dangling reference from casting of string literal.
250 const auto& lvals = extract_array(map, levels_name, path);
251 const auto nlevels = lvals.size();
252 auto fptr = process_array_or_scalar_values(map, path, [&](const auto& vals, bool named, bool scalar) -> auto {
253 auto ptr = Provisioner_::new_Factor(sanisizer::cast<std::size_t>(vals.size()), named, scalar, sanisizer::cast<std::size_t>(nlevels), ordered);
254 output.reset(ptr);
255 extract_integers(vals, ptr, [&](std::int32_t x) -> void {
256 if (x < 0) {
257 throw std::runtime_error("factor indices should be non-negative in '" + path + "/values'");
258 } else if (sanisizer::is_greater_than_or_equal(x, nlevels)) {
259 throw std::runtime_error("factor indices of out of range of levels in '" + path + "/values'");
260 }
261 }, path, version);
262 return ptr;
263 });
264
265 std::unordered_set<std::string> existing;
266 for (I<decltype(nlevels)> l = 0; l < nlevels; ++l) {
267 if (lvals[l]->type() != millijson::STRING) {
268 throw std::runtime_error("expected strings at '" + path + "/levels/" + std::to_string(l) + "'");
269 }
270
271 const auto& level = static_cast<const millijson::String*>(lvals[l].get())->value();
272 if (existing.find(level) != existing.end()) {
273 throw std::runtime_error("detected duplicate string at '" + path + "/levels/" + std::to_string(l) + "'");
274 }
275 fptr->set_level(l, level);
276 existing.insert(level);
277 }
278
279 } else if (type == "boolean") {
280 process_array_or_scalar_values(map, path, [&](const auto& vals, bool named, bool scalar) -> auto {
281 const auto n = vals.size();
282 auto ptr = Provisioner_::new_Boolean(sanisizer::cast<std::size_t>(n), named, scalar);
283 output.reset(ptr);
284
285 for (I<decltype(n)> i = 0; i < n; ++i) {
286 if (vals[i]->type() == millijson::NOTHING) {
287 ptr->set_missing(i);
288 continue;
289 }
290
291 if (vals[i]->type() != millijson::BOOLEAN) {
292 throw std::runtime_error("expected a boolean at '" + path + "/values/" + std::to_string(i) + "'");
293 }
294 ptr->set(i, static_cast<const millijson::Boolean*>(vals[i].get())->value());
295 }
296
297 return ptr;
298 });
299
300 } else if (type == "number") {
301 process_array_or_scalar_values(map, path, [&](const auto& vals, bool named, bool scalar) -> auto {
302 const auto n = vals.size();
303 auto ptr = Provisioner_::new_Number(sanisizer::cast<std::size_t>(n), named, scalar);
304 output.reset(ptr);
305
306 for (I<decltype(n)> i = 0; i < n; ++i) {
307 if (vals[i]->type() == millijson::NOTHING) {
308 ptr->set_missing(i);
309 continue;
310 }
311
312 if (vals[i]->type() == millijson::NUMBER) {
313 ptr->set(i, static_cast<const millijson::Number*>(vals[i].get())->value());
314 } else if (vals[i]->type() == millijson::STRING) {
315 auto str = static_cast<const millijson::String*>(vals[i].get())->value();
316 if (str == "NaN") {
317 ptr->set(i, std::numeric_limits<double>::quiet_NaN());
318 } else if (str == "Inf") {
319 ptr->set(i, std::numeric_limits<double>::infinity());
320 } else if (str == "-Inf") {
321 ptr->set(i, -std::numeric_limits<double>::infinity());
322 } else {
323 throw std::runtime_error("unsupported string '" + str + "' at '" + path + "/values/" + std::to_string(i) + "'");
324 }
325 } else {
326 throw std::runtime_error("expected a number at '" + path + "/values/" + std::to_string(i) + "'");
327 }
328 }
329
330 return ptr;
331 });
332
333 } else if (type == "string" || (version.equals(1, 0) && (type == "date" || type == "date-time"))) {
334 StringVector::Format format = StringVector::NONE;
335 if (version.equals(1, 0)) {
336 if (type == "date") {
337 format = StringVector::DATE;
338 } else if (type == "date-time") {
339 format = StringVector::DATETIME;
340 }
341 } else {
342 auto fIt = map.find("format");
343 if (fIt != map.end()) {
344 if (fIt->second->type() != millijson::STRING) {
345 throw std::runtime_error("expected a string at '" + path + "/format'");
346 }
347 auto fptr = static_cast<const millijson::String*>(fIt->second.get());
348 if (fptr->value() == "date") {
349 format = StringVector::DATE;
350 } else if (fptr->value() == "date-time") {
351 format = StringVector::DATETIME;
352 } else {
353 throw std::runtime_error("unsupported format '" + fptr->value() + "' at '" + path + "/format'");
354 }
355 }
356 }
357
358 process_array_or_scalar_values(map, path, [&](const auto& vals, bool named, bool scalar) -> auto {
359 auto ptr = Provisioner_::new_String(sanisizer::cast<std::size_t>(vals.size()), named, scalar, format);
360 output.reset(ptr);
361
362 if (format == StringVector::NONE) {
363 extract_strings(vals, ptr, [](const std::string&) -> void {}, path);
364 } else if (format == StringVector::DATE) {
365 extract_strings(vals, ptr, [&](const std::string& x) -> void {
366 if (!ritsuko::is_date(x.c_str(), x.size())) {
367 throw std::runtime_error("dates should follow YYYY-MM-DD formatting in '" + path + "/values'");
368 }
369 }, path);
370 } else if (format == StringVector::DATETIME) {
371 extract_strings(vals, ptr, [&](const std::string& x) -> void {
372 if (!ritsuko::is_rfc3339(x.c_str(), x.size())) {
373 throw std::runtime_error("date-times should follow the Internet Date/Time format in '" + path + "/values'");
374 }
375 }, path);
376 }
377
378 return ptr;
379 });
380
381 } else if (type == "list") {
382 auto names_ptr = has_names(map, path);
383 const bool has_names = names_ptr != NULL;
384
385 const std::string values_name = "values"; // avoid dangling reference from casting of string literal.
386 const auto& vals = extract_array(map, values_name, path);
387
388 const auto n = vals.size();
389 auto ptr = Provisioner_::new_List(sanisizer::cast<std::size_t>(n), has_names);
390 output.reset(ptr);
391
392 for (I<decltype(n)> i = 0; i < n; ++i) {
393 ptr->set(i, parse_object<Provisioner_>(vals[i].get(), ext, path + "/values/" + std::to_string(i), version));
394 }
395
396 if (has_names) {
397 fill_names(names_ptr, ptr, path);
398 }
399
400 } else {
401 throw std::runtime_error("unknown object type '" + type + "' at '" + path + "/type'");
402 }
403
404 return output;
405}
413struct Options {
418 bool parallel = false;
419
423 bool strict_list = true;
424
429 std::size_t buffer_size = 65536;
430};
431
451template<class Provisioner_, class Reader_, class Externals_>
452ParsedList parse(Reader_& reader, Externals_ ext, const Options& options) {
453 millijson::ParseOptions popt;
454 popt.buffer_size = options.buffer_size;
455 popt.parallel = options.parallel;
456 auto contents = millijson::parse(reader, popt);
457
458 Version version;
459 if (contents->type() == millijson::OBJECT) {
460 const auto& map = static_cast<const millijson::Object*>(contents.get())->value();
461 auto vIt = map.find("version");
462 if (vIt != map.end()) {
463 if (vIt->second->type() != millijson::STRING) {
464 throw std::runtime_error("expected a string in 'version'");
465 }
466 const auto& vstr = static_cast<const millijson::String*>(vIt->second.get())->value();
467 auto vraw = ritsuko::parse_version_string(vstr.c_str(), vstr.size(), /* skip_patch = */ true);
468 version.major = vraw.major;
469 version.minor = vraw.minor;
470 }
471 }
472
473 ExternalTracker etrack(std::move(ext));
474 auto output = parse_object<Provisioner_>(contents.get(), etrack, "", version);
475
476 if (options.strict_list && output->type() != LIST) {
477 throw std::runtime_error("top-level object should represent an R list");
478 }
479 etrack.validate();
480
481 return ParsedList(std::move(output), std::move(version));
482}
483
501template<class Provisioner_, class Externals_>
502ParsedList parse_file(const std::string& file, Externals_ ext, const Options& options) {
503 std::unique_ptr<byteme::Reader> ptr;
504 if (byteme::is_gzip(file.c_str())) {
505 ptr.reset(new byteme::GzipFileReader(file.c_str(), {}));
506 } else {
507 ptr.reset(new byteme::RawFileReader(file.c_str(), {}));
508 }
509 return parse<Provisioner_>(*ptr, std::move(ext), options);
510}
511
530template<class Provisioner_, class Externals_>
531ParsedList parse_buffer(const unsigned char* buffer, std::size_t len, Externals_ ext, const Options& options) {
532 std::unique_ptr<byteme::Reader> ptr;
533 if (byteme::is_zlib_or_gzip(buffer, len)) {
534 ptr.reset(new byteme::ZlibBufferReader(buffer, len, {}));
535 } else {
536 ptr.reset(new byteme::RawBufferReader(buffer, len));
537 }
538 return parse<Provisioner_>(*ptr, std::move(ext), options);
539}
540
549inline void validate(byteme::Reader& reader, int num_external, const Options& options) {
550 parse<DummyProvisioner>(reader, DummyExternals(num_external), options);
551}
552
561inline void validate_file(const std::string& file, int num_external, const Options& options) {
562 parse_file<DummyProvisioner>(file, DummyExternals(num_external), options);
563}
564
574inline void validate_buffer(const unsigned char* buffer, std::size_t len, int num_external, const Options& options) {
575 parse_buffer<DummyProvisioner>(buffer, len, DummyExternals(num_external), options);
576}
577
578}
579
580}
581
582#endif
Dummy classes for parsing without storing the results.
Class to hold the parsed list.
Dummy class satisfying the Externals_ interface of hdf5::parse().
Definition Dummy.hpp:131
Format
Definition interfaces.hpp:159
Defines the interfaces to use in HDF5 parsing.
bool is_zlib_or_gzip(const unsigned char *buffer, std::size_t n)
bool is_gzip(const unsigned char *buffer, std::size_t n)
ParsedList parse_buffer(const unsigned char *buffer, std::size_t len, Externals_ ext, const Options &options)
Definition parse_json.hpp:531
void validate_file(const std::string &file, int num_external, const Options &options)
Definition parse_json.hpp:561
ParsedList parse(Reader_ &reader, Externals_ ext, const Options &options)
Definition parse_json.hpp:452
void validate(byteme::Reader &reader, int num_external, const Options &options)
Definition parse_json.hpp:549
void validate_buffer(const unsigned char *buffer, std::size_t len, int num_external, const Options &options)
Definition parse_json.hpp:574
ParsedList parse_file(const std::string &file, Externals_ ext, const Options &options)
Definition parse_json.hpp:502
Parse an R list from a HDF5 or JSON file.
Definition parse_json.hpp:31
Results of parsing a list from file.
Definition ParsedList.hpp:19
Options for JSON file parsing.
Definition parse_json.hpp:413
std::size_t buffer_size
Definition parse_json.hpp:429
bool strict_list
Definition parse_json.hpp:423
bool parallel
Definition parse_json.hpp:418