Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/en/dev/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ cut.
action handling into focused internal modules. Command syntax and behavior are
unchanged. ([#167](https://github.com/DevilsAutumn/quater/issues/167))

- `Quater.add_route()` now validates the `inject` mapping at registration time,
matching `RouteGroup` behavior. Invalid injected parameter names (e.g.
`bad-name`) raise `ConfigurationError` and non-`Resource` values raise
`TypeError` when the route is added, instead of surfacing later during route
compilation. Deeper handler-plan checks (unused injected values, path/param
marker conflicts) remain compile-time.
([#140](https://github.com/DevilsAutumn/quater/issues/140))

### Fixed

- Fixed resource provider planning so valid provider parameters still resolve
Expand Down
14 changes: 14 additions & 0 deletions docs/en/dev/resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,12 @@ decorator says where its value comes from. This is the only form that a
[`RouteGroup`](#groups) can share across several routes, so prefer it when a
whole feature needs the same resource.

The `inject` map is validated when the route is registered (via
`app.add_route()`, `@app.get(...)`, `RouteGroup(...)`, or `group.add_route()`).
Keys must be valid Python identifiers and values must be `Resource` instances,
otherwise registration raises `ConfigurationError` or `TypeError` immediately
instead of failing later during route compilation.

### In the type annotation (`Annotated[T, resource]`)

Put the `Resource` in the parameter's annotation. The parameter type is still
Expand Down Expand Up @@ -413,6 +419,14 @@ The generated MCP and CLI schemas include `order_id`, not `session`.
`Resource provider 'db_session' yielded more than once`
: Use one `yield`, then cleanup after it.

`Invalid injected parameter name: 'bad-name'`
: An `inject={...}` key is not a valid Python identifier. Rename the key.
Raised at route registration.

`inject values must be Resource instances`
: An `inject={...}` value is not a `Resource`. Wrap the provider with
`Resource(...)`. Raised at route registration.

`Duplicate injected parameter: session`
: A group and a route both define `session` with different `Resource` objects.
Use the same object or rename one parameter.
Expand Down
2 changes: 1 addition & 1 deletion src/quater/_route_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def build_route_definition(
cli=cli,
route_name=route_name,
),
inject=dict(inject or {}),
inject=normalize_inject(inject),
metadata=dict(metadata or {}),
middleware=MiddlewareStack.from_parts(
before=before,
Expand Down
2 changes: 1 addition & 1 deletion src/quater/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def add_route(
cli=cli,
needs_approval=needs_approval,
public=public,
inject=normalize_inject(inject),
inject=inject,
metadata=metadata,
before=before,
after=after,
Expand Down
33 changes: 32 additions & 1 deletion tests/unit/test_app_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import pytest

from quater import AuthConfig, Quater, Request
from quater import AuthConfig, Quater, Request, Resource
from quater.config import AppConfig
from quater.exceptions import ConfigurationError, RouteConflictError
from quater.response import Response
Expand Down Expand Up @@ -486,3 +486,34 @@ async def handler() -> dict[str, bool]:
assert route.cli is False
assert route.needs_approval is False
assert route.public == ()


def test_app_add_route_rejects_invalid_inject_parameter_names() -> None:
async def provider() -> str:
return "bad"

async def handler(value: str) -> dict[str, str]:
return {"value": value}

app = Quater()
with pytest.raises(ConfigurationError, match="Invalid injected parameter name"):
app.add_route(
"GET",
"/",
handler,
inject={"bad-name": Resource(provider)},
)


def test_app_add_route_rejects_non_resource_inject_values() -> None:
async def handler(value: str) -> dict[str, str]:
return {"value": value}

app = Quater()
with pytest.raises(TypeError, match="inject values must be Resource instances"):
app.add_route(
"GET",
"/ok",
handler,
inject=cast(dict[str, Resource[str]], {"value": object()}),
)