| 1 | """Load + validate a ``sway.yaml`` into a :class:`SwaySpec`. |
| 2 | |
| 3 | Separated from :mod:`spec` so the data models stay trivially |
| 4 | importable (no YAML dependency at import time for callers that |
| 5 | construct specs programmatically). |
| 6 | """ |
| 7 | |
| 8 | from __future__ import annotations |
| 9 | |
| 10 | from pathlib import Path |
| 11 | from typing import Any |
| 12 | |
| 13 | import yaml |
| 14 | from pydantic import ValidationError |
| 15 | |
| 16 | from dlm_sway.core.errors import SpecValidationError |
| 17 | from dlm_sway.suite.spec import SwaySpec |
| 18 | |
| 19 | |
| 20 | def load_spec(path: Path | str) -> SwaySpec: |
| 21 | """Parse ``path`` and return a validated :class:`SwaySpec`.""" |
| 22 | resolved = Path(path).expanduser().resolve() |
| 23 | try: |
| 24 | raw_text = resolved.read_text(encoding="utf-8") |
| 25 | except FileNotFoundError as exc: |
| 26 | raise SpecValidationError(f"spec file not found: {resolved}", source=str(path)) from exc |
| 27 | |
| 28 | try: |
| 29 | data = yaml.safe_load(raw_text) |
| 30 | except yaml.YAMLError as exc: |
| 31 | raise SpecValidationError(f"invalid YAML: {exc}", source=str(path)) from exc |
| 32 | |
| 33 | if not isinstance(data, dict): |
| 34 | raise SpecValidationError("top-level document must be a mapping", source=str(path)) |
| 35 | return from_dict(data, source=str(path)) |
| 36 | |
| 37 | |
| 38 | def from_dict(data: dict[str, Any], *, source: str | None = None) -> SwaySpec: |
| 39 | """Validate a dict (already parsed from YAML or JSON) as a SwaySpec.""" |
| 40 | try: |
| 41 | spec = SwaySpec.model_validate(data) |
| 42 | except ValidationError as exc: |
| 43 | raise SpecValidationError(str(exc), source=source) from exc |
| 44 | try: |
| 45 | spec.check_version() |
| 46 | except ValueError as exc: |
| 47 | raise SpecValidationError(str(exc), source=source) from exc |
| 48 | return spec |