feat(api): streamline generated app authoring
Move the canonical examples toward generated component-root helpers, typed form decoding, async/state handler registration, and derive-driven app/component registry wiring. Tighten requirements and diagnostics for the server-first, selectorless authoring path. Verified with cargo run -p slhx-xtask -- test, cargo check --workspace, redgate list, redgate refs, redgate health --strict, and git diff --check. req: canonical/001 req: canonical/003 req: canonical/004 req: dx/002 req: derive_app/001 req: component/003 req: form/004 req: axum_integration/003
This commit is contained in:
+647
-23
@@ -1,13 +1,17 @@
|
||||
use axum::async_trait;
|
||||
use axum::body::{to_bytes, Body};
|
||||
pub use axum::extract::State;
|
||||
use axum::extract::{FromRequest, FromRequestParts, Multipart};
|
||||
use axum::http::{header, request::Parts, HeaderMap, HeaderValue, Request, Response, StatusCode};
|
||||
use axum::response::sse::{Event, Sse};
|
||||
use axum::response::sse::{Event, KeepAlive, Sse};
|
||||
use axum::response::IntoResponse;
|
||||
use futures_util::{Stream, StreamExt};
|
||||
use slhx_core::{BuildFingerprint, EffectBatch, Handle, IntoEffect, SafeHtml};
|
||||
use slhx_core::{BuildFingerprint, EffectBatch, FromForm, Handle, IntoEffect, SafeHtml};
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::Infallible;
|
||||
use std::fmt;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
pub const SLHX_PARTIAL_HEADER: &str = "x-slhx-partial";
|
||||
pub const SLHX_FINGERPRINT_HEADER: &str = "x-slhx-fingerprint";
|
||||
@@ -54,7 +58,11 @@ impl PageRequest {
|
||||
matches!(self.mode, PageMode::Partial)
|
||||
}
|
||||
|
||||
pub fn page(self, partial_html: impl Into<String>, shell: impl FnOnce(String) -> String) -> PageResponse {
|
||||
pub fn page(
|
||||
self,
|
||||
partial_html: impl Into<String>,
|
||||
shell: impl FnOnce(String) -> String,
|
||||
) -> PageResponse {
|
||||
let partial_html = partial_html.into();
|
||||
match self.mode {
|
||||
PageMode::Full => PageResponse::full(shell(partial_html)),
|
||||
@@ -62,14 +70,14 @@ impl PageRequest {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn page_html(
|
||||
self,
|
||||
partial_html: SafeHtml,
|
||||
shell: impl FnOnce(SafeHtml) -> SafeHtml,
|
||||
) -> PageResponse {
|
||||
pub fn page_html<P, S>(self, partial_html: P, shell: impl FnOnce(P) -> S) -> PageResponse
|
||||
where
|
||||
P: Into<SafeHtml>,
|
||||
S: Into<SafeHtml>,
|
||||
{
|
||||
match self.mode {
|
||||
PageMode::Full => PageResponse::full(shell(partial_html).into_string()),
|
||||
PageMode::Partial => PageResponse::partial(partial_html.into_string()),
|
||||
PageMode::Full => PageResponse::full(shell(partial_html).into().into_string()),
|
||||
PageMode::Partial => PageResponse::partial(partial_html.into().into_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,9 +153,73 @@ pub trait DispatchRegistry {
|
||||
fn dispatch_form(self, form: InteractionForm) -> Result<EffectResponse, DispatchRejection>;
|
||||
}
|
||||
|
||||
pub trait FromInteractionForm: Sized {
|
||||
fn from_interaction_form(form: &InteractionForm) -> Result<Self, FormDecodeError>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Form<T>(pub T);
|
||||
|
||||
impl<T> Form<T> {
|
||||
pub fn into_inner(self) -> T {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::Deref for Form<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FromInteractionForm for T
|
||||
where
|
||||
T: FromForm,
|
||||
{
|
||||
fn from_interaction_form(form: &InteractionForm) -> Result<Self, FormDecodeError> {
|
||||
T::from_form_fields(form.fields())
|
||||
.map_err(|error| FormDecodeError::new(error.message().to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FromInteractionForm for Form<T>
|
||||
where
|
||||
T: FromForm,
|
||||
{
|
||||
fn from_interaction_form(form: &InteractionForm) -> Result<Self, FormDecodeError> {
|
||||
T::from_form_fields(form.fields())
|
||||
.map(Self)
|
||||
.map_err(|error| FormDecodeError::new(error.message().to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FromHandlerState<S>: Sized {
|
||||
fn from_handler_state(state: S) -> Self;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct FormDecodeError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
type SyncHandler =
|
||||
Box<dyn Fn(InteractionForm) -> Result<EffectBatch, DispatchRejection> + Send + Sync>;
|
||||
type HandlerFuture = Pin<Box<dyn Future<Output = Result<EffectBatch, DispatchRejection>> + Send>>;
|
||||
type AsyncHandler = Box<dyn Fn(InteractionForm) -> HandlerFuture + Send + Sync>;
|
||||
|
||||
pub struct HandlerRegistry {
|
||||
fingerprint: BuildFingerprint,
|
||||
handlers: BTreeMap<u32, Box<dyn Fn(InteractionForm) -> EffectBatch + Send + Sync>>,
|
||||
handlers: BTreeMap<u32, SyncHandler>,
|
||||
async_handlers: BTreeMap<u32, AsyncHandler>,
|
||||
}
|
||||
|
||||
pub type Registry = HandlerRegistry;
|
||||
|
||||
pub struct StateHandlerRegistry<S> {
|
||||
registry: HandlerRegistry,
|
||||
state: S,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -160,6 +232,8 @@ pub enum InteractionFormRejection {
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum DispatchRejection {
|
||||
UnknownHandle(u32),
|
||||
InvalidForm { handle_id: u32, message: String },
|
||||
HandlerError { handle_id: u32, message: String },
|
||||
}
|
||||
|
||||
impl EffectResponse {
|
||||
@@ -170,6 +244,30 @@ impl EffectResponse {
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> FromHandlerState<S> for S {
|
||||
fn from_handler_state(state: S) -> Self {
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> FromHandlerState<S> for State<S> {
|
||||
fn from_handler_state(state: S) -> Self {
|
||||
State(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl FormDecodeError {
|
||||
pub fn new(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn message(&self) -> &str {
|
||||
&self.message
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractionForm {
|
||||
pub fn new(handle_id: u32, fields: impl IntoIterator<Item = (String, String)>) -> Self {
|
||||
Self {
|
||||
@@ -179,7 +277,10 @@ impl InteractionForm {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn for_handle<I>(handle: Handle<I>, fields: impl IntoIterator<Item = (String, String)>) -> Self {
|
||||
pub fn for_handle<I>(
|
||||
handle: Handle<I>,
|
||||
fields: impl IntoIterator<Item = (String, String)>,
|
||||
) -> Self {
|
||||
Self::new(handle.id().id, fields)
|
||||
}
|
||||
|
||||
@@ -188,7 +289,9 @@ impl InteractionForm {
|
||||
}
|
||||
|
||||
// req: multipart/001, req: multipart/002
|
||||
pub async fn parse_multipart(mut multipart: Multipart) -> Result<Self, InteractionFormRejection> {
|
||||
pub async fn parse_multipart(
|
||||
mut multipart: Multipart,
|
||||
) -> Result<Self, InteractionFormRejection> {
|
||||
let mut fields = Vec::new();
|
||||
let mut files = Vec::new();
|
||||
|
||||
@@ -224,7 +327,10 @@ impl InteractionForm {
|
||||
Self::from_parts(fields, files)
|
||||
}
|
||||
|
||||
fn from_parts(fields: Vec<(String, String)>, files: Vec<InteractionFile>) -> Result<Self, InteractionFormRejection> {
|
||||
fn from_parts(
|
||||
fields: Vec<(String, String)>,
|
||||
files: Vec<InteractionFile>,
|
||||
) -> Result<Self, InteractionFormRejection> {
|
||||
let Some(handle) = fields
|
||||
.iter()
|
||||
.find_map(|(name, value)| (name == SLHX_HANDLE_FIELD).then_some(value))
|
||||
@@ -234,7 +340,11 @@ impl InteractionForm {
|
||||
let handle_id = handle
|
||||
.parse::<u32>()
|
||||
.map_err(|_| InteractionFormRejection::InvalidHandle)?;
|
||||
Ok(Self { handle_id, fields, files })
|
||||
Ok(Self {
|
||||
handle_id,
|
||||
fields,
|
||||
files,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn value(&self, name: &str) -> Option<&str> {
|
||||
@@ -267,6 +377,20 @@ impl InteractionForm {
|
||||
pub fn file(&self, name: &str) -> Option<&InteractionFile> {
|
||||
self.files.iter().find(|file| file.name == name)
|
||||
}
|
||||
|
||||
pub fn required(&self, name: &str) -> Result<&str, FormDecodeError> {
|
||||
self.value(name)
|
||||
.ok_or_else(|| FormDecodeError::new(format!("missing form field `{name}`")))
|
||||
}
|
||||
|
||||
pub fn parse_required<T>(&self, name: &str) -> Result<T, FormDecodeError>
|
||||
where
|
||||
T: std::str::FromStr,
|
||||
{
|
||||
self.required(name)?
|
||||
.parse()
|
||||
.map_err(|_| FormDecodeError::new(format!("invalid form field `{name}`")))
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn handlers(fingerprint: BuildFingerprint) -> HandlerRegistry {
|
||||
@@ -285,6 +409,13 @@ impl InteractionRequest {
|
||||
registry.dispatch_form(self.form)
|
||||
}
|
||||
|
||||
pub async fn dispatch_async(
|
||||
self,
|
||||
registry: HandlerRegistry,
|
||||
) -> Result<EffectResponse, DispatchRejection> {
|
||||
registry.dispatch_async(self.form).await
|
||||
}
|
||||
|
||||
pub fn form(&self) -> &InteractionForm {
|
||||
&self.form
|
||||
}
|
||||
@@ -301,6 +432,7 @@ impl HandlerRegistry {
|
||||
Self {
|
||||
fingerprint,
|
||||
handlers: BTreeMap::new(),
|
||||
async_handlers: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,7 +447,240 @@ impl HandlerRegistry {
|
||||
let fingerprint = self.fingerprint;
|
||||
self.handlers.insert(
|
||||
handle_id,
|
||||
Box::new(move |form| handler(form).into_batch(fingerprint)),
|
||||
Box::new(move |form| Ok(handler(form).into_batch(fingerprint))),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn register_typed<T, E>(
|
||||
mut self,
|
||||
handle_id: u32,
|
||||
handler: impl Fn(T) -> E + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
T: FromInteractionForm,
|
||||
E: IntoEffect,
|
||||
{
|
||||
let fingerprint = self.fingerprint;
|
||||
self.handlers.insert(
|
||||
handle_id,
|
||||
Box::new(move |form| {
|
||||
let handle_id = form.handle_id;
|
||||
let input = T::from_interaction_form(&form).map_err(|error| {
|
||||
DispatchRejection::InvalidForm {
|
||||
handle_id,
|
||||
message: error.message,
|
||||
}
|
||||
})?;
|
||||
Ok(handler(input).into_batch(fingerprint))
|
||||
}),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn register_state<S, C, E>(
|
||||
mut self,
|
||||
handle_id: u32,
|
||||
state: S,
|
||||
handler: impl Fn(C) -> E + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
C: FromHandlerState<S>,
|
||||
E: IntoEffect,
|
||||
{
|
||||
let fingerprint = self.fingerprint;
|
||||
self.handlers.insert(
|
||||
handle_id,
|
||||
Box::new(move |_| {
|
||||
Ok(handler(C::from_handler_state(state.clone())).into_batch(fingerprint))
|
||||
}),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn register_state_typed<S, C, T, E>(
|
||||
mut self,
|
||||
handle_id: u32,
|
||||
state: S,
|
||||
handler: impl Fn(C, T) -> E + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
C: FromHandlerState<S>,
|
||||
T: FromInteractionForm,
|
||||
E: IntoEffect,
|
||||
{
|
||||
let fingerprint = self.fingerprint;
|
||||
self.handlers.insert(
|
||||
handle_id,
|
||||
Box::new(move |form| {
|
||||
let handle_id = form.handle_id;
|
||||
let input = T::from_interaction_form(&form).map_err(|error| {
|
||||
DispatchRejection::InvalidForm {
|
||||
handle_id,
|
||||
message: error.message,
|
||||
}
|
||||
})?;
|
||||
Ok(handler(C::from_handler_state(state.clone()), input).into_batch(fingerprint))
|
||||
}),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn register_async<E, F>(
|
||||
mut self,
|
||||
handle_id: u32,
|
||||
handler: impl Fn(InteractionForm) -> F + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
F: Future<Output = E> + Send + 'static,
|
||||
E: IntoEffect,
|
||||
{
|
||||
let fingerprint = self.fingerprint;
|
||||
self.async_handlers.insert(
|
||||
handle_id,
|
||||
Box::new(move |form| {
|
||||
let future = handler(form);
|
||||
Box::pin(async move { Ok(future.await.into_batch(fingerprint)) })
|
||||
}),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn register_typed_async<T, E, F>(
|
||||
mut self,
|
||||
handle_id: u32,
|
||||
handler: impl Fn(T) -> F + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
T: FromInteractionForm,
|
||||
F: Future<Output = E> + Send + 'static,
|
||||
E: IntoEffect,
|
||||
{
|
||||
let fingerprint = self.fingerprint;
|
||||
self.async_handlers.insert(
|
||||
handle_id,
|
||||
Box::new(move |form| {
|
||||
let handle_id = form.handle_id;
|
||||
let input = match T::from_interaction_form(&form) {
|
||||
Ok(input) => input,
|
||||
Err(error) => {
|
||||
return Box::pin(async move {
|
||||
Err(DispatchRejection::InvalidForm {
|
||||
handle_id,
|
||||
message: error.message,
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
let future = handler(input);
|
||||
Box::pin(async move { Ok(future.await.into_batch(fingerprint)) })
|
||||
}),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn register_state_async<S, C, E, F>(
|
||||
mut self,
|
||||
handle_id: u32,
|
||||
state: S,
|
||||
handler: impl Fn(C) -> F + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
C: FromHandlerState<S>,
|
||||
F: Future<Output = E> + Send + 'static,
|
||||
E: IntoEffect,
|
||||
{
|
||||
let fingerprint = self.fingerprint;
|
||||
self.async_handlers.insert(
|
||||
handle_id,
|
||||
Box::new(move |_| {
|
||||
let future = handler(C::from_handler_state(state.clone()));
|
||||
Box::pin(async move { Ok(future.await.into_batch(fingerprint)) })
|
||||
}),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn register_state_typed_async<S, C, T, E, F>(
|
||||
mut self,
|
||||
handle_id: u32,
|
||||
state: S,
|
||||
handler: impl Fn(C, T) -> F + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
C: FromHandlerState<S>,
|
||||
T: FromInteractionForm,
|
||||
F: Future<Output = E> + Send + 'static,
|
||||
E: IntoEffect,
|
||||
{
|
||||
let fingerprint = self.fingerprint;
|
||||
self.async_handlers.insert(
|
||||
handle_id,
|
||||
Box::new(move |form| {
|
||||
let handle_id = form.handle_id;
|
||||
let input = match T::from_interaction_form(&form) {
|
||||
Ok(input) => input,
|
||||
Err(error) => {
|
||||
return Box::pin(async move {
|
||||
Err(DispatchRejection::InvalidForm {
|
||||
handle_id,
|
||||
message: error.message,
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
let future = handler(C::from_handler_state(state.clone()), input);
|
||||
Box::pin(async move { Ok(future.await.into_batch(fingerprint)) })
|
||||
}),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn register_state_typed_async_result<S, C, T, E, O, F>(
|
||||
mut self,
|
||||
handle_id: u32,
|
||||
state: S,
|
||||
handler: impl Fn(C, T) -> F + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
C: FromHandlerState<S>,
|
||||
T: FromInteractionForm,
|
||||
F: Future<Output = Result<O, E>> + Send + 'static,
|
||||
O: IntoEffect,
|
||||
E: fmt::Display,
|
||||
{
|
||||
let fingerprint = self.fingerprint;
|
||||
self.async_handlers.insert(
|
||||
handle_id,
|
||||
Box::new(move |form| {
|
||||
let handle_id = form.handle_id;
|
||||
let input = match T::from_interaction_form(&form) {
|
||||
Ok(input) => input,
|
||||
Err(error) => {
|
||||
return Box::pin(async move {
|
||||
Err(DispatchRejection::InvalidForm {
|
||||
handle_id,
|
||||
message: error.message,
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
let future = handler(C::from_handler_state(state.clone()), input);
|
||||
Box::pin(async move {
|
||||
future
|
||||
.await
|
||||
.map(|effects| effects.into_batch(fingerprint))
|
||||
.map_err(|error| DispatchRejection::HandlerError {
|
||||
handle_id,
|
||||
message: error.to_string(),
|
||||
})
|
||||
})
|
||||
}),
|
||||
);
|
||||
self
|
||||
}
|
||||
@@ -331,6 +696,16 @@ impl HandlerRegistry {
|
||||
self.register(handle.id().id, handler)
|
||||
}
|
||||
|
||||
pub fn with_state<S>(self, state: S) -> StateHandlerRegistry<S>
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
StateHandlerRegistry {
|
||||
registry: self,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on<I, E>(
|
||||
self,
|
||||
handle: Handle<I>,
|
||||
@@ -342,18 +717,243 @@ impl HandlerRegistry {
|
||||
self.register_handle(handle, handler)
|
||||
}
|
||||
|
||||
pub fn on_form<I, T, E>(
|
||||
self,
|
||||
handle: Handle<I>,
|
||||
handler: impl Fn(T) -> E + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
T: FromInteractionForm,
|
||||
E: IntoEffect,
|
||||
{
|
||||
self.register_typed(handle.id().id, handler)
|
||||
}
|
||||
|
||||
pub fn on_state<I, S, C, E>(
|
||||
self,
|
||||
handle: Handle<I>,
|
||||
state: S,
|
||||
handler: impl Fn(C) -> E + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
C: FromHandlerState<S>,
|
||||
E: IntoEffect,
|
||||
{
|
||||
self.register_state(handle.id().id, state, handler)
|
||||
}
|
||||
|
||||
pub fn on_state_form<I, S, C, T, E>(
|
||||
self,
|
||||
handle: Handle<I>,
|
||||
state: S,
|
||||
handler: impl Fn(C, T) -> E + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
C: FromHandlerState<S>,
|
||||
T: FromInteractionForm,
|
||||
E: IntoEffect,
|
||||
{
|
||||
self.register_state_typed(handle.id().id, state, handler)
|
||||
}
|
||||
|
||||
pub fn on_async<I, E, F>(
|
||||
self,
|
||||
handle: Handle<I>,
|
||||
handler: impl Fn(InteractionForm) -> F + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
F: Future<Output = E> + Send + 'static,
|
||||
E: IntoEffect,
|
||||
{
|
||||
self.register_async(handle.id().id, handler)
|
||||
}
|
||||
|
||||
pub fn on_form_async<I, T, E, F>(
|
||||
self,
|
||||
handle: Handle<I>,
|
||||
handler: impl Fn(T) -> F + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
T: FromInteractionForm,
|
||||
F: Future<Output = E> + Send + 'static,
|
||||
E: IntoEffect,
|
||||
{
|
||||
self.register_typed_async(handle.id().id, handler)
|
||||
}
|
||||
|
||||
pub fn on_state_async<I, S, C, E, F>(
|
||||
self,
|
||||
handle: Handle<I>,
|
||||
state: S,
|
||||
handler: impl Fn(C) -> F + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
C: FromHandlerState<S>,
|
||||
F: Future<Output = E> + Send + 'static,
|
||||
E: IntoEffect,
|
||||
{
|
||||
self.register_state_async(handle.id().id, state, handler)
|
||||
}
|
||||
|
||||
pub fn on_state_form_async<I, S, C, T, E, F>(
|
||||
self,
|
||||
handle: Handle<I>,
|
||||
state: S,
|
||||
handler: impl Fn(C, T) -> F + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
C: FromHandlerState<S>,
|
||||
T: FromInteractionForm,
|
||||
F: Future<Output = E> + Send + 'static,
|
||||
E: IntoEffect,
|
||||
{
|
||||
self.register_state_typed_async(handle.id().id, state, handler)
|
||||
}
|
||||
|
||||
pub fn on_state_form_async_result<I, S, C, T, E, O, F>(
|
||||
self,
|
||||
handle: Handle<I>,
|
||||
state: S,
|
||||
handler: impl Fn(C, T) -> F + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
C: FromHandlerState<S>,
|
||||
T: FromInteractionForm,
|
||||
F: Future<Output = Result<O, E>> + Send + 'static,
|
||||
O: IntoEffect,
|
||||
E: fmt::Display,
|
||||
{
|
||||
self.register_state_typed_async_result(handle.id().id, state, handler)
|
||||
}
|
||||
|
||||
pub fn dispatch(&self, form: InteractionForm) -> Result<EffectResponse, DispatchRejection> {
|
||||
let handle_id = form.handle_id;
|
||||
let Some(handler) = self.handlers.get(&handle_id) else {
|
||||
return Err(DispatchRejection::UnknownHandle(handle_id));
|
||||
};
|
||||
Ok(EffectResponse {
|
||||
batch: handler(form),
|
||||
batch: handler(form)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn dispatch_async(
|
||||
&self,
|
||||
form: InteractionForm,
|
||||
) -> Result<EffectResponse, DispatchRejection> {
|
||||
let handle_id = form.handle_id;
|
||||
if let Some(handler) = self.async_handlers.get(&handle_id) {
|
||||
return Ok(EffectResponse {
|
||||
batch: handler(form).await?,
|
||||
});
|
||||
}
|
||||
self.dispatch(form)
|
||||
}
|
||||
|
||||
pub fn contains(&self, handle_id: u32) -> bool {
|
||||
self.handlers.contains_key(&handle_id)
|
||||
self.handlers.contains_key(&handle_id) || self.async_handlers.contains_key(&handle_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> StateHandlerRegistry<S>
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
pub fn on_state<I, C, E>(
|
||||
mut self,
|
||||
handle: Handle<I>,
|
||||
handler: impl Fn(C) -> E + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
C: FromHandlerState<S>,
|
||||
E: IntoEffect,
|
||||
{
|
||||
self.registry = self.registry.on_state(handle, self.state.clone(), handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on<I, C, T, E>(
|
||||
mut self,
|
||||
handle: Handle<I>,
|
||||
handler: impl Fn(C, T) -> E + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
C: FromHandlerState<S>,
|
||||
T: FromInteractionForm,
|
||||
E: IntoEffect,
|
||||
{
|
||||
self.registry = self
|
||||
.registry
|
||||
.on_state_form(handle, self.state.clone(), handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_state_async<I, C, E, F>(
|
||||
mut self,
|
||||
handle: Handle<I>,
|
||||
handler: impl Fn(C) -> F + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
C: FromHandlerState<S>,
|
||||
F: Future<Output = E> + Send + 'static,
|
||||
E: IntoEffect,
|
||||
{
|
||||
self.registry = self
|
||||
.registry
|
||||
.on_state_async(handle, self.state.clone(), handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_async<I, C, T, E, F>(
|
||||
mut self,
|
||||
handle: Handle<I>,
|
||||
handler: impl Fn(C, T) -> F + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
C: FromHandlerState<S>,
|
||||
T: FromInteractionForm,
|
||||
F: Future<Output = E> + Send + 'static,
|
||||
E: IntoEffect,
|
||||
{
|
||||
self.registry = self
|
||||
.registry
|
||||
.on_state_form_async(handle, self.state.clone(), handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_async_result<I, C, T, E, O, F>(
|
||||
mut self,
|
||||
handle: Handle<I>,
|
||||
handler: impl Fn(C, T) -> F + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
C: FromHandlerState<S>,
|
||||
T: FromInteractionForm,
|
||||
F: Future<Output = Result<O, E>> + Send + 'static,
|
||||
O: IntoEffect,
|
||||
E: fmt::Display,
|
||||
{
|
||||
self.registry =
|
||||
self.registry
|
||||
.on_state_form_async_result(handle, self.state.clone(), handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn into_registry(self) -> HandlerRegistry {
|
||||
self.registry
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> DispatchRegistry for StateHandlerRegistry<S>
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
fn dispatch_form(self, form: InteractionForm) -> Result<EffectResponse, DispatchRejection> {
|
||||
self.registry.dispatch_form(form)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,7 +1024,10 @@ fn is_multipart(headers: &HeaderMap) -> bool {
|
||||
|
||||
impl PageMode {
|
||||
pub fn from_headers(headers: &HeaderMap) -> Self {
|
||||
match headers.get(SLHX_PARTIAL_HEADER).and_then(|value| value.to_str().ok()) {
|
||||
match headers
|
||||
.get(SLHX_PARTIAL_HEADER)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
{
|
||||
Some("1" | "true") => Self::Partial,
|
||||
_ => Self::Full,
|
||||
}
|
||||
@@ -452,7 +1055,10 @@ impl IntoResponse for PageResponse {
|
||||
.headers_mut()
|
||||
.insert(SLHX_FINGERPRINT_HEADER, fingerprint);
|
||||
}
|
||||
if let Some(title) = self.title.and_then(|title| HeaderValue::from_str(&title).ok()) {
|
||||
if let Some(title) = self
|
||||
.title
|
||||
.and_then(|title| HeaderValue::from_str(&title).ok())
|
||||
{
|
||||
response.headers_mut().insert(SLHX_TITLE_HEADER, title);
|
||||
}
|
||||
response
|
||||
@@ -506,6 +1112,16 @@ impl IntoResponse for DispatchRejection {
|
||||
format!("unknown slhx handle id {handle_id}"),
|
||||
)
|
||||
.into_response(),
|
||||
Self::InvalidForm { handle_id, message } => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("invalid slhx form for handle id {handle_id}: {message}"),
|
||||
)
|
||||
.into_response(),
|
||||
Self::HandlerError { handle_id, message } => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("slhx handler for handle id {handle_id} failed: {message}"),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -535,7 +1151,7 @@ where
|
||||
S: Stream<Item = Result<EffectBatch, E>> + Send + 'static,
|
||||
E: Into<axum::BoxError>,
|
||||
{
|
||||
Sse::new(batches.map(|batch| batch.map(sse_event)))
|
||||
Sse::new(batches.map(|batch| batch.map(sse_event))).keep_alive(KeepAlive::default())
|
||||
}
|
||||
|
||||
pub fn sse_event(batch: EffectBatch) -> Event {
|
||||
@@ -630,8 +1246,16 @@ fn hex(byte: u8) -> Option<u8> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{encode_sse_batch, html_with_root_fingerprint, sse, BuildFingerprint, InteractionForm, SLHX_SSE_EVENT};
|
||||
use axum::{body::{to_bytes, Body}, extract::FromRequest, http::{header, Request}, response::IntoResponse};
|
||||
use super::{
|
||||
encode_sse_batch, html_with_root_fingerprint, sse, BuildFingerprint, InteractionForm,
|
||||
SLHX_SSE_EVENT,
|
||||
};
|
||||
use axum::{
|
||||
body::{to_bytes, Body},
|
||||
extract::FromRequest,
|
||||
http::{header, Request},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use futures_util::stream;
|
||||
use scraper::{Html, Selector};
|
||||
use slhx_core::{EffectBatch, EFFECT_BATCH_ABI_VERSION};
|
||||
|
||||
Reference in New Issue
Block a user