Mizan codegen substrate: Rust kernel + Rust codegen binary, JS generator deleted
The Mizan codegen substrate moves off JavaScript template-literal emission
onto a compiled Rust binary that consumes the same OpenAPI + x-mizan-* IR
the JS substrate consumed. Three structural wins fall out of one move:
1. Moat closes. The codegen logic (how `affects` becomes auto-invalidation,
how named contexts collapse onto bundled fetches, how the registry-to-
Provider mapping is shaped) ships compiled instead of as source bytes
in every consumer's node_modules.
2. Pattern F (lines.push append-walls) becomes structurally unauthorable.
The emit substrate is askama templates in templates/<target>/*.j2 —
actual target-language files with {{ ... }} substitution markers,
syntax-highlighted natively, type-checked against the render context
structs at compile time. The Rust emit modules build typed render
contexts and call .render(); no string-builder surface exists.
3. OpenAPI `default`-bearing fields now emit as non-optional in TS / Python
/ Rust — the server always populates them, so consumer code reads them
without nullable checks. Surfaced by Blazr's typecheck on regeneration.
Layout:
frontends/mizan-rust/ — Rust port of @mizan/base; #[cfg(feature="pyo3")]
exposes PyMizanClient for the Python target.
protocol/mizan-codegen/ — codegen binary source + askama templates.
protocol/mizan-generate/ — npm-package shim. bin/launcher.mjs dispatches
to the platform-appropriate prebuilt binary.
Old generator/ JS tree deleted.
tests/rust/ — wire-parity drivers. drive_kernel exercises
raw client.call() / fetch_context(); drive_emitted
exercises the typed crate the codegen emits.
tests/afi/afi_codegen_app.py — codegen entrypoint module (imports + registers).
backends/mizan-fastapi/.../schema.py — adds outputNullable so the Rust
codegen can wrap T | None responses in Option<T>.
Verification:
- 20 mizan-codegen tests green (IR deserialization, byte-equivalent
parity vs JS baseline for stage1/rust/python/react/vue/svelte,
structural test for channels).
- tests/rust/run_wire_parity.py — 12/12 probes green via the Rust binary
driving the FastAPI fixture end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
12
tests/afi/afi_codegen_app.py
Normal file
12
tests/afi/afi_codegen_app.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
Codegen entrypoint for the AFI fixture.
|
||||
|
||||
`mizan_fastapi.cli` imports a module and runs `build_schema()` from a
|
||||
populated registry. The fixture's `register_fixture()` is a function
|
||||
call, not an import side effect; this thin wrapper invokes it on
|
||||
import so the CLI works without modifying fixture.py's semantics.
|
||||
"""
|
||||
|
||||
from fixture import register_fixture
|
||||
|
||||
register_fixture()
|
||||
1
tests/rust/.gitignore
vendored
Normal file
1
tests/rust/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
target/
|
||||
1595
tests/rust/Cargo.lock
generated
Normal file
1595
tests/rust/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
tests/rust/Cargo.toml
Normal file
20
tests/rust/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "mizan-rust-wire-parity"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
mizan-rust = { path = "../../frontends/mizan-rust" }
|
||||
fixture_client = { path = "./fixture_client" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde_json = "1"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
||||
|
||||
[[bin]]
|
||||
name = "drive_kernel"
|
||||
path = "src/drive_kernel.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "drive_emitted"
|
||||
path = "src/drive_emitted.rs"
|
||||
10
tests/rust/fixture_client/Cargo.toml
Normal file
10
tests/rust/fixture_client/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "fixture_client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
mizan-rust = { path = "../../../frontends/mizan-rust" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["rt", "macros"] }
|
||||
3
tests/rust/fixture_client/src/contexts/mod.rs
Normal file
3
tests/rust/fixture_client/src/contexts/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
pub mod user;
|
||||
29
tests/rust/fixture_client/src/contexts/user.rs
Normal file
29
tests/rust/fixture_client/src/contexts/user.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use mizan_rust::{MizanClient, MizanError};
|
||||
|
||||
use crate::types::{UserProfileOutput, UserOrdersOutput};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserContextData {
|
||||
pub user_profile: UserProfileOutput,
|
||||
pub user_orders: UserOrdersOutput,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserContextParams {
|
||||
pub user_id: i64,
|
||||
}
|
||||
|
||||
pub async fn fetch_user_context(
|
||||
client: &MizanClient,
|
||||
params: &UserContextParams,
|
||||
) -> Result<UserContextData, MizanError> {
|
||||
let params_value = serde_json::to_value(params).unwrap_or(Value::Object(Default::default()));
|
||||
let raw = client.fetch_context("user", ¶ms_value).await?;
|
||||
serde_json::from_value(raw)
|
||||
.map_err(|e| MizanError::transport(format!("decode user context: {e}")))
|
||||
}
|
||||
14
tests/rust/fixture_client/src/functions/echo.rs
Normal file
14
tests/rust/fixture_client/src/functions/echo.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use mizan_rust::{MizanClient, MizanError};
|
||||
|
||||
use crate::types::{EchoOutput, EchoInput};
|
||||
|
||||
pub async fn call_echo(client: &MizanClient, args: &EchoInput) -> Result<EchoOutput, MizanError> {
|
||||
let args_value = serde_json::to_value(args).unwrap_or(Value::Object(Default::default()));
|
||||
let raw = client.call("echo", args_value).await?;
|
||||
serde_json::from_value(raw)
|
||||
.map_err(|e| MizanError::transport(format!("decode echo result: {e}")))
|
||||
}
|
||||
14
tests/rust/fixture_client/src/functions/find_user.rs
Normal file
14
tests/rust/fixture_client/src/functions/find_user.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use mizan_rust::{MizanClient, MizanError};
|
||||
|
||||
use crate::types::{FindUserOutput, FindUserInput};
|
||||
|
||||
pub async fn call_find_user(client: &MizanClient, args: &FindUserInput) -> Result<Option<FindUserOutput>, MizanError> {
|
||||
let args_value = serde_json::to_value(args).unwrap_or(Value::Object(Default::default()));
|
||||
let raw = client.call("find_user", args_value).await?;
|
||||
serde_json::from_value(raw)
|
||||
.map_err(|e| MizanError::transport(format!("decode find_user result: {e}")))
|
||||
}
|
||||
6
tests/rust/fixture_client/src/functions/mod.rs
Normal file
6
tests/rust/fixture_client/src/functions/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
pub mod echo;
|
||||
pub mod find_user;
|
||||
pub mod rename_user;
|
||||
pub mod whoami;
|
||||
14
tests/rust/fixture_client/src/functions/rename_user.rs
Normal file
14
tests/rust/fixture_client/src/functions/rename_user.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use mizan_rust::{MizanClient, MizanError};
|
||||
|
||||
use crate::types::{RenameUserOutput, RenameUserInput};
|
||||
|
||||
pub async fn call_rename_user(client: &MizanClient, args: &RenameUserInput) -> Result<RenameUserOutput, MizanError> {
|
||||
let args_value = serde_json::to_value(args).unwrap_or(Value::Object(Default::default()));
|
||||
let raw = client.call("rename_user", args_value).await?;
|
||||
serde_json::from_value(raw)
|
||||
.map_err(|e| MizanError::transport(format!("decode rename_user result: {e}")))
|
||||
}
|
||||
14
tests/rust/fixture_client/src/functions/whoami.rs
Normal file
14
tests/rust/fixture_client/src/functions/whoami.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use mizan_rust::{MizanClient, MizanError};
|
||||
|
||||
use crate::types::{WhoamiOutput};
|
||||
|
||||
pub async fn call_whoami(client: &MizanClient) -> Result<WhoamiOutput, MizanError> {
|
||||
let args_value = Value::Object(Default::default());
|
||||
let raw = client.call("whoami", args_value).await?;
|
||||
serde_json::from_value(raw)
|
||||
.map_err(|e| MizanError::transport(format!("decode whoami result: {e}")))
|
||||
}
|
||||
8
tests/rust/fixture_client/src/lib.rs
Normal file
8
tests/rust/fixture_client/src/lib.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
pub mod types;
|
||||
pub mod contexts;
|
||||
pub mod mutations;
|
||||
pub mod functions;
|
||||
|
||||
pub use mizan_rust::{MizanClient, MizanConfig, MizanError};
|
||||
3
tests/rust/fixture_client/src/mutations/mod.rs
Normal file
3
tests/rust/fixture_client/src/mutations/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
pub mod update_profile;
|
||||
14
tests/rust/fixture_client/src/mutations/update_profile.rs
Normal file
14
tests/rust/fixture_client/src/mutations/update_profile.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use mizan_rust::{MizanClient, MizanError};
|
||||
|
||||
use crate::types::{UpdateProfileOutput, UpdateProfileInput};
|
||||
|
||||
pub async fn call_update_profile(client: &MizanClient, args: &UpdateProfileInput) -> Result<UpdateProfileOutput, MizanError> {
|
||||
let args_value = serde_json::to_value(args).unwrap_or(Value::Object(Default::default()));
|
||||
let raw = client.call("update_profile", args_value).await?;
|
||||
serde_json::from_value(raw)
|
||||
.map_err(|e| MizanError::transport(format!("decode update_profile result: {e}")))
|
||||
}
|
||||
98
tests/rust/fixture_client/src/types.rs
Normal file
98
tests/rust/fixture_client/src/types.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
#![allow(non_camel_case_types)]
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HTTPValidationError {
|
||||
pub detail: Option<Vec<ValidationError>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OrderOutput {
|
||||
pub id: i64,
|
||||
pub user_id: i64,
|
||||
pub total: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ValidationError {
|
||||
pub loc: Vec<serde_json::Value>,
|
||||
pub msg: String,
|
||||
#[serde(rename = "type")]
|
||||
pub r#type: String,
|
||||
pub input: Option<serde_json::Value>,
|
||||
pub ctx: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EchoInput {
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EchoOutput {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FindUserInput {
|
||||
pub user_id: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FindUserOutput {
|
||||
pub user_id: i64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RenameUserInput {
|
||||
pub user_id: i64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RenameUserOutput {
|
||||
pub user_id: i64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateProfileInput {
|
||||
pub user_id: i64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateProfileOutput {
|
||||
pub ok: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserOrdersInput {
|
||||
pub user_id: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct UserOrdersOutput(pub Vec<OrderOutput>);
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserProfileInput {
|
||||
pub user_id: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserProfileOutput {
|
||||
pub user_id: i64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WhoamiOutput {
|
||||
pub email: String,
|
||||
pub authenticated: bool,
|
||||
}
|
||||
|
||||
11
tests/rust/mizan.toml
Normal file
11
tests/rust/mizan.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
output = "fixture_client"
|
||||
targets = ["rust"]
|
||||
rust_crate_name = "fixture_client"
|
||||
|
||||
[source.fastapi]
|
||||
module = "afi_codegen_app"
|
||||
cwd = "../afi"
|
||||
command = ["uv", "run", "python"]
|
||||
|
||||
[rust_kernel]
|
||||
path = "../../../frontends/mizan-rust"
|
||||
40
tests/rust/regen_fixture_client.py
Normal file
40
tests/rust/regen_fixture_client.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Regenerate the wire-parity fixture_client crate via the Rust codegen binary.
|
||||
|
||||
Drives the Rust `mizan-generate` binary against `tests/rust/mizan.toml`,
|
||||
which points at the AFI fixture's FastAPI registration module. Output
|
||||
lands under `tests/rust/fixture_client/` and is consumed by both
|
||||
`drive_emitted` (typed-emitted-crate probes) and `drive_kernel`
|
||||
(raw-kernel probes) via the parent `Cargo.toml` workspace.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
REPO_ROOT = HERE.parents[1]
|
||||
BINARY = REPO_ROOT / "protocol/mizan-codegen/target/release/mizan-generate"
|
||||
CONFIG = HERE / "mizan.toml"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not BINARY.exists():
|
||||
sys.stderr.write(
|
||||
f"[regen] binary missing: {BINARY}\n"
|
||||
"[regen] build it: cargo build --release "
|
||||
"--manifest-path protocol/mizan-codegen/Cargo.toml\n"
|
||||
)
|
||||
return 1
|
||||
|
||||
result = subprocess.run(
|
||||
[str(BINARY), "--config", str(CONFIG)],
|
||||
cwd=HERE,
|
||||
)
|
||||
return result.returncode
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
103
tests/rust/run_wire_parity.py
Normal file
103
tests/rust/run_wire_parity.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Drive the wire-parity check end-to-end.
|
||||
|
||||
1. Boot the FastAPI fixture app via uvicorn on a free port.
|
||||
2. Poll /openapi.json until the server is up.
|
||||
3. Run the Rust `drive_kernel` binary (raw kernel calls) against it.
|
||||
4. Run the Rust `drive_emitted` binary (typed codegen functions) against
|
||||
the same server.
|
||||
5. Tear the server down.
|
||||
|
||||
Either non-zero driver exit propagates as the script's exit code.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
AFI_DIR = REPO_ROOT / "tests" / "afi"
|
||||
RUST_DIR = REPO_ROOT / "tests" / "rust"
|
||||
BOOT_TIMEOUT_S = 15.0
|
||||
POLL_INTERVAL_S = 0.25
|
||||
|
||||
|
||||
def pick_free_port() -> int:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind(("127.0.0.1", 0))
|
||||
return s.getsockname()[1]
|
||||
|
||||
|
||||
def wait_for_server(port: int, timeout_s: float) -> bool:
|
||||
deadline = time.monotonic() + timeout_s
|
||||
url = f"http://127.0.0.1:{port}/openapi.json"
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=1.0) as resp:
|
||||
if resp.status == 200:
|
||||
return True
|
||||
except (urllib.error.URLError, ConnectionError, OSError) as e:
|
||||
# Surface the kind of failure so a stuck boot doesn't read
|
||||
# as "silently waiting"; the loop continues until timeout.
|
||||
sys.stderr.write(f"[wire_parity] waiting for server: {type(e).__name__}\n")
|
||||
time.sleep(POLL_INTERVAL_S)
|
||||
return False
|
||||
|
||||
|
||||
def run_driver(name: str, base_url: str) -> int:
|
||||
sys.stdout.write(f"\n=== {name} ===\n")
|
||||
sys.stdout.flush()
|
||||
return subprocess.run(
|
||||
["cargo", "run", "--quiet", "--bin", name, "--", base_url],
|
||||
cwd=RUST_DIR,
|
||||
).returncode
|
||||
|
||||
|
||||
def main() -> int:
|
||||
port = pick_free_port()
|
||||
base_url = f"http://127.0.0.1:{port}/api/mizan"
|
||||
|
||||
server = subprocess.Popen(
|
||||
["uv", "run", "uvicorn", "fastapi_app:make_app",
|
||||
"--factory", "--port", str(port), "--log-level", "warning"],
|
||||
cwd=AFI_DIR,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
|
||||
try:
|
||||
if not wait_for_server(port, BOOT_TIMEOUT_S):
|
||||
sys.stderr.write(
|
||||
f"[wire_parity] server failed to start within {BOOT_TIMEOUT_S}s\n",
|
||||
)
|
||||
stderr_tail = server.stderr.read(4096) if server.stderr else b""
|
||||
if stderr_tail:
|
||||
sys.stderr.write(stderr_tail.decode("utf-8", errors="replace"))
|
||||
return 1
|
||||
|
||||
failures = 0
|
||||
for driver in ("drive_kernel", "drive_emitted"):
|
||||
rc = run_driver(driver, base_url)
|
||||
if rc != 0:
|
||||
sys.stderr.write(f"[wire_parity] {driver} exited {rc}\n")
|
||||
failures += 1
|
||||
|
||||
return 0 if failures == 0 else 1
|
||||
finally:
|
||||
server.terminate()
|
||||
try:
|
||||
server.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired:
|
||||
server.kill()
|
||||
server.wait()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
75
tests/rust/src/drive_emitted.rs
Normal file
75
tests/rust/src/drive_emitted.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
//! Drive the codegen-emitted `fixture_client` crate against a live
|
||||
//! FastAPI fixture. Validates not just the kernel wire (which
|
||||
//! `drive_kernel.rs` already covers) but that the codegen actually
|
||||
//! produces typed-functions that round-trip cleanly through the same
|
||||
//! kernel.
|
||||
|
||||
use std::env;
|
||||
use std::process::ExitCode;
|
||||
|
||||
use mizan_rust::{MizanClient, MizanConfig};
|
||||
|
||||
use fixture_client::contexts::user::{fetch_user_context, UserContextParams};
|
||||
use fixture_client::functions::echo::call_echo;
|
||||
use fixture_client::functions::find_user::call_find_user;
|
||||
use fixture_client::functions::rename_user::call_rename_user;
|
||||
use fixture_client::functions::whoami::call_whoami;
|
||||
use fixture_client::mutations::update_profile::call_update_profile;
|
||||
use fixture_client::types::{
|
||||
EchoInput, FindUserInput, RenameUserInput, UpdateProfileInput,
|
||||
};
|
||||
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> ExitCode {
|
||||
let base_url = env::args().nth(1)
|
||||
.unwrap_or_else(|| "http://127.0.0.1:8765/api/mizan".to_string());
|
||||
let client = MizanClient::new(MizanConfig {
|
||||
base_url,
|
||||
session: false,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let mut failures = 0usize;
|
||||
|
||||
match call_echo(&client, &EchoInput { text: "hello".to_string() }).await {
|
||||
Ok(out) => println!("call_echo -> message={:?}", out.message),
|
||||
Err(e) => { eprintln!("call_echo ERR {e}"); failures += 1; }
|
||||
}
|
||||
|
||||
match call_whoami(&client).await {
|
||||
Ok(out) => println!("call_whoami -> authenticated={} email={:?}", out.authenticated, out.email),
|
||||
Err(e) => { eprintln!("call_whoami ERR {e}"); failures += 1; }
|
||||
}
|
||||
|
||||
match call_find_user(&client, &FindUserInput { user_id: 99999 }).await {
|
||||
Ok(None) => println!("call_find_user(99999) -> None"),
|
||||
Ok(Some(out)) => println!("call_find_user(99999) -> user_id={} name={:?}", out.user_id, out.name),
|
||||
Err(e) => { eprintln!("call_find_user ERR {e}"); failures += 1; }
|
||||
}
|
||||
|
||||
match call_update_profile(&client, &UpdateProfileInput { user_id: 5, name: "Ryth".to_string() }).await {
|
||||
Ok(out) => println!("call_update_profile -> ok={}", out.ok),
|
||||
Err(e) => { eprintln!("call_update_profile ERR {e}"); failures += 1; }
|
||||
}
|
||||
|
||||
match call_rename_user(&client, &RenameUserInput { user_id: 5, name: "RythR".to_string() }).await {
|
||||
Ok(out) => println!("call_rename_user -> user_id={} name={:?}", out.user_id, out.name),
|
||||
Err(e) => { eprintln!("call_rename_user ERR {e}"); failures += 1; }
|
||||
}
|
||||
|
||||
match fetch_user_context(&client, &UserContextParams { user_id: 5 }).await {
|
||||
Ok(out) => println!(
|
||||
"fetch_user_context(5) -> user_profile={{ user_id:{}, name:{:?} }} user_orders.len={}",
|
||||
out.user_profile.user_id, out.user_profile.name, out.user_orders.0.len(),
|
||||
),
|
||||
Err(e) => { eprintln!("fetch_user_context ERR {e}"); failures += 1; }
|
||||
}
|
||||
|
||||
if failures > 0 {
|
||||
eprintln!("[drive_emitted] {failures} probe(s) failed");
|
||||
ExitCode::FAILURE
|
||||
} else {
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
}
|
||||
74
tests/rust/src/drive_kernel.rs
Normal file
74
tests/rust/src/drive_kernel.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
//! Drive the mizan-rust kernel against a live FastAPI fixture app and
|
||||
//! print every response. Used by `run_wire_parity.sh` which:
|
||||
//!
|
||||
//! 1. Boots `tests/afi/fastapi_app.py` via uvicorn on port 8765.
|
||||
//! 2. Polls `/openapi.json` until the server is up.
|
||||
//! 3. Runs `cargo run --bin drive_kernel -- http://127.0.0.1:8765/api/mizan`.
|
||||
//! 4. Diffs the stdout against a committed snapshot.
|
||||
//!
|
||||
//! The kernel exercises every endpoint the fixture declares: the two
|
||||
//! plain functions (`echo`, `whoami`), the two-function `user` context,
|
||||
//! the `update_profile` mutation, the `find_user` Optional path, and
|
||||
//! the `rename_user` merge mutation.
|
||||
|
||||
use std::env;
|
||||
use std::process::ExitCode;
|
||||
|
||||
use mizan_rust::{MizanClient, MizanConfig};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> ExitCode {
|
||||
let base_url = env::args().nth(1)
|
||||
.unwrap_or_else(|| "http://127.0.0.1:8765/api/mizan".to_string());
|
||||
let client = MizanClient::new(MizanConfig {
|
||||
base_url,
|
||||
session: false,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let mut failures = 0usize;
|
||||
failures += probe(&client, "echo", json!({"text": "hello"})).await;
|
||||
failures += probe(&client, "whoami", json!({})).await;
|
||||
failures += probe(&client, "find_user", json!({"user_id": 99999})).await;
|
||||
failures += probe(&client, "update_profile", json!({"user_id": 5, "name": "Ryth"})).await;
|
||||
failures += probe(&client, "rename_user", json!({"user_id": 5, "name": "RythR"})).await;
|
||||
|
||||
failures += probe_context(&client, "user", json!({"user_id": 5})).await;
|
||||
|
||||
if failures > 0 {
|
||||
eprintln!("[drive_kernel] {failures} probe(s) failed");
|
||||
ExitCode::FAILURE
|
||||
} else {
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async fn probe(client: &MizanClient, fn_name: &str, args: Value) -> usize {
|
||||
match client.call(fn_name, args.clone()).await {
|
||||
Ok(result) => {
|
||||
println!("call {fn_name} args={args} -> {result}");
|
||||
0
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("call {fn_name} args={args} -> ERR {err}");
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async fn probe_context(client: &MizanClient, name: &str, params: Value) -> usize {
|
||||
match client.fetch_context(name, ¶ms).await {
|
||||
Ok(data) => {
|
||||
println!("ctx {name} params={params} -> {data}");
|
||||
0
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("ctx {name} params={params} -> ERR {err}");
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user