using System;
using System.Collections.Generic;
using Rugrid.Sdaid.Notation.Documents;
namespace Rugrid.Sdaid.Epas.Nts.Backend.Calc
{
/// <summary>
/// Система валидации расчёта: как устроены валидаторы, где описаны правила и что они
/// возвращают. Ошибки валидации сохраняются в доменной сущности «Расчёт ТС».
/// </summary>
public class DOC_CalcValidators : IDomainEntityDocument, IHasStructuralLinks, IFolder<NtsBackendFolder>
{
public SdaidLink[] Links => new[]
{
Rel.Uses<DOC_NtsCalc>(),
Rel.Uses<DOC_NtsCalcErrors>(),
Rel.Uses<DOC_InputModel>(),
Rel.Uses<Rugrid.Sdaid.Epas.Nts.Domain.DOC_VehicleCalc>(),
};
public static string _refName = "Валидаторы расчёта";
public string Name => "Валидаторы расчёта";
public string Description =>
@"Валидация исходных данных расчёта: как устроены валидаторы, где описаны правила и что возвращают; ошибки сохраняются в «Расчёт ТС».";
public string Version => "0.1";
public string Status => "draft";
public string[] Comments => new string[0];
public Type Entity => typeof(CalcErrors);
public static string Body = $$"""
## Как устроена валидация
- **У каждой секции методики ({{SdaidAnchors.RefTo<DOC_NtsCalc>()}}) есть валидатор** —
это архитектурное правило узла графа расчёта, не зависящее от того, есть ли у секции
собственные правила проверки: оркестратор вызывает валидатор и калькулятор секции
одинаково для любого узла, без ветвления «если у секции есть валидатор». У секции без
собственных исходных данных (все входы — выходы других секций, например
`fuel.q_transport`) валидатор пуст: возвращает пустой список ошибок, но всё равно
собирает полученные параметры в типизированный вход секции (`<Section>Input`) для её
калькулятора — не тождественная передача, а адаптация формы данных под контракт секции,
просто без правил отбраковки (см. прецедент `LightingModelTotalsValidator`/
`AirExchangeRateValidator` в `Ntr.Calc`). Это правило — стабильное, менять его не
планируется.
- **Сейчас валидируются исходные данные** расчёта — поля {{SdaidAnchors.RefTo<DOC_InputModel>()}};
входные параметры секции, посчитанные другими секциями, здесь не проверяются — они
валидированы в своих секциях. Это текущая практика, а не инвариант узла: если появится
потребность в перекрёстной валидации уже посчитанных параметров нескольких секций —
это утверждение (а не предыдущее) будет пересмотрено.
- **Правила.** Описаны в самой секции методики (блок «Правила валидации»); коды, условия и
тексты — в {{SdaidAnchors.RefTo<DOC_NtsCalcErrors>()}}.
- **Порядок.** Сначала валидатор, затем (при отсутствии нарушений) калькулятор секции. При
нарушении секция не считается, результат расчёта не собирается (`OutputModel = null`),
калькулятор возвращает `CalculationStatus.Failed`.
- **Что возвращает валидатор.** `(IReadOnlyList<ValidationError> Errors, <Section>Input? Validated)`:
список ошибок и вход секции для калькулятора (`null`, если есть ошибки).
## Ошибка валидации
Ошибка — `ValidationError`:
- `MethodSectionId` — идентификатор секции методики (`fuel.*`);
- `Path` — путь к атрибуту `InputModel`;
- `RuleCode` — категория правила (`ValidationRuleCode`: `Required`, `OutOfRange`,
`Inconsistent`, `EmptyCollection`);
- `Code` — код ошибки (категория) из классификатора {{SdaidAnchors.RefTo<DOC_NtsCalcErrors>()}};
- `Message` — текст для пользователя;
- `RefId` — идентификатор ТС.
Ошибки сохраняются в {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Domain.DOC_VehicleCalc>()}} (`calc_errors_json`).
""";
}
/// <summary>Ошибки расчёта (`CalcErrors`).</summary>
[DomainEntity]
public class CalcErrors
{
/// <summary>Ошибки расчёта</summary>
[DomainField(Alias = "fuel", Description = "Ошибки расчёта по секциям методики")]
public IReadOnlyList<ValidationError>? Fuel { get; set; }
}
/// <summary>Ошибка расчёта (`ValidationError`).</summary>
public record ValidationError(
string MethodSectionId,
string Path,
ValidationRuleCode RuleCode,
string Message,
string? RefId = null,
string? Code = null);
/// <summary>Категория правила валидации (`ValidationRuleCode`).</summary>
public enum ValidationRuleCode
{
/// <summary>Обязательное для секции поле не задано.</summary>
Required,
/// <summary>Значение вне допустимого диапазона.</summary>
OutOfRange,
/// <summary>Группа полей противоречит друг другу.</summary>
Inconsistent,
/// <summary>Список пуст там, где методика требует ≥ 1 элемента.</summary>
EmptyCollection,
}
}