feat(cuod-bundle): JSON Schema драфты для spacecraft/satellite_type/ground_station + auto-import

3 schema драфта в classpath:/bundles/cuod/:

- spacecraft.schema.json: КА с COSPAR designator, NORAD ID, multi-locale
  name/operator/mission, orbit params (type/semimajor/eccentricity/inclination/
  raan/aop/epoch), spectrum_bands (12 bands), launch/decay dates, status enum
- satellite_type.schema.json: классификация КА — code, multi-locale name/desc,
  mission_category enum (10 categories), mass_class, typical_orbit_classes,
  typical_resolution_m, typical_revisit_hours
- ground_station.schema.json: code, multi-locale name/location/operator,
  country (ISO 3166), lat/lon/elevation, antennas array (id, diameter,
  bands, polarizations, tx/rx_capable, G/T, EIRP), status

manifest.json: bundle 'cuod' v1.0.0, 3 dictionaries с supported_locales
[ru-RU, en-US] default ru-RU, scope PUBLIC

ordinis-cuod-bundle модуль:
- BundleManifest record + DictionaryEntry record (Jackson deserializer)
- CuodBundleProperties @ConfigurationProperties (auto-import flag,
  update-existing-schema flag, resource-root path)
- CuodBundleImporter @Service (proxified, @Transactional работает) — читает
  manifest + schemas, для каждого: findByName → если нет, создать definition
  + outbox event DefinitionCreated; если есть, проверить schema_version, при
  отличии и update_existing_schema=true — обновить + DefinitionUpdated outbox
- CuodBundleStartupRunner @Configuration с ApplicationRunner — триггерит
  importBundle при старте Spring Boot. Отделено от Importer чтобы Importer
  оставался proxified (@Transactional на importBundle vs lambda не работает)

ordinis-validation FIX:
- RecordValidator теперь учитывает required array родительской схемы.
  Отсутствие x-localized optional поля (например mission, operator) больше
  не считается ошибкой. Required x-localized поля по-прежнему обязательны.

E2E проверено:
- ORDINIS_BUNDLE_CUOD_AUTO_IMPORT=true → 3 справочника создались с правильными
  scope, locales, schema_version
- POST spacecraft с реальными Kanopus-V данными (orbit + spectrum_bands) → 201
- POST satellite_type с code='lower-case-bad' → 422 pattern error
- Idempotent: повторный старт пропускает existing без ошибок
This commit is contained in:
Zimin A.N.
2026-05-03 14:29:43 +03:00
parent 51fcc95818
commit ae69a7642b
10 changed files with 558 additions and 4 deletions
@@ -63,7 +63,8 @@ public class RecordValidator {
/**
* Рекурсивный обход schema. Для каждого {@code properties[name]} с
* {@code x-localized: true} — проверяет значение в data.
* {@code x-localized: true} — проверяет значение в data, учитывая required
* массив родительской схемы (optional поля не требуют значения).
*/
private void walkLocalizedFields(
JsonNode schema,
@@ -78,6 +79,12 @@ public class RecordValidator {
JsonNode properties = schema.get("properties");
if (properties == null || !properties.isObject()) return;
Set<String> requiredFields = new HashSet<>();
JsonNode required = schema.get("required");
if (required != null && required.isArray()) {
required.forEach(n -> { if (n.isTextual()) requiredFields.add(n.asText()); });
}
Iterator<Map.Entry<String, JsonNode>> it = properties.fields();
while (it.hasNext()) {
Map.Entry<String, JsonNode> entry = it.next();
@@ -86,10 +93,11 @@ public class RecordValidator {
String fieldPath = path.isEmpty() ? "/" + fieldName : path + "/" + fieldName;
JsonNode dataField = data == null ? null : data.get(fieldName);
boolean fieldIsRequired = requiredFields.contains(fieldName);
JsonNode xLocalized = fieldSchema.get("x-localized");
if (xLocalized != null && xLocalized.isBoolean() && xLocalized.asBoolean()) {
validateLocalizedValue(fieldPath, dataField, supported, defaultLocale, errors);
validateLocalizedValue(fieldPath, dataField, fieldIsRequired, supported, defaultLocale, errors);
} else if (fieldSchema.has("properties")) {
walkLocalizedFields(fieldSchema, dataField, fieldPath, supported, defaultLocale, errors);
}
@@ -99,13 +107,16 @@ public class RecordValidator {
private void validateLocalizedValue(
String path,
JsonNode value,
boolean isRequired,
Set<String> supported,
String defaultLocale,
List<ValidationError> errors) {
if (value == null || value.isNull()) {
errors.add(new ValidationError(path, "x-localized.missing",
"Localized field is required (must be object with locale keys)"));
if (isRequired) {
errors.add(new ValidationError(path, "x-localized.missing",
"Localized field is required (must be object with locale keys)"));
}
return;
}
if (!value.isObject()) {