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:
136
protocol/mizan-codegen/scripts/run_decoru.py
Normal file
136
protocol/mizan-codegen/scripts/run_decoru.py
Normal 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())
|
||||
Reference in New Issue
Block a user