Coverage for src/lexigram/web/middleware/registry.py: 51%
47 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"""Middleware registry for lexigram-web.
3Enables extensible adaptation of various middleware types (Starlette,
4DIScope, Lexigram-native) into the final Starlette middleware stack.
5"""
7from __future__ import annotations
9from typing import Any, Protocol, cast, runtime_checkable
11from starlette.middleware import Middleware as StarletteMiddleware
14@runtime_checkable
15class _MiddlewareAdapterProtocol(Protocol):
16 """Protocol for middleware adapters."""
18 def can_adapt(self, middleware: Any) -> bool:
19 """Return True if this adapter can handle the given middleware."""
20 ...
22 def adapt(self, middleware: Any) -> StarletteMiddleware:
23 """Convert the middleware into a Starlette-compatible Middleware object."""
24 ...
27class _StarletteMiddlewareAdapter:
28 """Handles native Starlette Middleware objects."""
30 def can_adapt(self, middleware: Any) -> bool:
31 return isinstance(middleware, StarletteMiddleware)
33 def adapt(self, middleware: Any) -> StarletteMiddleware:
34 return cast("StarletteMiddleware", middleware)
37class _TupleMiddlewareAdapter:
38 """Handles (MiddlewareClass, kwargs_dict) tuples."""
40 def can_adapt(self, middleware: Any) -> bool:
41 return isinstance(middleware, tuple) and len(middleware) >= 1
43 def adapt(self, middleware: Any) -> StarletteMiddleware:
44 if len(middleware) == 2 and isinstance(middleware[1], dict):
45 return StarletteMiddleware(middleware[0], **middleware[1])
46 # Positional args (deprecated)
47 return StarletteMiddleware(middleware[0], *middleware[1:])
50class _DIScopeMiddlewareAdapter:
51 """Handles DIScopeMiddleware instances."""
53 def can_adapt(self, middleware: Any) -> bool:
54 # Check by class name to avoid direct dependency if possible
55 return type(middleware).__name__ == "DIScopeMiddleware"
57 def adapt(self, middleware: Any) -> StarletteMiddleware:
58 return StarletteMiddleware(
59 cast("Any", type(middleware)),
60 container=getattr(middleware, "container", None),
61 )
64class _HTTPMiddlewareAdapterBase:
65 """Handles Starlette BaseHTTPMiddleware instances."""
67 def can_adapt(self, middleware: Any) -> bool:
68 from starlette.middleware.base import BaseHTTPMiddleware
70 return isinstance(middleware, BaseHTTPMiddleware)
72 def adapt(self, middleware: Any) -> StarletteMiddleware:
73 return StarletteMiddleware(
74 cast("Any", type(middleware)),
75 container=getattr(middleware, "container", None),
76 )
79class MiddlewareAdapterRegistry:
80 """Registry for middleware adapters."""
82 def __init__(self) -> None:
83 self._adapters: list[_MiddlewareAdapterProtocol] = []
84 self._register_defaults()
86 def _register_defaults(self) -> None:
87 # Default order matters: more specific adapters first
88 self.add_adapter(_StarletteMiddlewareAdapter())
89 self.add_adapter(_DIScopeMiddlewareAdapter())
90 self.add_adapter(_HTTPMiddlewareAdapterBase())
91 self.add_adapter(_TupleMiddlewareAdapter())
93 def add_adapter(self, adapter: _MiddlewareAdapterProtocol) -> None:
94 """Add a custom middleware adapter."""
95 self._adapters.insert(
96 0,
97 adapter,
98 ) # LIFO order so custom adapters take precedence
100 def adapt(self, middleware: Any) -> StarletteMiddleware:
101 """Adapt a middleware object using registered adapters."""
102 for adapter in self._adapters:
103 if adapter.can_adapt(middleware):
104 return adapter.adapt(middleware)
106 # Final fallback: Lexigram Middleware Adapter
107 from lexigram.web.middleware.adapter import _LexigramMiddlewareAdapter
109 return StarletteMiddleware(_LexigramMiddlewareAdapter, lexigram_mw=middleware)