From 558c7c6d6c68b53de5beb17bb94e9a065959c529 Mon Sep 17 00:00:00 2001 From: Ryth Azhur Date: Tue, 31 Mar 2026 01:22:32 -0400 Subject: [PATCH] Update documentation to reflect Djarea's RPC architecture - Root README: full quick start, architecture diagram, feature table, code generation workflow, error handling, forms, channels, testing - Django README: setup, auth variations, contexts, forms, channels - React README: clarify that generated hooks are the API (not library primitives), DjangoContext is the only provider needed, sub-exports are internals Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 304 ++++++++++++++++++++++++++++++++++++++++++++--- django/README.md | 106 ++++++++++++++--- react/README.md | 115 +++++++++++++++--- 3 files changed, 476 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 36d212e..60e3e5d 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,296 @@ -# djarea +# Djarea -Django + React server functions framework. +Django + React server functions framework. RPC, not REST. -| Package | Path | Registry | -|---------|------|----------| -| `djarea` (Python) | `django/` | PyPI / git | -| `djarea` (TypeScript) | `react/` | npm / git | +You define Python functions. Djarea generates typed React hooks. No API routes, no serializers, no endpoint boilerplate. -## Installation - -```bash -# Python -uv add "djarea[channels,allauth] @ git+https://git.impactsoundworks.com/isw/djarea.git#subdirectory=django" - -# TypeScript -npm install djarea@git+https://git.impactsoundworks.com/isw/djarea.git#workspace=react +```python +# Django +@client(context='global') +def current_user(request) -> UserOutput: + return UserOutput(email=request.user.email) ``` +```tsx +// React (generated) +const user = useCurrentUser() // typed, SSR-hydrated, auto-refreshed +``` + +## Packages + +| Package | Path | Install | +|---------|------|---------| +| `djarea` (Python) | `django/` | `uv add "djarea[channels] @ git+..."` | +| `@rythazhur/djarea` (TypeScript) | `react/` | `npm install @rythazhur/djarea@git+...` | + ## Quick Start -See [django/README.md](django/README.md) and [react/README.md](react/README.md). +### 1. Django setup + +```python +# settings.py +INSTALLED_APPS = [ + "djarea", + "myapp", +] + +# urls.py +from django.urls import include, path +urlpatterns = [ + path("api/djarea/", include("djarea.urls")), +] + +# asgi.py (for WebSocket support) +from djarea import wrap_asgi +from django.core.asgi import get_asgi_application +application = wrap_asgi(get_asgi_application()) +``` + +### 2. Define server functions + +```python +# myapp/djarea_clients.py +from django.http import HttpRequest +from djarea.client import client +from djarea.setup.registry import register +from pydantic import BaseModel + +class EchoOutput(BaseModel): + message: str + +@client +def echo(request: HttpRequest, text: str) -> EchoOutput: + return EchoOutput(message=text) + +register(echo, "echo") +``` + +### 3. Register in apps.py + +```python +class MyAppConfig(AppConfig): + name = "myapp" + + def ready(self): + import myapp.djarea_clients # noqa: F401 +``` + +### 4. Generate TypeScript + +```bash +# django.config.mjs +export default { + source: { + django: { + managePath: '../backend/manage.py', + command: ['uv', 'run', 'python'], + }, + }, + output: 'src/api/generated.ts', +} +``` + +```bash +npx djarea-generate +``` + +This produces typed hooks, a typed provider, form hooks with Zod validation, and channel hooks. + +### 5. Use in React + +```tsx +// layout.tsx +import { DjangoContext } from '@/api' + +export default function Layout({ children }) { + return {children} +} +``` + +```tsx +// page.tsx +import { useEcho, useCurrentUser, DjangoError } from '@/api' + +function MyComponent() { + const user = useCurrentUser() + const echo = useEcho() + + const handleClick = async () => { + try { + const result = await echo({ text: 'hello' }) + console.log(result.message) // typed + } catch (e) { + if (e instanceof DjangoError) { + console.log(e.code) // NOT_FOUND, VALIDATION_ERROR, etc. + } + } + } +} +``` + +## Features + +| Backend | Frontend (generated) | Transport | +|---------|---------------------|-----------| +| `@client` | `useXxx()` | HTTP | +| `@client(context='global')` | `useXxx()` + SSR hydration | HTTP | +| `@client(context='local')` | `useXxx()` with params | HTTP | +| `@client(websocket=True)` | `useXxx()` | WebSocket RPC | +| `@client(auth=True\|'staff'\|callable)` | Auth errors as `DjangoError` | HTTP | +| `DjareaFormMixin` | `useXxxForm()` + Zod validation | HTTP | +| `ReactChannel` | `useXxxChannel()` | WebSocket | +| `@compose(...)` | Combined providers | varies | + +## Architecture + +``` +React app + └─ ← generated provider (includes ChannelProvider) + ├─ useCurrentUser() ← generated context hook (SSR-hydrated) + ├─ useEcho() ← generated function hook + ├─ useContactForm() ← generated form hook (Zod + server validation) + └─ useChatChannel() ← generated channel hook (WebSocket) + │ + ├─ HTTP: POST /api/djarea/call/ { fn: "echo", args: { text: "hi" } } + └─ WS: { action: "rpc", fn: "echo", args: { text: "hi" } } + │ + Django executor + ├─ Pydantic input validation + ├─ Auth check (session, JWT, or custom) + ├─ Function execution + └─ Pydantic output serialization +``` + +The generated `DjangoContext` is the **only provider** needed. It wraps `DjareaProvider` + `ChannelProvider` and handles session init, CSRF, context auto-fetching, and WebSocket connection. + +## Code Generation + +`npx djarea-generate` reads Django schemas (no running server needed) and produces: + +| File | Contents | +|------|----------| +| `generated.djarea.ts` | Pydantic model types (via openapi-typescript) | +| `generated.django.tsx` | `DjangoContext` provider + all typed hooks | +| `generated.django.server.ts` | SSR hydration helper (`getDjangoHydration`) | +| `generated.forms.ts` | Form hooks with Zod schemas (`useContactForm`, etc.) | +| `generated.channels.ts` | Channel message types | +| `generated.channels.hooks.tsx` | Channel hooks (`useChatChannel`, etc.) | +| `index.ts` | Consolidated re-exports | + +## Error Handling + +All errors from server functions are thrown as `DjangoError`: + +```tsx +try { + await echo({ text: 'hello' }) +} catch (e) { + if (e instanceof DjangoError) { + e.code // 'NOT_FOUND' | 'VALIDATION_ERROR' | 'UNAUTHORIZED' | 'FORBIDDEN' | ... + e.message // Human-readable message + e.details // Field-level validation errors, etc. + e.isAuthError() + e.isValidationError() + e.getFieldErrors('email') + } +} +``` + +Error codes: `NOT_FOUND`, `VALIDATION_ERROR`, `UNAUTHORIZED`, `FORBIDDEN`, `BAD_REQUEST`, `INTERNAL_ERROR`, `NOT_IMPLEMENTED`. + +## Forms + +Django forms get typed React hooks with client-side Zod validation: + +```python +# Django +class ContactForm(DjareaFormMixin, forms.Form): + djarea = DjareaFormMeta( + name="contact", + title="Contact Us", + submit_label="Send", + live_validation=True, + ) + name = forms.CharField(max_length=100) + email = forms.EmailField() + message = forms.CharField(widget=forms.Textarea) + + def on_submit_success(self, request): + send_email(self.cleaned_data) + return {"sent": True} +``` + +```tsx +// React (generated) +const form = useContactForm() + +form.schema // { fields: { name: {...}, email: {...} }, title, submit_label } +form.data // { name: '', email: '', message: '' } +form.set('email', v) // typed setter +form.errors // field-level errors (Zod + server) +form.submit() // → { success: true, data: { sent: true } } +``` + +## Channels + +WebSocket channels with typed messages: + +```python +# Django +class ChatChannel(ReactChannel): + class Params(BaseModel): + room: str + class ReactMessage(BaseModel): + text: str + class DjangoMessage(BaseModel): + text: str + user: str + + def authorize(self, params): + return self.user.is_authenticated + + def group(self, params): + return f"chat_{params.room}" + + def receive(self, params, msg): + return self.DjangoMessage(text=msg.text, user=self.user.email) +``` + +```tsx +// React (generated) +const chat = useChatChannel({ room: 'general' }) + +chat.status // 'connecting' | 'connected' | 'disconnected' +chat.messages // ChatDjangoMessage[] +chat.send({ text: 'hello' }) +``` + +## Testing + +```bash +# Django unit tests +cd django && uv sync --extra dev --extra channels && uv run pytest + +# React unit tests +cd react && npm test + +# E2E integration tests (real browser, real backend) +docker compose -f docker-compose.test.yml up -d +cd e2e/harness && npm install && npx djarea-generate && npx vite --port 5174 & +npx playwright test + +# All at once +make test-all +``` + +## Project Structure + +``` +djarea/ + django/ Python package (djarea) + react/ TypeScript package (@rythazhur/djarea) + example/ Integration test backend (Docker) + desktop/ PyWebView desktop test app + e2e/ Playwright E2E tests + React harness + Makefile Test orchestration +``` diff --git a/django/README.md b/django/README.md index 90d1367..0e1c7ff 100644 --- a/django/README.md +++ b/django/README.md @@ -1,29 +1,105 @@ -# djarea +# djarea (Python) -Django + React server functions framework. See the [monorepo root](../README.md) for full documentation. +Django server functions framework. See the [monorepo root](../README.md) for full documentation. -## Installation +## Install ```bash -# From git uv add "djarea[channels,allauth] @ git+https://git.impactsoundworks.com/isw/djarea.git#subdirectory=django" - -# Local editable -uv add -e "../../web/djarea/django[channels,allauth]" ``` -## Quick Start +## Setup + +```python +# settings.py +INSTALLED_APPS = ["djarea", ...] + +# urls.py +path("api/djarea/", include("djarea.urls")) + +# asgi.py (optional, for WebSocket) +from djarea import wrap_asgi +application = wrap_asgi(get_asgi_application()) +``` + +## Define Functions ```python from djarea.client import client +from djarea.setup.registry import register from pydantic import BaseModel -class UserOutput(BaseModel): - email: str +class Output(BaseModel): + message: str -@client(context='global') -def current_user(request) -> UserOutput | None: - if not request.user.is_authenticated: - return None - return UserOutput(email=request.user.email) +@client +def echo(request, text: str) -> Output: + return Output(message=text) + +register(echo, "echo") +``` + +Register in `apps.py`: + +```python +def ready(self): + import myapp.djarea_clients +``` + +## Auth + +```python +@client(auth=True) # requires authentication +@client(auth='staff') # requires is_staff +@client(auth='superuser') # requires is_superuser +@client(auth=my_callable) # custom check +``` + +## Contexts + +```python +@client(context='global') # fetched once, SSR-hydrated, becomes useCurrentUser() +@client(context='local') # fetched with params, becomes +``` + +## Forms + +```python +from djarea.forms import DjareaFormMixin, DjareaFormMeta + +class ContactForm(DjareaFormMixin, forms.Form): + djarea = DjareaFormMeta(name="contact", title="Contact Us") + name = forms.CharField() + email = forms.EmailField() + + def on_submit_success(self, request): + return {"sent": True} +``` + +Auto-registers `contact.schema`, `contact.validate`, `contact.submit`. Generates `useContactForm()` with Zod validation. + +## Channels + +```python +from djarea.channels import ReactChannel + +class ChatChannel(ReactChannel): + class Params(BaseModel): + room: str + class DjangoMessage(BaseModel): + text: str + + def authorize(self, params): + return self.user.is_authenticated + def group(self, params): + return f"chat_{params.room}" +``` + +Generates `useChatChannel({ room })`. + +## Running Tests + +```bash +uv sync --extra dev --extra channels +uv run pytest ``` diff --git a/react/README.md b/react/README.md index 9f13f39..ae0b38c 100644 --- a/react/README.md +++ b/react/README.md @@ -1,26 +1,103 @@ -# djarea (TypeScript) +# @rythazhur/djarea (TypeScript) -TypeScript client library for the Djarea framework. See the [monorepo root](../README.md) for full documentation. +React client for the Djarea framework. See the [monorepo root](../README.md) for full documentation. -## Installation +## Install ```bash -# From git -npm install djarea@git+https://git.impactsoundworks.com/isw/djarea.git#workspace=react - -# Local development -npm install djarea@file:../../web/djarea/react +npm install @rythazhur/djarea@git+https://git.impactsoundworks.com/isw/djarea.git#workspace=react ``` -## Exports +## Usage -| Import | Purpose | -|--------|---------| -| `djarea` | Core: DjareaProvider, hooks, forms, errors | -| `djarea/client` | HTTP clients, SSR helpers, `ensureDjangoSession()` | -| `djarea/client/react` | React-specific client hooks | -| `djarea/client/nextjs` | Next.js integration | -| `djarea/channels` | WebSocket channels | -| `djarea/jwt` | JWT token management | -| `djarea/allauth` | Allauth UI components | -| `djarea/allauth/nextjs` | Next.js allauth context | +You don't use this package directly. You use the **generated hooks**. + +### 1. Configure + +```js +// django.config.mjs +export default { + source: { + django: { + managePath: '../backend/manage.py', + command: ['uv', 'run', 'python'], + }, + }, + output: 'src/api/generated.ts', +} +``` + +### 2. Generate + +```bash +npx djarea-generate # once +npx djarea-generate --watch # dev mode +``` + +### 3. Wrap your app + +```tsx +import { DjangoContext } from '@/api' + + + + +``` + +`DjangoContext` is the only provider you need. It handles HTTP, WebSocket, CSRF, session init, context auto-fetching, and channel connections. + +### 4. Use generated hooks + +```tsx +import { useCurrentUser, useEcho, useContactForm, useChatChannel } from '@/api' + +// Context (SSR-hydrated, auto-refreshed) +const user = useCurrentUser() + +// Server function +const echo = useEcho() +const result = await echo({ text: 'hello' }) + +// Form (Zod + server validation) +const form = useContactForm() +form.set('email', 'test@example.com') +await form.submit() + +// Channel (WebSocket) +const chat = useChatChannel({ room: 'general' }) +chat.send({ text: 'hello' }) +chat.messages // typed, reactive +``` + +## Generated Files + +| File | Contents | +|------|----------| +| `generated.django.tsx` | `DjangoContext` + typed hooks | +| `generated.djarea.ts` | Pydantic types | +| `generated.forms.ts` | Form hooks with Zod | +| `generated.channels.hooks.tsx` | Channel hooks | +| `index.ts` | Re-exports everything | + +## Sub-exports + +| Import | When to use | +|--------|------------| +| `@rythazhur/djarea` | Core: DjareaProvider, hooks, forms, errors | +| `@rythazhur/djarea/channels` | WebSocket channels | +| `@rythazhur/djarea/jwt` | JWT token management | +| `@rythazhur/djarea/client` | HTTP clients (CSR/SSR) | +| `@rythazhur/djarea/allauth` | Allauth UI components | + +These are **library internals** used by the generated code. You should import from `@/api` (your generated index), not from the library directly. + +## Running Tests + +```bash +# Unit tests (Vitest, jsdom) +npm test + +# E2E tests (Playwright, real browser) +# Requires Docker backend running +npx playwright test +```