Arachne 1.0
Arachne - the perpetual stitcher of Wikidata entities.
Loading...
Searching...
No Matches
arachnespace::contracts::anonymous_namespace{contracts.cpp} Namespace Reference

Typedefs

using json = nlohmann::json

Functions

void add (validation_result &result, std::string path, std::string code, std::string message)
std::string child_path (const std::string_view parent, const std::string_view key)
const jsonfield (const json &object, const std::string_view key, const std::string_view path, validation_result &result)
void reject_unknown_fields (const json &object, const std::string_view path, const std::initializer_list< std::string_view > allowed, validation_result &result)
const jsonrequire_object (const json &object, const std::string_view key, const std::string_view path, validation_result &result)
const jsonrequire_array (const json &object, const std::string_view key, const std::string_view path, validation_result &result)
const jsonoptional_object (const json &object, const std::string_view key, const std::string_view path, validation_result &result)
const jsonoptional_array (const json &object, const std::string_view key, const std::string_view path, validation_result &result)
const jsonrequire_string (const json &object, const std::string_view key, const std::string_view path, validation_result &result)
const jsonoptional_string (const json &object, const std::string_view key, const std::string_view path, validation_result &result)
bool string_is_one_of (const json *value, const std::initializer_list< std::string_view > choices)
void require_enum (const json &object, const std::string_view key, const std::string_view path, const std::initializer_list< std::string_view > choices, validation_result &result)
bool matches (const json *value, const std::regex &expression)
void require_stable_id (const json &object, const std::string_view key, const std::string_view path, validation_result &result)
void require_sha256 (const json &object, const std::string_view key, const std::string_view path, validation_result &result)
void require_timestamp (const json &object, const std::string_view key, const std::string_view path, validation_result &result)
void require_nonnegative_integer (const json &object, const std::string_view key, const std::string_view path, validation_result &result)
void require_boolean (const json &object, const std::string_view key, const std::string_view path, validation_result &result)
void optional_integer_range (const json &object, const std::string_view key, const std::string_view path, const std::int64_t minimum, const std::int64_t maximum, validation_result &result)
void require_integer_range (const json &object, const std::string_view key, const std::string_view path, const std::int64_t minimum, const std::int64_t maximum, validation_result &result)
void optional_number_range (const json &object, const std::string_view key, const std::string_view path, const double minimum, const double maximum, validation_result &result)
void require_number_range (const json &object, const std::string_view key, const std::string_view path, const double minimum, const double maximum, validation_result &result)
void optional_enum (const json &object, const std::string_view key, const std::string_view path, const std::initializer_list< std::string_view > choices, validation_result &result)
void require_pattern (const json &object, const std::string_view key, const std::string_view path, const std::regex &expression, validation_result &result)
void optional_pattern (const json &object, const std::string_view key, const std::string_view path, const std::regex &expression, validation_result &result)
void optional_bounded_integer (const json &object, const std::string_view key, const std::string_view path, const std::uint64_t minimum, const std::uint64_t maximum, validation_result &result)
void validate_extensions (const json &object, const std::string_view path, validation_result &result)
void validate_artifact (const json &artifact, const std::string_view path, validation_result &result)
void validate_artifact_field (const json &object, const std::string_view key, const std::string_view path, validation_result &result)
void validate_artifact_array (const json &object, const std::string_view key, const std::string_view path, validation_result &result)
void validate_string_array (const json &object, const std::string_view key, const std::string_view path, const bool require_nonempty, validation_result &result)
std::optional< unsigned int > declared_major (const std::string_view name)
bool known_contract_base (const std::string_view name)
std::optional< contract_nameinspect_contract (const json &document, validation_result &result)
void validate_header (const contract_name expected, const json &document, validation_result &result)
void validate_stable_reference (const json &value, const std::string_view path, validation_result &result)
void validate_database_or_local_reference (const json &value, const std::string_view path, validation_result &result)
void validate_unique_items (const json &array, const std::string_view path, validation_result &result)
template<typename Validator>
void validate_optional_object_array (const json &object, const std::string_view key, const std::string_view path, validation_result &result, Validator &&validator)
void validate_nonempty_optional_strings (const json &object, const std::string_view path, const std::initializer_list< std::string_view > keys, validation_result &result)
void validate_year_fields (const json &object, const std::string_view path, const std::initializer_list< std::string_view > keys, validation_result &result)
void validate_entity_id (const json &object, const std::string_view key, const std::string_view path, const std::string_view family, validation_result &result)
void validate_positive_id_array (const json &object, const std::string_view key, const std::string_view path, validation_result &result)
void validate_evidence_references (const json &object, const std::string_view key, const std::string_view path, validation_result &result)
void validate_create_operations (const json &create, const std::string_view path, validation_result &result)
void validate_update_fields (const json &item, const std::string_view item_path, const std::string_view family, const std::initializer_list< std::string_view > set_fields, const std::initializer_list< std::string_view > unset_fields, validation_result &result)
void validate_work_set (const json &set, const std::string_view path, validation_result &result)
void validate_update_operations (const json &update, const std::string_view path, validation_result &result)
void validate_merge_operations (const json &merge, const std::string_view path, validation_result &result)
void validate_arachne_batch (const json &document, validation_result &result)
void validate_batch_envelope (const json &document, validation_result &result)
void validate_fetch_plan (const json &document, validation_result &result)
void validate_fetch_request (const json &document, validation_result &result)
void validate_acquired_artifact (const json &document, validation_result &result)
void validate_candidate_plan (const json &document, validation_result &result)
void validate_structural_validation (const json &value, const std::string_view path, validation_result &result)
void validate_snapshot_common (const json &document, const std::initializer_list< std::string_view > allowed, validation_result &result)
void validate_product_snapshot (const json &document, validation_result &result)
void validate_candidate_snapshot (const json &document, validation_result &result)
void validate_viewer_projection (const json &document, validation_result &result)
void validate_site_bundle (const json &document, validation_result &result)
void validate_body (const contract_name name, const json &document, validation_result &result)
void reject_non_finite (const json &value, const std::string &path)

Variables

constexpr std::array< std::pair< std::string_view, contract_name >, 10 > contract_names

Typedef Documentation

◆ json

using arachnespace::contracts::anonymous_namespace{contracts.cpp}::json = nlohmann::json

Definition at line 22 of file contracts.cpp.

Function Documentation

◆ add()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::add ( validation_result & result,
std::string path,
std::string code,
std::string message )

Definition at line 42 of file contracts.cpp.

43 {
44 result.diagnostics.push_back(
45 { std::move(path), std::move(code), std::move(message) }
46 );
47 }
std::vector< diagnostic > diagnostics
Definition contracts.hpp:38

◆ child_path()

std::string arachnespace::contracts::anonymous_namespace{contracts.cpp}::child_path ( const std::string_view parent,
const std::string_view key )

Definition at line 50 of file contracts.cpp.

50 {
51 if (parent.empty()) {
52 return "/" + std::string(key);
53 }
54 return std::string(parent) + "/" + std::string(key);
55 }

◆ declared_major()

std::optional< unsigned int > arachnespace::contracts::anonymous_namespace{contracts.cpp}::declared_major ( const std::string_view name)

Definition at line 535 of file contracts.cpp.

535 {
536 const std::size_t separator = name.rfind("_v");
537 if (separator == std::string_view::npos
538 || separator + 2 >= name.size()) {
539 return std::nullopt;
540 }
541 unsigned int major = 0;
542 const char* begin = name.data() + separator + 2;
543 const char* end = name.data() + name.size();
544 const auto parsed = std::from_chars(begin, end, major);
545 if (parsed.ec != std::errc {} || parsed.ptr != end) {
546 return std::nullopt;
547 }
548 return major;
549 }

◆ field()

const json * arachnespace::contracts::anonymous_namespace{contracts.cpp}::field ( const json & object,
const std::string_view key,
const std::string_view path,
validation_result & result )

Definition at line 57 of file contracts.cpp.

60 {
61 const auto it = object.find(key);
62 if (it == object.end()) {
63 add(result, child_path(path, key), "required",
64 "required field is missing");
65 return nullptr;
66 }
67 return &*it;
68 }
std::string child_path(const std::string_view parent, const std::string_view key)
Definition contracts.cpp:50
void add(validation_result &result, std::string path, std::string code, std::string message)
Definition contracts.cpp:42

◆ inspect_contract()

std::optional< contract_name > arachnespace::contracts::anonymous_namespace{contracts.cpp}::inspect_contract ( const json & document,
validation_result & result )

Definition at line 565 of file contracts.cpp.

565 {
566 if (!document.is_object()) {
567 add(result, "", "type", "contract document must be a JSON object");
568 return std::nullopt;
569 }
570 const auto format = document.find("format");
571 if (format != document.end()) {
572 if (!format->is_string()) {
573 add(result, "/format", "type", "expected a JSON string");
574 return std::nullopt;
575 }
576 const std::string& name = format->get_ref<const std::string&>();
577 if (name == "arachne_batch_v2") {
579 }
580 if (name.starts_with("arachne_batch_v")) {
581 add(result, "/format", "unsupported_major",
582 "unsupported Arachne batch major version");
583 } else {
584 add(result, "/format", "unknown_contract",
585 "unknown or malformed batch format");
586 }
587 return std::nullopt;
588 }
589 const auto it = document.find("contract");
590 if (it == document.end()) {
591 add(result, "/contract", "required", "required field is missing");
592 return std::nullopt;
593 }
594 if (!it->is_string()) {
595 add(result, "/contract", "type", "expected a JSON string");
596 return std::nullopt;
597 }
598 const std::string& name = it->get_ref<const std::string&>();
599 if (const auto parsed = parse_contract_name(name); parsed.has_value()) {
600 return parsed;
601 }
602 if (known_contract_base(name) && declared_major(name).has_value()) {
603 add(result, "/contract", "unsupported_major",
604 "unsupported contract major version "
605 + std::to_string(*declared_major(name)));
606 } else {
607 add(result, "/contract", "unknown_contract",
608 "unknown or malformed contract name");
609 }
610 return std::nullopt;
611 }
std::optional< unsigned int > declared_major(const std::string_view name)
std::optional< contract_name > parse_contract_name(std::string_view name) noexcept

References arachnespace::contracts::arachne_batch.

◆ known_contract_base()

bool arachnespace::contracts::anonymous_namespace{contracts.cpp}::known_contract_base ( const std::string_view name)

Definition at line 551 of file contracts.cpp.

551 {
552 const std::size_t separator = name.rfind("_v");
553 if (separator == std::string_view::npos) {
554 return false;
555 }
556 const std::string_view base = name.substr(0, separator);
557 return std::ranges::any_of(contract_names, [base](const auto& entry) {
558 const std::string_view supported = entry.first;
559 const std::size_t supported_separator = supported.rfind("_v");
560 return supported.substr(0, supported_separator) == base;
561 });
562 }
constexpr std::array< std::pair< std::string_view, contract_name >, 10 > contract_names
Definition contracts.cpp:25

◆ matches()

bool arachnespace::contracts::anonymous_namespace{contracts.cpp}::matches ( const json * value,
const std::regex & expression )

Definition at line 211 of file contracts.cpp.

211 {
212 if (value == nullptr || !value->is_string()) {
213 return false;
214 }
215 return std::regex_match(
216 value->get_ref<const std::string&>(), expression
217 );
218 }

◆ optional_array()

const json * arachnespace::contracts::anonymous_namespace{contracts.cpp}::optional_array ( const json & object,
const std::string_view key,
const std::string_view path,
validation_result & result )

Definition at line 129 of file contracts.cpp.

132 {
133 const auto it = object.find(key);
134 if (it == object.end()) {
135 return nullptr;
136 }
137 if (!it->is_array()) {
138 add(result, child_path(path, key), "type",
139 "expected a JSON array");
140 return nullptr;
141 }
142 return &*it;
143 }

