Source code for genro_asgi.server.dispatcher

# Copyright 2025 Softwell S.r.l.
# Licensed under the Apache License, Version 2.0

"""Dispatcher - Routes ASGI requests to handlers via genro-routes.

The Dispatcher is the innermost layer of the middleware chain. It:
1. Creates Request from ASGI scope via RequestRegistry
2. Resolves the handler via router.node() with auth filtering
3. Calls the handler with query parameters
4. Sets the result on Response via set_result()
5. Sends the ASGI response

Request flow:
    scope → RequestRegistry.create() → Request
         → router.node(path, auth_tags, env_capabilities, errors)
         → handler(**query)
         → response.set_result(result, metadata)
         → response(scope, receive, send)

Error mapping (ROUTER_ERRORS):
    Router errors are mapped to HTTP exceptions:
    - not_found → HTTPNotFound (404)
    - not_authorized → HTTPForbidden (403)
    - not_authenticated → HTTPUnauthorized (401)
    - not_available → HTTPServiceUnavailable (503)
    - validation_error → HTTPBadRequest (400)
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from genro_routes import is_result_wrapper
from genro_toolbox.smartasync import smartasync

from ..exceptions import (
    HTTPBadRequest,
    HTTPForbidden,
    HTTPNotFound,
    HTTPServiceUnavailable,
    HTTPUnauthorized,
)
from ..request import set_current_request

if TYPE_CHECKING:
    from .server import AsgiServer
    from ..types import Receive, Scope, Send

# Error mapping for router.node()
ROUTER_ERRORS: dict[str, type[Exception]] = {
    "not_found": HTTPNotFound,
    "not_authorized": HTTPForbidden,
    "not_authenticated": HTTPUnauthorized,
    "not_available": HTTPServiceUnavailable,
    "validation_error": HTTPBadRequest,
}


[docs] class Dispatcher: """Routes ASGI requests to handlers via genro_routes Router.""" __slots__ = ("server",) def __init__(self, server: AsgiServer) -> None: self.server = server @property def router(self) -> Any: """Proxy to server.router.""" return self.server.router @property def request_registry(self) -> Any: """Proxy to server.request_registry.""" return self.server.request_registry async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """ASGI interface - dispatch request to handler via router.""" request = await self.request_registry.create(scope, receive, send) set_current_request(request) try: node = self.router.node( request.path, auth_tags=request.auth_tags, env_capabilities=request.env_capabilities, errors=ROUTER_ERRORS, ) result = await smartasync(node)(**dict(request.query)) if is_result_wrapper(result): metadata = {**node.metadata, **result.metadata} request.response.set_result(result.value, metadata) else: request.response.set_result(result, node.metadata) await request.response(scope, receive, send) finally: set_current_request(None) self.request_registry.unregister()
if __name__ == "__main__": pass