From 9fc152a7613d7fa8dac874619d7b2c9c7656ae7b Mon Sep 17 00:00:00 2001 From: Robert Halter Date: Fri, 20 Mar 2026 18:45:02 +0100 Subject: [PATCH 1/2] Feature: support spec.json as default alongside spec.yaml --- src/apiup/config.py | 24 +++++++++++++++++++----- tests/test_apiup.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 5 deletions(-) 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..3bb474e 100644 --- a/tests/test_apiup.py +++ b/tests/test_apiup.py @@ -119,3 +119,46 @@ 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") From 9f4cb702a38592b507bb56e20429c68e44022c59 Mon Sep 17 00:00:00 2001 From: Robert Halter Date: Fri, 20 Mar 2026 18:48:21 +0100 Subject: [PATCH 2/2] format correction --- tests/test_apiup.py | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/tests/test_apiup.py b/tests/test_apiup.py index 3bb474e..c0ea43a 100644 --- a/tests/test_apiup.py +++ b/tests/test_apiup.py @@ -123,23 +123,33 @@ def test_load_spec_missing_file() -> None: # ── 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: {}") + (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_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") @@ -150,15 +160,22 @@ def test_default_spec_falls_back_to_yaml(tmp_path: Path, monkeypatch: pytest.Mon 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: {}") + (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_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")