◆ optional_bounded_integer()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::optional_bounded_integer ( const json & object,
const std::string_view key,
const std::string_view path,
const std::uint64_t minimum,
const std::uint64_t maximum,
validation_result & result )

Definition at line 405 of file contracts.cpp.

409 {
410 const auto it = object.find(key);
411 if (it == object.end()) {
412 return;
413 }
414 bool in_range = false;
415 if (it->is_number_unsigned()) {
416 const std::uint64_t value = it->get<std::uint64_t>();
417 in_range = value >= minimum && value <= maximum;
418 } else if (it->is_number_integer()) {
419 const std::int64_t value = it->get<std::int64_t>();
420 in_range = value >= 0
421 && static_cast<std::uint64_t>(value) >= minimum
422 && static_cast<std::uint64_t>(value) <= maximum;
423 } else {
424 add(result, child_path(path, key), "type", "expected an integer");
425 return;
426 }
427 if (!in_range) {
428 add(result, child_path(path, key), "range",
429 "integer is outside the permitted safety range");
430 }
431 }

◆ optional_enum()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::optional_enum ( const json & object,
const std::string_view key,
const std::string_view path,
const std::initializer_list< std::string_view > choices,
validation_result & result )

Definition at line 368 of file contracts.cpp.

373 {
374 const json* value = optional_string(object, key, path, result);
375 if (value != nullptr && !string_is_one_of(value, choices)) {
376 add(result, child_path(path, key), "enum",
377 "value is not one of the allowed strings");
378 }
379 }
bool string_is_one_of(const json *value, const std::initializer_list< std::string_view > choices)
const json * optional_string(const json &object, const std::string_view key, const std::string_view path, validation_result &result)

◆ optional_integer_range()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::optional_integer_range ( const json & object,
const std::string_view key,
const std::string_view path,
const std::int64_t minimum,
const std::int64_t maximum,
validation_result & result )

Definition at line 299 of file contracts.cpp.

303 {
304 const auto it = object.find(key);
305 if (it == object.end()) {
306 return;
307 }
308 if (!it->is_number_integer() && !it->is_number_unsigned()) {
309 add(result, child_path(path, key), "type", "expected an integer");
310 return;
311 }
312 const long double value = it->is_number_unsigned()
313 ? static_cast<long double>(it->get<std::uint64_t>())
314 : static_cast<long double>(it->get<std::int64_t>());
315 if (value < static_cast<long double>(minimum)
316 || value > static_cast<long double>(maximum)) {
317 add(result, child_path(path, key), "range",
318 "integer is outside the permitted range");
319 }
320 }

◆ optional_number_range()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::optional_number_range ( const json & object,
const std::string_view key,
const std::string_view path,
const double minimum,
const double maximum,
validation_result & result )

Definition at line 336 of file contracts.cpp.

340 {
341 const auto it = object.find(key);
342 if (it == object.end()) {
343 return;
344 }
345 if (!it->is_number()) {
346 add(result, child_path(path, key), "type", "expected a number");
347 return;
348 }
349 const double value = it->get<double>();
350 if (!std::isfinite(value) || value < minimum || value > maximum) {
351 add(result, child_path(path, key), "range",
352 "number is outside the permitted range");
353 }
354 }

◆ optional_object()

const json * arachnespace::contracts::anonymous_namespace{contracts.cpp}::optional_object ( const json & object,
const std::string_view key,
const std::string_view path,
validation_result & result )

Definition at line 113 of file contracts.cpp.

116 {
117 const auto it = object.find(key);
118 if (it == object.end()) {
119 return nullptr;
120 }
121 if (!it->is_object()) {
122 add(result, child_path(path, key), "type",
123 "expected a JSON object");
124 return nullptr;
125 }
126 return &*it;
127 }

◆ optional_pattern()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::optional_pattern ( const json & object,
const std::string_view key,
const std::string_view path,
const std::regex & expression,
validation_result & result )

Definition at line 393 of file contracts.cpp.

397 {
398 const json* value = optional_string(object, key, path, result);
399 if (value != nullptr && !matches(value, expression)) {
400 add(result, child_path(path, key), "pattern",
401 "string does not match the required canonical form");
402 }
403 }
bool matches(const json *value, const std::regex &expression)

◆ optional_string()

const json * arachnespace::contracts::anonymous_namespace{contracts.cpp}::optional_string ( const json & object,
const std::string_view key,
const std::string_view path,
validation_result & result )

Definition at line 166 of file contracts.cpp.

169 {
170 const auto it = object.find(key);
171 if (it == object.end()) {
172 return nullptr;
173 }
174 if (!it->is_string()) {
175 add(result, child_path(path, key), "type",
176 "expected a JSON string");
177 return nullptr;
178 }
179 if (it->get_ref<const std::string&>().empty()) {
180 add(result, child_path(path, key), "min_length",
181 "string must not be empty");
182 return nullptr;
183 }
184 return &*it;
185 }

◆ reject_non_finite()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::reject_non_finite ( const json & value,
const std::string & path )

Definition at line 2465 of file contracts.cpp.

2465 {
2466 if (value.is_discarded()) {
2467 throw std::invalid_argument("discarded JSON value at " + path);
2468 }
2469 if (value.is_number_float() && !std::isfinite(value.get<double>())) {
2470 throw std::invalid_argument("non-finite number at " + path);
2471 }
2472 if (value.is_array()) {
2473 for (std::size_t index = 0; index < value.size(); ++index) {
2475 value[index], path + "/" + std::to_string(index)
2476 );
2477 }
2478 } else if (value.is_object()) {
2479 for (const auto& [key, child] : value.items()) {
2480 reject_non_finite(child, child_path(path, key));
2481 }
2482 }
2483 }
void reject_non_finite(const json &value, const std::string &path)

◆ reject_unknown_fields()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::reject_unknown_fields ( const json & object,
const std::string_view path,
const std::initializer_list< std::string_view > allowed,
validation_result & result )

Definition at line 70 of file contracts.cpp.

74 {
75 if (!object.is_object()) {
76 return;
77 }
78 for (const auto& [key, unused] : object.items()) {
79 (void)unused;
80 if (std::ranges::find(allowed, std::string_view(key))
81 == allowed.end()) {
82 add(result, child_path(path, key), "unknown_field",
83 "field is not defined by this contract version");
84 }
85 }
86 }

◆ require_array()

const json * arachnespace::contracts::anonymous_namespace{contracts.cpp}::require_array ( const json & object,
const std::string_view key,
const std::string_view path,
validation_result & result )

Definition at line 101 of file contracts.cpp.

104 {
105 const json* value = field(object, key, path, result);
106 if (value != nullptr && !value->is_array()) {
107 add(result, child_path(path, key), "type", "expected a JSON array");
108 return nullptr;
109 }
110 return value;
111 }
const json * field(const json &object, const std::string_view key, const std::string_view path, validation_result &result)
Definition contracts.cpp:57

◆ require_boolean()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::require_boolean ( const json & object,
const std::string_view key,
const std::string_view path,
validation_result & result )

Definition at line 288 of file contracts.cpp.

291 {
292 const json* value = field(object, key, path, result);
293 if (value != nullptr && !value->is_boolean()) {
294 add(result, child_path(path, key), "type",
295 "expected a JSON boolean");
296 }
297 }

◆ require_enum()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::require_enum ( const json & object,
const std::string_view key,
const std::string_view path,
const std::initializer_list< std::string_view > choices,
validation_result & result )

Definition at line 198 of file contracts.cpp.

203 {
204 const json* value = require_string(object, key, path, result);
205 if (value != nullptr && !string_is_one_of(value, choices)) {
206 add(result, child_path(path, key), "enum",
207 "value is not one of the allowed strings");
208 }
209 }
const json * require_string(const json &object, const std::string_view key, const std::string_view path, validation_result &result)

◆ require_integer_range()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::require_integer_range ( const json & object,
const std::string_view key,
const std::string_view path,
const std::int64_t minimum,
const std::int64_t maximum,
validation_result & result )

Definition at line 322 of file contracts.cpp.

326 {
327 if (!object.contains(key)) {
328 field(object, key, path, result);
329 return;
330 }
332 object, key, path, minimum, maximum, result
333 );
334 }
void optional_integer_range(const json &object, const std::string_view key, const std::string_view path, const std::int64_t minimum, const std::int64_t maximum, validation_result &result)

◆ require_nonnegative_integer()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::require_nonnegative_integer ( const json & object,
const std::string_view key,
const std::string_view path,
validation_result & result )

Definition at line 269 of file contracts.cpp.

272 {
273 const json* value = field(object, key, path, result);
274 if (value == nullptr) {
275 return;
276 }
277 if (!value->is_number_integer() && !value->is_number_unsigned()) {
278 add(result, child_path(path, key), "type",
279 "expected a non-negative integer");
280 return;
281 }
282 if (value->is_number_integer() && value->get<long long>() < 0) {
283 add(result, child_path(path, key), "minimum",
284 "integer must be non-negative");
285 }
286 }

◆ require_number_range()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::require_number_range ( const json & object,
const std::string_view key,
const std::string_view path,
const double minimum,
const double maximum,
validation_result & result )

Definition at line 356 of file contracts.cpp.

360 {
361 if (!object.contains(key)) {
362 field(object, key, path, result);
363 return;
364 }
365 optional_number_range(object, key, path, minimum, maximum, result);
366 }
void optional_number_range(const json &object, const std::string_view key, const std::string_view path, const double minimum, const double maximum, validation_result &result)

◆ require_object()

const json * arachnespace::contracts::anonymous_namespace{contracts.cpp}::require_object ( const json & object,
const std::string_view key,
const std::string_view path,
validation_result & result )

Definition at line 88 of file contracts.cpp.

91 {
92 const json* value = field(object, key, path, result);
93 if (value != nullptr && !value->is_object()) {
94 add(result, child_path(path, key), "type",
95 "expected a JSON object");
96 return nullptr;
97 }
98 return value;
99 }

◆ require_pattern()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::require_pattern ( const json & object,
const std::string_view key,
const std::string_view path,
const std::regex & expression,
validation_result & result )

Definition at line 381 of file contracts.cpp.

385 {
386 const json* value = require_string(object, key, path, result);
387 if (value != nullptr && !matches(value, expression)) {
388 add(result, child_path(path, key), "pattern",
389 "string does not match the required canonical form");
390 }
391 }

◆ require_sha256()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::require_sha256 ( const json & object,
const std::string_view key,
const std::string_view path,
validation_result & result )

Definition at line 243 of file contracts.cpp.

246 {
247 static const std::regex expression(R"(^[0-9a-f]{64}$)");
248 const json* value = require_string(object, key, path, result);
249 if (value != nullptr && !matches(value, expression)) {
250 add(result, child_path(path, key), "sha256",
251 "expected a lowercase 64-character SHA-256 digest");
252 }
253 }

◆ require_stable_id()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::require_stable_id ( const json & object,
const std::string_view key,
const std::string_view path,
validation_result & result )

Definition at line 220 of file contracts.cpp.

223 {
224 static const std::regex expression(
225 R"(^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$)"
226 );
227 static const std::regex canonical_entity(
228 R"(^(agent|work|concept|manifestation)-[0-9]{6,}$)"
229 );
230 const json* value = require_string(object, key, path, result);
231 if (value != nullptr && !matches(value, expression)) {
232 add(result, child_path(path, key), "pattern",
233 "expected a stable identifier of at most 128 characters");
234 } else if (
235 value != nullptr && key == "local_id"
236 && matches(value, canonical_entity)
237 ) {
238 add(result, child_path(path, key), "reserved_identifier",
239 "local IDs must not use a canonical entity ID pattern");
240 }
241 }

◆ require_string()

const json * arachnespace::contracts::anonymous_namespace{contracts.cpp}::require_string ( const json & object,
const std::string_view key,
const std::string_view path,
validation_result & result )

