From 0f8d07d36f9a4a5a5f07afe58361b7bfe4eea3bb Mon Sep 17 00:00:00 2001 From: slhx agent Date: Thu, 11 Jun 2026 21:55:29 +0200 Subject: [PATCH] feat(axum): start stateful interactions tersely Add state_interactions as a small helper for app-owned stateful registries, and use it in the workout exemplar to remove one layer of repetitive route wiring without hiding generated handles or app handlers. req: axum_integration/003 req: ceremony/001 req: dx/003 req: codegen/002 req: examples/001 --- examples/workout/src/lib.rs | 5 ++--- hemx-axum/src/lib.rs | 8 ++++++++ hemx-axum/tests/response.rs | 41 +++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/examples/workout/src/lib.rs b/examples/workout/src/lib.rs index 471697f..c583050 100644 --- a/examples/workout/src/lib.rs +++ b/examples/workout/src/lib.rs @@ -1,6 +1,6 @@ use hemplate::Hemplate; use hemx::{Html, IntoEffect}; -use hemx_axum::{interactions, Form, HandlerRegistry}; +use hemx_axum::{state_interactions, Form, HandlerRegistry}; use hemx_host::{ browser_pwa_host_profile, native_shell_host_profile, Capability, CapabilityManifest, CapabilityShape, CapabilityUse, HapticPattern, HostCall, HostCallId, HostEvent, @@ -365,8 +365,7 @@ pub struct RecordNoteInput { pub fn registry(state: AppState) -> HandlerRegistry { // req: codegen/002 req: examples/001 - interactions(ui::BUILD_FINGERPRINT) - .with_state(state) + state_interactions(ui::BUILD_FINGERPRINT, state) .on_state(ui::workout::complete_set, complete_set) .on(ui::workout::change_weight, change_weight_form) .on_state(ui::workout::skip_exercise, skip_exercise) diff --git a/hemx-axum/src/lib.rs b/hemx-axum/src/lib.rs index 33cf29b..8ad37e4 100644 --- a/hemx-axum/src/lib.rs +++ b/hemx-axum/src/lib.rs @@ -460,6 +460,14 @@ pub const fn interactions(fingerprint: BuildFingerprint) -> HandlerRegistry { HandlerRegistry::new(fingerprint) } +pub fn state_interactions(fingerprint: BuildFingerprint, state: S) -> StateHandlerRegistry +where + S: Clone + Send + Sync + 'static, +{ + // req: axum_integration/003 req: ceremony/001 + interactions(fingerprint).with_state(state) +} + impl InteractionRequest { pub fn dispatch( self, diff --git a/hemx-axum/tests/response.rs b/hemx-axum/tests/response.rs index f142088..47b2569 100644 --- a/hemx-axum/tests/response.rs +++ b/hemx-axum/tests/response.rs @@ -232,6 +232,47 @@ fn interaction_request_dispatches_with_concise_handlers_helper() { assert_eq!(response.batch.ops, vec![Slot::::new(3).text("ok")]); } +#[test] +fn state_interactions_starts_stateful_wiring_without_nested_closures() { + // req: axum_integration/003 req: ceremony/001 req: dx/003 + fn ping(prefix: String) -> impl IntoEffect { + Slot::::new(3).text(format!("{prefix}: ping")) + } + + fn open(prefix: String, input: OpenProject) -> impl IntoEffect { + Slot::::new(3).text(format!("{prefix}: {}", input.project_id.0)) + } + + let registry = || { + hemx_axum::state_interactions(BuildFingerprint(4), "project".to_owned()) + .on_state(Handle::<()>::new(7), ping) + .on(Handle::<()>::new(8), open) + .into_registry() + }; + + let ping_response = InteractionRequest::from(InteractionForm::for_handle( + Handle::<()>::new(7), + Vec::new(), + )) + .dispatch(registry()) + .unwrap(); + assert_eq!( + ping_response.batch.ops, + vec![Slot::::new(3).text("project: ping")] + ); + + let open_response = InteractionRequest::from(InteractionForm::for_handle( + Handle::<()>::new(8), + vec![("project_id".to_owned(), "42".to_owned())], + )) + .dispatch(registry()) + .unwrap(); + assert_eq!( + open_response.batch.ops, + vec![Slot::::new(3).text("project: 42")] + ); +} + #[test] fn interaction_request_dispatches_typed_form_inputs() { // req: axum_integration/003 req: form/004 req: canonical_authoring/003