Coverage Report

Created: 2026-07-30 12:27

/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