Definition at line 145 of file contracts.cpp.

148 {
149 const json* value = field(object, key, path, result);
150 if (value == nullptr) {
151 return nullptr;
152 }
153 if (!value->is_string()) {
154 add(result, child_path(path, key), "type",
155 "expected a JSON string");
156 return nullptr;
157 }
158 if (value->get_ref<const std::string&>().empty()) {
159 add(result, child_path(path, key), "min_length",
160 "string must not be empty");
161 return nullptr;
162 }
163 return value;
164 }

◆ require_timestamp()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::require_timestamp ( const json & object,
const std::string_view key,
const std::string_view path,
validation_result & result )

Definition at line 255 of file contracts.cpp.

258 {
259 static const std::regex expression(
260 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 );
262 const json* value = require_string(object, key, path, result);
263 if (value != nullptr && !matches(value, expression)) {
264 add(result, child_path(path, key), "date_time",
265 "expected an RFC 3339 date-time with an explicit offset");
266 }
267 }

◆ string_is_one_of()

bool arachnespace::contracts::anonymous_namespace{contracts.cpp}::string_is_one_of ( const json * value,
const std::initializer_list< std::string_view > choices )

Definition at line 187 of file contracts.cpp.

189 {
190 if (value == nullptr || !value->is_string()) {
191 return false;
192 }
193 const std::string& text = value->get_ref<const std::string&>();
194 return std::ranges::find(choices, std::string_view(text))
195 != choices.end();
196 }

◆ validate_acquired_artifact()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_acquired_artifact ( const json & document,
validation_result & result )

Definition at line 2110 of file contracts.cpp.

2112 {
2114 document, "",
2115 { "contract", "format_version", "artifact_id", "request_id",
2116 "door_id", "operation", "source_locator", "artifact", "transport",
2117 "response_metadata", "acquired_at", "extensions" },
2118 result
2119 );
2120 require_stable_id(document, "artifact_id", "", result);
2121 require_stable_id(document, "request_id", "", result);
2122 if (document.contains("door_id")) {
2123 require_stable_id(document, "door_id", "", result);
2124 }
2125 if (document.contains("operation")) {
2127 document, "operation", "",
2128 { "bulk_snapshot", "incremental_harvest", "point_lookup",
2129 "resume_download", "backend_read", "external_write" },
2130 result
2131 );
2132 }
2133 require_string(document, "source_locator", "", result);
2134 require_timestamp(document, "acquired_at", "", result);
2135 const json* transport
2136 = require_object(document, "transport", "", result);
2137 bool delivered = false;
2138 if (transport != nullptr) {
2140 *transport, "/transport",
2141 { "status", "attempts", "delivery_mode", "retry_after_ms",
2142 "error_code", "error_message" },
2143 result
2144 );
2145 const json* status
2146 = require_string(*transport, "status", "/transport", result);
2147 delivered = status != nullptr && *status == "delivered";
2148 if (status != nullptr
2149 && !string_is_one_of(status, { "delivered", "failed" })) {
2150 add(result, "/transport/status", "enum",
2151 "transport status must be delivered or failed");
2152 }
2154 *transport, "attempts", "/transport", result
2155 );
2156 if (transport->contains("delivery_mode")) {
2158 *transport, "delivery_mode", "/transport",
2159 { "fetched", "cache_validated", "stale", "resumed",
2160 "offline" },
2161 result
2162 );
2163 }
2164 if (transport->contains("retry_after_ms")) {
2166 *transport, "retry_after_ms", "/transport", result
2167 );
2168 }
2169 optional_string(*transport, "error_code", "/transport", result);
2170 optional_string(*transport, "error_message", "/transport", result);
2171 }
2172 const json* artifact
2173 = optional_object(document, "artifact", "", result);
2174 if (delivered && artifact == nullptr) {
2175 add(result, "/artifact", "required_on_success",
2176 "delivered transport requires an artifact reference");
2177 }
2178 if (artifact != nullptr) {
2179 validate_artifact(*artifact, "/artifact", result);
2180 }
2181 if (const json* response
2182 = require_object(document, "response_metadata", "", result)) {
2184 *response, "/response_metadata",
2185 { "status_code", "effective_url", "headers", "redirect_chain",
2186 "started_at", "completed_at" },
2187 result
2188 );
2190 *response, "status_code", "/response_metadata", result
2191 );
2193 *response, "status_code", "/response_metadata", 0, 999, result
2194 );
2196 *response, "effective_url", "/response_metadata", result
2197 );
2198 if (const json* headers = require_array(
2199 *response, "headers", "/response_metadata", result
2200 )) {
2201 if (headers->size() > 1'024U) {
2202 add(result, "/response_metadata/headers", "max_items",
2203 "response metadata has too many headers");
2204 }
2205 for (std::size_t index = 0; index < headers->size(); ++index) {
2206 const std::string path
2207 = "/response_metadata/headers/" + std::to_string(index);
2208 const json& header = headers->at(index);
2209 if (!header.is_object()) {
2210 add(result, path, "type", "expected a header object");
2211 continue;
2212 }
2214 header, path, { "name", "value" }, result
2215 );
2216 require_string(header, "name", path, result);
2217 const json* value = field(header, "value", path, result);
2218 if (value != nullptr && !value->is_string()) {
2219 add(result, path + "/value", "type",
2220 "expected a JSON string");
2221 }
2222 }
2223 }
2225 *response, "redirect_chain", "/response_metadata", false, result
2226 );
2227 if (const auto chain = response->find("redirect_chain");
2228 chain != response->end() && chain->is_array()
2229 && chain->size() > 20U) {
2230 add(result, "/response_metadata/redirect_chain", "max_items",
2231 "response metadata has too many redirects");
2232 }
2234 *response, "started_at", "/response_metadata", result
2235 );
2237 *response, "completed_at", "/response_metadata", result
2238 );
2239 }
2240 validate_extensions(document, "", result);
2241 }
json artifact(std::string storage_ref, std::string sha256, std::uintmax_t byte_length, std::string media_type)
Definition store.cpp:1484
void require_timestamp(const json &object, const std::string_view key, const std::string_view path, validation_result &result)
void require_nonnegative_integer(const json &object, const std::string_view key, const std::string_view path, validation_result &result)
void validate_artifact(const json &artifact, const std::string_view path, validation_result &result)
void validate_string_array(const json &object, const std::string_view key, const std::string_view path, const bool require_nonempty, validation_result &result)
const json * require_object(const json &object, const std::string_view key, const std::string_view path, validation_result &result)
Definition contracts.cpp:88
void require_enum(const json &object, const std::string_view key, const std::string_view path, const std::initializer_list< std::string_view > choices, validation_result &result)
void require_stable_id(const json &object, const std::string_view key, const std::string_view path, validation_result &result)
void optional_bounded_integer(const json &object, const std::string_view key, const std::string_view path, const std::uint64_t minimum, const std::uint64_t maximum, validation_result &result)
const json * require_array(const json &object, const std::string_view key, const std::string_view path, validation_result &result)
void reject_unknown_fields(const json &object, const std::string_view path, const std::initializer_list< std::string_view > allowed, validation_result &result)
Definition contracts.cpp:70
const json * optional_object(const json &object, const std::string_view key, const std::string_view path, validation_result &result)
void validate_extensions(const json &object, const std::string_view path, validation_result &result)

◆ validate_arachne_batch()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_arachne_batch ( const json & document,
validation_result & result )

Definition at line 1837 of file contracts.cpp.

1837 {
1839 document, "",
1840 { "format", "batch_id", "create", "update", "merge" }, result
1841 );
1842 const json* format = require_string(document, "format", "", result);
1843 if (format != nullptr && *format != "arachne_batch_v2") {
1844 add(result, "/format", "const",
1845 "format must be arachne_batch_v2");
1846 }
1847 require_stable_id(document, "batch_id", "", result);
1848 if (const json* create
1849 = require_object(document, "create", "", result)) {
1850 validate_create_operations(*create, "/create", result);
1851 }
1852 if (const json* update
1853 = require_object(document, "update", "", result)) {
1854 validate_update_operations(*update, "/update", result);
1855 }
1856 if (const json* merge
1857 = require_object(document, "merge", "", result)) {
1858 validate_merge_operations(*merge, "/merge", result);
1859 }
1860 }
void validate_create_operations(const json &create, const std::string_view path, validation_result &result)
void validate_merge_operations(const json &merge, const std::string_view path, validation_result &result)
void validate_update_operations(const json &update, const std::string_view path, validation_result &result)

◆ validate_artifact()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_artifact ( const json & artifact,
const std::string_view path,
validation_result & result )

Definition at line 457 of file contracts.cpp.

460 {
461 if (!artifact.is_object()) {
462 add(result, std::string(path), "type",
463 "expected an artifact object");
464 return;
465 }
467 artifact, path,
468 { "storage_ref", "sha256", "byte_length", "media_type" }, result
469 );
470 require_string(artifact, "storage_ref", path, result);
471 require_sha256(artifact, "sha256", path, result);
472 require_nonnegative_integer(artifact, "byte_length", path, result);
473 optional_string(artifact, "media_type", path, result);
474 }
void require_sha256(const json &object, const std::string_view key, const std::string_view path, validation_result &result)

◆ validate_artifact_array()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_artifact_array ( const json & object,
const std::string_view key,
const std::string_view path,
validation_result & result )

Definition at line 486 of file contracts.cpp.

489 {
490 const json* artifacts = require_array(object, key, path, result);
491 if (artifacts == nullptr) {
492 return;
493 }
494 if (artifacts->empty()) {
495 add(result, child_path(path, key), "min_items",
496 "at least one artifact is required");
497 }
498 for (std::size_t index = 0; index < artifacts->size(); ++index) {
499 const std::string item_path
500 = child_path(path, key) + "/" + std::to_string(index);
501 const json& entry = (*artifacts)[index];
502 if (!entry.is_object()) {
503 add(result, item_path, "type", "expected an export object");
504 continue;
505 }
507 entry, item_path, { "kind", "artifact" }, result
508 );
509 require_string(entry, "kind", item_path, result);
510 validate_artifact_field(entry, "artifact", item_path, result);
511 }
512 }
void validate_artifact_field(const json &object, const std::string_view key, const std::string_view path, validation_result &result)

◆ validate_artifact_field()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_artifact_field ( const json & object,
const std::string_view key,
const std::string_view path,
validation_result & result )

Definition at line 476 of file contracts.cpp.

479 {
480 const json* artifact = require_object(object, key, path, result);
481 if (artifact != nullptr) {
482 validate_artifact(*artifact, child_path(path, key), result);
483 }
484 }

◆ validate_batch_envelope()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_batch_envelope ( const json & document,
validation_result & result )

Definition at line 1863 of file contracts.cpp.

1863 {
1865 document, "",
1866 { "contract", "format_version", "envelope_id", "payload_ref",
1867 "payload_sha256", "submission_ref", "status", "accepted_by",
1868 "supersedes", "extensions" },
1869 result
1870 );
1871 require_stable_id(document, "envelope_id", "", result);
1872 require_string(document, "payload_ref", "", result);
1873 require_sha256(document, "payload_sha256", "", result);
1874 require_string(document, "submission_ref", "", result);
1876 document, "status", "",
1877 { "received", "needs_format_fix", "waiting_approval", "accepted",
1878 "waiting_processing", "processing", "integrated", "failed",
1879 "rejected", "superseded" },
1880 result
1881 );
1882 optional_string(document, "accepted_by", "", result);
1883 optional_string(document, "supersedes", "", result);
1884 validate_extensions(document, "", result);
1885 }

◆ validate_body()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_body ( const contract_name name,
const json & document,
validation_result & result )

Definition at line 2427 of file contracts.cpp.

