Concepts¶
A view or viewset here serves a kernel spec. This page is what happens between the request and the spec: which views take which specs, what the edge reads from the request, how the input is validated, and how each answer and refusal goes back as DRF's. What a spec itself declares is the kernel's documentation.
Views and the specs they take¶
| View | Spec on spec |
Methods |
|---|---|---|
ServiceCreateView |
ServiceSpec |
POST |
ServiceUpdateView |
ServiceSpec with a target |
PUT, PATCH |
ServiceDeleteView |
ServiceSpec with a target |
DELETE |
SelectorListView |
SelectorSpec(kind=SelectorKind.LIST) |
GET |
SelectorRetrieveView |
SelectorSpec(kind=SelectorKind.RETRIEVE) |
GET |
ServiceViewSet takes one spec per action in action_specs (list,
retrieve, create, update, partial_update, destroy), and
SelectorViewSet its list and retrieve; PATCH falls back to update's
spec. A custom route takes its
spec through @service_action(spec) or @selector_action(spec). See
Viewsets and custom actions.
as_view() checks the spec before the view serves anything: a spec of the
kind the view does not serve, an update or delete with no target, a
filter_set beside DjangoFilterBackend, and what the kernel's seed check
refuses of the callables' signatures, against the seeds the view registers on
pool_seeds, are each an ImproperlyConfigured at import. The specs of
@service_action and @selector_action are seed-checked by their viewset's
as_view(); on DRF's own GenericViewSet or ViewSet, with no base of this
package's, they are not, and a missing seed is the first request's
TypeError. A nested selector declaring permissions is refused by the kernel
when the ServiceSpec is built, not at as_view(). See
Registering a seed.
A spec handed to as_view(spec=...) is checked and served exactly as the same
class attribute is, and so is every other keyword the checks read
(pool_seeds, success_status, response_finalizer, filter_backends): DRF
sets each on the instance, which outranks the class. A viewset's specs are its
action_specs, so as_view(actions, action_specs=...) is checked the same way;
a viewset declares no spec, and DRF refuses that keyword with a TypeError.
What the edge reads from a request¶
The view turns the request into the one dict of arguments the kernel's
dispatch takes, and the principal into request.user:
- Route captures the spec declares as parameters (
reads, or a target selector'sreads) are arguments, coerced to their declared type and taken over anything the body or the query string sends for the same name. A capture the spec does not declare is no argument; it stays onview.kwargsfor DRF. - The query string is read by the kernel's flat reader: a blank value is absent, a typed array or a multiple-choice filter splits on a comma, and each value is coerced to its parameter's type.
- A JSON body is read as it was sent, and checked against the declared
types as sent: a JSON
"true"is a string, not a boolean. A view'slenient_json_body, or the project'sSPEC_REST_LENIENT_JSON_BODY, has its serializer alone answer one instead, as DRF does; the spec stays typed. - A form body is read as DRF reads one:
"on"and a blank checkbox for a boolean, and the prefixed spellingsv.tenantandtags[0]of a nested or list field. - A key no parameter declares is dropped, as DRF's serializer drops one.
A write's target is its spec's own: instance_selector_spec for one row,
collection_selector_spec for a set. Its reads and its filter_set come from
the route and the query string, never the body, so a body value cannot choose
the row a write changes. A miss answers 404.
Validation¶
A spec's SerializerValidator declares the serializer's writable fields as the
spec's parameters. The kernel checks the arguments against them first (a
missing required field and a mistyped one are refused together), then the
serializer validates the body it was bound to: its field validators,
validate_<field>() and validate(). The validated data reaches the service
as data.
PATCH validates only the fields sent. A spec declaring partial=True
validates PUT partially too.
Bulk writes¶
A spec declaring many=True takes a JSON list of rows, each validated by its
SerializerValidator, and the service is handed the validated rows as data,
a list. A row that fails is refused at the index it was sent at, under the
spec's many_argument (items by default), every failing row in one 400, and
the service never runs:
from django_service_specs import ServiceSpec, Unrestricted
from rest_framework import serializers
from django_spec_rest import ServiceCreateView
from django_spec_rest.service_specs.serializer_validator import SerializerValidator
from lines.models import Line
def create_lines(*, data):
return [Line.objects.create(**row) for row in data]
class LineIn(serializers.Serializer):
sku = serializers.CharField()
quantity = serializers.IntegerField()
class LinesCreateView(ServiceCreateView):
spec = ServiceSpec(
service=create_lines,
permissions=(Unrestricted(),),
validator=SerializerValidator(LineIn),
many=True,
)
POST /lines/
[{"sku": "a", "quantity": 1}, {"sku": "", "quantity": 1},
{"sku": "a", "quantity": 1}, {"sku": "", "quantity": 1}]
400 Bad Request
{"items": {"1": {"sku": ["This field may not be blank."]},
"3": {"sku": ["This field may not be blank."]}}}
A body that is no list is refused as a whole, {"items": ["Expected an
array."]}, and so is a list longer than the spec's many_max_length, else the
kernel's SERVICE_SPECS_MAX_ARRAY_LENGTH (1000 by default), with a 400 under
items. A view taking longer lists declares many_max_length on its spec.
Hooks and providers¶
Three layers feed a write beyond the client's body, the more specific winning on an overlapping key:
| Seat | View-wide | Per action (viewsets) | On the spec |
|---|---|---|---|
| Extra service keywords | get_service_kwargs() |
get_<action>_service_kwargs() |
kwargs= provider |
| Extra serializer input | get_input_data(request) |
get_<action>_input_data() |
input_data= provider |
| Input serializer context | get_serializer_context(), then get_input_serializer_context() |
get_<action>_input_serializer_context() |
SerializerValidator(serializer_context=) |
| Output serializer context | get_serializer_context(), then get_output_serializer_context() |
get_<action>_output_serializer_context() |
SerializerPresenter(serializer_context=) |
A view hook's value outranks the validated value, so a hook returning
{"owner": request.user} cannot be overwritten by a client sending owner. A
kwargs provider may not return a key a validator field declares; pass such a
value through input_data=, where the serializer validates it. A selector
view takes no keyword hook beside a kernel spec: as_view() refuses a
get_selector_kwargs override, and the spec's own kwargs provider fills its
keywords. See View hooks and providers.
Answers¶
A spec's presenter renders the result. A list view paginates with the view's
paginator and presents the page's rows; a list or retrieve spec declaring
neither a presenter nor affordances is rendered by the view's
serializer_class.
| Write | Default status |
|---|---|
| create | 201 |
| update | 200 |
| delete | 204 |
A result with nothing to present answers 204 with no body, and a body never
goes out under 204: a value to present answers 200. success_status on the
view overrides the default, with an int or a callable declaring any of
result, instance, request, view; response_finalizer may replace the
response built. On a viewset an action may declare its own
(Per-action answers). as_view() refuses a callable
requiring a name the view cannot supply, whether or not it also takes
**kwargs, which is handed the names the view has and no others.
Refusals¶
Every refusal leaves as a DRF exception, so DRF's exception handler, and a
project's EXCEPTION_HANDLER, answers it as it answers DRF's own:
| Refusal | Status | Body |
|---|---|---|
The kernel's InvalidArguments (a missing, mistyped or refused argument) |
400 | keyed by argument |
ServiceValidationError |
400 | its error tree |
A bare DispatchError |
400 | {"detail": ...} |
NotPermitted (a permission check refusing) |
403 with the check's message. A permission check the spec declares, a kernel PermissionCheck or an adapted DRF class, answering False to a caller the request's authenticators did not recognise is DRF's NotAuthenticated instead, as a DRF view answers it: 401 with the first authenticator's WWW-Authenticate, or 403 where it sends none. A service's own NotPermitted, an inner dispatch its check refused, and a check that raises NotPermitted itself rather than answering False, stay 403 with their message |
{"detail": ...} |
PrincipalUnavailable (a deactivated user) |
403 | {"detail": ...} |
ServiceNotFound, or a target selector's miss |
404 | {"detail": ...} |
ActionUnavailable |
409 | {"detail": ..., "code": ...} |
ServiceConflict |
409 | {"detail": ...} |
AdditionalInputRequired with a schema |
422 | {"detail": ..., "schema": ...} |
any other ServiceError |
422 | {"detail": ...} |
The refusal classes the kernel raises through this package's seats are DRF's
as well (DRFInvalidArguments, DRFNotPermitted, ...), so a caller of the
kernel's dispatch catches the kernel's class. See
DRF seats.
OPTIONS¶
OPTIONS answers DRF's shape (name, description, renders, parses,
actions) from the route's specs, and runs no stage of any of them: no
selector, provider, service, precondition or affordance condition.
Allownames the methods a spec serves, DRF'sHEADbeside aGET, andOPTIONS.actions[POST]andactions[PUT]are JSON Schema objects of what the caller is asked to send, the kernel'sspec_caller_input_schema: the spec's parameters less what the server fills (the keys a seed or a typed provider fills), with a key a provider may fill or decline offered and not required, none of apartialspec's fields required, and amanyspec's rows read the same way by what itsinput_datafills. The route's captures and the target lookup's parameters are left out too, since the edge never reads them from the body.- A method the caller may not send is not described: DRF's permission
classes for that method and the kernel's
authorizefor its spec are both asked, and either refusing leaves it out ofactions.
Serializer context¶
One context dict is built per request, with request, view, format and
action. The input serializer, the output serializer and the service's
call_context are handed that same dict, so a key one serializer's
validate() writes is there for the service and for the serializer rendering
the answer. See Serializer context.
Which transport is calling¶
That dict also carries the kernel's "transport" key, stamped "rest"
(REST_TRANSPORT, from the package root), so a callable shared with another
transport can tell them apart with the kernel's call_transport. Checks, a
discriminator, hooks, the validator, the service, the presenter and an OPTIONS
answer all read that one value. The dict is the view's, built by the kernel's
build_call_context.
- A serializer of this package under another transport keeps that transport's
stamp. Only a view stamps. A
SerializerValidatororSerializerPresenterdispatched by spec-mcp or spec-agents reads their value. - A context you write by hand reads as you wrote it.
build_offline_contextreturns a request and a view, not a context, so a dict built from them with no"transport"key reads as an in-process call, wherecall_transportanswersNone. - The key is the transport's to write. A view hook or a
serializer_contextcallable answering a"transport"key of its own is dropped from the dict every stage shares, silently and without an error, so every stage reads the stamp; name such a key something else. A serializer built over a dict of its own (a call with no request DRF built) keeps what its layers wrote there, since no other stage reads that dict.
What you override on this transport is DRF's and the view's: authenticators and
EXCEPTION_HANDLER, the view's attributes and its get_* hooks
(Serializer context).