11
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import quote, urlsplit
|
||||
|
||||
|
||||
class APIConfigError(ValueError):
|
||||
"""API 配置缺失或格式不合法。"""
|
||||
|
||||
|
||||
class APIEndpointRegistry:
|
||||
"""将服务地址与端点相对路径分离。"""
|
||||
|
||||
def __init__(self, config: Mapping[str, Any]):
|
||||
self._services = config.get("_services", {})
|
||||
self._endpoints = {
|
||||
key: value for key, value in config.items() if not str(key).startswith("_")
|
||||
}
|
||||
self._validate()
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, config_path: str | Path) -> "APIEndpointRegistry":
|
||||
path = Path(config_path)
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as file:
|
||||
config = json.load(file)
|
||||
except FileNotFoundError as exc:
|
||||
raise APIConfigError(f"API 配置文件不存在: {path}") from exc
|
||||
except json.JSONDecodeError as exc:
|
||||
raise APIConfigError(
|
||||
f"API 配置 JSON 不合法: {path}, line={exc.lineno}"
|
||||
) from exc
|
||||
|
||||
if not isinstance(config, Mapping):
|
||||
raise APIConfigError("API 配置根节点必须是对象")
|
||||
return cls(config)
|
||||
|
||||
def _validate(self) -> None:
|
||||
if not isinstance(self._services, Mapping):
|
||||
raise APIConfigError("_services 必须是对象")
|
||||
|
||||
for service_name, service in self._services.items():
|
||||
if not isinstance(service, Mapping):
|
||||
raise APIConfigError(f"服务配置必须是对象: {service_name}")
|
||||
base_url = str(service.get("base_url", "")).strip()
|
||||
parsed = urlsplit(base_url)
|
||||
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
||||
raise APIConfigError(f"服务 base_url 不合法: {service_name}={base_url}")
|
||||
adapter = str(service.get("adapter", "generic")).lower()
|
||||
if adapter not in {"generic", "jx3api", "jx3box"}:
|
||||
raise APIConfigError(f"服务 adapter 不受支持: {service_name}={adapter}")
|
||||
|
||||
for endpoint_name, endpoint in self._endpoints.items():
|
||||
if not isinstance(endpoint, Mapping):
|
||||
raise APIConfigError(f"端点配置必须是对象: {endpoint_name}")
|
||||
|
||||
method = str(endpoint.get("method", "GET")).upper()
|
||||
if method not in {"GET", "POST"}:
|
||||
raise APIConfigError(
|
||||
f"端点 method 仅支持 GET/POST: {endpoint_name}={method}"
|
||||
)
|
||||
params = endpoint.get("params", {})
|
||||
if not isinstance(params, Mapping):
|
||||
raise APIConfigError(f"端点 params 必须是对象: {endpoint_name}")
|
||||
if "retry" in endpoint and not isinstance(endpoint["retry"], bool):
|
||||
raise APIConfigError(f"端点 retry 必须是布尔值: {endpoint_name}")
|
||||
|
||||
service_name = endpoint.get("service")
|
||||
path = endpoint.get("path")
|
||||
if service_name not in self._services:
|
||||
raise APIConfigError(
|
||||
f"端点引用了不存在的服务: {endpoint_name} -> {service_name}"
|
||||
)
|
||||
if not isinstance(path, str) or not path.strip():
|
||||
raise APIConfigError(f"端点缺少相对路径: {endpoint_name}")
|
||||
|
||||
def get(self, endpoint_name: str) -> Mapping[str, Any]:
|
||||
endpoint = self._endpoints.get(endpoint_name)
|
||||
if endpoint is None:
|
||||
raise APIConfigError(f"未找到 API 端点: {endpoint_name}")
|
||||
return endpoint
|
||||
|
||||
def get_service(self, service_name: str) -> Mapping[str, Any]:
|
||||
service = self._services.get(service_name)
|
||||
if service is None:
|
||||
raise APIConfigError(f"未找到 API 服务: {service_name}")
|
||||
return service
|
||||
|
||||
def resolve(
|
||||
self,
|
||||
endpoint_name: str,
|
||||
path_params: Mapping[str, Any] | None = None,
|
||||
) -> str:
|
||||
endpoint = self.get(endpoint_name)
|
||||
|
||||
service_name = str(endpoint["service"])
|
||||
service = self._services[service_name]
|
||||
base_url = str(service["base_url"]).rstrip("/")
|
||||
path_prefix = str(service.get("path_prefix", "")).strip("/")
|
||||
endpoint_path = str(endpoint["path"]).lstrip("/")
|
||||
|
||||
encoded_params = {
|
||||
key: quote(str(value), safe="")
|
||||
for key, value in (path_params or {}).items()
|
||||
}
|
||||
try:
|
||||
endpoint_path = endpoint_path.format_map(encoded_params)
|
||||
except KeyError as exc:
|
||||
raise APIConfigError(
|
||||
f"端点 {endpoint_name} 缺少路径参数: {exc.args[0]}"
|
||||
) from exc
|
||||
|
||||
path_parts = [part for part in (path_prefix, endpoint_path) if part]
|
||||
return f"{base_url}/{'/'.join(path_parts)}"
|
||||
Reference in New Issue
Block a user