2430 {
2431 switch (name) {
2433 validate_arachne_batch(document, result);
2434 break;
2436 validate_batch_envelope(document, result);
2437 break;
2439 validate_fetch_plan(document, result);
2440 break;
2442 validate_fetch_request(document, result);
2443 break;
2445 validate_acquired_artifact(document, result);
2446 break;
2448 validate_candidate_plan(document, result);
2449 break;
2451 validate_product_snapshot(document, result);
2452 break;
2454 validate_candidate_snapshot(document, result);
2455 break;
2457 validate_viewer_projection(document, result);
2458 break;
2460 validate_site_bundle(document, result);
2461 break;
2462 }
2463 }
void validate_fetch_request(const json &document, validation_result &result)
void validate_candidate_snapshot(const json &document, validation_result &result)
void validate_arachne_batch(const json &document, validation_result &result)
void validate_acquired_artifact(const json &document, validation_result &result)
void validate_viewer_projection(const json &document, validation_result &result)
void validate_site_bundle(const json &document, validation_result &result)
void validate_batch_envelope(const json &document, validation_result &result)
void validate_fetch_plan(const json &document, validation_result &result)
void validate_candidate_plan(const json &document, validation_result &result)
void validate_product_snapshot(const json &document, validation_result &result)

References arachnespace::contracts::acquired_artifact, arachnespace::contracts::arachne_batch, arachnespace::contracts::batch_envelope, arachnespace::contracts::fetch_plan, arachnespace::contracts::fetch_request, arachnespace::contracts::product_graph_snapshot, arachnespace::contracts::research_candidate_graph_plan, arachnespace::contracts::research_candidate_graph_snapshot, arachnespace::contracts::site_bundle, and arachnespace::contracts::viewer_projection.

◆ validate_candidate_plan()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_candidate_plan ( const json & document,
validation_result & result )

Definition at line 2244 of file contracts.cpp.

2244 {
2246 document, "",
2247 { "contract", "format_version", "plan_id", "source_snapshot",
2248 "product_snapshot", "algorithm_version", "configuration",
2249 "plan_artifact", "summary", "created_at", "extensions" },
2250 result
2251 );
2252 require_stable_id(document, "plan_id", "", result);
2253 require_string(document, "algorithm_version", "", result);
2254 require_timestamp(document, "created_at", "", result);
2255 const json* source
2256 = require_object(document, "source_snapshot", "", result);
2257 if (source != nullptr) {
2259 *source, "/source_snapshot",
2260 { "snapshot_id", "storage_ref", "sha256" }, result
2261 );
2263 *source, "snapshot_id", "/source_snapshot", result
2264 );
2265 require_string(*source, "storage_ref", "/source_snapshot", result);
2266 require_sha256(*source, "sha256", "/source_snapshot", result);
2267 }
2268 const json* product
2269 = require_object(document, "product_snapshot", "", result);
2270 if (product != nullptr) {
2272 *product, "/product_snapshot", { "snapshot_id", "sha256" },
2273 result
2274 );
2276 *product, "snapshot_id", "/product_snapshot", result
2277 );
2278 require_sha256(*product, "sha256", "/product_snapshot", result);
2279 }
2280 const json* configuration
2281 = require_object(document, "configuration", "", result);
2282 if (configuration != nullptr) {
2284 *configuration, "/configuration", { "sha256", "values" }, result
2285 );
2286 require_sha256(*configuration, "sha256", "/configuration", result);
2287 require_object(*configuration, "values", "/configuration", result);
2288 }
2289 validate_artifact_field(document, "plan_artifact", "", result);
2290 const json* summary = require_object(document, "summary", "", result);
2291 if (summary != nullptr) {
2293 *summary, "/summary",
2294 { "candidate_count", "edge_count", "group_count" }, result
2295 );
2297 *summary, "candidate_count", "/summary", result
2298 );
2300 *summary, "edge_count", "/summary", result
2301 );
2303 *summary, "group_count", "/summary", result
2304 );
2305 }
2306 validate_extensions(document, "", result);
2307 }

◆ validate_candidate_snapshot()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_candidate_snapshot ( const json & document,
validation_result & result )

Definition at line 2360 of file contracts.cpp.

2362 {
2364 document,
2365 { "contract", "format_version", "snapshot_id", "run_id",
2366 "graph_version", "content_sha256", "database", "exports",
2367 "plan_id", "source_snapshot_id", "activated_at",
2368 "structural_validation", "extensions" },
2369 result
2370 );
2371 require_stable_id(document, "plan_id", "", result);
2372 require_stable_id(document, "source_snapshot_id", "", result);
2373 }
void validate_snapshot_common(const json &document, const std::initializer_list< std::string_view > allowed, validation_result &result)

◆ validate_create_operations()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_create_operations ( const json & create,
const std::string_view path,
validation_result & result )

Definition at line 819 of file contracts.cpp.

