Adds first-class Rust-backed Mizan to sit alongside mizan-django and
mizan-fastapi. A Rust dev writes:
#[derive(Mizan, Serialize, Deserialize)]
pub struct ProfileOutput { pub user_id: i64, pub name: String }
#[mizan::context("user")]
pub struct UserCtx;
#[mizan::client(context = UserCtx)]
pub async fn user_profile(_req: &RequestHandle<'_>, user_id: i64) -> ProfileOutput { ... }
…and gets byte-identical KDL to the Python emitters, served over the
same wire protocol the React / Rust / Vue / Svelte kernels speak.
New crates:
- cores/mizan-rust/ (Cargo: mizan-core) — IR types, KDL emitter, traits, registry,
runtime (compute_invalidation / compute_merges
ported from mizan-fastapi), graph_check with
structural type-matching
- cores/mizan-rust-macros/ (Cargo: mizan-macros) — #[derive(Mizan)], #[mizan::context],
#[mizan::client] proc macros
- backends/mizan-rust-axum/ (Cargo: mizan-axum) — axum HTTP adapter: /session/, /call/, /ctx/:name/
- tests/afi/rust_app/ — AFI fixture port + server / export-ir binaries
Substrate-shape moves required by cross-language equivalence:
- IR canonicalization: functions / contexts / context-members / shared-by
now sort alphabetically in both Python and Rust emitters. The IR is a
contract; linkme doesn't preserve declaration order, so canonical sort
is the only stable mapping. afi_ir.kdl + per-target baselines regenerated.
- MizanType::TYPE_NAME is a const (with a default type_name() reader) so
it's usable in linkme TypeEntry static initializers.
- Tree-shaken type registry: #[derive(Mizan)] only emits the trait impl;
the #[mizan::client] macro registers canonical-named entries from
fn signatures, including Vec<T> element types for ref resolution.
- Merge resolution is structural (NamedType shape comparison) rather than
by name — matches the Python types_match_for_merge semantics.
Three-way forcing functions:
- tests/afi/test_codegen_parity.py — Django ≡ FastAPI ≡ Rust on KDL bytes (3 pass)
- tests/rust/run_wire_parity.py — 12/12 probes against FastAPI + Rust (EXIT=0)
Incidental fixes surfaced by the new tests:
- Stale `from .registry import validate_registry` import removed from
mizan-django/setup/discovery.py (referenced a function that no longer
exists; was masking codegen-parity).
- BASE_DIR added to tests/afi/django_app/project/settings.py.
- /session/ endpoint added to mizan-fastapi for protocol-shaped readiness
probe parity (wire-parity harness now polls /api/mizan/session/ on both
backends rather than FastAPI's /openapi.json).
- Root .gitignore picks up Rust target/ across the tree so new crates
don't need per-crate gitignore.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
130 lines
3.6 KiB
Rust
130 lines
3.6 KiB
Rust
//! Byte-equivalence: the Rust KDL emitter (driven by the proc macros)
|
|
//! against `protocol/mizan-codegen/tests/fixtures/afi_ir.kdl` (canonical
|
|
//! Python-emitted reference).
|
|
//!
|
|
//! This is the Phase-2 verifier — the AFI fixture is authored against the
|
|
//! real consumer surface (`#[derive(Mizan)] / #[mizan::context] /
|
|
//! #[mizan::client]`), not hand-built static specs.
|
|
|
|
use mizan_core as mizan;
|
|
use mizan_core::prelude::*;
|
|
use mizan_core::RequestHandle;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::path::PathBuf;
|
|
|
|
// ─── Output / shared types ──────────────────────────────────────────────────
|
|
|
|
#[derive(Mizan, Serialize, Deserialize, Debug, Clone)]
|
|
pub struct EchoOutput {
|
|
pub message: String,
|
|
}
|
|
|
|
#[derive(Mizan, Serialize, Deserialize, Debug, Clone)]
|
|
pub struct WhoamiOutput {
|
|
pub email: String,
|
|
pub authenticated: bool,
|
|
}
|
|
|
|
#[derive(Mizan, Serialize, Deserialize, Debug, Clone)]
|
|
pub struct ProfileOutput {
|
|
pub user_id: i64,
|
|
pub name: String,
|
|
}
|
|
|
|
#[derive(Mizan, Serialize, Deserialize, Debug, Clone)]
|
|
pub struct OrderOutput {
|
|
pub id: i64,
|
|
pub user_id: i64,
|
|
pub total: i64,
|
|
}
|
|
|
|
#[derive(Mizan, Serialize, Deserialize, Debug, Clone)]
|
|
pub struct StatusOutput {
|
|
pub ok: bool,
|
|
}
|
|
|
|
#[mizan::context("user")]
|
|
pub struct UserCtx;
|
|
|
|
// ─── Fixture functions (mirroring tests/afi/fixture.py) ────────────────────
|
|
|
|
#[mizan::client]
|
|
pub async fn echo(_req: &RequestHandle<'_>, text: String) -> EchoOutput {
|
|
EchoOutput {
|
|
message: format!("echo: {text}"),
|
|
}
|
|
}
|
|
|
|
#[mizan::client]
|
|
pub async fn whoami(_req: &RequestHandle<'_>) -> WhoamiOutput {
|
|
WhoamiOutput {
|
|
email: "anon@example.com".into(),
|
|
authenticated: false,
|
|
}
|
|
}
|
|
|
|
#[mizan::client(context = UserCtx)]
|
|
pub async fn user_profile(_req: &RequestHandle<'_>, user_id: i64) -> ProfileOutput {
|
|
ProfileOutput {
|
|
user_id,
|
|
name: "placeholder".into(),
|
|
}
|
|
}
|
|
|
|
#[mizan::client(context = UserCtx)]
|
|
pub async fn user_orders(_req: &RequestHandle<'_>, _user_id: i64) -> Vec<OrderOutput> {
|
|
vec![]
|
|
}
|
|
|
|
#[mizan::client(affects = UserCtx)]
|
|
pub async fn update_profile(
|
|
_req: &RequestHandle<'_>,
|
|
_user_id: i64,
|
|
_name: String,
|
|
) -> StatusOutput {
|
|
StatusOutput { ok: true }
|
|
}
|
|
|
|
#[mizan::client]
|
|
pub async fn find_user(_req: &RequestHandle<'_>, _user_id: i64) -> Option<ProfileOutput> {
|
|
None
|
|
}
|
|
|
|
#[mizan::client(merge = UserCtx)]
|
|
pub async fn rename_user(
|
|
_req: &RequestHandle<'_>,
|
|
user_id: i64,
|
|
name: String,
|
|
) -> ProfileOutput {
|
|
ProfileOutput { user_id, name }
|
|
}
|
|
|
|
// ─── The byte-equivalence test ──────────────────────────────────────────────
|
|
|
|
fn canonical_kdl_path() -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("../../protocol/mizan-codegen/tests/fixtures/afi_ir.kdl")
|
|
}
|
|
|
|
#[test]
|
|
fn build_ir_matches_canonical_afi_kdl() {
|
|
let expected = std::fs::read_to_string(canonical_kdl_path()).expect("read canonical KDL");
|
|
let actual = mizan_core::build_ir();
|
|
|
|
if actual != expected {
|
|
for (lineno, (a, b)) in actual.lines().zip(expected.lines()).enumerate() {
|
|
if a != b {
|
|
panic!(
|
|
"KDL diverges at line {}:\n expected: {b:?}\n actual: {a:?}",
|
|
lineno + 1,
|
|
);
|
|
}
|
|
}
|
|
panic!(
|
|
"KDL diverges in length: actual_len={} expected_len={}",
|
|
actual.len(),
|
|
expected.len(),
|
|
);
|
|
}
|
|
}
|