mizan-fastapi e2e — example app + Playwright harness, 14/14 green

Demonstration milestone. The substrate work earlier in the session
established that mizan-fastapi can dispatch RPC, bundle context
fetches, and emit invalidation envelopes via TestClient (in-process
ASGI). This commit closes the demonstration gap: a real FastAPI server
on port 8001 + a real React harness on port 5175 + Playwright in real
Chromium, exercising generated hooks.

What ships:

backends/mizan-fastapi/src/mizan_fastapi/cli.py — schema-export CLI:
- `python -m mizan_fastapi.cli <module>` imports the named module
  (triggering @client decorations + register() side effects), then
  prints the OpenAPI schema to stdout. Mirrors mizan-django's
  `manage.py export_mizan_schema` so the codegen consumes either
  backend the same subprocess way.

backends/mizan-django/generate/generator/lib/fetch.mjs — codegen now
dispatches on source.django vs source.fastapi. Refactored the
subprocess plumbing into a shared runSubprocess helper. The codegen
package is still named "mizan-django" by historical accident — it's
the framework-agnostic CLI now (a rename for later).

backends/mizan-fastapi/src/mizan_fastapi/executor.py — bug fix:
mizan_core's @client decorator normalizes auth=True to
meta['auth']='required'. The executor's match was only handling True,
not 'required', so any auth-required endpoint failed with
INTERNAL_ERROR. Now matches both. Caught when wiring up the FastAPI
example backend's whoami fixture; would have surfaced first time any
real FastAPI app used auth=True.

backends/mizan-fastapi/tests/test_dispatch.py — added AuthTests
covering the auth=True path so the bug fix has unit coverage. Suite
now 12/12.

examples/fastapi-react-site/ — parallel to examples/django-react-site/:
- backend/main.py: FastAPI app with 11 @client fixtures matching the
  harness surface (echo, add, multiply, whoami, staff/superuser/
  verified-only, notImplementedFn, buggyFn, permissionCheckFn,
  current_user context). Drops Django-only stuff (forms, channels,
  ws-whoami, session-bound JWT).
- harness/: vite proxy → FastAPI on 8001; generated api/ produced by
  the codegen against fastapi.config.mjs.
- mizan.spec.ts: Playwright suite, 14 tests covering the same axes
  as Django minus channel-chat.
- ContextCurrentUser fixture renders 'loading' until data arrives
  rather than emitting <pre>null</pre> — fixes a race the Django
  harness has too (just doesn't trip in practice).

Verified:
- mizan-fastapi unit:    12/12 (incl. new auth=True coverage)
- mizan-fastapi e2e:     14/14 (Playwright via real Chromium)
- mizan-core unit:       15/15
- mizan-django unit:     348 pass, 21 skip
- AFI conformance:        3/3
- mizan-django e2e:      14/15 (1 skip — channels, deferred)

What remains for FastAPI side:
- Dockerfile.test + docker-compose.test.yml so CI can run the e2e
  in the same containerized way as the Django example.
- Makefile test-integration target for symmetry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 00:05:18 -04:00
parent 19ce4d4a2a
commit 255e10cb21
32 changed files with 2359 additions and 52 deletions

View File

@@ -0,0 +1,19 @@
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const root = path.resolve(__dirname, '../../..')
export default {
projectId: 'mizan-fastapi-e2e',
source: {
fastapi: {
module: 'main',
cwd: path.join(root, 'examples/fastapi-react-site/backend'),
command: ['uv', 'run', 'python'],
},
},
output: 'src/api',
}

View File

@@ -0,0 +1,5 @@
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8" /><title>mizan FastAPI E2E Harness</title></head>
<body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body>
</html>

View File

@@ -0,0 +1,22 @@
{
"name": "mizan-fastapi-e2e-harness",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite --port 5175"
},
"dependencies": {
"@mizan/base": "file:../../../frontends/mizan-base",
"@rythazhur/mizan": "file:../../../frontends/mizan-react",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.0.0",
"typescript": "^5.7.0",
"vite": "^6.0.0"
}
}

View File