822 {
824 create, path,
825 { "agents", "works", "concepts", "manifestations", "names",
826 "external_ids", "sources", "evidence", "credits",
827 "measurements", "financial_facts", "work_concepts",
828 "concept_relations", "parent_guide_assertions" },
829 result
830 );
831
833 create, "agents", path, result,
834 [](const json& item, const std::string& item_path,
835 validation_result& item_result) {
837 item, item_path,
838 { "local_id", "agent_type", "birth_year", "death_year" },
839 item_result
840 );
841 require_stable_id(item, "local_id", item_path, item_result);
843 item, "agent_type", item_path,
844 { "person", "organization", "group" }, item_result
845 );
847 item, item_path, { "birth_year", "death_year" },
848 item_result
849 );
850 }
851 );
852
854 create, "works", path, result,
855 [](const json& item, const std::string& item_path,
856 validation_result& item_result) {
858 item, item_path,
859 { "local_id", "medium", "year_start", "year_end",
860 "date_precision", "date_start_text", "date_end_text",
861 "date_qualifier", "language_code", "country_code",
862 "production_info_json" },
863 item_result
864 );
865 require_stable_id(item, "local_id", item_path, item_result);
867 item, "medium", item_path,
868 { "film", "short_film", "television", "novel", "novella",
869 "short_story", "poetry", "play", "essay", "album",
870 "single", "composition", "painting", "print",
871 "engraving", "drawing", "sculpture", "installation",
872 "photography", "mixed_media" },
873 item_result
874 );
876 item, item_path, { "year_start", "year_end" }, item_result
877 );
879 item, "date_precision", item_path,
880 { "year", "decade", "approximate", "range", "exact" },
881 item_result
882 );
884 item, item_path,
885 { "date_start_text", "date_end_text", "date_qualifier",
886 "language_code", "country_code",
887 "production_info_json" },
888 item_result
889 );
890 }
891 );
892
894 create, "concepts", path, result,
895 [](const json& item, const std::string& item_path,
896 validation_result& item_result) {
897 static const std::regex slug(
898 R"(^[a-z0-9]+(?:-[a-z0-9]+)*$)"
899 );
901 item, item_path, { "local_id", "concept_type", "slug" },
902 item_result
903 );
904 require_stable_id(item, "local_id", item_path, item_result);
906 item, "concept_type", item_path,
907 { "genre", "style", "theme", "keyword", "motif", "trope",
908 "phobia", "taboo", "technique", "movement", "setting",
909 "mood", "content_warning" },
910 item_result
911 );
912 require_pattern(item, "slug", item_path, slug, item_result);
913 }
914 );
915
917 create, "manifestations", path, result,
918 [](const json& item, const std::string& item_path,
919 validation_result& item_result) {
921 item, item_path,
922 { "local_id", "work_id", "manifestation_type",
923 "release_year", "region_code", "language_code", "label" },
924 item_result
925 );
926 require_stable_id(item, "local_id", item_path, item_result);
927 const json* work
928 = field(item, "work_id", item_path, item_result);
929 if (work != nullptr) {
931 *work, child_path(item_path, "work_id"), item_result
932 );
933 }
935 item, "manifestation_type", item_path,
936 { "edition", "translation", "release", "pressing", "cut",
937 "restoration", "reissue" },
938 item_result
939 );
941 item, "release_year", item_path, -9999, 9999, item_result
942 );
944 item, item_path, { "region_code", "language_code" },
945 item_result
946 );
947 require_string(item, "label", item_path, item_result);
948 }
949 );
950
952 create, "names", path, result,
953 [](const json& item, const std::string& item_path,
954 validation_result& item_result) {
956 item, item_path,
957 { "entity_id", "name_type", "language_code",
958 "script_code", "value", "is_preferred" },
959 item_result
960 );
961 const json* entity
962 = field(item, "entity_id", item_path, item_result);
963 if (entity != nullptr) {
965 *entity, child_path(item_path, "entity_id"), item_result
966 );
967 }
969 item, "name_type", item_path,
970 { "original", "english", "transliteration", "translation",
971 "alias", "credited" },
972 item_result
973 );
975 item, item_path, { "language_code", "script_code" },
976 item_result
977 );
978 require_string(item, "value", item_path, item_result);
980 item, "is_preferred", item_path, item_result
981 );
982 }
983 );
984
986 create, "external_ids", path, result,
987 [](const json& item, const std::string& item_path,
988 validation_result& item_result) {
990 item, item_path,
991 { "entity_id", "scheme", "value", "canonical_url" },
992 item_result
993 );
994 const json* entity
995 = field(item, "entity_id", item_path, item_result);
996 if (entity != nullptr) {
998 *entity, child_path(item_path, "entity_id"), item_result
999 );
1000 }
1001 require_string(item, "scheme", item_path, item_result);
1002 require_string(item, "value", item_path, item_result);
1004 item, "canonical_url", item_path, item_result
1005 );
1006 }
1007 );
1008
1010 create, "sources", path, result,
1011 [](const json& item, const std::string& item_path,
1012 validation_result& item_result) {
1014 item, item_path,
1015 { "local_id", "source_type", "title",
1016 "bibliography_text", "author_text", "publisher",
1017 "publication_date", "url", "doi", "isbn",
1018 "language_code" },
1019 item_result
1020 );
1021 require_stable_id(item, "local_id", item_path, item_result);
1023 item, "source_type", item_path,
1024 { "book", "article", "catalogue", "web_page", "interview",
1025 "database", "video", "audio", "PDF" },
1026 item_result
1027 );
1029 item, item_path,
1030 { "title", "bibliography_text", "author_text", "publisher",
1031 "publication_date", "url", "doi", "isbn",
1032 "language_code" },
1033 item_result
1034 );
1035 if (!item.contains("url") && !item.contains("doi")
1036 && !item.contains("isbn")
1037 && !item.contains("bibliography_text")) {
1038 add(item_result, item_path, "any_of",
1039 "source requires url, doi, isbn, or bibliography_text");
1040 }
1041 }
1042 );
1043
1045 create, "evidence", path, result,
1046 [](const json& item, const std::string& item_path,
1047 validation_result& item_result) {
1049 item, item_path,
1050 { "local_id", "source_id", "exact_quote",
1051 "quote_language", "quote_translation", "locator_json",
1052 "stance" },
1053 item_result
1054 );
1055 require_stable_id(item, "local_id", item_path, item_result);
1056 const json* source
1057 = field(item, "source_id", item_path, item_result);
1058 if (source != nullptr) {
1060 *source, child_path(item_path, "source_id"), item_result
1061 );
1062 }
1063 require_string(item, "exact_quote", item_path, item_result);
1065 item, item_path,
1066 { "quote_language", "quote_translation", "locator_json" },
1067 item_result
1068 );
1070 item, "stance", item_path,
1071 { "supports", "contradicts", "contextualizes" },
1072 item_result
1073 );
1074 }
1075 );
1076
1078 create, "credits", path, result,
1079 [](const json& item, const std::string& item_path,
1080 validation_result& item_result) {
1082 item, item_path,
1083 { "work_id", "agent_id", "role", "credit_order",
1084 "importance", "credited_as" },
1085 item_result
1086 );
1087 for (const std::string_view key : { "work_id", "agent_id" }) {
1088 const json* reference
1089 = field(item, key, item_path, item_result);
1090 if (reference != nullptr) {
1092 *reference, child_path(item_path, key), item_result
1093 );
1094 }
1095 }
1097 item, "role", item_path,
1098 { "author", "director", "screenwriter", "producer",
1099 "actor", "composer", "performer", "artist", "engraver",
1100 "sculptor", "photographer", "editor", "cinematographer",
1101 "production_company", "publisher", "record_label",
1102 "band" },
1103 item_result
1104 );
1106 item, "credit_order", item_path, 0,
1107 std::numeric_limits<std::int64_t>::max(), item_result
1108 );
1110 item, "importance", item_path,
1111 { "primary", "key", "supporting" }, item_result
1112 );
1113 optional_string(item, "credited_as", item_path, item_result);
1114 }
1115 );
1116
1118 create, "measurements", path, result,
1119 [](const json& item, const std::string& item_path,
1120 validation_result& item_result) {
1122 item, item_path,
1123 { "entity_id", "measurement_type", "value", "unit",
1124 "qualifier" },
1125 item_result
1126 );
1127 const json* entity
1128 = field(item, "entity_id", item_path, item_result);
1129 if (entity != nullptr) {
1131 *entity, child_path(item_path, "entity_id"), item_result
1132 );
1133 }
1135 item, "measurement_type", item_path,
1136 { "duration", "height", "width", "depth", "pages" },
1137 item_result
1138 );
1140 item, "value", item_path, 0.0,
1141 std::numeric_limits<double>::max(), item_result
1142 );
1144 item, "unit", item_path,
1145 { "seconds", "millimetres", "pages" }, item_result
1146 );
1147 optional_string(item, "qualifier", item_path, item_result);
1148 }
1149 );
1150
1152 create, "financial_facts", path, result,
1153 [](const json& item, const std::string& item_path,
1154 validation_result& item_result) {
1155 static const std::regex currency(R"(^[A-Z]{3}$)");
1157 item, item_path,
1158 { "work_id", "fact_type", "amount_min", "amount_max",
1159 "currency_code", "value_year", "is_estimate",
1160 "confidence" },
1161 item_result
1162 );
1163 const json* work
1164 = field(item, "work_id", item_path, item_result);
1165 if (work != nullptr) {
1167 *work, child_path(item_path, "work_id"), item_result
1168 );
1169 }
1170 const json* fact
1171 = require_string(item, "fact_type", item_path, item_result);
1172 if (fact != nullptr && *fact != "budget") {
1173 add(item_result, child_path(item_path, "fact_type"),
1174 "const", "fact_type must be budget");
1175 }
1177 item, "amount_min", item_path, 0,
1178 std::numeric_limits<std::int64_t>::max(), item_result
1179 );
1181 item, "amount_max", item_path, 0,
1182 std::numeric_limits<std::int64_t>::max(), item_result
1183 );
1185 item, "currency_code", item_path, currency, item_result
1186 );
1188 item, "value_year", item_path, -9999, 9999, item_result
1189 );
1191 item, "is_estimate", item_path, item_result
1192 );
1194 item, "confidence", item_path, 0.0, 1.0, item_result
1195 );
1196 }
1197 );
1198
1200 create, "work_concepts", path, result,
1201 [](const json& item, const std::string& item_path,
1202 validation_result& item_result) {
1204 item, item_path,
1205 { "local_id", "work_id", "concept_id", "relation_type",
1206 "centrality", "historical_role", "confidence",
1207 "evidence" },
1208 item_result
1209 );
1210 require_stable_id(item, "local_id", item_path, item_result);
1211 for (const std::string_view key : { "work_id", "concept_id" }) {
1212 const json* reference
1213 = field(item, key, item_path, item_result);
1214 if (reference != nullptr) {
1216 *reference, child_path(item_path, key), item_result
1217 );
1218 }
1219 }
1221 item, "relation_type", item_path,
1222 { "exemplifies", "contains", "anticipates",
1223 "influenced_by", "influences", "revives", "parodies",
1224 "deconstructs", "associated_with" },
1225 item_result
1226 );
1228 item, "centrality", item_path, 1, 100, item_result
1229 );
1231 item, "historical_role", item_path,
1232 { "formative", "canonical", "transitional", "hybrid",
1233 "revival", "late_derivative", "peripheral", "precursor" },
1234 item_result
1235 );
1237 item, "confidence", item_path, 0.0, 1.0, item_result
1238 );
1240 item, "evidence", item_path, item_result
1241 );
1242 }
1243 );
1244
1246 create, "concept_relations", path, result,
1247 [](const json& item, const std::string& item_path,
1248 validation_result& item_result) {
1250 item, item_path,
1251 { "local_id", "subject_concept_id", "relation_type",
1252 "object_concept_id", "strength", "from_year", "to_year",
1253 "region_code", "confidence", "evidence" },
1254 item_result
1255 );
1256 require_stable_id(item, "local_id", item_path, item_result);
1257 for (const std::string_view key :
1258 { "subject_concept_id", "object_concept_id" }) {
1259 const json* reference
1260 = field(item, key, item_path, item_result);
1261 if (reference != nullptr) {
1263 *reference, child_path(item_path, key), item_result
1264 );
1265 }
1266 }
1268 item, "relation_type", item_path,
1269 { "broader_than", "narrower_than", "derived_from",
1270 "precursor_of", "hybrid_of", "revival_of",
1271 "regional_variant_of", "influenced_by", "opposes",
1272 "alias_of" },
1273 item_result
1274 );
1276 item, "strength", item_path, 1, 100, item_result
1277 );
1279 item, item_path, { "from_year", "to_year" }, item_result
1280 );
1281 optional_string(item, "region_code", item_path, item_result);
1283 item, "confidence", item_path, 0.0, 1.0, item_result
1284 );
1286 item, "evidence", item_path, item_result
1287 );
1288 }
1289 );
1290
1292 create, "parent_guide_assertions", path, result,
1293 [](const json& item, const std::string& item_path,
1294 validation_result& item_result) {
1296 item, item_path,
1297 { "local_id", "work_id", "concept_id", "category",
1298 "intensity", "explicitness", "frequency", "centrality",
1299 "realism", "spoiler_level", "confidence", "evidence" },
1300 item_result
1301 );
1302 require_stable_id(item, "local_id", item_path, item_result);
1303 for (const std::string_view key : { "work_id", "concept_id" }) {
1304 const json* reference
1305 = field(item, key, item_path, item_result);
1306 if (reference != nullptr) {
1308 *reference, child_path(item_path, key), item_result
1309 );
1310 }
1311 }
1313 item, "category", item_path,
1314 { "violence", "sex_nudity", "language", "drugs",
1315 "frightening", "self_harm", "discrimination", "abuse",
1316 "taboo" },
1317 item_result
1318 );
1319 for (const std::string_view key :
1320 { "intensity", "explicitness", "frequency", "centrality",
1321 "realism" }) {
1323 item, key, item_path, 1, 5, item_result
1324 );
1325 }
1327 item, "spoiler_level", item_path,
1328 { "none", "mild", "major" }, item_result
1329 );
1331 item, "confidence", item_path, 0.0, 1.0, item_result
1332 );
1334 item, "evidence", item_path, item_result
1335 );
1336 }
1337 );
1338 }
void require_boolean(const json &object, const std::string_view key, const std::string_view path, validation_result &result)
void validate_database_or_local_reference(const json &value, const std::string_view path, validation_result &result)
void optional_enum(const json &object, const std::string_view key, const std::string_view path, const std::initializer_list< std::string_view > choices, validation_result &result)
void validate_optional_object_array(const json &object, const std::string_view key, const std::string_view path, validation_result &result, Validator &&validator)
void validate_evidence_references(const json &object, const std::string_view key, const std::string_view path, validation_result &result)
void require_integer_range(const json &object, const std::string_view key, const std::string_view path, const std::int64_t minimum, const std::int64_t maximum, validation_result &result)
void require_pattern(const json &object, const std::string_view key, const std::string_view path, const std::regex &expression, validation_result &result)
void validate_nonempty_optional_strings(const json &object, const std::string_view path, const std::initializer_list< std::string_view > keys, validation_result &result)
void validate_year_fields(const json &object, const std::string_view path, const std::initializer_list< std::string_view > keys, validation_result &result)
void validate_stable_reference(const json &value, const std::string_view path, validation_result &result)
void require_number_range(const json &object, const std::string_view key, const std::string_view path, const double minimum, const double maximum, validation_result &result)
@ item
IDs prefixed with 'Q'.
Definition utils.hpp:48

◆ validate_database_or_local_reference()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_database_or_local_reference ( const json & value,
const std::string_view path,
validation_result & result )

Definition at line 680 of file contracts.cpp.

683 {
684 if (value.is_string()) {
685 validate_stable_reference(value, path, result);
686 static const std::regex canonical_entity(
687 R"(^(agent|work|concept|manifestation)-[0-9]{6,}$)"
688 );
689 if (std::regex_match(
690 value.get_ref<const std::string&>(), canonical_entity
691 )) {
692 add(result, std::string(path), "reserved_identifier",
693 "local references must not use a canonical entity ID pattern");
694 }
695 return;
696 }
697 const bool positive_integer
698 = (value.is_number_unsigned() && value.get<std::uint64_t>() >= 1)
699 || (value.is_number_integer() && value.get<std::int64_t>() >= 1);
700 if (!positive_integer) {
701 add(result, std::string(path), "type",
702 "expected a positive database ID or stable local reference");
703 }
704 }

◆ validate_entity_id()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_entity_id ( const json & object,
const std::string_view key,
const std::string_view path,
const std::string_view family,
validation_result & result )

Definition at line 763 of file contracts.cpp.

767 {
768 const std::regex expression(
769 "^" + std::string(family) + R"(-[0-9]{6,}$)"
770 );
771 require_pattern(object, key, path, expression, result);
772 }

◆ validate_evidence_references()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_evidence_references ( const json & object,
const std::string_view key,
const std::string_view path,
validation_result & result )

Definition at line 797 of file contracts.cpp.

800 {
801 const json* values = require_array(object, key, path, result);
802 if (values == nullptr) {
803 return;
804 }
805 const std::string array_path = child_path(path, key);
806 if (values->empty()) {
807 add(result, array_path, "min_items",
808 "at least one source-backed evidence reference is required");
809 }
810 validate_unique_items(*values, array_path, result);
811 for (std::size_t index = 0; index < values->size(); ++index) {
813 (*values)[index],
814 array_path + "/" + std::to_string(index), result
815 );
816 }
817 }
void validate_unique_items(const json &array, const std::string_view path, validation_result &result)

◆ validate_extensions()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_extensions ( const json & object,
const std::string_view path,
validation_result & result )

Definition at line 433 of file contracts.cpp.

436 {
437 static const std::regex expression(
438 R"(^[a-z][a-z0-9]*(\.[a-z0-9][a-z0-9_-]*)+$)"
439 );
440 const json* extensions
441 = optional_object(object, "extensions", path, result);
442 if (extensions == nullptr) {
443 return;
444 }
445 for (const auto& [key, unused] : extensions->items()) {
446 (void)unused;
447 if (!std::regex_match(key, expression)) {
448 add(
449 result, child_path(child_path(path, "extensions"), key),
450 "extension_namespace",
451 "extension keys must be reverse-DNS-style namespaced names"
452 );
453 }
454 }
455 }

◆ validate_fetch_plan()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_fetch_plan ( const json & document,
validation_result & result )

Definition at line 1887 of file contracts.cpp.

