/home/runner/work/arachne/arachne/src/contracts/contracts.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | #include "arachne/contracts.hpp" |
2 | | |
3 | | #include <nlohmann/json.hpp> |
4 | | |
5 | | #include <algorithm> |
6 | | #include <array> |
7 | | #include <charconv> |
8 | | #include <cmath> |
9 | | #include <cstdint> |
10 | | #include <initializer_list> |
11 | | #include <limits> |
12 | | #include <regex> |
13 | | #include <set> |
14 | | #include <stdexcept> |
15 | | #include <string> |
16 | | #include <string_view> |
17 | | #include <utility> |
18 | | |
19 | | namespace arachnespace::contracts { |
20 | | namespace { |
21 | | |
22 | | using json = nlohmann::json; |
23 | | |
24 | | constexpr std::array<std::pair<std::string_view, contract_name>, 10> |
25 | | contract_names { { |
26 | | { "arachne_batch_v2", contract_name::arachne_batch }, |
27 | | { "batch_envelope_v1", contract_name::batch_envelope }, |
28 | | { "fetch_plan_v1", contract_name::fetch_plan }, |
29 | | { "fetch_request_v1", contract_name::fetch_request }, |
30 | | { "acquired_artifact_v1", contract_name::acquired_artifact }, |
31 | | { "research_candidate_graph_plan_v1", |
32 | | contract_name::research_candidate_graph_plan }, |
33 | | { "product_graph_snapshot_v1", |
34 | | contract_name::product_graph_snapshot }, |
35 | | { "research_candidate_graph_snapshot_v1", |
36 | | contract_name::research_candidate_graph_snapshot }, |
37 | | { "viewer_projection_v1", contract_name::viewer_projection }, |
38 | | { "site_bundle_v1", contract_name::site_bundle }, |
39 | | } }; |
40 | | |
41 | | void |
42 | | add(validation_result& result, std::string path, std::string code, |
43 | 46 | std::string message) { |
44 | 46 | result.diagnostics.push_back( |
45 | 46 | { std::move(path), std::move(code), std::move(message) } |
46 | 46 | ); |
47 | 46 | } |
48 | | |
49 | | std::string |
50 | 941 | child_path(const std::string_view parent, const std::string_view key) { |
51 | 941 | if (parent.empty()) { |
52 | 423 | return "/" + std::string(key); |
53 | 423 | } |
54 | 518 | return std::string(parent) + "/" + std::string(key); |
55 | 941 | } |
56 | | |
57 | | const json* field( |
58 | | const json& object, const std::string_view key, |
59 | | const std::string_view path, validation_result& result |
60 | 1.93k | ) { |
61 | 1.93k | const auto it = object.find(key); |
62 | 1.93k | if (it == object.end()) { |
63 | 6 | add(result, child_path(path, key), "required", |
64 | 6 | "required field is missing"); |
65 | 6 | return nullptr; |
66 | 6 | } |
67 | 1.92k | return &*it; |
68 | 1.93k | } |
69 | | |
70 | | void reject_unknown_fields( |
71 | | const json& object, const std::string_view path, |
72 | | const std::initializer_list<std::string_view> allowed, |
73 | | validation_result& result |
74 | 706 | ) { |
75 | 706 | if (!object.is_object()) { |
76 | 0 | return; |
77 | 0 | } |
78 | 2.74k | for (const auto& [key, unused] : object.items()) { |
79 | 2.74k | (void)unused; |
80 | 2.74k | if (std::ranges::find(allowed, std::string_view(key)) |
81 | 2.74k | == allowed.end()) { |
82 | 23 | add(result, child_path(path, key), "unknown_field", |
83 | 23 | "field is not defined by this contract version"); |
84 | 23 | } |
85 | 2.74k | } |
86 | 706 | } |
87 | | |
88 | | const json* require_object( |
89 | | const json& object, const std::string_view key, |
90 | | const std::string_view path, validation_result& result |
91 | 314 | ) { |
92 | 314 | const json* value = field(object, key, path, result); |
93 | 314 | if (value != nullptr && !value->is_object()) { |
94 | 0 | add(result, child_path(path, key), "type", |
95 | 0 | "expected a JSON object"); |
96 | 0 | return nullptr; |
97 | 0 | } |
98 | 314 | return value; |
99 | 314 | } |
100 | | |
101 | | const json* require_array( |
102 | | const json& object, const std::string_view key, |
103 | | const std::string_view path, validation_result& result |
104 | 116 | ) { |
105 | 116 | const json* value = field(object, key, path, result); |
106 | 116 | if (value != nullptr && !value->is_array()) { |
107 | 0 | add(result, child_path(path, key), "type", "expected a JSON array"); |
108 | 0 | return nullptr; |
109 | 0 | } |
110 | 116 | return value; |
111 | 116 | } |
112 | | |
113 | | const json* optional_object( |
114 | | const json& object, const std::string_view key, |
115 | | const std::string_view path, validation_result& result |
116 | 172 | ) { |
117 | 172 | const auto it = object.find(key); |
118 | 172 | if (it == object.end()) { |
119 | 103 | return nullptr; |
120 | 103 | } |
121 | 69 | if (!it->is_object()) { |
122 | 0 | add(result, child_path(path, key), "type", |
123 | 0 | "expected a JSON object"); |
124 | 0 | return nullptr; |
125 | 0 | } |
126 | 69 | return &*it; |
127 | 69 | } |
128 | | |
129 | | const json* optional_array( |
130 | | const json& object, const std::string_view key, |
131 | | const std::string_view path, validation_result& result |
132 | 1.17k | ) { |
133 | 1.17k | const auto it = object.find(key); |
134 | 1.17k | if (it == object.end()) { |
135 | 972 | return nullptr; |
136 | 972 | } |
137 | 204 | if (!it->is_array()) { |
138 | 0 | add(result, child_path(path, key), "type", |
139 | 0 | "expected a JSON array"); |
140 | 0 | return nullptr; |
141 | 0 | } |
142 | 204 | return &*it; |
143 | 204 | } |
144 | | |
145 | | const json* require_string( |
146 | | const json& object, const std::string_view key, |
147 | | const std::string_view path, validation_result& result |
148 | 1.14k | ) { |
149 | 1.14k | const json* value = field(object, key, path, result); |
150 | 1.14k | if (value == nullptr) { |
151 | 4 | return nullptr; |
152 | 4 | } |
153 | 1.14k | if (!value->is_string()) { |
154 | 0 | add(result, child_path(path, key), "type", |
155 | 0 | "expected a JSON string"); |
156 | 0 | return nullptr; |
157 | 0 | } |
158 | 1.14k | if (value->get_ref<const std::string&>().empty()) { |
159 | 0 | add(result, child_path(path, key), "min_length", |
160 | 0 | "string must not be empty"); |
161 | 0 | return nullptr; |
162 | 0 | } |
163 | 1.14k | return value; |
164 | 1.14k | } |
165 | | |
166 | | const json* optional_string( |
167 | | const json& object, const std::string_view key, |
168 | | const std::string_view path, validation_result& result |
169 | 711 | ) { |
170 | 711 | const auto it = object.find(key); |
171 | 711 | if (it == object.end()) { |
172 | 506 | return nullptr; |
173 | 506 | } |
174 | 205 | if (!it->is_string()) { |
175 | 0 | add(result, child_path(path, key), "type", |
176 | 0 | "expected a JSON string"); |
177 | 0 | return nullptr; |
178 | 0 | } |
179 | 205 | if (it->get_ref<const std::string&>().empty()) { |
180 | 0 | add(result, child_path(path, key), "min_length", |
181 | 0 | "string must not be empty"); |
182 | 0 | return nullptr; |
183 | 0 | } |
184 | 205 | return &*it; |
185 | 205 | } |
186 | | |
187 | | bool string_is_one_of( |
188 | | const json* value, const std::initializer_list<std::string_view> choices |
189 | 271 | ) { |
190 | 271 | if (value == nullptr || !value->is_string()) { |
191 | 0 | return false; |
192 | 0 | } |
193 | 271 | const std::string& text = value->get_ref<const std::string&>(); |
194 | 271 | return std::ranges::find(choices, std::string_view(text)) |
195 | 271 | != choices.end(); |
196 | 271 | } |
197 | | |
198 | | void require_enum( |
199 | | const json& object, const std::string_view key, |
200 | | const std::string_view path, |
201 | | const std::initializer_list<std::string_view> choices, |
202 | | validation_result& result |
203 | 248 | ) { |
204 | 248 | const json* value = require_string(object, key, path, result); |
205 | 248 | if (value != nullptr && !string_is_one_of(value, choices)) { |
206 | 1 | add(result, child_path(path, key), "enum", |
207 | 1 | "value is not one of the allowed strings"); |
208 | 1 | } |
209 | 248 | } |
210 | | |
211 | 691 | bool matches(const json* value, const std::regex& expression) { |
212 | 691 | if (value == nullptr || !value->is_string()) { |
213 | 0 | return false; |
214 | 0 | } |
215 | 691 | return std::regex_match( |
216 | 691 | value->get_ref<const std::string&>(), expression |
217 | 691 | ); |
218 | 691 | } |
219 | | |
220 | | void require_stable_id( |
221 | | const json& object, const std::string_view key, |
222 | | const std::string_view path, validation_result& result |
223 | 327 | ) { |
224 | 327 | static const std::regex expression( |
225 | 327 | R"(^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$)" |
226 | 327 | ); |
227 | 327 | static const std::regex canonical_entity( |
228 | 327 | R"(^(agent|work|concept|manifestation)-[0-9]{6,}$)" |
229 | 327 | ); |
230 | 327 | const json* value = require_string(object, key, path, result); |
231 | 327 | if (value != nullptr && !matches(value, expression)) { |
232 | 0 | add(result, child_path(path, key), "pattern", |
233 | 0 | "expected a stable identifier of at most 128 characters"); |
234 | 327 | } else if ( |
235 | 327 | value != nullptr && key == "local_id" |
236 | 327 | && matches(value, canonical_entity) |
237 | 327 | ) { |
238 | 2 | add(result, child_path(path, key), "reserved_identifier", |
239 | 2 | "local IDs must not use a canonical entity ID pattern"); |
240 | 2 | } |
241 | 327 | } |
242 | | |
243 | | void require_sha256( |
244 | | const json& object, const std::string_view key, |
245 | | const std::string_view path, validation_result& result |
246 | 122 | ) { |
247 | 122 | static const std::regex expression(R"(^[0-9a-f]{64}$)"); |
248 | 122 | const json* value = require_string(object, key, path, result); |
249 | 122 | if (value != nullptr && !matches(value, expression)) { |
250 | 1 | add(result, child_path(path, key), "sha256", |
251 | 1 | "expected a lowercase 64-character SHA-256 digest"); |
252 | 1 | } |
253 | 122 | } |
254 | | |
255 | | void require_timestamp( |
256 | | const json& object, const std::string_view key, |
257 | | const std::string_view path, validation_result& result |
258 | 66 | ) { |
259 | 66 | static const std::regex expression( |
260 | 66 | R"(^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$)" |
261 | 66 | ); |
262 | 66 | const json* value = require_string(object, key, path, result); |
263 | 66 | if (value != nullptr && !matches(value, expression)) { |
264 | 0 | add(result, child_path(path, key), "date_time", |
265 | 0 | "expected an RFC 3339 date-time with an explicit offset"); |
266 | 0 | } |
267 | 66 | } |
268 | | |
269 | | void require_nonnegative_integer( |
270 | | const json& object, const std::string_view key, |
271 | | const std::string_view path, validation_result& result |
272 | 122 | ) { |
273 | 122 | const json* value = field(object, key, path, result); |
274 | 122 | if (value == nullptr) { |
275 | 0 | return; |
276 | 0 | } |
277 | 122 | if (!value->is_number_integer() && !value->is_number_unsigned()) { |
278 | 0 | add(result, child_path(path, key), "type", |
279 | 0 | "expected a non-negative integer"); |
280 | 0 | return; |
281 | 0 | } |
282 | 122 | if (value->is_number_integer() && value->get<long long>() < 0) { |
283 | 1 | add(result, child_path(path, key), "minimum", |
284 | 1 | "integer must be non-negative"); |
285 | 1 | } |
286 | 122 | } |
287 | | |
288 | | void require_boolean( |
289 | | const json& object, const std::string_view key, |
290 | | const std::string_view path, validation_result& result |
291 | 39 | ) { |
292 | 39 | const json* value = field(object, key, path, result); |
293 | 39 | if (value != nullptr && !value->is_boolean()) { |
294 | 0 | add(result, child_path(path, key), "type", |
295 | 0 | "expected a JSON boolean"); |
296 | 0 | } |
297 | 39 | } |
298 | | |
299 | | void optional_integer_range( |
300 | | const json& object, const std::string_view key, |
301 | | const std::string_view path, const std::int64_t minimum, |
302 | | const std::int64_t maximum, validation_result& result |
303 | 209 | ) { |
304 | 209 | const auto it = object.find(key); |
305 | 209 | if (it == object.end()) { |
306 | 99 | return; |
307 | 99 | } |
308 | 110 | if (!it->is_number_integer() && !it->is_number_unsigned()) { |
309 | 0 | add(result, child_path(path, key), "type", "expected an integer"); |
310 | 0 | return; |
311 | 0 | } |
312 | 110 | const long double value = it->is_number_unsigned() |
313 | 110 | ? static_cast<long double>(it->get<std::uint64_t>()) |
314 | 110 | : static_cast<long double>(it->get<std::int64_t>()); |
315 | 110 | if (value < static_cast<long double>(minimum) |
316 | 110 | || value > static_cast<long double>(maximum)) { |
317 | 0 | add(result, child_path(path, key), "range", |
318 | 0 | "integer is outside the permitted range"); |
319 | 0 | } |
320 | 110 | } |
321 | | |
322 | | void require_integer_range( |
323 | | const json& object, const std::string_view key, |
324 | | const std::string_view path, const std::int64_t minimum, |
325 | | const std::int64_t maximum, validation_result& result |
326 | 57 | ) { |
327 | 57 | if (!object.contains(key)) { |
328 | 0 | field(object, key, path, result); |
329 | 0 | return; |
330 | 0 | } |
331 | 57 | optional_integer_range( |
332 | 57 | object, key, path, minimum, maximum, result |
333 | 57 | ); |
334 | 57 | } |
335 | | |
336 | | void optional_number_range( |
337 | | const json& object, const std::string_view key, |
338 | | const std::string_view path, const double minimum, |
339 | | const double maximum, validation_result& result |
340 | 38 | ) { |
341 | 38 | const auto it = object.find(key); |
342 | 38 | if (it == object.end()) { |
343 | 8 | return; |
344 | 8 | } |
345 | 30 | if (!it->is_number()) { |
346 | 0 | add(result, child_path(path, key), "type", "expected a number"); |
347 | 0 | return; |
348 | 0 | } |
349 | 30 | const double value = it->get<double>(); |
350 | 30 | if (!std::isfinite(value) || value < minimum || value > maximum) { |
351 | 0 | add(result, child_path(path, key), "range", |
352 | 0 | "number is outside the permitted range"); |
353 | 0 | } |
354 | 30 | } |
355 | | |
356 | | void require_number_range( |
357 | | const json& object, const std::string_view key, |
358 | | const std::string_view path, const double minimum, |
359 | | const double maximum, validation_result& result |
360 | 6 | ) { |
361 | 6 | if (!object.contains(key)) { |
362 | 0 | field(object, key, path, result); |
363 | 0 | return; |
364 | 0 | } |
365 | 6 | optional_number_range(object, key, path, minimum, maximum, result); |
366 | 6 | } |
367 | | |
368 | | void optional_enum( |
369 | | const json& object, const std::string_view key, |
370 | | const std::string_view path, |
371 | | const std::initializer_list<std::string_view> choices, |
372 | | validation_result& result |
373 | 71 | ) { |
374 | 71 | const json* value = optional_string(object, key, path, result); |
375 | 71 | if (value != nullptr && !string_is_one_of(value, choices)) { |
376 | 0 | add(result, child_path(path, key), "enum", |
377 | 0 | "value is not one of the allowed strings"); |
378 | 0 | } |
379 | 71 | } |
380 | | |
381 | | void require_pattern( |
382 | | const json& object, const std::string_view key, |
383 | | const std::string_view path, const std::regex& expression, |
384 | | validation_result& result |
385 | 55 | ) { |
386 | 55 | const json* value = require_string(object, key, path, result); |
387 | 55 | if (value != nullptr && !matches(value, expression)) { |
388 | 0 | add(result, child_path(path, key), "pattern", |
389 | 0 | "string does not match the required canonical form"); |
390 | 0 | } |
391 | 55 | } |
392 | | |
393 | | void optional_pattern( |
394 | | const json& object, const std::string_view key, |
395 | | const std::string_view path, const std::regex& expression, |
396 | | validation_result& result |
397 | 3 | ) { |
398 | 3 | const json* value = optional_string(object, key, path, result); |
399 | 3 | if (value != nullptr && !matches(value, expression)) { |
400 | 0 | add(result, child_path(path, key), "pattern", |
401 | 0 | "string does not match the required canonical form"); |
402 | 0 | } |
403 | 3 | } |
404 | | |
405 | | void optional_bounded_integer( |
406 | | const json& object, const std::string_view key, |
407 | | const std::string_view path, const std::uint64_t minimum, |
408 | | const std::uint64_t maximum, validation_result& result |
409 | 72 | ) { |
410 | 72 | const auto it = object.find(key); |
411 | 72 | if (it == object.end()) { |
412 | 0 | return; |
413 | 0 | } |
414 | 72 | bool in_range = false; |
415 | 72 | if (it->is_number_unsigned()) { |
416 | 54 | const std::uint64_t value = it->get<std::uint64_t>(); |
417 | 54 | in_range = value >= minimum && value <= maximum; |
418 | 54 | } else if (it->is_number_integer()) { |
419 | 18 | const std::int64_t value = it->get<std::int64_t>(); |
420 | 18 | in_range = value >= 0 |
421 | 18 | && static_cast<std::uint64_t>(value) >= minimum |
422 | 18 | && static_cast<std::uint64_t>(value) <= maximum; |
423 | 18 | } else { |
424 | 0 | add(result, child_path(path, key), "type", "expected an integer"); |
425 | 0 | return; |
426 | 0 | } |
427 | 72 | if (!in_range) { |
428 | 2 | add(result, child_path(path, key), "range", |
429 | 2 | "integer is outside the permitted safety range"); |
430 | 2 | } |
431 | 72 | } |
432 | | |
433 | | void validate_extensions( |
434 | | const json& object, const std::string_view path, |
435 | | validation_result& result |
436 | 57 | ) { |
437 | 57 | static const std::regex expression( |
438 | 57 | R"(^[a-z][a-z0-9]*(\.[a-z0-9][a-z0-9_-]*)+$)" |
439 | 57 | ); |
440 | 57 | const json* extensions |
441 | 57 | = optional_object(object, "extensions", path, result); |
442 | 57 | if (extensions == nullptr) { |
443 | 47 | return; |
444 | 47 | } |
445 | 10 | for (const auto& [key, unused] : extensions->items()) { |
446 | 10 | (void)unused; |
447 | 10 | if (!std::regex_match(key, expression)) { |
448 | 1 | add( |
449 | 1 | result, child_path(child_path(path, "extensions"), key), |
450 | 1 | "extension_namespace", |
451 | 1 | "extension keys must be reverse-DNS-style namespaced names" |
452 | 1 | ); |
453 | 1 | } |
454 | 10 | } |
455 | 10 | } |
456 | | |
457 | | void validate_artifact( |
458 | | const json& artifact, const std::string_view path, |
459 | | validation_result& result |
460 | 73 | ) { |
461 | 73 | if (!artifact.is_object()) { |
462 | 0 | add(result, std::string(path), "type", |
463 | 0 | "expected an artifact object"); |
464 | 0 | return; |
465 | 0 | } |
466 | 73 | reject_unknown_fields( |
467 | 73 | artifact, path, |
468 | 73 | { "storage_ref", "sha256", "byte_length", "media_type" }, result |
469 | 73 | ); |
470 | 73 | require_string(artifact, "storage_ref", path, result); |
471 | 73 | require_sha256(artifact, "sha256", path, result); |
472 | 73 | require_nonnegative_integer(artifact, "byte_length", path, result); |
473 | 73 | optional_string(artifact, "media_type", path, result); |
474 | 73 | } |
475 | | |
476 | | void validate_artifact_field( |
477 | | const json& object, const std::string_view key, |
478 | | const std::string_view path, validation_result& result |
479 | 58 | ) { |
480 | 58 | const json* artifact = require_object(object, key, path, result); |
481 | 58 | if (artifact != nullptr) { |
482 | 58 | validate_artifact(*artifact, child_path(path, key), result); |
483 | 58 | } |
484 | 58 | } |
485 | | |
486 | | void validate_artifact_array( |
487 | | const json& object, const std::string_view key, |
488 | | const std::string_view path, validation_result& result |
489 | 14 | ) { |
490 | 14 | const json* artifacts = require_array(object, key, path, result); |
491 | 14 | if (artifacts == nullptr) { |
492 | 0 | return; |
493 | 0 | } |
494 | 14 | if (artifacts->empty()) { |
495 | 0 | add(result, child_path(path, key), "min_items", |
496 | 0 | "at least one artifact is required"); |
497 | 0 | } |
498 | 28 | for (std::size_t index = 0; index < artifacts->size(); ++index) { |
499 | 14 | const std::string item_path |
500 | 14 | = child_path(path, key) + "/" + std::to_string(index); |
501 | 14 | const json& entry = (*artifacts)[index]; |
502 | 14 | if (!entry.is_object()) { |
503 | 0 | add(result, item_path, "type", "expected an export object"); |
504 | 0 | continue; |
505 | 0 | } |
506 | 14 | reject_unknown_fields( |
507 | 14 | entry, item_path, { "kind", "artifact" }, result |
508 | 14 | ); |
509 | 14 | require_string(entry, "kind", item_path, result); |
510 | 14 | validate_artifact_field(entry, "artifact", item_path, result); |
511 | 14 | } |
512 | 14 | } |
513 | | |
514 | | void validate_string_array( |
515 | | const json& object, const std::string_view key, |
516 | | const std::string_view path, const bool require_nonempty, |
517 | | validation_result& result |
518 | 22 | ) { |
519 | 22 | const json* values = require_array(object, key, path, result); |
520 | 22 | if (values == nullptr) { |
521 | 0 | return; |
522 | 0 | } |
523 | 22 | if (require_nonempty && values->empty()) { |
524 | 0 | add(result, child_path(path, key), "min_items", |
525 | 0 | "array must contain at least one item"); |
526 | 0 | } |
527 | 38 | for (std::size_t index = 0; index < values->size(); ++index) { |
528 | 16 | if (!(*values)[index].is_string() || (*values)[index].empty()) { |
529 | 0 | add(result, child_path(path, key) + "/" + std::to_string(index), |
530 | 0 | "type", "expected a non-empty string"); |
531 | 0 | } |
532 | 16 | } |
533 | 22 | } |
534 | | |
535 | 2 | std::optional<unsigned int> declared_major(const std::string_view name) { |
536 | 2 | const std::size_t separator = name.rfind("_v"); |
537 | 2 | if (separator == std::string_view::npos |
538 | 2 | || separator + 2 >= name.size()) { |
539 | 0 | return std::nullopt; |
540 | 0 | } |
541 | 2 | unsigned int major = 0; |
542 | 2 | const char* begin = name.data() + separator + 2; |
543 | 2 | const char* end = name.data() + name.size(); |
544 | 2 | const auto parsed = std::from_chars(begin, end, major); |
545 | 2 | if (parsed.ec != std::errc {} || parsed.ptr != end) { |
546 | 0 | return std::nullopt; |
547 | 0 | } |
548 | 2 | return major; |
549 | 2 | } |
550 | | |
551 | 1 | bool known_contract_base(const std::string_view name) { |
552 | 1 | const std::size_t separator = name.rfind("_v"); |
553 | 1 | if (separator == std::string_view::npos) { |
554 | 0 | return false; |
555 | 0 | } |
556 | 1 | const std::string_view base = name.substr(0, separator); |
557 | 4 | return std::ranges::any_of(contract_names, [base](const auto& entry) { |
558 | 4 | const std::string_view supported = entry.first; |
559 | 4 | const std::size_t supported_separator = supported.rfind("_v"); |
560 | 4 | return supported.substr(0, supported_separator) == base; |
561 | 4 | }); |
562 | 1 | } |
563 | | |
564 | | std::optional<contract_name> |
565 | 135 | inspect_contract(const json& document, validation_result& result) { |
566 | 135 | if (!document.is_object()) { |
567 | 0 | add(result, "", "type", "contract document must be a JSON object"); |
568 | 0 | return std::nullopt; |
569 | 0 | } |
570 | 135 | const auto format = document.find("format"); |
571 | 135 | if (format != document.end()) { |
572 | 52 | if (!format->is_string()) { |
573 | 0 | add(result, "/format", "type", "expected a JSON string"); |
574 | 0 | return std::nullopt; |
575 | 0 | } |
576 | 52 | const std::string& name = format->get_ref<const std::string&>(); |
577 | 52 | if (name == "arachne_batch_v2") { |
578 | 52 | return contract_name::arachne_batch; |
579 | 52 | } |
580 | 0 | if (name.starts_with("arachne_batch_v")) { |
581 | 0 | add(result, "/format", "unsupported_major", |
582 | 0 | "unsupported Arachne batch major version"); |
583 | 0 | } else { |
584 | 0 | add(result, "/format", "unknown_contract", |
585 | 0 | "unknown or malformed batch format"); |
586 | 0 | } |
587 | 0 | return std::nullopt; |
588 | 52 | } |
589 | 83 | const auto it = document.find("contract"); |
590 | 83 | if (it == document.end()) { |
591 | 2 | add(result, "/contract", "required", "required field is missing"); |
592 | 2 | return std::nullopt; |
593 | 2 | } |
594 | 81 | if (!it->is_string()) { |
595 | 0 | add(result, "/contract", "type", "expected a JSON string"); |
596 | 0 | return std::nullopt; |
597 | 0 | } |
598 | 81 | const std::string& name = it->get_ref<const std::string&>(); |
599 | 81 | if (const auto parsed = parse_contract_name(name); parsed.has_value()) { |
600 | 80 | return parsed; |
601 | 80 | } |
602 | 1 | if (known_contract_base(name) && declared_major(name).has_value()) { |
603 | 1 | add(result, "/contract", "unsupported_major", |
604 | 1 | "unsupported contract major version " |
605 | 1 | + std::to_string(*declared_major(name))); |
606 | 1 | } else { |
607 | 0 | add(result, "/contract", "unknown_contract", |
608 | 0 | "unknown or malformed contract name"); |
609 | 0 | } |
610 | 1 | return std::nullopt; |
611 | 81 | } |
612 | | |
613 | | void validate_header( |
614 | | const contract_name expected, const json& document, |
615 | | validation_result& result |
616 | 105 | ) { |
617 | 105 | if (expected == contract_name::arachne_batch) { |
618 | 48 | const auto discovered = inspect_contract(document, result); |
619 | 48 | if (discovered.has_value() && *discovered != expected) { |
620 | 0 | add(result, document.contains("format") ? "/format" : "/contract", |
621 | 0 | "contract_mismatch", |
622 | 0 | "expected arachne_batch_v2 but received " |
623 | 0 | + std::string(to_string(*discovered))); |
624 | 0 | } |
625 | 48 | return; |
626 | 48 | } |
627 | 57 | { |
628 | 57 | const auto discovered = inspect_contract(document, result); |
629 | 57 | if (discovered.has_value() && *discovered != expected) { |
630 | 1 | add(result, "/contract", "contract_mismatch", |
631 | 1 | "expected " + std::string(to_string(expected)) |
632 | 1 | + " but received " |
633 | 1 | + std::string(to_string(*discovered))); |
634 | 1 | } |
635 | 57 | } |
636 | 57 | const auto version = document.find("format_version"); |
637 | 57 | if (version == document.end()) { |
638 | 0 | add(result, "/format_version", "required", |
639 | 0 | "required field is missing"); |
640 | 57 | } else if ( |
641 | 57 | !version->is_number_integer() && !version->is_number_unsigned() |
642 | 57 | ) { |
643 | 0 | add(result, "/format_version", "type", |
644 | 0 | "format_version must be an integer"); |
645 | 57 | } else if (*version != 1) { |
646 | 1 | add(result, "/format_version", "unsupported_version", |
647 | 1 | "only format version 1 is supported"); |
648 | 1 | } |
649 | 57 | } |
650 | | |
651 | | void validate_stable_reference( |
652 | | const json& value, const std::string_view path, |
653 | | validation_result& result |
654 | 170 | ) { |
655 | 170 | static const std::regex expression( |
656 | 170 | R"(^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$)" |
657 | 170 | ); |
658 | 170 | static const std::regex canonical_entity( |
659 | 170 | R"(^(agent|work|concept|manifestation)-[0-9]{6,}$)" |
660 | 170 | ); |
661 | 170 | if (!value.is_string()) { |
662 | 0 | add(result, std::string(path), "type", |
663 | 0 | "expected a stable string reference"); |
664 | 170 | } else if (!std::regex_match( |
665 | 170 | value.get_ref<const std::string&>(), expression |
666 | 170 | )) { |
667 | 0 | add(result, std::string(path), "pattern", |
668 | 0 | "expected a stable identifier of at most 128 characters"); |
669 | 170 | } else if ( |
670 | 170 | path.ends_with("/local_id") |
671 | 170 | && std::regex_match( |
672 | 0 | value.get_ref<const std::string&>(), canonical_entity |
673 | 0 | ) |
674 | 170 | ) { |
675 | 0 | add(result, std::string(path), "reserved_identifier", |
676 | 0 | "local IDs must not use a canonical entity ID pattern"); |
677 | 0 | } |
678 | 170 | } |
679 | | |
680 | | void validate_database_or_local_reference( |
681 | | const json& value, const std::string_view path, |
682 | | validation_result& result |
683 | 40 | ) { |
684 | 40 | if (value.is_string()) { |
685 | 37 | validate_stable_reference(value, path, result); |
686 | 37 | static const std::regex canonical_entity( |
687 | 37 | R"(^(agent|work|concept|manifestation)-[0-9]{6,}$)" |
688 | 37 | ); |
689 | 37 | if (std::regex_match( |
690 | 37 | value.get_ref<const std::string&>(), canonical_entity |
691 | 37 | )) { |
692 | 1 | add(result, std::string(path), "reserved_identifier", |
693 | 1 | "local references must not use a canonical entity ID pattern"); |
694 | 1 | } |
695 | 37 | return; |
696 | 37 | } |
697 | 3 | const bool positive_integer |
698 | 3 | = (value.is_number_unsigned() && value.get<std::uint64_t>() >= 1) |
699 | 3 | || (value.is_number_integer() && value.get<std::int64_t>() >= 1); |
700 | 3 | if (!positive_integer) { |
701 | 0 | add(result, std::string(path), "type", |
702 | 0 | "expected a positive database ID or stable local reference"); |
703 | 0 | } |
704 | 3 | } |
705 | | |
706 | | void validate_unique_items( |
707 | | const json& array, const std::string_view path, |
708 | | validation_result& result |
709 | 85 | ) { |
710 | 85 | std::set<std::string> seen; |
711 | 166 | for (std::size_t index = 0; index < array.size(); ++index) { |
712 | 81 | const std::string key = array[index].dump(); |
713 | 81 | if (!seen.insert(key).second) { |
714 | 0 | add(result, std::string(path) + "/" + std::to_string(index), |
715 | 0 | "unique_items", "array items must be unique"); |
716 | 0 | } |
717 | 81 | } |
718 | 85 | } |
719 | | |
720 | | template<typename Validator> |
721 | | void validate_optional_object_array( |
722 | | const json& object, const std::string_view key, |
723 | | const std::string_view path, validation_result& result, |
724 | | Validator&& validator |
725 | 1.05k | ) { |
726 | 1.05k | const json* values = optional_array(object, key, path, result); |
727 | 1.05k | if (values == nullptr) { |
728 | 871 | return; |
729 | 871 | } |
730 | 185 | const std::string array_path = child_path(path, key); |
731 | 396 | for (std::size_t index = 0; index < values->size(); ++index) { |
732 | 211 | const std::string item_path |
733 | 211 | = array_path + "/" + std::to_string(index); |
734 | 211 | const json& item = (*values)[index]; |
735 | 211 | if (!item.is_object()) { |
736 | 0 | add(result, item_path, "type", "expected a JSON object"); |
737 | 0 | continue; |
738 | 0 | } |
739 | 211 | validator(item, item_path, result); |
740 | 211 | } |
741 | 185 | } contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZNS1_26validate_create_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEE3$_0EEvSJ_SL_SL_SN_OT_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 35 | return; | 729 | 35 | } | 730 | 13 | const std::string array_path = child_path(path, key); | 731 | 28 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 15 | const std::string item_path | 733 | 15 | = array_path + "/" + std::to_string(index); | 734 | 15 | const json& item = (*values)[index]; | 735 | 15 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 15 | validator(item, item_path, result); | 740 | 15 | } | 741 | 13 | } |
contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZNS1_26validate_create_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEE3$_1EEvSJ_SL_SL_SN_OT_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 30 | return; | 729 | 30 | } | 730 | 18 | const std::string array_path = child_path(path, key); | 731 | 36 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 18 | const std::string item_path | 733 | 18 | = array_path + "/" + std::to_string(index); | 734 | 18 | const json& item = (*values)[index]; | 735 | 18 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 18 | validator(item, item_path, result); | 740 | 18 | } | 741 | 18 | } |
contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZNS1_26validate_create_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEE3$_2EEvSJ_SL_SL_SN_OT_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 31 | return; | 729 | 31 | } | 730 | 17 | const std::string array_path = child_path(path, key); | 731 | 44 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 27 | const std::string item_path | 733 | 27 | = array_path + "/" + std::to_string(index); | 734 | 27 | const json& item = (*values)[index]; | 735 | 27 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 27 | validator(item, item_path, result); | 740 | 27 | } | 741 | 17 | } |
contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZNS1_26validate_create_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEE3$_3EEvSJ_SL_SL_SN_OT_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 42 | return; | 729 | 42 | } | 730 | 6 | const std::string array_path = child_path(path, key); | 731 | 12 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 6 | const std::string item_path | 733 | 6 | = array_path + "/" + std::to_string(index); | 734 | 6 | const json& item = (*values)[index]; | 735 | 6 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 6 | validator(item, item_path, result); | 740 | 6 | } | 741 | 6 | } |
contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZNS1_26validate_create_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEE3$_4EEvSJ_SL_SL_SN_OT_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 36 | return; | 729 | 36 | } | 730 | 12 | const std::string array_path = child_path(path, key); | 731 | 45 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 33 | const std::string item_path | 733 | 33 | = array_path + "/" + std::to_string(index); | 734 | 33 | const json& item = (*values)[index]; | 735 | 33 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 33 | validator(item, item_path, result); | 740 | 33 | } | 741 | 12 | } |
contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZNS1_26validate_create_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEE3$_5EEvSJ_SL_SL_SN_OT_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 42 | return; | 729 | 42 | } | 730 | 6 | const std::string array_path = child_path(path, key); | 731 | 12 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 6 | const std::string item_path | 733 | 6 | = array_path + "/" + std::to_string(index); | 734 | 6 | const json& item = (*values)[index]; | 735 | 6 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 6 | validator(item, item_path, result); | 740 | 6 | } | 741 | 6 | } |
contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZNS1_26validate_create_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEE3$_6EEvSJ_SL_SL_SN_OT_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 35 | return; | 729 | 35 | } | 730 | 13 | const std::string array_path = child_path(path, key); | 731 | 26 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 13 | const std::string item_path | 733 | 13 | = array_path + "/" + std::to_string(index); | 734 | 13 | const json& item = (*values)[index]; | 735 | 13 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 13 | validator(item, item_path, result); | 740 | 13 | } | 741 | 13 | } |
contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZNS1_26validate_create_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEE3$_7EEvSJ_SL_SL_SN_OT_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 36 | return; | 729 | 36 | } | 730 | 12 | const std::string array_path = child_path(path, key); | 731 | 26 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 14 | const std::string item_path | 733 | 14 | = array_path + "/" + std::to_string(index); | 734 | 14 | const json& item = (*values)[index]; | 735 | 14 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 14 | validator(item, item_path, result); | 740 | 14 | } | 741 | 12 | } |
contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZNS1_26validate_create_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEE3$_8EEvSJ_SL_SL_SN_OT_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 37 | return; | 729 | 37 | } | 730 | 11 | const std::string array_path = child_path(path, key); | 731 | 23 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 12 | const std::string item_path | 733 | 12 | = array_path + "/" + std::to_string(index); | 734 | 12 | const json& item = (*values)[index]; | 735 | 12 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 12 | validator(item, item_path, result); | 740 | 12 | } | 741 | 11 | } |
contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZNS1_26validate_create_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEE3$_9EEvSJ_SL_SL_SN_OT_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 42 | return; | 729 | 42 | } | 730 | 6 | const std::string array_path = child_path(path, key); | 731 | 12 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 6 | const std::string item_path | 733 | 6 | = array_path + "/" + std::to_string(index); | 734 | 6 | const json& item = (*values)[index]; | 735 | 6 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 6 | validator(item, item_path, result); | 740 | 6 | } | 741 | 6 | } |
contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZNS1_26validate_create_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEE4$_10EEvSJ_SL_SL_SN_OT_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 42 | return; | 729 | 42 | } | 730 | 6 | const std::string array_path = child_path(path, key); | 731 | 12 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 6 | const std::string item_path | 733 | 6 | = array_path + "/" + std::to_string(index); | 734 | 6 | const json& item = (*values)[index]; | 735 | 6 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 6 | validator(item, item_path, result); | 740 | 6 | } | 741 | 6 | } |
contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZNS1_26validate_create_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEE4$_11EEvSJ_SL_SL_SN_OT_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 35 | return; | 729 | 35 | } | 730 | 13 | const std::string array_path = child_path(path, key); | 731 | 27 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 14 | const std::string item_path | 733 | 14 | = array_path + "/" + std::to_string(index); | 734 | 14 | const json& item = (*values)[index]; | 735 | 14 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 14 | validator(item, item_path, result); | 740 | 14 | } | 741 | 13 | } |
contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZNS1_26validate_create_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEE4$_12EEvSJ_SL_SL_SN_OT_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 42 | return; | 729 | 42 | } | 730 | 6 | const std::string array_path = child_path(path, key); | 731 | 12 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 6 | const std::string item_path | 733 | 6 | = array_path + "/" + std::to_string(index); | 734 | 6 | const json& item = (*values)[index]; | 735 | 6 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 6 | validator(item, item_path, result); | 740 | 6 | } | 741 | 6 | } |
contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZNS1_26validate_create_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEE4$_13EEvSJ_SL_SL_SN_OT_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 42 | return; | 729 | 42 | } | 730 | 6 | const std::string array_path = child_path(path, key); | 731 | 12 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 6 | const std::string item_path | 733 | 6 | = array_path + "/" + std::to_string(index); | 734 | 6 | const json& item = (*values)[index]; | 735 | 6 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 6 | validator(item, item_path, result); | 740 | 6 | } | 741 | 6 | } |
contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZNS1_26validate_update_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEE3$_0EEvSJ_SL_SL_SN_OT_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 48 | return; | 729 | 48 | } | 730 | 0 | const std::string array_path = child_path(path, key); | 731 | 0 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 0 | const std::string item_path | 733 | 0 | = array_path + "/" + std::to_string(index); | 734 | 0 | const json& item = (*values)[index]; | 735 | 0 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 0 | validator(item, item_path, result); | 740 | 0 | } | 741 | 0 | } |
contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZNS1_26validate_update_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEE3$_1EEvSJ_SL_SL_SN_OT_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 39 | return; | 729 | 39 | } | 730 | 9 | const std::string array_path = child_path(path, key); | 731 | 19 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 10 | const std::string item_path | 733 | 10 | = array_path + "/" + std::to_string(index); | 734 | 10 | const json& item = (*values)[index]; | 735 | 10 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 10 | validator(item, item_path, result); | 740 | 10 | } | 741 | 9 | } |
contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZNS1_26validate_update_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEE3$_2EEvSJ_SL_SL_SN_OT_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 47 | return; | 729 | 47 | } | 730 | 1 | const std::string array_path = child_path(path, key); | 731 | 2 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 1 | const std::string item_path | 733 | 1 | = array_path + "/" + std::to_string(index); | 734 | 1 | const json& item = (*values)[index]; | 735 | 1 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 1 | validator(item, item_path, result); | 740 | 1 | } | 741 | 1 | } |
contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZNS1_26validate_update_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEE3$_3EEvSJ_SL_SL_SN_OT_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 48 | return; | 729 | 48 | } | 730 | 0 | const std::string array_path = child_path(path, key); | 731 | 0 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 0 | const std::string item_path | 733 | 0 | = array_path + "/" + std::to_string(index); | 734 | 0 | const json& item = (*values)[index]; | 735 | 0 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 0 | validator(item, item_path, result); | 740 | 0 | } | 741 | 0 | } |
contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZNS1_26validate_update_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEE3$_4EEvSJ_SL_SL_SN_OT_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 41 | return; | 729 | 41 | } | 730 | 7 | const std::string array_path = child_path(path, key); | 731 | 14 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 7 | const std::string item_path | 733 | 7 | = array_path + "/" + std::to_string(index); | 734 | 7 | const json& item = (*values)[index]; | 735 | 7 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 7 | validator(item, item_path, result); | 740 | 7 | } | 741 | 7 | } |
contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZZNS1_25validate_merge_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEENK3$_0clIZNS1_25validate_merge_operationsESJ_SL_SN_E3$_1St16initializer_listISL_ESS_EEDaSL_SL_RKT_T0_T1_bEUlSJ_RKSD_SN_E_EEvSJ_SL_SL_SN_OST_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 39 | return; | 729 | 39 | } | 730 | 9 | const std::string array_path = child_path(path, key); | 731 | 18 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 9 | const std::string item_path | 733 | 9 | = array_path + "/" + std::to_string(index); | 734 | 9 | const json& item = (*values)[index]; | 735 | 9 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 9 | validator(item, item_path, result); | 740 | 9 | } | 741 | 9 | } |
contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZZNS1_25validate_merge_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEENK3$_0clIZNS1_25validate_merge_operationsESJ_SL_SN_E3$_2St16initializer_listISL_ESS_EEDaSL_SL_RKT_T0_T1_bEUlSJ_RKSD_SN_E_EEvSJ_SL_SL_SN_OST_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 42 | return; | 729 | 42 | } | 730 | 6 | const std::string array_path = child_path(path, key); | 731 | 6 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 0 | const std::string item_path | 733 | 0 | = array_path + "/" + std::to_string(index); | 734 | 0 | const json& item = (*values)[index]; | 735 | 0 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 0 | validator(item, item_path, result); | 740 | 0 | } | 741 | 6 | } |
contracts.cpp:_ZN12arachnespace9contracts12_GLOBAL__N_130validate_optional_object_arrayIZZNS1_25validate_merge_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS4_14adl_serializerES7_IhSaIhEEvEESt17basic_string_viewIcSB_ERNS0_17validation_resultEENK3$_0clIZNS1_25validate_merge_operationsESJ_SL_SN_E3$_3St16initializer_listISL_ESS_EEDaSL_SL_RKT_T0_T1_bEUlSJ_RKSD_SN_E_EEvSJ_SL_SL_SN_OST_ Line | Count | Source | 725 | 48 | ) { | 726 | 48 | const json* values = optional_array(object, key, path, result); | 727 | 48 | if (values == nullptr) { | 728 | 40 | return; | 729 | 40 | } | 730 | 8 | const std::string array_path = child_path(path, key); | 731 | 10 | for (std::size_t index = 0; index < values->size(); ++index) { | 732 | 2 | const std::string item_path | 733 | 2 | = array_path + "/" + std::to_string(index); | 734 | 2 | const json& item = (*values)[index]; | 735 | 2 | if (!item.is_object()) { | 736 | 0 | add(result, item_path, "type", "expected a JSON object"); | 737 | 0 | continue; | 738 | 0 | } | 739 | 2 | validator(item, item_path, result); | 740 | 2 | } | 741 | 8 | } |
|
742 | | |
743 | | void validate_nonempty_optional_strings( |
744 | | const json& object, const std::string_view path, |
745 | | const std::initializer_list<std::string_view> keys, |
746 | | validation_result& result |
747 | 101 | ) { |
748 | 468 | for (const std::string_view key : keys) { |
749 | 468 | optional_string(object, key, path, result); |
750 | 468 | } |
751 | 101 | } |
752 | | |
753 | | void validate_year_fields( |
754 | | const json& object, const std::string_view path, |
755 | | const std::initializer_list<std::string_view> keys, |
756 | | validation_result& result |
757 | 58 | ) { |
758 | 116 | for (const std::string_view key : keys) { |
759 | 116 | optional_integer_range(object, key, path, -9999, 9999, result); |
760 | 116 | } |
761 | 58 | } |
762 | | |
763 | | void validate_entity_id( |
764 | | const json& object, const std::string_view key, |
765 | | const std::string_view path, const std::string_view family, |
766 | | validation_result& result |
767 | 22 | ) { |
768 | 22 | const std::regex expression( |
769 | 22 | "^" + std::string(family) + R"(-[0-9]{6,}$)" |
770 | 22 | ); |
771 | 22 | require_pattern(object, key, path, expression, result); |
772 | 22 | } |
773 | | |
774 | | void validate_positive_id_array( |
775 | | const json& object, const std::string_view key, |
776 | | const std::string_view path, validation_result& result |
777 | 108 | ) { |
778 | 108 | const json* values = optional_array(object, key, path, result); |
779 | 108 | if (values == nullptr) { |
780 | 91 | return; |
781 | 91 | } |
782 | 17 | validate_unique_items(*values, child_path(path, key), result); |
783 | 34 | for (std::size_t index = 0; index < values->size(); ++index) { |
784 | 17 | const json& value = (*values)[index]; |
785 | 17 | const bool positive_integer = (value.is_number_unsigned() |
786 | 17 | && value.get<std::uint64_t>() >= 1) |
787 | 17 | || (value.is_number_integer() |
788 | 0 | && value.get<std::int64_t>() >= 1); |
789 | 17 | if (!positive_integer) { |
790 | 0 | add(result, |
791 | 0 | child_path(path, key) + "/" + std::to_string(index), |
792 | 0 | "minimum", "expected a positive integer database ID"); |
793 | 0 | } |
794 | 17 | } |
795 | 17 | } |
796 | | |
797 | | void validate_evidence_references( |
798 | | const json& object, const std::string_view key, |
799 | | const std::string_view path, validation_result& result |
800 | 26 | ) { |
801 | 26 | const json* values = require_array(object, key, path, result); |
802 | 26 | if (values == nullptr) { |
803 | 0 | return; |
804 | 0 | } |
805 | 26 | const std::string array_path = child_path(path, key); |
806 | 26 | if (values->empty()) { |
807 | 1 | add(result, array_path, "min_items", |
808 | 1 | "at least one source-backed evidence reference is required"); |
809 | 1 | } |
810 | 26 | validate_unique_items(*values, array_path, result); |
811 | 52 | for (std::size_t index = 0; index < values->size(); ++index) { |
812 | 26 | validate_database_or_local_reference( |
813 | 26 | (*values)[index], |
814 | 26 | array_path + "/" + std::to_string(index), result |
815 | 26 | ); |
816 | 26 | } |
817 | 26 | } |
818 | | |
819 | | void validate_create_operations( |
820 | | const json& create, const std::string_view path, |
821 | | validation_result& result |
822 | 48 | ) { |
823 | 48 | reject_unknown_fields( |
824 | 48 | create, path, |
825 | 48 | { "agents", "works", "concepts", "manifestations", "names", |
826 | 48 | "external_ids", "sources", "evidence", "credits", |
827 | 48 | "measurements", "financial_facts", "work_concepts", |
828 | 48 | "concept_relations", "parent_guide_assertions" }, |
829 | 48 | result |
830 | 48 | ); |
831 | | |
832 | 48 | validate_optional_object_array( |
833 | 48 | create, "agents", path, result, |
834 | 48 | [](const json& item, const std::string& item_path, |
835 | 48 | validation_result& item_result) { |
836 | 15 | reject_unknown_fields( |
837 | 15 | item, item_path, |
838 | 15 | { "local_id", "agent_type", "birth_year", "death_year" }, |
839 | 15 | item_result |
840 | 15 | ); |
841 | 15 | require_stable_id(item, "local_id", item_path, item_result); |
842 | 15 | require_enum( |
843 | 15 | item, "agent_type", item_path, |
844 | 15 | { "person", "organization", "group" }, item_result |
845 | 15 | ); |
846 | 15 | validate_year_fields( |
847 | 15 | item, item_path, { "birth_year", "death_year" }, |
848 | 15 | item_result |
849 | 15 | ); |
850 | 15 | } |
851 | 48 | ); |
852 | | |
853 | 48 | validate_optional_object_array( |
854 | 48 | create, "works", path, result, |
855 | 48 | [](const json& item, const std::string& item_path, |
856 | 48 | validation_result& item_result) { |
857 | 18 | reject_unknown_fields( |
858 | 18 | item, item_path, |
859 | 18 | { "local_id", "medium", "year_start", "year_end", |
860 | 18 | "date_precision", "date_start_text", "date_end_text", |
861 | 18 | "date_qualifier", "language_code", "country_code", |
862 | 18 | "production_info_json" }, |
863 | 18 | item_result |
864 | 18 | ); |
865 | 18 | require_stable_id(item, "local_id", item_path, item_result); |
866 | 18 | require_enum( |
867 | 18 | item, "medium", item_path, |
868 | 18 | { "film", "short_film", "television", "novel", "novella", |
869 | 18 | "short_story", "poetry", "play", "essay", "album", |
870 | 18 | "single", "composition", "painting", "print", |
871 | 18 | "engraving", "drawing", "sculpture", "installation", |
872 | 18 | "photography", "mixed_media" }, |
873 | 18 | item_result |
874 | 18 | ); |
875 | 18 | validate_year_fields( |
876 | 18 | item, item_path, { "year_start", "year_end" }, item_result |
877 | 18 | ); |
878 | 18 | optional_enum( |
879 | 18 | item, "date_precision", item_path, |
880 | 18 | { "year", "decade", "approximate", "range", "exact" }, |
881 | 18 | item_result |
882 | 18 | ); |
883 | 18 | validate_nonempty_optional_strings( |
884 | 18 | item, item_path, |
885 | 18 | { "date_start_text", "date_end_text", "date_qualifier", |
886 | 18 | "language_code", "country_code", |
887 | 18 | "production_info_json" }, |
888 | 18 | item_result |
889 | 18 | ); |
890 | 18 | } |
891 | 48 | ); |
892 | | |
893 | 48 | validate_optional_object_array( |
894 | 48 | create, "concepts", path, result, |
895 | 48 | [](const json& item, const std::string& item_path, |
896 | 48 | validation_result& item_result) { |
897 | 27 | static const std::regex slug( |
898 | 27 | R"(^[a-z0-9]+(?:-[a-z0-9]+)*$)" |
899 | 27 | ); |
900 | 27 | reject_unknown_fields( |
901 | 27 | item, item_path, { "local_id", "concept_type", "slug" }, |
902 | 27 | item_result |
903 | 27 | ); |
904 | 27 | require_stable_id(item, "local_id", item_path, item_result); |
905 | 27 | require_enum( |
906 | 27 | item, "concept_type", item_path, |
907 | 27 | { "genre", "style", "theme", "keyword", "motif", "trope", |
908 | 27 | "phobia", "taboo", "technique", "movement", "setting", |
909 | 27 | "mood", "content_warning" }, |
910 | 27 | item_result |
911 | 27 | ); |
912 | 27 | require_pattern(item, "slug", item_path, slug, item_result); |
913 | 27 | } |
914 | 48 | ); |
915 | | |
916 | 48 | validate_optional_object_array( |
917 | 48 | create, "manifestations", path, result, |
918 | 48 | [](const json& item, const std::string& item_path, |
919 | 48 | validation_result& item_result) { |
920 | 6 | reject_unknown_fields( |
921 | 6 | item, item_path, |
922 | 6 | { "local_id", "work_id", "manifestation_type", |
923 | 6 | "release_year", "region_code", "language_code", "label" }, |
924 | 6 | item_result |
925 | 6 | ); |
926 | 6 | require_stable_id(item, "local_id", item_path, item_result); |
927 | 6 | const json* work |
928 | 6 | = field(item, "work_id", item_path, item_result); |
929 | 6 | if (work != nullptr) { |
930 | 6 | validate_stable_reference( |
931 | 6 | *work, child_path(item_path, "work_id"), item_result |
932 | 6 | ); |
933 | 6 | } |
934 | 6 | require_enum( |
935 | 6 | item, "manifestation_type", item_path, |
936 | 6 | { "edition", "translation", "release", "pressing", "cut", |
937 | 6 | "restoration", "reissue" }, |
938 | 6 | item_result |
939 | 6 | ); |
940 | 6 | optional_integer_range( |
941 | 6 | item, "release_year", item_path, -9999, 9999, item_result |
942 | 6 | ); |
943 | 6 | validate_nonempty_optional_strings( |
944 | 6 | item, item_path, { "region_code", "language_code" }, |
945 | 6 | item_result |
946 | 6 | ); |
947 | 6 | require_string(item, "label", item_path, item_result); |
948 | 6 | } |
949 | 48 | ); |
950 | | |
951 | 48 | validate_optional_object_array( |
952 | 48 | create, "names", path, result, |
953 | 48 | [](const json& item, const std::string& item_path, |
954 | 48 | validation_result& item_result) { |
955 | 33 | reject_unknown_fields( |
956 | 33 | item, item_path, |
957 | 33 | { "entity_id", "name_type", "language_code", |
958 | 33 | "script_code", "value", "is_preferred" }, |
959 | 33 | item_result |
960 | 33 | ); |
961 | 33 | const json* entity |
962 | 33 | = field(item, "entity_id", item_path, item_result); |
963 | 33 | if (entity != nullptr) { |
964 | 33 | validate_stable_reference( |
965 | 33 | *entity, child_path(item_path, "entity_id"), item_result |
966 | 33 | ); |
967 | 33 | } |
968 | 33 | require_enum( |
969 | 33 | item, "name_type", item_path, |
970 | 33 | { "original", "english", "transliteration", "translation", |
971 | 33 | "alias", "credited" }, |
972 | 33 | item_result |
973 | 33 | ); |
974 | 33 | validate_nonempty_optional_strings( |
975 | 33 | item, item_path, { "language_code", "script_code" }, |
976 | 33 | item_result |
977 | 33 | ); |
978 | 33 | require_string(item, "value", item_path, item_result); |
979 | 33 | require_boolean( |
980 | 33 | item, "is_preferred", item_path, item_result |
981 | 33 | ); |
982 | 33 | } |
983 | 48 | ); |
984 | | |
985 | 48 | validate_optional_object_array( |
986 | 48 | create, "external_ids", path, result, |
987 | 48 | [](const json& item, const std::string& item_path, |
988 | 48 | validation_result& item_result) { |
989 | 6 | reject_unknown_fields( |
990 | 6 | item, item_path, |
991 | 6 | { "entity_id", "scheme", "value", "canonical_url" }, |
992 | 6 | item_result |
993 | 6 | ); |
994 | 6 | const json* entity |
995 | 6 | = field(item, "entity_id", item_path, item_result); |
996 | 6 | if (entity != nullptr) { |
997 | 6 | validate_stable_reference( |
998 | 6 | *entity, child_path(item_path, "entity_id"), item_result |
999 | 6 | ); |
1000 | 6 | } |
1001 | 6 | require_string(item, "scheme", item_path, item_result); |
1002 | 6 | require_string(item, "value", item_path, item_result); |
1003 | 6 | optional_string( |
1004 | 6 | item, "canonical_url", item_path, item_result |
1005 | 6 | ); |
1006 | 6 | } |
1007 | 48 | ); |
1008 | | |
1009 | 48 | validate_optional_object_array( |
1010 | 48 | create, "sources", path, result, |
1011 | 48 | [](const json& item, const std::string& item_path, |
1012 | 48 | validation_result& item_result) { |
1013 | 13 | reject_unknown_fields( |
1014 | 13 | item, item_path, |
1015 | 13 | { "local_id", "source_type", "title", |
1016 | 13 | "bibliography_text", "author_text", "publisher", |
1017 | 13 | "publication_date", "url", "doi", "isbn", |
1018 | 13 | "language_code" }, |
1019 | 13 | item_result |
1020 | 13 | ); |
1021 | 13 | require_stable_id(item, "local_id", item_path, item_result); |
1022 | 13 | require_enum( |
1023 | 13 | item, "source_type", item_path, |
1024 | 13 | { "book", "article", "catalogue", "web_page", "interview", |
1025 | 13 | "database", "video", "audio", "PDF" }, |
1026 | 13 | item_result |
1027 | 13 | ); |
1028 | 13 | validate_nonempty_optional_strings( |
1029 | 13 | item, item_path, |
1030 | 13 | { "title", "bibliography_text", "author_text", "publisher", |
1031 | 13 | "publication_date", "url", "doi", "isbn", |
1032 | 13 | "language_code" }, |
1033 | 13 | item_result |
1034 | 13 | ); |
1035 | 13 | if (!item.contains("url") && !item.contains("doi") |
1036 | 13 | && !item.contains("isbn") |
1037 | 13 | && !item.contains("bibliography_text")) { |
1038 | 0 | add(item_result, item_path, "any_of", |
1039 | 0 | "source requires url, doi, isbn, or bibliography_text"); |
1040 | 0 | } |
1041 | 13 | } |
1042 | 48 | ); |
1043 | | |
1044 | 48 | validate_optional_object_array( |
1045 | 48 | create, "evidence", path, result, |
1046 | 48 | [](const json& item, const std::string& item_path, |
1047 | 48 | validation_result& item_result) { |
1048 | 14 | reject_unknown_fields( |
1049 | 14 | item, item_path, |
1050 | 14 | { "local_id", "source_id", "exact_quote", |
1051 | 14 | "quote_language", "quote_translation", "locator_json", |
1052 | 14 | "stance" }, |
1053 | 14 | item_result |
1054 | 14 | ); |
1055 | 14 | require_stable_id(item, "local_id", item_path, item_result); |
1056 | 14 | const json* source |
1057 | 14 | = field(item, "source_id", item_path, item_result); |
1058 | 14 | if (source != nullptr) { |
1059 | 14 | validate_database_or_local_reference( |
1060 | 14 | *source, child_path(item_path, "source_id"), item_result |
1061 | 14 | ); |
1062 | 14 | } |
1063 | 14 | require_string(item, "exact_quote", item_path, item_result); |
1064 | 14 | validate_nonempty_optional_strings( |
1065 | 14 | item, item_path, |
1066 | 14 | { "quote_language", "quote_translation", "locator_json" }, |
1067 | 14 | item_result |
1068 | 14 | ); |
1069 | 14 | require_enum( |
1070 | 14 | item, "stance", item_path, |
1071 | 14 | { "supports", "contradicts", "contextualizes" }, |
1072 | 14 | item_result |
1073 | 14 | ); |
1074 | 14 | } |
1075 | 48 | ); |
1076 | | |
1077 | 48 | validate_optional_object_array( |
1078 | 48 | create, "credits", path, result, |
1079 | 48 | [](const json& item, const std::string& item_path, |
1080 | 48 | validation_result& item_result) { |
1081 | 12 | reject_unknown_fields( |
1082 | 12 | item, item_path, |
1083 | 12 | { "work_id", "agent_id", "role", "credit_order", |
1084 | 12 | "importance", "credited_as" }, |
1085 | 12 | item_result |
1086 | 12 | ); |
1087 | 24 | for (const std::string_view key : { "work_id", "agent_id" }) { |
1088 | 24 | const json* reference |
1089 | 24 | = field(item, key, item_path, item_result); |
1090 | 24 | if (reference != nullptr) { |
1091 | 24 | validate_stable_reference( |
1092 | 24 | *reference, child_path(item_path, key), item_result |
1093 | 24 | ); |
1094 | 24 | } |
1095 | 24 | } |
1096 | 12 | require_enum( |
1097 | 12 | item, "role", item_path, |
1098 | 12 | { "author", "director", "screenwriter", "producer", |
1099 | 12 | "actor", "composer", "performer", "artist", "engraver", |
1100 | 12 | "sculptor", "photographer", "editor", "cinematographer", |
1101 | 12 | "production_company", "publisher", "record_label", |
1102 | 12 | "band" }, |
1103 | 12 | item_result |
1104 | 12 | ); |
1105 | 12 | optional_integer_range( |
1106 | 12 | item, "credit_order", item_path, 0, |
1107 | 12 | std::numeric_limits<std::int64_t>::max(), item_result |
1108 | 12 | ); |
1109 | 12 | require_enum( |
1110 | 12 | item, "importance", item_path, |
1111 | 12 | { "primary", "key", "supporting" }, item_result |
1112 | 12 | ); |
1113 | 12 | optional_string(item, "credited_as", item_path, item_result); |
1114 | 12 | } |
1115 | 48 | ); |
1116 | | |
1117 | 48 | validate_optional_object_array( |
1118 | 48 | create, "measurements", path, result, |
1119 | 48 | [](const json& item, const std::string& item_path, |
1120 | 48 | validation_result& item_result) { |
1121 | 6 | reject_unknown_fields( |
1122 | 6 | item, item_path, |
1123 | 6 | { "entity_id", "measurement_type", "value", "unit", |
1124 | 6 | "qualifier" }, |
1125 | 6 | item_result |
1126 | 6 | ); |
1127 | 6 | const json* entity |
1128 | 6 | = field(item, "entity_id", item_path, item_result); |
1129 | 6 | if (entity != nullptr) { |
1130 | 6 | validate_stable_reference( |
1131 | 6 | *entity, child_path(item_path, "entity_id"), item_result |
1132 | 6 | ); |
1133 | 6 | } |
1134 | 6 | require_enum( |
1135 | 6 | item, "measurement_type", item_path, |
1136 | 6 | { "duration", "height", "width", "depth", "pages" }, |
1137 | 6 | item_result |
1138 | 6 | ); |
1139 | 6 | require_number_range( |
1140 | 6 | item, "value", item_path, 0.0, |
1141 | 6 | std::numeric_limits<double>::max(), item_result |
1142 | 6 | ); |
1143 | 6 | require_enum( |
1144 | 6 | item, "unit", item_path, |
1145 | 6 | { "seconds", "millimetres", "pages" }, item_result |
1146 | 6 | ); |
1147 | 6 | optional_string(item, "qualifier", item_path, item_result); |
1148 | 6 | } |
1149 | 48 | ); |
1150 | | |
1151 | 48 | validate_optional_object_array( |
1152 | 48 | create, "financial_facts", path, result, |
1153 | 48 | [](const json& item, const std::string& item_path, |
1154 | 48 | validation_result& item_result) { |
1155 | 6 | static const std::regex currency(R"(^[A-Z]{3}$)"); |
1156 | 6 | reject_unknown_fields( |
1157 | 6 | item, item_path, |
1158 | 6 | { "work_id", "fact_type", "amount_min", "amount_max", |
1159 | 6 | "currency_code", "value_year", "is_estimate", |
1160 | 6 | "confidence" }, |
1161 | 6 | item_result |
1162 | 6 | ); |
1163 | 6 | const json* work |
1164 | 6 | = field(item, "work_id", item_path, item_result); |
1165 | 6 | if (work != nullptr) { |
1166 | 6 | validate_stable_reference( |
1167 | 6 | *work, child_path(item_path, "work_id"), item_result |
1168 | 6 | ); |
1169 | 6 | } |
1170 | 6 | const json* fact |
1171 | 6 | = require_string(item, "fact_type", item_path, item_result); |
1172 | 6 | if (fact != nullptr && *fact != "budget") { |
1173 | 0 | add(item_result, child_path(item_path, "fact_type"), |
1174 | 0 | "const", "fact_type must be budget"); |
1175 | 0 | } |
1176 | 6 | require_integer_range( |
1177 | 6 | item, "amount_min", item_path, 0, |
1178 | 6 | std::numeric_limits<std::int64_t>::max(), item_result |
1179 | 6 | ); |
1180 | 6 | optional_integer_range( |
1181 | 6 | item, "amount_max", item_path, 0, |
1182 | 6 | std::numeric_limits<std::int64_t>::max(), item_result |
1183 | 6 | ); |
1184 | 6 | require_pattern( |
1185 | 6 | item, "currency_code", item_path, currency, item_result |
1186 | 6 | ); |
1187 | 6 | optional_integer_range( |
1188 | 6 | item, "value_year", item_path, -9999, 9999, item_result |
1189 | 6 | ); |
1190 | 6 | require_boolean( |
1191 | 6 | item, "is_estimate", item_path, item_result |
1192 | 6 | ); |
1193 | 6 | optional_number_range( |
1194 | 6 | item, "confidence", item_path, 0.0, 1.0, item_result |
1195 | 6 | ); |
1196 | 6 | } |
1197 | 48 | ); |
1198 | | |
1199 | 48 | validate_optional_object_array( |
1200 | 48 | create, "work_concepts", path, result, |
1201 | 48 | [](const json& item, const std::string& item_path, |
1202 | 48 | validation_result& item_result) { |
1203 | 14 | reject_unknown_fields( |
1204 | 14 | item, item_path, |
1205 | 14 | { "local_id", "work_id", "concept_id", "relation_type", |
1206 | 14 | "centrality", "historical_role", "confidence", |
1207 | 14 | "evidence" }, |
1208 | 14 | item_result |
1209 | 14 | ); |
1210 | 14 | require_stable_id(item, "local_id", item_path, item_result); |
1211 | 28 | for (const std::string_view key : { "work_id", "concept_id" }) { |
1212 | 28 | const json* reference |
1213 | 28 | = field(item, key, item_path, item_result); |
1214 | 28 | if (reference != nullptr) { |
1215 | 28 | validate_stable_reference( |
1216 | 28 | *reference, child_path(item_path, key), item_result |
1217 | 28 | ); |
1218 | 28 | } |
1219 | 28 | } |
1220 | 14 | require_enum( |
1221 | 14 | item, "relation_type", item_path, |
1222 | 14 | { "exemplifies", "contains", "anticipates", |
1223 | 14 | "influenced_by", "influences", "revives", "parodies", |
1224 | 14 | "deconstructs", "associated_with" }, |
1225 | 14 | item_result |
1226 | 14 | ); |
1227 | 14 | require_integer_range( |
1228 | 14 | item, "centrality", item_path, 1, 100, item_result |
1229 | 14 | ); |
1230 | 14 | optional_enum( |
1231 | 14 | item, "historical_role", item_path, |
1232 | 14 | { "formative", "canonical", "transitional", "hybrid", |
1233 | 14 | "revival", "late_derivative", "peripheral", "precursor" }, |
1234 | 14 | item_result |
1235 | 14 | ); |
1236 | 14 | optional_number_range( |
1237 | 14 | item, "confidence", item_path, 0.0, 1.0, item_result |
1238 | 14 | ); |
1239 | 14 | validate_evidence_references( |
1240 | 14 | item, "evidence", item_path, item_result |
1241 | 14 | ); |
1242 | 14 | } |
1243 | 48 | ); |
1244 | | |
1245 | 48 | validate_optional_object_array( |
1246 | 48 | create, "concept_relations", path, result, |
1247 | 48 | [](const json& item, const std::string& item_path, |
1248 | 48 | validation_result& item_result) { |
1249 | 6 | reject_unknown_fields( |
1250 | 6 | item, item_path, |
1251 | 6 | { "local_id", "subject_concept_id", "relation_type", |
1252 | 6 | "object_concept_id", "strength", "from_year", "to_year", |
1253 | 6 | "region_code", "confidence", "evidence" }, |
1254 | 6 | item_result |
1255 | 6 | ); |
1256 | 6 | require_stable_id(item, "local_id", item_path, item_result); |
1257 | 6 | for (const std::string_view key : |
1258 | 12 | { "subject_concept_id", "object_concept_id" }) { |
1259 | 12 | const json* reference |
1260 | 12 | = field(item, key, item_path, item_result); |
1261 | 12 | if (reference != nullptr) { |
1262 | 12 | validate_stable_reference( |
1263 | 12 | *reference, child_path(item_path, key), item_result |
1264 | 12 | ); |
1265 | 12 | } |
1266 | 12 | } |
1267 | 6 | require_enum( |
1268 | 6 | item, "relation_type", item_path, |
1269 | 6 | { "broader_than", "narrower_than", "derived_from", |
1270 | 6 | "precursor_of", "hybrid_of", "revival_of", |
1271 | 6 | "regional_variant_of", "influenced_by", "opposes", |
1272 | 6 | "alias_of" }, |
1273 | 6 | item_result |
1274 | 6 | ); |
1275 | 6 | optional_integer_range( |
1276 | 6 | item, "strength", item_path, 1, 100, item_result |
1277 | 6 | ); |
1278 | 6 | validate_year_fields( |
1279 | 6 | item, item_path, { "from_year", "to_year" }, item_result |
1280 | 6 | ); |
1281 | 6 | optional_string(item, "region_code", item_path, item_result); |
1282 | 6 | optional_number_range( |
1283 | 6 | item, "confidence", item_path, 0.0, 1.0, item_result |
1284 | 6 | ); |
1285 | 6 | validate_evidence_references( |
1286 | 6 | item, "evidence", item_path, item_result |
1287 | 6 | ); |
1288 | 6 | } |
1289 | 48 | ); |
1290 | | |
1291 | 48 | validate_optional_object_array( |
1292 | 48 | create, "parent_guide_assertions", path, result, |
1293 | 48 | [](const json& item, const std::string& item_path, |
1294 | 48 | validation_result& item_result) { |
1295 | 6 | reject_unknown_fields( |
1296 | 6 | item, item_path, |
1297 | 6 | { "local_id", "work_id", "concept_id", "category", |
1298 | 6 | "intensity", "explicitness", "frequency", "centrality", |
1299 | 6 | "realism", "spoiler_level", "confidence", "evidence" }, |
1300 | 6 | item_result |
1301 | 6 | ); |
1302 | 6 | require_stable_id(item, "local_id", item_path, item_result); |
1303 | 12 | for (const std::string_view key : { "work_id", "concept_id" }) { |
1304 | 12 | const json* reference |
1305 | 12 | = field(item, key, item_path, item_result); |
1306 | 12 | if (reference != nullptr) { |
1307 | 12 | validate_stable_reference( |
1308 | 12 | *reference, child_path(item_path, key), item_result |
1309 | 12 | ); |
1310 | 12 | } |
1311 | 12 | } |
1312 | 6 | require_enum( |
1313 | 6 | item, "category", item_path, |
1314 | 6 | { "violence", "sex_nudity", "language", "drugs", |
1315 | 6 | "frightening", "self_harm", "discrimination", "abuse", |
1316 | 6 | "taboo" }, |
1317 | 6 | item_result |
1318 | 6 | ); |
1319 | 6 | for (const std::string_view key : |
1320 | 6 | { "intensity", "explicitness", "frequency", "centrality", |
1321 | 30 | "realism" }) { |
1322 | 30 | require_integer_range( |
1323 | 30 | item, key, item_path, 1, 5, item_result |
1324 | 30 | ); |
1325 | 30 | } |
1326 | 6 | require_enum( |
1327 | 6 | item, "spoiler_level", item_path, |
1328 | 6 | { "none", "mild", "major" }, item_result |
1329 | 6 | ); |
1330 | 6 | optional_number_range( |
1331 | 6 | item, "confidence", item_path, 0.0, 1.0, item_result |
1332 | 6 | ); |
1333 | 6 | validate_evidence_references( |
1334 | 6 | item, "evidence", item_path, item_result |
1335 | 6 | ); |
1336 | 6 | } |
1337 | 48 | ); |
1338 | 48 | } |
1339 | | |
1340 | | void validate_update_fields( |
1341 | | const json& item, const std::string_view item_path, |
1342 | | const std::string_view family, |
1343 | | const std::initializer_list<std::string_view> set_fields, |
1344 | | const std::initializer_list<std::string_view> unset_fields, |
1345 | | validation_result& result |
1346 | 11 | ) { |
1347 | 11 | reject_unknown_fields( |
1348 | 11 | item, item_path, { "id", "set", "unset" }, result |
1349 | 11 | ); |
1350 | 11 | validate_entity_id(item, "id", item_path, family, result); |
1351 | 11 | const json* set = require_object(item, "set", item_path, result); |
1352 | 11 | const json* unset = require_array(item, "unset", item_path, result); |
1353 | 11 | if (set != nullptr) { |
1354 | 11 | reject_unknown_fields(*set, child_path(item_path, "set"), |
1355 | 11 | set_fields, result); |
1356 | 11 | } |
1357 | 11 | if (unset != nullptr) { |
1358 | 11 | const std::string unset_path = child_path(item_path, "unset"); |
1359 | 11 | validate_unique_items(*unset, unset_path, result); |
1360 | 18 | for (std::size_t index = 0; index < unset->size(); ++index) { |
1361 | 7 | const json& value = (*unset)[index]; |
1362 | 7 | if (!value.is_string() |
1363 | 7 | || std::ranges::find( |
1364 | 7 | unset_fields, |
1365 | 7 | std::string_view( |
1366 | 7 | value.is_string() |
1367 | 7 | ? value.get_ref<const std::string&>() |
1368 | 7 | : std::string {} |
1369 | 7 | ) |
1370 | 7 | ) |
1371 | 7 | == unset_fields.end()) { |
1372 | 0 | add(result, unset_path + "/" + std::to_string(index), |
1373 | 0 | "enum", "field cannot be removed from this entity"); |
1374 | 0 | } |
1375 | 7 | } |
1376 | 11 | } |
1377 | 11 | if (set != nullptr && unset != nullptr && set->empty() |
1378 | 11 | && unset->empty()) { |
1379 | 0 | add(result, std::string(item_path), "min_operations", |
1380 | 0 | "update must set or unset at least one field"); |
1381 | 0 | } |
1382 | 11 | } |
1383 | | |
1384 | | void validate_work_set( |
1385 | | const json& set, const std::string_view path, |
1386 | | validation_result& result |
1387 | 10 | ) { |
1388 | 10 | optional_enum( |
1389 | 10 | set, "medium", path, |
1390 | 10 | { "film", "short_film", "television", "novel", "novella", |
1391 | 10 | "short_story", "poetry", "play", "essay", "album", "single", |
1392 | 10 | "composition", "painting", "print", "engraving", "drawing", |
1393 | 10 | "sculpture", "installation", "photography", "mixed_media" }, |
1394 | 10 | result |
1395 | 10 | ); |
1396 | 10 | validate_year_fields(set, path, { "year_start", "year_end" }, result); |
1397 | 10 | optional_enum( |
1398 | 10 | set, "date_precision", path, |
1399 | 10 | { "year", "decade", "approximate", "range", "exact" }, result |
1400 | 10 | ); |
1401 | 10 | validate_nonempty_optional_strings( |
1402 | 10 | set, path, |
1403 | 10 | { "date_start_text", "date_end_text", "date_qualifier", |
1404 | 10 | "language_code", "country_code", "production_info_json" }, |
1405 | 10 | result |
1406 | 10 | ); |
1407 | 10 | } |
1408 | | |
1409 | | void validate_update_operations( |
1410 | | const json& update, const std::string_view path, |
1411 | | validation_result& result |
1412 | 48 | ) { |
1413 | 48 | reject_unknown_fields( |
1414 | 48 | update, path, |
1415 | 48 | { "agents", "works", "concepts", "manifestations", "sources", |
1416 | 48 | "delete" }, |
1417 | 48 | result |
1418 | 48 | ); |
1419 | 48 | validate_optional_object_array( |
1420 | 48 | update, "agents", path, result, |
1421 | 48 | [](const json& item, const std::string& item_path, |
1422 | 48 | validation_result& item_result) { |
1423 | 0 | validate_update_fields( |
1424 | 0 | item, item_path, "agent", |
1425 | 0 | { "birth_year", "death_year" }, |
1426 | 0 | { "birth_year", "death_year" }, item_result |
1427 | 0 | ); |
1428 | 0 | if (const json* set = item.find("set") != item.end() |
1429 | 0 | && item["set"].is_object() |
1430 | 0 | ? &item["set"] |
1431 | 0 | : nullptr) { |
1432 | 0 | validate_year_fields( |
1433 | 0 | *set, child_path(item_path, "set"), |
1434 | 0 | { "birth_year", "death_year" }, item_result |
1435 | 0 | ); |
1436 | 0 | } |
1437 | 0 | } |
1438 | 48 | ); |
1439 | 48 | validate_optional_object_array( |
1440 | 48 | update, "works", path, result, |
1441 | 48 | [](const json& item, const std::string& item_path, |
1442 | 48 | validation_result& item_result) { |
1443 | 10 | validate_update_fields( |
1444 | 10 | item, item_path, "work", |
1445 | 10 | { "medium", "year_start", "year_end", "date_precision", |
1446 | 10 | "date_start_text", "date_end_text", "date_qualifier", |
1447 | 10 | "language_code", "country_code", |
1448 | 10 | "production_info_json" }, |
1449 | 10 | { "year_start", "year_end", "date_precision", |
1450 | 10 | "date_start_text", "date_end_text", "date_qualifier", |
1451 | 10 | "language_code", "country_code", |
1452 | 10 | "production_info_json" }, |
1453 | 10 | item_result |
1454 | 10 | ); |
1455 | 10 | if (item.contains("set") && item["set"].is_object()) { |
1456 | 10 | validate_work_set( |
1457 | 10 | item["set"], child_path(item_path, "set"), item_result |
1458 | 10 | ); |
1459 | 10 | } |
1460 | 10 | } |
1461 | 48 | ); |
1462 | 48 | validate_optional_object_array( |
1463 | 48 | update, "concepts", path, result, |
1464 | 48 | [](const json& item, const std::string& item_path, |
1465 | 48 | validation_result& item_result) { |
1466 | 1 | static const std::regex slug( |
1467 | 1 | R"(^[a-z0-9]+(?:-[a-z0-9]+)*$)" |
1468 | 1 | ); |
1469 | 1 | validate_update_fields( |
1470 | 1 | item, item_path, "concept", { "concept_type", "slug" }, |
1471 | 1 | {}, item_result |
1472 | 1 | ); |
1473 | 1 | if (item.contains("unset") && item["unset"].is_array() |
1474 | 1 | && !item["unset"].empty()) { |
1475 | 0 | add(item_result, child_path(item_path, "unset"), |
1476 | 0 | "max_items", "concept fields cannot be unset"); |
1477 | 0 | } |
1478 | 1 | if (item.contains("set") && item["set"].is_object()) { |
1479 | 1 | const json& set = item["set"]; |
1480 | 1 | if (set.empty()) { |
1481 | 0 | add(item_result, child_path(item_path, "set"), |
1482 | 0 | "min_properties", |
1483 | 0 | "concept update must set at least one field"); |
1484 | 0 | } |
1485 | 1 | optional_enum( |
1486 | 1 | set, "concept_type", child_path(item_path, "set"), |
1487 | 1 | { "genre", "style", "theme", "keyword", "motif", |
1488 | 1 | "trope", "phobia", "taboo", "technique", "movement", |
1489 | 1 | "setting", "mood", "content_warning" }, |
1490 | 1 | item_result |
1491 | 1 | ); |
1492 | 1 | optional_pattern( |
1493 | 1 | set, "slug", child_path(item_path, "set"), slug, |
1494 | 1 | item_result |
1495 | 1 | ); |
1496 | 1 | } |
1497 | 1 | } |
1498 | 48 | ); |
1499 | 48 | validate_optional_object_array( |
1500 | 48 | update, "manifestations", path, result, |
1501 | 48 | [](const json& item, const std::string& item_path, |
1502 | 48 | validation_result& item_result) { |
1503 | 0 | validate_update_fields( |
1504 | 0 | item, item_path, "manifestation", |
1505 | 0 | { "work_id", "manifestation_type", "release_year", |
1506 | 0 | "region_code", "language_code", "label" }, |
1507 | 0 | { "release_year", "region_code", "language_code" }, |
1508 | 0 | item_result |
1509 | 0 | ); |
1510 | 0 | if (item.contains("set") && item["set"].is_object()) { |
1511 | 0 | const json& set = item["set"]; |
1512 | 0 | const std::string set_path = child_path(item_path, "set"); |
1513 | 0 | static const std::regex work_id(R"(^work-[0-9]{6,}$)"); |
1514 | 0 | optional_pattern( |
1515 | 0 | set, "work_id", set_path, work_id, item_result |
1516 | 0 | ); |
1517 | 0 | optional_enum( |
1518 | 0 | set, "manifestation_type", set_path, |
1519 | 0 | { "edition", "translation", "release", "pressing", |
1520 | 0 | "cut", "restoration", "reissue" }, |
1521 | 0 | item_result |
1522 | 0 | ); |
1523 | 0 | optional_integer_range( |
1524 | 0 | set, "release_year", set_path, -9999, 9999, item_result |
1525 | 0 | ); |
1526 | 0 | validate_nonempty_optional_strings( |
1527 | 0 | set, set_path, |
1528 | 0 | { "region_code", "language_code", "label" }, |
1529 | 0 | item_result |
1530 | 0 | ); |
1531 | 0 | } |
1532 | 0 | } |
1533 | 48 | ); |
1534 | 48 | validate_optional_object_array( |
1535 | 48 | update, "sources", path, result, |
1536 | 48 | [](const json& item, const std::string& item_path, |
1537 | 48 | validation_result& item_result) { |
1538 | 7 | reject_unknown_fields( |
1539 | 7 | item, item_path, { "id", "set", "unset" }, item_result |
1540 | 7 | ); |
1541 | 7 | require_integer_range( |
1542 | 7 | item, "id", item_path, 1, |
1543 | 7 | std::numeric_limits<std::int64_t>::max(), item_result |
1544 | 7 | ); |
1545 | 7 | const json* set |
1546 | 7 | = require_object(item, "set", item_path, item_result); |
1547 | 7 | const json* unset |
1548 | 7 | = require_array(item, "unset", item_path, item_result); |
1549 | 7 | const std::string set_path = child_path(item_path, "set"); |
1550 | 7 | if (set != nullptr) { |
1551 | 7 | reject_unknown_fields( |
1552 | 7 | *set, set_path, |
1553 | 7 | { "source_type", "title", "bibliography_text", |
1554 | 7 | "author_text", "publisher", "publication_date", "url", |
1555 | 7 | "doi", "isbn", "language_code" }, |
1556 | 7 | item_result |
1557 | 7 | ); |
1558 | 7 | optional_enum( |
1559 | 7 | *set, "source_type", set_path, |
1560 | 7 | { "book", "article", "catalogue", "web_page", |
1561 | 7 | "interview", "database", "video", "audio", "PDF" }, |
1562 | 7 | item_result |
1563 | 7 | ); |
1564 | 7 | validate_nonempty_optional_strings( |
1565 | 7 | *set, set_path, |
1566 | 7 | { "title", "bibliography_text", "author_text", |
1567 | 7 | "publisher", "publication_date", "url", "doi", |
1568 | 7 | "isbn", "language_code" }, |
1569 | 7 | item_result |
1570 | 7 | ); |
1571 | 7 | } |
1572 | 7 | if (unset != nullptr) { |
1573 | 7 | const std::string unset_path |
1574 | 7 | = child_path(item_path, "unset"); |
1575 | 7 | validate_unique_items(*unset, unset_path, item_result); |
1576 | 13 | for (std::size_t index = 0; index < unset->size(); |
1577 | 7 | ++index) { |
1578 | 6 | const json& value = (*unset)[index]; |
1579 | 6 | constexpr std::array<std::string_view, 9> choices { |
1580 | 6 | "title", "bibliography_text", "author_text", |
1581 | 6 | "publisher", "publication_date", "url", "doi", |
1582 | 6 | "isbn", "language_code" |
1583 | 6 | }; |
1584 | 6 | if (!value.is_string() |
1585 | 6 | || std::ranges::find( |
1586 | 6 | choices, |
1587 | 6 | std::string_view( |
1588 | 6 | value.is_string() |
1589 | 6 | ? value.get_ref<const std::string&>() |
1590 | 6 | : std::string {} |
1591 | 6 | ) |
1592 | 6 | ) |
1593 | 6 | == choices.end()) { |
1594 | 0 | add(item_result, |
1595 | 0 | unset_path + "/" + std::to_string(index), |
1596 | 0 | "enum", |
1597 | 0 | "field cannot be removed from a source"); |
1598 | 0 | } |
1599 | 6 | } |
1600 | 7 | } |
1601 | 7 | if (set != nullptr && unset != nullptr && set->empty() |
1602 | 7 | && unset->empty()) { |
1603 | 0 | add(item_result, item_path, "min_operations", |
1604 | 0 | "update must set or unset at least one field"); |
1605 | 0 | } |
1606 | 7 | } |
1607 | 48 | ); |
1608 | 48 | if (const json* deletes |
1609 | 48 | = optional_object(update, "delete", path, result)) { |
1610 | 12 | const std::string delete_path = child_path(path, "delete"); |
1611 | 12 | reject_unknown_fields( |
1612 | 12 | *deletes, delete_path, |
1613 | 12 | { "names", "external_ids", "credits", "measurements", |
1614 | 12 | "financial_facts", "evidence", "work_concepts", |
1615 | 12 | "concept_relations", "parent_guide_assertions", |
1616 | 12 | "ingest_issues" }, |
1617 | 12 | result |
1618 | 12 | ); |
1619 | 12 | for (const std::string_view key : |
1620 | 12 | { "names", "external_ids", "credits", "measurements", |
1621 | 12 | "financial_facts", "evidence", "work_concepts", |
1622 | 108 | "concept_relations", "parent_guide_assertions" }) { |
1623 | 108 | validate_positive_id_array( |
1624 | 108 | *deletes, key, delete_path, result |
1625 | 108 | ); |
1626 | 108 | } |
1627 | 12 | if (const json* issues = optional_array( |
1628 | 12 | *deletes, "ingest_issues", delete_path, result |
1629 | 12 | )) { |
1630 | 2 | const std::string issues_path |
1631 | 2 | = child_path(delete_path, "ingest_issues"); |
1632 | 2 | validate_unique_items(*issues, issues_path, result); |
1633 | 4 | for (std::size_t index = 0; index < issues->size(); ++index) { |
1634 | 2 | const json& issue = (*issues)[index]; |
1635 | 2 | const std::string issue_path |
1636 | 2 | = issues_path + "/" + std::to_string(index); |
1637 | 2 | if (!issue.is_object()) { |
1638 | 0 | add(result, issue_path, "type", |
1639 | 0 | "expected an ingest issue key object"); |
1640 | 0 | continue; |
1641 | 0 | } |
1642 | 2 | reject_unknown_fields( |
1643 | 2 | issue, issue_path, |
1644 | 2 | { "batch_id", "code", "json_path" }, result |
1645 | 2 | ); |
1646 | 2 | require_stable_id( |
1647 | 2 | issue, "batch_id", issue_path, result |
1648 | 2 | ); |
1649 | 2 | require_string(issue, "code", issue_path, result); |
1650 | 2 | if (const json* json_path = require_string( |
1651 | 2 | issue, "json_path", issue_path, result |
1652 | 2 | ); |
1653 | 2 | json_path != nullptr |
1654 | 2 | && !json_path->get_ref<const std::string&>() |
1655 | 2 | .starts_with('/')) { |
1656 | 0 | add( |
1657 | 0 | result, child_path(issue_path, "json_path"), |
1658 | 0 | "pattern", "JSON Pointer must start with '/'" |
1659 | 0 | ); |
1660 | 0 | } |
1661 | 2 | } |
1662 | 2 | } |
1663 | 12 | } |
1664 | 48 | } |
1665 | | |
1666 | | void validate_merge_operations( |
1667 | | const json& merge, const std::string_view path, |
1668 | | validation_result& result |
1669 | 48 | ) { |
1670 | 48 | reject_unknown_fields( |
1671 | 48 | merge, path, { "agents", "works", "concepts" }, result |
1672 | 48 | ); |
1673 | 48 | const auto validate_merge = [&merge, path, &result]( |
1674 | 48 | const std::string_view key, |
1675 | 48 | const std::string_view family, |
1676 | 48 | const auto& validate_set, |
1677 | 48 | const auto set_fields, |
1678 | 48 | const auto unset_fields, |
1679 | 144 | const bool unset_forbidden) { |
1680 | 144 | validate_optional_object_array( |
1681 | 144 | merge, key, path, result, |
1682 | 144 | [family, &validate_set, set_fields, unset_fields, |
1683 | 144 | unset_forbidden]( |
1684 | 144 | const json& item, const std::string& item_path, |
1685 | 144 | validation_result& item_result |
1686 | 144 | ) { |
1687 | 11 | reject_unknown_fields( |
1688 | 11 | item, item_path, |
1689 | 11 | { "target", "members", "set", "unset" }, item_result |
1690 | 11 | ); |
1691 | 11 | validate_entity_id( |
1692 | 11 | item, "target", item_path, family, item_result |
1693 | 11 | ); |
1694 | 11 | const json* members |
1695 | 11 | = require_array(item, "members", item_path, item_result); |
1696 | 11 | const json* set |
1697 | 11 | = require_object(item, "set", item_path, item_result); |
1698 | 11 | const json* unset |
1699 | 11 | = require_array(item, "unset", item_path, item_result); |
1700 | 11 | if (members != nullptr) { |
1701 | 11 | const std::string members_path |
1702 | 11 | = child_path(item_path, "members"); |
1703 | 11 | if (members->empty()) { |
1704 | 0 | add(item_result, members_path, "min_items", |
1705 | 0 | "merge requires at least one member"); |
1706 | 0 | } |
1707 | 11 | validate_unique_items( |
1708 | 11 | *members, members_path, item_result |
1709 | 11 | ); |
1710 | 11 | const std::regex identifier( |
1711 | 11 | "^" + std::string(family) + R"(-[0-9]{6,}$)" |
1712 | 11 | ); |
1713 | 28 | for (std::size_t index = 0; index < members->size(); |
1714 | 17 | ++index) { |
1715 | 17 | const json& value = (*members)[index]; |
1716 | 17 | if (!value.is_string() |
1717 | 17 | || !std::regex_match( |
1718 | 17 | value.is_string() |
1719 | 17 | ? value.get_ref<const std::string&>() |
1720 | 17 | : std::string {}, |
1721 | 17 | identifier |
1722 | 17 | )) { |
1723 | 0 | add(item_result, |
1724 | 0 | members_path + "/" |
1725 | 0 | + std::to_string(index), |
1726 | 0 | "pattern", |
1727 | 0 | "member uses the wrong canonical family"); |
1728 | 0 | } |
1729 | 17 | } |
1730 | 11 | } |
1731 | 11 | if (set != nullptr) { |
1732 | 11 | const std::string set_path |
1733 | 11 | = child_path(item_path, "set"); |
1734 | 11 | reject_unknown_fields( |
1735 | 11 | *set, set_path, set_fields, item_result |
1736 | 11 | ); |
1737 | 11 | validate_set(*set, set_path, item_result); |
1738 | 11 | } |
1739 | 11 | if (unset != nullptr) { |
1740 | 11 | const std::string unset_path |
1741 | 11 | = child_path(item_path, "unset"); |
1742 | 11 | validate_unique_items(*unset, unset_path, item_result); |
1743 | 11 | if (unset_forbidden && !unset->empty()) { |
1744 | 0 | add(item_result, unset_path, "max_items", |
1745 | 0 | "fields cannot be unset for this family"); |
1746 | 0 | } |
1747 | 17 | for (std::size_t index = 0; index < unset->size(); |
1748 | 11 | ++index) { |
1749 | 6 | const json& value = (*unset)[index]; |
1750 | 6 | if (!unset_forbidden |
1751 | 6 | && (!value.is_string() |
1752 | 6 | || std::ranges::find( |
1753 | 6 | unset_fields, |
1754 | 6 | std::string_view( |
1755 | 6 | value.is_string() |
1756 | 6 | ? value.get_ref< |
1757 | 6 | const std::string&>() |
1758 | 6 | : std::string {} |
1759 | 6 | ) |
1760 | 6 | ) |
1761 | 6 | == unset_fields.end())) { |
1762 | 0 | add(item_result, |
1763 | 0 | unset_path + "/" |
1764 | 0 | + std::to_string(index), |
1765 | 0 | "enum", |
1766 | 0 | "field cannot be removed during merge"); |
1767 | 0 | } |
1768 | 6 | } |
1769 | 11 | } |
1770 | 11 | } contracts.cpp:_ZZZN12arachnespace9contracts12_GLOBAL__N_125validate_merge_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS3_14adl_serializerES6_IhSaIhEEvEESt17basic_string_viewIcSA_ERNS0_17validation_resultEENK3$_0clIZNS1_25validate_merge_operationsESI_SK_SM_E3$_1St16initializer_listISK_ESR_EEDaSK_SK_RKT_T0_T1_bENKUlSI_RKSC_SM_E_clESI_SY_SM_ Line | Count | Source | 1686 | 9 | ) { | 1687 | 9 | reject_unknown_fields( | 1688 | 9 | item, item_path, | 1689 | 9 | { "target", "members", "set", "unset" }, item_result | 1690 | 9 | ); | 1691 | 9 | validate_entity_id( | 1692 | 9 | item, "target", item_path, family, item_result | 1693 | 9 | ); | 1694 | 9 | const json* members | 1695 | 9 | = require_array(item, "members", item_path, item_result); | 1696 | 9 | const json* set | 1697 | 9 | = require_object(item, "set", item_path, item_result); | 1698 | 9 | const json* unset | 1699 | 9 | = require_array(item, "unset", item_path, item_result); | 1700 | 9 | if (members != nullptr) { | 1701 | 9 | const std::string members_path | 1702 | 9 | = child_path(item_path, "members"); | 1703 | 9 | if (members->empty()) { | 1704 | 0 | add(item_result, members_path, "min_items", | 1705 | 0 | "merge requires at least one member"); | 1706 | 0 | } | 1707 | 9 | validate_unique_items( | 1708 | 9 | *members, members_path, item_result | 1709 | 9 | ); | 1710 | 9 | const std::regex identifier( | 1711 | 9 | "^" + std::string(family) + R"(-[0-9]{6,}$)" | 1712 | 9 | ); | 1713 | 24 | for (std::size_t index = 0; index < members->size(); | 1714 | 15 | ++index) { | 1715 | 15 | const json& value = (*members)[index]; | 1716 | 15 | if (!value.is_string() | 1717 | 15 | || !std::regex_match( | 1718 | 15 | value.is_string() | 1719 | 15 | ? value.get_ref<const std::string&>() | 1720 | 15 | : std::string {}, | 1721 | 15 | identifier | 1722 | 15 | )) { | 1723 | 0 | add(item_result, | 1724 | 0 | members_path + "/" | 1725 | 0 | + std::to_string(index), | 1726 | 0 | "pattern", | 1727 | 0 | "member uses the wrong canonical family"); | 1728 | 0 | } | 1729 | 15 | } | 1730 | 9 | } | 1731 | 9 | if (set != nullptr) { | 1732 | 9 | const std::string set_path | 1733 | 9 | = child_path(item_path, "set"); | 1734 | 9 | reject_unknown_fields( | 1735 | 9 | *set, set_path, set_fields, item_result | 1736 | 9 | ); | 1737 | 9 | validate_set(*set, set_path, item_result); | 1738 | 9 | } | 1739 | 9 | if (unset != nullptr) { | 1740 | 9 | const std::string unset_path | 1741 | 9 | = child_path(item_path, "unset"); | 1742 | 9 | validate_unique_items(*unset, unset_path, item_result); | 1743 | 9 | if (unset_forbidden && !unset->empty()) { | 1744 | 0 | add(item_result, unset_path, "max_items", | 1745 | 0 | "fields cannot be unset for this family"); | 1746 | 0 | } | 1747 | 15 | for (std::size_t index = 0; index < unset->size(); | 1748 | 9 | ++index) { | 1749 | 6 | const json& value = (*unset)[index]; | 1750 | 6 | if (!unset_forbidden | 1751 | 6 | && (!value.is_string() | 1752 | 6 | || std::ranges::find( | 1753 | 6 | unset_fields, | 1754 | 6 | std::string_view( | 1755 | 6 | value.is_string() | 1756 | 6 | ? value.get_ref< | 1757 | 6 | const std::string&>() | 1758 | 6 | : std::string {} | 1759 | 6 | ) | 1760 | 6 | ) | 1761 | 6 | == unset_fields.end())) { | 1762 | 0 | add(item_result, | 1763 | 0 | unset_path + "/" | 1764 | 0 | + std::to_string(index), | 1765 | 0 | "enum", | 1766 | 0 | "field cannot be removed during merge"); | 1767 | 0 | } | 1768 | 6 | } | 1769 | 9 | } | 1770 | 9 | } |
Unexecuted instantiation: contracts.cpp:_ZZZN12arachnespace9contracts12_GLOBAL__N_125validate_merge_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS3_14adl_serializerES6_IhSaIhEEvEESt17basic_string_viewIcSA_ERNS0_17validation_resultEENK3$_0clIZNS1_25validate_merge_operationsESI_SK_SM_E3$_2St16initializer_listISK_ESR_EEDaSK_SK_RKT_T0_T1_bENKUlSI_RKSC_SM_E_clESI_SY_SM_ contracts.cpp:_ZZZN12arachnespace9contracts12_GLOBAL__N_125validate_merge_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS3_14adl_serializerES6_IhSaIhEEvEESt17basic_string_viewIcSA_ERNS0_17validation_resultEENK3$_0clIZNS1_25validate_merge_operationsESI_SK_SM_E3$_3St16initializer_listISK_ESR_EEDaSK_SK_RKT_T0_T1_bENKUlSI_RKSC_SM_E_clESI_SY_SM_ Line | Count | Source | 1686 | 2 | ) { | 1687 | 2 | reject_unknown_fields( | 1688 | 2 | item, item_path, | 1689 | 2 | { "target", "members", "set", "unset" }, item_result | 1690 | 2 | ); | 1691 | 2 | validate_entity_id( | 1692 | 2 | item, "target", item_path, family, item_result | 1693 | 2 | ); | 1694 | 2 | const json* members | 1695 | 2 | = require_array(item, "members", item_path, item_result); | 1696 | 2 | const json* set | 1697 | 2 | = require_object(item, "set", item_path, item_result); | 1698 | 2 | const json* unset | 1699 | 2 | = require_array(item, "unset", item_path, item_result); | 1700 | 2 | if (members != nullptr) { | 1701 | 2 | const std::string members_path | 1702 | 2 | = child_path(item_path, "members"); | 1703 | 2 | if (members->empty()) { | 1704 | 0 | add(item_result, members_path, "min_items", | 1705 | 0 | "merge requires at least one member"); | 1706 | 0 | } | 1707 | 2 | validate_unique_items( | 1708 | 2 | *members, members_path, item_result | 1709 | 2 | ); | 1710 | 2 | const std::regex identifier( | 1711 | 2 | "^" + std::string(family) + R"(-[0-9]{6,}$)" | 1712 | 2 | ); | 1713 | 4 | for (std::size_t index = 0; index < members->size(); | 1714 | 2 | ++index) { | 1715 | 2 | const json& value = (*members)[index]; | 1716 | 2 | if (!value.is_string() | 1717 | 2 | || !std::regex_match( | 1718 | 2 | value.is_string() | 1719 | 2 | ? value.get_ref<const std::string&>() | 1720 | 2 | : std::string {}, | 1721 | 2 | identifier | 1722 | 2 | )) { | 1723 | 0 | add(item_result, | 1724 | 0 | members_path + "/" | 1725 | 0 | + std::to_string(index), | 1726 | 0 | "pattern", | 1727 | 0 | "member uses the wrong canonical family"); | 1728 | 0 | } | 1729 | 2 | } | 1730 | 2 | } | 1731 | 2 | if (set != nullptr) { | 1732 | 2 | const std::string set_path | 1733 | 2 | = child_path(item_path, "set"); | 1734 | 2 | reject_unknown_fields( | 1735 | 2 | *set, set_path, set_fields, item_result | 1736 | 2 | ); | 1737 | 2 | validate_set(*set, set_path, item_result); | 1738 | 2 | } | 1739 | 2 | if (unset != nullptr) { | 1740 | 2 | const std::string unset_path | 1741 | 2 | = child_path(item_path, "unset"); | 1742 | 2 | validate_unique_items(*unset, unset_path, item_result); | 1743 | 2 | if (unset_forbidden && !unset->empty()) { | 1744 | 0 | add(item_result, unset_path, "max_items", | 1745 | 0 | "fields cannot be unset for this family"); | 1746 | 0 | } | 1747 | 2 | for (std::size_t index = 0; index < unset->size(); | 1748 | 2 | ++index) { | 1749 | 0 | const json& value = (*unset)[index]; | 1750 | 0 | if (!unset_forbidden | 1751 | 0 | && (!value.is_string() | 1752 | 0 | || std::ranges::find( | 1753 | 0 | unset_fields, | 1754 | 0 | std::string_view( | 1755 | 0 | value.is_string() | 1756 | 0 | ? value.get_ref< | 1757 | 0 | const std::string&>() | 1758 | 0 | : std::string {} | 1759 | 0 | ) | 1760 | 0 | ) | 1761 | 0 | == unset_fields.end())) { | 1762 | 0 | add(item_result, | 1763 | 0 | unset_path + "/" | 1764 | 0 | + std::to_string(index), | 1765 | 0 | "enum", | 1766 | 0 | "field cannot be removed during merge"); | 1767 | 0 | } | 1768 | 0 | } | 1769 | 2 | } | 1770 | 2 | } |
|
1771 | 144 | ); |
1772 | 144 | }; contracts.cpp:_ZZN12arachnespace9contracts12_GLOBAL__N_125validate_merge_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS3_14adl_serializerES6_IhSaIhEEvEESt17basic_string_viewIcSA_ERNS0_17validation_resultEENK3$_0clIZNS1_25validate_merge_operationsESI_SK_SM_E3$_1St16initializer_listISK_ESR_EEDaSK_SK_RKT_T0_T1_b Line | Count | Source | 1679 | 48 | const bool unset_forbidden) { | 1680 | 48 | validate_optional_object_array( | 1681 | 48 | merge, key, path, result, | 1682 | 48 | [family, &validate_set, set_fields, unset_fields, | 1683 | 48 | unset_forbidden]( | 1684 | 48 | const json& item, const std::string& item_path, | 1685 | 48 | validation_result& item_result | 1686 | 48 | ) { | 1687 | 48 | reject_unknown_fields( | 1688 | 48 | item, item_path, | 1689 | 48 | { "target", "members", "set", "unset" }, item_result | 1690 | 48 | ); | 1691 | 48 | validate_entity_id( | 1692 | 48 | item, "target", item_path, family, item_result | 1693 | 48 | ); | 1694 | 48 | const json* members | 1695 | 48 | = require_array(item, "members", item_path, item_result); | 1696 | 48 | const json* set | 1697 | 48 | = require_object(item, "set", item_path, item_result); | 1698 | 48 | const json* unset | 1699 | 48 | = require_array(item, "unset", item_path, item_result); | 1700 | 48 | if (members != nullptr) { | 1701 | 48 | const std::string members_path | 1702 | 48 | = child_path(item_path, "members"); | 1703 | 48 | if (members->empty()) { | 1704 | 48 | add(item_result, members_path, "min_items", | 1705 | 48 | "merge requires at least one member"); | 1706 | 48 | } | 1707 | 48 | validate_unique_items( | 1708 | 48 | *members, members_path, item_result | 1709 | 48 | ); | 1710 | 48 | const std::regex identifier( | 1711 | 48 | "^" + std::string(family) + R"(-[0-9]{6,}$)" | 1712 | 48 | ); | 1713 | 48 | for (std::size_t index = 0; index < members->size(); | 1714 | 48 | ++index) { | 1715 | 48 | const json& value = (*members)[index]; | 1716 | 48 | if (!value.is_string() | 1717 | 48 | || !std::regex_match( | 1718 | 48 | value.is_string() | 1719 | 48 | ? value.get_ref<const std::string&>() | 1720 | 48 | : std::string {}, | 1721 | 48 | identifier | 1722 | 48 | )) { | 1723 | 48 | add(item_result, | 1724 | 48 | members_path + "/" | 1725 | 48 | + std::to_string(index), | 1726 | 48 | "pattern", | 1727 | 48 | "member uses the wrong canonical family"); | 1728 | 48 | } | 1729 | 48 | } | 1730 | 48 | } | 1731 | 48 | if (set != nullptr) { | 1732 | 48 | const std::string set_path | 1733 | 48 | = child_path(item_path, "set"); | 1734 | 48 | reject_unknown_fields( | 1735 | 48 | *set, set_path, set_fields, item_result | 1736 | 48 | ); | 1737 | 48 | validate_set(*set, set_path, item_result); | 1738 | 48 | } | 1739 | 48 | if (unset != nullptr) { | 1740 | 48 | const std::string unset_path | 1741 | 48 | = child_path(item_path, "unset"); | 1742 | 48 | validate_unique_items(*unset, unset_path, item_result); | 1743 | 48 | if (unset_forbidden && !unset->empty()) { | 1744 | 48 | add(item_result, unset_path, "max_items", | 1745 | 48 | "fields cannot be unset for this family"); | 1746 | 48 | } | 1747 | 48 | for (std::size_t index = 0; index < unset->size(); | 1748 | 48 | ++index) { | 1749 | 48 | const json& value = (*unset)[index]; | 1750 | 48 | if (!unset_forbidden | 1751 | 48 | && (!value.is_string() | 1752 | 48 | || std::ranges::find( | 1753 | 48 | unset_fields, | 1754 | 48 | std::string_view( | 1755 | 48 | value.is_string() | 1756 | 48 | ? value.get_ref< | 1757 | 48 | const std::string&>() | 1758 | 48 | : std::string {} | 1759 | 48 | ) | 1760 | 48 | ) | 1761 | 48 | == unset_fields.end())) { | 1762 | 48 | add(item_result, | 1763 | 48 | unset_path + "/" | 1764 | 48 | + std::to_string(index), | 1765 | 48 | "enum", | 1766 | 48 | "field cannot be removed during merge"); | 1767 | 48 | } | 1768 | 48 | } | 1769 | 48 | } | 1770 | 48 | } | 1771 | 48 | ); | 1772 | 48 | }; |
contracts.cpp:_ZZN12arachnespace9contracts12_GLOBAL__N_125validate_merge_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS3_14adl_serializerES6_IhSaIhEEvEESt17basic_string_viewIcSA_ERNS0_17validation_resultEENK3$_0clIZNS1_25validate_merge_operationsESI_SK_SM_E3$_2St16initializer_listISK_ESR_EEDaSK_SK_RKT_T0_T1_b Line | Count | Source | 1679 | 48 | const bool unset_forbidden) { | 1680 | 48 | validate_optional_object_array( | 1681 | 48 | merge, key, path, result, | 1682 | 48 | [family, &validate_set, set_fields, unset_fields, | 1683 | 48 | unset_forbidden]( | 1684 | 48 | const json& item, const std::string& item_path, | 1685 | 48 | validation_result& item_result | 1686 | 48 | ) { | 1687 | 48 | reject_unknown_fields( | 1688 | 48 | item, item_path, | 1689 | 48 | { "target", "members", "set", "unset" }, item_result | 1690 | 48 | ); | 1691 | 48 | validate_entity_id( | 1692 | 48 | item, "target", item_path, family, item_result | 1693 | 48 | ); | 1694 | 48 | const json* members | 1695 | 48 | = require_array(item, "members", item_path, item_result); | 1696 | 48 | const json* set | 1697 | 48 | = require_object(item, "set", item_path, item_result); | 1698 | 48 | const json* unset | 1699 | 48 | = require_array(item, "unset", item_path, item_result); | 1700 | 48 | if (members != nullptr) { | 1701 | 48 | const std::string members_path | 1702 | 48 | = child_path(item_path, "members"); | 1703 | 48 | if (members->empty()) { | 1704 | 48 | add(item_result, members_path, "min_items", | 1705 | 48 | "merge requires at least one member"); | 1706 | 48 | } | 1707 | 48 | validate_unique_items( | 1708 | 48 | *members, members_path, item_result | 1709 | 48 | ); | 1710 | 48 | const std::regex identifier( | 1711 | 48 | "^" + std::string(family) + R"(-[0-9]{6,}$)" | 1712 | 48 | ); | 1713 | 48 | for (std::size_t index = 0; index < members->size(); | 1714 | 48 | ++index) { | 1715 | 48 | const json& value = (*members)[index]; | 1716 | 48 | if (!value.is_string() | 1717 | 48 | || !std::regex_match( | 1718 | 48 | value.is_string() | 1719 | 48 | ? value.get_ref<const std::string&>() | 1720 | 48 | : std::string {}, | 1721 | 48 | identifier | 1722 | 48 | )) { | 1723 | 48 | add(item_result, | 1724 | 48 | members_path + "/" | 1725 | 48 | + std::to_string(index), | 1726 | 48 | "pattern", | 1727 | 48 | "member uses the wrong canonical family"); | 1728 | 48 | } | 1729 | 48 | } | 1730 | 48 | } | 1731 | 48 | if (set != nullptr) { | 1732 | 48 | const std::string set_path | 1733 | 48 | = child_path(item_path, "set"); | 1734 | 48 | reject_unknown_fields( | 1735 | 48 | *set, set_path, set_fields, item_result | 1736 | 48 | ); | 1737 | 48 | validate_set(*set, set_path, item_result); | 1738 | 48 | } | 1739 | 48 | if (unset != nullptr) { | 1740 | 48 | const std::string unset_path | 1741 | 48 | = child_path(item_path, "unset"); | 1742 | 48 | validate_unique_items(*unset, unset_path, item_result); | 1743 | 48 | if (unset_forbidden && !unset->empty()) { | 1744 | 48 | add(item_result, unset_path, "max_items", | 1745 | 48 | "fields cannot be unset for this family"); | 1746 | 48 | } | 1747 | 48 | for (std::size_t index = 0; index < unset->size(); | 1748 | 48 | ++index) { | 1749 | 48 | const json& value = (*unset)[index]; | 1750 | 48 | if (!unset_forbidden | 1751 | 48 | && (!value.is_string() | 1752 | 48 | || std::ranges::find( | 1753 | 48 | unset_fields, | 1754 | 48 | std::string_view( | 1755 | 48 | value.is_string() | 1756 | 48 | ? value.get_ref< | 1757 | 48 | const std::string&>() | 1758 | 48 | : std::string {} | 1759 | 48 | ) | 1760 | 48 | ) | 1761 | 48 | == unset_fields.end())) { | 1762 | 48 | add(item_result, | 1763 | 48 | unset_path + "/" | 1764 | 48 | + std::to_string(index), | 1765 | 48 | "enum", | 1766 | 48 | "field cannot be removed during merge"); | 1767 | 48 | } | 1768 | 48 | } | 1769 | 48 | } | 1770 | 48 | } | 1771 | 48 | ); | 1772 | 48 | }; |
contracts.cpp:_ZZN12arachnespace9contracts12_GLOBAL__N_125validate_merge_operationsERKN8nlohmann16json_abi_v3_11_310basic_jsonISt3mapSt6vectorNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEblmdSaNS3_14adl_serializerES6_IhSaIhEEvEESt17basic_string_viewIcSA_ERNS0_17validation_resultEENK3$_0clIZNS1_25validate_merge_operationsESI_SK_SM_E3$_3St16initializer_listISK_ESR_EEDaSK_SK_RKT_T0_T1_b Line | Count | Source | 1679 | 48 | const bool unset_forbidden) { | 1680 | 48 | validate_optional_object_array( | 1681 | 48 | merge, key, path, result, | 1682 | 48 | [family, &validate_set, set_fields, unset_fields, | 1683 | 48 | unset_forbidden]( | 1684 | 48 | const json& item, const std::string& item_path, | 1685 | 48 | validation_result& item_result | 1686 | 48 | ) { | 1687 | 48 | reject_unknown_fields( | 1688 | 48 | item, item_path, | 1689 | 48 | { "target", "members", "set", "unset" }, item_result | 1690 | 48 | ); | 1691 | 48 | validate_entity_id( | 1692 | 48 | item, "target", item_path, family, item_result | 1693 | 48 | ); | 1694 | 48 | const json* members | 1695 | 48 | = require_array(item, "members", item_path, item_result); | 1696 | 48 | const json* set | 1697 | 48 | = require_object(item, "set", item_path, item_result); | 1698 | 48 | const json* unset | 1699 | 48 | = require_array(item, "unset", item_path, item_result); | 1700 | 48 | if (members != nullptr) { | 1701 | 48 | const std::string members_path | 1702 | 48 | = child_path(item_path, "members"); | 1703 | 48 | if (members->empty()) { | 1704 | 48 | add(item_result, members_path, "min_items", | 1705 | 48 | "merge requires at least one member"); | 1706 | 48 | } | 1707 | 48 | validate_unique_items( | 1708 | 48 | *members, members_path, item_result | 1709 | 48 | ); | 1710 | 48 | const std::regex identifier( | 1711 | 48 | "^" + std::string(family) + R"(-[0-9]{6,}$)" | 1712 | 48 | ); | 1713 | 48 | for (std::size_t index = 0; index < members->size(); | 1714 | 48 | ++index) { | 1715 | 48 | const json& value = (*members)[index]; | 1716 | 48 | if (!value.is_string() | 1717 | 48 | || !std::regex_match( | 1718 | 48 | value.is_string() | 1719 | 48 | ? value.get_ref<const std::string&>() | 1720 | 48 | : std::string {}, | 1721 | 48 | identifier | 1722 | 48 | )) { | 1723 | 48 | add(item_result, | 1724 | 48 | members_path + "/" | 1725 | 48 | + std::to_string(index), | 1726 | 48 | "pattern", | 1727 | 48 | "member uses the wrong canonical family"); | 1728 | 48 | } | 1729 | 48 | } | 1730 | 48 | } | 1731 | 48 | if (set != nullptr) { | 1732 | 48 | const std::string set_path | 1733 | 48 | = child_path(item_path, "set"); | 1734 | 48 | reject_unknown_fields( | 1735 | 48 | *set, set_path, set_fields, item_result | 1736 | 48 | ); | 1737 | 48 | validate_set(*set, set_path, item_result); | 1738 | 48 | } | 1739 | 48 | if (unset != nullptr) { | 1740 | 48 | const std::string unset_path | 1741 | 48 | = child_path(item_path, "unset"); | 1742 | 48 | validate_unique_items(*unset, unset_path, item_result); | 1743 | 48 | if (unset_forbidden && !unset->empty()) { | 1744 | 48 | add(item_result, unset_path, "max_items", | 1745 | 48 | "fields cannot be unset for this family"); | 1746 | 48 | } | 1747 | 48 | for (std::size_t index = 0; index < unset->size(); | 1748 | 48 | ++index) { | 1749 | 48 | const json& value = (*unset)[index]; | 1750 | 48 | if (!unset_forbidden | 1751 | 48 | && (!value.is_string() | 1752 | 48 | || std::ranges::find( | 1753 | 48 | unset_fields, | 1754 | 48 | std::string_view( | 1755 | 48 | value.is_string() | 1756 | 48 | ? value.get_ref< | 1757 | 48 | const std::string&>() | 1758 | 48 | : std::string {} | 1759 | 48 | ) | 1760 | 48 | ) | 1761 | 48 | == unset_fields.end())) { | 1762 | 48 | add(item_result, | 1763 | 48 | unset_path + "/" | 1764 | 48 | + std::to_string(index), | 1765 | 48 | "enum", | 1766 | 48 | "field cannot be removed during merge"); | 1767 | 48 | } | 1768 | 48 | } | 1769 | 48 | } | 1770 | 48 | } | 1771 | 48 | ); | 1772 | 48 | }; |
|
1773 | | |
1774 | 48 | validate_merge( |
1775 | 48 | "agents", "agent", |
1776 | 48 | [](const json& set, const std::string& set_path, |
1777 | 48 | validation_result& item_result) { |
1778 | 9 | optional_enum( |
1779 | 9 | set, "agent_type", set_path, |
1780 | 9 | { "person", "organization", "group" }, item_result |
1781 | 9 | ); |
1782 | 9 | validate_year_fields( |
1783 | 9 | set, set_path, { "birth_year", "death_year" }, item_result |
1784 | 9 | ); |
1785 | 9 | }, |
1786 | 48 | std::initializer_list<std::string_view> { |
1787 | 48 | "agent_type", "birth_year", "death_year" |
1788 | 48 | }, |
1789 | 48 | std::initializer_list<std::string_view> { |
1790 | 48 | "birth_year", "death_year" |
1791 | 48 | }, |
1792 | 48 | false |
1793 | 48 | ); |
1794 | 48 | validate_merge( |
1795 | 48 | "works", "work", |
1796 | 48 | [](const json& set, const std::string& set_path, |
1797 | 48 | validation_result& item_result) { |
1798 | 0 | validate_work_set(set, set_path, item_result); |
1799 | 0 | }, |
1800 | 48 | std::initializer_list<std::string_view> { |
1801 | 48 | "medium", "year_start", "year_end", "date_precision", |
1802 | 48 | "date_start_text", "date_end_text", "date_qualifier", |
1803 | 48 | "language_code", "country_code", "production_info_json" |
1804 | 48 | }, |
1805 | 48 | std::initializer_list<std::string_view> { |
1806 | 48 | "year_start", "year_end", "date_precision", "date_start_text", |
1807 | 48 | "date_end_text", "date_qualifier", "language_code", |
1808 | 48 | "country_code", "production_info_json" |
1809 | 48 | }, |
1810 | 48 | false |
1811 | 48 | ); |
1812 | 48 | validate_merge( |
1813 | 48 | "concepts", "concept", |
1814 | 48 | [](const json& set, const std::string& set_path, |
1815 | 48 | validation_result& item_result) { |
1816 | 2 | static const std::regex slug( |
1817 | 2 | R"(^[a-z0-9]+(?:-[a-z0-9]+)*$)" |
1818 | 2 | ); |
1819 | 2 | optional_enum( |
1820 | 2 | set, "concept_type", set_path, |
1821 | 2 | { "genre", "style", "theme", "keyword", "motif", "trope", |
1822 | 2 | "phobia", "taboo", "technique", "movement", "setting", |
1823 | 2 | "mood", "content_warning" }, |
1824 | 2 | item_result |
1825 | 2 | ); |
1826 | 2 | optional_pattern( |
1827 | 2 | set, "slug", set_path, slug, item_result |
1828 | 2 | ); |
1829 | 2 | }, |
1830 | 48 | std::initializer_list<std::string_view> { "concept_type", "slug" }, |
1831 | 48 | std::initializer_list<std::string_view> {}, |
1832 | 48 | true |
1833 | 48 | ); |
1834 | 48 | } |
1835 | | |
1836 | | void |
1837 | 48 | validate_arachne_batch(const json& document, validation_result& result) { |
1838 | 48 | reject_unknown_fields( |
1839 | 48 | document, "", |
1840 | 48 | { "format", "batch_id", "create", "update", "merge" }, result |
1841 | 48 | ); |
1842 | 48 | const json* format = require_string(document, "format", "", result); |
1843 | 48 | if (format != nullptr && *format != "arachne_batch_v2") { |
1844 | 0 | add(result, "/format", "const", |
1845 | 0 | "format must be arachne_batch_v2"); |
1846 | 0 | } |
1847 | 48 | require_stable_id(document, "batch_id", "", result); |
1848 | 48 | if (const json* create |
1849 | 48 | = require_object(document, "create", "", result)) { |
1850 | 48 | validate_create_operations(*create, "/create", result); |
1851 | 48 | } |
1852 | 48 | if (const json* update |
1853 | 48 | = require_object(document, "update", "", result)) { |
1854 | 48 | validate_update_operations(*update, "/update", result); |
1855 | 48 | } |
1856 | 48 | if (const json* merge |
1857 | 48 | = require_object(document, "merge", "", result)) { |
1858 | 48 | validate_merge_operations(*merge, "/merge", result); |
1859 | 48 | } |
1860 | 48 | } |
1861 | | |
1862 | | void |
1863 | 5 | validate_batch_envelope(const json& document, validation_result& result) { |
1864 | 5 | reject_unknown_fields( |
1865 | 5 | document, "", |
1866 | 5 | { "contract", "format_version", "envelope_id", "payload_ref", |
1867 | 5 | "payload_sha256", "submission_ref", "status", "accepted_by", |
1868 | 5 | "supersedes", "extensions" }, |
1869 | 5 | result |
1870 | 5 | ); |
1871 | 5 | require_stable_id(document, "envelope_id", "", result); |
1872 | 5 | require_string(document, "payload_ref", "", result); |
1873 | 5 | require_sha256(document, "payload_sha256", "", result); |
1874 | 5 | require_string(document, "submission_ref", "", result); |
1875 | 5 | require_enum( |
1876 | 5 | document, "status", "", |
1877 | 5 | { "received", "needs_format_fix", "waiting_approval", "accepted", |
1878 | 5 | "waiting_processing", "processing", "integrated", "failed", |
1879 | 5 | "rejected", "superseded" }, |
1880 | 5 | result |
1881 | 5 | ); |
1882 | 5 | optional_string(document, "accepted_by", "", result); |
1883 | 5 | optional_string(document, "supersedes", "", result); |
1884 | 5 | validate_extensions(document, "", result); |
1885 | 5 | } |
1886 | | |
1887 | 3 | void validate_fetch_plan(const json& document, validation_result& result) { |
1888 | 3 | reject_unknown_fields( |
1889 | 3 | document, "", |
1890 | 3 | { "contract", "format_version", "plan_id", "source", "requests", |
1891 | 3 | "created_at", "extensions" }, |
1892 | 3 | result |
1893 | 3 | ); |
1894 | 3 | require_stable_id(document, "plan_id", "", result); |
1895 | 3 | require_string(document, "source", "", result); |
1896 | 3 | require_timestamp(document, "created_at", "", result); |
1897 | 3 | const json* requests = require_array(document, "requests", "", result); |
1898 | 3 | if (requests != nullptr && requests->empty()) { |
1899 | 0 | add(result, "/requests", "min_items", |
1900 | 0 | "a fetch plan must contain at least one request"); |
1901 | 0 | } |
1902 | 3 | if (requests != nullptr) { |
1903 | 4 | for (std::size_t index = 0; index < requests->size(); ++index) { |
1904 | 2 | const std::string path = "/requests/" + std::to_string(index); |
1905 | 2 | const json& request = (*requests)[index]; |
1906 | 2 | if (!request.is_object()) { |
1907 | 0 | add(result, path, "type", "expected a request object"); |
1908 | 0 | continue; |
1909 | 0 | } |
1910 | 2 | reject_unknown_fields( |
1911 | 2 | request, path, |
1912 | 2 | { "request_id", "locator", "purpose", "entities", "fields", |
1913 | 2 | "pages", "archives", "follow_up" }, |
1914 | 2 | result |
1915 | 2 | ); |
1916 | 2 | require_stable_id(request, "request_id", path, result); |
1917 | 2 | require_string(request, "locator", path, result); |
1918 | 2 | require_string(request, "purpose", path, result); |
1919 | 2 | } |
1920 | 2 | } |
1921 | 3 | validate_extensions(document, "", result); |
1922 | 3 | } |
1923 | | |
1924 | | void |
1925 | 8 | validate_fetch_request(const json& document, validation_result& result) { |
1926 | 8 | reject_unknown_fields( |
1927 | 8 | document, "", |
1928 | 8 | { "contract", "format_version", "request_id", |
1929 | 8 | "door_id", "endpoint_id", "operation", |
1930 | 8 | "freshness_policy", "idempotency_key", "plan_id", |
1931 | 8 | "locator", "method", "headers", |
1932 | 8 | "pagination", "retry", "expected", |
1933 | 8 | "redirect_policy", "output_ref", "body_artifact", |
1934 | 8 | "resume_artifact", "extensions" }, |
1935 | 8 | result |
1936 | 8 | ); |
1937 | 8 | require_stable_id(document, "request_id", "", result); |
1938 | 8 | if (document.contains("door_id")) { |
1939 | 7 | require_stable_id(document, "door_id", "", result); |
1940 | 7 | } |
1941 | 8 | if (document.contains("endpoint_id")) { |
1942 | 7 | require_stable_id(document, "endpoint_id", "", result); |
1943 | 7 | } |
1944 | 8 | if (document.contains("operation")) { |
1945 | 7 | require_enum( |
1946 | 7 | document, "operation", "", |
1947 | 7 | { "bulk_snapshot", "incremental_harvest", "point_lookup", |
1948 | 7 | "resume_download", "backend_read", "external_write" }, |
1949 | 7 | result |
1950 | 7 | ); |
1951 | 7 | } |
1952 | 8 | if (document.contains("freshness_policy")) { |
1953 | 7 | require_enum( |
1954 | 7 | document, "freshness_policy", "", |
1955 | 7 | { "fresh_required", "cache_allowed", "stale_allowed", |
1956 | 7 | "offline_only" }, |
1957 | 7 | result |
1958 | 7 | ); |
1959 | 7 | } |
1960 | 8 | optional_string(document, "idempotency_key", "", result); |
1961 | 8 | optional_string(document, "plan_id", "", result); |
1962 | 8 | require_string(document, "locator", "", result); |
1963 | 8 | require_enum(document, "method", "", { "GET", "POST" }, result); |
1964 | 8 | require_string(document, "output_ref", "", result); |
1965 | | |
1966 | 8 | optional_object(document, "headers", "", result); |
1967 | 8 | if (const json* pagination |
1968 | 8 | = optional_object(document, "pagination", "", result)) { |
1969 | 5 | reject_unknown_fields( |
1970 | 5 | *pagination, "/pagination", { "mode" }, result |
1971 | 5 | ); |
1972 | 5 | require_enum( |
1973 | 5 | *pagination, "mode", "/pagination", |
1974 | 5 | { "none", "link_header", "cursor", "page_number" }, result |
1975 | 5 | ); |
1976 | 5 | } |
1977 | 8 | if (const json* retry |
1978 | 8 | = optional_object(document, "retry", "", result)) { |
1979 | 6 | reject_unknown_fields( |
1980 | 6 | *retry, "/retry", |
1981 | 6 | { "maximum_attempts", "initial_delay_ms", "maximum_delay_ms", |
1982 | 6 | "total_delay_budget_ms", "respect_retry_after" }, |
1983 | 6 | result |
1984 | 6 | ); |
1985 | 6 | optional_bounded_integer( |
1986 | 6 | *retry, "maximum_attempts", "/retry", 1, 20, result |
1987 | 6 | ); |
1988 | 6 | optional_bounded_integer( |
1989 | 6 | *retry, "initial_delay_ms", "/retry", 0, 60'000, result |
1990 | 6 | ); |
1991 | 6 | optional_bounded_integer( |
1992 | 6 | *retry, "maximum_delay_ms", "/retry", 0, 3'600'000, result |
1993 | 6 | ); |
1994 | 6 | optional_bounded_integer( |
1995 | 6 | *retry, "total_delay_budget_ms", "/retry", 0, 3'600'000, result |
1996 | 6 | ); |
1997 | 6 | if (const auto value = retry->find("respect_retry_after"); |
1998 | 6 | value != retry->end() && !value->is_boolean()) { |
1999 | 0 | add(result, "/retry/respect_retry_after", "type", |
2000 | 0 | "expected a boolean"); |
2001 | 0 | } |
2002 | 6 | } |
2003 | 8 | if (const json* expected |
2004 | 8 | = optional_object(document, "expected", "", result)) { |
2005 | 6 | reject_unknown_fields( |
2006 | 6 | *expected, "/expected", |
2007 | 6 | { "maximum_bytes", "timeout_ms", "connect_timeout_ms", |
2008 | 6 | "read_timeout_ms", "write_timeout_ms", "sha256" }, |
2009 | 6 | result |
2010 | 6 | ); |
2011 | 6 | optional_bounded_integer( |
2012 | 6 | *expected, "maximum_bytes", "/expected", 1, |
2013 | 6 | 1'099'511'627'776ULL, result |
2014 | 6 | ); |
2015 | 6 | optional_bounded_integer( |
2016 | 6 | *expected, "timeout_ms", "/expected", 1, 86'400'000, result |
2017 | 6 | ); |
2018 | 6 | optional_bounded_integer( |
2019 | 6 | *expected, "connect_timeout_ms", "/expected", 1, 86'400'000, |
2020 | 6 | result |
2021 | 6 | ); |
2022 | 6 | optional_bounded_integer( |
2023 | 6 | *expected, "read_timeout_ms", "/expected", 1, 86'400'000, result |
2024 | 6 | ); |
2025 | 6 | optional_bounded_integer( |
2026 | 6 | *expected, "write_timeout_ms", "/expected", 1, 86'400'000, |
2027 | 6 | result |
2028 | 6 | ); |
2029 | 6 | if (expected->contains("sha256")) { |
2030 | 1 | require_sha256(*expected, "sha256", "/expected", result); |
2031 | 1 | } |
2032 | 6 | } |
2033 | 8 | if (const json* redirect |
2034 | 8 | = optional_object(document, "redirect_policy", "", result)) { |
2035 | 7 | reject_unknown_fields( |
2036 | 7 | *redirect, "/redirect_policy", |
2037 | 7 | { "follow", "maximum_redirects", "allow_https_to_http", |
2038 | 7 | "allowed_hosts" }, |
2039 | 7 | result |
2040 | 7 | ); |
2041 | 7 | for (const std::string_view key : |
2042 | 14 | { "follow", "allow_https_to_http" }) { |
2043 | 14 | const json* value |
2044 | 14 | = field(*redirect, key, "/redirect_policy", result); |
2045 | 14 | if (value != nullptr && !value->is_boolean()) { |
2046 | 0 | add(result, child_path("/redirect_policy", key), "type", |
2047 | 0 | "expected a boolean"); |
2048 | 0 | } |
2049 | 14 | } |
2050 | 7 | field(*redirect, "maximum_redirects", "/redirect_policy", result); |
2051 | 7 | optional_bounded_integer( |
2052 | 7 | *redirect, "maximum_redirects", "/redirect_policy", 0, 20, |
2053 | 7 | result |
2054 | 7 | ); |
2055 | 7 | validate_string_array( |
2056 | 7 | *redirect, "allowed_hosts", "/redirect_policy", true, result |
2057 | 7 | ); |
2058 | 7 | const auto hosts = redirect->find("allowed_hosts"); |
2059 | 7 | if (hosts != redirect->end() && hosts->is_array()) { |
2060 | 7 | static const std::regex host_expression( |
2061 | 7 | R"(^[A-Za-z0-9.-]+(:[0-9]{1,5})?$)" |
2062 | 7 | ); |
2063 | 7 | if (hosts->size() > 128) { |
2064 | 0 | add(result, "/redirect_policy/allowed_hosts", "max_items", |
2065 | 0 | "at most 128 allowed hosts may be declared"); |
2066 | 0 | } |
2067 | 7 | std::set<std::string> seen; |
2068 | 14 | for (std::size_t index = 0; index < hosts->size(); ++index) { |
2069 | 7 | const json& host = (*hosts)[index]; |
2070 | 7 | if (!host.is_string() || host.empty()) { |
2071 | 0 | continue; |
2072 | 0 | } |
2073 | 7 | const std::string& value |
2074 | 7 | = host.get_ref<const std::string&>(); |
2075 | 7 | const std::string path = "/redirect_policy/allowed_hosts/" |
2076 | 7 | + std::to_string(index); |
2077 | 7 | if (value.size() > 253 |
2078 | 7 | || !std::regex_match(value, host_expression)) { |
2079 | 1 | add(result, path, "host", |
2080 | 1 | "expected a host or host:port without scheme or " |
2081 | 1 | "path"); |
2082 | 1 | } |
2083 | 7 | if (!seen.insert(value).second) { |
2084 | 0 | add(result, path, "unique", |
2085 | 0 | "allowed hosts must be unique"); |
2086 | 0 | } |
2087 | 7 | } |
2088 | 7 | } |
2089 | 7 | } |
2090 | 8 | if (const json* body |
2091 | 8 | = optional_object(document, "body_artifact", "", result)) { |
2092 | 7 | validate_artifact(*body, "/body_artifact", result); |
2093 | 7 | } |
2094 | 8 | if (const json* partial |
2095 | 8 | = optional_object(document, "resume_artifact", "", result)) { |
2096 | 0 | validate_artifact(*partial, "/resume_artifact", result); |
2097 | 0 | } |
2098 | 8 | const bool resume_operation |
2099 | 8 | = document.value("operation", "point_lookup") == "resume_download"; |
2100 | 8 | if (resume_operation && !document.contains("resume_artifact")) { |
2101 | 0 | add(result, "/resume_artifact", "required", |
2102 | 0 | "resume_download requires a partial artifact"); |
2103 | 8 | } else if (!resume_operation && document.contains("resume_artifact")) { |
2104 | 0 | add(result, "/resume_artifact", "operation", |
2105 | 0 | "resume_artifact is reserved for resume_download"); |
2106 | 0 | } |
2107 | 8 | validate_extensions(document, "", result); |
2108 | 8 | } |
2109 | | |
2110 | | void validate_acquired_artifact( |
2111 | | const json& document, validation_result& result |
2112 | 11 | ) { |
2113 | 11 | reject_unknown_fields( |
2114 | 11 | document, "", |
2115 | 11 | { "contract", "format_version", "artifact_id", "request_id", |
2116 | 11 | "door_id", "operation", "source_locator", "artifact", "transport", |
2117 | 11 | "response_metadata", "acquired_at", "extensions" }, |
2118 | 11 | result |
2119 | 11 | ); |
2120 | 11 | require_stable_id(document, "artifact_id", "", result); |
2121 | 11 | require_stable_id(document, "request_id", "", result); |
2122 | 11 | if (document.contains("door_id")) { |
2123 | 8 | require_stable_id(document, "door_id", "", result); |
2124 | 8 | } |
2125 | 11 | if (document.contains("operation")) { |
2126 | 11 | require_enum( |
2127 | 11 | document, "operation", "", |
2128 | 11 | { "bulk_snapshot", "incremental_harvest", "point_lookup", |
2129 | 11 | "resume_download", "backend_read", "external_write" }, |
2130 | 11 | result |
2131 | 11 | ); |
2132 | 11 | } |
2133 | 11 | require_string(document, "source_locator", "", result); |
2134 | 11 | require_timestamp(document, "acquired_at", "", result); |
2135 | 11 | const json* transport |
2136 | 11 | = require_object(document, "transport", "", result); |
2137 | 11 | bool delivered = false; |
2138 | 11 | if (transport != nullptr) { |
2139 | 11 | reject_unknown_fields( |
2140 | 11 | *transport, "/transport", |
2141 | 11 | { "status", "attempts", "delivery_mode", "retry_after_ms", |
2142 | 11 | "error_code", "error_message" }, |
2143 | 11 | result |
2144 | 11 | ); |
2145 | 11 | const json* status |
2146 | 11 | = require_string(*transport, "status", "/transport", result); |
2147 | 11 | delivered = status != nullptr && *status == "delivered"; |
2148 | 11 | if (status != nullptr |
2149 | 11 | && !string_is_one_of(status, { "delivered", "failed" })) { |
2150 | 0 | add(result, "/transport/status", "enum", |
2151 | 0 | "transport status must be delivered or failed"); |
2152 | 0 | } |
2153 | 11 | require_nonnegative_integer( |
2154 | 11 | *transport, "attempts", "/transport", result |
2155 | 11 | ); |
2156 | 11 | if (transport->contains("delivery_mode")) { |
2157 | 11 | require_enum( |
2158 | 11 | *transport, "delivery_mode", "/transport", |
2159 | 11 | { "fetched", "cache_validated", "stale", "resumed", |
2160 | 11 | "offline" }, |
2161 | 11 | result |
2162 | 11 | ); |
2163 | 11 | } |
2164 | 11 | if (transport->contains("retry_after_ms")) { |
2165 | 0 | require_nonnegative_integer( |
2166 | 0 | *transport, "retry_after_ms", "/transport", result |
2167 | 0 | ); |
2168 | 0 | } |
2169 | 11 | optional_string(*transport, "error_code", "/transport", result); |
2170 | 11 | optional_string(*transport, "error_message", "/transport", result); |
2171 | 11 | } |
2172 | 11 | const json* artifact |
2173 | 11 | = optional_object(document, "artifact", "", result); |
2174 | 11 | if (delivered && artifact == nullptr) { |
2175 | 1 | add(result, "/artifact", "required_on_success", |
2176 | 1 | "delivered transport requires an artifact reference"); |
2177 | 1 | } |
2178 | 11 | if (artifact != nullptr) { |
2179 | 8 | validate_artifact(*artifact, "/artifact", result); |
2180 | 8 | } |
2181 | 11 | if (const json* response |
2182 | 11 | = require_object(document, "response_metadata", "", result)) { |
2183 | 11 | reject_unknown_fields( |
2184 | 11 | *response, "/response_metadata", |
2185 | 11 | { "status_code", "effective_url", "headers", "redirect_chain", |
2186 | 11 | "started_at", "completed_at" }, |
2187 | 11 | result |
2188 | 11 | ); |
2189 | 11 | require_nonnegative_integer( |
2190 | 11 | *response, "status_code", "/response_metadata", result |
2191 | 11 | ); |
2192 | 11 | optional_bounded_integer( |
2193 | 11 | *response, "status_code", "/response_metadata", 0, 999, result |
2194 | 11 | ); |
2195 | 11 | optional_string( |
2196 | 11 | *response, "effective_url", "/response_metadata", result |
2197 | 11 | ); |
2198 | 11 | if (const json* headers = require_array( |
2199 | 11 | *response, "headers", "/response_metadata", result |
2200 | 11 | )) { |
2201 | 11 | if (headers->size() > 1'024U) { |
2202 | 0 | add(result, "/response_metadata/headers", "max_items", |
2203 | 0 | "response metadata has too many headers"); |
2204 | 0 | } |
2205 | 27 | for (std::size_t index = 0; index < headers->size(); ++index) { |
2206 | 16 | const std::string path |
2207 | 16 | = "/response_metadata/headers/" + std::to_string(index); |
2208 | 16 | const json& header = headers->at(index); |
2209 | 16 | if (!header.is_object()) { |
2210 | 0 | add(result, path, "type", "expected a header object"); |
2211 | 0 | continue; |
2212 | 0 | } |
2213 | 16 | reject_unknown_fields( |
2214 | 16 | header, path, { "name", "value" }, result |
2215 | 16 | ); |
2216 | 16 | require_string(header, "name", path, result); |
2217 | 16 | const json* value = field(header, "value", path, result); |
2218 | 16 | if (value != nullptr && !value->is_string()) { |
2219 | 0 | add(result, path + "/value", "type", |
2220 | 0 | "expected a JSON string"); |
2221 | 0 | } |
2222 | 16 | } |
2223 | 11 | } |
2224 | 11 | validate_string_array( |
2225 | 11 | *response, "redirect_chain", "/response_metadata", false, result |
2226 | 11 | ); |
2227 | 11 | if (const auto chain = response->find("redirect_chain"); |
2228 | 11 | chain != response->end() && chain->is_array() |
2229 | 11 | && chain->size() > 20U) { |
2230 | 0 | add(result, "/response_metadata/redirect_chain", "max_items", |
2231 | 0 | "response metadata has too many redirects"); |
2232 | 0 | } |
2233 | 11 | require_timestamp( |
2234 | 11 | *response, "started_at", "/response_metadata", result |
2235 | 11 | ); |
2236 | 11 | require_timestamp( |
2237 | 11 | *response, "completed_at", "/response_metadata", result |
2238 | 11 | ); |
2239 | 11 | } |
2240 | 11 | validate_extensions(document, "", result); |
2241 | 11 | } |
2242 | | |
2243 | | void |
2244 | 9 | validate_candidate_plan(const json& document, validation_result& result) { |
2245 | 9 | reject_unknown_fields( |
2246 | 9 | document, "", |
2247 | 9 | { "contract", "format_version", "plan_id", "source_snapshot", |
2248 | 9 | "product_snapshot", "algorithm_version", "configuration", |
2249 | 9 | "plan_artifact", "summary", "created_at", "extensions" }, |
2250 | 9 | result |
2251 | 9 | ); |
2252 | 9 | require_stable_id(document, "plan_id", "", result); |
2253 | 9 | require_string(document, "algorithm_version", "", result); |
2254 | 9 | require_timestamp(document, "created_at", "", result); |
2255 | 9 | const json* source |
2256 | 9 | = require_object(document, "source_snapshot", "", result); |
2257 | 9 | if (source != nullptr) { |
2258 | 9 | reject_unknown_fields( |
2259 | 9 | *source, "/source_snapshot", |
2260 | 9 | { "snapshot_id", "storage_ref", "sha256" }, result |
2261 | 9 | ); |
2262 | 9 | require_stable_id( |
2263 | 9 | *source, "snapshot_id", "/source_snapshot", result |
2264 | 9 | ); |
2265 | 9 | require_string(*source, "storage_ref", "/source_snapshot", result); |
2266 | 9 | require_sha256(*source, "sha256", "/source_snapshot", result); |
2267 | 9 | } |
2268 | 9 | const json* product |
2269 | 9 | = require_object(document, "product_snapshot", "", result); |
2270 | 9 | if (product != nullptr) { |
2271 | 9 | reject_unknown_fields( |
2272 | 9 | *product, "/product_snapshot", { "snapshot_id", "sha256" }, |
2273 | 9 | result |
2274 | 9 | ); |
2275 | 9 | require_stable_id( |
2276 | 9 | *product, "snapshot_id", "/product_snapshot", result |
2277 | 9 | ); |
2278 | 9 | require_sha256(*product, "sha256", "/product_snapshot", result); |
2279 | 9 | } |
2280 | 9 | const json* configuration |
2281 | 9 | = require_object(document, "configuration", "", result); |
2282 | 9 | if (configuration != nullptr) { |
2283 | 9 | reject_unknown_fields( |
2284 | 9 | *configuration, "/configuration", { "sha256", "values" }, result |
2285 | 9 | ); |
2286 | 9 | require_sha256(*configuration, "sha256", "/configuration", result); |
2287 | 9 | require_object(*configuration, "values", "/configuration", result); |
2288 | 9 | } |
2289 | 9 | validate_artifact_field(document, "plan_artifact", "", result); |
2290 | 9 | const json* summary = require_object(document, "summary", "", result); |
2291 | 9 | if (summary != nullptr) { |
2292 | 9 | reject_unknown_fields( |
2293 | 9 | *summary, "/summary", |
2294 | 9 | { "candidate_count", "edge_count", "group_count" }, result |
2295 | 9 | ); |
2296 | 9 | require_nonnegative_integer( |
2297 | 9 | *summary, "candidate_count", "/summary", result |
2298 | 9 | ); |
2299 | 9 | require_nonnegative_integer( |
2300 | 9 | *summary, "edge_count", "/summary", result |
2301 | 9 | ); |
2302 | 9 | require_nonnegative_integer( |
2303 | 9 | *summary, "group_count", "/summary", result |
2304 | 9 | ); |
2305 | 9 | } |
2306 | 9 | validate_extensions(document, "", result); |
2307 | 9 | } |
2308 | | |
2309 | | void validate_structural_validation( |
2310 | | const json& value, const std::string_view path, |
2311 | | validation_result& result |
2312 | 14 | ) { |
2313 | 14 | reject_unknown_fields(value, path, { "passed", "report" }, result); |
2314 | 14 | const json* passed = field(value, "passed", path, result); |
2315 | 14 | if (passed != nullptr && !passed->is_boolean()) { |
2316 | 0 | add(result, child_path(path, "passed"), "type", |
2317 | 0 | "expected a boolean"); |
2318 | 0 | } |
2319 | 14 | validate_artifact_field(value, "report", path, result); |
2320 | 14 | } |
2321 | | |
2322 | | void validate_snapshot_common( |
2323 | | const json& document, |
2324 | | const std::initializer_list<std::string_view> allowed, |
2325 | | validation_result& result |
2326 | 14 | ) { |
2327 | 14 | reject_unknown_fields(document, "", allowed, result); |
2328 | 14 | require_stable_id(document, "snapshot_id", "", result); |
2329 | 14 | require_stable_id(document, "run_id", "", result); |
2330 | 14 | require_string(document, "graph_version", "", result); |
2331 | 14 | require_sha256(document, "content_sha256", "", result); |
2332 | 14 | validate_artifact_field(document, "database", "", result); |
2333 | 14 | validate_artifact_array(document, "exports", "", result); |
2334 | 14 | require_timestamp(document, "activated_at", "", result); |
2335 | 14 | const json* validation |
2336 | 14 | = require_object(document, "structural_validation", "", result); |
2337 | 14 | if (validation != nullptr) { |
2338 | 14 | validate_structural_validation( |
2339 | 14 | *validation, "/structural_validation", result |
2340 | 14 | ); |
2341 | 14 | } |
2342 | 14 | validate_extensions(document, "", result); |
2343 | 14 | } |
2344 | | |
2345 | | void |
2346 | 3 | validate_product_snapshot(const json& document, validation_result& result) { |
2347 | 3 | validate_snapshot_common( |
2348 | 3 | document, |
2349 | 3 | { "contract", "format_version", "snapshot_id", "run_id", |
2350 | 3 | "graph_version", "content_sha256", "database", "exports", |
2351 | 3 | "cocoon_ids", "activated_at", "structural_validation", |
2352 | 3 | "extensions" }, |
2353 | 3 | result |
2354 | 3 | ); |
2355 | 3 | if (document.contains("cocoon_ids")) { |
2356 | 2 | validate_string_array(document, "cocoon_ids", "", false, result); |
2357 | 2 | } |
2358 | 3 | } |
2359 | | |
2360 | | void validate_candidate_snapshot( |
2361 | | const json& document, validation_result& result |
2362 | 11 | ) { |
2363 | 11 | validate_snapshot_common( |
2364 | 11 | document, |
2365 | 11 | { "contract", "format_version", "snapshot_id", "run_id", |
2366 | 11 | "graph_version", "content_sha256", "database", "exports", |
2367 | 11 | "plan_id", "source_snapshot_id", "activated_at", |
2368 | 11 | "structural_validation", "extensions" }, |
2369 | 11 | result |
2370 | 11 | ); |
2371 | 11 | require_stable_id(document, "plan_id", "", result); |
2372 | 11 | require_stable_id(document, "source_snapshot_id", "", result); |
2373 | 11 | } |
2374 | | |
2375 | | void validate_viewer_projection( |
2376 | | const json& document, validation_result& result |
2377 | 2 | ) { |
2378 | 2 | reject_unknown_fields( |
2379 | 2 | document, "", |
2380 | 2 | { "contract", "format_version", "projection_id", |
2381 | 2 | "product_snapshot_id", "candidate_snapshot_id", |
2382 | 2 | "projection_version", "settings_sha256", "projection", |
2383 | 2 | "edge_semantics", "generated_at", "extensions" }, |
2384 | 2 | result |
2385 | 2 | ); |
2386 | 2 | require_stable_id(document, "projection_id", "", result); |
2387 | 2 | require_stable_id(document, "product_snapshot_id", "", result); |
2388 | 2 | optional_string(document, "candidate_snapshot_id", "", result); |
2389 | 2 | require_string(document, "projection_version", "", result); |
2390 | 2 | require_sha256(document, "settings_sha256", "", result); |
2391 | 2 | validate_artifact_field(document, "projection", "", result); |
2392 | 2 | require_timestamp(document, "generated_at", "", result); |
2393 | 2 | const json* semantics |
2394 | 2 | = require_object(document, "edge_semantics", "", result); |
2395 | 2 | if (semantics != nullptr) { |
2396 | 2 | reject_unknown_fields( |
2397 | 2 | *semantics, "/edge_semantics", |
2398 | 2 | { "human_type", "derived_types" }, result |
2399 | 2 | ); |
2400 | 2 | require_string(*semantics, "human_type", "/edge_semantics", result); |
2401 | 2 | validate_string_array( |
2402 | 2 | *semantics, "derived_types", "/edge_semantics", true, result |
2403 | 2 | ); |
2404 | 2 | } |
2405 | 2 | validate_extensions(document, "", result); |
2406 | 2 | } |
2407 | | |
2408 | 5 | void validate_site_bundle(const json& document, validation_result& result) { |
2409 | 5 | reject_unknown_fields( |
2410 | 5 | document, "", |
2411 | 5 | { "contract", "format_version", "bundle_id", "projection_id", |
2412 | 5 | "product_snapshot_id", "candidate_snapshot_id", "viewer_version", |
2413 | 5 | "entrypoint", "bundle", "generated_at", "extensions" }, |
2414 | 5 | result |
2415 | 5 | ); |
2416 | 5 | require_stable_id(document, "bundle_id", "", result); |
2417 | 5 | require_stable_id(document, "projection_id", "", result); |
2418 | 5 | require_stable_id(document, "product_snapshot_id", "", result); |
2419 | 5 | optional_string(document, "candidate_snapshot_id", "", result); |
2420 | 5 | require_string(document, "viewer_version", "", result); |
2421 | 5 | require_string(document, "entrypoint", "", result); |
2422 | 5 | validate_artifact_field(document, "bundle", "", result); |
2423 | 5 | require_timestamp(document, "generated_at", "", result); |
2424 | 5 | validate_extensions(document, "", result); |
2425 | 5 | } |
2426 | | |
2427 | | void validate_body( |
2428 | | const contract_name name, const json& document, |
2429 | | validation_result& result |
2430 | 105 | ) { |
2431 | 105 | switch (name) { |
2432 | 48 | case contract_name::arachne_batch: |
2433 | 48 | validate_arachne_batch(document, result); |
2434 | 48 | break; |
2435 | 5 | case contract_name::batch_envelope: |
2436 | 5 | validate_batch_envelope(document, result); |
2437 | 5 | break; |
2438 | 3 | case contract_name::fetch_plan: |
2439 | 3 | validate_fetch_plan(document, result); |
2440 | 3 | break; |
2441 | 8 | case contract_name::fetch_request: |
2442 | 8 | validate_fetch_request(document, result); |
2443 | 8 | break; |
2444 | 11 | case contract_name::acquired_artifact: |
2445 | 11 | validate_acquired_artifact(document, result); |
2446 | 11 | break; |
2447 | 9 | case contract_name::research_candidate_graph_plan: |
2448 | 9 | validate_candidate_plan(document, result); |
2449 | 9 | break; |
2450 | 3 | case contract_name::product_graph_snapshot: |
2451 | 3 | validate_product_snapshot(document, result); |
2452 | 3 | break; |
2453 | 11 | case contract_name::research_candidate_graph_snapshot: |
2454 | 11 | validate_candidate_snapshot(document, result); |
2455 | 11 | break; |
2456 | 2 | case contract_name::viewer_projection: |
2457 | 2 | validate_viewer_projection(document, result); |
2458 | 2 | break; |
2459 | 5 | case contract_name::site_bundle: |
2460 | 5 | validate_site_bundle(document, result); |
2461 | 5 | break; |
2462 | 105 | } |
2463 | 105 | } |
2464 | | |
2465 | 412 | void reject_non_finite(const json& value, const std::string& path) { |
2466 | 412 | if (value.is_discarded()) { |
2467 | 0 | throw std::invalid_argument("discarded JSON value at " + path); |
2468 | 0 | } |
2469 | 412 | if (value.is_number_float() && !std::isfinite(value.get<double>())) { |
2470 | 1 | throw std::invalid_argument("non-finite number at " + path); |
2471 | 1 | } |
2472 | 411 | if (value.is_array()) { |
2473 | 29 | for (std::size_t index = 0; index < value.size(); ++index) { |
2474 | 6 | reject_non_finite( |
2475 | 6 | value[index], path + "/" + std::to_string(index) |
2476 | 6 | ); |
2477 | 6 | } |
2478 | 388 | } else if (value.is_object()) { |
2479 | 362 | for (const auto& [key, child] : value.items()) { |
2480 | 362 | reject_non_finite(child, child_path(path, key)); |
2481 | 362 | } |
2482 | 46 | } |
2483 | 411 | } |
2484 | | |
2485 | | } // namespace |
2486 | | |
2487 | 12 | std::string_view to_string(const contract_name name) noexcept { |
2488 | 12 | const auto it |
2489 | 62 | = std::ranges::find_if(contract_names, [name](const auto& item) { |
2490 | 62 | return item.second == name; |
2491 | 62 | }); |
2492 | 12 | return it == contract_names.end() ? std::string_view {} : it->first; |
2493 | 12 | } |
2494 | | |
2495 | | std::optional<contract_name> |
2496 | 107 | parse_contract_name(const std::string_view name) noexcept { |
2497 | 107 | const auto it |
2498 | 635 | = std::ranges::find_if(contract_names, [name](const auto& item) { |
2499 | 635 | return item.first == name; |
2500 | 635 | }); |
2501 | 107 | if (it == contract_names.end()) { |
2502 | 7 | return std::nullopt; |
2503 | 7 | } |
2504 | 100 | return it->second; |
2505 | 107 | } |
2506 | | |
2507 | 4 | bool is_artifact_bearing(const contract_name name) noexcept { |
2508 | 4 | switch (name) { |
2509 | 1 | case contract_name::batch_envelope: |
2510 | 1 | case contract_name::fetch_request: |
2511 | 1 | case contract_name::acquired_artifact: |
2512 | 1 | case contract_name::research_candidate_graph_plan: |
2513 | 1 | case contract_name::product_graph_snapshot: |
2514 | 1 | case contract_name::research_candidate_graph_snapshot: |
2515 | 1 | case contract_name::viewer_projection: |
2516 | 2 | case contract_name::site_bundle: |
2517 | 2 | return true; |
2518 | 1 | case contract_name::arachne_batch: |
2519 | 2 | case contract_name::fetch_plan: |
2520 | 2 | return false; |
2521 | 4 | } |
2522 | 0 | return false; |
2523 | 4 | } |
2524 | | |
2525 | 30 | validation_result validate(const json& document) { |
2526 | 30 | validation_result result; |
2527 | 30 | const auto name = inspect_contract(document, result); |
2528 | 30 | if (!name.has_value()) { |
2529 | 3 | return result; |
2530 | 3 | } |
2531 | 27 | validate_header(*name, document, result); |
2532 | 27 | validate_body(*name, document, result); |
2533 | 27 | return result; |
2534 | 30 | } |
2535 | | |
2536 | 78 | validation_result validate(const contract_name expected, const json& document) { |
2537 | 78 | validation_result result; |
2538 | 78 | if (!document.is_object()) { |
2539 | 0 | add(result, "", "type", "contract document must be a JSON object"); |
2540 | 0 | return result; |
2541 | 0 | } |
2542 | 78 | validate_header(expected, document, result); |
2543 | 78 | validate_body(expected, document, result); |
2544 | 78 | return result; |
2545 | 78 | } |
2546 | | |
2547 | 43 | std::string canonical_json(const json& document) { |
2548 | 43 | reject_non_finite(document, ""); |
2549 | 43 | return document.dump( |
2550 | 43 | -1, ' ', false, nlohmann::json::error_handler_t::strict |
2551 | 43 | ); |
2552 | 43 | } |
2553 | | |
2554 | | } // namespace arachnespace::contracts |