diff --git a/src/apiup/config.py b/src/apiup/config.py index cbe7b5f..264ed31 100644 --- a/src/apiup/config.py +++ b/src/apiup/config.py @@ -2,25 +2,39 @@ Config resolution for apiup. Convention: - ~/.openapi/spec.yaml — default OpenAPI spec + ~/.openapi/spec.json — default OpenAPI spec (JSON) + ~/.openapi/spec.yaml — default OpenAPI spec (YAML, checked second) ~/.openapi/config.yaml — optional defaults (port, host, mode, spec) """ from __future__ import annotations import sys -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path from typing import Any DEFAULT_CONFIG_DIR: Path = Path.home() / ".openapi" -DEFAULT_SPEC: Path = DEFAULT_CONFIG_DIR / "spec.yaml" DEFAULT_CONFIG: Path = DEFAULT_CONFIG_DIR / "config.yaml" +# Auto-detection order: spec.json first, then spec.yaml +DEFAULT_SPEC_CANDIDATES: list[Path] = [ + DEFAULT_CONFIG_DIR / "spec.json", + DEFAULT_CONFIG_DIR / "spec.yaml", +] + + +def _default_spec() -> str: + """Return first existing default spec, or spec.json as the canonical fallback.""" + for candidate in DEFAULT_SPEC_CANDIDATES: + if candidate.exists(): + return str(candidate) + return str(DEFAULT_SPEC_CANDIDATES[0]) + @dataclass class ApiupConfig: - spec: str = str(DEFAULT_SPEC) + spec: str = field(default_factory=_default_spec) port: int = 8080 host: str = "127.0.0.1" mode: str = "mock" @@ -47,7 +61,7 @@ def load_config( ) -> ApiupConfig: """Resolve config from file + CLI overrides. - Precedence: CLI args > ~/.openapi/config.yaml > built-in defaults. + Precedence: CLI args > ~/.openapi/config.yaml > auto-detected default > spec.json fallback. """ cfg = ApiupConfig() diff --git a/tests/test_apiup.py b/tests/test_apiup.py index af9fd07..c0ea43a 100644 --- a/tests/test_apiup.py +++ b/tests/test_apiup.py @@ -119,3 +119,63 @@ def test_mock_no_example_returns_stub() -> None: def test_load_spec_missing_file() -> None: with pytest.raises(SystemExit): load_spec("/nonexistent/path/spec.yaml") + + +# ── config.py tests ────────────────────────────────────────────────────────── + + +def test_default_spec_prefers_json(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """spec.json is picked over spec.yaml when both exist.""" + import apiup.config as cfg_mod + + openapi_dir = tmp_path / ".openapi" + openapi_dir.mkdir() + (openapi_dir / "spec.json").write_text( + '{"openapi":"3.0.3","info":{"title":"T","version":"1"},"paths":{}}' + ) + (openapi_dir / "spec.yaml").write_text( + "openapi: 3.0.3\ninfo:\n title: T\n version: '1'\npaths: {}" + ) + + monkeypatch.setattr(cfg_mod, "DEFAULT_CONFIG_DIR", openapi_dir) + monkeypatch.setattr( + cfg_mod, + "DEFAULT_SPEC_CANDIDATES", + [ + openapi_dir / "spec.json", + openapi_dir / "spec.yaml", + ], + ) + monkeypatch.setattr(cfg_mod, "DEFAULT_CONFIG", openapi_dir / "config.yaml") + + from apiup.config import load_config + + config = load_config() + assert config.spec.endswith("spec.json") + + +def test_default_spec_falls_back_to_yaml(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """spec.yaml is used when spec.json does not exist.""" + import apiup.config as cfg_mod + + openapi_dir = tmp_path / ".openapi" + openapi_dir.mkdir() + (openapi_dir / "spec.yaml").write_text( + "openapi: 3.0.3\ninfo:\n title: T\n version: '1'\npaths: {}" + ) + + monkeypatch.setattr(cfg_mod, "DEFAULT_CONFIG_DIR", openapi_dir) + monkeypatch.setattr( + cfg_mod, + "DEFAULT_SPEC_CANDIDATES", + [ + openapi_dir / "spec.json", + openapi_dir / "spec.yaml", + ], + ) + monkeypatch.setattr(cfg_mod, "DEFAULT_CONFIG", openapi_dir / "config.yaml") + + from apiup.config import load_config + + config = load_config() + assert config.spec.endswith("spec.yaml")