Repository navigation
PoC: typed REST contract with DRF + drf-spectacular, piloted on /rest/rooms - #397
jamesmulcahy wants to merge 2 commits into
Conversation
|
Hey, this looks awesome but I'm currently looking through PR #388 and it's taking a while. I'll try and have a read through this when that's done. I'm not sure as to what is the best cause here since I believe there are some endpoints in /rest that the NSPanel relies on and would be better left alone but those are perhaps not affected by the change to DRF. The hold endpoints are also a bit of hit or miss regarding structure and maybe it's better to create a new rest/v2/ endpoint that follows a more logical layout as to how resources are created, updated, fetched and so on and then build out the test suite and React parts along side it. Regarding Python version I'm not against updating, mostly I'm for it but we probably need to also update the Django version then and I've not had time to look into a for a while.
This may be a problem. We had this issue previously where we tried using ZeroMQ for communication between Django and MQTTManager instead of the manager having to reload everything all the time but the ZeroMQ dependency we used in Django had to have the wheel built for some ARM architectures and that caused the built to timeout after 6 hours in Github. |
|
Thanks Tim, no rush at all. To be clear about where this sits: I put it up as a proof of concept to try out your DRF + drf-spectacular suggestion from #388, not as something I'm pushing to merge soon. I think it shows the approach solves the problem we were worried about. With the Python/Django: this needs neither upgrade. It runs on the current Python 3.12 image and Django 5.1.1, which is why it pins DRF 3.17.1. DRF 3.18 needs Django 5.2, but that's optional, and Django 5.2 still supports Python 3.12. So a Django upgrade can happen separately whenever you want it. rpds-py: I overstated this in the description. PyPI has Python 3.12 wheels for x86_64, i386, aarch64 and armv7. The armhf release job is the only build that would compile it from source, but that's the same situation as ZeroMQ, so it shouldn't go into the image. drf-spectacular only needs
Panels and |
Port GET/PUT/POST /rest/rooms and DELETE /rest/rooms/<id> from hand-parsed
function views to DRF APIViews with serializers, keeping the URLs and the
{"status": ...} response envelope.
- Serializers declare the wire format. RoomSerializer maps display_order to
the model's displayOrder in one place, which fixes the room_put bug where
display_order was ignored and displayOrder returned 500.
- Errors go through one exception handler: {"status": "error", "message",
"errors"?: {field: [msg]}}. Validation failures are 400 and a missing room
is 404; previously most bad input was a 500.
- Unknown request fields are logged and ignored, for compatibility between UI
and server versions. REST_REJECT_UNKNOWN_FIELDS (on in test_settings only)
rejects them with a 400, so tests whose bodies drift from the API fail.
- PUT only saves and reloads MQTTManager if a value actually changed; the UI
sends every field on each save.
- drf-spectacular generates docker/web/openapi.yaml (also served at
/rest/schema). test_openapi fails if the committed file is stale or if
spectacular emits any warning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- src/api/schema.d.ts is generated from openapi.yaml by openapi-typescript; openapi-fetch turns it into a client whose URLs, methods, request bodies and responses are type-checked. RoomsStore, EntityStore and RoomPage use it, and the hand-written room interfaces are replaced by the generated Room type, so renaming a serializer field breaks the UI build. - docker/web/generate_api.sh regenerates openapi.yaml and schema.d.ts. - New api-types CI job fails if schema.d.ts is stale with openapi.yaml (test_openapi covers openapi.yaml vs the views). - CONTRIBUTING.md: "Changing the REST API" - the generation chain, what to run, what CI checks, and conventions for DRF views and UI callers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
d14c1c4 to
7f82b97
Compare
Summary
A proof of concept of the approach @tpanajott suggested on #388: use Django REST Framework + drf-spectacular to generate an OpenAPI schema from the Django views, and generate the React UI's TypeScript types from that. It's piloted on one endpoint,
/rest/rooms, the one with thedisplay_orderbug, so we can see what it looks like before deciding whether to roll it out.It builds on the test harness from #388, which has merged.
How it fits together
After changing a serializer, run
docker/web/generate_api.shand commit both generated files.CONTRIBUTING.mddescribes the workflow.What changed
Django
GET/PUT/POST /rest/roomsandDELETE /rest/rooms/<id>are now DRFAPIViews with serializers. The URLs and the{"status": ...}envelope are unchanged, so the UI's error toasts still work.display_orderis mapped to the model'sdisplayOrderonce, inRoomSerializer. That fixes theroom_putbug from Add a Django test harness and initial REST API tests #388: sendingdisplay_orderwas ignored and sendingdisplayOrderreturned a 500.{"status": "error", "message": ..., "errors"?: {field: [msg]}}. Bad input is now a 400 and a missing room a 404; before, most of it was a 500. PUT also now enforces the 30-character name limit.REST_REJECT_UNKNOWN_FIELDS = True, which turns them into a 400 so a test whose request body has drifted from the API fails./rest/schema.React
RoomsStore,EntityStoreandRoomPageuse the generated client. The three hand-copied room interfaces are replaced by the generatedRoomtype.CI
web/tests/test_openapi.pyfails ifopenapi.yamldoesn't match the views, or if drf-spectacular warns about a view it can't fully describe, which would otherwise end up as a loose type.api-typesjob fails ifschema.d.tsdoesn't matchopenapi.yaml.What it catches
register("displayOrder")in the room formtscerrorroom.displayOrderfrom a responsetscerror: "did you meandisplay_order?"/rest/room) or a PUT body missingidtscerrorapi.PUT(..., { body: { id: 1, displayOrder: 7 } })bodygenerically, which turns off TypeScript's excess-property check for inline literals. The server logs a warning. Typed bodies, such as the form data fromuseForm<RoomUpdateRequest>(), are checked.Things to note
jsonschema, which pulls inrpds-py, a Rust extension. PyPI has Python 3.12 wheels for x86_64, i386, aarch64 and armv7; only the armhf release build would compile it from source. Onlymanage.py spectacular(schema generation) needs it, not the running app, so it can be kept out of the image. See the linked comment for options.npx(npm run generate:api-types), not as a devDependency. It declares atypescript ^5peer and the UI is on TypeScript 6.package-lock.jsongains only theopenapi-fetchentries; every npm version I tried rewrote the rest of the file.bun.lockwas already out of sync withpackage.json(e.g. zod), so I left it alone.Not in this PR
/rest/*endpoints. If we go ahead, they could be ported a few at a time; the light entity save is the obvious next one.Test plan
docker/web/run_tests.sh: 223 tests pass. Thedisplay_order@expectedFailurefrom Add a Django test harness and initial REST API tests #388 is gone.npx tsc -bpasses; the vite build succeeds.api-typescheck passes on this branch and fails after editingopenapi.yamlwithout regenerating.generate_api.shreproduces the committed files exactly.api-typesjob green in Actions.🤖 Generated with Claude Code