View hooks and providers¶
A service is handed the keywords it declares: the kernel's seeds (user,
data, instance, request, view, ...), the spec's arguments, and the
values the server adds. The server adds them in two places: on the spec, which
every transport serving it runs, and on the view, which only HTTP runs.
On the spec: providers¶
A kwargs provider returns extra keywords for the service; an input_data
provider returns values merged into the input before the serializer validates
it. Either declares the seeds it reads: user is the principal on every
transport, and over HTTP request and view are the call's.
from django_service_specs import ServiceSpec
def tenant_kwargs(*, user):
return {"tenant": user.tenant}
CREATE_SPEC = ServiceSpec(
service=create_note, # def create_note(*, data, tenant): ...
validator=SerializerValidator(NoteInput),
kwargs=tenant_kwargs,
permissions=OPEN,
)
A kwargs provider may not return a key a validator field declares: its
value would reach the service unvalidated. A typed provider (one whose return
annotation is a TypedDict) naming such a key is refused when the view is
built, and an untyped one on the request whose answer names it. Return the
value from input_data= instead, where the serializer validates it. A key
named like a field's source stays the provider's.
A provider is server code, and its values reach the callable as returned: a
provider's "abc" for an x: int parameter is not coerced.
Registering a seed¶
A fact every callable may need and no argument carries (a tenant, a feature
flag, a "sending is paused" switch) is a seed: register a resolver for it on
a PoolSeeds and set that registry on the view's pool_seeds, as a class
attribute or as as_view(pool_seeds=...). Any callable of the spec declaring
the seed's name is handed its value: the service and its selectors, the
providers, a permission check, a precondition and an affordance condition.
from django_service_specs import DEFAULT_POOL_SEEDS, Affordance, ServiceSpec
from django_spec_rest import ServiceViewSet
SEEDS = DEFAULT_POOL_SEEDS.extend(frozen=board_is_frozen)
RENAME_SPEC = ServiceSpec(
service=rename_task,
instance_selector_spec=TASK_BY_PK,
affordances=[
Affordance(
code="board_frozen", reason="The board is frozen.", when=lambda *, frozen: not frozen
),
],
permissions=OPEN,
)
class TaskViewSet(ServiceViewSet):
pool_seeds = SEEDS
action_specs = {"partial_update": RENAME_SPEC}
Build the registry once, at import: the view checks its spec against it at
as_view() and hands that one object to every call. A view that does not
register a seed one of its spec's callables requires refuses the spec at
as_view(), naming the seed, rather than failing every request. A
@service_action or @selector_action spec is checked by its viewset's
as_view(), against the viewset's pool_seeds, as an action_specs entry
is, so a seed the viewset registers serves and one it does not is refused
there. A @service_action on a viewset overriding get_service_kwargs or
get_<action>_service_kwargs is not asked, since the hook may fill what the
check would refuse. Each seed a
permission check names is resolved once per call, its value handed from the
check to the run. The OpenAPI request body and the OPTIONS answer read the
same registry: a read a registered seed fills is no key a caller is asked to
send, and OPTIONS asks a permission check naming a registered seed.
The decorated action's check is made by this package's viewsets
(ServiceViewSet, SelectorViewSet or a class built on their mixins or
ActionSerializerResolver). On DRF's own GenericViewSet or ViewSet
nothing asks it, so a seed nobody registers is the action's first request's
TypeError.
On the view: hooks¶
A mutation view or viewset has three hook chains, view-wide then per action, the more specific winning on a key both return, and the spec's provider over both:
| Hook | Feeds |
|---|---|
get_service_kwargs(), get_<action>_service_kwargs() |
the service's keywords |
get_input_data(request), get_<action>_input_data() |
the input, before validation |
get_input_serializer_context(), get_<action>_input_serializer_context() |
the input serializer's context |
from django_spec_rest import ServiceViewSet
class NoteViewSet(ServiceViewSet):
action_specs = {"create": CREATE_SPEC}
def get_service_kwargs(self):
return {"owner": self.request.user}
A hook's value outranks the client's. Where the serializer declares a
writable owner, the hook's value is what reaches the service, so a client
sending owner cannot replace the hook's. A key a validate() derives, or a
source="*" nested serializer flattens, reaches the service's keyword as the
hook's value, while data keeps what the serializer validated.
A selector view takes no keyword hook beside a kernel spec: as_view()
refuses a get_selector_kwargs or get_<action>_selector_kwargs override,
which would never reach the spec. Return those keys from the spec's kwargs
provider.
Prefer the spec's providers for anything the operation needs wherever it is
served: a hook runs over HTTP only, and the same spec dispatched through the
kernel's dispatch, or served on another transport, never asks it.