@@ -0,0 +1,15 @@
// AUTO-GENERATED by mizan — do not edit
import { mizanFetch } from '@mizan/base'
import type { currentUserOutput } from '../types'
export interface GlobalContextData {
current_user: currentUserOutput
}
export type GlobalContextParams = Record<string, never>
export function fetchGlobalContext(params: GlobalContextParams): Promise<GlobalContextData> {
return mizanFetch('global', params)
}

View File

@@ -0,0 +1,9 @@
// AUTO-GENERATED by mizan — do not edit
import { mizanCall } from '@mizan/base'
import type { addInput, addOutput } from '../types'
export function callAdd(args: addInput): Promise<addOutput> {
return mizanCall('add', args)
}

View File

@@ -0,0 +1,9 @@
// AUTO-GENERATED by mizan — do not edit
import { mizanCall } from '@mizan/base'
import type { buggyFnOutput } from '../types'
export function callBuggyFn(): Promise<buggyFnOutput> {
return mizanCall('buggy_fn', {})
}

View File

@@ -0,0 +1,9 @@
// AUTO-GENERATED by mizan — do not edit
import { mizanCall } from '@mizan/base'
import type { echoInput, echoOutput } from '../types'
export function callEcho(args: echoInput): Promise<echoOutput> {
return mizanCall('echo', args)
}

View File

@@ -0,0 +1,9 @@
// AUTO-GENERATED by mizan — do not edit
import { mizanCall } from '@mizan/base'
import type { multiplyInput, multiplyOutput } from '../types'
export function callMultiply(args: multiplyInput): Promise<multiplyOutput> {
return mizanCall('multiply', args)
}

View File

@@ -0,0 +1,9 @@
// AUTO-GENERATED by mizan — do not edit
import { mizanCall } from '@mizan/base'
import type { notImplementedFnOutput } from '../types'
export function callNotImplementedFn(): Promise<notImplementedFnOutput> {
return mizanCall('not_implemented_fn', {})
}

View File

@@ -0,0 +1,9 @@
// AUTO-GENERATED by mizan — do not edit
import { mizanCall } from '@mizan/base'
import type { permissionCheckFnInput, permissionCheckFnOutput } from '../types'
export function callPermissionCheckFn(args: permissionCheckFnInput): Promise<permissionCheckFnOutput> {
return mizanCall('permission_check_fn', args)
}

View File

@@ -0,0 +1,9 @@
// AUTO-GENERATED by mizan — do not edit
import { mizanCall } from '@mizan/base'
import type { staffOnlyOutput } from '../types'
export function callStaffOnly(): Promise<staffOnlyOutput> {
return mizanCall('staff_only', {})
}

View File

@@ -0,0 +1,9 @@
// AUTO-GENERATED by mizan — do not edit
import { mizanCall } from '@mizan/base'
import type { superuserOnlyOutput } from '../types'
export function callSuperuserOnly(): Promise<superuserOnlyOutput> {
return mizanCall('superuser_only', {})
}

View File

@@ -0,0 +1,9 @@
// AUTO-GENERATED by mizan — do not edit
import { mizanCall } from '@mizan/base'
import type { verifiedOnlyOutput } from '../types'
export function callVerifiedOnly(): Promise<verifiedOnlyOutput> {
return mizanCall('verified_only', {})
}

View File

@@ -0,0 +1,9 @@
// AUTO-GENERATED by mizan — do not edit
import { mizanCall } from '@mizan/base'
import type { whoamiOutput } from '../types'
export function callWhoami(): Promise<whoamiOutput> {
return mizanCall('whoami', {})
}

View File

@@ -0,0 +1,19 @@
// AUTO-GENERATED by mizan — do not edit
export * from './types'
export { fetchGlobalContext, type GlobalContextData, type GlobalContextParams } from './contexts/global'
export { callEcho } from './functions/echo'
export { callAdd } from './functions/add'
export { callMultiply } from './functions/multiply'
export { callWhoami } from './functions/whoami'
export { callStaffOnly } from './functions/staffOnly'
export { callSuperuserOnly } from './functions/superuserOnly'
export { callVerifiedOnly } from './functions/verifiedOnly'
export { callNotImplementedFn } from './functions/notImplementedFn'
export { callBuggyFn } from './functions/buggyFn'
export { callPermissionCheckFn } from './functions/permissionCheckFn'
// Stage 2 framework adapter
export * from './react'

