Source code for buffalo_wings.airfoil.internal.validation.common

"""Shared validation utilities and result types for airfoil schema checks."""

from __future__ import annotations

import math
import re
from copy import deepcopy
from dataclasses import dataclass
from typing import TYPE_CHECKING, Literal, cast

from buffalo_core.diagnostics import (
    Diagnostic,
    DiagnosticLocation,
    DiagnosticReport,
    DiagnosticSeverity,
)

from buffalo_wings.airfoil.internal.schema import AirfoilDefinitionSpec
from buffalo_wings.airfoil.internal.schema.common import Point2DSpec

if TYPE_CHECKING:
    from buffalo_wings.airfoil.internal.schema.document import (
        AirfoilDocumentSpec,
    )

NACA5_REFLEX_DIGIT_INDEX = 2
NACA16_DESIGNATION_PATTERN = re.compile(r"^16-(\d)(\d{2})$")
NACA6_DESIGNATION_PATTERN = re.compile(
    r"^(6[3-7])(?:\(([1-9])\))?-(\d)(\d{2})$"
)
NACA6A_DESIGNATION_PATTERN = re.compile(r"^(6[3-5]A)-(\d)(\d{2})$")
SUPPORTED_FILE_FORMATS = {
    "auto",
    "lednicer",
    "selig",
    "surface_curve",
    "upper_lower",
}
SUPPORTED_POINTS_FORMATS = {"surface_curve", "upper_lower"}
SUPPORTED_NACA6_SERIES = {"63", "64", "65", "66", "67"}
SUPPORTED_NACA6A_SERIES = {"63A", "64A", "65A"}
UPPER_LOWER_MIN_POINTS = 2
SURFACE_CURVE_MIN_POINTS = 3
POLYGON_MIN_VERTICES = 3
POINT2D_LENGTH = 2
NACA4_MODIFIED_DIGIT_LENGTH = 6
NACA5_MODIFIED_DIGIT_LENGTH = 7
NACA16_MAX_IDEAL_LIFT_COEFFICIENT = 0.9
NACA_SERIES_MAX_THICKNESS = 0.4
CAMBER_COMPONENT_MIN = -0.5
CAMBER_COMPONENT_MAX = 0.5
SUPPORTED_TRAILING_EDGE_OPTIONS = {"standard", "sharp"}
SUPPORTED_LEADING_EDGE_RADIUS_OPTIONS = {"standard", "exact"}
SUPPORTED_STRUCTURED_FILE_SUFFIXES = {".json", ".yaml", ".yml"}
SUPPORTED_STRUCTURED_PAYLOAD_FORMATS = {"surface_curve", "upper_lower"}
TITLE_AND_COUNT_LINE_COUNT = 2

StructuredPayloadFormat = Literal["surface_curve", "upper_lower"]
DetectedFileFormat = Literal[
    "selig",
    "lednicer",
    "surface_curve",
    "upper_lower",
]


