File size: 1,389 Bytes
feb1b1c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e776c3c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50


from __future__ import annotations

from typing import final

from pydantic import BaseModel, ConfigDict, model_validator

from redstack.domain.enums import Severity, ValidationCode

_STRICT = ConfigDict(
    frozen=True, extra="forbid", str_strip_whitespace=True, validate_default=True
)


@final
class ValidationFinding(BaseModel):
    """A single validation outcome."""

    model_config = _STRICT

    code: ValidationCode
    severity: Severity
    message: str
    location: str | None


@final
class ValidationReport(BaseModel):
    """The validator verdict; valid iff no HARD finding."""

    model_config = _STRICT

    findings: tuple[ValidationFinding, ...]
    is_valid: bool
    checks_run: frozenset[ValidationCode]

    @model_validator(mode="after")
    def _check(self) -> ValidationReport:
        if tuple(sorted(self.findings, key=lambda f: f.code.value)) != self.findings:
            raise ValueError("validation findings must be sorted by code")
        if self.is_valid != all(f.severity is not Severity.HARD for f in self.findings):
            raise ValueError("is_valid must equal (no HARD finding)")
        for finding in self.findings:
            if finding.code not in self.checks_run:
                raise ValueError("every finding's code must be in checks_run")
        return self


__all__: tuple[str, ...] = ("ValidationFinding", "ValidationReport")