View File

@@ -0,0 +1,169 @@
'use client'
// AUTO-GENERATED by mizan — do not edit
import {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
useSyncExternalStore,
type ReactNode,
} from 'react'
import {
configure,
initSession,
mizanCall,
mizanFetch,
MizanError,
registerContext,
type ContextState,
} from '@mizan/base'
import { fetchGlobalContext, type GlobalContextData, type GlobalContextParams, callEcho, callAdd, callMultiply, callWhoami, callStaffOnly, callSuperuserOnly, callVerifiedOnly, callNotImplementedFn, callBuggyFn, callPermissionCheckFn } from './index'
// Internal — runs inside a Provider, registers with the kernel exactly once.
function useContextSubscription<T>(
name: string,
params: Record<string, any>,
fetchFn: () => Promise<T>,
initialData?: T,
): ContextState<T> {
const ref = useRef<ReturnType<typeof registerContext> | null>(null)
if (!ref.current) {
ref.current = registerContext(name, params, fetchFn, initialData)
}
const handle = ref.current
useEffect(() => {
if (handle.getState().status === 'idle') handle.refetch()
return () => handle.unregister()
}, [handle])
return useSyncExternalStore(handle.subscribe, handle.getState, handle.getState)
}
// Internal — wraps an imperative call() with isPending / error state.
interface MutationHook<TArgs, TResult> {
mutate: (args: TArgs) => Promise<TResult>
isPending: boolean
error: Error | null
}
function useMutation<TArgs, TResult>(
callFn: (args: TArgs) => Promise<TResult>,
): MutationHook<TArgs, TResult> {
const [isPending, setIsPending] = useState(false)
const [error, setError] = useState<Error | null>(null)
const mutate = useCallback(async (args: TArgs) => {
setIsPending(true)
setError(null)
try {
return await callFn(args)
} catch (e) {
setError(e as Error)
throw e
} finally {
setIsPending(false)
}
}, [callFn])
return { mutate, isPending, error }
}
// ── Global Context ──
const GlobalCtx = createContext<ContextState<GlobalContextData> | null>(null)
export function GlobalContextProvider({ children }: { children: ReactNode }) {
const ssrData = typeof window !== 'undefined' ? (window as any).__MIZAN_SSR_DATA__ : undefined
const state = useContextSubscription('global', {}, () => fetchGlobalContext({} as any), ssrData)
return <GlobalCtx.Provider value={state}>{children}</GlobalCtx.Provider>
}
export function useGlobalContext(): ContextState<GlobalContextData> {
const ctx = useContext(GlobalCtx)
if (!ctx) throw new Error('useGlobalContext requires <MizanContext> or <GlobalContextProvider>')
return ctx
}
export function useCurrentUser(): currentUserOutput | null {
return useGlobalContext().data?.current_user ?? null
}
export function useEcho() {
return useMutation<Parameters<typeof callEcho>[0], Awaited<ReturnType<typeof callEcho>>>(callEcho)
}
export function useAdd() {
return useMutation<Parameters<typeof callAdd>[0], Awaited<ReturnType<typeof callAdd>>>(callAdd)
}
export function useMultiply() {
return useMutation<Parameters<typeof callMultiply>[0], Awaited<ReturnType<typeof callMultiply>>>(callMultiply)
}
export function useWhoami() {
return useMutation<void, Awaited<ReturnType<typeof callWhoami>>>(() => callWhoami() as any)
}
export function useStaffOnly() {
return useMutation<void, Awaited<ReturnType<typeof callStaffOnly>>>(() => callStaffOnly() as any)
}
export function useSuperuserOnly() {
return useMutation<void, Awaited<ReturnType<typeof callSuperuserOnly>>>(() => callSuperuserOnly() as any)
}
export function useVerifiedOnly() {
return useMutation<void, Awaited<ReturnType<typeof callVerifiedOnly>>>(() => callVerifiedOnly() as any)
}
export function useNotImplementedFn() {
return useMutation<void, Awaited<ReturnType<typeof callNotImplementedFn>>>(() => callNotImplementedFn() as any)
}
export function useBuggyFn() {
return useMutation<void, Awaited<ReturnType<typeof callBuggyFn>>>(() => callBuggyFn() as any)
}
export function usePermissionCheckFn() {
return useMutation<Parameters<typeof callPermissionCheckFn>[0], Awaited<ReturnType<typeof callPermissionCheckFn>>>(callPermissionCheckFn)
}
// ── MizanContext root provider ──
export interface MizanContextProps {
/** Base URL for protocol endpoints. Defaults to "/api/mizan". */
baseUrl?: string
children: ReactNode
}
/**
* Root provider — calls configure() once and mounts the global context (if defined).
* Must wrap any component using Mizan-generated hooks.
*/
export function MizanContext({ baseUrl, children }: MizanContextProps) {
const configured = useRef(false)
if (!configured.current) {
if (baseUrl) configure({ baseUrl })
configured.current = true
}
return <GlobalContextProvider>{children}</GlobalContextProvider>
}
// ── Imperative escape hatch ──
/**
* Returns the imperative kernel API. For test harnesses or rare cases where
* a typed generated hook does not fit. Most app code should use the typed hooks.
*/
export function useMizan() {
return { call: mizanCall, fetch: mizanFetch }
}
export type { ContextState } from '@mizan/base'
export { configure, initSession, MizanError } from '@mizan/base'

