载入中...
搜索中...
未找到
SchemaRegistry.cpp
浏览该文件的文档.
2
3#include "common/Json.h"
4
5#include <algorithm>
6#include <charconv>
7#include <cmath>
8#include <cstdint>
9#include <exception>
10#include <functional>
11#include <limits>
12#include <map>
13#include <set>
14#include <string_view>
15#include <unordered_map>
16#include <unordered_set>
17#include <utility>
18
19namespace eve::schema {
20namespace {
21
22using JsonValue = eve::json::Value;
23using SchemaVersions = std::map<int, SchemaDefinition>;
24
25std::unordered_map<std::string, SchemaVersions>& schemas() {
26 static std::unordered_map<std::string, SchemaVersions> value;
27 return value;
28}
29
30using MigrationKey = std::pair<std::string, int>;
31
32struct MigrationStep {
33 int toVersion = 0;
35};
36
37std::map<MigrationKey, MigrationStep>& migrationSteps() {
38 static std::map<MigrationKey, MigrationStep> value;
39 return value;
40}
41
42void addError(std::vector<ValidationError>& out, std::string path, std::string code, std::string message) {
43 out.push_back({std::move(path), std::move(code), std::move(message)});
44}
45
46bool hasType(JsonValue value, ValueType type) {
47 switch (type) {
48 case ValueType::Any: return static_cast<bool>(value);
49 case ValueType::Null: return value.isNull();
50 case ValueType::Boolean: return value.isBool();
51 case ValueType::Integer: return value.isNumber() && std::floor(value.asDouble()) == value.asDouble();
52 case ValueType::Number: return value.isNumber();
53 case ValueType::String: return value.isString();
54 case ValueType::Object: return value.isObject();
55 case ValueType::Array: return value.isArray();
56 }
57 return false;
58}
59
60std::string pointerSegment(std::string_view value) {
61 std::string result;
62 result.reserve(value.size());
63 for (const char character : value) {
64 if (character == '~')
65 result += "~0";
66 else if (character == '/')
67 result += "~1";
68 else
69 result += character;
70 }
71 return result;
72}
73
74bool readStrictString(JsonValue value, std::string& output) {
75 if (!value.isString()) return false;
76 output = value.asString();
77 return true;
78}
79
80bool readStrictBool(JsonValue value, bool& output) {
81 if (!value.isBool()) return false;
82 output = value.asBool();
83 return true;
84}
85
86bool readInteger(JsonValue value, int& output) {
87 if (!value.isNumber()) return false;
88 const double number = value.asDouble();
89 if (!std::isfinite(number) || std::floor(number) != number ||
90 number < static_cast<double>(std::numeric_limits<int>::min()) ||
91 number > static_cast<double>(std::numeric_limits<int>::max()))
92 return false;
93 output = static_cast<int>(number);
94 return true;
95}
96
97bool parseNode(JsonValue value, SchemaNode& output, std::string& error, const std::string& path,
98 bool allowFieldMetadata = false);
99
100bool parseField(JsonValue value, FieldDefinition& output, std::string& error, const std::string& path) {
101 if (!value.isObject()) {
102 error = path + " must be an object";
103 return false;
104 }
105
106 output = FieldDefinition{};
107 const JsonValue name = value.get("name");
108 if (!readStrictString(name, output.name) || output.name.empty()) {
109 error = path + "/name must be a non-empty string";
110 return false;
111 }
112 if (!parseNode(value, output, error, path, true)) return false;
113
114 if (value.has("required")) {
115 if (!readStrictBool(value.get("required"), output.required)) {
116 error = path + "/required must be a boolean";
117 return false;
118 }
119 }
120 if (value.has("title") && !readStrictString(value.get("title"), output.title)) {
121 error = path + "/title must be a string";
122 return false;
123 }
124 if (value.has("description") && !readStrictString(value.get("description"), output.description)) {
125 error = path + "/description must be a string";
126 return false;
127 }
128 if (value.has("reference") && !readStrictString(value.get("reference"), output.reference)) {
129 error = path + "/reference must be a string";
130 return false;
131 }
132 if (value.has("defaultJson")) {
133 if (!readStrictString(value.get("defaultJson"), output.defaultJson)) {
134 error = path + "/defaultJson must be a string";
135 return false;
136 }
137 std::string defaultError;
138 const auto defaultDocument = eve::json::Document::parse(output.defaultJson, &defaultError);
139 if (!defaultDocument.valid()) {
140 error = path + "/defaultJson is not valid JSON: " + defaultError;
141 return false;
142 }
143 }
144 return true;
145}
146
147bool parseFieldArray(JsonValue fields, std::vector<FieldDefinition>& output, std::string& error,
148 const std::string& path) {
149 if (!fields.isArray()) {
150 error = path + " must be an array";
151 return false;
152 }
153 output.clear();
154 output.reserve(fields.size());
155 for (size_t index = 0; index < fields.size(); ++index) {
156 FieldDefinition field;
157 if (!parseField(fields.at(index), field, error, path + "/" + std::to_string(index))) return false;
158 output.push_back(std::move(field));
159 }
160 return true;
161}
162
163bool parseNode(JsonValue value, SchemaNode& output, std::string& error, const std::string& path,
164 bool allowFieldMetadata) {
165 if (!value.isObject()) {
166 error = path + " must be an object";
167 return false;
168 }
169
170 static const std::set<std::string> nodeKeys = {"additionalProperties",
171 "discriminator",
172 "discriminatorMapping",
173 "elementType",
174 "enum",
175 "fields",
176 "itemSchema",
177 "items",
178 "maxItems",
179 "maxLength",
180 "maximum",
181 "minItems",
182 "minLength",
183 "minimum",
184 "oneOf",
185 "ref",
186 "refVersion",
187 "referenceVersion",
188 "schemaRef",
189 "type",
190 "union"};
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";
196 return false;
197 }
198 }
199
200 output = SchemaNode{};
201 if (value.has("type")) {
202 if (!value.get("type").isString()) {
203 error = path + "/type must be a string";
204 return false;
205 }
206 const auto parsedType = valueTypeFromString(value.getString("type"));
207 if (!parsedType) {
208 error = "unknown type at " + path;
209 return false;
210 }
211 output.type = *parsedType;
212 }
213
214 if (value.has("elementType")) {
215 if (!value.get("elementType").isString()) {
216 error = path + "/elementType must be a string";
217 return false;
218 }
219 const auto parsedType = valueTypeFromString(value.getString("elementType"));
220 if (!parsedType) {
221 error = "unknown elementType at " + path;
222 return false;
223 }
224 output.elementType = *parsedType;
225 }
226
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";
235 return false;
236 }
237 }
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";
242 return false;
243 }
244 if (output.ref.find('#') != std::string::npos) {
245 error = path + "/ref only supports whole-schema references; fragments are not in Eve Schema";
246 return false;
247 }
248 }
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";
257 return false;
258 }
259 }
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";
264 return false;
265 }
266 }
267
268 auto readNumberConstraint = [&](const char* name, std::optional<double>& target) {
269 if (!value.has(name)) return true;
270 const JsonValue constraint = value.get(name);
271 if (!constraint.isNumber() || !std::isfinite(constraint.asDouble())) {
272 error = path + "/" + name + " must be a finite number";
273 return false;
274 }
275 target = constraint.asDouble();
276 return true;
277 };
278 if (!readNumberConstraint("minimum", output.minimum) || !readNumberConstraint("maximum", output.maximum))
279 return false;
280 if (output.minimum && output.maximum && *output.minimum > *output.maximum) {
281 error = path + "/minimum must not exceed maximum";
282 return false;
283 }
284
285 auto readLengthConstraint = [&](const char* name, std::optional<int>& target) {
286 if (!value.has(name)) return true;
287 int parsed = 0;
288 if (!readInteger(value.get(name), parsed) || parsed < 0) {
289 error = path + "/" + name + " must be a non-negative integer";
290 return false;
291 }
292 target = parsed;
293 return true;
294 };
295 if (!readLengthConstraint("minLength", output.minLength) || !readLengthConstraint("maxLength", output.maxLength) ||
296 !readLengthConstraint("minItems", output.minItems) || !readLengthConstraint("maxItems", output.maxItems))
297 return false;
298 if (output.minLength && output.maxLength && *output.minLength > *output.maxLength) {
299 error = path + "/minLength must not exceed maxLength";
300 return false;
301 }
302 if (output.minItems && output.maxItems && *output.minItems > *output.maxItems) {
303 error = path + "/minItems must not exceed maxItems";
304 return false;
305 }
306
307 if (value.has("enum")) {
308 const JsonValue enumValues = value.get("enum");
309 if (!enumValues.isArray()) {
310 error = path + "/enum must be an array";
311 return false;
312 }
313 for (size_t index = 0; index < enumValues.size(); ++index) {
314 std::string entry;
315 if (!readStrictString(enumValues.at(index), entry)) {
316 error = path + "/enum supports string values only";
317 return false;
318 }
319 output.enumValues.push_back(std::move(entry));
320 }
321 }
322
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";
327 return false;
328 }
329 if (hasItems || hasItemSchema) {
330 if (output.type != ValueType::Array) {
331 error = path + "/items is only valid for an array node";
332 return false;
333 }
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);
337 }
338
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";
343 return false;
344 }
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";
349 return false;
350 }
351 output.variants.reserve(alternatives.size());
352 for (size_t index = 0; index < alternatives.size(); ++index) {
353 SchemaNode alternative;
354 if (!parseNode(alternatives.at(index), alternative, error, path + "/union/" + std::to_string(index)))
355 return false;
356 output.variants.push_back(std::move(alternative));
357 }
358 }
359
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";
363 return false;
364 }
365 }
366 if (value.has("discriminatorMapping")) {
367 const JsonValue mapping = value.get("discriminatorMapping");
368 if (!mapping.isObject()) {
369 error = path + "/discriminatorMapping must be an object";
370 return false;
371 }
372 for (const std::string& key : mapping.keys()) {
373 const JsonValue mapped = mapping.get(key.c_str());
374 std::string target;
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";
379 return false;
380 }
381 target = std::to_string(mappingIndex);
382 }
383 output.discriminatorMapping.emplace(key, std::move(target));
384 }
385 }
386 if (!output.variants.empty() && output.type != ValueType::Any && output.type != ValueType::Object) {
387 error = path + "/union type must be any or object in Eve Schema";
388 return false;
389 }
390
391 if (value.has("fields") || value.has("additionalProperties")) {
392 if (output.type != ValueType::Object) {
393 error = path + "/fields or additionalProperties requires type object";
394 return false;
395 }
396 auto nested = std::make_shared<SchemaDefinition>();
397 nested->version = 1;
398 if (value.has("additionalProperties")) {
399 if (!readStrictBool(value.get("additionalProperties"), nested->additionalProperties)) {
400 error = path + "/additionalProperties must be a boolean";
401 return false;
402 }
403 }
404 if (value.has("fields") && !parseFieldArray(value.get("fields"), nested->fields, error, path + "/fields"))
405 return false;
406 output.objectSchema = std::move(nested);
407 }
408 return true;
409}
410
411bool parseDefinition(const std::string& json, SchemaDefinition& definition, std::string* error) {
412 std::string parseError;
413 const auto document = eve::json::Document::parse(json, &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;
417 return false;
418 }
419
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";
425 return false;
426 }
427 }
428
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";
435 return false;
436 }
437 if (!hasSchemaVersion && hasLegacyVersion && !readInteger(root.get("version"), schemaVersion)) {
438 if (error) *error = "version must be an integer";
439 return false;
440 }
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";
445 return false;
446 }
447 }
448
449 if (root.has("id") && !readStrictString(root.get("id"), definition.id)) {
450 if (error) *error = "id must be a string";
451 return false;
452 }
453 definition.version = schemaVersion;
454 if (root.has("title") && !readStrictString(root.get("title"), definition.title)) {
455 if (error) *error = "title must be a string";
456 return false;
457 }
458 if (root.has("description") && !readStrictString(root.get("description"), definition.description)) {
459 if (error) *error = "description must be a string";
460 return false;
461 }
462 if (root.has("additionalProperties") &&
463 !readStrictBool(root.get("additionalProperties"), definition.additionalProperties)) {
464 if (error) *error = "additionalProperties must be a boolean";
465 return false;
466 }
467 if (root.has("fields")) {
468 std::string fieldError;
469 if (!parseFieldArray(root.get("fields"), definition.fields, fieldError, "/fields")) {
470 if (error) *error = std::move(fieldError);
471 return false;
472 }
473 }
474 return true;
475}
476
477bool validateNodeDefinition(const SchemaNode& node, const std::string& path, std::string& error,
478 std::unordered_set<const SchemaDefinition*>& activeDefinitions);
479
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";
484 return false;
485 }
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);
491 return false;
492 }
493 if (!validateNodeDefinition(field, path + "/fields/" + pointerSegment(field.name), error, activeDefinitions)) {
494 activeDefinitions.erase(&definition);
495 return false;
496 }
497 }
498 activeDefinitions.erase(&definition);
499 return true;
500}
501
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";
506 return false;
507 }
508 if (node.ref.empty() && node.refVersion != 0) {
509 error = path + "/refVersion requires ref";
510 return false;
511 }
512 if (!node.ref.empty() && (node.type != ValueType::Any || node.elementType != ValueType::Any || node.objectSchema ||
513 node.itemSchema || !node.variants.empty() || !node.discriminator.empty() ||
514 !node.discriminatorMapping.empty() || !node.enumValues.empty() || node.minimum ||
515 node.maximum || node.minLength || node.maxLength || node.minItems || node.maxItems)) {
516 error = path + " cannot combine ref with type, constraints, fields, items, or union";
517 return false;
518 }
519 if (node.type != ValueType::Array &&
520 (node.elementType != ValueType::Any || node.itemSchema || node.minItems || node.maxItems)) {
521 error = path + " array item constraints require type array";
522 return false;
523 }
524 if (node.type != ValueType::String && (node.enumValues.size() > 0 || node.minLength || node.maxLength)) {
525 error = path + " string constraints require type string";
526 return false;
527 }
528 if (node.type != ValueType::Number && node.type != ValueType::Integer && (node.minimum || node.maximum)) {
529 error = path + " numeric constraints require type number or integer";
530 return false;
531 }
532 if (node.type != ValueType::Object && node.objectSchema) {
533 error = path + " object fields require type object";
534 return false;
535 }
536 if (!node.variants.empty() &&
537 (node.objectSchema || node.itemSchema || node.elementType != ValueType::Any || !node.enumValues.empty() ||
538 node.minimum || node.maximum || node.minLength || node.maxLength || node.minItems || node.maxItems)) {
539 error = path + " union cannot combine with object, array, scalar, or length constraints";
540 return false;
541 }
542 if (!node.variants.empty() && node.discriminator.empty() && !node.discriminatorMapping.empty()) {
543 error = path + " discriminatorMapping requires discriminator";
544 return false;
545 }
546 if (!node.variants.empty() && !node.discriminator.empty()) {
547 for (const auto& [value, target] : node.discriminatorMapping) {
548 if (value.empty() || target.empty()) {
549 error = path + "/discriminatorMapping keys and values must not be empty";
550 return false;
551 }
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{}) {
555 if (end != target.data() + target.size() || mappingIndex >= node.variants.size()) {
556 error = path + "/discriminatorMapping points outside the union";
557 return false;
558 }
559 } else {
560 const bool found = std::any_of(node.variants.begin(), node.variants.end(),
561 [&](const SchemaNode& variant) { return variant.ref == target; });
562 if (!found) {
563 error = path + "/discriminatorMapping ref is not a union alternative";
564 return false;
565 }
566 }
567 }
568 } else if (!node.discriminatorMapping.empty()) {
569 error = path + "/discriminatorMapping requires a union";
570 return false;
571 }
572 if (node.variants.empty() && !node.discriminator.empty()) {
573 error = path + "/discriminator requires a union";
574 return false;
575 }
576 if (node.objectSchema && !validateDefinition(*node.objectSchema, path + "/fields", error, activeDefinitions))
577 return false;
578 if (node.itemSchema && !validateNodeDefinition(*node.itemSchema, path + "/items", error, activeDefinitions))
579 return false;
580 for (size_t index = 0; index < node.variants.size(); ++index) {
581 if (!validateNodeDefinition(node.variants[index], path + "/union/" + std::to_string(index), error,
582 activeDefinitions))
583 return false;
584 }
585 return true;
586}
587
588SchemaRegistrationStatus registerDefinition(const SchemaDefinition& definition, bool replaceExisting,
589 std::string* error) {
590 if (definition.id.empty()) {
591 if (error) *error = "schema id must not be empty";
593 }
594 if (definition.version <= 0) {
595 if (error) *error = "schema version must be positive";
597 }
598 std::string validationError;
599 std::unordered_set<const SchemaDefinition*> activeDefinitions;
600 if (!validateDefinition(definition, "schema", validationError, activeDefinitions)) {
601 if (error) *error = validationError;
603 }
604
605 auto& versions = schemas()[definition.id];
606 const auto it = versions.find(definition.version);
607 if (it != versions.end()) {
608 if (!replaceExisting) {
609 if (error) {
610 *error = "schema '" + definition.id + "' version " + std::to_string(definition.version) +
611 " is already registered";
612 }
614 }
615 it->second = definition;
617 }
618 versions.emplace(definition.version, definition);
620}
621
622const SchemaDefinition* referencedSchema(const SchemaNode& node) {
623 if (node.ref.empty()) return nullptr;
624 if (node.refVersion > 0) return SchemaRegistry::resolve(node.ref, node.refVersion);
625 return SchemaRegistry::find(node.ref);
626}
627
628void validateNode(const SchemaNode& node, JsonValue value, const std::string& path,
629 std::vector<ValidationError>& errors, std::set<std::string>& activeRefs);
630
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");
635 return;
636 }
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);
642 if (!fieldValue) {
643 if (field.required) addError(errors, fieldPath, "required", "required field is missing");
644 continue;
645 }
646 validateNode(field, fieldValue, fieldPath, errors, activeRefs);
647 }
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");
654 }
655 }
656}
657
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 &&
665 std::find(field.enumValues.begin(), field.enumValues.end(), value) != field.enumValues.end())
666 return true;
667 }
668 return false;
669}
670
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");
677 return;
678 }
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");
683 return;
684 }
685 const std::string valueText = discriminatorValue.asString();
686 const auto mapping = node.discriminatorMapping.find(valueText);
687 if (mapping != node.discriminatorMapping.end()) {
688 size_t index = 0;
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() &&
692 index < node.variants.size()) {
693 candidates.push_back(index);
694 } else {
695 for (size_t indexValue = 0; indexValue < node.variants.size(); ++indexValue)
696 if (node.variants[indexValue].ref == mapping->second) candidates.push_back(indexValue);
697 }
698 } else {
699 for (size_t index = 0; index < node.variants.size(); ++index) {
700 if (variantMatchesDiscriminator(node.variants[index], node.discriminator, valueText))
701 candidates.push_back(index);
702 }
703 }
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");
709 return;
710 }
711 validateNode(node.variants[candidates.front()], value, path, errors, activeRefs);
712 return;
713 }
714
715 for (size_t index = 0; index < node.variants.size(); ++index) {
716 std::vector<ValidationError> candidateErrors;
717 validateNode(node.variants[index], value, path, candidateErrors, activeRefs);
718 if (candidateErrors.empty()) return;
719 }
720 addError(errors, path, "union", "value does not match any union alternative");
721}
722
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);
727 if (!referenced) {
728 addError(errors, path, "schema_ref_not_found", "referenced schema is not registered");
729 return;
730 }
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");
734 return;
735 }
736 validateObject(*referenced, value, path, errors, activeRefs);
737 activeRefs.erase(key);
738 return;
739 }
740 if (!node.variants.empty()) {
741 validateUnionValue(node, value, path, errors, activeRefs);
742 return;
743 }
744 if (!hasType(value, node.type)) {
745 addError(errors, path, "type", "expected " + std::string(valueTypeName(node.type)));
746 return;
747 }
748 if (node.type == ValueType::Number || node.type == ValueType::Integer) {
749 const double number = value.asDouble();
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");
752 }
753 if (node.type == ValueType::String) {
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");
762 }
763 if (node.type == ValueType::Object && node.objectSchema)
764 validateObject(*node.objectSchema, value, path, errors, activeRefs);
765 if (node.type != ValueType::Array) return;
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) {
771 for (size_t index = 0; index < value.size(); ++index)
772 validateNode(*node.itemSchema, value.at(index), path + "/" + std::to_string(index), errors, activeRefs);
773 } else if (node.elementType != ValueType::Any) {
774 for (size_t index = 0; index < value.size(); ++index) {
775 if (!hasType(value.at(index), node.elementType))
776 addError(errors, path + "/" + std::to_string(index), "element_type",
777 "expected " + std::string(valueTypeName(node.elementType)));
778 }
779 }
780}
781
782bool migrationReaches(const std::string& schemaId, int start, int target) {
783 std::set<int> visited;
784 int current = start;
785 while (visited.insert(current).second) {
786 if (current == target) return true;
787 const auto it = migrationSteps().find(MigrationKey{schemaId, current});
788 if (it == migrationSteps().end()) return false;
789 current = it->second.toVersion;
790 }
791 return true;
792}
793
794} // namespace
795
796std::optional<ValueType> valueTypeFromString(const std::string& name) {
797 if (name == "any") return ValueType::Any;
798 if (name == "null") return ValueType::Null;
799 if (name == "boolean" || name == "bool") return ValueType::Boolean;
800 if (name == "integer" || name == "int") return ValueType::Integer;
801 if (name == "number" || name == "float") return ValueType::Number;
802 if (name == "string") return ValueType::String;
803 if (name == "object") return ValueType::Object;
804 if (name == "array") return ValueType::Array;
805 return std::nullopt;
806}
807
809 switch (type) {
810 case ValueType::Any: return "any";
811 case ValueType::Null: return "null";
812 case ValueType::Boolean: return "boolean";
813 case ValueType::Integer: return "integer";
814 case ValueType::Number: return "number";
815 case ValueType::String: return "string";
816 case ValueType::Object: return "object";
817 case ValueType::Array: return "array";
818 }
819 return "any";
820}
821
823 switch (status) {
824 case SchemaRegistrationStatus::Registered: return "registered";
825 case SchemaRegistrationStatus::Replaced: return "replaced";
826 case SchemaRegistrationStatus::Conflict: return "conflict";
827 case SchemaRegistrationStatus::Invalid: return "invalid";
828 }
829 return "invalid";
830}
831
833 std::string path = {}) {
836 eve::DiagnosticCode::Conflict, error.empty() ? "schema version is already registered" : error,
837 path, {}, "schema"));
840 eve::DiagnosticCode::InvalidArgument, error.empty() ? "invalid schema definition" : error,
841 path, {}, "schema"));
843}
844
846 std::string error;
847 return registrationResult(registerDefinition(definition, true, &error), std::move(error), definition.id);
848}
849
851 SchemaDefinition definition;
852 std::string error;
853 if (!parseDefinition(json, definition, &error))
855 eve::DiagnosticCode::ParseError, error.empty() ? "invalid schema JSON" : error, {}, {}, "schema"));
856 return registrationResult(registerDefinition(definition, true, &error), std::move(error), definition.id);
857}
858
859const SchemaDefinition* SchemaRegistry::find(const std::string& id) {
860 const auto it = schemas().find(id);
861 return it == schemas().end() || it->second.empty() ? nullptr : &it->second.rbegin()->second;
862}
863
865 std::string error;
866 return registrationResult(registerDefinition(definition, false, &error), std::move(error), definition.id);
867}
868
870 SchemaDefinition definition;
871 std::string error;
872 if (!parseDefinition(json, definition, &error))
875 error.empty() ? "invalid schema JSON" : error, {}, {}, "schema"));
876 return registerVersioned(definition);
877}
878
879const SchemaDefinition* SchemaRegistry::resolve(const std::string& schemaId, int schemaVersion) {
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;
884}
885
886std::vector<int> SchemaRegistry::versions(const std::string& schemaId) {
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) {
892 (void)definition;
893 result.push_back(version);
894 }
895 return result;
896}
897
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);
903 else
904 ++it;
905 }
906 if (!removed)
908 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "schema id is not registered", id, {}, "schema"));
910}
911
912eve::Result<void> SchemaRegistry::remove(const std::string& id, int schemaVersion) {
913 const auto schemaIt = schemas().find(id);
914 if (schemaIt == schemas().end())
916 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "schema id is not registered", id, {}, "schema"));
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);
922 else
923 ++it;
924 }
925 if (!removed)
927 "schema version is not registered", id, {}, "schema"));
929}
930
932 schemas().clear();
933 migrationSteps().clear();
934}
935
936int SchemaRegistry::count() { return static_cast<int>(schemas().size()); }
937
939 int result = 0;
940 for (const auto& [id, versions] : schemas()) {
941 (void)id;
942 result += static_cast<int>(versions.size());
943 }
944 return result;
945}
946
947std::vector<std::string> SchemaRegistry::ids() {
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());
952 return result;
953}
954
955std::vector<ValidationError> SchemaRegistry::validate(const std::string& schemaId, const std::string& json) {
956 const auto* schema = find(schemaId);
957 std::vector<ValidationError> errors;
958 if (!schema) {
959 addError(errors, "", "schema_not_found", "schema '" + schemaId + "' is not registered");
960 return errors;
961 }
962 return validate(schemaId, schema->version, json);
963}
964
965std::vector<ValidationError> SchemaRegistry::validate(const std::string& schemaId, int schemaVersion,
966 const std::string& json) {
967 std::vector<ValidationError> errors;
968 const auto* schema = resolve(schemaId, schemaVersion);
969 if (!schema) {
970 addError(errors, "", "schema_version_not_found",
971 "schema '" + schemaId + "' version " + std::to_string(schemaVersion) + " is not registered");
972 return errors;
973 }
974 std::string parseError;
975 const auto document = eve::json::Document::parse(json, &parseError);
976 const JsonValue root = document.root();
977 if (!document.valid()) {
978 addError(errors, "", "invalid_json", parseError);
979 return errors;
980 }
981 std::set<std::string> activeRefs;
982 activeRefs.insert(schema->id + "@" + std::to_string(schema->version));
983 validateObject(*schema, root, "", errors, activeRefs);
984 return errors;
985}
986
987eve::Result<void> SchemaRegistry::registerMigration(const std::string& schemaId, int fromVersion, int toVersion,
988 MigrationFunction migration) {
989 if (schemaId.empty() || fromVersion <= 0 || toVersion <= 0 || !migration)
991 eve::DiagnosticCode::InvalidArgument, "migration id, versions, and function are required", {}));
992 if (fromVersion == toVersion)
994 eve::Diagnostic::error(eve::DiagnosticCode::Conflict, "a self migration edge is a cycle", "schemaVersion"));
995 if (fromVersion > toVersion)
997 eve::DiagnosticCode::Unsupported, "schema downgrade migrations are not supported", "schemaVersion"));
998
999 const MigrationKey key{schemaId, fromVersion};
1000 if (migrationSteps().contains(key))
1002 eve::Diagnostic::error(eve::DiagnosticCode::Conflict, "a migration edge already exists", "schemaVersion"));
1003 if (migrationReaches(schemaId, toVersion, fromVersion))
1005 eve::DiagnosticCode::Conflict, "migration edge would create a cycle", "schemaVersion"));
1006
1007 try {
1008 auto candidate = migrationSteps();
1009 candidate.emplace(key, MigrationStep{toVersion, std::move(migration)});
1010 migrationSteps().swap(candidate);
1011 } catch (const std::exception& exception) {
1013 eve::DiagnosticCode::Failed, std::string("migration registration failed: ") + exception.what(), {}));
1014 } catch (...) {
1016 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "migration registration failed", {}));
1017 }
1019}
1020
1022 int toVersion) {
1023 if (schemaId.empty() || fromVersion <= 0 || toVersion <= 0)
1025 eve::DiagnosticCode::InvalidArgument, "schema id and versions must be positive", "schemaVersion"));
1026 if (!resolve(schemaId, fromVersion) || !resolve(schemaId, toVersion))
1028 eve::DiagnosticCode::UnknownVersion, "source or target schema version is not registered", "schemaVersion"));
1029 if (fromVersion > toVersion)
1031 eve::DiagnosticCode::Unsupported, "schema downgrade compatibility is not supported", "schemaVersion"));
1032
1033 SchemaCompatibility result;
1034 result.schemaId = schemaId;
1035 result.fromVersion = fromVersion;
1036 result.toVersion = toVersion;
1037 result.versions.push_back(fromVersion);
1038 if (fromVersion == toVersion) return eve::Result<SchemaCompatibility>::success(std::move(result));
1039
1040 std::set<int> visited;
1041 int current = fromVersion;
1042 while (current != toVersion) {
1043 if (current > toVersion)
1045 eve::DiagnosticCode::Unsupported, "migration chain would downgrade the payload", "schemaVersion"));
1046 if (!visited.insert(current).second)
1048 eve::DiagnosticCode::Conflict, "migration chain contains a cycle", "schemaVersion"));
1049 const auto it = migrationSteps().find(MigrationKey{schemaId, current});
1050 if (it == migrationSteps().end())
1052 eve::DiagnosticCode::Unsupported, "migration chain is missing an explicit edge", "schemaVersion"));
1053 if (it->second.toVersion <= current)
1055 eve::DiagnosticCode::Conflict, "migration chain contains a non-forward edge", "schemaVersion"));
1056 if (it->second.toVersion > toVersion)
1058 eve::DiagnosticCode::Unsupported, "migration edge overshoots the requested target", "schemaVersion"));
1059 current = it->second.toVersion;
1060 if (!resolve(schemaId, current))
1063 "migration chain reaches an unregistered schema version", "schemaVersion"));
1064 result.versions.push_back(current);
1065 }
1066 return eve::Result<SchemaCompatibility>::success(std::move(result));
1067}
1068
1069eve::Result<eve::Value> SchemaRegistry::migrate(const std::string& schemaId, int fromVersion, int toVersion,
1070 const eve::Value& input) {
1071 auto compatibility = queryCompatibility(schemaId, fromVersion, toVersion);
1072 if (!compatibility.ok()) return eve::Result<eve::Value>::failure(compatibility.status());
1073
1074 auto inputJson = input.toJson();
1075 if (!inputJson.ok()) return eve::Result<eve::Value>::failure(inputJson.status());
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));
1081
1083 try {
1084 current = input;
1085 const auto& path = compatibility.value().versions;
1086 std::vector<MigrationFunction> chain;
1087 chain.reserve(path.size() > 0 ? path.size() - 1 : 0);
1088 for (size_t index = 1; index < path.size(); ++index) {
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);
1095 }
1096 for (size_t index = 1; index < path.size(); ++index) {
1097 auto next = chain[index - 1](current);
1098 if (!next.ok()) return eve::Result<eve::Value>::failure(next.status());
1099 eve::Value candidate = std::move(next).takeValue();
1100 auto candidateJson = candidate.toJson();
1101 if (!candidateJson.ok()) return eve::Result<eve::Value>::failure(candidateJson.status());
1102 const auto errors = validate(schemaId, path[index], candidateJson.value());
1103 if (!errors.empty())
1106 "migration output does not satisfy its target schema", errors.front().path));
1107 current = std::move(candidate);
1108 }
1109 } catch (const std::exception& exception) {
1111 eve::DiagnosticCode::Failed, std::string("migration execution failed: ") + exception.what(), {}));
1112 } catch (...) {
1114 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "migration execution failed", {}));
1115 }
1116 return eve::Result<eve::Value>::success(std::move(current));
1117}
1118
1119eve::Result<std::string> SchemaRegistry::migrateJson(const std::string& schemaId, int fromVersion, int toVersion,
1120 const std::string& json) {
1121 auto input = eve::Value::fromJson(json);
1122 if (!input.ok()) return eve::Result<std::string>::failure(input.status());
1123 auto migrated = migrate(schemaId, fromVersion, toVersion, input.value());
1124 if (!migrated.ok()) return eve::Result<std::string>::failure(migrated.status());
1125 return std::move(migrated).andThen([](eve::Value&& value) { return value.toJson(); });
1126}
1127
1128} // namespace eve::schema
LogicalId target
double value
Duration start
int root
Definition AnimSmr.cpp:119
std::string output
std::string variant
EvpackChunkInput input
Definition Evpack.cpp:170
std::string message
DiagnosticCode code
std::uint32_t key
wgpu::PopErrorScopeStatus status
std::string text
std::string name
std::string error
Definition Package.cpp:60
std::string path
Definition PlayHost.cpp:110
std::vector< std::string > fields
Definition PlayHost.cpp:111
float begin
const RoadNode * node
double number
bool found
int removed
MigrationFunction function
int toVersion
double current
float step
Definition TreeMesh.cpp:314
float size
Definition TreeMesh.cpp:156
uint32_t index
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.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
Result< std::string > toJson() const
Serialize this value as deterministic compact JSON.
Definition Value.cpp:67
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
Definition Value.cpp:57
Value * find(const std::string &key) noexcept
Return an object member, or nullptr when absent.
Definition Value.cpp:124
static Document parse(const std::string &text, std::string *error=nullptr)
Parse.
Definition Json.cpp:581
EVENGINE_API_FOUNDATION public API.
Definition Json.h:38
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.
Definition SchemaTypes.h:19
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.
Definition SchemaTypes.h:16
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.
Versioned runtime schema for a JSON object.
Definition SchemaTypes.h:93