Coverage for src/lexigram/web/config/api_docs.py: 70%
30 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
1"""API documentation and static file configuration."""
3from __future__ import annotations
5from dataclasses import dataclass
6from typing import ClassVar
8from lexigram.config import BaseConfig
9from lexigram.validation import ConfigDict, Field
12@dataclass(init=False)
13class APIDocsConfig(BaseConfig):
14 """API documentation configuration.
16 Automatically configures CSP directives for API documentation endpoints
17 (/docs for Swagger UI, /redoc for ReDoc) when enabled.
18 """
20 model_config: ClassVar[ConfigDict] = ConfigDict(extra="ignore")
22 enabled: bool = Field(
23 default=True,
24 description=(
25 "Enable API documentation endpoints (/docs, /redoc) and auto-configure "
26 "CSP for their CDN assets"
27 ),
28 )
29 provider: str = Field(
30 default="both",
31 description="Documentation provider: 'swagger', 'redoc', or 'both'",
32 )
34 # Domains needed for API docs
35 SWAGGER_DOMAINS: ClassVar[dict[str, set[str]]] = {
36 "script-src": {"https://unpkg.com"},
37 "script-src-elem": {"https://unpkg.com"}, # For <script src> elements
38 "style-src": {"https://unpkg.com"},
39 "style-src-elem": {"https://unpkg.com"}, # For <link rel=stylesheet> elements
40 }
42 REDOC_DOMAINS: ClassVar[dict[str, set[str]]] = {
43 "script-src": {"https://cdn.redoc.ly"},
44 "script-src-elem": {"https://cdn.redoc.ly"}, # For <script src> elements
45 "style-src": {"https://fonts.googleapis.com"},
46 "style-src-elem": {
47 "https://fonts.googleapis.com"
48 }, # For <link rel=stylesheet> elements
49 "font-src": {"https://fonts.gstatic.com"},
50 "worker-src": {"blob:"},
51 }
53 def get_required_domains(self) -> dict[str, set[str]]:
54 """Get required CSP domains based on provider setting."""
55 result: dict[str, set[str]] = {}
57 # Determine which providers to include
58 providers = {"swagger", "redoc"} if self.provider == "both" else {self.provider}
60 if "swagger" in providers:
61 for key, values in self.SWAGGER_DOMAINS.items():
62 result.setdefault(key, set()).update(values)
64 if "redoc" in providers:
65 for key, values in self.REDOC_DOMAINS.items():
66 result.setdefault(key, set()).update(values)
68 return result
71@dataclass(init=False)
72class StaticFileConfig(BaseConfig):
73 """Static file serving configuration."""
75 model_config: ClassVar[ConfigDict] = ConfigDict(extra="ignore")
77 enabled: bool = Field(default=False, description="Enable static file serving")
78 directory: str = Field(default="static", description="Directory to serve")
79 prefix: str = Field(default="/static", description="URL prefix for static files")
80 html: bool = Field(default=False, description="Serve HTML files (SPA mode)")
83__all__ = [
84 "APIDocsConfig",
85 "StaticFileConfig",
86]