Mizan-Rust backend adapter: server-side substrate + three-way parity
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>
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -11,6 +11,10 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
|
# Rust — every crate's build dir, anywhere in the tree
|
||||||
|
target/
|
||||||
|
**/target/
|
||||||
|
|
||||||
# Playwright
|
# Playwright
|
||||||
/test-results/
|
/test-results/
|
||||||
/playwright-report/
|
/playwright-report/
|
||||||
|
|||||||
@@ -71,9 +71,6 @@ def mizan_clients(apps_root: str, layer: str = "clients") -> None:
|
|||||||
visitor = DjangoAppVisitor(layer=layer, apps_root=apps_root)
|
visitor = DjangoAppVisitor(layer=layer, apps_root=apps_root)
|
||||||
visitor.visit(_RegisterServerFunctions())
|
visitor.visit(_RegisterServerFunctions())
|
||||||
|
|
||||||
from .registry import validate_registry
|
|
||||||
validate_registry()
|
|
||||||
|
|
||||||
|
|
||||||
def mizan_module(module_path: str) -> None:
|
def mizan_module(module_path: str) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -43,6 +43,17 @@ def _no_store(payload: Any, status_code: int = 200) -> JSONResponse:
|
|||||||
# ─── Endpoints ──────────────────────────────────────────────────────────────
|
# ─── Endpoints ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/session/")
|
||||||
|
async def session_init() -> JSONResponse:
|
||||||
|
"""Session-init probe. Parity with mizan-django's session endpoint.
|
||||||
|
|
||||||
|
CSRF is a Django-only concern at the protocol level; FastAPI surfaces a
|
||||||
|
null token so the response shape stays uniform across backends. The
|
||||||
|
wire-parity harness uses this endpoint as its readiness probe.
|
||||||
|
"""
|
||||||
|
return _no_store({"csrfToken": None})
|
||||||
|
|
||||||
|
|
||||||
class CallBody(BaseModel):
|
class CallBody(BaseModel):
|
||||||
fn: str = Field(..., min_length=1)
|
fn: str = Field(..., min_length=1)
|
||||||
args: dict[str, Any] = Field(default_factory=dict)
|
args: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|||||||
591
backends/mizan-rust-axum/Cargo.lock
generated
Normal file
591
backends/mizan-rust-axum/Cargo.lock
generated
Normal file
@@ -0,0 +1,591 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-trait"
|
||||||
|
version = "0.1.89"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomic-waker"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum"
|
||||||
|
version = "0.7.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"axum-core",
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"itoa",
|
||||||
|
"matchit",
|
||||||
|
"memchr",
|
||||||
|
"mime",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustversion",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_path_to_error",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-core"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"mime",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustversion",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "form_urlencoded"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-channel"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"itoa",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-body"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-body-util"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httparse"
|
||||||
|
version = "1.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpdate"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"bytes",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"httparse",
|
||||||
|
"httpdate",
|
||||||
|
"itoa",
|
||||||
|
"pin-project-lite",
|
||||||
|
"smallvec",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-util"
|
||||||
|
version = "0.1.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"hyper",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.186"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linkme"
|
||||||
|
version = "0.3.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e83272d46373fb8decca684579ac3e7c8f3d71d4cc3aa693df8759e260ae41cf"
|
||||||
|
dependencies = [
|
||||||
|
"linkme-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linkme-impl"
|
||||||
|
version = "0.3.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32d59e20403c7d08fe62b4376edfe5c7fb2ef1e6b1465379686d0f21c8df444b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchit"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime"
|
||||||
|
version = "0.3.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mizan-axum"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"mizan-core",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"tower-http",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mizan-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"linkme",
|
||||||
|
"mizan-macros",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mizan-macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "percent-encoding"
|
||||||
|
version = "2.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.149"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_path_to_error"
|
||||||
|
version = "0.1.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_urlencoded"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.117"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sync_wrapper"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio"
|
||||||
|
version = "1.52.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"pin-project-lite",
|
||||||
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"pin-project-lite",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-http"
|
||||||
|
version = "0.6.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-layer"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-service"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing"
|
||||||
|
version = "0.1.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-core"
|
||||||
|
version = "0.1.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
15
backends/mizan-rust-axum/Cargo.toml
Normal file
15
backends/mizan-rust-axum/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "mizan-axum"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "axum HTTP adapter for Mizan — typed RPC dispatch + context-bundle fetch on top of mizan-core's compile-time function registry."
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
mizan-core = { path = "../../cores/mizan-rust" }
|
||||||
|
axum = "0.7"
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
tower = "0.5"
|
||||||
|
tower-http = { version = "0.6", features = ["trace"] }
|
||||||
27
backends/mizan-rust-axum/src/errors.rs
Normal file
27
backends/mizan-rust-axum/src/errors.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
//! Convert `MizanError` into axum's `Response`. Mirrors mizan-fastapi's
|
||||||
|
//! envelope: `{"error": {"code": "...", "message": "...", "details": ...}}`
|
||||||
|
//! with a Cache-Control: no-store header.
|
||||||
|
|
||||||
|
use axum::http::{header, HeaderValue, StatusCode};
|
||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
|
use axum::Json;
|
||||||
|
use mizan_core::MizanError;
|
||||||
|
|
||||||
|
pub struct ApiError(pub MizanError);
|
||||||
|
|
||||||
|
impl From<MizanError> for ApiError {
|
||||||
|
fn from(e: MizanError) -> Self {
|
||||||
|
Self(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for ApiError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let status = StatusCode::from_u16(self.0.http_status())
|
||||||
|
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
let mut resp = (status, Json(self.0.to_json())).into_response();
|
||||||
|
resp.headers_mut()
|
||||||
|
.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
|
||||||
|
resp
|
||||||
|
}
|
||||||
|
}
|
||||||
153
backends/mizan-rust-axum/src/handlers.rs
Normal file
153
backends/mizan-rust-axum/src/handlers.rs
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
//! HTTP handlers. Mirrors `backends/mizan-fastapi/src/mizan_fastapi/router.py`.
|
||||||
|
|
||||||
|
use axum::extract::{Path, Query};
|
||||||
|
use axum::http::{header, HeaderValue, StatusCode};
|
||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
|
use axum::Json;
|
||||||
|
use mizan_core::{
|
||||||
|
compute_invalidation, compute_merges, lookup_function, lookup_context, FunctionSpec,
|
||||||
|
InvalidationTarget, MergeEntry, MizanError, RequestHandle, FUNCTIONS,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::{Map, Value};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use crate::errors::ApiError;
|
||||||
|
|
||||||
|
/// Body for POST /call/. Matches the Python `CallBody` shape.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct CallBody {
|
||||||
|
pub fn_: Option<String>,
|
||||||
|
#[serde(rename = "fn")]
|
||||||
|
pub function_name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub args: Map<String, Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CallBody {
|
||||||
|
fn resolved_name(&self) -> Option<&str> {
|
||||||
|
self.function_name
|
||||||
|
.as_deref()
|
||||||
|
.or(self.fn_.as_deref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct CallResponse {
|
||||||
|
pub result: Value,
|
||||||
|
pub invalidate: Vec<Value>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub merge: Option<Vec<Value>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn no_store(json: Value) -> Response {
|
||||||
|
let mut resp = (StatusCode::OK, Json(json)).into_response();
|
||||||
|
resp.headers_mut()
|
||||||
|
.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
|
||||||
|
resp
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST /call/ — RPC dispatch.
|
||||||
|
pub async fn function_call(Json(body): Json<CallBody>) -> Result<Response, ApiError> {
|
||||||
|
let fn_name = body
|
||||||
|
.resolved_name()
|
||||||
|
.ok_or_else(|| ApiError(MizanError::BadRequest("missing `fn` field".into())))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let fn_spec = lookup_function(&fn_name)
|
||||||
|
.ok_or_else(|| ApiError(MizanError::NotFound(format!("function {fn_name:?} not registered"))))?;
|
||||||
|
|
||||||
|
let unit = ();
|
||||||
|
let req = RequestHandle::new(&unit);
|
||||||
|
let result = fn_spec.dispatch(req, Value::Object(body.args.clone())).await.map_err(ApiError)?;
|
||||||
|
|
||||||
|
let invalidate: Vec<Value> = compute_invalidation(fn_spec, &body.args)
|
||||||
|
.iter()
|
||||||
|
.map(InvalidationTarget::to_json)
|
||||||
|
.collect();
|
||||||
|
let merges = compute_merges(fn_spec, &body.args, &result);
|
||||||
|
let merge_payload: Option<Vec<Value>> = if merges.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(merges.iter().map(MergeEntry::to_json).collect())
|
||||||
|
};
|
||||||
|
|
||||||
|
let payload = CallResponse {
|
||||||
|
result,
|
||||||
|
invalidate,
|
||||||
|
merge: merge_payload,
|
||||||
|
};
|
||||||
|
Ok(no_store(serde_json::to_value(&payload).unwrap()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /ctx/:context_name/ — bundled context fetch.
|
||||||
|
pub async fn context_fetch(
|
||||||
|
Path(context_name): Path<String>,
|
||||||
|
Query(params): Query<BTreeMap<String, String>>,
|
||||||
|
) -> Result<Response, ApiError> {
|
||||||
|
if lookup_context(&context_name).is_none() {
|
||||||
|
return Err(ApiError(MizanError::NotFound(format!(
|
||||||
|
"context {context_name:?} not registered"
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
|
||||||
|
let members: Vec<&dyn FunctionSpec> = FUNCTIONS
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|f| f.context() == Some(&context_name))
|
||||||
|
.collect();
|
||||||
|
if members.is_empty() {
|
||||||
|
return Err(ApiError(MizanError::NotFound(format!(
|
||||||
|
"context {context_name:?} has no registered members"
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert query params (all-string values) to the JSON arg map. Numeric
|
||||||
|
// params get parsed via the per-function input_params primitive table.
|
||||||
|
let mut bundled = Map::new();
|
||||||
|
let unit = ();
|
||||||
|
for fn_spec in &members {
|
||||||
|
let args = coerce_query_args(*fn_spec, ¶ms);
|
||||||
|
let req = RequestHandle::new(&unit);
|
||||||
|
let result = fn_spec.dispatch(req, Value::Object(args)).await.map_err(ApiError)?;
|
||||||
|
bundled.insert(fn_spec.name().to_string(), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(no_store(Value::Object(bundled)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coerce string-valued query params into typed JSON values using the
|
||||||
|
/// function's declared input_params. Strings that don't parse stay as
|
||||||
|
/// strings — the dispatch wrapper will raise ValidationFailed downstream.
|
||||||
|
fn coerce_query_args(
|
||||||
|
fn_spec: &dyn FunctionSpec,
|
||||||
|
params: &BTreeMap<String, String>,
|
||||||
|
) -> Map<String, Value> {
|
||||||
|
let mut out = Map::new();
|
||||||
|
for ip in fn_spec.input_params() {
|
||||||
|
if let Some(raw) = params.get(ip.name) {
|
||||||
|
let parsed = match ip.primitive {
|
||||||
|
mizan_core::Primitive::Integer => raw.parse::<i64>().ok().map(Value::from),
|
||||||
|
mizan_core::Primitive::Number => raw.parse::<f64>().ok().and_then(|v| {
|
||||||
|
serde_json::Number::from_f64(v).map(Value::Number)
|
||||||
|
}),
|
||||||
|
mizan_core::Primitive::Boolean => raw.parse::<bool>().ok().map(Value::from),
|
||||||
|
mizan_core::Primitive::String => Some(Value::from(raw.clone())),
|
||||||
|
};
|
||||||
|
if let Some(v) = parsed {
|
||||||
|
out.insert(ip.name.into(), v);
|
||||||
|
} else {
|
||||||
|
out.insert(ip.name.into(), Value::from(raw.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /session/ — placeholder for the Mizan-protocol session-init endpoint.
|
||||||
|
/// CSRF is a Django-only concern; the Rust adapter returns a null token so
|
||||||
|
/// readiness-probe consumers see a well-formed response.
|
||||||
|
pub async fn session_init() -> Response {
|
||||||
|
let body = serde_json::json!({ "csrfToken": null });
|
||||||
|
no_store(body)
|
||||||
|
}
|
||||||
37
backends/mizan-rust-axum/src/lib.rs
Normal file
37
backends/mizan-rust-axum/src/lib.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
//! Mizan axum HTTP adapter — typed RPC over `mizan-core`'s function registry.
|
||||||
|
//!
|
||||||
|
//! Usage:
|
||||||
|
//! ```ignore
|
||||||
|
//! use axum::Router;
|
||||||
|
//! use mizan_axum::router;
|
||||||
|
//!
|
||||||
|
//! #[tokio::main]
|
||||||
|
//! async fn main() {
|
||||||
|
//! let app = Router::new().nest("/api/mizan", router());
|
||||||
|
//! let listener = tokio::net::TcpListener::bind("127.0.0.1:8000").await.unwrap();
|
||||||
|
//! axum::serve(listener, app).await.unwrap();
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Exposed endpoints (mirroring `mizan-fastapi` / `mizan-django`):
|
||||||
|
//! * `GET /session/` — session-init probe (placeholder CSRF token)
|
||||||
|
//! * `POST /call/` — RPC dispatch with invalidate+merge response
|
||||||
|
//! * `GET /ctx/:name/` — bundled context fetch
|
||||||
|
|
||||||
|
mod errors;
|
||||||
|
mod handlers;
|
||||||
|
|
||||||
|
pub use errors::ApiError;
|
||||||
|
pub use handlers::{context_fetch, function_call, session_init, CallBody, CallResponse};
|
||||||
|
|
||||||
|
use axum::routing::{get, post};
|
||||||
|
use axum::Router;
|
||||||
|
|
||||||
|
/// Build the Mizan router. Mount it under a prefix:
|
||||||
|
/// `Router::new().nest("/api/mizan", router())`.
|
||||||
|
pub fn router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/session/", get(handlers::session_init))
|
||||||
|
.route("/call/", post(handlers::function_call))
|
||||||
|
.route("/ctx/:context_name/", get(handlers::context_fetch))
|
||||||
|
}
|
||||||
@@ -469,7 +469,11 @@ def build_ir() -> str:
|
|||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# ── Functions ──
|
# ── Functions ──
|
||||||
for fn_name, fn_class in functions.items():
|
# Alphabetical by wire name — the IR is a canonical contract, not a
|
||||||
|
# transcript of registration order. Both Python and Rust emitters sort
|
||||||
|
# so byte-equivalence holds across language-backed backends.
|
||||||
|
for fn_name in sorted(functions):
|
||||||
|
fn_class = functions[fn_name]
|
||||||
meta = getattr(fn_class, "_meta", {})
|
meta = getattr(fn_class, "_meta", {})
|
||||||
if meta.get("private") or meta.get("view_path"):
|
if meta.get("private") or meta.get("view_path"):
|
||||||
continue
|
continue
|
||||||
@@ -481,8 +485,9 @@ def build_ir() -> str:
|
|||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# ── Contexts ──
|
# ── Contexts ──
|
||||||
for ctx_name, fn_names in context_groups.items():
|
# Alphabetical by context name — same reason as functions above.
|
||||||
_emit_context(root, ctx_name, fn_names)
|
for ctx_name in sorted(context_groups):
|
||||||
|
_emit_context(root, ctx_name, context_groups[ctx_name])
|
||||||
|
|
||||||
if context_groups:
|
if context_groups:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
@@ -541,14 +546,16 @@ def _emit_context(root: _Block, ctx_name: str, fn_names: list[str]) -> None:
|
|||||||
slot["required"] = len(slot["shared_by"]) == len(fn_names)
|
slot["required"] = len(slot["shared_by"]) == len(fn_names)
|
||||||
|
|
||||||
with root.node("context", _kdl_string(ctx_name)) as block:
|
with root.node("context", _kdl_string(ctx_name)) as block:
|
||||||
for fn_name in fn_names:
|
# Members alphabetical — canonical order.
|
||||||
|
for fn_name in sorted(fn_names):
|
||||||
block.leaf("function", _kdl_string(fn_name))
|
block.leaf("function", _kdl_string(fn_name))
|
||||||
for param_name in sorted(param_info):
|
for param_name in sorted(param_info):
|
||||||
slot = param_info[param_name]
|
slot = param_info[param_name]
|
||||||
with block.node("param", _kdl_string(param_name)) as param_block:
|
with block.node("param", _kdl_string(param_name)) as param_block:
|
||||||
param_block.leaf("type", _kdl_string(slot["type"]))
|
param_block.leaf("type", _kdl_string(slot["type"]))
|
||||||
param_block.leaf("required", _kdl_bool(slot["required"]))
|
param_block.leaf("required", _kdl_bool(slot["required"]))
|
||||||
for sharer in slot["shared_by"]:
|
# `shared-by` follows the same canonical ordering.
|
||||||
|
for sharer in sorted(slot["shared_by"]):
|
||||||
param_block.leaf("shared-by", _kdl_string(sharer))
|
param_block.leaf("shared-by", _kdl_string(sharer))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
15
cores/mizan-rust-macros/Cargo.toml
Normal file
15
cores/mizan-rust-macros/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "mizan-macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Proc macros for mizan-core: #[derive(Mizan)], #[mizan::context], #[mizan(...)]. Emits MizanType / ContextMarker / FunctionSpec impls plus linkme registrations."
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
proc-macro2 = "1"
|
||||||
|
quote = "1"
|
||||||
|
syn = { version = "2", features = ["full", "extra-traits"] }
|
||||||
|
heck = "0.5"
|
||||||
77
cores/mizan-rust-macros/src/context.rs
Normal file
77
cores/mizan-rust-macros/src/context.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
//! `#[mizan::context]` / `#[mizan::context("name")]` — emit `ContextMarker`
|
||||||
|
//! impl + linkme registration for a unit struct.
|
||||||
|
|
||||||
|
use heck::ToSnakeCase;
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use syn::{parse::Parser, punctuated::Punctuated, ItemStruct, Lit, LitStr, Meta, Token};
|
||||||
|
|
||||||
|
/// Attribute args: either nothing, or one string literal that overrides the
|
||||||
|
/// derived snake_case context name.
|
||||||
|
pub struct ContextArgs {
|
||||||
|
pub explicit_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextArgs {
|
||||||
|
pub fn parse(attr_tokens: TokenStream) -> syn::Result<Self> {
|
||||||
|
if attr_tokens.is_empty() {
|
||||||
|
return Ok(Self { explicit_name: None });
|
||||||
|
}
|
||||||
|
// Support both `#[mizan::context("user")]` (string literal) and
|
||||||
|
// `#[mizan::context(name = "user")]` (key=value).
|
||||||
|
if let Ok(lit) = syn::parse2::<LitStr>(attr_tokens.clone()) {
|
||||||
|
return Ok(Self {
|
||||||
|
explicit_name: Some(lit.value()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
|
||||||
|
let metas = parser.parse2(attr_tokens)?;
|
||||||
|
for meta in metas {
|
||||||
|
if let Meta::NameValue(nv) = meta {
|
||||||
|
if nv.path.is_ident("name") {
|
||||||
|
if let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = nv.value {
|
||||||
|
return Ok(Self {
|
||||||
|
explicit_name: Some(s.value()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(syn::Error::new_spanned(
|
||||||
|
"",
|
||||||
|
"expected `#[mizan::context]` or `#[mizan::context(\"<name>\")]` or `#[mizan::context(name = \"<name>\")]`",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expand(args: ContextArgs, item: ItemStruct) -> TokenStream {
|
||||||
|
if !item.fields.is_empty() {
|
||||||
|
return syn::Error::new_spanned(
|
||||||
|
&item.fields,
|
||||||
|
"#[mizan::context] requires a unit struct — context markers carry no data.",
|
||||||
|
)
|
||||||
|
.to_compile_error();
|
||||||
|
}
|
||||||
|
|
||||||
|
let ident = item.ident.clone();
|
||||||
|
let name = args
|
||||||
|
.explicit_name
|
||||||
|
.unwrap_or_else(|| ident.to_string().to_snake_case());
|
||||||
|
|
||||||
|
let register_static =
|
||||||
|
format_ident!("__MIZAN_CTX_REGISTER_{}", ident.to_string().to_uppercase());
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#item
|
||||||
|
|
||||||
|
impl ::mizan_core::ContextMarker for #ident {
|
||||||
|
const NAME: &'static str = #name;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[::mizan_core::__priv::linkme::distributed_slice(::mizan_core::CONTEXTS)]
|
||||||
|
#[linkme(crate = ::mizan_core::__priv::linkme)]
|
||||||
|
static #register_static: ::mizan_core::ContextEntry = ::mizan_core::ContextEntry {
|
||||||
|
name: #name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
102
cores/mizan-rust-macros/src/derive.rs
Normal file
102
cores/mizan-rust-macros/src/derive.rs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
//! `#[derive(Mizan)]` — emit `MizanType` impl + linkme registration.
|
||||||
|
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{Data, DataEnum, DataStruct, DeriveInput, Fields};
|
||||||
|
|
||||||
|
use crate::shape::type_shape_expr;
|
||||||
|
|
||||||
|
/// Expand `#[derive(Mizan)]`. Emits only the `MizanType` trait impl —
|
||||||
|
/// registration into the IR `TYPES` slice happens at the function macro
|
||||||
|
/// (which owns the canonical-named type entries `<camelName>Input` /
|
||||||
|
/// `<camelName>Output`) and at sub-type discovery inside `Vec<T>` outputs.
|
||||||
|
/// This keeps the type registry tree-shaken: only types actually reachable
|
||||||
|
/// from a registered function appear in the emitted IR.
|
||||||
|
pub fn expand(input: DeriveInput) -> TokenStream {
|
||||||
|
let ident = input.ident.clone();
|
||||||
|
let type_name = ident.to_string();
|
||||||
|
|
||||||
|
let named_type_body = match &input.data {
|
||||||
|
Data::Struct(s) => emit_struct(s),
|
||||||
|
Data::Enum(e) => emit_enum(e),
|
||||||
|
Data::Union(_) => {
|
||||||
|
return syn::Error::new_spanned(
|
||||||
|
&input,
|
||||||
|
"#[derive(Mizan)] does not support `union` types — use a struct or enum.",
|
||||||
|
)
|
||||||
|
.to_compile_error();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
impl ::mizan_core::MizanType for #ident {
|
||||||
|
const TYPE_NAME: &'static str = #type_name;
|
||||||
|
fn shape() -> ::mizan_core::NamedType { #named_type_body }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_struct(s: &DataStruct) -> TokenStream {
|
||||||
|
let fields = match &s.fields {
|
||||||
|
Fields::Named(named) => &named.named,
|
||||||
|
Fields::Unnamed(_) | Fields::Unit => {
|
||||||
|
return syn::Error::new_spanned(
|
||||||
|
&s.fields,
|
||||||
|
"#[derive(Mizan)] requires named fields. Tuple structs and unit structs aren't part of the IR shape.",
|
||||||
|
)
|
||||||
|
.to_compile_error();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut field_exprs: Vec<TokenStream> = Vec::new();
|
||||||
|
for field in fields {
|
||||||
|
let ident = field
|
||||||
|
.ident
|
||||||
|
.as_ref()
|
||||||
|
.expect("named field always has an ident");
|
||||||
|
let name = ident.to_string();
|
||||||
|
let shape = type_shape_expr(&field.ty);
|
||||||
|
|
||||||
|
// A field is `required` iff its type is not `Option<...>`. Defaults
|
||||||
|
// are not encodable from Rust syntax (no `= expr` on a struct field
|
||||||
|
// declaration) — the macro emits `required: false, default: None`
|
||||||
|
// for Option-wrapped fields, leaving defaults for a future
|
||||||
|
// attribute-based extension.
|
||||||
|
let is_optional = crate::shape::unwrap_option(&field.ty).is_some();
|
||||||
|
let required = !is_optional;
|
||||||
|
field_exprs.push(quote! {
|
||||||
|
::mizan_core::StructField {
|
||||||
|
name: #name,
|
||||||
|
required: #required,
|
||||||
|
default: ::std::option::Option::None,
|
||||||
|
shape: #shape,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
::mizan_core::NamedType::Struct(::std::vec![
|
||||||
|
#(#field_exprs),*
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_enum(e: &DataEnum) -> TokenStream {
|
||||||
|
let mut variants: Vec<TokenStream> = Vec::new();
|
||||||
|
for variant in &e.variants {
|
||||||
|
if !matches!(variant.fields, Fields::Unit) {
|
||||||
|
return syn::Error::new_spanned(
|
||||||
|
&variant.fields,
|
||||||
|
"#[derive(Mizan)] only supports unit-variant enums (string-literal enums in the IR). Variants with payload aren't expressible in the current IR.",
|
||||||
|
)
|
||||||
|
.to_compile_error();
|
||||||
|
}
|
||||||
|
let name = variant.ident.to_string();
|
||||||
|
variants.push(quote! { #name });
|
||||||
|
}
|
||||||
|
quote! {
|
||||||
|
::mizan_core::NamedType::Enum(::std::vec![
|
||||||
|
#(#variants),*
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
494
cores/mizan-rust-macros/src/function.rs
Normal file
494
cores/mizan-rust-macros/src/function.rs
Normal file
@@ -0,0 +1,494 @@
|
|||||||
|
//! `#[mizan(...)]` — on async fns. Generates:
|
||||||
|
//! * a synthetic Input struct (`<camelName>Input`) when the fn has params
|
||||||
|
//! * `MizanType` impl on the Input struct
|
||||||
|
//! * canonical type entries (`<camelName>Input` / `<camelName>Output`)
|
||||||
|
//! * Vec-element sub-type entries (so `Vec<T>` outputs surface `T` too)
|
||||||
|
//! * `FunctionSpec` impl on a ZST `__MizanFn_<name>`
|
||||||
|
//! * `FUNCTIONS` linkme registration of `&__MIZAN_FN_<NAME>_INSTANCE`
|
||||||
|
|
||||||
|
use heck::{ToLowerCamelCase, ToShoutySnakeCase};
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use syn::{
|
||||||
|
parse::Parser,
|
||||||
|
punctuated::Punctuated,
|
||||||
|
spanned::Spanned,
|
||||||
|
Expr, ExprPath, ExprTuple, FnArg, ItemFn, Meta, Pat, Path, ReturnType, Token, Type,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::shape::{analyze_return, primitive_of, type_shape_expr, unwrap_option};
|
||||||
|
|
||||||
|
/// Parsed attribute args for `#[mizan(...)]`.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct FunctionArgs {
|
||||||
|
pub context: Option<Path>,
|
||||||
|
pub affects: Vec<Path>,
|
||||||
|
pub merge: Vec<Path>,
|
||||||
|
pub websocket: bool,
|
||||||
|
pub private: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FunctionArgs {
|
||||||
|
pub fn parse(attr_tokens: TokenStream) -> syn::Result<Self> {
|
||||||
|
if attr_tokens.is_empty() {
|
||||||
|
return Ok(Self::default());
|
||||||
|
}
|
||||||
|
let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
|
||||||
|
let metas = parser.parse2(attr_tokens)?;
|
||||||
|
let mut out = Self::default();
|
||||||
|
for meta in metas {
|
||||||
|
match meta {
|
||||||
|
Meta::NameValue(nv) => {
|
||||||
|
if nv.path.is_ident("context") {
|
||||||
|
out.context = Some(expect_path(&nv.value)?);
|
||||||
|
} else if nv.path.is_ident("affects") {
|
||||||
|
out.affects = collect_paths(&nv.value)?;
|
||||||
|
} else if nv.path.is_ident("merge") {
|
||||||
|
out.merge = collect_paths(&nv.value)?;
|
||||||
|
} else {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
nv.path,
|
||||||
|
"unknown attribute key; expected one of: context, affects, merge",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Meta::Path(p) => {
|
||||||
|
if p.is_ident("websocket") {
|
||||||
|
out.websocket = true;
|
||||||
|
} else if p.is_ident("private") {
|
||||||
|
out.private = true;
|
||||||
|
} else {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
p,
|
||||||
|
"unknown flag; expected `websocket` or `private`",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Meta::List(l) => {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
l,
|
||||||
|
"list-shaped attribute args not supported here",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if out.context.is_some() && !out.affects.is_empty() {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
out.context.as_ref().unwrap(),
|
||||||
|
"`context` and `affects` are mutually exclusive — a function is either a context reader or a mutation.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if out.context.is_some() && !out.merge.is_empty() {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
out.context.as_ref().unwrap(),
|
||||||
|
"`context` and `merge` are mutually exclusive — a function is either a context reader or a mutation.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect_path(expr: &Expr) -> syn::Result<Path> {
|
||||||
|
if let Expr::Path(ExprPath { path, .. }) = expr {
|
||||||
|
Ok(path.clone())
|
||||||
|
} else {
|
||||||
|
Err(syn::Error::new_spanned(
|
||||||
|
expr,
|
||||||
|
"expected a type path (e.g. `UserCtx`)",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_paths(expr: &Expr) -> syn::Result<Vec<Path>> {
|
||||||
|
match expr {
|
||||||
|
Expr::Path(_) => Ok(vec![expect_path(expr)?]),
|
||||||
|
Expr::Tuple(ExprTuple { elems, .. }) => elems.iter().map(expect_path).collect(),
|
||||||
|
_ => Err(syn::Error::new_spanned(
|
||||||
|
expr,
|
||||||
|
"expected a context type or a tuple of context types (e.g. `UserCtx` or `(UserCtx, OrderCtx)`)",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about one input parameter, extracted from the fn signature.
|
||||||
|
struct InputArg {
|
||||||
|
ident: syn::Ident,
|
||||||
|
ty: Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expand(args: FunctionArgs, item: ItemFn) -> TokenStream {
|
||||||
|
if item.sig.asyncness.is_none() {
|
||||||
|
return syn::Error::new_spanned(
|
||||||
|
&item.sig.fn_token,
|
||||||
|
"#[mizan] requires an `async fn`. Wrap synchronous handlers if needed.",
|
||||||
|
)
|
||||||
|
.to_compile_error();
|
||||||
|
}
|
||||||
|
|
||||||
|
let fn_name = item.sig.ident.to_string();
|
||||||
|
let camel = fn_name.to_lower_camel_case();
|
||||||
|
let input_type_name = format!("{camel}Input");
|
||||||
|
let output_type_name = format!("{camel}Output");
|
||||||
|
|
||||||
|
let input_args = match collect_input_args(&item) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => return e.to_compile_error(),
|
||||||
|
};
|
||||||
|
let has_input = !input_args.is_empty();
|
||||||
|
let input_type_ident = format_ident!("{}", input_type_name);
|
||||||
|
|
||||||
|
let return_ty = match &item.sig.output {
|
||||||
|
ReturnType::Type(_, t) => (**t).clone(),
|
||||||
|
ReturnType::Default => {
|
||||||
|
return syn::Error::new_spanned(
|
||||||
|
&item.sig,
|
||||||
|
"#[mizan] requires an explicit return type. Add `-> T` to the signature.",
|
||||||
|
)
|
||||||
|
.to_compile_error();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let analysis = analyze_return(&return_ty);
|
||||||
|
|
||||||
|
// ─── Synthetic Input struct ────────────────────────────────────────────
|
||||||
|
let input_struct = if has_input {
|
||||||
|
let mut field_defs = Vec::new();
|
||||||
|
let mut field_shapes = Vec::new();
|
||||||
|
for arg in &input_args {
|
||||||
|
let ident = &arg.ident;
|
||||||
|
let ty = &arg.ty;
|
||||||
|
// Strip a leading underscore from the wire-level field name —
|
||||||
|
// Rust convention uses `_foo` to silence unused-arg warnings,
|
||||||
|
// but the wire schema and the Python fixture name the param
|
||||||
|
// `foo`. The struct field keeps its source ident (so the
|
||||||
|
// dispatch wrapper's `validated.#ident` compiles), and a serde
|
||||||
|
// `rename` bridges the wire-level JSON name.
|
||||||
|
let name_str = ident.to_string();
|
||||||
|
let wire_name = name_str.trim_start_matches('_').to_string();
|
||||||
|
let serde_rename = if wire_name != name_str {
|
||||||
|
quote! { #[serde(rename = #wire_name)] }
|
||||||
|
} else {
|
||||||
|
TokenStream::new()
|
||||||
|
};
|
||||||
|
field_defs.push(quote! { #serde_rename pub #ident: #ty, });
|
||||||
|
let is_optional = unwrap_option(ty).is_some();
|
||||||
|
let required = !is_optional;
|
||||||
|
let shape = type_shape_expr(ty);
|
||||||
|
field_shapes.push(quote! {
|
||||||
|
::mizan_core::StructField {
|
||||||
|
name: #wire_name,
|
||||||
|
required: #required,
|
||||||
|
default: ::std::option::Option::None,
|
||||||
|
shape: #shape,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
quote! {
|
||||||
|
#[derive(::std::fmt::Debug, ::std::clone::Clone, ::serde::Serialize, ::serde::Deserialize)]
|
||||||
|
pub struct #input_type_ident {
|
||||||
|
#(#field_defs)*
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ::mizan_core::MizanType for #input_type_ident {
|
||||||
|
const TYPE_NAME: &'static str = #input_type_name;
|
||||||
|
fn shape() -> ::mizan_core::NamedType {
|
||||||
|
::mizan_core::NamedType::Struct(::std::vec![
|
||||||
|
#(#field_shapes),*
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TokenStream::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Type entry registrations ──────────────────────────────────────────
|
||||||
|
// - Input: TypeEntry pointing at the synthetic input struct's shape_fn.
|
||||||
|
// - Output: TypeEntry whose shape is a copy of the user's Output shape
|
||||||
|
// (for struct outputs) or an `Alias(List(Ref("T")))` (for Vec outputs).
|
||||||
|
// - For Vec<T> outputs, ALSO register T's TypeEntry pointing at T's
|
||||||
|
// MizanType impl (so the Ref resolves in the IR).
|
||||||
|
let mut type_registrations = Vec::new();
|
||||||
|
if has_input {
|
||||||
|
let static_ident =
|
||||||
|
format_ident!("__MIZAN_TYPE_{}", input_type_name.to_shouty_snake_case());
|
||||||
|
type_registrations.push(quote! {
|
||||||
|
#[::mizan_core::__priv::linkme::distributed_slice(::mizan_core::TYPES)]
|
||||||
|
#[linkme(crate = ::mizan_core::__priv::linkme)]
|
||||||
|
static #static_ident: ::mizan_core::TypeEntry = ::mizan_core::TypeEntry {
|
||||||
|
name: #input_type_name,
|
||||||
|
shape_fn: <#input_type_ident as ::mizan_core::MizanType>::shape,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let output_static = format_ident!("__MIZAN_TYPE_{}", output_type_name.to_shouty_snake_case());
|
||||||
|
if analysis.is_vec {
|
||||||
|
let elem = analysis.vec_inner.as_ref().expect("vec_inner set");
|
||||||
|
// userOrdersOutput → alias { list { ref "OrderOutput" } }
|
||||||
|
// The Ref name is resolved via `<T as MizanType>::type_name()`.
|
||||||
|
type_registrations.push(quote! {
|
||||||
|
#[::mizan_core::__priv::linkme::distributed_slice(::mizan_core::TYPES)]
|
||||||
|
#[linkme(crate = ::mizan_core::__priv::linkme)]
|
||||||
|
static #output_static: ::mizan_core::TypeEntry = ::mizan_core::TypeEntry {
|
||||||
|
name: #output_type_name,
|
||||||
|
shape_fn: || ::mizan_core::NamedType::Alias(
|
||||||
|
::mizan_core::TypeShape::List(::std::boxed::Box::new(
|
||||||
|
::mizan_core::TypeShape::Ref(<#elem as ::mizan_core::MizanType>::TYPE_NAME)
|
||||||
|
))
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// Also register the element type itself by its own name. `TYPE_NAME`
|
||||||
|
// is an associated const, so this is usable in a static initializer.
|
||||||
|
let elem_static = element_type_static_ident(elem);
|
||||||
|
type_registrations.push(quote! {
|
||||||
|
#[::mizan_core::__priv::linkme::distributed_slice(::mizan_core::TYPES)]
|
||||||
|
#[linkme(crate = ::mizan_core::__priv::linkme)]
|
||||||
|
static #elem_static: ::mizan_core::TypeEntry = ::mizan_core::TypeEntry {
|
||||||
|
name: <#elem as ::mizan_core::MizanType>::TYPE_NAME,
|
||||||
|
shape_fn: <#elem as ::mizan_core::MizanType>::shape,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Non-Vec output: copy the inner type's shape under the canonical name.
|
||||||
|
let inner_ty = &analysis.inner;
|
||||||
|
type_registrations.push(quote! {
|
||||||
|
#[::mizan_core::__priv::linkme::distributed_slice(::mizan_core::TYPES)]
|
||||||
|
#[linkme(crate = ::mizan_core::__priv::linkme)]
|
||||||
|
static #output_static: ::mizan_core::TypeEntry = ::mizan_core::TypeEntry {
|
||||||
|
name: #output_type_name,
|
||||||
|
shape_fn: <#inner_ty as ::mizan_core::MizanType>::shape,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── InputParam slice (for context-builder shared-param elevation) ────
|
||||||
|
let mut input_params = Vec::new();
|
||||||
|
for arg in &input_args {
|
||||||
|
// Wire-level name strips the underscore prefix — see input_struct
|
||||||
|
// above for the rationale.
|
||||||
|
let name_str = arg.ident.to_string();
|
||||||
|
let name_str = name_str.trim_start_matches('_').to_string();
|
||||||
|
let primitive = primitive_of(&arg.ty).unwrap_or_else(|| {
|
||||||
|
// Non-primitive params don't surface in the context's `param`
|
||||||
|
// block; they participate as opaque payloads. Using `String` as
|
||||||
|
// the placeholder primitive matches Python's fallback in
|
||||||
|
// `_annotation_to_primitive`.
|
||||||
|
quote! { ::mizan_core::Primitive::String }
|
||||||
|
});
|
||||||
|
let is_optional = unwrap_option(&arg.ty).is_some();
|
||||||
|
let required = !is_optional;
|
||||||
|
input_params.push(quote! {
|
||||||
|
::mizan_core::InputParam {
|
||||||
|
name: #name_str,
|
||||||
|
primitive: #primitive,
|
||||||
|
required: #required,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let params_static = format_ident!("__MIZAN_FN_{}_PARAMS", fn_name.to_shouty_snake_case());
|
||||||
|
let params_const = quote! {
|
||||||
|
const #params_static: &[::mizan_core::InputParam] = &[
|
||||||
|
#(#input_params),*
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── AffectTarget / merge / context wiring ─────────────────────────────
|
||||||
|
let affects_static = format_ident!("__MIZAN_FN_{}_AFFECTS", fn_name.to_shouty_snake_case());
|
||||||
|
let affects_entries: Vec<_> = args
|
||||||
|
.affects
|
||||||
|
.iter()
|
||||||
|
.map(|p| {
|
||||||
|
quote! {
|
||||||
|
::mizan_core::AffectTarget::Context(<#p as ::mizan_core::ContextMarker>::NAME)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let affects_const = quote! {
|
||||||
|
const #affects_static: &[::mizan_core::AffectTarget] = &[
|
||||||
|
#(#affects_entries),*
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
let merge_static = format_ident!("__MIZAN_FN_{}_MERGE", fn_name.to_shouty_snake_case());
|
||||||
|
let merge_entries: Vec<_> = args
|
||||||
|
.merge
|
||||||
|
.iter()
|
||||||
|
.map(|p| quote! { <#p as ::mizan_core::ContextMarker>::NAME })
|
||||||
|
.collect();
|
||||||
|
let merge_const = quote! {
|
||||||
|
const #merge_static: &[&'static str] = &[
|
||||||
|
#(#merge_entries),*
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
let context_value = match &args.context {
|
||||||
|
Some(p) => quote! { ::std::option::Option::Some(<#p as ::mizan_core::ContextMarker>::NAME) },
|
||||||
|
None => quote! { ::std::option::Option::None },
|
||||||
|
};
|
||||||
|
|
||||||
|
let transport_value = if args.websocket {
|
||||||
|
quote! { ::mizan_core::Transport::Websocket }
|
||||||
|
} else {
|
||||||
|
quote! { ::mizan_core::Transport::Http }
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Dispatch wrapper + FunctionSpec impl ──────────────────────────────
|
||||||
|
let inner_fn_ident = item.sig.ident.clone();
|
||||||
|
let spec_struct = format_ident!("__MizanFn_{}", inner_fn_ident);
|
||||||
|
let spec_const = format_ident!("__MIZAN_FN_{}_SPEC", fn_name.to_shouty_snake_case());
|
||||||
|
let register_static =
|
||||||
|
format_ident!("__MIZAN_FN_{}_REGISTER", fn_name.to_shouty_snake_case());
|
||||||
|
|
||||||
|
let input_type_opt = if has_input {
|
||||||
|
quote! { ::std::option::Option::Some(#input_type_name) }
|
||||||
|
} else {
|
||||||
|
quote! { ::std::option::Option::None }
|
||||||
|
};
|
||||||
|
|
||||||
|
let output_nullable = analysis.nullable;
|
||||||
|
let private = args.private;
|
||||||
|
|
||||||
|
let dispatch_body = build_dispatch(&item, &input_args, has_input, &input_type_ident);
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
// Keep the user's original fn intact — the macro never rewrites the
|
||||||
|
// body, only wraps it for dispatch.
|
||||||
|
#item
|
||||||
|
|
||||||
|
#input_struct
|
||||||
|
|
||||||
|
#(#type_registrations)*
|
||||||
|
|
||||||
|
#params_const
|
||||||
|
#affects_const
|
||||||
|
#merge_const
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub struct #spec_struct;
|
||||||
|
|
||||||
|
impl ::mizan_core::FunctionSpec for #spec_struct {
|
||||||
|
fn name(&self) -> &'static str { #fn_name }
|
||||||
|
fn camel_name(&self) -> &'static str { #camel }
|
||||||
|
fn has_input(&self) -> bool { #has_input }
|
||||||
|
fn input_type(&self) -> ::std::option::Option<&'static str> { #input_type_opt }
|
||||||
|
fn output_type(&self) -> &'static str { #output_type_name }
|
||||||
|
fn output_nullable(&self) -> bool { #output_nullable }
|
||||||
|
fn context(&self) -> ::std::option::Option<&'static str> { #context_value }
|
||||||
|
fn affects(&self) -> &'static [::mizan_core::AffectTarget] { #affects_static }
|
||||||
|
fn merge(&self) -> &'static [&'static str] { #merge_static }
|
||||||
|
fn transport(&self) -> ::mizan_core::Transport { #transport_value }
|
||||||
|
fn private(&self) -> bool { #private }
|
||||||
|
fn input_params(&self) -> &'static [::mizan_core::InputParam] { #params_static }
|
||||||
|
|
||||||
|
fn dispatch<'a>(
|
||||||
|
&'a self,
|
||||||
|
req: ::mizan_core::RequestHandle<'a>,
|
||||||
|
args: ::mizan_core::__priv::serde_json::Value,
|
||||||
|
) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<
|
||||||
|
Output = ::std::result::Result<
|
||||||
|
::mizan_core::__priv::serde_json::Value,
|
||||||
|
::mizan_core::MizanError,
|
||||||
|
>
|
||||||
|
> + ::std::marker::Send + 'a>> {
|
||||||
|
::std::boxed::Box::pin(async move {
|
||||||
|
#dispatch_body
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_upper_case_globals)]
|
||||||
|
static #spec_const: #spec_struct = #spec_struct;
|
||||||
|
|
||||||
|
#[::mizan_core::__priv::linkme::distributed_slice(::mizan_core::FUNCTIONS)]
|
||||||
|
#[linkme(crate = ::mizan_core::__priv::linkme)]
|
||||||
|
static #register_static: &dyn ::mizan_core::FunctionSpec = &#spec_const;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_input_args(item: &ItemFn) -> syn::Result<Vec<InputArg>> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut iter = item.sig.inputs.iter();
|
||||||
|
// First arg is the request handle — skip without inspection. The function
|
||||||
|
// body uses it directly; the dispatch wrapper forwards `req`.
|
||||||
|
if iter.next().is_none() {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
item.sig.span(),
|
||||||
|
"#[mizan] functions must accept at least a request handle as the first parameter (e.g. `&Request` or `RequestHandle`).",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for arg in iter {
|
||||||
|
match arg {
|
||||||
|
FnArg::Typed(pat) => {
|
||||||
|
let ident = match &*pat.pat {
|
||||||
|
Pat::Ident(pi) => pi.ident.clone(),
|
||||||
|
_ => {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
&pat.pat,
|
||||||
|
"#[mizan] function parameters must be plain identifiers (no destructuring).",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
out.push(InputArg {
|
||||||
|
ident,
|
||||||
|
ty: (*pat.ty).clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
FnArg::Receiver(_) => {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
arg,
|
||||||
|
"#[mizan] functions are free functions, not methods. `self` is not allowed.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_dispatch(
|
||||||
|
item: &ItemFn,
|
||||||
|
input_args: &[InputArg],
|
||||||
|
has_input: bool,
|
||||||
|
input_type_ident: &syn::Ident,
|
||||||
|
) -> TokenStream {
|
||||||
|
let inner = &item.sig.ident;
|
||||||
|
if has_input {
|
||||||
|
let arg_names: Vec<_> = input_args.iter().map(|a| &a.ident).collect();
|
||||||
|
quote! {
|
||||||
|
let validated: #input_type_ident = ::mizan_core::__priv::serde_json::from_value(args)
|
||||||
|
.map_err(|e| ::mizan_core::MizanError::ValidationFailed {
|
||||||
|
message: format!("input validation failed: {e}"),
|
||||||
|
details: ::mizan_core::__priv::serde_json::Value::Null,
|
||||||
|
})?;
|
||||||
|
let result = #inner(
|
||||||
|
&req,
|
||||||
|
#( validated.#arg_names ),*
|
||||||
|
).await;
|
||||||
|
::mizan_core::__priv::serde_json::to_value(&result)
|
||||||
|
.map_err(|e| ::mizan_core::MizanError::InternalError(
|
||||||
|
format!("output serialization failed: {e}"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
let _ = args;
|
||||||
|
let result = #inner(&req).await;
|
||||||
|
::mizan_core::__priv::serde_json::to_value(&result)
|
||||||
|
.map_err(|e| ::mizan_core::MizanError::InternalError(
|
||||||
|
format!("output serialization failed: {e}"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn element_type_static_ident(ty: &Type) -> syn::Ident {
|
||||||
|
// Derive a unique static-name for the type's registration entry. Uses
|
||||||
|
// the last path segment's identifier as the discriminator.
|
||||||
|
let last = match ty {
|
||||||
|
Type::Path(tp) => tp.path.segments.last().map(|s| s.ident.to_string()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let suffix = last.unwrap_or_else(|| "ANON".to_string()).to_shouty_snake_case();
|
||||||
|
format_ident!("__MIZAN_TYPE_ELEM_{}", suffix)
|
||||||
|
}
|
||||||
|
|
||||||
58
cores/mizan-rust-macros/src/lib.rs
Normal file
58
cores/mizan-rust-macros/src/lib.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
//! Proc macros for `mizan-core`. See sibling modules for each macro's body.
|
||||||
|
//!
|
||||||
|
//! Consumer code reads:
|
||||||
|
//! ```ignore
|
||||||
|
//! use mizan_core::prelude::*;
|
||||||
|
//! pub use mizan_core as mizan; // so `#[mizan::context]` / `#[mizan::client]` read naturally
|
||||||
|
//!
|
||||||
|
//! #[derive(Mizan, serde::Serialize, serde::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: &Request, user_id: i64) -> ProfileOutput { ... }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! The function macro is named `client` to mirror Python's `@client`
|
||||||
|
//! decorator and to keep the namespace `mizan::` purely a module path —
|
||||||
|
//! `#[mizan(...)]` would collide with `mizan::context` (a module path
|
||||||
|
//! can't simultaneously be a callable macro in Rust).
|
||||||
|
|
||||||
|
mod context;
|
||||||
|
mod derive;
|
||||||
|
mod function;
|
||||||
|
mod shape;
|
||||||
|
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use syn::{parse_macro_input, DeriveInput, ItemFn, ItemStruct};
|
||||||
|
|
||||||
|
#[proc_macro_derive(Mizan)]
|
||||||
|
pub fn derive_mizan(input: TokenStream) -> TokenStream {
|
||||||
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
derive::expand(input).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn context(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
let args = match context::ContextArgs::parse(attr.into()) {
|
||||||
|
Ok(a) => a,
|
||||||
|
Err(e) => return e.to_compile_error().into(),
|
||||||
|
};
|
||||||
|
let item = parse_macro_input!(item as ItemStruct);
|
||||||
|
context::expand(args, item).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The function-registration attribute macro. Used as `#[mizan::client]`
|
||||||
|
/// (no args) or `#[mizan::client(context = X, affects = Y, merge = Z,
|
||||||
|
/// websocket, private)]`.
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn client(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
let args = match function::FunctionArgs::parse(attr.into()) {
|
||||||
|
Ok(a) => a,
|
||||||
|
Err(e) => return e.to_compile_error().into(),
|
||||||
|
};
|
||||||
|
let item = parse_macro_input!(item as ItemFn);
|
||||||
|
function::expand(args, item).into()
|
||||||
|
}
|
||||||
123
cores/mizan-rust-macros/src/shape.rs
Normal file
123
cores/mizan-rust-macros/src/shape.rs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
//! Lower a `syn::Type` to a TypeShape construction expression. Shared by
|
||||||
|
//! `#[derive(Mizan)]` (for struct fields) and `#[mizan(...)]` (for fn input
|
||||||
|
//! params + return-type analysis).
|
||||||
|
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{GenericArgument, PathArguments, Type, TypePath};
|
||||||
|
|
||||||
|
/// Result of inspecting a fn's return type.
|
||||||
|
pub struct ReturnAnalysis {
|
||||||
|
/// Inner type once `Option<...>` is unwrapped.
|
||||||
|
pub inner: Type,
|
||||||
|
/// True if the outermost wrapper is `Option<...>`.
|
||||||
|
pub nullable: bool,
|
||||||
|
/// True if `inner` is `Vec<T>` — caller emits an alias type entry.
|
||||||
|
pub is_vec: bool,
|
||||||
|
/// When `is_vec`, this is the element type `T`.
|
||||||
|
pub vec_inner: Option<Type>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn analyze_return(ty: &Type) -> ReturnAnalysis {
|
||||||
|
let (inner, nullable) = if let Some(t) = unwrap_option(ty) {
|
||||||
|
(t, true)
|
||||||
|
} else {
|
||||||
|
(ty.clone(), false)
|
||||||
|
};
|
||||||
|
if let Some(elem) = unwrap_vec(&inner) {
|
||||||
|
ReturnAnalysis {
|
||||||
|
inner: inner.clone(),
|
||||||
|
nullable,
|
||||||
|
is_vec: true,
|
||||||
|
vec_inner: Some(elem),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ReturnAnalysis {
|
||||||
|
inner,
|
||||||
|
nullable,
|
||||||
|
is_vec: false,
|
||||||
|
vec_inner: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit a `TypeShape` const-expression for `ty`. Used inside `#[derive(Mizan)]`
|
||||||
|
/// when constructing the struct field shapes.
|
||||||
|
pub fn type_shape_expr(ty: &Type) -> TokenStream {
|
||||||
|
if let Some(inner) = unwrap_option(ty) {
|
||||||
|
let inner_shape = type_shape_expr(&inner);
|
||||||
|
return quote! {
|
||||||
|
::mizan_core::TypeShape::Optional(::std::boxed::Box::new(#inner_shape))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if let Some(elem) = unwrap_vec(ty) {
|
||||||
|
let inner_shape = type_shape_expr(&elem);
|
||||||
|
return quote! {
|
||||||
|
::mizan_core::TypeShape::List(::std::boxed::Box::new(#inner_shape))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if let Some(p) = primitive_of(ty) {
|
||||||
|
return quote! { ::mizan_core::TypeShape::Primitive(#p) };
|
||||||
|
}
|
||||||
|
// Fallback: assume a user-defined struct/enum implementing MizanType.
|
||||||
|
// The Ref name comes from `<T as MizanType>::type_name()` at runtime.
|
||||||
|
quote! { ::mizan_core::TypeShape::Ref(<#ty as ::mizan_core::MizanType>::type_name()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit a `Primitive` const-expression for `ty`, or `None` if `ty` isn't a
|
||||||
|
/// known primitive scalar.
|
||||||
|
pub fn primitive_of(ty: &Type) -> Option<TokenStream> {
|
||||||
|
let path = match ty {
|
||||||
|
Type::Path(TypePath { qself: None, path }) => path,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let last = path.segments.last()?;
|
||||||
|
let name = last.ident.to_string();
|
||||||
|
match name.as_str() {
|
||||||
|
"i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "u8" | "u16" | "u32" | "u64" | "u128"
|
||||||
|
| "usize" => Some(quote! { ::mizan_core::Primitive::Integer }),
|
||||||
|
"f32" | "f64" => Some(quote! { ::mizan_core::Primitive::Number }),
|
||||||
|
"bool" => Some(quote! { ::mizan_core::Primitive::Boolean }),
|
||||||
|
"String" | "str" => Some(quote! { ::mizan_core::Primitive::String }),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If `ty` is `Option<T>`, return `T`. Otherwise None.
|
||||||
|
pub fn unwrap_option(ty: &Type) -> Option<Type> {
|
||||||
|
let path = match ty {
|
||||||
|
Type::Path(TypePath { qself: None, path }) => path,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let last = path.segments.last()?;
|
||||||
|
if last.ident != "Option" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
extract_single_generic(&last.arguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If `ty` is `Vec<T>`, return `T`. Otherwise None.
|
||||||
|
pub fn unwrap_vec(ty: &Type) -> Option<Type> {
|
||||||
|
let path = match ty {
|
||||||
|
Type::Path(TypePath { qself: None, path }) => path,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let last = path.segments.last()?;
|
||||||
|
if last.ident != "Vec" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
extract_single_generic(&last.arguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_single_generic(args: &PathArguments) -> Option<Type> {
|
||||||
|
let args = match args {
|
||||||
|
PathArguments::AngleBracketed(a) => a,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
for arg in &args.args {
|
||||||
|
if let GenericArgument::Type(t) = arg {
|
||||||
|
return Some(t.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
173
cores/mizan-rust/Cargo.lock
generated
Normal file
173
cores/mizan-rust/Cargo.lock
generated
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-trait"
|
||||||
|
version = "0.1.89"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indoc"
|
||||||
|
version = "2.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linkme"
|
||||||
|
version = "0.3.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e83272d46373fb8decca684579ac3e7c8f3d71d4cc3aa693df8759e260ae41cf"
|
||||||
|
dependencies = [
|
||||||
|
"linkme-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linkme-impl"
|
||||||
|
version = "0.3.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32d59e20403c7d08fe62b4376edfe5c7fb2ef1e6b1465379686d0f21c8df444b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mizan-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"indoc",
|
||||||
|
"linkme",
|
||||||
|
"mizan-macros",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mizan-macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.149"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.117"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
16
cores/mizan-rust/Cargo.toml
Normal file
16
cores/mizan-rust/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "mizan-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Mizan server-side IR substrate — types, traits, KDL emitter, registry. Rust analog of cores/mizan-python/src/mizan_core/."
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
linkme = "0.3"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
async-trait = "0.1"
|
||||||
|
mizan-macros = { path = "../mizan-rust-macros" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
indoc = "2"
|
||||||
165
cores/mizan-rust/src/graph_check.rs
Normal file
165
cores/mizan-rust/src/graph_check.rs
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
//! Cross-function invariant verification — fails at `build_ir()` time, which
|
||||||
|
//! runs at the codegen subprocess (`cargo run --bin export-ir`). All
|
||||||
|
//! graph-level inconsistencies surface before any client artifact is emitted.
|
||||||
|
|
||||||
|
use crate::ir::{AffectTarget, NamedType, StructField, TypeShape};
|
||||||
|
use crate::registry::{lookup_context, CONTEXTS, FUNCTIONS, TYPES};
|
||||||
|
|
||||||
|
/// Walk the registered types and find the named type's shape. Used by both
|
||||||
|
/// graph-check and runtime merge resolution.
|
||||||
|
pub(crate) fn resolve_type_shape(name: &str) -> Option<NamedType> {
|
||||||
|
for entry in TYPES {
|
||||||
|
if entry.name == name {
|
||||||
|
return Some((entry.shape_fn)());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Structural equality on named types. Two types are merge-compatible iff
|
||||||
|
/// they have identical shape — matches Python's `types_match_for_merge`.
|
||||||
|
pub(crate) fn types_match(a: &NamedType, b: &NamedType) -> bool {
|
||||||
|
match (a, b) {
|
||||||
|
(NamedType::Struct(fa), NamedType::Struct(fb)) => fields_match(fa, fb),
|
||||||
|
(NamedType::Alias(sa), NamedType::Alias(sb)) => shapes_match(sa, sb),
|
||||||
|
(NamedType::Enum(va), NamedType::Enum(vb)) => va == vb,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fields_match(a: &[StructField], b: &[StructField]) -> bool {
|
||||||
|
if a.len() != b.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
a.iter().zip(b.iter()).all(|(fa, fb)| {
|
||||||
|
fa.name == fb.name && fa.required == fb.required && shapes_match(&fa.shape, &fb.shape)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shapes_match(a: &TypeShape, b: &TypeShape) -> bool {
|
||||||
|
match (a, b) {
|
||||||
|
(TypeShape::Primitive(pa), TypeShape::Primitive(pb)) => {
|
||||||
|
std::mem::discriminant(pa) == std::mem::discriminant(pb)
|
||||||
|
}
|
||||||
|
(TypeShape::Ref(na), TypeShape::Ref(nb)) => {
|
||||||
|
// Refs match iff the named types they reference match.
|
||||||
|
match (resolve_type_shape(na), resolve_type_shape(nb)) {
|
||||||
|
(Some(ta), Some(tb)) => types_match(&ta, &tb),
|
||||||
|
_ => na == nb,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(TypeShape::List(ia), TypeShape::List(ib)) => shapes_match(ia, ib),
|
||||||
|
(TypeShape::Optional(ia), TypeShape::Optional(ib)) => shapes_match(ia, ib),
|
||||||
|
(TypeShape::Enum(va), TypeShape::Enum(vb)) => va == vb,
|
||||||
|
(TypeShape::Union(ba), TypeShape::Union(bb)) => {
|
||||||
|
ba.len() == bb.len() && ba.iter().zip(bb.iter()).all(|(x, y)| shapes_match(x, y))
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Panic with a structured message if the registered function graph is
|
||||||
|
/// inconsistent. Called from `build_ir()`.
|
||||||
|
pub fn verify_invariants() {
|
||||||
|
check_affects_targets();
|
||||||
|
check_merge_targets();
|
||||||
|
check_shared_param_types();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_affects_targets() {
|
||||||
|
for fn_spec in FUNCTIONS {
|
||||||
|
for affect in fn_spec.affects() {
|
||||||
|
if let AffectTarget::Context(name) = affect {
|
||||||
|
if lookup_context(name).is_none() {
|
||||||
|
panic!(
|
||||||
|
"Mizan graph-check: function `{}` declares `affects = \"{}\"` but no context with that name is registered. \
|
||||||
|
Either register a context with that name (via `#[mizan::context(\"{}\")]`) or remove the affects target.",
|
||||||
|
fn_spec.name(),
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_merge_targets() {
|
||||||
|
for fn_spec in FUNCTIONS {
|
||||||
|
for merge_target in fn_spec.merge() {
|
||||||
|
let ctx_entry = match lookup_context(merge_target) {
|
||||||
|
Some(c) => c,
|
||||||
|
None => panic!(
|
||||||
|
"Mizan graph-check: function `{}` declares `merge = \"{}\"` but no context with that name is registered.",
|
||||||
|
fn_spec.name(),
|
||||||
|
merge_target,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mutation_output = fn_spec.output_type();
|
||||||
|
let mutation_shape = match resolve_type_shape(mutation_output) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => panic!(
|
||||||
|
"Mizan graph-check: function `{}` has output type `{}` but no such named type is registered.",
|
||||||
|
fn_spec.name(), mutation_output,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let mut matches: Vec<&'static str> = Vec::new();
|
||||||
|
for candidate in FUNCTIONS {
|
||||||
|
if candidate.context() != Some(ctx_entry.name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(candidate_shape) = resolve_type_shape(candidate.output_type()) {
|
||||||
|
if types_match(&candidate_shape, &mutation_shape) {
|
||||||
|
matches.push(candidate.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches.is_empty() {
|
||||||
|
panic!(
|
||||||
|
"Mizan graph-check: function `{}` declares `merge = \"{}\"` but no member of that context has output type `{}`. \
|
||||||
|
Add a context member returning `{}`, or remove the merge declaration in favor of `affects` for plain refetch.",
|
||||||
|
fn_spec.name(), merge_target, mutation_output, mutation_output,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if matches.len() > 1 {
|
||||||
|
panic!(
|
||||||
|
"Mizan graph-check: function `{}` declares `merge = \"{}\"` but multiple members ({}) share output type `{}`. \
|
||||||
|
Merge resolution requires exactly one match. Distinguish the outputs or use `affects` for refetch.",
|
||||||
|
fn_spec.name(), merge_target, matches.join(", "), mutation_output,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_shared_param_types() {
|
||||||
|
for ctx in CONTEXTS {
|
||||||
|
let mut by_name: std::collections::HashMap<&'static str, (crate::ir::Primitive, &'static str)>
|
||||||
|
= std::collections::HashMap::new();
|
||||||
|
for fn_spec in FUNCTIONS {
|
||||||
|
if fn_spec.context() != Some(ctx.name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for p in fn_spec.input_params() {
|
||||||
|
if let Some((prev_primitive, prev_fn)) = by_name.get(p.name) {
|
||||||
|
if std::mem::discriminant(prev_primitive)
|
||||||
|
!= std::mem::discriminant(&p.primitive)
|
||||||
|
{
|
||||||
|
panic!(
|
||||||
|
"Mizan graph-check: context `{}` has a parameter `{}` whose type diverges across members. \
|
||||||
|
Function `{}` declares it as `{}`, function `{}` declares it as `{}`. \
|
||||||
|
Shared params must have one type across the whole context.",
|
||||||
|
ctx.name, p.name,
|
||||||
|
prev_fn, prev_primitive.name(),
|
||||||
|
fn_spec.name(), p.primitive.name(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
by_name.insert(p.name, (p.primitive, fn_spec.name()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
93
cores/mizan-rust/src/ir.rs
Normal file
93
cores/mizan-rust/src/ir.rs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
//! IR data model — mirrors `cores/mizan-python/src/mizan_core/ir.py` 1:1.
|
||||||
|
//!
|
||||||
|
//! The IR is the contract. Backends emit it; codegen consumes it. The Rust
|
||||||
|
//! side produces byte-equivalent KDL to the Python emitter against the same
|
||||||
|
//! function registry.
|
||||||
|
|
||||||
|
/// A named type that appears in the IR's `type "<Name>" { ... }` section.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum NamedType {
|
||||||
|
/// `type "X" { struct { field ... } }` — a Pydantic-model-shaped record.
|
||||||
|
Struct(Vec<StructField>),
|
||||||
|
/// `type "X" { alias { <type-child> } }` — a named wrapper around an
|
||||||
|
/// inline type shape, e.g. `userOrdersOutput = list[OrderOutput]`.
|
||||||
|
Alias(TypeShape),
|
||||||
|
/// `type "X" { enum "A" "B" ... }` — a string-literal enum.
|
||||||
|
Enum(Vec<&'static str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The set of in-place type shapes referenced from struct fields, function
|
||||||
|
/// inputs/outputs, and alias bodies.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TypeShape {
|
||||||
|
Primitive(Primitive),
|
||||||
|
Ref(&'static str),
|
||||||
|
List(Box<TypeShape>),
|
||||||
|
Optional(Box<TypeShape>),
|
||||||
|
Enum(Vec<&'static str>),
|
||||||
|
Union(Vec<TypeShape>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum Primitive {
|
||||||
|
Integer,
|
||||||
|
Number,
|
||||||
|
Boolean,
|
||||||
|
String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Primitive {
|
||||||
|
pub fn name(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Primitive::Integer => "integer",
|
||||||
|
Primitive::Number => "number",
|
||||||
|
Primitive::Boolean => "boolean",
|
||||||
|
Primitive::String => "string",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct StructField {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub required: bool,
|
||||||
|
pub default: Option<DefaultValue>,
|
||||||
|
pub shape: TypeShape,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum DefaultValue {
|
||||||
|
Integer(i64),
|
||||||
|
Number(f64),
|
||||||
|
Boolean(bool),
|
||||||
|
String(&'static str),
|
||||||
|
Null,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One descriptor of what a mutation `affects`. Mirrors Python's
|
||||||
|
/// `_normalize_affects` shape — either a named context or a named function.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum AffectTarget {
|
||||||
|
Context(&'static str),
|
||||||
|
Function {
|
||||||
|
name: &'static str,
|
||||||
|
context: Option<&'static str>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Transport {
|
||||||
|
Http,
|
||||||
|
Websocket,
|
||||||
|
Both,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Transport {
|
||||||
|
pub fn name(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Transport::Http => "http",
|
||||||
|
Transport::Websocket => "websocket",
|
||||||
|
Transport::Both => "both",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
397
cores/mizan-rust/src/kdl.rs
Normal file
397
cores/mizan-rust/src/kdl.rs
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
//! KDL emitter — byte-equivalent to `cores/mizan-python/src/mizan_core/ir.py`.
|
||||||
|
//!
|
||||||
|
//! The Python emitter is the spec; this is the second implementation under
|
||||||
|
//! the same contract. Any divergence is a bug here, not a contract change.
|
||||||
|
|
||||||
|
use crate::ir::{DefaultValue, NamedType, Primitive, StructField, TypeShape};
|
||||||
|
use crate::registry::{CONTEXTS, FUNCTIONS, TYPES};
|
||||||
|
use crate::traits::FunctionSpec;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
const INDENT: &str = " ";
|
||||||
|
|
||||||
|
/// Escape a string for KDL — same escape set as the Python emitter.
|
||||||
|
fn kdl_string(s: &str) -> String {
|
||||||
|
let mut out = String::with_capacity(s.len() + 2);
|
||||||
|
out.push('"');
|
||||||
|
for c in s.chars() {
|
||||||
|
match c {
|
||||||
|
'\\' => out.push_str("\\\\"),
|
||||||
|
'"' => out.push_str("\\\""),
|
||||||
|
'\n' => out.push_str("\\n"),
|
||||||
|
'\r' => out.push_str("\\r"),
|
||||||
|
'\t' => out.push_str("\\t"),
|
||||||
|
other => out.push(other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push('"');
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kdl_bool(b: bool) -> &'static str {
|
||||||
|
if b {
|
||||||
|
"#true"
|
||||||
|
} else {
|
||||||
|
"#false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kdl_default(v: &DefaultValue) -> String {
|
||||||
|
match v {
|
||||||
|
DefaultValue::Null => "#null".into(),
|
||||||
|
DefaultValue::Boolean(b) => kdl_bool(*b).into(),
|
||||||
|
DefaultValue::Integer(i) => i.to_string(),
|
||||||
|
DefaultValue::Number(f) => {
|
||||||
|
// Match Python's `repr(float)` for whole-number-equal-but-float
|
||||||
|
// values: e.g. 1.0 → "1.0", not "1".
|
||||||
|
if f.fract() == 0.0 && f.is_finite() {
|
||||||
|
format!("{f:.1}")
|
||||||
|
} else {
|
||||||
|
f.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DefaultValue::String(s) => kdl_string(s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert snake_case to camelCase. Matches Python's `_snake_to_camel`.
|
||||||
|
pub fn snake_to_camel(name: &str) -> String {
|
||||||
|
let normalized = name.replace('.', "_").replace('-', "_");
|
||||||
|
let mut parts = normalized.split('_');
|
||||||
|
let mut out = String::new();
|
||||||
|
if let Some(first) = parts.next() {
|
||||||
|
out.push_str(first);
|
||||||
|
}
|
||||||
|
for part in parts {
|
||||||
|
if part.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut chars = part.chars();
|
||||||
|
if let Some(c) = chars.next() {
|
||||||
|
out.extend(c.to_uppercase());
|
||||||
|
out.push_str(chars.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Emitter {
|
||||||
|
lines: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Emitter {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self { lines: Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prefix(&self, indent: usize) -> String {
|
||||||
|
INDENT.repeat(indent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn leaf(&mut self, indent: usize, parts: &[&str]) {
|
||||||
|
let mut line = self.prefix(indent);
|
||||||
|
line.push_str(&parts.join(" "));
|
||||||
|
self.lines.push(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open(&mut self, indent: usize, parts: &[&str]) {
|
||||||
|
let mut line = self.prefix(indent);
|
||||||
|
line.push_str(&parts.join(" "));
|
||||||
|
line.push_str(" {");
|
||||||
|
self.lines.push(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close(&mut self, indent: usize) {
|
||||||
|
let mut line = self.prefix(indent);
|
||||||
|
line.push('}');
|
||||||
|
self.lines.push(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blank(&mut self) {
|
||||||
|
self.lines.push(String::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_type_child(&mut self, indent: usize, shape: &TypeShape) {
|
||||||
|
match shape {
|
||||||
|
TypeShape::Primitive(p) => {
|
||||||
|
let name = kdl_string(p.name());
|
||||||
|
self.leaf(indent, &["primitive", &name]);
|
||||||
|
}
|
||||||
|
TypeShape::Ref(name) => {
|
||||||
|
let n = kdl_string(name);
|
||||||
|
self.leaf(indent, &["ref", &n]);
|
||||||
|
}
|
||||||
|
TypeShape::List(inner) => {
|
||||||
|
self.open(indent, &["list"]);
|
||||||
|
self.emit_type_child(indent + 1, inner);
|
||||||
|
self.close(indent);
|
||||||
|
}
|
||||||
|
TypeShape::Optional(inner) => {
|
||||||
|
self.open(indent, &["optional"]);
|
||||||
|
self.emit_type_child(indent + 1, inner);
|
||||||
|
self.close(indent);
|
||||||
|
}
|
||||||
|
TypeShape::Enum(variants) => {
|
||||||
|
let mut parts: Vec<String> = vec!["enum".into()];
|
||||||
|
for v in variants {
|
||||||
|
parts.push(kdl_string(v));
|
||||||
|
}
|
||||||
|
let line: Vec<&str> = parts.iter().map(String::as_str).collect();
|
||||||
|
self.leaf(indent, &line);
|
||||||
|
}
|
||||||
|
TypeShape::Union(branches) => {
|
||||||
|
self.open(indent, &["union"]);
|
||||||
|
for b in branches {
|
||||||
|
self.emit_type_child(indent + 1, b);
|
||||||
|
}
|
||||||
|
self.close(indent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_named_type(&mut self, indent: usize, name: &str, body: &NamedType) {
|
||||||
|
let name_lit = kdl_string(name);
|
||||||
|
self.open(indent, &["type", &name_lit]);
|
||||||
|
match body {
|
||||||
|
NamedType::Struct(fields) => {
|
||||||
|
self.open(indent + 1, &["struct"]);
|
||||||
|
for field in fields {
|
||||||
|
self.emit_struct_field(indent + 2, field);
|
||||||
|
}
|
||||||
|
self.close(indent + 1);
|
||||||
|
}
|
||||||
|
NamedType::Alias(inner) => {
|
||||||
|
self.open(indent + 1, &["alias"]);
|
||||||
|
self.emit_type_child(indent + 2, inner);
|
||||||
|
self.close(indent + 1);
|
||||||
|
}
|
||||||
|
NamedType::Enum(variants) => {
|
||||||
|
let mut parts: Vec<String> = vec!["enum".into()];
|
||||||
|
for v in variants {
|
||||||
|
parts.push(kdl_string(v));
|
||||||
|
}
|
||||||
|
let line: Vec<&str> = parts.iter().map(String::as_str).collect();
|
||||||
|
self.leaf(indent + 1, &line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.close(indent);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_struct_field(&mut self, indent: usize, field: &StructField) {
|
||||||
|
let name = kdl_string(field.name);
|
||||||
|
let mut header: Vec<String> = vec!["field".into(), name];
|
||||||
|
if !field.required {
|
||||||
|
header.push(format!("required={}", kdl_bool(false)));
|
||||||
|
if let Some(default) = &field.default {
|
||||||
|
header.push(format!("default={}", kdl_default(default)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let line_parts: Vec<&str> = header.iter().map(String::as_str).collect();
|
||||||
|
self.open(indent, &line_parts);
|
||||||
|
self.emit_type_child(indent + 1, &field.shape);
|
||||||
|
self.close(indent);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_function(&mut self, indent: usize, fn_spec: &dyn FunctionSpec) {
|
||||||
|
let name = kdl_string(fn_spec.name());
|
||||||
|
self.open(indent, &["function", &name]);
|
||||||
|
|
||||||
|
let camel = kdl_string(fn_spec.camel_name());
|
||||||
|
self.leaf(indent + 1, &["camel", &camel]);
|
||||||
|
|
||||||
|
self.leaf(indent + 1, &["has-input", kdl_bool(fn_spec.has_input())]);
|
||||||
|
|
||||||
|
if let Some(input_type) = fn_spec.input_type() {
|
||||||
|
let lit = kdl_string(input_type);
|
||||||
|
self.leaf(indent + 1, &["input", &lit]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let output_lit = kdl_string(fn_spec.output_type());
|
||||||
|
self.leaf(indent + 1, &["output", &output_lit]);
|
||||||
|
|
||||||
|
if fn_spec.output_nullable() {
|
||||||
|
self.leaf(indent + 1, &["output-nullable", kdl_bool(true)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let transport_lit = kdl_string(fn_spec.transport().name());
|
||||||
|
self.leaf(indent + 1, &["transport", &transport_lit]);
|
||||||
|
|
||||||
|
if let Some(ctx) = fn_spec.context() {
|
||||||
|
let lit = kdl_string(ctx);
|
||||||
|
self.leaf(indent + 1, &["context", &lit]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for affect in fn_spec.affects() {
|
||||||
|
// Mirror Python's behavior: only context-typed affects make it
|
||||||
|
// into the KDL `affects` leaf. Function-typed affects are
|
||||||
|
// reserved for a future IR extension.
|
||||||
|
if let crate::ir::AffectTarget::Context(name) = affect {
|
||||||
|
let lit = kdl_string(name);
|
||||||
|
self.leaf(indent + 1, &["affects", &lit]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for merge in fn_spec.merge() {
|
||||||
|
let lit = kdl_string(merge);
|
||||||
|
self.leaf(indent + 1, &["merge", &lit]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if fn_spec.is_form() {
|
||||||
|
self.leaf(indent + 1, &["is-form", kdl_bool(true)]);
|
||||||
|
if let Some(form_name) = fn_spec.form_name() {
|
||||||
|
let lit = kdl_string(form_name);
|
||||||
|
self.leaf(indent + 1, &["form-name", &lit]);
|
||||||
|
}
|
||||||
|
if let Some(form_role) = fn_spec.form_role() {
|
||||||
|
let lit = kdl_string(form_role);
|
||||||
|
self.leaf(indent + 1, &["form-role", &lit]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.close(indent);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_context(&mut self, indent: usize, ctx_name: &str, members: &[&'static dyn FunctionSpec]) {
|
||||||
|
let name_lit = kdl_string(ctx_name);
|
||||||
|
self.open(indent, &["context", &name_lit]);
|
||||||
|
|
||||||
|
// Function membership in registration order.
|
||||||
|
for fn_spec in members {
|
||||||
|
let lit = kdl_string(fn_spec.name());
|
||||||
|
self.leaf(indent + 1, &["function", &lit]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Param info — collect across every member, then emit alphabetized
|
||||||
|
// by param name to match Python.
|
||||||
|
struct ParamSlot {
|
||||||
|
primitive: Primitive,
|
||||||
|
shared_by: Vec<&'static str>,
|
||||||
|
}
|
||||||
|
let mut params: BTreeMap<&'static str, ParamSlot> = BTreeMap::new();
|
||||||
|
for fn_spec in members {
|
||||||
|
for p in fn_spec.input_params() {
|
||||||
|
let slot = params.entry(p.name).or_insert(ParamSlot {
|
||||||
|
primitive: p.primitive,
|
||||||
|
shared_by: Vec::new(),
|
||||||
|
});
|
||||||
|
slot.primitive = p.primitive;
|
||||||
|
slot.shared_by.push(fn_spec.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let member_count = members.len();
|
||||||
|
for (param_name, slot) in params.iter() {
|
||||||
|
let name_lit = kdl_string(param_name);
|
||||||
|
self.open(indent + 1, &["param", &name_lit]);
|
||||||
|
let type_lit = kdl_string(slot.primitive.name());
|
||||||
|
self.leaf(indent + 2, &["type", &type_lit]);
|
||||||
|
let required = slot.shared_by.len() == member_count;
|
||||||
|
self.leaf(indent + 2, &["required", kdl_bool(required)]);
|
||||||
|
for sharer in &slot.shared_by {
|
||||||
|
let lit = kdl_string(sharer);
|
||||||
|
self.leaf(indent + 2, &["shared-by", &lit]);
|
||||||
|
}
|
||||||
|
self.close(indent + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.close(indent);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_string(mut self) -> String {
|
||||||
|
// Trim trailing blanks, then add a single terminating newline.
|
||||||
|
while matches!(self.lines.last(), Some(s) if s.is_empty()) {
|
||||||
|
self.lines.pop();
|
||||||
|
}
|
||||||
|
let mut out = self.lines.join("\n");
|
||||||
|
out.push('\n');
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collected typed registries view used by `build_ir`.
|
||||||
|
pub(crate) struct IrSnapshot {
|
||||||
|
pub types: BTreeMap<&'static str, NamedType>,
|
||||||
|
pub functions: Vec<&'static dyn FunctionSpec>,
|
||||||
|
pub contexts: Vec<(&'static str, Vec<&'static dyn FunctionSpec>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IrSnapshot {
|
||||||
|
pub(crate) fn collect() -> Self {
|
||||||
|
// Types: alphabetized for byte-equivalence with Python's `sorted(named_types)`.
|
||||||
|
let mut types: BTreeMap<&'static str, NamedType> = BTreeMap::new();
|
||||||
|
for entry in TYPES {
|
||||||
|
types.insert(entry.name, (entry.shape_fn)());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Functions: alphabetical by wire name (canonical IR ordering,
|
||||||
|
// matches the Python emitter's `sorted(functions)`). Skip `private`.
|
||||||
|
let mut functions: Vec<&'static dyn FunctionSpec> = FUNCTIONS
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|f| !f.private())
|
||||||
|
.collect();
|
||||||
|
functions.sort_by_key(|f| f.name());
|
||||||
|
|
||||||
|
// Contexts: alphabetical by name (canonical IR ordering), each with
|
||||||
|
// its members sorted alphabetically too.
|
||||||
|
let mut context_names: Vec<&'static str> = CONTEXTS.iter().map(|c| c.name).collect();
|
||||||
|
context_names.sort();
|
||||||
|
let mut contexts: Vec<(&'static str, Vec<&'static dyn FunctionSpec>)> = Vec::new();
|
||||||
|
for name in context_names {
|
||||||
|
let mut members: Vec<&'static dyn FunctionSpec> = functions
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|f| f.context() == Some(name))
|
||||||
|
.collect();
|
||||||
|
members.sort_by_key(|f| f.name());
|
||||||
|
if !members.is_empty() {
|
||||||
|
contexts.push((name, members));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
types,
|
||||||
|
functions,
|
||||||
|
contexts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the Mizan IR for every registered type/function/context. Returns KDL.
|
||||||
|
pub fn build_ir() -> String {
|
||||||
|
crate::graph_check::verify_invariants();
|
||||||
|
let snap = IrSnapshot::collect();
|
||||||
|
let mut em = Emitter::new();
|
||||||
|
|
||||||
|
// Type definitions
|
||||||
|
let types_emitted = !snap.types.is_empty();
|
||||||
|
for (name, body) in &snap.types {
|
||||||
|
em.emit_named_type(0, name, body);
|
||||||
|
}
|
||||||
|
if types_emitted {
|
||||||
|
em.blank();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
let fns_emitted = !snap.functions.is_empty();
|
||||||
|
for fn_spec in &snap.functions {
|
||||||
|
em.emit_function(0, *fn_spec);
|
||||||
|
}
|
||||||
|
if fns_emitted {
|
||||||
|
em.blank();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contexts
|
||||||
|
let ctxs_emitted = !snap.contexts.is_empty();
|
||||||
|
for (ctx_name, members) in &snap.contexts {
|
||||||
|
em.emit_context(0, ctx_name, members);
|
||||||
|
}
|
||||||
|
if ctxs_emitted {
|
||||||
|
em.blank();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Future: channels — once channel registry lands on the Rust side.
|
||||||
|
|
||||||
|
em.into_string()
|
||||||
|
}
|
||||||
|
|
||||||
57
cores/mizan-rust/src/lib.rs
Normal file
57
cores/mizan-rust/src/lib.rs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
//! Mizan server-side IR substrate. Rust analog of `cores/mizan-python/src/mizan_core/`.
|
||||||
|
//!
|
||||||
|
//! Three load-bearing concerns:
|
||||||
|
//!
|
||||||
|
//! 1. **IR data model + KDL emitter.** `build_ir()` produces byte-equivalent
|
||||||
|
//! KDL to the Python emitter. Both backends emit the same contract.
|
||||||
|
//! 2. **Compile-time registry.** Proc macros from `mizan-macros` populate
|
||||||
|
//! linkme distributed slices (`TYPES`, `CONTEXTS`, `FUNCTIONS`) at the
|
||||||
|
//! consumer crate's expansion sites.
|
||||||
|
//! 3. **Runtime helpers.** `compute_invalidation` / `compute_merges` /
|
||||||
|
//! `lookup_function` ported from `mizan-fastapi`'s executor; the HTTP
|
||||||
|
//! adapter calls these per request.
|
||||||
|
//!
|
||||||
|
//! Consumers `use mizan_core::prelude::*;` and alias the crate as `mizan` at
|
||||||
|
//! their call sites so authored code reads `#[mizan::context]` / `#[mizan(...)]`.
|
||||||
|
|
||||||
|
pub mod graph_check;
|
||||||
|
pub mod ir;
|
||||||
|
pub mod kdl;
|
||||||
|
pub mod registry;
|
||||||
|
pub mod runtime;
|
||||||
|
pub mod traits;
|
||||||
|
|
||||||
|
pub use ir::{
|
||||||
|
AffectTarget, DefaultValue, NamedType, Primitive, StructField, Transport, TypeShape,
|
||||||
|
};
|
||||||
|
pub use kdl::{build_ir, snake_to_camel};
|
||||||
|
pub use registry::{
|
||||||
|
context_members, lookup_context, lookup_function, ContextEntry, TypeEntry, CONTEXTS,
|
||||||
|
FUNCTIONS, TYPES,
|
||||||
|
};
|
||||||
|
pub use runtime::{
|
||||||
|
compute_invalidation, compute_merges, InvalidationTarget, MergeEntry, MizanError,
|
||||||
|
RequestHandle,
|
||||||
|
};
|
||||||
|
pub use traits::{ContextMarker, FunctionSpec, InputParam, MizanType};
|
||||||
|
|
||||||
|
// Re-export proc macros so consumers depend on one crate.
|
||||||
|
pub use mizan_macros::{client, context, Mizan};
|
||||||
|
|
||||||
|
pub mod prelude {
|
||||||
|
pub use crate::ir::{
|
||||||
|
AffectTarget, DefaultValue, NamedType, Primitive, StructField, Transport, TypeShape,
|
||||||
|
};
|
||||||
|
pub use crate::registry::{ContextEntry, TypeEntry};
|
||||||
|
pub use crate::runtime::{MizanError, RequestHandle};
|
||||||
|
pub use crate::traits::{ContextMarker, FunctionSpec, InputParam, MizanType};
|
||||||
|
pub use mizan_macros::Mizan;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal re-exports used by `mizan-macros`-generated code. Not part of
|
||||||
|
/// the public API — consumers must not depend on names under `__priv`.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub mod __priv {
|
||||||
|
pub use linkme;
|
||||||
|
pub use serde_json;
|
||||||
|
}
|
||||||
47
cores/mizan-rust/src/registry.rs
Normal file
47
cores/mizan-rust/src/registry.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
//! Compile-time-populated registries, distributed across the consuming crate's
|
||||||
|
//! source via linkme. The proc macros emit `#[linkme::distributed_slice(...)]`
|
||||||
|
//! statics that land here at link time.
|
||||||
|
|
||||||
|
use crate::ir::NamedType;
|
||||||
|
use crate::traits::FunctionSpec;
|
||||||
|
use linkme::distributed_slice;
|
||||||
|
|
||||||
|
/// One named-type registration. Emitted by `#[derive(Mizan)]`.
|
||||||
|
pub struct TypeEntry {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub shape_fn: fn() -> NamedType,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One context-marker registration. Emitted by `#[mizan::context]`.
|
||||||
|
pub struct ContextEntry {
|
||||||
|
pub name: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[distributed_slice]
|
||||||
|
pub static TYPES: [TypeEntry] = [..];
|
||||||
|
|
||||||
|
#[distributed_slice]
|
||||||
|
pub static CONTEXTS: [ContextEntry] = [..];
|
||||||
|
|
||||||
|
#[distributed_slice]
|
||||||
|
pub static FUNCTIONS: [&'static dyn FunctionSpec] = [..];
|
||||||
|
|
||||||
|
/// Find a registered function by wire name. Used by the HTTP adapter.
|
||||||
|
pub fn lookup_function(name: &str) -> Option<&'static dyn FunctionSpec> {
|
||||||
|
FUNCTIONS.iter().copied().find(|f| f.name() == name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find a registered context by name. Used by graph_check.
|
||||||
|
pub fn lookup_context(name: &str) -> Option<&'static ContextEntry> {
|
||||||
|
CONTEXTS.iter().find(|c| c.name == name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All functions that declare a given context as their `context` membership.
|
||||||
|
/// Order matches `FUNCTIONS` iteration order — i.e., registration order.
|
||||||
|
pub fn context_members(ctx_name: &str) -> Vec<&'static dyn FunctionSpec> {
|
||||||
|
FUNCTIONS
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|f| f.context() == Some(ctx_name))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
252
cores/mizan-rust/src/runtime.rs
Normal file
252
cores/mizan-rust/src/runtime.rs
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
//! Runtime helpers — error envelope, request handle, invalidation/merge
|
||||||
|
//! resolution. Ports `compute_invalidation` / `compute_merges` /
|
||||||
|
//! `_resolve_merge_slot` / `_scoped_params` from
|
||||||
|
//! `backends/mizan-fastapi/src/mizan_fastapi/executor.py:189-263`.
|
||||||
|
|
||||||
|
use crate::registry::context_members;
|
||||||
|
use crate::traits::FunctionSpec;
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::any::Any;
|
||||||
|
|
||||||
|
/// Type-erased handle to the framework's request object. The HTTP adapter
|
||||||
|
/// stuffs its native `Request` here; user code casts back via the adapter's
|
||||||
|
/// helper types.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RequestHandle<'a> {
|
||||||
|
pub inner: &'a (dyn Any + Send + Sync),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> RequestHandle<'a> {
|
||||||
|
pub fn new<T: Any + Send + Sync>(req: &'a T) -> Self {
|
||||||
|
Self { inner: req }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn downcast<T: Any + Send + Sync>(&self) -> Option<&'a T> {
|
||||||
|
self.inner.downcast_ref::<T>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mizan's standard error envelope. Mirrors FastAPI's MizanError enum.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum MizanError {
|
||||||
|
NotFound(String),
|
||||||
|
BadRequest(String),
|
||||||
|
ValidationFailed {
|
||||||
|
message: String,
|
||||||
|
details: Value,
|
||||||
|
},
|
||||||
|
Unauthorized(String),
|
||||||
|
Forbidden(String),
|
||||||
|
NotImplementedYet(String),
|
||||||
|
InternalError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MizanError {
|
||||||
|
pub fn code(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
MizanError::NotFound(_) => "NOT_FOUND",
|
||||||
|
MizanError::BadRequest(_) => "BAD_REQUEST",
|
||||||
|
MizanError::ValidationFailed { .. } => "VALIDATION_FAILED",
|
||||||
|
MizanError::Unauthorized(_) => "UNAUTHORIZED",
|
||||||
|
MizanError::Forbidden(_) => "FORBIDDEN",
|
||||||
|
MizanError::NotImplementedYet(_) => "NOT_IMPLEMENTED",
|
||||||
|
MizanError::InternalError(_) => "INTERNAL_ERROR",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn message(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
MizanError::NotFound(m)
|
||||||
|
| MizanError::BadRequest(m)
|
||||||
|
| MizanError::Unauthorized(m)
|
||||||
|
| MizanError::Forbidden(m)
|
||||||
|
| MizanError::NotImplementedYet(m)
|
||||||
|
| MizanError::InternalError(m) => m,
|
||||||
|
MizanError::ValidationFailed { message, .. } => message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn http_status(&self) -> u16 {
|
||||||
|
match self {
|
||||||
|
MizanError::NotFound(_) => 404,
|
||||||
|
MizanError::BadRequest(_) => 400,
|
||||||
|
MizanError::ValidationFailed { .. } => 422,
|
||||||
|
MizanError::Unauthorized(_) => 401,
|
||||||
|
MizanError::Forbidden(_) => 403,
|
||||||
|
MizanError::NotImplementedYet(_) => 501,
|
||||||
|
MizanError::InternalError(_) => 500,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// JSON envelope shape consumers see on the wire.
|
||||||
|
pub fn to_json(&self) -> Value {
|
||||||
|
let mut body = serde_json::Map::new();
|
||||||
|
body.insert("code".into(), Value::String(self.code().into()));
|
||||||
|
body.insert("message".into(), Value::String(self.message().into()));
|
||||||
|
if let MizanError::ValidationFailed { details, .. } = self {
|
||||||
|
body.insert("details".into(), details.clone());
|
||||||
|
}
|
||||||
|
Value::Object({
|
||||||
|
let mut env = serde_json::Map::new();
|
||||||
|
env.insert("error".into(), Value::Object(body));
|
||||||
|
env
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One entry in the response's `invalidate` array.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum InvalidationTarget {
|
||||||
|
/// A whole context is invalidated.
|
||||||
|
Context(String),
|
||||||
|
/// A context, scoped to specific param values.
|
||||||
|
ScopedContext {
|
||||||
|
context: String,
|
||||||
|
params: serde_json::Map<String, Value>,
|
||||||
|
},
|
||||||
|
/// A specific function output is invalidated.
|
||||||
|
Function(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InvalidationTarget {
|
||||||
|
pub fn to_json(&self) -> Value {
|
||||||
|
match self {
|
||||||
|
InvalidationTarget::Context(name) => Value::String(name.clone()),
|
||||||
|
InvalidationTarget::ScopedContext { context, params } => {
|
||||||
|
let mut m = serde_json::Map::new();
|
||||||
|
m.insert("context".into(), Value::String(context.clone()));
|
||||||
|
m.insert("params".into(), Value::Object(params.clone()));
|
||||||
|
Value::Object(m)
|
||||||
|
}
|
||||||
|
InvalidationTarget::Function(name) => {
|
||||||
|
let mut m = serde_json::Map::new();
|
||||||
|
m.insert("function".into(), Value::String(name.clone()));
|
||||||
|
Value::Object(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One entry in the response's `merge` array. Server-resolved slot — the
|
||||||
|
/// kernel writes the value into `bundle[slot]` directly.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MergeEntry {
|
||||||
|
pub context: String,
|
||||||
|
pub slot: String,
|
||||||
|
pub value: Value,
|
||||||
|
pub params: Option<serde_json::Map<String, Value>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MergeEntry {
|
||||||
|
pub fn to_json(&self) -> Value {
|
||||||
|
let mut m = serde_json::Map::new();
|
||||||
|
m.insert("context".into(), Value::String(self.context.clone()));
|
||||||
|
m.insert("slot".into(), Value::String(self.slot.clone()));
|
||||||
|
m.insert("value".into(), self.value.clone());
|
||||||
|
if let Some(params) = &self.params {
|
||||||
|
m.insert("params".into(), Value::Object(params.clone()));
|
||||||
|
}
|
||||||
|
Value::Object(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the `invalidate` list from a function's `affects` metadata,
|
||||||
|
/// auto-scoping when arg names match context params.
|
||||||
|
pub fn compute_invalidation(
|
||||||
|
fn_spec: &dyn FunctionSpec,
|
||||||
|
args: &serde_json::Map<String, Value>,
|
||||||
|
) -> Vec<InvalidationTarget> {
|
||||||
|
fn_spec
|
||||||
|
.affects()
|
||||||
|
.iter()
|
||||||
|
.map(|target| match target {
|
||||||
|
crate::ir::AffectTarget::Context(name) => {
|
||||||
|
let scoped = scoped_params(name, args);
|
||||||
|
if scoped.is_empty() {
|
||||||
|
InvalidationTarget::Context((*name).into())
|
||||||
|
} else {
|
||||||
|
InvalidationTarget::ScopedContext {
|
||||||
|
context: (*name).into(),
|
||||||
|
params: scoped,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::ir::AffectTarget::Function { name, .. } => {
|
||||||
|
InvalidationTarget::Function((*name).into())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the `merge` list from a function's `merge` metadata. Each entry
|
||||||
|
/// names the slot inside the context bundle the return value lands in.
|
||||||
|
pub fn compute_merges(
|
||||||
|
fn_spec: &dyn FunctionSpec,
|
||||||
|
args: &serde_json::Map<String, Value>,
|
||||||
|
result: &Value,
|
||||||
|
) -> Vec<MergeEntry> {
|
||||||
|
let targets = fn_spec.merge();
|
||||||
|
if targets.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let mutation_output = fn_spec.output_type();
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for ctx_name in targets {
|
||||||
|
let slot = match resolve_merge_slot(ctx_name, mutation_output) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let scoped = scoped_params(ctx_name, args);
|
||||||
|
out.push(MergeEntry {
|
||||||
|
context: (*ctx_name).into(),
|
||||||
|
slot,
|
||||||
|
value: result.clone(),
|
||||||
|
params: if scoped.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(scoped)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the unique function-name slot whose Output type matches the
|
||||||
|
/// mutation's Output type. Matches Python's `types_match_for_merge` —
|
||||||
|
/// structural shape comparison, not name comparison. Returns None on no
|
||||||
|
/// match or ambiguous match.
|
||||||
|
fn resolve_merge_slot(context_name: &str, mutation_output: &str) -> Option<String> {
|
||||||
|
let mutation_shape = crate::graph_check::resolve_type_shape(mutation_output)?;
|
||||||
|
let mut matches: Vec<&'static str> = Vec::new();
|
||||||
|
for fn_spec in context_members(context_name) {
|
||||||
|
if let Some(candidate_shape) = crate::graph_check::resolve_type_shape(fn_spec.output_type())
|
||||||
|
{
|
||||||
|
if crate::graph_check::types_match(&candidate_shape, &mutation_shape) {
|
||||||
|
matches.push(fn_spec.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matches.len() == 1 {
|
||||||
|
Some(matches[0].into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Match input args against the context's declared Input field names.
|
||||||
|
fn scoped_params(
|
||||||
|
context_name: &str,
|
||||||
|
args: &serde_json::Map<String, Value>,
|
||||||
|
) -> serde_json::Map<String, Value> {
|
||||||
|
let mut declared: std::collections::HashSet<&'static str> = std::collections::HashSet::new();
|
||||||
|
for fn_spec in context_members(context_name) {
|
||||||
|
for p in fn_spec.input_params() {
|
||||||
|
declared.insert(p.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args.iter()
|
||||||
|
.filter(|(k, _)| declared.contains(k.as_str()))
|
||||||
|
.map(|(k, v)| (k.clone(), v.clone()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
92
cores/mizan-rust/src/traits.rs
Normal file
92
cores/mizan-rust/src/traits.rs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
//! Surface traits the proc macros implement.
|
||||||
|
|
||||||
|
use crate::ir::{AffectTarget, NamedType, Transport};
|
||||||
|
use crate::runtime::{MizanError, RequestHandle};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::pin::Pin;
|
||||||
|
|
||||||
|
/// A type that participates in the Mizan IR. Generated by `#[derive(Mizan)]`.
|
||||||
|
///
|
||||||
|
/// `TYPE_NAME` is a `const` (not a function) so it's usable in `static`
|
||||||
|
/// initializers — TypeEntry's `name` field reads it directly without an
|
||||||
|
/// init-time function call.
|
||||||
|
pub trait MizanType {
|
||||||
|
const TYPE_NAME: &'static str;
|
||||||
|
fn shape() -> NamedType;
|
||||||
|
|
||||||
|
fn type_name() -> &'static str {
|
||||||
|
Self::TYPE_NAME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A marker type for a Mizan context. Generated by `#[mizan::context]`.
|
||||||
|
pub trait ContextMarker {
|
||||||
|
const NAME: &'static str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One Mizan-registered function. Generated by `#[mizan(...)]` on async fns.
|
||||||
|
///
|
||||||
|
/// Everything here is plain data except `dispatch`, which is the type-erased
|
||||||
|
/// runtime entry point used by the HTTP adapter.
|
||||||
|
pub trait FunctionSpec: Send + Sync {
|
||||||
|
fn name(&self) -> &'static str;
|
||||||
|
fn camel_name(&self) -> &'static str;
|
||||||
|
fn has_input(&self) -> bool;
|
||||||
|
fn input_type(&self) -> Option<&'static str>;
|
||||||
|
fn output_type(&self) -> &'static str;
|
||||||
|
fn output_nullable(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn context(&self) -> Option<&'static str> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn affects(&self) -> &'static [AffectTarget] {
|
||||||
|
&[]
|
||||||
|
}
|
||||||
|
fn merge(&self) -> &'static [&'static str] {
|
||||||
|
&[]
|
||||||
|
}
|
||||||
|
fn transport(&self) -> Transport {
|
||||||
|
Transport::Http
|
||||||
|
}
|
||||||
|
fn private(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn is_form(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn form_name(&self) -> Option<&'static str> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn form_role(&self) -> Option<&'static str> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Field-shape description of this function's Input parameters, used by
|
||||||
|
/// the context builder to compute shared-param elevation. Empty when
|
||||||
|
/// `has_input()` is false.
|
||||||
|
fn input_params(&self) -> &'static [InputParam] {
|
||||||
|
&[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Type-erased dispatch. The HTTP adapter calls this with deserialized
|
||||||
|
/// JSON arguments; the macro-generated impl deserializes into the
|
||||||
|
/// function's typed input, awaits the body, and serializes the result.
|
||||||
|
fn dispatch<'a>(
|
||||||
|
&'a self,
|
||||||
|
req: RequestHandle<'a>,
|
||||||
|
args: Value,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<Value, MizanError>> + Send + 'a>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One parameter of a function's synthesized Input. The macro emits a static
|
||||||
|
/// slice of these so the context builder can find shared params across
|
||||||
|
/// context members and produce the `context { param ... shared-by ... }`
|
||||||
|
/// section of the IR.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct InputParam {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub primitive: crate::ir::Primitive,
|
||||||
|
pub required: bool,
|
||||||
|
}
|
||||||
129
cores/mizan-rust/tests/afi_parity.rs
Normal file
129
cores/mizan-rust/tests/afi_parity.rs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
//! 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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
protocol/mizan-codegen/tests/fixtures/afi_ir.kdl
vendored
64
protocol/mizan-codegen/tests/fixtures/afi_ir.kdl
vendored
@@ -128,36 +128,6 @@ function "echo" {
|
|||||||
output "echoOutput"
|
output "echoOutput"
|
||||||
transport "http"
|
transport "http"
|
||||||
}
|
}
|
||||||
function "whoami" {
|
|
||||||
camel "whoami"
|
|
||||||
has-input #false
|
|
||||||
output "whoamiOutput"
|
|
||||||
transport "http"
|
|
||||||
}
|
|
||||||
function "user_profile" {
|
|
||||||
camel "userProfile"
|
|
||||||
has-input #true
|
|
||||||
input "userProfileInput"
|
|
||||||
output "userProfileOutput"
|
|
||||||
transport "http"
|
|
||||||
context "user"
|
|
||||||
}
|
|
||||||
function "user_orders" {
|
|
||||||
camel "userOrders"
|
|
||||||
has-input #true
|
|
||||||
input "userOrdersInput"
|
|
||||||
output "userOrdersOutput"
|
|
||||||
transport "http"
|
|
||||||
context "user"
|
|
||||||
}
|
|
||||||
function "update_profile" {
|
|
||||||
camel "updateProfile"
|
|
||||||
has-input #true
|
|
||||||
input "updateProfileInput"
|
|
||||||
output "updateProfileOutput"
|
|
||||||
transport "http"
|
|
||||||
affects "user"
|
|
||||||
}
|
|
||||||
function "find_user" {
|
function "find_user" {
|
||||||
camel "findUser"
|
camel "findUser"
|
||||||
has-input #true
|
has-input #true
|
||||||
@@ -174,14 +144,44 @@ function "rename_user" {
|
|||||||
transport "http"
|
transport "http"
|
||||||
merge "user"
|
merge "user"
|
||||||
}
|
}
|
||||||
|
function "update_profile" {
|
||||||
|
camel "updateProfile"
|
||||||
|
has-input #true
|
||||||
|
input "updateProfileInput"
|
||||||
|
output "updateProfileOutput"
|
||||||
|
transport "http"
|
||||||
|
affects "user"
|
||||||
|
}
|
||||||
|
function "user_orders" {
|
||||||
|
camel "userOrders"
|
||||||
|
has-input #true
|
||||||
|
input "userOrdersInput"
|
||||||
|
output "userOrdersOutput"
|
||||||
|
transport "http"
|
||||||
|
context "user"
|
||||||
|
}
|
||||||
|
function "user_profile" {
|
||||||
|
camel "userProfile"
|
||||||
|
has-input #true
|
||||||
|
input "userProfileInput"
|
||||||
|
output "userProfileOutput"
|
||||||
|
transport "http"
|
||||||
|
context "user"
|
||||||
|
}
|
||||||
|
function "whoami" {
|
||||||
|
camel "whoami"
|
||||||
|
has-input #false
|
||||||
|
output "whoamiOutput"
|
||||||
|
transport "http"
|
||||||
|
}
|
||||||
|
|
||||||
context "user" {
|
context "user" {
|
||||||
function "user_profile"
|
|
||||||
function "user_orders"
|
function "user_orders"
|
||||||
|
function "user_profile"
|
||||||
param "user_id" {
|
param "user_id" {
|
||||||
type "integer"
|
type "integer"
|
||||||
required #true
|
required #true
|
||||||
shared-by "user_profile"
|
|
||||||
shared-by "user_orders"
|
shared-by "user_orders"
|
||||||
|
shared-by "user_profile"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,14 +36,6 @@ class MizanClient:
|
|||||||
raw = self._inner.call("echo", args.model_dump())
|
raw = self._inner.call("echo", args.model_dump())
|
||||||
return EchoOutput(**raw)
|
return EchoOutput(**raw)
|
||||||
|
|
||||||
def call_whoami(self) -> WhoamiOutput:
|
|
||||||
raw = self._inner.call("whoami", {})
|
|
||||||
return WhoamiOutput(**raw)
|
|
||||||
|
|
||||||
def call_update_profile(self, args: UpdateProfileInput) -> UpdateProfileOutput:
|
|
||||||
raw = self._inner.call("update_profile", args.model_dump())
|
|
||||||
return UpdateProfileOutput(**raw)
|
|
||||||
|
|
||||||
def call_find_user(self, args: FindUserInput) -> FindUserOutput | None:
|
def call_find_user(self, args: FindUserInput) -> FindUserOutput | None:
|
||||||
raw = self._inner.call("find_user", args.model_dump())
|
raw = self._inner.call("find_user", args.model_dump())
|
||||||
return FindUserOutput(**raw) if raw is not None else None
|
return FindUserOutput(**raw) if raw is not None else None
|
||||||
@@ -52,6 +44,14 @@ class MizanClient:
|
|||||||
raw = self._inner.call("rename_user", args.model_dump())
|
raw = self._inner.call("rename_user", args.model_dump())
|
||||||
return RenameUserOutput(**raw)
|
return RenameUserOutput(**raw)
|
||||||
|
|
||||||
|
def call_update_profile(self, args: UpdateProfileInput) -> UpdateProfileOutput:
|
||||||
|
raw = self._inner.call("update_profile", args.model_dump())
|
||||||
|
return UpdateProfileOutput(**raw)
|
||||||
|
|
||||||
|
def call_whoami(self) -> WhoamiOutput:
|
||||||
|
raw = self._inner.call("whoami", {})
|
||||||
|
return WhoamiOutput(**raw)
|
||||||
|
|
||||||
def invalidate(self, context: str) -> None:
|
def invalidate(self, context: str) -> None:
|
||||||
self._inner.invalidate(context)
|
self._inner.invalidate(context)
|
||||||
|
|
||||||
@@ -63,5 +63,5 @@ class MizanClient:
|
|||||||
|
|
||||||
class UserContextData(BaseModel):
|
class UserContextData(BaseModel):
|
||||||
"""Bundled return of fetch_user_context."""
|
"""Bundled return of fetch_user_context."""
|
||||||
user_profile: UserProfileOutput
|
|
||||||
user_orders: UserOrdersOutput
|
user_orders: UserOrdersOutput
|
||||||
|
user_profile: UserProfileOutput
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
import { mizanFetch } from '@mizan/base'
|
import { mizanFetch } from '@mizan/base'
|
||||||
|
|
||||||
import type { userProfileOutput, userOrdersOutput } from '../types'
|
import type { userOrdersOutput, userProfileOutput } from '../types'
|
||||||
|
|
||||||
export interface UserContextData {
|
export interface UserContextData {
|
||||||
user_profile: userProfileOutput
|
|
||||||
user_orders: userOrdersOutput
|
user_orders: userOrdersOutput
|
||||||
|
user_profile: userProfileOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserContextParams {
|
export interface UserContextParams {
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ export * from './types'
|
|||||||
export { fetchUserContext, type UserContextData, type UserContextParams } from './contexts/user'
|
export { fetchUserContext, type UserContextData, type UserContextParams } from './contexts/user'
|
||||||
|
|
||||||
export { callEcho } from './functions/echo'
|
export { callEcho } from './functions/echo'
|
||||||
export { callWhoami } from './functions/whoami'
|
|
||||||
export { callUpdateProfile } from './mutations/updateProfile'
|
|
||||||
export { callFindUser } from './functions/findUser'
|
export { callFindUser } from './functions/findUser'
|
||||||
export { callRenameUser } from './functions/renameUser'
|
export { callRenameUser } from './functions/renameUser'
|
||||||
|
export { callUpdateProfile } from './mutations/updateProfile'
|
||||||
|
export { callWhoami } from './functions/whoami'
|
||||||
|
|
||||||
// Stage 2 framework adapter
|
// Stage 2 framework adapter
|
||||||
export * from './react'
|
export * from './react'
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
type ContextState,
|
type ContextState,
|
||||||
} from '@mizan/base'
|
} from '@mizan/base'
|
||||||
|
|
||||||
import { fetchUserContext, type UserContextData, type UserContextParams, callUpdateProfile, callEcho, callWhoami, callFindUser, callRenameUser, type userProfileOutput, type userOrdersOutput } from './index'
|
import { fetchUserContext, type UserContextData, type UserContextParams, callUpdateProfile, callEcho, callFindUser, callRenameUser, callWhoami, type userOrdersOutput, type userProfileOutput } from './index'
|
||||||
|
|
||||||
// Internal — runs inside a Provider, registers with the kernel exactly once.
|
// Internal — runs inside a Provider, registers with the kernel exactly once.
|
||||||
function useContextSubscription<T>(
|
function useContextSubscription<T>(
|
||||||
@@ -89,14 +89,14 @@ export function useUserContext(): ContextState<UserContextData> {
|
|||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUserProfile(): userProfileOutput | null {
|
|
||||||
return useUserContext().data?.user_profile ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useUserOrders(): userOrdersOutput | null {
|
export function useUserOrders(): userOrdersOutput | null {
|
||||||
return useUserContext().data?.user_orders ?? null
|
return useUserContext().data?.user_orders ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useUserProfile(): userProfileOutput | null {
|
||||||
|
return useUserContext().data?.user_profile ?? null
|
||||||
|
}
|
||||||
|
|
||||||
export function useUpdateProfile() {
|
export function useUpdateProfile() {
|
||||||
return useMutation<Parameters<typeof callUpdateProfile>[0], Awaited<ReturnType<typeof callUpdateProfile>>>(callUpdateProfile)
|
return useMutation<Parameters<typeof callUpdateProfile>[0], Awaited<ReturnType<typeof callUpdateProfile>>>(callUpdateProfile)
|
||||||
}
|
}
|
||||||
@@ -105,10 +105,6 @@ export function useEcho() {
|
|||||||
return useMutation<Parameters<typeof callEcho>[0], Awaited<ReturnType<typeof callEcho>>>(callEcho)
|
return useMutation<Parameters<typeof callEcho>[0], Awaited<ReturnType<typeof callEcho>>>(callEcho)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWhoami() {
|
|
||||||
return useMutation<void, Awaited<ReturnType<typeof callWhoami>>>(() => callWhoami() as any)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useFindUser() {
|
export function useFindUser() {
|
||||||
return useMutation<Parameters<typeof callFindUser>[0], Awaited<ReturnType<typeof callFindUser>>>(callFindUser)
|
return useMutation<Parameters<typeof callFindUser>[0], Awaited<ReturnType<typeof callFindUser>>>(callFindUser)
|
||||||
}
|
}
|
||||||
@@ -117,6 +113,10 @@ export function useRenameUser() {
|
|||||||
return useMutation<Parameters<typeof callRenameUser>[0], Awaited<ReturnType<typeof callRenameUser>>>(callRenameUser)
|
return useMutation<Parameters<typeof callRenameUser>[0], Awaited<ReturnType<typeof callRenameUser>>>(callRenameUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useWhoami() {
|
||||||
|
return useMutation<void, Awaited<ReturnType<typeof callWhoami>>>(() => callWhoami() as any)
|
||||||
|
}
|
||||||
|
|
||||||
// ── MizanContext root provider ──
|
// ── MizanContext root provider ──
|
||||||
|
|
||||||
export interface MizanContextProps {
|
export interface MizanContextProps {
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ use serde_json::Value;
|
|||||||
|
|
||||||
use mizan_rust::{MizanClient, MizanError};
|
use mizan_rust::{MizanClient, MizanError};
|
||||||
|
|
||||||
use crate::types::{UserProfileOutput, UserOrdersOutput};
|
use crate::types::{UserOrdersOutput, UserProfileOutput};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct UserContextData {
|
pub struct UserContextData {
|
||||||
pub user_profile: UserProfileOutput,
|
|
||||||
pub user_orders: UserOrdersOutput,
|
pub user_orders: UserOrdersOutput,
|
||||||
|
pub user_profile: UserProfileOutput,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
import { mizanFetch } from '@mizan/base'
|
import { mizanFetch } from '@mizan/base'
|
||||||
|
|
||||||
import type { userProfileOutput, userOrdersOutput } from '../types'
|
import type { userOrdersOutput, userProfileOutput } from '../types'
|
||||||
|
|
||||||
export interface UserContextData {
|
export interface UserContextData {
|
||||||
user_profile: userProfileOutput
|
|
||||||
user_orders: userOrdersOutput
|
user_orders: userOrdersOutput
|
||||||
|
user_profile: userProfileOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserContextParams {
|
export interface UserContextParams {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export * from './types'
|
|||||||
export { fetchUserContext, type UserContextData, type UserContextParams } from './contexts/user'
|
export { fetchUserContext, type UserContextData, type UserContextParams } from './contexts/user'
|
||||||
|
|
||||||
export { callEcho } from './functions/echo'
|
export { callEcho } from './functions/echo'
|
||||||
export { callWhoami } from './functions/whoami'
|
|
||||||
export { callUpdateProfile } from './mutations/updateProfile'
|
|
||||||
export { callFindUser } from './functions/findUser'
|
export { callFindUser } from './functions/findUser'
|
||||||
export { callRenameUser } from './functions/renameUser'
|
export { callRenameUser } from './functions/renameUser'
|
||||||
|
export { callUpdateProfile } from './mutations/updateProfile'
|
||||||
|
export { callWhoami } from './functions/whoami'
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
import { mizanFetch } from '@mizan/base'
|
import { mizanFetch } from '@mizan/base'
|
||||||
|
|
||||||
import type { userProfileOutput, userOrdersOutput } from '../types'
|
import type { userOrdersOutput, userProfileOutput } from '../types'
|
||||||
|
|
||||||
export interface UserContextData {
|
export interface UserContextData {
|
||||||
user_profile: userProfileOutput
|
|
||||||
user_orders: userOrdersOutput
|
user_orders: userOrdersOutput
|
||||||
|
user_profile: userProfileOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserContextParams {
|
export interface UserContextParams {
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ export * from './types'
|
|||||||
export { fetchUserContext, type UserContextData, type UserContextParams } from './contexts/user'
|
export { fetchUserContext, type UserContextData, type UserContextParams } from './contexts/user'
|
||||||
|
|
||||||
export { callEcho } from './functions/echo'
|
export { callEcho } from './functions/echo'
|
||||||
export { callWhoami } from './functions/whoami'
|
|
||||||
export { callUpdateProfile } from './mutations/updateProfile'
|
|
||||||
export { callFindUser } from './functions/findUser'
|
export { callFindUser } from './functions/findUser'
|
||||||
export { callRenameUser } from './functions/renameUser'
|
export { callRenameUser } from './functions/renameUser'
|
||||||
|
export { callUpdateProfile } from './mutations/updateProfile'
|
||||||
|
export { callWhoami } from './functions/whoami'
|
||||||
|
|
||||||
// Stage 2 framework adapter
|
// Stage 2 framework adapter
|
||||||
export * from './svelte'
|
export * from './svelte'
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { readable, type Readable } from 'svelte/store'
|
import { readable, type Readable } from 'svelte/store'
|
||||||
import { registerContext, type ContextState } from '@mizan/base'
|
import { registerContext, type ContextState } from '@mizan/base'
|
||||||
|
|
||||||
import { fetchUserContext, type UserContextData, type UserContextParams, callUpdateProfile, callEcho, callWhoami, callFindUser, callRenameUser } from '../index'
|
import { fetchUserContext, type UserContextData, type UserContextParams, callUpdateProfile, callEcho, callFindUser, callRenameUser, callWhoami } from '../index'
|
||||||
|
|
||||||
export function createUserContext(params: UserContextParams) {
|
export function createUserContext(params: UserContextParams) {
|
||||||
const store = readable<ContextState<UserContextData>>(
|
const store = readable<ContextState<UserContextData>>(
|
||||||
@@ -21,9 +21,9 @@ export function createUserContext(params: UserContextParams) {
|
|||||||
|
|
||||||
export { callUpdateProfile } from '../index'
|
export { callUpdateProfile } from '../index'
|
||||||
export { callEcho } from '../index'
|
export { callEcho } from '../index'
|
||||||
export { callWhoami } from '../index'
|
|
||||||
export { callFindUser } from '../index'
|
export { callFindUser } from '../index'
|
||||||
export { callRenameUser } from '../index'
|
export { callRenameUser } from '../index'
|
||||||
|
export { callWhoami } from '../index'
|
||||||
|
|
||||||
export type { ContextState } from '@mizan/base'
|
export type { ContextState } from '@mizan/base'
|
||||||
export { configure, initSession, MizanError } from '@mizan/base'
|
export { configure, initSession, MizanError } from '@mizan/base'
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
import { mizanFetch } from '@mizan/base'
|
import { mizanFetch } from '@mizan/base'
|
||||||
|
|
||||||
import type { userProfileOutput, userOrdersOutput } from '../types'
|
import type { userOrdersOutput, userProfileOutput } from '../types'
|
||||||
|
|
||||||
export interface UserContextData {
|
export interface UserContextData {
|
||||||
user_profile: userProfileOutput
|
|
||||||
user_orders: userOrdersOutput
|
user_orders: userOrdersOutput
|
||||||
|
user_profile: userProfileOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserContextParams {
|
export interface UserContextParams {
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ export * from './types'
|
|||||||
export { fetchUserContext, type UserContextData, type UserContextParams } from './contexts/user'
|
export { fetchUserContext, type UserContextData, type UserContextParams } from './contexts/user'
|
||||||
|
|
||||||
export { callEcho } from './functions/echo'
|
export { callEcho } from './functions/echo'
|
||||||
export { callWhoami } from './functions/whoami'
|
|
||||||
export { callUpdateProfile } from './mutations/updateProfile'
|
|
||||||
export { callFindUser } from './functions/findUser'
|
export { callFindUser } from './functions/findUser'
|
||||||
export { callRenameUser } from './functions/renameUser'
|
export { callRenameUser } from './functions/renameUser'
|
||||||
|
export { callUpdateProfile } from './mutations/updateProfile'
|
||||||
|
export { callWhoami } from './functions/whoami'
|
||||||
|
|
||||||
// Stage 2 framework adapter
|
// Stage 2 framework adapter
|
||||||
export * from './vue'
|
export * from './vue'
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { ref, computed, onMounted, onUnmounted, onServerPrefetch, type ComputedRef } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, onServerPrefetch, type ComputedRef } from 'vue'
|
||||||
import { registerContext, type ContextState } from '@mizan/base'
|
import { registerContext, type ContextState } from '@mizan/base'
|
||||||
|
|
||||||
import { fetchUserContext, type UserContextData, type UserContextParams, callUpdateProfile, callEcho, callWhoami, callFindUser, callRenameUser } from '../index'
|
import { fetchUserContext, type UserContextData, type UserContextParams, callUpdateProfile, callEcho, callFindUser, callRenameUser, callWhoami } from '../index'
|
||||||
|
|
||||||
export function useUserContext(params: UserContextParams) {
|
export function useUserContext(params: UserContextParams) {
|
||||||
const state = ref<ContextState<UserContextData>>({ data: null, status: 'idle', error: null })
|
const state = ref<ContextState<UserContextData>>({ data: null, status: 'idle', error: null })
|
||||||
@@ -25,8 +25,8 @@ export function useUserContext(params: UserContextParams) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
userProfile: computed(() => state.value.data?.user_profile ?? null) as ComputedRef<userProfileOutput | null>,
|
|
||||||
userOrders: computed(() => state.value.data?.user_orders ?? null) as ComputedRef<userOrdersOutput | null>,
|
userOrders: computed(() => state.value.data?.user_orders ?? null) as ComputedRef<userOrdersOutput | null>,
|
||||||
|
userProfile: computed(() => state.value.data?.user_profile ?? null) as ComputedRef<userProfileOutput | null>,
|
||||||
loading: computed(() => state.value.status === 'loading'),
|
loading: computed(() => state.value.status === 'loading'),
|
||||||
error: computed(() => state.value.error),
|
error: computed(() => state.value.error),
|
||||||
}
|
}
|
||||||
@@ -56,18 +56,6 @@ export function useEcho() {
|
|||||||
return { mutate, isPending, error }
|
return { mutate, isPending, error }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWhoami() {
|
|
||||||
const isPending = ref(false)
|
|
||||||
const error = ref<Error | null>(null)
|
|
||||||
async function mutate() {
|
|
||||||
isPending.value = true; error.value = null
|
|
||||||
try { return await callWhoami() }
|
|
||||||
catch (e) { error.value = e as Error; throw e }
|
|
||||||
finally { isPending.value = false }
|
|
||||||
}
|
|
||||||
return { mutate, isPending, error }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useFindUser() {
|
export function useFindUser() {
|
||||||
const isPending = ref(false)
|
const isPending = ref(false)
|
||||||
const error = ref<Error | null>(null)
|
const error = ref<Error | null>(null)
|
||||||
@@ -92,5 +80,17 @@ export function useRenameUser() {
|
|||||||
return { mutate, isPending, error }
|
return { mutate, isPending, error }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useWhoami() {
|
||||||
|
const isPending = ref(false)
|
||||||
|
const error = ref<Error | null>(null)
|
||||||
|
async function mutate() {
|
||||||
|
isPending.value = true; error.value = null
|
||||||
|
try { return await callWhoami() }
|
||||||
|
catch (e) { error.value = e as Error; throw e }
|
||||||
|
finally { isPending.value = false }
|
||||||
|
}
|
||||||
|
return { mutate, isPending, error }
|
||||||
|
}
|
||||||
|
|
||||||
export type { ContextState } from '@mizan/base'
|
export type { ContextState } from '@mizan/base'
|
||||||
export { configure, initSession, MizanError } from '@mizan/base'
|
export { configure, initSession, MizanError } from '@mizan/base'
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
"""Minimal Django settings for the AFI conformance fixture."""
|
"""Minimal Django settings for the AFI conformance fixture."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
SECRET_KEY = "afi-conformance-test-only"
|
SECRET_KEY = "afi-conformance-test-only"
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
ALLOWED_HOSTS = ["*"]
|
ALLOWED_HOSTS = ["*"]
|
||||||
|
|||||||
679
tests/afi/rust_app/Cargo.lock
generated
Normal file
679
tests/afi/rust_app/Cargo.lock
generated
Normal file
@@ -0,0 +1,679 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "afi_rust_app"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"mizan-axum",
|
||||||
|
"mizan-core",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-trait"
|
||||||
|
version = "0.1.89"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomic-waker"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum"
|
||||||
|
version = "0.7.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"axum-core",
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"itoa",
|
||||||
|
"matchit",
|
||||||
|
"memchr",
|
||||||
|
"mime",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustversion",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_path_to_error",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-core"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"mime",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustversion",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "form_urlencoded"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-channel"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"itoa",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-body"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-body-util"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httparse"
|
||||||
|
version = "1.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpdate"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"bytes",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"httparse",
|
||||||
|
"httpdate",
|
||||||
|
"itoa",
|
||||||
|
"pin-project-lite",
|
||||||
|
"smallvec",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-util"
|
||||||
|
version = "0.1.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"hyper",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.186"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linkme"
|
||||||
|
version = "0.3.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e83272d46373fb8decca684579ac3e7c8f3d71d4cc3aa693df8759e260ae41cf"
|
||||||
|
dependencies = [
|
||||||
|
"linkme-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linkme-impl"
|
||||||
|
version = "0.3.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32d59e20403c7d08fe62b4376edfe5c7fb2ef1e6b1465379686d0f21c8df444b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||||
|
dependencies = [
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchit"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime"
|
||||||
|
version = "0.3.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mizan-axum"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"mizan-core",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"tower-http",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mizan-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"linkme",
|
||||||
|
"mizan-macros",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mizan-macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot"
|
||||||
|
version = "0.12.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||||
|
dependencies = [
|
||||||
|
"lock_api",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.9.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"smallvec",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "percent-encoding"
|
||||||
|
version = "2.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.149"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_path_to_error"
|
||||||
|
version = "0.1.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_urlencoded"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-registry"
|
||||||
|
version = "1.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||||
|
dependencies = [
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.117"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sync_wrapper"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio"
|
||||||
|
version = "1.52.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
|
"pin-project-lite",
|
||||||
|
"signal-hook-registry",
|
||||||
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"pin-project-lite",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-http"
|
||||||
|
version = "0.6.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-layer"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-service"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing"
|
||||||
|
version = "0.1.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-core"
|
||||||
|
version = "0.1.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
21
tests/afi/rust_app/Cargo.toml
Normal file
21
tests/afi/rust_app/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "afi_rust_app"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
mizan-core = { path = "../../../cores/mizan-rust" }
|
||||||
|
mizan-axum = { path = "../../../backends/mizan-rust-axum" }
|
||||||
|
axum = "0.7"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "server"
|
||||||
|
path = "src/bin/server.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "export-ir"
|
||||||
|
path = "src/bin/export_ir.rs"
|
||||||
10
tests/afi/rust_app/src/bin/export_ir.rs
Normal file
10
tests/afi/rust_app/src/bin/export_ir.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
//! Emit the AFI fixture's Mizan IR (KDL) to stdout. The codegen subprocess
|
||||||
|
//! consumes this; the three-way codegen-parity test asserts it equals what
|
||||||
|
//! Django and FastAPI emit.
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// The fixture is registered via the `afi_rust_app` library crate at
|
||||||
|
// link time — referencing any symbol keeps the linkme statics alive.
|
||||||
|
let _ = afi_rust_app::echo;
|
||||||
|
print!("{}", mizan_core::build_ir());
|
||||||
|
}
|
||||||
25
tests/afi/rust_app/src/bin/server.rs
Normal file
25
tests/afi/rust_app/src/bin/server.rs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
//! Serve the AFI fixture under axum on `PORT` env var (or 8765 default).
|
||||||
|
//! Used by the wire-parity test as the third-backend probe target.
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// Keep the fixture's linkme statics alive by touching one of its
|
||||||
|
// symbols (the bin would otherwise dead-strip the library crate).
|
||||||
|
let _ = afi_rust_app::echo;
|
||||||
|
|
||||||
|
let port: u16 = std::env::var("PORT")
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.unwrap_or(8765);
|
||||||
|
|
||||||
|
let app = Router::new().nest("/api/mizan", mizan_axum::router());
|
||||||
|
|
||||||
|
let bind = format!("127.0.0.1:{port}");
|
||||||
|
let listener = tokio::net::TcpListener::bind(&bind)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e| panic!("bind {bind}: {e}"));
|
||||||
|
eprintln!("afi_rust_app listening on http://{bind}");
|
||||||
|
axum::serve(listener, app).await.unwrap();
|
||||||
|
}
|
||||||
94
tests/afi/rust_app/src/lib.rs
Normal file
94
tests/afi/rust_app/src/lib.rs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
//! AFI fixture — Rust port of `tests/afi/fixture.py`.
|
||||||
|
//!
|
||||||
|
//! Same 7 functions, same 5 shared types, same context+affects+merge graph.
|
||||||
|
//! The KDL emitted by `build_ir()` against this registry is byte-identical
|
||||||
|
//! to the canonical Python-emitted `protocol/mizan-codegen/tests/fixtures/
|
||||||
|
//! afi_ir.kdl` — gated by the three-way codegen-parity test.
|
||||||
|
|
||||||
|
use mizan_core as mizan;
|
||||||
|
use mizan_core::prelude::*;
|
||||||
|
use mizan_core::RequestHandle;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[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;
|
||||||
|
|
||||||
|
#[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 }
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
AFI conformance — same @client fixture, same Mizan IR (KDL), both adapters.
|
AFI conformance — same fixture, same Mizan IR (KDL), all three adapters.
|
||||||
|
|
||||||
Gates that mizan-django and mizan-fastapi emit byte-equivalent IR
|
Gates that mizan-django, mizan-fastapi, and the Rust mizan-axum backend
|
||||||
for the same registered functions. If this passes, the codegen
|
emit byte-equivalent KDL for the same registered functions. The IR is the
|
||||||
produces identical TypeScript output regardless of backend
|
contract; whatever language wrote the backend, the wire/codegen-facing
|
||||||
(codegen is deterministic over IR input).
|
artifact is identical.
|
||||||
|
|
||||||
Substrate-level gate, not e2e. Catches adapter symmetry problems —
|
Substrate-level gate, not e2e. Catches adapter symmetry problems —
|
||||||
type-introspection divergence, ordering non-determinism — without
|
type-introspection divergence, ordering non-determinism — across all
|
||||||
running a real frontend or backend.
|
backends in one place.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -23,6 +23,7 @@ import pytest
|
|||||||
|
|
||||||
HERE = Path(__file__).parent
|
HERE = Path(__file__).parent
|
||||||
DJANGO_MANAGE = HERE / "django_app" / "manage.py"
|
DJANGO_MANAGE = HERE / "django_app" / "manage.py"
|
||||||
|
RUST_APP_DIR = HERE / "rust_app"
|
||||||
|
|
||||||
|
|
||||||
def _fetch_django_ir() -> str:
|
def _fetch_django_ir() -> str:
|
||||||
@@ -56,17 +57,60 @@ def _fetch_fastapi_ir() -> str:
|
|||||||
sys.path.remove(str(HERE))
|
sys.path.remove(str(HERE))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
def _fetch_rust_ir() -> str:
|
||||||
def ir_pair() -> tuple[str, str]:
|
"""Spawn the Rust `export-ir` bin and capture stdout."""
|
||||||
return _fetch_django_ir(), _fetch_fastapi_ir()
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"cargo", "run",
|
||||||
class IRParityTests:
|
"--quiet", "--release",
|
||||||
"""The Mizan IR is the contract — both backends must emit the same KDL."""
|
"--manifest-path", str(RUST_APP_DIR / "Cargo.toml"),
|
||||||
|
"--bin", "export-ir",
|
||||||
def test_ir_bytes_match(self, ir_pair):
|
],
|
||||||
django, fastapi = ir_pair
|
capture_output=True,
|
||||||
assert django == fastapi, (
|
text=True,
|
||||||
"Django and FastAPI emit divergent Mizan IR for the same "
|
check=False,
|
||||||
"registered functions. Substrate gate is now red."
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
pytest.fail(
|
||||||
|
f"Rust export-ir failed:\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}",
|
||||||
)
|
)
|
||||||
|
return result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def fastapi_ir() -> str:
|
||||||
|
return _fetch_fastapi_ir()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def rust_ir() -> str:
|
||||||
|
return _fetch_rust_ir()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def django_ir() -> str:
|
||||||
|
return _fetch_django_ir()
|
||||||
|
|
||||||
|
|
||||||
|
def test_fastapi_matches_rust(fastapi_ir: str, rust_ir: str) -> None:
|
||||||
|
"""FastAPI ≡ Rust. The Mizan IR is the contract across languages."""
|
||||||
|
assert fastapi_ir == rust_ir, (
|
||||||
|
"FastAPI and Rust emit divergent Mizan IR for the same registered "
|
||||||
|
"functions. Substrate gate is red."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_django_matches_fastapi(django_ir: str, fastapi_ir: str) -> None:
|
||||||
|
"""Django ≡ FastAPI."""
|
||||||
|
assert django_ir == fastapi_ir, (
|
||||||
|
"Django and FastAPI emit divergent Mizan IR for the same "
|
||||||
|
"registered functions. Substrate gate is red."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_three_match(django_ir: str, fastapi_ir: str, rust_ir: str) -> None:
|
||||||
|
"""All three backends emit byte-identical KDL."""
|
||||||
|
assert django_ir == fastapi_ir == rust_ir, (
|
||||||
|
"Three-way IR divergence — see test_django_matches_fastapi and "
|
||||||
|
"test_fastapi_matches_rust for which pair drifts."
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
"""Drive the wire-parity check end-to-end.
|
"""Three-way wire-parity check.
|
||||||
|
|
||||||
1. Boot the FastAPI fixture app via uvicorn on a free port.
|
For each backend (FastAPI, Rust axum):
|
||||||
2. Poll /openapi.json until the server is up.
|
1. Boot the fixture server on a free port.
|
||||||
3. Run the Rust `drive_kernel` binary (raw kernel calls) against it.
|
2. Poll /api/mizan/session/ until the server responds.
|
||||||
4. Run the Rust `drive_emitted` binary (typed codegen functions) against
|
3. Run the Rust `drive_kernel` binary (raw kernel calls) against it.
|
||||||
the same server.
|
4. Run the Rust `drive_emitted` binary (typed codegen functions) against it.
|
||||||
5. Tear the server down.
|
5. Tear the server down.
|
||||||
|
|
||||||
Either non-zero driver exit propagates as the script's exit code.
|
Any non-zero driver exit propagates as the script's exit code. Adding the
|
||||||
|
Rust backend here proves that mizan-axum honors the same wire contract as
|
||||||
|
mizan-fastapi — same JSON shapes, same invalidate/merge semantics — beyond
|
||||||
|
the static IR equivalence the codegen-parity test gates.
|
||||||
|
|
||||||
|
Readiness probe is `/api/mizan/session/` (Mizan-protocol-shaped) rather
|
||||||
|
than `/openapi.json` (FastAPI-feature-shaped) so the harness reads the
|
||||||
|
same surface across backends.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -25,6 +32,7 @@ from pathlib import Path
|
|||||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||||
AFI_DIR = REPO_ROOT / "tests" / "afi"
|
AFI_DIR = REPO_ROOT / "tests" / "afi"
|
||||||
RUST_DIR = REPO_ROOT / "tests" / "rust"
|
RUST_DIR = REPO_ROOT / "tests" / "rust"
|
||||||
|
RUST_APP_DIR = AFI_DIR / "rust_app"
|
||||||
BOOT_TIMEOUT_S = 15.0
|
BOOT_TIMEOUT_S = 15.0
|
||||||
POLL_INTERVAL_S = 0.25
|
POLL_INTERVAL_S = 0.25
|
||||||
|
|
||||||
@@ -35,24 +43,22 @@ def pick_free_port() -> int:
|
|||||||
return s.getsockname()[1]
|
return s.getsockname()[1]
|
||||||
|
|
||||||
|
|
||||||
def wait_for_server(port: int, timeout_s: float) -> bool:
|
def wait_for_server(port: int, timeout_s: float, label: str) -> bool:
|
||||||
deadline = time.monotonic() + timeout_s
|
deadline = time.monotonic() + timeout_s
|
||||||
url = f"http://127.0.0.1:{port}/openapi.json"
|
url = f"http://127.0.0.1:{port}/api/mizan/session/"
|
||||||
while time.monotonic() < deadline:
|
while time.monotonic() < deadline:
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(url, timeout=1.0) as resp:
|
with urllib.request.urlopen(url, timeout=1.0) as resp:
|
||||||
if resp.status == 200:
|
if resp.status == 200:
|
||||||
return True
|
return True
|
||||||
except (urllib.error.URLError, ConnectionError, OSError) as e:
|
except (urllib.error.URLError, ConnectionError, OSError) as e:
|
||||||
# Surface the kind of failure so a stuck boot doesn't read
|
sys.stderr.write(f"[wire_parity:{label}] waiting: {type(e).__name__}\n")
|
||||||
# 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)
|
time.sleep(POLL_INTERVAL_S)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def run_driver(name: str, base_url: str) -> int:
|
def run_driver(name: str, base_url: str, label: str) -> int:
|
||||||
sys.stdout.write(f"\n=== {name} ===\n")
|
sys.stdout.write(f"\n=== {label} :: {name} ===\n")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
return subprocess.run(
|
return subprocess.run(
|
||||||
["cargo", "run", "--quiet", "--bin", name, "--", base_url],
|
["cargo", "run", "--quiet", "--bin", name, "--", base_url],
|
||||||
@@ -60,36 +66,47 @@ def run_driver(name: str, base_url: str) -> int:
|
|||||||
).returncode
|
).returncode
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def boot_fastapi(port: int) -> subprocess.Popen:
|
||||||
port = pick_free_port()
|
return subprocess.Popen(
|
||||||
base_url = f"http://127.0.0.1:{port}/api/mizan"
|
[
|
||||||
|
"uv", "run", "uvicorn", "fastapi_app:make_app",
|
||||||
server = subprocess.Popen(
|
"--factory", "--port", str(port), "--log-level", "warning",
|
||||||
["uv", "run", "uvicorn", "fastapi_app:make_app",
|
],
|
||||||
"--factory", "--port", str(port), "--log-level", "warning"],
|
|
||||||
cwd=AFI_DIR,
|
cwd=AFI_DIR,
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def boot_rust(port: int) -> subprocess.Popen:
|
||||||
|
return subprocess.Popen(
|
||||||
|
[
|
||||||
|
"cargo", "run", "--quiet", "--release",
|
||||||
|
"--manifest-path", str(RUST_APP_DIR / "Cargo.toml"),
|
||||||
|
"--bin", "server",
|
||||||
|
],
|
||||||
|
env={**os.environ, "PORT": str(port)},
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_against(label: str, server: subprocess.Popen, port: int) -> int:
|
||||||
|
failures = 0
|
||||||
try:
|
try:
|
||||||
if not wait_for_server(port, BOOT_TIMEOUT_S):
|
if not wait_for_server(port, BOOT_TIMEOUT_S, label):
|
||||||
sys.stderr.write(
|
sys.stderr.write(f"[wire_parity:{label}] server boot timed out after {BOOT_TIMEOUT_S}s\n")
|
||||||
f"[wire_parity] server failed to start within {BOOT_TIMEOUT_S}s\n",
|
if server.stderr:
|
||||||
)
|
tail = server.stderr.read(4096)
|
||||||
stderr_tail = server.stderr.read(4096) if server.stderr else b""
|
if tail:
|
||||||
if stderr_tail:
|
sys.stderr.write(tail.decode("utf-8", errors="replace"))
|
||||||
sys.stderr.write(stderr_tail.decode("utf-8", errors="replace"))
|
|
||||||
return 1
|
return 1
|
||||||
|
base_url = f"http://127.0.0.1:{port}/api/mizan"
|
||||||
failures = 0
|
|
||||||
for driver in ("drive_kernel", "drive_emitted"):
|
for driver in ("drive_kernel", "drive_emitted"):
|
||||||
rc = run_driver(driver, base_url)
|
rc = run_driver(driver, base_url, label)
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
sys.stderr.write(f"[wire_parity] {driver} exited {rc}\n")
|
sys.stderr.write(f"[wire_parity:{label}] {driver} exited {rc}\n")
|
||||||
failures += 1
|
failures += 1
|
||||||
|
|
||||||
return 0 if failures == 0 else 1
|
|
||||||
finally:
|
finally:
|
||||||
server.terminate()
|
server.terminate()
|
||||||
try:
|
try:
|
||||||
@@ -97,6 +114,21 @@ def main() -> int:
|
|||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
server.kill()
|
server.kill()
|
||||||
server.wait()
|
server.wait()
|
||||||
|
return failures
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
total_failures = 0
|
||||||
|
|
||||||
|
fastapi_port = pick_free_port()
|
||||||
|
sys.stdout.write(f"[wire_parity] booting fastapi on port {fastapi_port}\n")
|
||||||
|
total_failures += run_against("fastapi", boot_fastapi(fastapi_port), fastapi_port)
|
||||||
|
|
||||||
|
rust_port = pick_free_port()
|
||||||
|
sys.stdout.write(f"[wire_parity] booting rust on port {rust_port}\n")
|
||||||
|
total_failures += run_against("rust", boot_rust(rust_port), rust_port)
|
||||||
|
|
||||||
|
return 0 if total_failures == 0 else 1
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user