View File

@@ -0,0 +1,756 @@
{
"openapi": "3.1.0",
"info": {
"title": "mizan Server Functions",
"description": "Auto-generated schema for mizan server functions",
"version": "1.0.0"
},
"paths": {
"/mizan/echo": {
"post": {
"summary": "Echoes the input text.",
"operationId": "echo",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/echoInput"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/echoOutput"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
},
"x-mizan": {
"transport": "http",
"isContext": false
}
}
},
"/mizan/add": {
"post": {
"summary": "Returns a + b.",
"operationId": "add",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/addInput"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/addOutput"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
},
"x-mizan": {
"transport": "http",
"isContext": false
}
}
},
"/mizan/multiply": {
"post": {
"summary": "Returns x * y.",
"operationId": "multiply",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/multiplyInput"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/multiplyOutput"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
},
"x-mizan": {
"transport": "http",
"isContext": false
}
}
},
"/mizan/whoami": {
"post": {
"summary": "Returns the authenticated user's identity. Anonymous → UNAUTHORIZED.",
"operationId": "whoami",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/whoamiOutput"
}
}
}
}
},
"x-mizan": {
"transport": "http",
"isContext": false
}
}
},
"/mizan/staff_only": {
"post": {
"summary": "Staff-only endpoint.",
"operationId": "staffOnly",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/staffOnlyOutput"
}
}
}
}
},
"x-mizan": {
"transport": "http",
"isContext": false
}
}
},
"/mizan/superuser_only": {
"post": {
"summary": "Superuser-only endpoint.",
"operationId": "superuserOnly",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/superuserOnlyOutput"
}
}
}
}
},
"x-mizan": {
"transport": "http",
"isContext": false
}
}
},
"/mizan/verified_only": {
"post": {
"summary": "Verified-users-only endpoint. Anonymous → FORBIDDEN.",
"operationId": "verifiedOnly",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/verifiedOnlyOutput"
}
}
}
}
},
"x-mizan": {
"transport": "http",
"isContext": false
}
}
},
"/mizan/not_implemented_fn": {
"post": {
"summary": "Always raises NotImplementedError → NOT_IMPLEMENTED.",
"operationId": "notImplementedFn",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/notImplementedFnOutput"
}
}
}
}
},
"x-mizan": {
"transport": "http",
"isContext": false
}
}
},
"/mizan/buggy_fn": {
"post": {
"summary": "Always raises a generic exception → INTERNAL_ERROR.",
"operationId": "buggyFn",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/buggyFnOutput"
}
}
}
}
},
"x-mizan": {
"transport": "http",
"isContext": false
}
}
},
"/mizan/permission_check_fn": {
"post": {
"summary": "Wrong secret → FORBIDDEN; correct secret → success.",
"operationId": "permissionCheckFn",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/permissionCheckFnInput"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/permissionCheckFnOutput"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
},
"x-mizan": {
"transport": "http",
"isContext": false
}
}
},
"/mizan/current_user": {
"post": {
"summary": "The global context — auto-mounted at the React root.",
"operationId": "currentUser",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/currentUserOutput"
}
}
}
}
},
"x-mizan": {
"transport": "http",
"isContext": "global"
}
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
},
"input": {
"title": "Input"
},
"ctx": {
"type": "object",
"title": "Context"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"addInput": {
"properties": {
"a": {
"type": "integer",
"title": "A"
},
"b": {
"type": "integer",
"title": "B"
}
},
"type": "object",
"required": [
"a",
"b"
],
"title": "addInput"
},
"addOutput": {
"properties": {
"result": {
"type": "integer",
"title": "Result"
}
},
"type": "object",
"required": [
"result"
],
"title": "addOutput"
},
"buggyFnOutput": {
"properties": {
"message": {
"type": "string",
"title": "Message"
}
},
"type": "object",
"required": [
"message"
],
"title": "buggyFnOutput"
},
"currentUserOutput": {
"properties": {
"email": {
"type": "string",
"title": "Email"
},
"authenticated": {
"type": "boolean",
"title": "Authenticated"
},
"is_staff": {
"type": "boolean",
"title": "Is Staff",
"default": false
}
},
"type": "object",
"required": [
"email",
"authenticated"
],
"title": "currentUserOutput"
},
"echoInput": {
"properties": {
"text": {
"type": "string",
"title": "Text"
}
},
"type": "object",
"required": [
"text"
],
"title": "echoInput"
},
"echoOutput": {
"properties": {
"message": {
"type": "string",
"title": "Message"
}
},
"type": "object",
"required": [
"message"
],
"title": "echoOutput"
},
"multiplyInput": {
"properties": {
"x": {
"type": "integer",
"title": "X"
},
"y": {
"type": "integer",
"title": "Y"
}
},
"type": "object",
"required": [
"x",
"y"
],
"title": "multiplyInput"
},
"multiplyOutput": {
"properties": {
"product": {
"type": "integer",
"title": "Product"
}
},
"type": "object",
"required": [
"product"
],
"title": "multiplyOutput"
},
"notImplementedFnOutput": {
"properties": {
"message": {
"type": "string",
"title": "Message"
}
},
"type": "object",
"required": [
"message"
],
"title": "notImplementedFnOutput"
},
"permissionCheckFnInput": {
"properties": {
"secret": {
"type": "string",
"title": "Secret"
}
},
"type": "object",
"required": [
"secret"
],
"title": "permissionCheckFnInput"
},
"permissionCheckFnOutput": {
"properties": {
"message": {
"type": "string",
"title": "Message"
}
},
"type": "object",
"required": [
"message"
],
"title": "permissionCheckFnOutput"
},
"staffOnlyOutput": {
"properties": {
"message": {
"type": "string",
"title": "Message"
}
},
"type": "object",
"required": [
"message"
],
"title": "staffOnlyOutput"
},
"superuserOnlyOutput": {
"properties": {
"message": {
"type": "string",
"title": "Message"
}
},
"type": "object",
"required": [
"message"
],
"title": "superuserOnlyOutput"
},
"verifiedOnlyOutput": {
"properties": {
"message": {
"type": "string",
"title": "Message"
}
},
"type": "object",
"required": [
"message"
],
"title": "verifiedOnlyOutput"
},
"whoamiOutput": {
"properties": {
"email": {
"type": "string",
"title": "Email"
},
"authenticated": {
"type": "boolean",
"title": "Authenticated"
},
"is_staff": {
"type": "boolean",
"title": "Is Staff",
"default": false
}
},
"type": "object",
"required": [
"email",
"authenticated"
],
"title": "whoamiOutput"
}
}
},
"x-mizan-functions": [
{
"name": "echo",
"camelName": "echo",
"hasInput": true,
"inputType": "echoInput",
"outputType": "echoOutput",
"transport": "http",
"isContext": false,
"isForm": false,
"formName": null,
"formRole": null
},
{
"name": "add",
"camelName": "add",
"hasInput": true,
"inputType": "addInput",
"outputType": "addOutput",
"transport": "http",
"isContext": false,
"isForm": false,
"formName": null,
"formRole": null
},
{
"name": "multiply",
"camelName": "multiply",
"hasInput": true,
"inputType": "multiplyInput",
"outputType": "multiplyOutput",
"transport": "http",
"isContext": false,
"isForm": false,
"formName": null,
"formRole": null
},
{
"name": "whoami",
"camelName": "whoami",
"hasInput": false,
"inputType": null,
"outputType": "whoamiOutput",
"transport": "http",
"isContext": false,
"isForm": false,
"formName": null,
"formRole": null
},
{
"name": "staff_only",
"camelName": "staffOnly",
"hasInput": false,
"inputType": null,
"outputType": "staffOnlyOutput",
"transport": "http",
"isContext": false,
"isForm": false,
"formName": null,
"formRole": null
},
{
"name": "superuser_only",
"camelName": "superuserOnly",
"hasInput": false,
"inputType": null,
"outputType": "superuserOnlyOutput",
"transport": "http",
"isContext": false,
"isForm": false,
"formName": null,
"formRole": null
},
{
"name": "verified_only",
"camelName": "verifiedOnly",
"hasInput": false,
"inputType": null,
"outputType": "verifiedOnlyOutput",
"transport": "http",
"isContext": false,
"isForm": false,
"formName": null,
"formRole": null
},
{
"name": "not_implemented_fn",
"camelName": "notImplementedFn",
"hasInput": false,
"inputType": null,
"outputType": "notImplementedFnOutput",
"transport": "http",
"isContext": false,
"isForm": false,
"formName": null,
"formRole": null
},
{
"name": "buggy_fn",
"camelName": "buggyFn",
"hasInput": false,
"inputType": null,
"outputType": "buggyFnOutput",
"transport": "http",
"isContext": false,
"isForm": false,
"formName": null,
"formRole": null
},
{
"name": "permission_check_fn",
"camelName": "permissionCheckFn",
"hasInput": true,
"inputType": "permissionCheckFnInput",
"outputType": "permissionCheckFnOutput",
"transport": "http",
"isContext": false,
"isForm": false,
"formName": null,
"formRole": null
},
{
"name": "current_user",
"camelName": "currentUser",
"hasInput": false,
"inputType": null,
"outputType": "currentUserOutput",
"transport": "http",
"isContext": "global",
"isForm": false,
"formName": null,
"formRole": null
}
],
"x-mizan-contexts": {
"global": {
"functions": [
"current_user"
],
"params": {}
}
}
}