[docs] @dataclass(frozen=True, slots=True) class AirfoilValidationResult: """Structured validation result for one airfoil schema definition.""" is_valid: bool """ Whether validation completed without any error diagnostics. This flag is ``True`` only when the associated diagnostic report contains no errors. """ diagnostics: DiagnosticReport """ Structured diagnostics emitted by the validation workflow. The report may include warnings or informational entries even when ``is_valid`` is ``True``. """ validated_spec: AirfoilDefinitionSpec | None = None """ Defensive copy of the validated schema when validation succeeds. This field remains ``None`` whenever validation reports any error. """
[docs] @dataclass(frozen=True, slots=True) class AirfoilDocumentValidationResult: """Structured validation result for one airfoil document schema.""" is_valid: bool """Whether validation completed without any error diagnostics.""" diagnostics: DiagnosticReport """Structured diagnostics emitted by the document validation workflow.""" validated_spec: AirfoilDocumentSpec | None = None """ Defensive copy of the validated document when validation succeeds. This field remains ``None`` whenever validation reports any error. """
def _is_real_number(value: object) -> bool: """ Return whether ``value`` is a finite real scalar. Parameters ---------- value : object Candidate scalar-like object. Returns ------- bool ``True`` when ``value`` is a finite ``int`` or ``float`` but not a boolean. """ if isinstance(value, bool): return False if not isinstance(value, int | float): return False return math.isfinite(float(value)) def _is_point2d(value: object) -> bool: """ Return whether ``value`` is a numeric two-component point. Parameters ---------- value : object Candidate point-like object. Returns ------- bool ``True`` when ``value`` is a list or tuple of two finite real scalars. """ if not isinstance(value, tuple | list): return False point = cast(tuple[object, ...] | list[object], value) if len(point) != POINT2D_LENGTH: return False return _is_real_number(point[0]) and _is_real_number(point[1]) def _structured_payload_format( value: object, ) -> StructuredPayloadFormat | None: """ Return one validated structured point-payload format literal. Parameters ---------- value : object Candidate structured payload format token. Returns ------- StructuredPayloadFormat | None Normalized format literal when supported, otherwise ``None``. """ if value == "surface_curve": return "surface_curve" if value == "upper_lower": return "upper_lower" return None def _distance(point_a: Point2DSpec, point_b: Point2DSpec) -> float: """ Return the Euclidean distance between two planar points. Returns ------- float Straight-line distance between ``point_a`` and ``point_b``. """ delta_x = float(point_a[0]) - float(point_b[0]) delta_y = float(point_a[1]) - float(point_b[1]) return math.hypot(delta_x, delta_y) def _trailing_edge_midpoint( point_a: Point2DSpec, point_b: Point2DSpec, ) -> Point2DSpec: """ Return the midpoint of two trailing-edge endpoints. Returns ------- Point2DSpec Midpoint between the two provided endpoints. """ return ( 0.5 * (float(point_a[0]) + float(point_b[0])), 0.5 * (float(point_a[1]) + float(point_b[1])), ) def _error( *, code: str, message: str, field_path: str, object_path: str = "airfoil", ) -> Diagnostic: """ Build one schema-validation error diagnostic. Returns ------- Diagnostic Error diagnostic localized to the requested object and field path. """ return Diagnostic( severity=DiagnosticSeverity.ERROR, code=code, message=message, location=DiagnosticLocation( object_path=object_path, field_path=field_path, ), ) def _file_error( *, code: str, message: str, field_path: str, ) -> Diagnostic: """ Build one file-source validation error diagnostic. Returns ------- Diagnostic Error diagnostic rooted at the file-source object path. """ return _error( code=code, message=message, field_path=field_path, object_path="airfoil.file_source", ) def _file_info( *, code: str, message: str, field_path: str, ) -> Diagnostic: """ Build one file-source informational diagnostic. Returns ------- Diagnostic Informational diagnostic rooted at the file-source object path. """ return Diagnostic( severity=DiagnosticSeverity.INFO, code=code, message=message, location=DiagnosticLocation( object_path="airfoil.file_source", field_path=field_path, ), ) def _support_error(spec: AirfoilDefinitionSpec) -> Diagnostic: """ Build the shared unsupported-runtime diagnostic. Returns ------- Diagnostic Error diagnostic stating that the current runtime cannot yet construct ``spec``. """ return _error( code="airfoil.support.unsupported_type", message=f"Airfoil runtime does not support {type(spec).__name__} yet.", field_path="type", ) def _result_from_diagnostics( spec: AirfoilDefinitionSpec, diagnostics: tuple[Diagnostic, ...], ) -> AirfoilValidationResult: """ Build the public validation result from a diagnostic tuple. Returns ------- AirfoilValidationResult Structured validation result with a defensive spec copy when no errors are present. """ report = DiagnosticReport(entries=diagnostics) if report.has_errors: return AirfoilValidationResult( is_valid=False, diagnostics=report, validated_spec=None, ) return AirfoilValidationResult( is_valid=True, diagnostics=report, validated_spec=deepcopy(spec), ) def _document_result_from_diagnostics( document: AirfoilDocumentSpec, diagnostics: tuple[Diagnostic, ...], ) -> AirfoilDocumentValidationResult: """ Build the public document-validation result from a diagnostic tuple. Returns ------- AirfoilDocumentValidationResult Structured validation result with a defensive document copy when no errors are present. """ report = DiagnosticReport(entries=diagnostics) if report.has_errors: return AirfoilDocumentValidationResult( is_valid=False, diagnostics=report, validated_spec=None, ) return AirfoilDocumentValidationResult( is_valid=True, diagnostics=report, validated_spec=deepcopy(document), ) def _validated_or_unreachable[T]( value: T | None, *, context: str, ) -> T: """ Return one guarded validated value or fail as an internal invariant. Raises ------ AssertionError If ``value`` is unexpectedly ``None`` after prior validation guards. """ if value is None: msg = ( f"{context} validation invariant violated after prior guard checks." ) raise AssertionError(msg) return value is_real_number = _is_real_number is_point2d = _is_point2d coerce_structured_payload_format = _structured_payload_format point_distance = _distance trailing_edge_midpoint = _trailing_edge_midpoint build_validation_error = _error build_file_validation_error = _file_error build_file_validation_info = _file_info build_support_error = _support_error document_result_from_diagnostics = _document_result_from_diagnostics result_from_diagnostics = _result_from_diagnostics validated_or_unreachable = _validated_or_unreachable