feat(axum): add concise interaction request dispatch
Wrap extracted interaction forms in an InteractionRequest with a concise dispatch path, add a handlers() helper, and move canonical examples off direct HandlerRegistry/InteractionForm plumbing. req: axum_integration/003 req: ceremony/001 req: dx/003 req: examples/003
This commit is contained in:
@@ -5,7 +5,10 @@ use axum::Router;
|
|||||||
use futures_util::{stream, StreamExt};
|
use futures_util::{stream, StreamExt};
|
||||||
use hemplate::Hemplate;
|
use hemplate::Hemplate;
|
||||||
use slhx::{IntoEffect, RenderSlotExt, SafeHtml};
|
use slhx::{IntoEffect, RenderSlotExt, SafeHtml};
|
||||||
use slhx_axum::{runtime_js, sse, DispatchRejection, EffectResponse, HandlerRegistry, InteractionForm, PageRequest};
|
use slhx_axum::{
|
||||||
|
handlers, runtime_js, sse, DispatchRegistry, DispatchRejection, EffectResponse,
|
||||||
|
InteractionRequest, PageRequest,
|
||||||
|
};
|
||||||
use slhx_kanban_example::ui::{self, board as board_ui};
|
use slhx_kanban_example::ui::{self, board as board_ui};
|
||||||
use slhx_kanban_example::ui::board::{forms, handles, slots};
|
use slhx_kanban_example::ui::board::{forms, handles, slots};
|
||||||
use slhx_kanban_example::ui::board_card::handles as card_handles;
|
use slhx_kanban_example::ui::board_card::handles as card_handles;
|
||||||
@@ -127,9 +130,9 @@ async fn runtime() -> impl IntoResponse {
|
|||||||
|
|
||||||
async fn interact(
|
async fn interact(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
form: InteractionForm,
|
request: InteractionRequest,
|
||||||
) -> Result<EffectResponse, DispatchRejection> {
|
) -> Result<EffectResponse, DispatchRejection> {
|
||||||
registry(state).dispatch(form)
|
request.dispatch(registry(state))
|
||||||
}
|
}
|
||||||
|
|
||||||
// req: push/001 req: push/003 req: examples/001
|
// req: push/001 req: push/003 req: examples/001
|
||||||
@@ -148,8 +151,8 @@ async fn events(Query(params): Query<BTreeMap<String, String>>) -> impl IntoResp
|
|||||||
sse(batches)
|
sse(batches)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn registry(state: Arc<AppState>) -> HandlerRegistry {
|
fn registry(state: Arc<AppState>) -> impl DispatchRegistry {
|
||||||
HandlerRegistry::new(ui::BUILD_FINGERPRINT)
|
handlers(ui::BUILD_FINGERPRINT)
|
||||||
.register_handle(handles::create_card, {
|
.register_handle(handles::create_card, {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
move |form| {
|
move |form| {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use futures_util::{stream, StreamExt};
|
|||||||
use hemplate::Hemplate;
|
use hemplate::Hemplate;
|
||||||
use slhx::{CssClass, CssClasses, IntoEffect, RenderSlotExt, SafeHtml};
|
use slhx::{CssClass, CssClasses, IntoEffect, RenderSlotExt, SafeHtml};
|
||||||
use slhx_axum::{
|
use slhx_axum::{
|
||||||
runtime_js, sse, DispatchRejection, EffectResponse, HandlerRegistry, InteractionForm,
|
handlers, runtime_js, sse, DispatchRegistry, DispatchRejection, EffectResponse,
|
||||||
PageRequest,
|
InteractionRequest, PageRequest,
|
||||||
};
|
};
|
||||||
use slhx_techdemo::ui;
|
use slhx_techdemo::ui;
|
||||||
use slhx_techdemo::ui::control_center::{classes, forms, handles, slots};
|
use slhx_techdemo::ui::control_center::{classes, forms, handles, slots};
|
||||||
@@ -261,9 +261,9 @@ async fn island_js() -> impl IntoResponse {
|
|||||||
|
|
||||||
async fn interact(
|
async fn interact(
|
||||||
State(state): State<Arc<Shared>>,
|
State(state): State<Arc<Shared>>,
|
||||||
form: InteractionForm,
|
request: InteractionRequest,
|
||||||
) -> Result<EffectResponse, DispatchRejection> {
|
) -> Result<EffectResponse, DispatchRejection> {
|
||||||
registry(state).dispatch(form)
|
request.dispatch(registry(state))
|
||||||
}
|
}
|
||||||
|
|
||||||
// req: push/001 req: push/003 req: examples/001
|
// req: push/001 req: push/003 req: examples/001
|
||||||
@@ -282,8 +282,8 @@ async fn events(Query(params): Query<BTreeMap<String, String>>) -> impl IntoResp
|
|||||||
sse(batches)
|
sse(batches)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn registry(shared: Arc<Shared>) -> HandlerRegistry {
|
fn registry(shared: Arc<Shared>) -> impl DispatchRegistry {
|
||||||
HandlerRegistry::new(ui::BUILD_FINGERPRINT)
|
handlers(ui::BUILD_FINGERPRINT)
|
||||||
.register_handle(handles::launch_work, {
|
.register_handle(handles::launch_work, {
|
||||||
let shared = shared.clone();
|
let shared = shared.clone();
|
||||||
move |form| {
|
move |form| {
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ use axum::Router;
|
|||||||
use futures_util::{stream, StreamExt};
|
use futures_util::{stream, StreamExt};
|
||||||
use hemplate::Hemplate;
|
use hemplate::Hemplate;
|
||||||
use slhx::{push, IntoEffect, RenderSlotExt, SafeHtml};
|
use slhx::{push, IntoEffect, RenderSlotExt, SafeHtml};
|
||||||
use slhx_axum::{runtime_js, sse, EffectResponse, HandlerRegistry, InteractionForm, PageRequest};
|
use slhx_axum::{
|
||||||
|
handlers, runtime_js, sse, DispatchRegistry, EffectResponse, InteractionRequest, PageRequest,
|
||||||
|
};
|
||||||
use slhx_v0_examples::ui;
|
use slhx_v0_examples::ui;
|
||||||
use slhx_v0_examples::ui::{auth, counter, notifications, page_swap, todos, wizard};
|
use slhx_v0_examples::ui::{auth, counter, notifications, page_swap, todos, wizard};
|
||||||
use slhx_v0_examples::ui::auth::{handles as auth_handles, slots as auth_slots};
|
use slhx_v0_examples::ui::auth::{handles as auth_handles, slots as auth_slots};
|
||||||
@@ -96,9 +98,9 @@ async fn docs(request: PageRequest) -> impl IntoResponse {
|
|||||||
|
|
||||||
async fn interact(
|
async fn interact(
|
||||||
State(state): State<Arc<ExampleState>>,
|
State(state): State<Arc<ExampleState>>,
|
||||||
form: InteractionForm,
|
request: InteractionRequest,
|
||||||
) -> Result<EffectResponse, impl IntoResponse> {
|
) -> Result<EffectResponse, impl IntoResponse> {
|
||||||
registry(state).dispatch(form)
|
request.dispatch(registry(state))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn runtime() -> impl IntoResponse {
|
async fn runtime() -> impl IntoResponse {
|
||||||
@@ -121,8 +123,8 @@ async fn events(Query(params): Query<BTreeMap<String, String>>) -> impl IntoResp
|
|||||||
sse(batches)
|
sse(batches)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn registry(state: Arc<ExampleState>) -> HandlerRegistry {
|
fn registry(state: Arc<ExampleState>) -> impl DispatchRegistry {
|
||||||
HandlerRegistry::new(ui::BUILD_FINGERPRINT)
|
handlers(ui::BUILD_FINGERPRINT)
|
||||||
.register_handle(counter_handles::increment, {
|
.register_handle(counter_handles::increment, {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
move |_| {
|
move |_| {
|
||||||
|
|||||||
@@ -136,6 +136,15 @@ pub struct InteractionForm {
|
|||||||
files: Vec<InteractionFile>,
|
files: Vec<InteractionFile>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct InteractionRequest {
|
||||||
|
form: InteractionForm,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait DispatchRegistry {
|
||||||
|
fn dispatch_form(self, form: InteractionForm) -> Result<EffectResponse, DispatchRejection>;
|
||||||
|
}
|
||||||
|
|
||||||
pub struct HandlerRegistry {
|
pub struct HandlerRegistry {
|
||||||
fingerprint: BuildFingerprint,
|
fingerprint: BuildFingerprint,
|
||||||
handlers: BTreeMap<u32, Box<dyn Fn(InteractionForm) -> EffectBatch + Send + Sync>>,
|
handlers: BTreeMap<u32, Box<dyn Fn(InteractionForm) -> EffectBatch + Send + Sync>>,
|
||||||
@@ -253,6 +262,29 @@ impl InteractionForm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn handlers(fingerprint: BuildFingerprint) -> HandlerRegistry {
|
||||||
|
HandlerRegistry::new(fingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InteractionRequest {
|
||||||
|
pub fn dispatch(
|
||||||
|
self,
|
||||||
|
registry: impl DispatchRegistry,
|
||||||
|
) -> Result<EffectResponse, DispatchRejection> {
|
||||||
|
registry.dispatch_form(self.form)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn form(&self) -> &InteractionForm {
|
||||||
|
&self.form
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<InteractionForm> for InteractionRequest {
|
||||||
|
fn from(form: InteractionForm) -> Self {
|
||||||
|
Self { form }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl HandlerRegistry {
|
impl HandlerRegistry {
|
||||||
pub const fn new(fingerprint: BuildFingerprint) -> Self {
|
pub const fn new(fingerprint: BuildFingerprint) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -303,6 +335,12 @@ impl HandlerRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DispatchRegistry for HandlerRegistry {
|
||||||
|
fn dispatch_form(self, form: InteractionForm) -> Result<EffectResponse, DispatchRejection> {
|
||||||
|
self.dispatch(form)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl IntoResponse for InteractionFormRejection {
|
impl IntoResponse for InteractionFormRejection {
|
||||||
fn into_response(self) -> axum::response::Response {
|
fn into_response(self) -> axum::response::Response {
|
||||||
let (status, message) = match self {
|
let (status, message) = match self {
|
||||||
@@ -314,6 +352,20 @@ impl IntoResponse for InteractionFormRejection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S> FromRequest<S> for InteractionRequest
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = InteractionFormRejection;
|
||||||
|
|
||||||
|
async fn from_request(req: Request<Body>, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
InteractionForm::from_request(req, state)
|
||||||
|
.await
|
||||||
|
.map(|form| Self { form })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<S> FromRequest<S> for InteractionForm
|
impl<S> FromRequest<S> for InteractionForm
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ use axum::http::{header, HeaderMap};
|
|||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use scraper::{Html, Selector};
|
use scraper::{Html, Selector};
|
||||||
use slhx_axum::{
|
use slhx_axum::{
|
||||||
runtime_js, DispatchRejection, EffectResponse, HandlerRegistry, InteractionForm,
|
handlers, runtime_js, DispatchRejection, EffectResponse, HandlerRegistry, InteractionForm,
|
||||||
InteractionFormRejection, PageMode, PageRequest, PageResponse, SLHX_CONTENT_TYPE,
|
InteractionFormRejection, InteractionRequest, PageMode, PageRequest, PageResponse,
|
||||||
SLHX_FINGERPRINT_HEADER, SLHX_PARTIAL_HEADER, SLHX_RUNTIME_CONTENT_TYPE, SLHX_TITLE_HEADER,
|
SLHX_CONTENT_TYPE, SLHX_FINGERPRINT_HEADER, SLHX_PARTIAL_HEADER, SLHX_RUNTIME_CONTENT_TYPE,
|
||||||
|
SLHX_TITLE_HEADER,
|
||||||
};
|
};
|
||||||
use slhx_core::{push, BuildFingerprint, Handle, SafeHtml, Slot};
|
use slhx_core::{push, BuildFingerprint, Handle, SafeHtml, Slot};
|
||||||
|
|
||||||
@@ -139,6 +140,23 @@ fn interaction_form_preserves_hidden_csrf_fields_for_extractors() {
|
|||||||
assert!(form.fields().iter().any(|(name, _)| name == "csrf_token"));
|
assert!(form.fields().iter().any(|(name, _)| name == "csrf_token"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn interaction_request_dispatches_with_concise_handlers_helper() {
|
||||||
|
// req: axum_integration/003 req: ceremony/001 req: dx/003
|
||||||
|
let request = InteractionRequest::from(InteractionForm::for_handle(
|
||||||
|
Handle::<()>::new(7),
|
||||||
|
Vec::new(),
|
||||||
|
));
|
||||||
|
let response = request
|
||||||
|
.dispatch(handlers(BuildFingerprint(4)).register_handle(Handle::<()>::new(7), |_| {
|
||||||
|
Slot::<String>::new(3).text("ok")
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(response.batch.fingerprint, BuildFingerprint(4));
|
||||||
|
assert_eq!(response.batch.ops, vec![Slot::<String>::new(3).text("ok")]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn handler_registry_dispatches_by_checked_handle() {
|
fn handler_registry_dispatches_by_checked_handle() {
|
||||||
// req: ceremony/004 req: public_api/001
|
// req: ceremony/004 req: public_api/001
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ fn canonical_examples_do_not_author_low_level_resource_plumbing() {
|
|||||||
"::lower(include_str!",
|
"::lower(include_str!",
|
||||||
"lower_html(",
|
"lower_html(",
|
||||||
"render_html(",
|
"render_html(",
|
||||||
|
"HandlerRegistry",
|
||||||
|
"InteractionForm",
|
||||||
"name=\"__h\"",
|
"name=\"__h\"",
|
||||||
"name='__h'",
|
"name='__h'",
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user