1887 {
1889 document, "",
1890 { "contract", "format_version", "plan_id", "source", "requests",
1891 "created_at", "extensions" },
1892 result
1893 );
1894 require_stable_id(document, "plan_id", "", result);
1895 require_string(document, "source", "", result);
1896 require_timestamp(document, "created_at", "", result);
1897 const json* requests = require_array(document, "requests", "", result);
1898 if (requests != nullptr && requests->empty()) {
1899 add(result, "/requests", "min_items",
1900 "a fetch plan must contain at least one request");
1901 }
1902 if (requests != nullptr) {
1903 for (std::size_t index = 0; index < requests->size(); ++index) {
1904 const std::string path = "/requests/" + std::to_string(index);
1905 const json& request = (*requests)[index];
1906 if (!request.is_object()) {
1907 add(result, path, "type", "expected a request object");
1908 continue;
1909 }
1911 request, path,
1912 { "request_id", "locator", "purpose", "entities", "fields",
1913 "pages", "archives", "follow_up" },
1914 result
1915 );
1916 require_stable_id(request, "request_id", path, result);
1917 require_string(request, "locator", path, result);
1918 require_string(request, "purpose", path, result);
1919 }
1920 }
1921 validate_extensions(document, "", result);
1922 }

◆ validate_fetch_request()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_fetch_request ( const json & document,
validation_result & result )

Definition at line 1925 of file contracts.cpp.

1925 {
1927 document, "",
1928 { "contract", "format_version", "request_id",
1929 "door_id", "endpoint_id", "operation",
1930 "freshness_policy", "idempotency_key", "plan_id",
1931 "locator", "method", "headers",
1932 "pagination", "retry", "expected",
1933 "redirect_policy", "output_ref", "body_artifact",
1934 "resume_artifact", "extensions" },
1935 result
1936 );
1937 require_stable_id(document, "request_id", "", result);
1938 if (document.contains("door_id")) {
1939 require_stable_id(document, "door_id", "", result);
1940 }
1941 if (document.contains("endpoint_id")) {
1942 require_stable_id(document, "endpoint_id", "", result);
1943 }
1944 if (document.contains("operation")) {
1946 document, "operation", "",
1947 { "bulk_snapshot", "incremental_harvest", "point_lookup",
1948 "resume_download", "backend_read", "external_write" },
1949 result
1950 );
1951 }
1952 if (document.contains("freshness_policy")) {
1954 document, "freshness_policy", "",
1955 { "fresh_required", "cache_allowed", "stale_allowed",
1956 "offline_only" },
1957 result
1958 );
1959 }
1960 optional_string(document, "idempotency_key", "", result);
1961 optional_string(document, "plan_id", "", result);
1962 require_string(document, "locator", "", result);
1963 require_enum(document, "method", "", { "GET", "POST" }, result);
1964 require_string(document, "output_ref", "", result);
1965
1966 optional_object(document, "headers", "", result);
1967 if (const json* pagination
1968 = optional_object(document, "pagination", "", result)) {
1970 *pagination, "/pagination", { "mode" }, result
1971 );
1973 *pagination, "mode", "/pagination",
1974 { "none", "link_header", "cursor", "page_number" }, result
1975 );
1976 }
1977 if (const json* retry
1978 = optional_object(document, "retry", "", result)) {
1980 *retry, "/retry",
1981 { "maximum_attempts", "initial_delay_ms", "maximum_delay_ms",
1982 "total_delay_budget_ms", "respect_retry_after" },
1983 result
1984 );
1986 *retry, "maximum_attempts", "/retry", 1, 20, result
1987 );
1989 *retry, "initial_delay_ms", "/retry", 0, 60'000, result
1990 );
1992 *retry, "maximum_delay_ms", "/retry", 0, 3'600'000, result
1993 );
1995 *retry, "total_delay_budget_ms", "/retry", 0, 3'600'000, result
1996 );
1997 if (const auto value = retry->find("respect_retry_after");
1998 value != retry->end() && !value->is_boolean()) {
1999 add(result, "/retry/respect_retry_after", "type",
2000 "expected a boolean");
2001 }
2002 }
2003 if (const json* expected
2004 = optional_object(document, "expected", "", result)) {
2006 *expected, "/expected",
2007 { "maximum_bytes", "timeout_ms", "connect_timeout_ms",
2008 "read_timeout_ms", "write_timeout_ms", "sha256" },
2009 result
2010 );
2012 *expected, "maximum_bytes", "/expected", 1,
2013 1'099'511'627'776ULL, result
2014 );
2016 *expected, "timeout_ms", "/expected", 1, 86'400'000, result
2017 );
2019 *expected, "connect_timeout_ms", "/expected", 1, 86'400'000,
2020 result
2021 );
2023 *expected, "read_timeout_ms", "/expected", 1, 86'400'000, result
2024 );
2026 *expected, "write_timeout_ms", "/expected", 1, 86'400'000,
2027 result
2028 );
2029 if (expected->contains("sha256")) {
2030 require_sha256(*expected, "sha256", "/expected", result);
2031 }
2032 }
2033 if (const json* redirect
2034 = optional_object(document, "redirect_policy", "", result)) {
2036 *redirect, "/redirect_policy",
2037 { "follow", "maximum_redirects", "allow_https_to_http",
2038 "allowed_hosts" },
2039 result
2040 );
2041 for (const std::string_view key :
2042 { "follow", "allow_https_to_http" }) {
2043 const json* value
2044 = field(*redirect, key, "/redirect_policy", result);
2045 if (value != nullptr && !value->is_boolean()) {
2046 add(result, child_path("/redirect_policy", key), "type",
2047 "expected a boolean");
2048 }
2049 }
2050 field(*redirect, "maximum_redirects", "/redirect_policy", result);
2052 *redirect, "maximum_redirects", "/redirect_policy", 0, 20,
2053 result
2054 );
2056 *redirect, "allowed_hosts", "/redirect_policy", true, result
2057 );
2058 const auto hosts = redirect->find("allowed_hosts");
2059 if (hosts != redirect->end() && hosts->is_array()) {
2060 static const std::regex host_expression(
2061 R"(^[A-Za-z0-9.-]+(:[0-9]{1,5})?$)"
2062 );
2063 if (hosts->size() > 128) {
2064 add(result, "/redirect_policy/allowed_hosts", "max_items",
2065 "at most 128 allowed hosts may be declared");
2066 }
2067 std::set<std::string> seen;
2068 for (std::size_t index = 0; index < hosts->size(); ++index) {
2069 const json& host = (*hosts)[index];
2070 if (!host.is_string() || host.empty()) {
2071 continue;
2072 }
2073 const std::string& value
2074 = host.get_ref<const std::string&>();
2075 const std::string path = "/redirect_policy/allowed_hosts/"
2076 + std::to_string(index);
2077 if (value.size() > 253
2078 || !std::regex_match(value, host_expression)) {
2079 add(result, path, "host",
2080 "expected a host or host:port without scheme or "
2081 "path");
2082 }
2083 if (!seen.insert(value).second) {
2084 add(result, path, "unique",
2085 "allowed hosts must be unique");
2086 }
2087 }
2088 }
2089 }
2090 if (const json* body
2091 = optional_object(document, "body_artifact", "", result)) {
2092 validate_artifact(*body, "/body_artifact", result);
2093 }
2094 if (const json* partial
2095 = optional_object(document, "resume_artifact", "", result)) {
2096 validate_artifact(*partial, "/resume_artifact", result);
2097 }
2098 const bool resume_operation
2099 = document.value("operation", "point_lookup") == "resume_download";
2100 if (resume_operation && !document.contains("resume_artifact")) {
2101 add(result, "/resume_artifact", "required",
2102 "resume_download requires a partial artifact");
2103 } else if (!resume_operation && document.contains("resume_artifact")) {
2104 add(result, "/resume_artifact", "operation",
2105 "resume_artifact is reserved for resume_download");
2106 }
2107 validate_extensions(document, "", result);
2108 }

◆ validate_header()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_header ( const contract_name expected,
const json & document,
validation_result & result )

Definition at line 613 of file contracts.cpp.

616 {
617 if (expected == contract_name::arachne_batch) {
618 const auto discovered = inspect_contract(document, result);
619 if (discovered.has_value() && *discovered != expected) {
620 add(result, document.contains("format") ? "/format" : "/contract",
621 "contract_mismatch",
622 "expected arachne_batch_v2 but received "
623 + std::string(to_string(*discovered)));
624 }
625 return;
626 }
627 {
628 const auto discovered = inspect_contract(document, result);
629 if (discovered.has_value() && *discovered != expected) {
630 add(result, "/contract", "contract_mismatch",
631 "expected " + std::string(to_string(expected))
632 + " but received "
633 + std::string(to_string(*discovered)));
634 }
635 }
636 const auto version = document.find("format_version");
637 if (version == document.end()) {
638 add(result, "/format_version", "required",
639 "required field is missing");
640 } else if (
641 !version->is_number_integer() && !version->is_number_unsigned()
642 ) {
643 add(result, "/format_version", "type",
644 "format_version must be an integer");
645 } else if (*version != 1) {
646 add(result, "/format_version", "unsupported_version",
647 "only format version 1 is supported");
648 }
649 }
std::optional< contract_name > inspect_contract(const json &document, validation_result &result)
std::string_view to_string(contract_name name) noexcept

References arachnespace::contracts::arachne_batch.

◆ validate_merge_operations()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_merge_operations ( const json & merge,
const std::string_view path,
validation_result & result )

Definition at line 1666 of file contracts.cpp.

