Files
mizan/backends/mizan-fastapi
Ryth Azhur 45bde51166 Mizan-Rust backend adapter: server-side substrate + three-way parity
Adds first-class Rust-backed Mizan to sit alongside mizan-django and
mizan-fastapi. A Rust dev writes:

    #[derive(Mizan, Serialize, Deserialize)]
    pub struct ProfileOutput { pub user_id: i64, pub name: String }

    #[mizan::context("user")]
    pub struct UserCtx;

    #[mizan::client(context = UserCtx)]
    pub async fn user_profile(_req: &RequestHandle<'_>, user_id: i64) -> ProfileOutput { ... }

…and gets byte-identical KDL to the Python emitters, served over the
same wire protocol the React / Rust / Vue / Svelte kernels speak.

New crates:
- cores/mizan-rust/         (Cargo: mizan-core)     — IR types, KDL emitter, traits, registry,
                                                       runtime (compute_invalidation / compute_merges
                                                       ported from mizan-fastapi), graph_check with
                                                       structural type-matching
- cores/mizan-rust-macros/  (Cargo: mizan-macros)   — #[derive(Mizan)], #[mizan::context],
                                                       #[mizan::client] proc macros
- backends/mizan-rust-axum/ (Cargo: mizan-axum)     — axum HTTP adapter: /session/, /call/, /ctx/:name/
- tests/afi/rust_app/                                — AFI fixture port + server / export-ir binaries

Substrate-shape moves required by cross-language equivalence:
- IR canonicalization: functions / contexts / context-members / shared-by
  now sort alphabetically in both Python and Rust emitters. The IR is a
  contract; linkme doesn't preserve declaration order, so canonical sort
  is the only stable mapping. afi_ir.kdl + per-target baselines regenerated.
- MizanType::TYPE_NAME is a const (with a default type_name() reader) so
  it's usable in linkme TypeEntry static initializers.
- Tree-shaken type registry: #[derive(Mizan)] only emits the trait impl;
  the #[mizan::client] macro registers canonical-named entries from
  fn signatures, including Vec<T> element types for ref resolution.
- Merge resolution is structural (NamedType shape comparison) rather than
  by name — matches the Python types_match_for_merge semantics.

Three-way forcing functions:
- tests/afi/test_codegen_parity.py — Django ≡ FastAPI ≡ Rust on KDL bytes (3 pass)
- tests/rust/run_wire_parity.py    — 12/12 probes against FastAPI + Rust (EXIT=0)

Incidental fixes surfaced by the new tests:
- Stale `from .registry import validate_registry` import removed from
  mizan-django/setup/discovery.py (referenced a function that no longer
  exists; was masking codegen-parity).
- BASE_DIR added to tests/afi/django_app/project/settings.py.
- /session/ endpoint added to mizan-fastapi for protocol-shaped readiness
  probe parity (wire-parity harness now polls /api/mizan/session/ on both
  backends rather than FastAPI's /openapi.json).
- Root .gitignore picks up Rust target/ across the tree so new crates
  don't need per-crate gitignore.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:31:26 -04:00
..

mizan-fastapi

FastAPI backend adapter for the Mizan protocol. One decorator on a server function. Typed React client generated. Invalidation automatic.

Scope

mizan-fastapi targets the AFI-common subset — RPC dispatch, context bundling, JSON-body invalidation, and auth gating. Forms, Channels, Shapes, and SSR are out of scope for the FastAPI adapter — FastAPI projects use native equivalents (Pydantic, native WebSockets, ORM-of-choice, FastAPI's own SSR ecosystem).

Install

uv add mizan-fastapi

Setup

# main.py
from fastapi import FastAPI
from fastapi.exceptions import RequestValidationError

from mizan_fastapi import (
    MizanError,
    mizan_exception_handler,
    mizan_validation_handler,
    router as mizan_router,
)


app = FastAPI()
app.include_router(mizan_router, prefix="/api/mizan")
app.add_exception_handler(MizanError, mizan_exception_handler)
app.add_exception_handler(RequestValidationError, mizan_validation_handler)

The exception handlers render every error path through the Mizan envelope ({"error": {"code", "message", "details"}}) so the kernel's MizanError parses status + code on the frontend regardless of which failure happened.

Define server functions

from mizan_core.client.function import client
from mizan_core.registry import register
from pydantic import BaseModel


class EchoOutput(BaseModel):
    message: str


@client
def echo(request, text: str) -> EchoOutput:
    return EchoOutput(message=text)


register(echo, "echo")

mizan-fastapi has no auto-discovery (FastAPI doesn't have an app registry to walk). Register every @client-decorated function explicitly. A typical project keeps registrations in main.py (alongside the FastAPI app) or in a dedicated clients.py imported during startup.

@client parameters

@client                              # plain RPC function
@client(context="global")            # singleton context — fetched once, SSR-hydrated
@client(context="user")              # named context — fetched per provider mount
@client(affects="user")              # mutation — invalidates the user context
@client(affects=user_profile)        # mutation — invalidates a specific function
@client(auth=True)                   # requires authentication
@client(auth="staff")                # requires is_staff
@client(auth="superuser")            # requires is_superuser
@client(auth=lambda req: ...)        # custom predicate
@client(rev=2)                       # cache revision (busts on bump)

websocket=True, Forms, and Channels parameters are accepted by the decorator (they're a mizan-core primitive) but ignored by mizan-fastapi — those features only have effect when paired with mizan-django.

Auth integration

The executor expects request.state.user to be populated by your FastAPI middleware or dependency tree before dispatch:

from fastapi import Request


@app.middleware("http")
async def attach_user(request: Request, call_next):
    request.state.user = await resolve_user_from_token(request)
    return await call_next(request)

Where resolve_user_from_token returns either a user object with is_authenticated, is_staff, is_superuser attributes, or None for an anonymous request. The executor branches on those for auth=True, auth="staff", auth="superuser" requirements.

Generate the frontend

The codegen is mizan-generate (in protocol/mizan-generate/). Point a config at your FastAPI app and run the CLI:

// frontend/fastapi.config.mjs
import path from "path"
import { fileURLToPath } from "url"

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const root = path.resolve(__dirname, "..")

export default {
    source: {
        fastapi: {
            module: "main",                           // module to import for @client side effects
            cwd: path.join(root, "backend"),          // python cwd for module resolution
            command: ["uv", "run", "python"],         // optional — defaults to ["python"]
        },
    },
    output: "src/api",
}
npx mizan-generate --config fastapi.config.mjs

The codegen drives python -m mizan_fastapi.cli <module> under the hood, then emits Stage 1 (typed callXxx/fetchXxx over the runtime kernel) + Stage 2 (<MizanContext> provider, per-context providers, use{Hook}() hooks) into src/api/.

// app.tsx
import { MizanContext } from "./api"

export default function App({ children }) {
    return <MizanContext baseUrl="/api/mizan">{children}</MizanContext>
}
// any component
import { useEcho, useCurrentUser } from "./api"

const echo = useEcho()
echo.mutate({ text: "hi" }).then(r => console.log(r.message))

const user = useCurrentUser()  // global context — auto-fetched, auto-refreshed on mutation

Running tests

uv sync --extra dev
uv run pytest

Schema export CLI

For codegen consumption (or any tooling that wants the Mizan schema):

python -m mizan_fastapi.cli <module>

Imports the named module (which must register every @client function as import-time side effects), then prints the OpenAPI schema as JSON to stdout. Mirrors mizan-django's manage.py export_mizan_schema so the codegen consumes either backend the same subprocess way.

Architecture

mizan-fastapi is one of two reference backend adapters (the other is backends/mizan-django). Both implement the same Mizan protocol on top of the shared cores/mizan-python core (@client, registry, MWT, HMAC cache keys). The AFI conformance suite at tests/afi/ gates that the two adapters emit equivalent schemas for the same registered functions. See docs/AFI_ARCHITECTURE.md.

A live e2e harness exercises this adapter end-to-end at examples/fastapi-react-site/ (real Chromium → React with generated hooks → FastAPI server, 14/14 Playwright tests).