View File

@@ -0,0 +1,607 @@
// AUTO-GENERATED by mizan — do not edit
export interface paths {
"/mizan/echo": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Echoes the input text. */
post: operations["echo"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/mizan/add": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Returns a + b. */
post: operations["add"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/mizan/multiply": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Returns x * y. */
post: operations["multiply"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/mizan/whoami": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Returns the authenticated user's identity. Anonymous → UNAUTHORIZED. */
post: operations["whoami"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/mizan/staff_only": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Staff-only endpoint. */
post: operations["staffOnly"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/mizan/superuser_only": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Superuser-only endpoint. */
post: operations["superuserOnly"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/mizan/verified_only": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Verified-users-only endpoint. Anonymous → FORBIDDEN. */
post: operations["verifiedOnly"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/mizan/not_implemented_fn": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Always raises NotImplementedError → NOT_IMPLEMENTED. */
post: operations["notImplementedFn"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/mizan/buggy_fn": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Always raises a generic exception → INTERNAL_ERROR. */
post: operations["buggyFn"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/mizan/permission_check_fn": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Wrong secret → FORBIDDEN; correct secret → success. */
post: operations["permissionCheckFn"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/mizan/current_user": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** The global context — auto-mounted at the React root. */
post: operations["currentUser"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
/** HTTPValidationError */
HTTPValidationError: {
/** Detail */
detail?: components["schemas"]["ValidationError"][];
};
/** ValidationError */
ValidationError: {
/** Location */
loc: (string | number)[];
/** Message */
msg: string;
/** Error Type */
type: string;
/** Input */
input?: unknown;
/** Context */
ctx?: Record<string, never>;
};
/** addInput */
addInput: {
/** A */
a: number;
/** B */
b: number;
};
/** addOutput */
addOutput: {
/** Result */
result: number;
};
/** buggyFnOutput */
buggyFnOutput: {
/** Message */
message: string;
};
/** currentUserOutput */
currentUserOutput: {
/** Email */
email: string;
/** Authenticated */
authenticated: boolean;
/**
* Is Staff
* @default false
*/
is_staff: boolean;
};
/** echoInput */
echoInput: {
/** Text */
text: string;
};
/** echoOutput */
echoOutput: {
/** Message */
message: string;
};
/** multiplyInput */
multiplyInput: {
/** X */
x: number;
/** Y */
y: number;
};
/** multiplyOutput */
multiplyOutput: {
/** Product */
product: number;
};
/** notImplementedFnOutput */
notImplementedFnOutput: {
/** Message */
message: string;
};
/** permissionCheckFnInput */
permissionCheckFnInput: {
/** Secret */
secret: string;
};
/** permissionCheckFnOutput */
permissionCheckFnOutput: {
/** Message */
message: string;
};
/** staffOnlyOutput */
staffOnlyOutput: {
/** Message */
message: string;
};
/** superuserOnlyOutput */
superuserOnlyOutput: {
/** Message */
message: string;
};
/** verifiedOnlyOutput */
verifiedOnlyOutput: {
/** Message */
message: string;
};
/** whoamiOutput */
whoamiOutput: {
/** Email */
email: string;
/** Authenticated */
authenticated: boolean;
/**
* Is Staff
* @default false
*/
is_staff: boolean;
};
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export interface operations {
echo: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["echoInput"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["echoOutput"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
add: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["addInput"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["addOutput"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
multiply: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["multiplyInput"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["multiplyOutput"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
whoami: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["whoamiOutput"];
};
};
};
};
staffOnly: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["staffOnlyOutput"];
};
};
};
};
superuserOnly: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["superuserOnlyOutput"];
};
};
};
};
verifiedOnly: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["verifiedOnlyOutput"];
};
};
};
};
notImplementedFn: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["notImplementedFnOutput"];
};
};
};
};
buggyFn: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["buggyFnOutput"];
};
};
};
};
permissionCheckFn: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["permissionCheckFnInput"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["permissionCheckFnOutput"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
currentUser: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["currentUserOutput"];
};
};
};
};
}
// Convenience type exports
export type HTTPValidationError = components["schemas"]["HTTPValidationError"]
export type ValidationError = components["schemas"]["ValidationError"]
export type addInput = components["schemas"]["addInput"]
export type addOutput = components["schemas"]["addOutput"]
export type buggyFnOutput = components["schemas"]["buggyFnOutput"]
export type currentUserOutput = components["schemas"]["currentUserOutput"]
export type echoInput = components["schemas"]["echoInput"]
export type echoOutput = components["schemas"]["echoOutput"]
export type multiplyInput = components["schemas"]["multiplyInput"]
export type multiplyOutput = components["schemas"]["multiplyOutput"]
export type notImplementedFnOutput = components["schemas"]["notImplementedFnOutput"]
export type permissionCheckFnInput = components["schemas"]["permissionCheckFnInput"]
export type permissionCheckFnOutput = components["schemas"]["permissionCheckFnOutput"]
export type staffOnlyOutput = components["schemas"]["staffOnlyOutput"]
export type superuserOnlyOutput = components["schemas"]["superuserOnlyOutput"]
export type verifiedOnlyOutput = components["schemas"]["verifiedOnlyOutput"]
export type whoamiOutput = components["schemas"]["whoamiOutput"]

