feat(axum): register checked handles
Add HandlerRegistry::register_handle for generated Handle<T> values and migrate examples away from passing raw numeric handle ids at registration sites. req: ceremony/004 req: public_api/001
This commit is contained in:
@@ -146,7 +146,7 @@ async fn events(Query(params): Query<BTreeMap<String, String>>) -> impl IntoResp
|
|||||||
|
|
||||||
fn registry(state: Arc<AppState>) -> HandlerRegistry {
|
fn registry(state: Arc<AppState>) -> HandlerRegistry {
|
||||||
HandlerRegistry::new(ui::BUILD_FINGERPRINT)
|
HandlerRegistry::new(ui::BUILD_FINGERPRINT)
|
||||||
.register(ui::board::handles::create_card.id().id, {
|
.register_handle(ui::board::handles::create_card, {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
move |form| {
|
move |form| {
|
||||||
// req: examples/001 req: form/002
|
// req: examples/001 req: form/002
|
||||||
@@ -161,7 +161,7 @@ fn registry(state: Arc<AppState>) -> HandlerRegistry {
|
|||||||
board_effects(&board, "Card added")
|
board_effects(&board, "Card added")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.register(ui::board::handles::move_left.id().id, {
|
.register_handle(ui::board::handles::move_left, {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
move |form| {
|
move |form| {
|
||||||
// req: examples/001 req: list/003
|
// req: examples/001 req: list/003
|
||||||
@@ -172,7 +172,7 @@ fn registry(state: Arc<AppState>) -> HandlerRegistry {
|
|||||||
board_effects(&board, if moved { "Card moved left" } else { "Card not found" })
|
board_effects(&board, if moved { "Card moved left" } else { "Card not found" })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.register(ui::board::handles::move_right.id().id, {
|
.register_handle(ui::board::handles::move_right, {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
move |form| {
|
move |form| {
|
||||||
// req: examples/001 req: list/003
|
// req: examples/001 req: list/003
|
||||||
@@ -183,7 +183,7 @@ fn registry(state: Arc<AppState>) -> HandlerRegistry {
|
|||||||
board_effects(&board, if moved { "Card moved right" } else { "Card not found" })
|
board_effects(&board, if moved { "Card moved right" } else { "Card not found" })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.register(ui::board::handles::delete_card.id().id, {
|
.register_handle(ui::board::handles::delete_card, {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
move |form| {
|
move |form| {
|
||||||
// req: examples/001 req: list/003
|
// req: examples/001 req: list/003
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ async fn events(Query(params): Query<BTreeMap<String, String>>) -> impl IntoResp
|
|||||||
|
|
||||||
fn registry(shared: Arc<Shared>) -> HandlerRegistry {
|
fn registry(shared: Arc<Shared>) -> HandlerRegistry {
|
||||||
HandlerRegistry::new(ui::BUILD_FINGERPRINT)
|
HandlerRegistry::new(ui::BUILD_FINGERPRINT)
|
||||||
.register(ui::control_center::handles::launch_work.id().id, {
|
.register_handle(ui::control_center::handles::launch_work, {
|
||||||
let shared = shared.clone();
|
let shared = shared.clone();
|
||||||
move |form| {
|
move |form| {
|
||||||
// req: form/002 req: examples/001
|
// req: form/002 req: examples/001
|
||||||
@@ -297,7 +297,7 @@ fn registry(shared: Arc<Shared>) -> HandlerRegistry {
|
|||||||
demo_effects(&demo, "Launch accepted · 4 targets updated")
|
demo_effects(&demo, "Launch accepted · 4 targets updated")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.register(ui::control_center::handles::advance_work.id().id, {
|
.register_handle(ui::control_center::handles::advance_work, {
|
||||||
let shared = shared.clone();
|
let shared = shared.clone();
|
||||||
move |form| {
|
move |form| {
|
||||||
// req: list/003 req: examples/001
|
// req: list/003 req: examples/001
|
||||||
@@ -315,7 +315,7 @@ fn registry(shared: Arc<Shared>) -> HandlerRegistry {
|
|||||||
demo_effects(&demo, "Pipeline advanced")
|
demo_effects(&demo, "Pipeline advanced")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.register(ui::control_center::handles::move_to_lane.id().id, {
|
.register_handle(ui::control_center::handles::move_to_lane, {
|
||||||
let shared = shared.clone();
|
let shared = shared.clone();
|
||||||
move |form| {
|
move |form| {
|
||||||
// req: list/003 req: examples/001
|
// req: list/003 req: examples/001
|
||||||
@@ -337,7 +337,7 @@ fn registry(shared: Arc<Shared>) -> HandlerRegistry {
|
|||||||
demo_effects(&demo, "Drag-and-drop move persisted")
|
demo_effects(&demo, "Drag-and-drop move persisted")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.register(ui::control_center::handles::delete_work.id().id, {
|
.register_handle(ui::control_center::handles::delete_work, {
|
||||||
let shared = shared.clone();
|
let shared = shared.clone();
|
||||||
move |form| {
|
move |form| {
|
||||||
// req: list/003 req: examples/001
|
// req: list/003 req: examples/001
|
||||||
@@ -356,7 +356,7 @@ fn registry(shared: Arc<Shared>) -> HandlerRegistry {
|
|||||||
demo_effects(&demo, "Card removed")
|
demo_effects(&demo, "Card removed")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.register(ui::control_center::handles::spotlight_work.id().id, {
|
.register_handle(ui::control_center::handles::spotlight_work, {
|
||||||
let shared = shared.clone();
|
let shared = shared.clone();
|
||||||
move |form| {
|
move |form| {
|
||||||
// req: examples/001
|
// req: examples/001
|
||||||
@@ -371,7 +371,7 @@ fn registry(shared: Arc<Shared>) -> HandlerRegistry {
|
|||||||
demo_effects(&demo, "Inspector focused")
|
demo_effects(&demo, "Inspector focused")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.register(ui::control_center::handles::simulate_push.id().id, {
|
.register_handle(ui::control_center::handles::simulate_push, {
|
||||||
let shared = shared.clone();
|
let shared = shared.clone();
|
||||||
move |_| {
|
move |_| {
|
||||||
// req: push/003 req: examples/001
|
// req: push/003 req: examples/001
|
||||||
@@ -384,7 +384,7 @@ fn registry(shared: Arc<Shared>) -> HandlerRegistry {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.register(ui::control_center::handles::reset_demo.id().id, {
|
.register_handle(ui::control_center::handles::reset_demo, {
|
||||||
let shared = shared.clone();
|
let shared = shared.clone();
|
||||||
move |_| {
|
move |_| {
|
||||||
// req: examples/001
|
// req: examples/001
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ async fn events(Query(params): Query<BTreeMap<String, String>>) -> impl IntoResp
|
|||||||
|
|
||||||
fn registry(state: Arc<ExampleState>) -> HandlerRegistry {
|
fn registry(state: Arc<ExampleState>) -> HandlerRegistry {
|
||||||
HandlerRegistry::new(ui::BUILD_FINGERPRINT)
|
HandlerRegistry::new(ui::BUILD_FINGERPRINT)
|
||||||
.register(ui::counter::handles::increment.id().id, {
|
.register_handle(ui::counter::handles::increment, {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
move |_| {
|
move |_| {
|
||||||
// req: examples/001
|
// req: examples/001
|
||||||
@@ -125,7 +125,7 @@ fn registry(state: Arc<ExampleState>) -> HandlerRegistry {
|
|||||||
ui::counter::slots::counter_value.text(*counter)
|
ui::counter::slots::counter_value.text(*counter)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.register(ui::todos::handles::add_todo.id().id, {
|
.register_handle(ui::todos::handles::add_todo, {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
move |form| {
|
move |form| {
|
||||||
// req: examples/001
|
// req: examples/001
|
||||||
@@ -141,7 +141,7 @@ fn registry(state: Arc<ExampleState>) -> HandlerRegistry {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.register(ui::wizard::handles::next_step.id().id, {
|
.register_handle(ui::wizard::handles::next_step, {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
move |_| {
|
move |_| {
|
||||||
// req: examples/001
|
// req: examples/001
|
||||||
@@ -150,7 +150,7 @@ fn registry(state: Arc<ExampleState>) -> HandlerRegistry {
|
|||||||
ui::wizard::slots::wizard_step.text(format!("Step {}", *step + 1))
|
ui::wizard::slots::wizard_step.text(format!("Step {}", *step + 1))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.register(ui::auth::handles::login.id().id, |form| {
|
.register_handle(ui::auth::handles::login, |form| {
|
||||||
// req: examples/001
|
// req: examples/001
|
||||||
let ok = form.value("email") == Some("demo@example.com")
|
let ok = form.value("email") == Some("demo@example.com")
|
||||||
&& form.value("password").is_some_and(|password| !password.is_empty());
|
&& form.value("password").is_some_and(|password| !password.is_empty());
|
||||||
@@ -160,7 +160,7 @@ fn registry(state: Arc<ExampleState>) -> HandlerRegistry {
|
|||||||
"Try demo@example.com with any password"
|
"Try demo@example.com with any password"
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.register(ui::page_swap::handles::load_docs.id().id, |_| {
|
.register_handle(ui::page_swap::handles::load_docs, |_| {
|
||||||
// req: page_swap/002, req: examples/001
|
// req: page_swap/002, req: examples/001
|
||||||
(
|
(
|
||||||
ui::page_swap::slots::content.html(render_docs_content(
|
ui::page_swap::slots::content.html(render_docs_content(
|
||||||
|
|||||||
+12
-1
@@ -5,7 +5,7 @@ use axum::http::{header, request::Parts, HeaderMap, HeaderValue, Request, Respon
|
|||||||
use axum::response::sse::{Event, Sse};
|
use axum::response::sse::{Event, Sse};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use futures_util::{Stream, StreamExt};
|
use futures_util::{Stream, StreamExt};
|
||||||
use slhx_core::{BuildFingerprint, EffectBatch, IntoEffect};
|
use slhx_core::{BuildFingerprint, EffectBatch, Handle, IntoEffect};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
|
|
||||||
@@ -262,6 +262,17 @@ impl HandlerRegistry {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn register_handle<I, E>(
|
||||||
|
self,
|
||||||
|
handle: Handle<I>,
|
||||||
|
handler: impl Fn(InteractionForm) -> E + Send + Sync + 'static,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
E: IntoEffect,
|
||||||
|
{
|
||||||
|
self.register(handle.id().id, handler)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn dispatch(&self, form: InteractionForm) -> Result<EffectResponse, DispatchRejection> {
|
pub fn dispatch(&self, form: InteractionForm) -> Result<EffectResponse, DispatchRejection> {
|
||||||
let handle_id = form.handle_id;
|
let handle_id = form.handle_id;
|
||||||
let Some(handler) = self.handlers.get(&handle_id) else {
|
let Some(handler) = self.handlers.get(&handle_id) else {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use slhx_axum::{
|
|||||||
InteractionFormRejection, PageMode, PageRequest, PageResponse, SLHX_CONTENT_TYPE,
|
InteractionFormRejection, PageMode, PageRequest, PageResponse, SLHX_CONTENT_TYPE,
|
||||||
SLHX_FINGERPRINT_HEADER, SLHX_PARTIAL_HEADER, SLHX_RUNTIME_CONTENT_TYPE, SLHX_TITLE_HEADER,
|
SLHX_FINGERPRINT_HEADER, SLHX_PARTIAL_HEADER, SLHX_RUNTIME_CONTENT_TYPE, SLHX_TITLE_HEADER,
|
||||||
};
|
};
|
||||||
use slhx_core::{push, BuildFingerprint, Slot};
|
use slhx_core::{push, BuildFingerprint, Handle, Slot};
|
||||||
|
|
||||||
fn selector(value: &str) -> Selector {
|
fn selector(value: &str) -> Selector {
|
||||||
Selector::parse(value).expect("test selector parses")
|
Selector::parse(value).expect("test selector parses")
|
||||||
@@ -100,14 +100,16 @@ fn interaction_form_preserves_hidden_csrf_fields_for_extractors() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn handler_registry_dispatches_by_numeric_handle_id() {
|
fn handler_registry_dispatches_by_checked_handle() {
|
||||||
|
// req: ceremony/004 req: public_api/001
|
||||||
let title = Slot::<String>::new(7);
|
let title = Slot::<String>::new(7);
|
||||||
let registry = HandlerRegistry::new(BuildFingerprint(123)).register(42, move |form| {
|
let create = Handle::<()>::new(42);
|
||||||
|
let registry = HandlerRegistry::new(BuildFingerprint(123)).register_handle(create, move |form| {
|
||||||
title.text(form.value("title").unwrap_or(""))
|
title.text(form.value("title").unwrap_or(""))
|
||||||
});
|
});
|
||||||
|
|
||||||
let response = registry
|
let response = registry
|
||||||
.dispatch(InteractionForm::new(42, [("title".into(), "Hello".into())]))
|
.dispatch(InteractionForm::new(create.id().id, [("title".into(), "Hello".into())]))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(response.batch.fingerprint, BuildFingerprint(123));
|
assert_eq!(response.batch.fingerprint, BuildFingerprint(123));
|
||||||
|
|||||||
Reference in New Issue
Block a user