The IR called them react-message and django-message, so a FastAPI channel had to declare a DjangoMessage. They are client-message and server-message now, and the direction words hold wherever a channel is declared: Params / ClientMessage / ServerMessage, with mizan-core deriving <Pascal>Params and friends so no backend names a type itself. Django's ReactChannel and FastAPI's ReactChannel are both Channel. mizan-fastapi never registered a channels extension, so build_ir() emitted no channel at all and every payload type was invisible to codegen. It registers one now. RegistryExtension is an ABC requiring all(), which is what the IR reads — an extension that cannot enumerate its registrations no longer exists. The gate that should have caught the rename could not: tests/afi registered no channel because mizan-rust had no channel registry to register one in, so a five-package rename of the wire contract passed byte-parity without a channel byte crossing it. mizan-rust grows ChannelSlotKind, a CHANNELS slice, a #[mizan::channel] macro, and KDL emission whose wire_to_pascal matches Python's split; the AFI fixture now carries a channel with every slot and one with a single slot, so all three backends prove the contract byte for byte. MizanChannel held three Option<String> beside three has_*() predicates and unwrapped them with defaults; it holds an ordered slot vector, so an absent slot is absent rather than defaulted. The channels target emitted a React hooks file that a stage1-only consumer could not compile — react emits that now. The codegen's parity tests byte-compared emitted source against baselines without ever compiling it: they compile the generated crate and run its tests, import the generated Python package and call every method, and typecheck each TypeScript target against a consumer. Also fixed at source: app_visitor printed its import diagnostic to stdout, the stream export_mizan_ir writes KDL to, so a failed import silently corrupted the IR; the apps root was hardcoded to "apps"; _default_literal crashed build_ir on any non-JSON-serializable field default; Django and mizan-core derived Pascal names two different ways, disagreeing on every dotted channel name. ir.py builds a document and renders templates/ir/document.kdl.j2 rather than appending KDL strings with hand-tracked indentation, and named types resolve to a fixed point — a model reachable only through a union branch was referenced by a ref that no type block ever defined. The rest is the write-gate's own classifiers run over the standing tree: relative imports, silent swallows, Protocol contracts that should be ABCs, emitters hand-rendering target source, catch-all arms over closed enums, and comments narrating the project rather than the code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
413 lines
10 KiB
Python
413 lines
10 KiB
Python
from django import forms
|
|
from django.http import HttpRequest
|
|
from pydantic import BaseModel
|
|
|
|
from mizan.client import ServerFunction, client
|
|
from mizan.channels import Channel
|
|
from mizan.setup import register, register_form, register_as
|
|
from mizan.channels import register as register_channel
|
|
from mizan.forms import mizanFormMixin, mizanFormMeta
|
|
from mizan.jwt import jwt_obtain, jwt_refresh
|
|
|
|
|
|
# =============================================================================
|
|
# Server Functions
|
|
# =============================================================================
|
|
|
|
|
|
class EchoOutput(BaseModel):
|
|
message: str
|
|
|
|
|
|
@client(websocket=True)
|
|
def echo(request: HttpRequest, text: str) -> EchoOutput:
|
|
return EchoOutput(message=text)
|
|
|
|
|
|
register(echo, "echo")
|
|
|
|
|
|
class AddOutput(BaseModel):
|
|
result: int
|
|
|
|
|
|
@client(websocket=True)
|
|
def add(request: HttpRequest, a: int, b: int) -> AddOutput:
|
|
return AddOutput(result=a + b)
|
|
|
|
|
|
register(add, "add")
|
|
|
|
|
|
class WhoamiOutput(BaseModel):
|
|
user_id: int | None
|
|
email: str
|
|
is_staff: bool
|
|
|
|
|
|
@client(auth=True)
|
|
def whoami(request: HttpRequest) -> WhoamiOutput:
|
|
return WhoamiOutput(
|
|
user_id=getattr(request.user, "id", None),
|
|
email=getattr(request.user, "email", ""),
|
|
is_staff=getattr(request.user, "is_staff", False),
|
|
)
|
|
|
|
|
|
register(whoami, "whoami")
|
|
|
|
|
|
@client
|
|
def http_only_echo(request: HttpRequest, text: str) -> EchoOutput:
|
|
return EchoOutput(message=text)
|
|
|
|
|
|
register(http_only_echo, "http_only_echo")
|
|
|
|
|
|
# =============================================================================
|
|
# Forms
|
|
# =============================================================================
|
|
|
|
|
|
class LoginForm(forms.Form):
|
|
login = forms.CharField(max_length=150, label="Login")
|
|
password = forms.CharField(widget=forms.PasswordInput, label="Password")
|
|
|
|
|
|
def handle_login(request, form):
|
|
"""Authenticate the credentials and open a session."""
|
|
from django.contrib.auth import authenticate, login
|
|
|
|
user = authenticate(
|
|
request,
|
|
username=form.cleaned_data["login"],
|
|
password=form.cleaned_data["password"],
|
|
)
|
|
if user is not None:
|
|
login(request, user)
|
|
return {"success": True}
|
|
form.add_error(None, "Invalid login credentials.")
|
|
return None # Signals validation failure
|
|
|
|
|
|
register_form(LoginForm, "login", submit_handler=handle_login)
|
|
|
|
|
|
class SignupForm(forms.Form):
|
|
email = forms.EmailField(label="Email")
|
|
password1 = forms.CharField(widget=forms.PasswordInput, label="Password")
|
|
|
|
|
|
def handle_signup(request, form):
|
|
"""Create the user, surfacing any creation failure as a form error."""
|
|
from django.contrib.auth import get_user_model
|
|
|
|
User = get_user_model()
|
|
try:
|
|
user = User.objects.create_user(
|
|
email=form.cleaned_data["email"],
|
|
password=form.cleaned_data["password1"],
|
|
)
|
|
except Exception as e:
|
|
form.add_error(None, str(e))
|
|
return None
|
|
|
|
return {"success": True, "data": {"user_id": user.pk}}
|
|
|
|
|
|
register_form(SignupForm, "signup", submit_handler=handle_signup)
|
|
|
|
|
|
class AddEmailForm(forms.Form):
|
|
email = forms.EmailField(label="Email address")
|
|
|
|
|
|
register_form(AddEmailForm, "add_email")
|
|
|
|
|
|
# =============================================================================
|
|
# Channels
|
|
# =============================================================================
|
|
|
|
|
|
class ChatChannel(Channel):
|
|
class Params(BaseModel):
|
|
room: str
|
|
|
|
class ClientMessage(BaseModel):
|
|
text: str
|
|
|
|
class ServerMessage(BaseModel):
|
|
text: str
|
|
|
|
def authorize(self, params=None):
|
|
return bool(params and params.room)
|
|
|
|
def group(self, params=None):
|
|
return f"chat_{params.room}"
|
|
|
|
def receive(self, params, msg):
|
|
return self.ServerMessage(text=msg.text)
|
|
|
|
|
|
register_channel(ChatChannel, "chat")
|
|
|
|
|
|
class NotificationsChannel(Channel):
|
|
class ServerMessage(BaseModel):
|
|
text: str
|
|
|
|
def authorize(self, params=None):
|
|
return self.user.is_authenticated
|
|
|
|
def group(self, params=None):
|
|
return "notifications_global"
|
|
|
|
|
|
register_channel(NotificationsChannel, "notifications")
|
|
|
|
|
|
class PresenceChannel(Channel):
|
|
class ServerMessage(BaseModel):
|
|
value: int
|
|
|
|
def authorize(self, params=None):
|
|
return self.user.is_authenticated
|
|
|
|
def group(self, params=None):
|
|
return "presence_global"
|
|
|
|
|
|
register_channel(PresenceChannel, "presence")
|
|
|
|
|
|
# =============================================================================
|
|
# Auth Variations
|
|
# =============================================================================
|
|
|
|
|
|
# --- Staff-only ---
|
|
@client(auth="staff")
|
|
def staff_only(request: HttpRequest) -> EchoOutput:
|
|
return EchoOutput(message=f"staff:{request.user.email}")
|
|
|
|
|
|
register(staff_only, "staff_only")
|
|
|
|
|
|
# --- Superuser-only ---
|
|
@client(auth="superuser")
|
|
def superuser_only(request: HttpRequest) -> EchoOutput:
|
|
return EchoOutput(message=f"superuser:{request.user.email}")
|
|
|
|
|
|
register(superuser_only, "superuser_only")
|
|
|
|
|
|
# --- Callable auth ---
|
|
def check_verified_email(request):
|
|
if not request.user.is_authenticated:
|
|
return False
|
|
return getattr(request.user, "email", "").endswith("@verified.com")
|
|
|
|
|
|
@client(auth=check_verified_email)
|
|
def verified_only(request: HttpRequest) -> EchoOutput:
|
|
return EchoOutput(message="verified")
|
|
|
|
|
|
register(verified_only, "verified_only")
|
|
|
|
|
|
# =============================================================================
|
|
# Context Functions
|
|
# =============================================================================
|
|
|
|
|
|
class CurrentUserOutput(BaseModel):
|
|
authenticated: bool
|
|
email: str
|
|
is_staff: bool
|
|
|
|
|
|
@client(context="global")
|
|
def current_user(request: HttpRequest) -> CurrentUserOutput:
|
|
if request.user.is_authenticated:
|
|
return CurrentUserOutput(
|
|
authenticated=True,
|
|
email=request.user.email,
|
|
is_staff=request.user.is_staff,
|
|
)
|
|
return CurrentUserOutput(authenticated=False, email="", is_staff=False)
|
|
|
|
|
|
register(current_user, "current_user")
|
|
|
|
|
|
class GreetOutput(BaseModel):
|
|
greeting: str
|
|
|
|
|
|
@client(context="local")
|
|
def greet(request: HttpRequest, name: str) -> GreetOutput:
|
|
return GreetOutput(greeting=f"Hello, {name}!")
|
|
|
|
|
|
register(greet, "greet")
|
|
|
|
|
|
# =============================================================================
|
|
# Class-based ServerFunction
|
|
# =============================================================================
|
|
|
|
|
|
class MultiplyInput(BaseModel):
|
|
x: int
|
|
y: int
|
|
|
|
|
|
class MultiplyOutput(BaseModel):
|
|
product: int
|
|
|
|
|
|
@register_as("multiply")
|
|
class Multiply(ServerFunction):
|
|
Input = MultiplyInput
|
|
Output = MultiplyOutput
|
|
|
|
def call(self, input: MultiplyInput) -> MultiplyOutput:
|
|
return MultiplyOutput(product=input.x * input.y)
|
|
|
|
|
|
# =============================================================================
|
|
# Functions carrying error branches
|
|
# =============================================================================
|
|
|
|
|
|
_ECHO_TRANSFORMS = {"upper": str.upper, "lower": str.lower}
|
|
|
|
|
|
@client
|
|
def echo_transform(request: HttpRequest, text: str, mode: str) -> EchoOutput:
|
|
transform = _ECHO_TRANSFORMS.get(mode)
|
|
if transform is None:
|
|
raise NotImplementedError(f"echo mode {mode!r} has no transform")
|
|
return EchoOutput(message=transform(text))
|
|
|
|
|
|
register(echo_transform, "echo_transform")
|
|
|
|
|
|
class DivideOutput(BaseModel):
|
|
quotient: float
|
|
|
|
|
|
@client
|
|
def divide(request: HttpRequest, numerator: int, denominator: int) -> DivideOutput:
|
|
return DivideOutput(quotient=numerator / denominator)
|
|
|
|
|
|
register(divide, "divide")
|
|
|
|
|
|
@client
|
|
def permission_check_fn(request: HttpRequest, secret: str) -> EchoOutput:
|
|
if secret != "open-sesame":
|
|
raise PermissionError("Wrong secret")
|
|
return EchoOutput(message="access granted")
|
|
|
|
|
|
register(permission_check_fn, "permission_check_fn")
|
|
|
|
|
|
# =============================================================================
|
|
# WebSocket + Auth Function
|
|
# =============================================================================
|
|
|
|
|
|
@client(websocket=True, auth=True)
|
|
def ws_whoami(request: HttpRequest) -> WhoamiOutput:
|
|
return WhoamiOutput(
|
|
user_id=getattr(request.user, "id", None),
|
|
email=getattr(request.user, "email", ""),
|
|
is_staff=getattr(request.user, "is_staff", False),
|
|
)
|
|
|
|
|
|
register(ws_whoami, "ws_whoami")
|
|
|
|
|
|
# =============================================================================
|
|
# mizanFormMixin Forms
|
|
# =============================================================================
|
|
|
|
|
|
class ContactForm(mizanFormMixin, forms.Form):
|
|
mizan = mizanFormMeta(
|
|
name="contact",
|
|
title="Contact Us",
|
|
subtitle="We'd love to hear from you",
|
|
submit_label="Send Message",
|
|
live_validation=True,
|
|
live_form_errors=False,
|
|
)
|
|
|
|
name = forms.CharField(max_length=100, label="Your Name")
|
|
email = forms.EmailField(label="Email Address")
|
|
message = forms.CharField(widget=forms.Textarea, label="Message")
|
|
|
|
def on_submit_success(self, request):
|
|
return {"received": True, "from": self.cleaned_data["email"]}
|
|
|
|
|
|
# =============================================================================
|
|
# Formset-enabled Form
|
|
# =============================================================================
|
|
|
|
|
|
class ItemForm(mizanFormMixin, forms.Form):
|
|
mizan = mizanFormMeta(
|
|
name="item",
|
|
title="Items",
|
|
submit_label="Save Items",
|
|
enable_formset=True,
|
|
)
|
|
|
|
label = forms.CharField(max_length=50, label="Item Label")
|
|
quantity = forms.IntegerField(min_value=1, label="Quantity")
|
|
|
|
def on_submit_success(self, request):
|
|
return {
|
|
"label": self.cleaned_data["label"],
|
|
"qty": self.cleaned_data["quantity"],
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# Staff-gated Channel
|
|
# =============================================================================
|
|
|
|
|
|
class PrivateChannel(Channel):
|
|
class ServerMessage(BaseModel):
|
|
text: str
|
|
|
|
def authorize(self, params=None):
|
|
return self.user.is_staff
|
|
|
|
def group(self, params=None):
|
|
return "private_global"
|
|
|
|
|
|
register_channel(PrivateChannel, "private")
|
|
|
|
|
|
# =============================================================================
|
|
# JWT Function Registration
|
|
# =============================================================================
|
|
|
|
|
|
register(jwt_obtain, "jwt_obtain")
|
|
register(jwt_refresh, "jwt_refresh")
|