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

@@ -62,6 +62,13 @@ pub struct SourceConfig {
#[serde(default)]
pub django: Option<DjangoSource>,
/// Canonical "Pydantic + Rust" DX path. The Rust crate is the IR
/// authority; an optional `pydantic` sub-block invokes decoru as a
/// pre-step to author Rust types from Pydantic models. Pure-Rust
/// usage (no Pydantic) just omits the sub-block.
#[serde(default)]
pub rust: Option<RustSource>,
}
@@ -98,6 +105,107 @@ pub struct DjangoSource {
}
/// `[source.rust]` — spawn a Cargo binary that emits the Mizan IR (KDL)
/// to stdout. The binary uses `mizan_core::build_ir()` after force-linking
/// the consumer crate's `#[derive(Mizan)]` types and `#[mizan::client]`
/// functions.
#[derive(Debug, Deserialize)]
pub struct RustSource {
/// Path to the consumer's Cargo.toml, relative to the codegen config
/// directory. Defaults to `Cargo.toml` (i.e. config_dir/Cargo.toml).
#[serde(default)]
pub manifest_path: Option<PathBuf>,
/// Name of the binary under `[[bin]]` that exports the IR. Defaults
/// to `emit-mizan-ir` — the convention this substrate documents.
#[serde(default = "default_rust_bin")]
pub bin: String,
/// Cargo features to enable when building the bin.
#[serde(default)]
pub features: Vec<String>,
/// Build in release mode. Defaults to false (dev mode is faster for
/// codegen, and the binary is throwaway).
#[serde(default)]
pub release: bool,
/// Environment overrides for the cargo subprocess.
#[serde(default)]
pub env: BTreeMap<String, String>,
/// Optional pre-step — invoke decoru on a Pydantic source module
/// before running the Cargo bin. When present, the pipeline becomes:
/// 1. python + decoru → write Rust types to `pydantic.output`
/// 2. cargo run --bin <bin> → emit IR to stdout
/// Omit for pure-Rust usage (hand-authored or otherwise-generated
/// Rust types with `#[derive(Mizan)]`).
#[serde(default)]
pub pydantic: Option<PydanticPreStep>,
}
fn default_rust_bin() -> String {
"emit-mizan-ir".to_string()
}
/// Pydantic → Rust pre-step. Runs an embedded Python helper that walks
/// the named module for `BaseModel` subclasses and invokes decoru's
/// `walk_pydantic_model` + `emit_rust_struct` to produce a Rust file.
#[derive(Debug, Deserialize)]
pub struct PydanticPreStep {
/// Python module name to import (e.g. `claude_manage.schema`).
pub module: String,
/// Path to write the generated Rust file, relative to the codegen
/// config directory.
pub output: PathBuf,
/// Working directory for the python subprocess, relative to the
/// codegen config directory. Defaults to the config directory itself.
/// The script prepends this to `sys.path` so the module imports.
#[serde(default)]
pub cwd: Option<PathBuf>,
/// Python executable. Defaults to `python`.
#[serde(default)]
pub python: Option<String>,
/// Full command override (e.g. `["uv", "run", "python"]`). Wins over
/// `python` when present.
#[serde(default)]
pub command: Option<Vec<String>>,
/// Derive macros to apply to every generated struct. The default
/// matches the Mizan-canonical set used in `cores/rust/blazr/session`
/// — serde + mizan_core::Mizan for end-to-end RPC participation.
#[serde(default = "default_pydantic_derives")]
pub derives: Vec<String>,
/// Optional prelude inserted at the top of the generated file
/// (typically a "// AUTO-GENERATED" warning + `use` statements for
/// referenced types not produced by decoru itself).
#[serde(default)]
pub header: Option<String>,
/// Environment overrides for the python subprocess.
#[serde(default)]
pub env: BTreeMap<String, String>,
}
fn default_pydantic_derives() -> Vec<String> {
vec![
"Debug".to_string(),
"Clone".to_string(),
"::serde::Serialize".to_string(),
"::serde::Deserialize".to_string(),
"::mizan_core::Mizan".to_string(),
]
}
#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
pub enum RustKernelSpec {

View File

@@ -4,24 +4,48 @@
//! Backends:
//! - FastAPI: `python -m mizan_fastapi.ir <module>`
//! - Django: `python manage.py export_mizan_ir`
//! - Rust: `cargo run --bin <bin>` (consumer-side binary that
//! force-links its `#[derive(Mizan)]` types and
//! `#[mizan::client]` functions, then calls
//! `mizan_core::build_ir()`).
//!
//! The Rust source supports an optional `[source.rust.pydantic]`
//! pre-step that invokes decoru on a Pydantic module to author the
//! Rust types before the cargo bin runs — the "Pydantic + Rust"
//! canonical DX.
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::process::{Command, Stdio};
use anyhow::{anyhow, Context, Result};
use serde_json::json;
use crate::config::{Config, DjangoSource, FastapiSource};
use crate::config::{Config, DjangoSource, FastapiSource, PydanticPreStep, RustSource};
use crate::ir::{parse_ir, MizanIR};
/// Embedded decoru bridge script — piped to `python -` at codegen time
/// when `[source.rust.pydantic]` is set. The script imports decoru,
/// walks the named module's BaseModel subclasses, and writes a Rust
/// file. See `scripts/run_decoru.py` for the full body.
const DECORU_BRIDGE_SCRIPT: &str = include_str!("../scripts/run_decoru.py");
pub fn fetch_schema(config: &Config, config_dir: &Path) -> Result<MizanIR> {
let raw = if let Some(fa) = &config.source.fastapi {
let raw = if let Some(rs) = &config.source.rust {
if let Some(py) = &rs.pydantic {
run_pydantic_prestep(py, config_dir)
.context("running [source.rust.pydantic] pre-step")?;
}
run_rust(rs, config_dir)?
} else if let Some(fa) = &config.source.fastapi {
run_fastapi(fa, config_dir)?
} else if let Some(dj) = &config.source.django {
run_django(dj, config_dir)?
} else {
return Err(anyhow!(
"config.source must declare either [source.fastapi] or [source.django]"
"config.source must declare one of [source.rust], [source.fastapi], or [source.django]"
));
};
@@ -111,6 +135,96 @@ fn run_subprocess(
}
fn run_rust(src: &RustSource, config_dir: &Path) -> Result<String> {
let manifest = config_dir.join(
src.manifest_path
.clone()
.unwrap_or_else(|| PathBuf::from("Cargo.toml")),
);
let mut args: Vec<String> = vec![
"run".to_string(),
"--quiet".to_string(),
"--manifest-path".to_string(),
manifest.to_string_lossy().into_owned(),
"--bin".to_string(),
src.bin.clone(),
];
if src.release {
args.push("--release".to_string());
}
if !src.features.is_empty() {
args.push("--features".to_string());
args.push(src.features.join(","));
}
run_subprocess("cargo", &args, config_dir, &src.env, "Rust IR export")
}
fn run_pydantic_prestep(src: &PydanticPreStep, config_dir: &Path) -> Result<()> {
let cwd = match &src.cwd {
Some(rel) => config_dir.join(rel),
None => config_dir.to_path_buf(),
};
let output_abs = if src.output.is_absolute() {
src.output.clone()
} else {
config_dir.join(&src.output)
};
let payload = json!({
"module": &src.module,
"output": output_abs.to_string_lossy(),
"derives": &src.derives,
"header": &src.header,
})
.to_string();
let (program, mut args) = resolve_command(&src.command, &src.python);
// `python -` reads the script body from stdin; the JSON payload is
// passed as argv[1] (which lands on sys.argv[1] inside the script).
args.push("-".to_string());
args.push(payload);
let mut cmd = Command::new(&program);
cmd.args(&args).current_dir(&cwd);
for (k, v) in &src.env {
cmd.env(k, v);
}
cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::inherit());
cmd.stderr(Stdio::inherit());
let mut child = cmd
.spawn()
.with_context(|| format!("spawning decoru bridge ({program})"))?;
{
let stdin = child
.stdin
.as_mut()
.ok_or_else(|| anyhow!("failed to acquire decoru bridge stdin"))?;
stdin
.write_all(DECORU_BRIDGE_SCRIPT.as_bytes())
.context("piping decoru bridge script to python")?;
}
let status = child
.wait()
.context("waiting for decoru bridge to complete")?;
if !status.success() {
return Err(anyhow!(
"[source.rust.pydantic]: decoru bridge exited with status {:?}",
status.code()
));
}
Ok(())
}
/// Library helper: parse a KDL IR from a string.
pub fn parse_ir_from_str(source: &str) -> Result<MizanIR> {
parse_ir(source)