1669 {
1671 merge, path, { "agents", "works", "concepts" }, result
1672 );
1673 const auto validate_merge = [&merge, path, &result](
1674 const std::string_view key,
1675 const std::string_view family,
1676 const auto& validate_set,
1677 const auto set_fields,
1678 const auto unset_fields,
1679 const bool unset_forbidden) {
1681 merge, key, path, result,
1682 [family, &validate_set, set_fields, unset_fields,
1683 unset_forbidden](
1684 const json& item, const std::string& item_path,
1685 validation_result& item_result
1686 ) {
1688 item, item_path,
1689 { "target", "members", "set", "unset" }, item_result
1690 );
1692 item, "target", item_path, family, item_result
1693 );
1694 const json* members
1695 = require_array(item, "members", item_path, item_result);
1696 const json* set
1697 = require_object(item, "set", item_path, item_result);
1698 const json* unset
1699 = require_array(item, "unset", item_path, item_result);
1700 if (members != nullptr) {
1701 const std::string members_path
1702 = child_path(item_path, "members");
1703 if (members->empty()) {
1704 add(item_result, members_path, "min_items",
1705 "merge requires at least one member");
1706 }
1708 *members, members_path, item_result
1709 );
1710 const std::regex identifier(
1711 "^" + std::string(family) + R"(-[0-9]{6,}$)"
1712 );
1713 for (std::size_t index = 0; index < members->size();
1714 ++index) {
1715 const json& value = (*members)[index];
1716 if (!value.is_string()
1717 || !std::regex_match(
1718 value.is_string()
1719 ? value.get_ref<const std::string&>()
1720 : std::string {},
1721 identifier
1722 )) {
1723 add(item_result,
1724 members_path + "/"
1725 + std::to_string(index),
1726 "pattern",
1727 "member uses the wrong canonical family");
1728 }
1729 }
1730 }
1731 if (set != nullptr) {
1732 const std::string set_path
1733 = child_path(item_path, "set");
1735 *set, set_path, set_fields, item_result
1736 );
1737 validate_set(*set, set_path, item_result);
1738 }
1739 if (unset != nullptr) {
1740 const std::string unset_path
1741 = child_path(item_path, "unset");
1742 validate_unique_items(*unset, unset_path, item_result);
1743 if (unset_forbidden && !unset->empty()) {
1744 add(item_result, unset_path, "max_items",
1745 "fields cannot be unset for this family");
1746 }
1747 for (std::size_t index = 0; index < unset->size();
1748 ++index) {
1749 const json& value = (*unset)[index];
1750 if (!unset_forbidden
1751 && (!value.is_string()
1752 || std::ranges::find(
1753 unset_fields,
1754 std::string_view(
1755 value.is_string()
1756 ? value.get_ref<
1757 const std::string&>()
1758 : std::string {}
1759 )
1760 )
1761 == unset_fields.end())) {
1762 add(item_result,
1763 unset_path + "/"
1764 + std::to_string(index),
1765 "enum",
1766 "field cannot be removed during merge");
1767 }
1768 }
1769 }
1770 }
1771 );
1772 };
1773
1774 validate_merge(
1775 "agents", "agent",
1776 [](const json& set, const std::string& set_path,
1777 validation_result& item_result) {
1779 set, "agent_type", set_path,
1780 { "person", "organization", "group" }, item_result
1781 );
1783 set, set_path, { "birth_year", "death_year" }, item_result
1784 );
1785 },
1786 std::initializer_list<std::string_view> {
1787 "agent_type", "birth_year", "death_year"
1788 },
1789 std::initializer_list<std::string_view> {
1790 "birth_year", "death_year"
1791 },
1792 false
1793 );
1794 validate_merge(
1795 "works", "work",
1796 [](const json& set, const std::string& set_path,
1797 validation_result& item_result) {
1798 validate_work_set(set, set_path, item_result);
1799 },
1800 std::initializer_list<std::string_view> {
1801 "medium", "year_start", "year_end", "date_precision",
1802 "date_start_text", "date_end_text", "date_qualifier",
1803 "language_code", "country_code", "production_info_json"
1804 },
1805 std::initializer_list<std::string_view> {
1806 "year_start", "year_end", "date_precision", "date_start_text",
1807 "date_end_text", "date_qualifier", "language_code",
1808 "country_code", "production_info_json"
1809 },
1810 false
1811 );
1812 validate_merge(
1813 "concepts", "concept",
1814 [](const json& set, const std::string& set_path,
1815 validation_result& item_result) {
1816 static const std::regex slug(
1817 R"(^[a-z0-9]+(?:-[a-z0-9]+)*$)"
1818 );
1820 set, "concept_type", set_path,
1821 { "genre", "style", "theme", "keyword", "motif", "trope",
1822 "phobia", "taboo", "technique", "movement", "setting",
1823 "mood", "content_warning" },
1824 item_result
1825 );
1827 set, "slug", set_path, slug, item_result
1828 );
1829 },
1830 std::initializer_list<std::string_view> { "concept_type", "slug" },
1831 std::initializer_list<std::string_view> {},
1832 true
1833 );
1834 }
void validate_work_set(const json &set, const std::string_view path, validation_result &result)
void validate_entity_id(const json &object, const std::string_view key, const std::string_view path, const std::string_view family, validation_result &result)
void optional_pattern(const json &object, const std::string_view key, const std::string_view path, const std::regex &expression, validation_result &result)

◆ validate_nonempty_optional_strings()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_nonempty_optional_strings ( const json & object,
const std::string_view path,
const std::initializer_list< std::string_view > keys,
validation_result & result )

Definition at line 743 of file contracts.cpp.

747 {
748 for (const std::string_view key : keys) {
749 optional_string(object, key, path, result);
750 }
751 }

◆ validate_optional_object_array()

template<typename Validator>
void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_optional_object_array ( const json & object,
const std::string_view key,
const std::string_view path,
validation_result & result,
Validator && validator )

Definition at line 721 of file contracts.cpp.

725 {
726 const json* values = optional_array(object, key, path, result);
727 if (values == nullptr) {
728 return;
729 }
730 const std::string array_path = child_path(path, key);
731 for (std::size_t index = 0; index < values->size(); ++index) {
732 const std::string item_path
733 = array_path + "/" + std::to_string(index);
734 const json& item = (*values)[index];
735 if (!item.is_object()) {
736 add(result, item_path, "type", "expected a JSON object");
737 continue;
738 }
739 validator(item, item_path, result);
740 }
741 }
const json * optional_array(const json &object, const std::string_view key, const std::string_view path, validation_result &result)

◆ validate_positive_id_array()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_positive_id_array ( const json & object,
const std::string_view key,
const std::string_view path,
validation_result & result )

Definition at line 774 of file contracts.cpp.

777 {
778 const json* values = optional_array(object, key, path, result);
779 if (values == nullptr) {
780 return;
781 }
782 validate_unique_items(*values, child_path(path, key), result);
783 for (std::size_t index = 0; index < values->size(); ++index) {
784 const json& value = (*values)[index];
785 const bool positive_integer = (value.is_number_unsigned()
786 && value.get<std::uint64_t>() >= 1)
787 || (value.is_number_integer()
788 && value.get<std::int64_t>() >= 1);
789 if (!positive_integer) {
790 add(result,
791 child_path(path, key) + "/" + std::to_string(index),
792 "minimum", "expected a positive integer database ID");
793 }
794 }
795 }

◆ validate_product_snapshot()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_product_snapshot ( const json & document,
validation_result & result )

Definition at line 2346 of file contracts.cpp.

2346 {
2348 document,
2349 { "contract", "format_version", "snapshot_id", "run_id",
2350 "graph_version", "content_sha256", "database", "exports",
2351 "cocoon_ids", "activated_at", "structural_validation",
2352 "extensions" },
2353 result
2354 );
2355 if (document.contains("cocoon_ids")) {
2356 validate_string_array(document, "cocoon_ids", "", false, result);
2357 }
2358 }

◆ validate_site_bundle()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_site_bundle ( const json & document,
validation_result & result )

Definition at line 2408 of file contracts.cpp.

2408 {
2410 document, "",
2411 { "contract", "format_version", "bundle_id", "projection_id",
2412 "product_snapshot_id", "candidate_snapshot_id", "viewer_version",
2413 "entrypoint", "bundle", "generated_at", "extensions" },
2414 result
2415 );
2416 require_stable_id(document, "bundle_id", "", result);
2417 require_stable_id(document, "projection_id", "", result);
2418 require_stable_id(document, "product_snapshot_id", "", result);
2419 optional_string(document, "candidate_snapshot_id", "", result);
2420 require_string(document, "viewer_version", "", result);
2421 require_string(document, "entrypoint", "", result);
2422 validate_artifact_field(document, "bundle", "", result);
2423 require_timestamp(document, "generated_at", "", result);
2424 validate_extensions(document, "", result);
2425 }

◆ validate_snapshot_common()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_snapshot_common ( const json & document,
const std::initializer_list< std::string_view > allowed,
validation_result & result )

Definition at line 2322 of file contracts.cpp.

2326 {
2327 reject_unknown_fields(document, "", allowed, result);
2328 require_stable_id(document, "snapshot_id", "", result);
2329 require_stable_id(document, "run_id", "", result);
2330 require_string(document, "graph_version", "", result);
2331 require_sha256(document, "content_sha256", "", result);
2332 validate_artifact_field(document, "database", "", result);
2333 validate_artifact_array(document, "exports", "", result);
2334 require_timestamp(document, "activated_at", "", result);
2335 const json* validation
2336 = require_object(document, "structural_validation", "", result);
2337 if (validation != nullptr) {
2339 *validation, "/structural_validation", result
2340 );
2341 }
2342 validate_extensions(document, "", result);
2343 }
void validate_artifact_array(const json &object, const std::string_view key, const std::string_view path, validation_result &result)
void validate_structural_validation(const json &value, const std::string_view path, validation_result &result)

◆ validate_stable_reference()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_stable_reference ( const json & value,
const std::string_view path,
validation_result & result )

Definition at line 651 of file contracts.cpp.

654 {
655 static const std::regex expression(
656 R"(^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$)"
657 );
658 static const std::regex canonical_entity(
659 R"(^(agent|work|concept|manifestation)-[0-9]{6,}$)"
660 );
661 if (!value.is_string()) {
662 add(result, std::string(path), "type",
663 "expected a stable string reference");
664 } else if (!std::regex_match(
665 value.get_ref<const std::string&>(), expression
666 )) {
667 add(result, std::string(path), "pattern",
668 "expected a stable identifier of at most 128 characters");
669 } else if (
670 path.ends_with("/local_id")
671 && std::regex_match(
672 value.get_ref<const std::string&>(), canonical_entity
673 )
674 ) {
675 add(result, std::string(path), "reserved_identifier",
676 "local IDs must not use a canonical entity ID pattern");
677 }
678 }

◆ validate_string_array()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_string_array ( const json & object,
const std::string_view key,
const std::string_view path,
const bool require_nonempty,
validation_result & result )

Definition at line 514 of file contracts.cpp.

518 {
519 const json* values = require_array(object, key, path, result);
520 if (values == nullptr) {
521 return;
522 }
523 if (require_nonempty && values->empty()) {
524 add(result, child_path(path, key), "min_items",
525 "array must contain at least one item");
526 }
527 for (std::size_t index = 0; index < values->size(); ++index) {
528 if (!(*values)[index].is_string() || (*values)[index].empty()) {
529 add(result, child_path(path, key) + "/" + std::to_string(index),
530 "type", "expected a non-empty string");
531 }
532 }
533 }

◆ validate_structural_validation()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_structural_validation ( const json & value,
const std::string_view path,
validation_result & result )

Definition at line 2309 of file contracts.cpp.

2312 {
2313 reject_unknown_fields(value, path, { "passed", "report" }, result);
2314 const json* passed = field(value, "passed", path, result);
2315 if (passed != nullptr && !passed->is_boolean()) {
2316 add(result, child_path(path, "passed"), "type",
2317 "expected a boolean");
2318 }
2319 validate_artifact_field(value, "report", path, result);
2320 }

◆ validate_unique_items()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_unique_items ( const json & array,
const std::string_view path,
validation_result & result )

Definition at line 706 of file contracts.cpp.

709 {
710 std::set<std::string> seen;
711 for (std::size_t index = 0; index < array.size(); ++index) {
712 const std::string key = array[index].dump();
713 if (!seen.insert(key).second) {
714 add(result, std::string(path) + "/" + std::to_string(index),
715 "unique_items", "array items must be unique");
716 }
717 }
718 }

◆ validate_update_fields()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_update_fields ( const json & item,
const std::string_view item_path,
const std::string_view family,
const std::initializer_list< std::string_view > set_fields,
const std::initializer_list< std::string_view > unset_fields,
validation_result & result )

Definition at line 1340 of file contracts.cpp.

1346 {
1348 item, item_path, { "id", "set", "unset" }, result
1349 );
1350 validate_entity_id(item, "id", item_path, family, result);
1351 const json* set = require_object(item, "set", item_path, result);
1352 const json* unset = require_array(item, "unset", item_path, result);
1353 if (set != nullptr) {
1354 reject_unknown_fields(*set, child_path(item_path, "set"),
1355 set_fields, result);
1356 }
1357 if (unset != nullptr) {
1358 const std::string unset_path = child_path(item_path, "unset");
1359 validate_unique_items(*unset, unset_path, result);
1360 for (std::size_t index = 0; index < unset->size(); ++index) {
1361 const json& value = (*unset)[index];
1362 if (!value.is_string()
1363 || std::ranges::find(
1364 unset_fields,
1365 std::string_view(
1366 value.is_string()
1367 ? value.get_ref<const std::string&>()
1368 : std::string {}
1369 )
1370 )
1371 == unset_fields.end()) {
1372 add(result, unset_path + "/" + std::to_string(index),
1373 "enum", "field cannot be removed from this entity");
1374 }
1375 }
1376 }
1377 if (set != nullptr && unset != nullptr && set->empty()
1378 && unset->empty()) {
1379 add(result, std::string(item_path), "min_operations",
1380 "update must set or unset at least one field");
1381 }
1382 }

◆ validate_update_operations()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_update_operations ( const json & update,
const std::string_view path,
validation_result & result )

Definition at line 1409 of file contracts.cpp.

