Routers & injection¶
Routes opt into by-type injection by living on a
GazeboRouter.LinkedRouteradditionally builds hierarchical landing pages from router nesting.
Bare-type injection¶
On a GazeboRouter, a handler declares its dependencies as ordinary typed
parameters — no Depends. At decoration the router rewrites the signature: any
parameter whose type carries a __provide__ recipe is resolved from the
per-request DI scope, while ordinary query/path/body params are left untouched. So
injection reads as plain function arguments:
from dataclasses import dataclass
from gazebo.ext.fastapi import GazeboRouter
@dataclass
class Catalog:
name: str = 'default'
@classmethod
def __provide__(cls) -> Catalog:
return cls()
router = GazeboRouter()
@router.get('/things')
async def list_things(catalog: Catalog, limit: int = 10) -> dict:
# `catalog` is injected by type; `limit` stays an ordinary query parameter.
return {'catalog': catalog.name, 'limit': limit}
External types: the Inject marker¶
A type without __provide__ — bound by a
standalone recipe — has
nothing for the router to detect, so mark it Annotated[T, Inject] to opt it into
injection explicitly:
from gazebo.ext.fastapi import Inject
class Session: # external type, no __provide__
def __init__(self, catalog: Catalog) -> None:
self.catalog = catalog
def provide_session(catalog: Catalog) -> Session:
return Session(catalog)
@router.get('/items')
async def list_items(session: Annotated[Session, Inject]) -> dict:
return {'via': session.catalog.name}
The loud-failure guarantee¶
Put an injectable-typed parameter on a plain APIRouter and FastAPI would
silently treat it as a request body — a quiet, confusing bug. gazebo guards
against it: at startup the app validates every route and fails loudly, naming
the offending route, if an injectable parameter wasn't rewritten. This is the
safety net behind the composition rules — mistakes surface
at boot, not in production.
A related sharp edge lives one level down, in Python's annotations. gazebo decides
what to inject by resolving each parameter's annotation — and an annotation referring
to a name importable only under if TYPE_CHECKING: can't be resolved at runtime. To
keep one such parameter from poisoning the others, gazebo resolves annotations
per-parameter and leniently (the way FastAPI itself does), so an injectable
parameter still wires even when a sibling annotation is unresolvable. The unresolvable
parameter is left for FastAPI to interpret, and gazebo warns, naming that
parameter — heed it by importing the annotated type at runtime rather than only
under TYPE_CHECKING (FastAPI can't type it either otherwise).
Hierarchical landing pages: LinkedRouter¶
A LinkedRouter mounts a landing endpoint at its own root (its
title/description plus self and root links). Include one LinkedRouter into
another and — if the child declares a rel — a link to the child's landing page
is added to the parent automatically. So the landing hierarchy falls out of how
you nest routers, with no hand-maintained link list. Link.to_route(name,
rel=...) builds an ad-hoc deferred link to any route when you need one outside
this scheme.
from gazebo.ext.fastapi import LinkedRouter
from gazebo.rels import Rel
root = LinkedRouter(title='API', landing_name='landing')
collections = LinkedRouter(
prefix='/collections',
rel=Rel.DATA,
title='Collections',
landing_name='collections',
)
root.include_router(collections) # adds a link to the child's landing page
The service root: RootRouter¶
The landing page at the top of the tree is special: it describes the whole
service, so OGC puts the API-definition links and the conformance declaration there.
RootRouter is the LinkedRouter for that spot — it keeps the hierarchical wiring
and adds the service-level concerns that only make sense at the root, all derived
from the running app rather than hand-maintained:
service-desc/service-doclinks to the app's OpenAPI document and its docs UI (each omitted when the app has that URL disabled, so the links never dangle).- Title/description fallback to the app's, so the service name lives in one place
— on the app — and an explicit router
titlestill wins when you set one. - An auto-mounted
/conformancewhose baseline (core/landing-page/json, plusoas30when the app exposes OpenAPI) is read from the live app and merged with the feature classes you contribute viaconformance=. The declaration tracks what's actually wired instead of drifting from it.
from gazebo.ext.fastapi import RootRouter
service = RootRouter(
landing_name='landing',
# Contribute feature-level conformance classes; the baseline
# (core/landing-page/json, plus oas30 when OpenAPI is on) is derived from the
# running app — so the declaration can't drift from what's actually mounted.
conformance=['http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core'],
)
Contribute feature-level classes as a list or a Conformance,
e.g. conformance=[*filter_conformance_classes()] for CQL2.
Reference¶
See gazebo.ext.fastapi
(GazeboRouter, LinkedRouter, RootRouter, Inject).