mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 07:31:32 +02:00
137 lines
3.7 KiB
Python
137 lines
3.7 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any
|
|
import tomllib
|
|
|
|
|
|
DEFAULT_DAEMONS = {
|
|
"clearnet": "nginx",
|
|
"tor": "tor",
|
|
"i2p": "i2pd",
|
|
"gemini": "agate",
|
|
"reticulum": "rnsd",
|
|
}
|
|
|
|
DEFAULT_POLICIES = {
|
|
"clearnet": "adopt",
|
|
"tor": "auto",
|
|
"i2p": "auto",
|
|
"gemini": "auto",
|
|
"reticulum": "auto",
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SourceConfig:
|
|
kind: str
|
|
path: Path
|
|
canonical_url: str | None = None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class OutputConfig:
|
|
root: Path
|
|
plan_root: Path
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ProtocolConfig:
|
|
name: str
|
|
enabled: bool
|
|
renderer: str
|
|
daemon: str
|
|
daemon_policy: str
|
|
options: dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SiteConfig:
|
|
id: str
|
|
domain: str
|
|
source: SourceConfig
|
|
outputs: OutputConfig
|
|
protocols: dict[str, ProtocolConfig]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class GatewayConfig:
|
|
config_path: Path
|
|
sites: list[SiteConfig]
|
|
|
|
|
|
def load_config(path: Path) -> GatewayConfig:
|
|
config_path = path.resolve()
|
|
data = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
|
sites = [_parse_site(raw_site, config_path.parent) for raw_site in data.get("site", [])]
|
|
if not sites:
|
|
raise ValueError("config must declare at least one [[site]]")
|
|
return GatewayConfig(config_path=config_path, sites=sites)
|
|
|
|
|
|
def _parse_site(raw_site: dict[str, Any], base_dir: Path) -> SiteConfig:
|
|
source = raw_site.get("source") or {}
|
|
outputs = raw_site.get("outputs") or {}
|
|
protocols = raw_site.get("protocols") or {}
|
|
|
|
site_id = _required_str(raw_site, "id")
|
|
domain = _required_str(raw_site, "domain")
|
|
source_kind = _required_str(source, "kind")
|
|
if source_kind != "static-html":
|
|
raise ValueError(f"{site_id}: unsupported source kind {source_kind!r}")
|
|
|
|
source_path = _resolve_path(base_dir, _required_str(source, "path"))
|
|
output_root = _resolve_path(base_dir, _required_str(outputs, "root"))
|
|
plan_root = _resolve_path(base_dir, str(outputs.get("plan_root", "../dist/ampg-plan")))
|
|
|
|
parsed_protocols = {
|
|
name: _parse_protocol(name, raw_protocol)
|
|
for name, raw_protocol in protocols.items()
|
|
if isinstance(raw_protocol, dict)
|
|
}
|
|
|
|
return SiteConfig(
|
|
id=site_id,
|
|
domain=domain,
|
|
source=SourceConfig(
|
|
kind=source_kind,
|
|
path=source_path,
|
|
canonical_url=source.get("canonical_url"),
|
|
),
|
|
outputs=OutputConfig(root=output_root, plan_root=plan_root),
|
|
protocols=parsed_protocols,
|
|
)
|
|
|
|
|
|
def _parse_protocol(name: str, raw_protocol: dict[str, Any]) -> ProtocolConfig:
|
|
enabled = bool(raw_protocol.get("enabled", False))
|
|
renderer = str(raw_protocol.get("renderer", name))
|
|
daemon = str(raw_protocol.get("daemon", DEFAULT_DAEMONS.get(name, name)))
|
|
policy = str(raw_protocol.get("daemon_policy", DEFAULT_POLICIES.get(name, "auto")))
|
|
options = dict(raw_protocol)
|
|
for key in ("enabled", "renderer", "daemon", "daemon_policy"):
|
|
options.pop(key, None)
|
|
return ProtocolConfig(
|
|
name=name,
|
|
enabled=enabled,
|
|
renderer=renderer,
|
|
daemon=daemon,
|
|
daemon_policy=policy,
|
|
options=options,
|
|
)
|
|
|
|
|
|
def _resolve_path(base_dir: Path, raw_path: str) -> Path:
|
|
path = Path(raw_path).expanduser()
|
|
if not path.is_absolute():
|
|
path = base_dir / path
|
|
return path.resolve()
|
|
|
|
|
|
def _required_str(data: dict[str, Any], key: str) -> str:
|
|
value = data.get(key)
|
|
if not isinstance(value, str) or not value:
|
|
raise ValueError(f"missing required string field {key!r}")
|
|
return value
|