1412 {
1414 update, path,
1415 { "agents", "works", "concepts", "manifestations", "sources",
1416 "delete" },
1417 result
1418 );
1420 update, "agents", path, result,
1421 [](const json& item, const std::string& item_path,
1422 validation_result& item_result) {
1424 item, item_path, "agent",
1425 { "birth_year", "death_year" },
1426 { "birth_year", "death_year" }, item_result
1427 );
1428 if (const json* set = item.find("set") != item.end()
1429 && item["set"].is_object()
1430 ? &item["set"]
1431 : nullptr) {
1433 *set, child_path(item_path, "set"),
1434 { "birth_year", "death_year" }, item_result
1435 );
1436 }
1437 }
1438 );
1440 update, "works", path, result,
1441 [](const json& item, const std::string& item_path,
1442 validation_result& item_result) {
1444 item, item_path, "work",
1445 { "medium", "year_start", "year_end", "date_precision",
1446 "date_start_text", "date_end_text", "date_qualifier",
1447 "language_code", "country_code",
1448 "production_info_json" },
1449 { "year_start", "year_end", "date_precision",
1450 "date_start_text", "date_end_text", "date_qualifier",
1451 "language_code", "country_code",
1452 "production_info_json" },
1453 item_result
1454 );
1455 if (item.contains("set") && item["set"].is_object()) {
1457 item["set"], child_path(item_path, "set"), item_result
1458 );
1459 }
1460 }
1461 );
1463 update, "concepts", path, result,
1464 [](const json& item, const std::string& item_path,
1465 validation_result& item_result) {
1466 static const std::regex slug(
1467 R"(^[a-z0-9]+(?:-[a-z0-9]+)*$)"
1468 );
1470 item, item_path, "concept", { "concept_type", "slug" },
1471 {}, item_result
1472 );
1473 if (item.contains("unset") && item["unset"].is_array()
1474 && !item["unset"].empty()) {
1475 add(item_result, child_path(item_path, "unset"),
1476 "max_items", "concept fields cannot be unset");
1477 }
1478 if (item.contains("set") && item["set"].is_object()) {
1479 const json& set = item["set"];
1480 if (set.empty()) {
1481 add(item_result, child_path(item_path, "set"),
1482 "min_properties",
1483 "concept update must set at least one field");
1484 }
1486 set, "concept_type", child_path(item_path, "set"),
1487 { "genre", "style", "theme", "keyword", "motif",
1488 "trope", "phobia", "taboo", "technique", "movement",
1489 "setting", "mood", "content_warning" },
1490 item_result
1491 );
1493 set, "slug", child_path(item_path, "set"), slug,
1494 item_result
1495 );
1496 }
1497 }
1498 );
1500 update, "manifestations", path, result,
1501 [](const json& item, const std::string& item_path,
1502 validation_result& item_result) {
1504 item, item_path, "manifestation",
1505 { "work_id", "manifestation_type", "release_year",
1506 "region_code", "language_code", "label" },
1507 { "release_year", "region_code", "language_code" },
1508 item_result
1509 );
1510 if (item.contains("set") && item["set"].is_object()) {
1511 const json& set = item["set"];
1512 const std::string set_path = child_path(item_path, "set");
1513 static const std::regex work_id(R"(^work-[0-9]{6,}$)");
1515 set, "work_id", set_path, work_id, item_result
1516 );
1518 set, "manifestation_type", set_path,
1519 { "edition", "translation", "release", "pressing",
1520 "cut", "restoration", "reissue" },
1521 item_result
1522 );
1524 set, "release_year", set_path, -9999, 9999, item_result
1525 );
1527 set, set_path,
1528 { "region_code", "language_code", "label" },
1529 item_result
1530 );
1531 }
1532 }
1533 );
1535 update, "sources", path, result,
1536 [](const json& item, const std::string& item_path,
1537 validation_result& item_result) {
1539 item, item_path, { "id", "set", "unset" }, item_result
1540 );
1542 item, "id", item_path, 1,
1543 std::numeric_limits<std::int64_t>::max(), item_result
1544 );
1545 const json* set
1546 = require_object(item, "set", item_path, item_result);
1547 const json* unset
1548 = require_array(item, "unset", item_path, item_result);
1549 const std::string set_path = child_path(item_path, "set");
1550 if (set != nullptr) {
1552 *set, set_path,
1553 { "source_type", "title", "bibliography_text",
1554 "author_text", "publisher", "publication_date", "url",
1555 "doi", "isbn", "language_code" },
1556 item_result
1557 );
1559 *set, "source_type", set_path,
1560 { "book", "article", "catalogue", "web_page",
1561 "interview", "database", "video", "audio", "PDF" },
1562 item_result
1563 );
1565 *set, set_path,
1566 { "title", "bibliography_text", "author_text",
1567 "publisher", "publication_date", "url", "doi",
1568 "isbn", "language_code" },
1569 item_result
1570 );
1571 }
1572 if (unset != nullptr) {
1573 const std::string unset_path
1574 = child_path(item_path, "unset");
1575 validate_unique_items(*unset, unset_path, item_result);
1576 for (std::size_t index = 0; index < unset->size();
1577 ++index) {
1578 const json& value = (*unset)[index];
1579 constexpr std::array<std::string_view, 9> choices {
1580 "title", "bibliography_text", "author_text",
1581 "publisher", "publication_date", "url", "doi",
1582 "isbn", "language_code"
1583 };
1584 if (!value.is_string()
1585 || std::ranges::find(
1586 choices,
1587 std::string_view(
1588 value.is_string()
1589 ? value.get_ref<const std::string&>()
1590 : std::string {}
1591 )
1592 )
1593 == choices.end()) {
1594 add(item_result,
1595 unset_path + "/" + std::to_string(index),
1596 "enum",
1597 "field cannot be removed from a source");
1598 }
1599 }
1600 }
1601 if (set != nullptr && unset != nullptr && set->empty()
1602 && unset->empty()) {
1603 add(item_result, item_path, "min_operations",
1604 "update must set or unset at least one field");
1605 }
1606 }
1607 );
1608 if (const json* deletes
1609 = optional_object(update, "delete", path, result)) {
1610 const std::string delete_path = child_path(path, "delete");
1612 *deletes, delete_path,
1613 { "names", "external_ids", "credits", "measurements",
1614 "financial_facts", "evidence", "work_concepts",
1615 "concept_relations", "parent_guide_assertions",
1616 "ingest_issues" },
1617 result
1618 );
1619 for (const std::string_view key :
1620 { "names", "external_ids", "credits", "measurements",
1621 "financial_facts", "evidence", "work_concepts",
1622 "concept_relations", "parent_guide_assertions" }) {
1624 *deletes, key, delete_path, result
1625 );
1626 }
1627 if (const json* issues = optional_array(
1628 *deletes, "ingest_issues", delete_path, result
1629 )) {
1630 const std::string issues_path
1631 = child_path(delete_path, "ingest_issues");
1632 validate_unique_items(*issues, issues_path, result);
1633 for (std::size_t index = 0; index < issues->size(); ++index) {
1634 const json& issue = (*issues)[index];
1635 const std::string issue_path
1636 = issues_path + "/" + std::to_string(index);
1637 if (!issue.is_object()) {
1638 add(result, issue_path, "type",
1639 "expected an ingest issue key object");
1640 continue;
1641 }
1643 issue, issue_path,
1644 { "batch_id", "code", "json_path" }, result
1645 );
1647 issue, "batch_id", issue_path, result
1648 );
1649 require_string(issue, "code", issue_path, result);
1650 if (const json* json_path = require_string(
1651 issue, "json_path", issue_path, result
1652 );
1653 json_path != nullptr
1654 && !json_path->get_ref<const std::string&>()
1655 .starts_with('/')) {
1656 add(
1657 result, child_path(issue_path, "json_path"),
1658 "pattern", "JSON Pointer must start with '/'"
1659 );
1660 }
1661 }
1662 }
1663 }
1664 }
void validate_positive_id_array(const json &object, const std::string_view key, const std::string_view path, validation_result &result)
void validate_update_fields(const json &item, const std::string_view item_path, const std::string_view family, const std::initializer_list< std::string_view > set_fields, const std::initializer_list< std::string_view > unset_fields, validation_result &result)

◆ validate_viewer_projection()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_viewer_projection ( const json & document,
validation_result & result )

Definition at line 2375 of file contracts.cpp.

2377 {
2379 document, "",
2380 { "contract", "format_version", "projection_id",
2381 "product_snapshot_id", "candidate_snapshot_id",
2382 "projection_version", "settings_sha256", "projection",
2383 "edge_semantics", "generated_at", "extensions" },
2384 result
2385 );
2386 require_stable_id(document, "projection_id", "", result);
2387 require_stable_id(document, "product_snapshot_id", "", result);
2388 optional_string(document, "candidate_snapshot_id", "", result);
2389 require_string(document, "projection_version", "", result);
2390 require_sha256(document, "settings_sha256", "", result);
2391 validate_artifact_field(document, "projection", "", result);
2392 require_timestamp(document, "generated_at", "", result);
2393 const json* semantics
2394 = require_object(document, "edge_semantics", "", result);
2395 if (semantics != nullptr) {
2397 *semantics, "/edge_semantics",
2398 { "human_type", "derived_types" }, result
2399 );
2400 require_string(*semantics, "human_type", "/edge_semantics", result);
2402 *semantics, "derived_types", "/edge_semantics", true, result
2403 );
2404 }
2405 validate_extensions(document, "", result);
2406 }

◆ validate_work_set()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_work_set ( const json & set,
const std::string_view path,
validation_result & result )

Definition at line 1384 of file contracts.cpp.

1387 {
1389 set, "medium", path,
1390 { "film", "short_film", "television", "novel", "novella",
1391 "short_story", "poetry", "play", "essay", "album", "single",
1392 "composition", "painting", "print", "engraving", "drawing",
1393 "sculpture", "installation", "photography", "mixed_media" },
1394 result
1395 );
1396 validate_year_fields(set, path, { "year_start", "year_end" }, result);
1398 set, "date_precision", path,
1399 { "year", "decade", "approximate", "range", "exact" }, result
1400 );
1402 set, path,
1403 { "date_start_text", "date_end_text", "date_qualifier",
1404 "language_code", "country_code", "production_info_json" },
1405 result
1406 );
1407 }

◆ validate_year_fields()

void arachnespace::contracts::anonymous_namespace{contracts.cpp}::validate_year_fields ( const json & object,
const std::string_view path,
const std::initializer_list< std::string_view > keys,
validation_result & result )

Definition at line 753 of file contracts.cpp.

757 {
758 for (const std::string_view key : keys) {
759 optional_integer_range(object, key, path, -9999, 9999, result);
760 }
761 }

Variable Documentation

◆ contract_names

std::array< std::pair< std::string_view, contract_name >, 10 > arachnespace::contracts::anonymous_namespace{contracts.cpp}::contract_names
constexpr
Initial value:
{ {
{ "arachne_batch_v2", contract_name::arachne_batch },
{ "batch_envelope_v1", contract_name::batch_envelope },
{ "fetch_plan_v1", contract_name::fetch_plan },
{ "fetch_request_v1", contract_name::fetch_request },
{ "acquired_artifact_v1", contract_name::acquired_artifact },
{ "research_candidate_graph_plan_v1",
{ "product_graph_snapshot_v1",
{ "research_candidate_graph_snapshot_v1",
{ "viewer_projection_v1", contract_name::viewer_projection },
{ "site_bundle_v1", contract_name::site_bundle },
} }

Definition at line 25 of file contracts.cpp.

25 { {
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",
33 { "product_graph_snapshot_v1",
35 { "research_candidate_graph_snapshot_v1",
37 { "viewer_projection_v1", contract_name::viewer_projection },
38 { "site_bundle_v1", contract_name::site_bundle },
39 } };