15#include <unordered_map>
16#include <unordered_set>
23using SchemaVersions = std::map<int, SchemaDefinition>;
25std::unordered_map<std::string, SchemaVersions>& schemas() {
26 static std::unordered_map<std::string, SchemaVersions>
value;
30using MigrationKey = std::pair<std::string, int>;
37std::map<MigrationKey, MigrationStep>& migrationSteps() {
38 static std::map<MigrationKey, MigrationStep>
value;
42void addError(std::vector<ValidationError>& out, std::string
path, std::string
code, std::string
message) {
60std::string pointerSegment(std::string_view
value) {
62 result.reserve(
value.size());
63 for (
const char character :
value) {
66 else if (character ==
'/')
74bool readStrictString(JsonValue
value, std::string&
output) {
75 if (!
value.isString())
return false;
81 if (!
value.isBool())
return false;
87 if (!
value.isNumber())
return false;
90 number <
static_cast<double>(std::numeric_limits<int>::min()) ||
91 number >
static_cast<double>(std::numeric_limits<int>::max()))
98 bool allowFieldMetadata =
false);
100bool parseField(JsonValue
value, FieldDefinition&
output, std::string&
error,
const std::string&
path) {
101 if (!
value.isObject()) {
106 output = FieldDefinition{};
107 const JsonValue
name =
value.get(
"name");
109 error =
path +
"/name must be a non-empty string";
114 if (
value.has(
"required")) {
115 if (!readStrictBool(
value.get(
"required"),
output.required)) {
116 error =
path +
"/required must be a boolean";
120 if (
value.has(
"title") && !readStrictString(
value.get(
"title"),
output.title)) {
121 error =
path +
"/title must be a string";
124 if (
value.has(
"description") && !readStrictString(
value.get(
"description"),
output.description)) {
125 error =
path +
"/description must be a string";
128 if (
value.has(
"reference") && !readStrictString(
value.get(
"reference"),
output.reference)) {
129 error =
path +
"/reference must be a string";
132 if (
value.has(
"defaultJson")) {
133 if (!readStrictString(
value.get(
"defaultJson"),
output.defaultJson)) {
134 error =
path +
"/defaultJson must be a string";
137 std::string defaultError;
139 if (!defaultDocument.valid()) {
140 error =
path +
"/defaultJson is not valid JSON: " + defaultError;
147bool parseFieldArray(JsonValue
fields, std::vector<FieldDefinition>&
output, std::string&
error,
148 const std::string&
path) {
156 FieldDefinition
field;
158 output.push_back(std::move(field));
164 bool allowFieldMetadata) {
165 if (!
value.isObject()) {
170 static const std::set<std::string> nodeKeys = {
"additionalProperties",
172 "discriminatorMapping",
191 static const std::set<std::string> fieldMetadataKeys = {
"defaultJson",
"description",
"name",
192 "reference",
"required",
"title"};
193 for (
const auto&
key :
value.keys()) {
194 if (!nodeKeys.contains(
key) && (!allowFieldMetadata || !fieldMetadataKeys.contains(
key))) {
195 error =
path +
"/" + pointerSegment(
key) +
" is not supported by Eve Schema v1";
201 if (
value.has(
"type")) {
202 if (!
value.get(
"type").isString()) {
211 output.type = *parsedType;
214 if (
value.has(
"elementType")) {
215 if (!
value.get(
"elementType").isString()) {
216 error =
path +
"/elementType must be a string";
221 error =
"unknown elementType at " +
path;
224 output.elementType = *parsedType;
227 const bool hasRef =
value.has(
"ref");
228 const bool hasSchemaRef =
value.has(
"schemaRef");
229 if (hasRef && hasSchemaRef) {
230 std::string directRef;
231 std::string aliasRef;
232 if (!readStrictString(
value.get(
"ref"), directRef) || !readStrictString(
value.get(
"schemaRef"), aliasRef) ||
233 directRef != aliasRef) {
234 error =
path +
"/ref conflicts with schemaRef";
238 if (hasRef || hasSchemaRef) {
239 const JsonValue ref =
value.get(hasRef ?
"ref" :
"schemaRef");
240 if (!readStrictString(ref,
output.ref) ||
output.ref.empty()) {
241 error =
path +
"/ref must be a non-empty string";
244 if (
output.ref.find(
'#') != std::string::npos) {
245 error =
path +
"/ref only supports whole-schema references; fragments are not in Eve Schema";
249 const bool hasRefVersion =
value.has(
"refVersion");
250 const bool hasReferenceVersion =
value.has(
"referenceVersion");
251 if (hasRefVersion && hasReferenceVersion) {
252 int directVersion = 0;
253 int aliasVersion = 0;
254 if (!readInteger(
value.get(
"refVersion"), directVersion) ||
255 !readInteger(
value.get(
"referenceVersion"), aliasVersion) || directVersion != aliasVersion) {
256 error =
path +
"/refVersion conflicts with referenceVersion";
260 if (hasRefVersion || hasReferenceVersion) {
261 const JsonValue refVersion =
value.get(hasRefVersion ?
"refVersion" :
"referenceVersion");
262 if (!readInteger(refVersion,
output.refVersion) ||
output.refVersion <= 0) {
263 error =
path +
"/refVersion must be a positive integer";
268 auto readNumberConstraint = [&](
const char*
name, std::optional<double>&
target) {
270 const JsonValue constraint =
value.get(
name);
271 if (!constraint.isNumber() || !std::isfinite(constraint.asDouble())) {
275 target = constraint.asDouble();
278 if (!readNumberConstraint(
"minimum",
output.minimum) || !readNumberConstraint(
"maximum",
output.maximum))
281 error =
path +
"/minimum must not exceed maximum";
285 auto readLengthConstraint = [&](
const char*
name, std::optional<int>&
target) {
288 if (!readInteger(
value.get(
name), parsed) || parsed < 0) {
289 error =
path +
"/" +
name +
" must be a non-negative integer";
295 if (!readLengthConstraint(
"minLength",
output.minLength) || !readLengthConstraint(
"maxLength",
output.maxLength) ||
296 !readLengthConstraint(
"minItems",
output.minItems) || !readLengthConstraint(
"maxItems",
output.maxItems))
299 error =
path +
"/minLength must not exceed maxLength";
303 error =
path +
"/minItems must not exceed maxItems";
307 if (
value.has(
"enum")) {
308 const JsonValue enumValues =
value.get(
"enum");
309 if (!enumValues.isArray()) {
315 if (!readStrictString(enumValues.at(
index), entry)) {
316 error =
path +
"/enum supports string values only";
319 output.enumValues.push_back(std::move(entry));
323 const bool hasItems =
value.has(
"items");
324 const bool hasItemSchema =
value.has(
"itemSchema");
325 if (hasItems && hasItemSchema) {
326 error =
path +
" cannot define both items and itemSchema";
329 if (hasItems || hasItemSchema) {
331 error =
path +
"/items is only valid for an array node";
334 auto item = std::make_shared<SchemaNode>();
335 if (!parseNode(
value.get(hasItems ?
"items" :
"itemSchema"), *item,
error,
path +
"/items")) return false;
336 output.itemSchema = std::move(item);
339 const bool hasUnion =
value.has(
"union");
340 const bool hasOneOf =
value.has(
"oneOf");
341 if (hasUnion && hasOneOf) {
342 error =
path +
" cannot define both union and oneOf";
345 if (hasUnion || hasOneOf) {
346 const JsonValue alternatives =
value.get(hasUnion ?
"union" :
"oneOf");
347 if (!alternatives.isArray() || alternatives.size() == 0) {
348 error =
path +
"/union must be a non-empty array";
351 output.variants.reserve(alternatives.size());
353 SchemaNode alternative;
354 if (!parseNode(alternatives.at(
index), alternative,
error,
path +
"/union/" + std::to_string(
index)))
356 output.variants.push_back(std::move(alternative));
360 if (
value.has(
"discriminator")) {
361 if (!readStrictString(
value.get(
"discriminator"),
output.discriminator) ||
output.discriminator.empty()) {
362 error =
path +
"/discriminator must be a non-empty string";
366 if (
value.has(
"discriminatorMapping")) {
367 const JsonValue mapping =
value.get(
"discriminatorMapping");
368 if (!mapping.isObject()) {
369 error =
path +
"/discriminatorMapping must be an object";
372 for (
const std::string&
key : mapping.keys()) {
373 const JsonValue mapped = mapping.get(
key.c_str());
375 if (!readStrictString(mapped,
target)) {
376 int mappingIndex = 0;
377 if (!readInteger(mapped, mappingIndex) || mappingIndex < 0) {
378 error =
path +
"/discriminatorMapping values must be refs, indexes, or strings";
381 target = std::to_string(mappingIndex);
387 error =
path +
"/union type must be any or object in Eve Schema";
391 if (
value.has(
"fields") ||
value.has(
"additionalProperties")) {
393 error =
path +
"/fields or additionalProperties requires type object";
396 auto nested = std::make_shared<SchemaDefinition>();
398 if (
value.has(
"additionalProperties")) {
399 if (!readStrictBool(
value.get(
"additionalProperties"), nested->additionalProperties)) {
400 error =
path +
"/additionalProperties must be a boolean";
404 if (
value.has(
"fields") && !parseFieldArray(
value.get(
"fields"), nested->fields,
error,
path +
"/fields"))
406 output.objectSchema = std::move(nested);
411bool parseDefinition(
const std::string& json, SchemaDefinition& definition, std::string*
error) {
412 std::string parseError;
414 const JsonValue
root = document.root();
415 if (!document.valid() || !
root.isObject()) {
416 if (
error) *
error = parseError.empty() ?
"schema must be a JSON object" : parseError;
420 static const std::set<std::string> definitionKeys = {
"additionalProperties",
"description",
"fields",
"id",
421 "schemaVersion",
"title",
"version"};
422 for (
const auto&
key :
root.keys()) {
423 if (!definitionKeys.contains(
key)) {
424 if (
error) *
error =
"/" + pointerSegment(
key) +
" is not supported by Eve Schema v1";
429 definition = SchemaDefinition{};
430 const bool hasSchemaVersion =
root.has(
"schemaVersion");
431 const bool hasLegacyVersion =
root.has(
"version");
432 int schemaVersion = 1;
433 if (hasSchemaVersion && !readInteger(
root.get(
"schemaVersion"), schemaVersion)) {
434 if (
error) *
error =
"schemaVersion must be an integer";
437 if (!hasSchemaVersion && hasLegacyVersion && !readInteger(
root.get(
"version"), schemaVersion)) {
438 if (
error) *
error =
"version must be an integer";
441 if (hasSchemaVersion && hasLegacyVersion) {
442 int legacyVersion = 0;
443 if (!readInteger(
root.get(
"version"), legacyVersion) || legacyVersion != schemaVersion) {
444 if (
error) *
error =
"schemaVersion conflicts with legacy version";
449 if (
root.has(
"id") && !readStrictString(
root.get(
"id"), definition.id)) {
453 definition.version = schemaVersion;
454 if (
root.has(
"title") && !readStrictString(
root.get(
"title"), definition.title)) {
458 if (
root.has(
"description") && !readStrictString(
root.get(
"description"), definition.description)) {
459 if (
error) *
error =
"description must be a string";
462 if (
root.has(
"additionalProperties") &&
463 !readStrictBool(
root.get(
"additionalProperties"), definition.additionalProperties)) {
464 if (
error) *
error =
"additionalProperties must be a boolean";
467 if (
root.has(
"fields")) {
468 std::string fieldError;
469 if (!parseFieldArray(
root.get(
"fields"), definition.fields, fieldError,
"/fields")) {
477bool validateNodeDefinition(
const SchemaNode&
node,
const std::string&
path, std::string&
error,
478 std::unordered_set<const SchemaDefinition*>& activeDefinitions);
480bool validateDefinition(
const SchemaDefinition& definition,
const std::string&
path, std::string&
error,
481 std::unordered_set<const SchemaDefinition*>& activeDefinitions) {
482 if (!activeDefinitions.insert(&definition).second) {
483 error =
path +
" contains an inline schema cycle";
486 std::unordered_set<std::string> names;
487 for (
const auto& field : definition.
fields) {
488 if (
field.name.empty() || !names.insert(
field.name).second) {
489 error =
path +
" field names must be non-empty and unique";
490 activeDefinitions.erase(&definition);
493 if (!validateNodeDefinition(field,
path +
"/fields/" + pointerSegment(
field.name),
error, activeDefinitions)) {
494 activeDefinitions.erase(&definition);
498 activeDefinitions.erase(&definition);
502bool validateNodeDefinition(
const SchemaNode&
node,
const std::string&
path, std::string&
error,
503 std::unordered_set<const SchemaDefinition*>& activeDefinitions) {
504 if (
node.refVersion < 0) {
505 error =
path +
"/refVersion must not be negative";
508 if (
node.ref.empty() &&
node.refVersion != 0) {
509 error =
path +
"/refVersion requires ref";
513 node.itemSchema || !
node.variants.empty() || !
node.discriminator.empty() ||
514 !
node.discriminatorMapping.empty() || !
node.enumValues.empty() ||
node.minimum ||
516 error =
path +
" cannot combine ref with type, constraints, fields, items, or union";
521 error =
path +
" array item constraints require type array";
525 error =
path +
" string constraints require type string";
529 error =
path +
" numeric constraints require type number or integer";
533 error =
path +
" object fields require type object";
536 if (!
node.variants.empty() &&
539 error =
path +
" union cannot combine with object, array, scalar, or length constraints";
542 if (!
node.variants.empty() &&
node.discriminator.empty() && !
node.discriminatorMapping.empty()) {
543 error =
path +
" discriminatorMapping requires discriminator";
546 if (!
node.variants.empty() && !
node.discriminator.empty()) {
549 error =
path +
"/discriminatorMapping keys and values must not be empty";
552 size_t mappingIndex = 0;
553 const auto [
end, parseError] = std::from_chars(
target.data(),
target.data() +
target.size(), mappingIndex);
554 if (parseError == std::errc{}) {
556 error =
path +
"/discriminatorMapping points outside the union";
560 const bool found = std::any_of(
node.variants.begin(),
node.variants.end(),
561 [&](
const SchemaNode&
variant) { return variant.ref == target; });
563 error =
path +
"/discriminatorMapping ref is not a union alternative";
568 }
else if (!
node.discriminatorMapping.empty()) {
569 error =
path +
"/discriminatorMapping requires a union";
572 if (
node.variants.empty() && !
node.discriminator.empty()) {
573 error =
path +
"/discriminator requires a union";
576 if (
node.objectSchema && !validateDefinition(*
node.objectSchema,
path +
"/fields",
error, activeDefinitions))
578 if (
node.itemSchema && !validateNodeDefinition(*
node.itemSchema,
path +
"/items",
error, activeDefinitions))
589 std::string*
error) {
590 if (definition.id.empty()) {
591 if (
error) *
error =
"schema id must not be empty";
594 if (definition.version <= 0) {
595 if (
error) *
error =
"schema version must be positive";
598 std::string validationError;
599 std::unordered_set<const SchemaDefinition*> activeDefinitions;
600 if (!validateDefinition(definition,
"schema", validationError, activeDefinitions)) {
605 auto& versions = schemas()[definition.id];
606 const auto it = versions.find(definition.version);
607 if (it != versions.end()) {
608 if (!replaceExisting) {
610 *
error =
"schema '" + definition.id +
"' version " + std::to_string(definition.version) +
611 " is already registered";
615 it->second = definition;
618 versions.emplace(definition.version, definition);
622const SchemaDefinition* referencedSchema(
const SchemaNode&
node) {
623 if (
node.ref.empty())
return nullptr;
628void validateNode(
const SchemaNode&
node, JsonValue
value,
const std::string&
path,
629 std::vector<ValidationError>& errors, std::set<std::string>& activeRefs);
631void validateObject(
const SchemaDefinition& schema, JsonValue
value,
const std::string&
path,
632 std::vector<ValidationError>& errors, std::set<std::string>& activeRefs) {
633 if (!
value.isObject()) {
634 addError(errors,
path,
"type",
"expected object");
637 std::unordered_set<std::string> known;
638 for (
const auto& field : schema.
fields) {
639 known.insert(
field.name);
640 const JsonValue fieldValue =
value.get(
field.name.c_str());
641 const std::string fieldPath =
path +
"/" + pointerSegment(
field.name);
643 if (
field.required) addError(errors, fieldPath,
"required",
"required field is missing");
646 validateNode(field, fieldValue, fieldPath, errors, activeRefs);
648 if (!schema.additionalProperties) {
649 auto keys =
value.keys();
650 std::sort(keys.begin(), keys.end());
651 for (
const auto&
key : keys) {
652 if (!known.contains(
key))
653 addError(errors,
path +
"/" + pointerSegment(
key),
"additional_property",
"field is not declared");
658bool variantMatchesDiscriminator(
const SchemaNode&
variant,
const std::string& discriminator,
659 const std::string&
value) {
660 const SchemaDefinition* definition =
variant.objectSchema.get();
661 if (!definition && !
variant.ref.empty()) definition = referencedSchema(
variant);
662 if (!definition)
return false;
663 for (
const auto& field : definition->
fields) {
664 if (
field.name == discriminator &&
671void validateUnionValue(
const SchemaNode&
node, JsonValue
value,
const std::string&
path,
672 std::vector<ValidationError>& errors, std::set<std::string>& activeRefs) {
673 std::vector<size_t> candidates;
674 if (!
node.discriminator.empty()) {
675 if (!
value.isObject()) {
676 addError(errors,
path,
"type",
"discriminator requires an object");
679 const JsonValue discriminatorValue =
value.get(
node.discriminator.c_str());
680 if (!discriminatorValue.isString()) {
681 addError(errors,
path +
"/" + pointerSegment(
node.discriminator),
"discriminator",
682 "discriminator value must be a string");
685 const std::string valueText = discriminatorValue.asString();
686 const auto mapping =
node.discriminatorMapping.find(valueText);
687 if (mapping !=
node.discriminatorMapping.end()) {
689 const auto [
end, parseError] =
690 std::from_chars(mapping->second.data(), mapping->second.data() + mapping->second.size(),
index);
691 if (parseError == std::errc{} &&
end == mapping->second.data() + mapping->second.size() &&
693 candidates.push_back(
index);
695 for (
size_t indexValue = 0; indexValue <
node.variants.size(); ++indexValue)
696 if (
node.variants[indexValue].ref == mapping->second) candidates.push_back(indexValue);
700 if (variantMatchesDiscriminator(
node.variants[
index],
node.discriminator, valueText))
701 candidates.push_back(
index);
704 if (candidates.size() != 1) {
705 addError(errors,
path +
"/" + pointerSegment(
node.discriminator),
706 candidates.empty() ?
"discriminator" :
"discriminator_ambiguous",
707 candidates.empty() ?
"discriminator value is not supported"
708 :
"discriminator matches more than one union alternative");
711 validateNode(
node.variants[candidates.front()],
value,
path, errors, activeRefs);
716 std::vector<ValidationError> candidateErrors;
718 if (candidateErrors.empty())
return;
720 addError(errors,
path,
"union",
"value does not match any union alternative");
723void validateNode(
const SchemaNode&
node, JsonValue
value,
const std::string&
path,
724 std::vector<ValidationError>& errors, std::set<std::string>& activeRefs) {
725 if (!
node.ref.empty()) {
726 const auto* referenced = referencedSchema(
node);
728 addError(errors,
path,
"schema_ref_not_found",
"referenced schema is not registered");
731 const std::string
key = referenced->id +
"@" + std::to_string(referenced->version);
732 if (!activeRefs.insert(
key).second) {
733 addError(errors,
path,
"schema_ref_cycle",
"schema reference cycle detected");
736 validateObject(*referenced,
value,
path, errors, activeRefs);
737 activeRefs.erase(
key);
740 if (!
node.variants.empty()) {
750 if (
node.minimum &&
number < *
node.minimum) addError(errors,
path,
"minimum",
"value is below minimum");
751 if (
node.maximum &&
number > *
node.maximum) addError(errors,
path,
"maximum",
"value exceeds maximum");
754 const std::string
text =
value.asString();
755 if (
node.minLength &&
static_cast<int>(
text.size()) < *
node.minLength)
756 addError(errors,
path,
"min_length",
"string is shorter than minLength");
757 if (
node.maxLength &&
static_cast<int>(
text.size()) > *
node.maxLength)
758 addError(errors,
path,
"max_length",
"string is longer than maxLength");
759 if (!
node.enumValues.empty() &&
760 std::find(
node.enumValues.begin(),
node.enumValues.end(),
text) ==
node.enumValues.end())
761 addError(errors,
path,
"enum",
"value is not in the allowed set");
764 validateObject(*
node.objectSchema,
value,
path, errors, activeRefs);
766 if (
node.minItems &&
static_cast<int>(
value.size()) < *
node.minItems)
767 addError(errors,
path,
"min_items",
"array has fewer than minItems");
768 if (
node.maxItems &&
static_cast<int>(
value.size()) > *
node.maxItems)
769 addError(errors,
path,
"max_items",
"array has more than maxItems");
770 if (
node.itemSchema) {
776 addError(errors,
path +
"/" + std::to_string(
index),
"element_type",
782bool migrationReaches(
const std::string& schemaId,
int start,
int target) {
783 std::set<int> visited;
785 while (visited.insert(
current).second) {
787 const auto it = migrationSteps().find(MigrationKey{schemaId,
current});
788 if (it == migrationSteps().
end())
return false;
789 current = it->second.toVersion;
833 std::string
path = {}) {
837 path, {},
"schema"));
841 path, {},
"schema"));
847 return registrationResult(registerDefinition(definition,
true, &
error), std::move(
error), definition.id);
853 if (!parseDefinition(json, definition, &
error))
856 return registrationResult(registerDefinition(definition,
true, &
error), std::move(
error), definition.id);
860 const auto it = schemas().find(
id);
861 return it == schemas().end() || it->second.empty() ? nullptr : &it->second.rbegin()->second;
866 return registrationResult(registerDefinition(definition,
false, &
error), std::move(
error), definition.id);
872 if (!parseDefinition(json, definition, &
error))
875 error.empty() ?
"invalid schema JSON" :
error, {}, {},
"schema"));
880 const auto schemaIt = schemas().find(schemaId);
881 if (schemaIt == schemas().
end())
return nullptr;
882 const auto versionIt = schemaIt->second.find(schemaVersion);
883 return versionIt == schemaIt->second.end() ? nullptr : &versionIt->second;
887 std::vector<int> result;
888 const auto it = schemas().find(schemaId);
889 if (it == schemas().
end())
return result;
890 result.reserve(it->second.size());
891 for (
const auto& [version, definition] : it->second) {
893 result.push_back(version);
899 const bool removed = schemas().erase(
id) != 0;
900 for (
auto it = migrationSteps().
begin(); it != migrationSteps().end();) {
901 if (it->first.first ==
id)
902 it = migrationSteps().erase(it);
913 const auto schemaIt = schemas().find(
id);
914 if (schemaIt == schemas().
end())
917 const bool removed = schemaIt->second.erase(schemaVersion) != 0;
918 if (schemaIt->second.empty()) schemas().erase(schemaIt);
919 for (
auto it = migrationSteps().
begin(); it != migrationSteps().end();) {
920 if (it->first.first ==
id && (it->first.second == schemaVersion || it->second.toVersion == schemaVersion))
921 it = migrationSteps().erase(it);
927 "schema version is not registered",
id, {},
"schema"));
933 migrationSteps().clear();
940 for (
const auto& [
id,
versions] : schemas()) {
942 result +=
static_cast<int>(
versions.size());
948 std::vector<std::string> result;
949 result.reserve(schemas().
size());
950 for (
const auto& item : schemas()) result.push_back(item.first);
951 std::sort(result.begin(), result.end());
956 const auto* schema =
find(schemaId);
957 std::vector<ValidationError> errors;
959 addError(errors,
"",
"schema_not_found",
"schema '" + schemaId +
"' is not registered");
962 return validate(schemaId, schema->version, json);
966 const std::string& json) {
967 std::vector<ValidationError> errors;
968 const auto* schema =
resolve(schemaId, schemaVersion);
970 addError(errors,
"",
"schema_version_not_found",
971 "schema '" + schemaId +
"' version " + std::to_string(schemaVersion) +
" is not registered");
974 std::string parseError;
976 const JsonValue
root = document.root();
977 if (!document.valid()) {
978 addError(errors,
"",
"invalid_json", parseError);
981 std::set<std::string> activeRefs;
982 activeRefs.insert(schema->id +
"@" + std::to_string(schema->version));
983 validateObject(*schema,
root,
"", errors, activeRefs);
989 if (schemaId.empty() || fromVersion <= 0 ||
toVersion <= 0 || !migration)
999 const MigrationKey
key{schemaId, fromVersion};
1000 if (migrationSteps().contains(
key))
1003 if (migrationReaches(schemaId,
toVersion, fromVersion))
1008 auto candidate = migrationSteps();
1009 candidate.emplace(
key, MigrationStep{
toVersion, std::move(migration)});
1010 migrationSteps().swap(candidate);
1011 }
catch (
const std::exception& exception) {
1023 if (schemaId.empty() || fromVersion <= 0 ||
toVersion <= 0)
1037 result.
versions.push_back(fromVersion);
1040 std::set<int> visited;
1046 if (!visited.insert(
current).second)
1049 const auto it = migrationSteps().find(MigrationKey{schemaId,
current});
1050 if (it == migrationSteps().
end())
1053 if (it->second.toVersion <=
current)
1059 current = it->second.toVersion;
1063 "migration chain reaches an unregistered schema version",
"schemaVersion"));
1074 auto inputJson =
input.toJson();
1076 const auto sourceErrors =
validate(schemaId, fromVersion, inputJson.value());
1077 if (!sourceErrors.empty())
1080 "migration input does not satisfy its source schema", sourceErrors.front().path));
1085 const auto&
path = compatibility.value().versions;
1086 std::vector<MigrationFunction> chain;
1087 chain.reserve(
path.size() > 0 ?
path.size() - 1 : 0);
1089 const auto step = migrationSteps().
find(MigrationKey{schemaId,
path[
index - 1]});
1090 if (
step == migrationSteps().
end())
1093 "migration chain changed while it was being evaluated",
"schemaVersion"));
1094 chain.push_back(
step->second.function);
1099 eve::Value candidate = std::move(next).takeValue();
1100 auto candidateJson = candidate.
toJson();
1103 if (!errors.empty())
1106 "migration output does not satisfy its target schema", errors.front().path));
1107 current = std::move(candidate);
1109 }
catch (
const std::exception& exception) {
1120 const std::string& json) {
wgpu::PopErrorScopeStatus status
std::vector< std::string > fields
MigrationFunction function
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
The canonical owning dynamic value used by data-facing protocols.
Result< std::string > toJson() const
Serialize this value as deterministic compact JSON.
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
Value * find(const std::string &key) noexcept
Return an object member, or nullptr when absent.
static Document parse(const std::string &text, std::string *error=nullptr)
Parse.
EVENGINE_API_FOUNDATION public API.
static eve::Result< std::string > migrateJson(const std::string &schemaId, int fromVersion, int toVersion, const std::string &json)
Parses, migrates and serializes a JSON payload through the chain.
static const SchemaDefinition * find(const std::string &id)
Finds the highest registered version for an id, or nullptr.
static eve::Result< SchemaRegistrationStatus > registerVersioned(const SchemaDefinition &definition)
Registers a new exact (id, version) entry without replacing it.
static std::vector< int > versions(const std::string &schemaId)
Returns all registered versions for an id in ascending order.
static void clear()
Clears all registered schemas.
static eve::Result< SchemaRegistrationStatus > registerSchema(const SchemaDefinition &definition)
Registers or replaces the exact (id, version) entry.
static eve::Result< eve::Value > migrate(const std::string &schemaId, int fromVersion, int toVersion, const eve::Value &input)
Applies an explicit migration chain to an owning payload.
static std::vector< ValidationError > validate(const std::string &schemaId, const std::string &json)
Validates JSON text against the highest registered schema version.
static const SchemaDefinition * resolve(const std::string &schemaId, int schemaVersion)
Resolves one exact (schemaId, schemaVersion) pair, or nullptr.
static eve::Result< SchemaRegistrationStatus > registerFromJsonVersioned(const std::string &json)
Parses and registers a new exact (id, schemaVersion) entry.
static eve::Result< void > remove(const std::string &id)
Removes all registered versions for a stable id.
static eve::Result< SchemaCompatibility > queryCompatibility(const std::string &schemaId, int fromVersion, int toVersion)
Resolves an exact compatibility path without executing migrations.
static int versionCount()
Returns the number of registered (id, version) entries.
static eve::Result< void > registerMigration(const std::string &schemaId, int fromVersion, int toVersion, MigrationFunction migration)
Registers one explicit forward migration edge.
static int count()
Returns the number of registered schema ids.
static std::vector< std::string > ids()
Returns registered schema ids, unique and in deterministic lexical order.
static eve::Result< SchemaRegistrationStatus > registerFromJson(const std::string &json)
Parses and registers or replaces one schema definition from JSON.
const EditorValue * field(const EditorValue &value, const char *name)
std::optional< ValueType > valueTypeFromString(const std::string &name)
Converts a stable schema type name to its enum value.
SchemaRegistrationStatus
Outcome of an exact schema-version registration.
const char * valueTypeName(ValueType type)
Returns the stable lowercase name of a schema value type.
ValueType
JSON-compatible value kinds understood by a schema field.
const char * schemaRegistrationStatusName(SchemaRegistrationStatus status)
Returns the stable lowercase name of a registration outcome.
std::function< eve::Result< eve::Value >(const eve::Value &input)> MigrationFunction
One pure transformation from one registered schema version to the next.
The exact compatibility path selected by SchemaRegistry.
std::vector< int > versions
Versioned runtime schema for a JSON object.