mizan-axum + macros: state threading, array/map lowering, merge shape semantics
Three substrate extensions surfaced by the Blazr session port: 1. **App-state threading.** mizan-axum::router() is now generic over a user-supplied state type and threads `Arc<dyn Any + Send + Sync>` into every dispatch via RequestHandle. Handlers downcast to their concrete AppState. The stateless AFI fixture uses `router_stateless()` (matches the prior signature). RequestHandle gains a `from_dyn()` constructor to wrap already-erased trait-object references. 2. **`[T; N]` and `BTreeMap<K, V>` lowering in #[derive(Mizan)].** Fixed arrays emit as `List<T>` (matches Python `tuple[float,...]` → JSON array). String-keyed maps emit as `List<V>` — closest approximation until KDL grows a `dict` shape. Also: vec-element registrations get a per-function scope suffix so two handlers returning `Vec<Same>` don't collide at the static-name layer. 3. **`types_match` for merge: upsert-into-list semantics.** Now matches Python `types_match_for_merge`: direct (T == T), upsert (slot is `Alias(List(T))`, value is T), and list-replace (both sides list). The AFI fixture only exercised the direct path; the Blazr port's `morph_set_value` returning a single `MorphLayer` into a context with `Vec<MorphLayer>` slot is what surfaced the gap. AFI codegen + wire parity stays 12/12 green after these substrate changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
//! HTTP handlers. Mirrors `backends/mizan-fastapi/src/mizan_fastapi/router.py`.
|
||||
|
||||
use axum::extract::{Path, Query};
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::http::{header, HeaderValue, StatusCode};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::Json;
|
||||
@@ -10,10 +10,17 @@ use mizan_core::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
use std::any::Any;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::errors::ApiError;
|
||||
|
||||
/// Type-erased application state threaded into every `dispatch()` call via
|
||||
/// `RequestHandle`. User handlers downcast to their concrete state type.
|
||||
/// `Arc` keeps the clone cheap across per-request handler invocations.
|
||||
pub type AppStateAny = Arc<dyn Any + Send + Sync>;
|
||||
|
||||
/// Body for POST /call/. Matches the Python `CallBody` shape.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CallBody {
|
||||
@@ -48,7 +55,10 @@ fn no_store(json: Value) -> Response {
|
||||
}
|
||||
|
||||
/// POST /call/ — RPC dispatch.
|
||||
pub async fn function_call(Json(body): Json<CallBody>) -> Result<Response, ApiError> {
|
||||
pub async fn function_call(
|
||||
State(app_state): State<AppStateAny>,
|
||||
Json(body): Json<CallBody>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let fn_name = body
|
||||
.resolved_name()
|
||||
.ok_or_else(|| ApiError(MizanError::BadRequest("missing `fn` field".into())))?
|
||||
@@ -57,8 +67,7 @@ pub async fn function_call(Json(body): Json<CallBody>) -> Result<Response, ApiEr
|
||||
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 req = RequestHandle::from_dyn(app_state.as_ref());
|
||||
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)
|
||||
@@ -82,6 +91,7 @@ pub async fn function_call(Json(body): Json<CallBody>) -> Result<Response, ApiEr
|
||||
|
||||
/// GET /ctx/:context_name/ — bundled context fetch.
|
||||
pub async fn context_fetch(
|
||||
State(app_state): State<AppStateAny>,
|
||||
Path(context_name): Path<String>,
|
||||
Query(params): Query<BTreeMap<String, String>>,
|
||||
) -> Result<Response, ApiError> {
|
||||
@@ -105,10 +115,9 @@ pub async fn context_fetch(
|
||||
// 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 req = RequestHandle::from_dyn(app_state.as_ref());
|
||||
let result = fn_spec.dispatch(req, Value::Object(args)).await.map_err(ApiError)?;
|
||||
bundled.insert(fn_spec.name().to_string(), result);
|
||||
}
|
||||
|
||||
@@ -22,16 +22,37 @@ mod errors;
|
||||
mod handlers;
|
||||
|
||||
pub use errors::ApiError;
|
||||
pub use handlers::{context_fetch, function_call, session_init, CallBody, CallResponse};
|
||||
pub use handlers::{
|
||||
context_fetch, function_call, session_init, AppStateAny, CallBody, CallResponse,
|
||||
};
|
||||
|
||||
use axum::routing::{get, post};
|
||||
use axum::Router;
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Build the Mizan router. Mount it under a prefix:
|
||||
/// `Router::new().nest("/api/mizan", router())`.
|
||||
pub fn router() -> Router {
|
||||
/// Build the Mizan router with user-supplied app state. The state is
|
||||
/// type-erased into an `Arc<dyn Any + Send + Sync>` and threaded into every
|
||||
/// dispatch via `RequestHandle`. Handlers downcast to their concrete state
|
||||
/// type.
|
||||
///
|
||||
/// Mount under a prefix:
|
||||
/// `Router::new().nest("/api/mizan", router(my_state))`.
|
||||
pub fn router<S>(state: S) -> Router
|
||||
where
|
||||
S: Any + Send + Sync + 'static,
|
||||
{
|
||||
let state: AppStateAny = Arc::new(state);
|
||||
Router::new()
|
||||
.route("/session/", get(handlers::session_init))
|
||||
.route("/call/", post(handlers::function_call))
|
||||
.route("/ctx/:context_name/", get(handlers::context_fetch))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
/// Router variant for callers that have no app state to thread — the
|
||||
/// dispatch path receives a unit-typed handle. Used by the AFI fixture
|
||||
/// and other stateless test apps.
|
||||
pub fn router_stateless() -> Router {
|
||||
router(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user