mizan-tauri + Pydantic-aware codegen: Tauri-as-Mizan-backend substrate

Tauri now joins FastAPI/Django/axum as a first-class Mizan backend. The
React frontend calls Mizan-registered functions through Tauri's IPC
with the same {result, invalidate, merge} envelope the HTTP path uses;
the schema flows Pydantic → decoru → Rust → KDL → TS in one
mizan-generate invocation.

New packages:
* backends/mizan-tauri — Tauri plugin exposing a single `mizan_invoke`
  command that routes through mizan-core's FUNCTIONS / CONTEXTS
  registries. No per-function tauri::command; the linkme slice IS the
  dispatch table.
* frontends/mizan-tauri-transport — TS package exporting
  tauriTransport() that wraps invoke('plugin:mizan|mizan_invoke', ...)
  and re-shapes errors into MizanError. Pairs with mizan-tauri.

@mizan/base — pluggable transport:
* Adds MizanTransport interface + transport config field.
* Existing fetch-based body factored into httpTransport() (default).
* mizanCall/mizanFetch delegate to config.transport; merge/invalidate
  side-effects stay in the kernel (transport-agnostic).
* Consumers swap via configure({ transport: tauriTransport() }).

mizan-codegen — Rust source + Pydantic pre-step:
* [source.rust] runs a Cargo bin (cargo run --bin <name>) and parses
  KDL from stdout. The bin uses mizan_core::build_ir() after
  force-linking the consumer's #[derive(Mizan)] / #[mizan::client]
  registrations.
* [source.rust.pydantic] is an optional pre-step that pipes an
  embedded Python bridge (scripts/run_decoru.py) to python and writes
  decoru-emitted Rust types into the consumer crate. The bridge
  auto-discovers BaseModel subclasses AND Enum subclasses
  (last-variant-is-default convention so decoru's impl Default keeps
  compiling against enum-typed fields without explicit Pydantic
  defaults).
* Pure-Rust usage stays intact — omit pydantic block and write Rust
  types by hand.

mizan-macros:
* #[mizan::client] now supports Result<T, MizanError> returns. The
  dispatch wrapper `?`-unwraps the user fn so server-side errors
  surface as the protocol's standard {code, message, details?}
  envelope; T-returning functions stay unchanged.
* #[derive(Mizan)] strips the r# raw-identifier prefix and honors
  field-level #[serde(rename = "...")] when emitting IR field names.
  Matches serde's wire shape — fixes IR-vs-JSON drift for Rust-keyword
  fields (e.g. `r#type` → `type`).

react.tsx template:
* Conditionally emits context-related imports / useContextSubscription
  helper based on has_global || !named_contexts.is_empty(). Consumers
  without contexts (mutation/RPC-only apps like claude-manage) no
  longer get dead imports that trip noUnusedLocals.

Verified end-to-end: cargo build clean across mizan-tauri,
mizan-codegen, AFI rust_app; AFI three-way KDL parity tests pass;
claude-manage migration drives the full stack (Pydantic schema →
generated TS api → Tauri-IPC transport → mizan-core dispatch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-19 19:01:45 -04:00
parent 54f060c273
commit 22dcf0e3c1
13 changed files with 5478 additions and 39 deletions

View File

@@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""Pydantic → Rust codegen helper invoked by mizan-codegen's
`[source.rust.pydantic]` step.
Reads a JSON payload from argv[1] with keys:
- module: Python module to import (e.g. "claude_manage.schema")
- output: Path to write the generated Rust file
- derives: List of derive identifiers to apply to every emitted item
- header: Optional file prefix (e.g. an AUTO-GENERATED warning)
Discovers every BaseModel subclass declared in the module (handled by
decoru) AND every Enum subclass declared there (handled inline — decoru
itself is scoped to BaseModel). Writes one Rust file containing both.
Bundled with the mizan-codegen binary (include_str!) and piped to
`python -` at codegen time — no install step beyond decoru being
importable in the python environment.
"""
import importlib
import inspect
import json
import sys
import textwrap
from enum import Enum
from pathlib import Path
from pydantic import BaseModel # type: ignore[import-untyped]
from decoru import emit_rust_struct, walk_pydantic_model # type: ignore[import-untyped]
def _declared_in(module, obj) -> bool:
return getattr(obj, "__module__", None) == module.__name__
def discover_models(module) -> list[type[BaseModel]]:
"""BaseModel subclasses declared in this module. Imported helpers are
skipped — only own-module declarations qualify."""
return [
obj
for _, obj in inspect.getmembers(module, inspect.isclass)
if issubclass(obj, BaseModel)
and obj is not BaseModel
and _declared_in(module, obj)
]
def discover_enums(module) -> list[type[Enum]]:
"""Enum subclasses declared in this module. Filters out the
framework's own Enum class and anything imported from elsewhere."""
return [
obj
for _, obj in inspect.getmembers(module, inspect.isclass)
if issubclass(obj, Enum) and obj is not Enum and _declared_in(module, obj)
]
def _pascal_case(s: str) -> str:
return "".join(part.capitalize() for part in s.split("_") if part)
# Last-variant-is-default matches the catch-all idiom (e.g. `Metadata`
# in `claude_manage.schema.EntryType`). Decoru's `emit_rust_struct`
# emits `impl Default` unconditionally on every BaseModel, so any
# enum-typed field that lacks a Pydantic default must still satisfy
# `EntryType::default()`. Forcing #[default] on the last member keeps
# the generated structs compilable without per-enum config.
_ENUM_TEMPLATE = textwrap.dedent("""\
#[derive({derives})]
#[serde(rename_all = "snake_case")]
pub enum {name} {{
{variants}
}}
""")
def _render_variant(member: Enum, *, is_default: bool) -> str:
pascal = _pascal_case(member.name)
default_attr = " #[default]\n" if is_default else ""
return f"{default_attr} {pascal},"
def emit_rust_enum(enum_class: type[Enum], derives: tuple[str, ...]) -> str:
"""Render a Rust enum with PascalCase variants from Python member
names. Pairs `#[serde(rename_all = "snake_case")]` so the wire form
matches each member's `value`. Adds `Default` to the derives and
marks the last member `#[default]` — see `_ENUM_TEMPLATE` for the
rationale."""
name = enum_class.__name__
members = list(enum_class)
full_derives = ", ".join((*derives, "Default"))
variants = "\n".join(
_render_variant(m, is_default=(i == len(members) - 1))
for i, m in enumerate(members)
)
return _ENUM_TEMPLATE.format(derives=full_derives, name=name, variants=variants)
def main() -> int:
if len(sys.argv) < 2:
sys.stderr.write("run_decoru.py: missing JSON payload argument\n")
return 2
payload = json.loads(sys.argv[1])
module_name: str = payload["module"]
output_path = Path(payload["output"]).resolve()
derives = tuple(payload.get("derives", ()))
header = payload.get("header") or ""
sys.path.insert(0, str(Path.cwd()))
module = importlib.import_module(module_name)
enums = discover_enums(module)
models = discover_models(module)
if not enums and not models:
sys.stderr.write(
f"run_decoru.py: no Enum or BaseModel subclasses declared in {module_name!r}\n"
)
return 3
enum_blocks = [emit_rust_enum(e, derives) for e in enums]
struct_blocks = [emit_rust_struct(walk_pydantic_model(m), derives=derives) for m in models]
body = "\n".join((*enum_blocks, *struct_blocks))
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(header + body)
sys.stderr.write(
f"run_decoru.py: wrote {len(enums)} enum(s) + {len(models)} struct(s) to {output_path}\n"
)
return 0
if __name__ == "__main__":
sys.exit(main())