View File

@@ -0,0 +1,132 @@
/**
* E2E Test Fixtures — FastAPI flavor.
*
* Mirrors examples/django-react-site/harness/src/fixtures.tsx minus the
* Django-only surfaces (channels, forms). Each fixture uses GENERATED
* mizan hooks (not raw call()). Playwright reads the DOM to verify
* behavior. URL hash selects the fixture: #echo, #add, etc.
*/
import { useState, useEffect } from 'react'
import {
MizanContext,
useEcho,
useAdd,
useMultiply,
useWhoami,
useStaffOnly,
useSuperuserOnly,
useVerifiedOnly,
useNotImplementedFn,
useBuggyFn,
usePermissionCheckFn,
useCurrentUser,
MizanError,
useMizan,
} from './api'
export function Fixtures() {
const [hash, setHash] = useState(window.location.hash.slice(1))
useEffect(() => {
const onHash = () => setHash(window.location.hash.slice(1))
window.addEventListener('hashchange', onHash)
return () => window.removeEventListener('hashchange', onHash)
}, [])
switch (hash) {
case 'echo': return <Echo />
case 'add': return <Add />
case 'multiply': return <Multiply />
case 'not-found': return <NotFound />
case 'validation-error': return <ValidationError />
case 'auth-required': return <AuthRequired />
case 'staff-only': return <StaffOnly />
case 'superuser-only': return <SuperuserOnly />
case 'verified-only': return <VerifiedOnly />
case 'not-implemented': return <NotImplemented />
case 'internal-error': return <InternalError />
case 'permission-error': return <PermissionError_ />
case 'permission-success': return <PermissionSuccess />
case 'context-current-user': return <ContextCurrentUser />
default: return <div data-testid="ready">Harness ready. Set #hash.</div>
}
}
function Result({ data, error }: { data?: unknown; error?: unknown }) {
return (
<>
{data !== undefined && (
<pre data-testid="result">{JSON.stringify(data)}</pre>
)}
{error !== undefined && error !== null && (
<>
<div data-testid="error-type">
{error instanceof MizanError ? 'MizanError' : 'Error'}
</div>
<div data-testid="error-code">
{error instanceof MizanError ? error.code : ''}
</div>
<pre data-testid="error-message">
{error instanceof Error ? error.message : String(error)}
</pre>
</>
)}
</>
)
}
function useRun<T>(hook: () => { mutate: (input?: any) => Promise<T> }, input?: any) {
const { mutate } = hook()
const [data, setData] = useState<T>()
const [error, setError] = useState<unknown>()
useEffect(() => {
mutate(input).then(setData).catch(setError)
}, []) // eslint-disable-line react-hooks/exhaustive-deps
return { data, error }
}
function Echo() { const r = useRun(useEcho, { text: 'e2e-test' }); return <Result {...r} /> }
function Add() { const r = useRun(useAdd, { a: 17, b: 25 }); return <Result {...r} /> }
function Multiply() { const r = useRun(useMultiply, { x: 6, y: 7 }); return <Result {...r} /> }
function NotFound() {
const { call } = useMizan()
const [error, setError] = useState<unknown>()
useEffect(() => { call('does_not_exist').catch(setError) }, [call])
return <Result error={error} />
}
function ValidationError() {
const { mutate } = useAdd()
const [error, setError] = useState<unknown>()
useEffect(() => { (mutate as any)({ a: 'not_a_number', b: 'also_not' }).catch(setError) }, [mutate])
return <Result error={error} />
}
function AuthRequired() { const r = useRun(useWhoami); return <Result {...r} /> }
function StaffOnly() { const r = useRun(useStaffOnly); return <Result {...r} /> }
function SuperuserOnly() { const r = useRun(useSuperuserOnly); return <Result {...r} /> }
function VerifiedOnly() { const r = useRun(useVerifiedOnly); return <Result {...r} /> }
function NotImplemented() { const r = useRun(useNotImplementedFn); return <Result {...r} /> }
function InternalError() { const r = useRun(useBuggyFn); return <Result {...r} /> }
function PermissionError_() { const r = useRun(usePermissionCheckFn, { secret: 'wrong' }); return <Result {...r} /> }
function PermissionSuccess() { const r = useRun(usePermissionCheckFn, { secret: 'open-sesame' }); return <Result {...r} /> }
function ContextCurrentUser() {
try {
const user = useCurrentUser()
if (user === null) return <div>loading context...</div>
return <pre data-testid="result">{JSON.stringify(user)}</pre>
} catch {
return <div>loading context...</div>
}
}

View File

@@ -0,0 +1,13 @@
import { createRoot } from 'react-dom/client'
import { MizanContext } from './api'
import { Fixtures } from './fixtures'
function App() {
return (
<MizanContext baseUrl="/api/mizan">
<Fixtures />
</MizanContext>
)
}
createRoot(document.getElementById('root')!).render(<App />)

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "react-jsx",
"skipLibCheck": true
},
"include": ["src"]
}

View File

@@ -0,0 +1,11 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': 'http://localhost:8001',
},
},
})