⟨/⟩ Nts/55_Backend/Calc/CalcValidators.cs

107 строк · в начало

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,
    }
}