feat(hemx): clarify generated authoring path
Close the fallible handler and example coherence slice: v0 uses typed todo newtypes, Result handler failure mapping, generated form/partial helpers, and page composition; kanban is explicitly marked as an advanced north-star milestone; raw render is isolated behind hemx::advanced::render. req: canonical_authoring/003 req: failure/004 req: public_api/005 req: examples/004
This commit is contained in:
+263
-18
@@ -9,7 +9,6 @@ use futures_util::{Stream, StreamExt};
|
||||
use hemx_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;
|
||||
|
||||
@@ -229,11 +228,61 @@ pub enum InteractionFormRejection {
|
||||
InvalidHandle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct HandlerErrorContext {
|
||||
pub handle_id: u32,
|
||||
pub fingerprint: BuildFingerprint,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum HandlerFailure {
|
||||
Response { status: StatusCode, message: String },
|
||||
Effects(EffectBatch),
|
||||
}
|
||||
|
||||
pub trait IntoHandlerFailure {
|
||||
fn into_handler_failure(self, context: HandlerErrorContext) -> HandlerFailure;
|
||||
}
|
||||
|
||||
fn map_handler_failure<E>(
|
||||
error: E,
|
||||
handle_id: u32,
|
||||
fingerprint: BuildFingerprint,
|
||||
) -> Result<EffectBatch, DispatchRejection>
|
||||
where
|
||||
E: IntoHandlerFailure,
|
||||
{
|
||||
match error.into_handler_failure(HandlerErrorContext {
|
||||
handle_id,
|
||||
fingerprint,
|
||||
}) {
|
||||
HandlerFailure::Effects(batch) => Ok(batch),
|
||||
failure => Err(DispatchRejection::HandlerError(failure)),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum DispatchRejection {
|
||||
UnknownHandle(u32),
|
||||
InvalidForm { handle_id: u32, message: String },
|
||||
HandlerError { handle_id: u32, message: String },
|
||||
HandlerError(HandlerFailure),
|
||||
}
|
||||
|
||||
impl HandlerFailure {
|
||||
pub fn internal(message: impl Into<String>) -> Self {
|
||||
Self::response(StatusCode::INTERNAL_SERVER_ERROR, message)
|
||||
}
|
||||
|
||||
pub fn response(status: StatusCode, message: impl Into<String>) -> Self {
|
||||
Self::Response {
|
||||
status,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn effects(effects: impl IntoEffect, context: HandlerErrorContext) -> Self {
|
||||
Self::Effects(effects.into_batch(context.fingerprint))
|
||||
}
|
||||
}
|
||||
|
||||
impl EffectResponse {
|
||||
@@ -528,6 +577,67 @@ impl HandlerRegistry {
|
||||
self
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn register_state_result<S, C, E, O>(
|
||||
mut self,
|
||||
handle_id: u32,
|
||||
state: S,
|
||||
handler: impl Fn(C) -> Result<O, E> + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
C: FromHandlerState<S>,
|
||||
O: IntoEffect,
|
||||
E: IntoHandlerFailure,
|
||||
{
|
||||
let fingerprint = self.fingerprint;
|
||||
self.handlers.insert(
|
||||
handle_id,
|
||||
Box::new(move |form| {
|
||||
let handle_id = form.handle_id;
|
||||
match handler(C::from_handler_state(state.clone())) {
|
||||
Ok(effects) => Ok(effects.into_batch(fingerprint)),
|
||||
Err(error) => map_handler_failure(error, handle_id, fingerprint),
|
||||
}
|
||||
}),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn register_state_typed_result<S, C, T, E, O>(
|
||||
mut self,
|
||||
handle_id: u32,
|
||||
state: S,
|
||||
handler: impl Fn(C, T) -> Result<O, E> + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
C: FromHandlerState<S>,
|
||||
T: FromInteractionForm,
|
||||
O: IntoEffect,
|
||||
E: IntoHandlerFailure,
|
||||
{
|
||||
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,
|
||||
}
|
||||
})?;
|
||||
match handler(C::from_handler_state(state.clone()), input) {
|
||||
Ok(effects) => Ok(effects.into_batch(fingerprint)),
|
||||
Err(error) => map_handler_failure(error, handle_id, fingerprint),
|
||||
}
|
||||
}),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn register_async<E, F>(
|
||||
mut self,
|
||||
handle_id: u32,
|
||||
@@ -604,6 +714,37 @@ impl HandlerRegistry {
|
||||
self
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn register_state_async_result<S, C, E, O, 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 = Result<O, E>> + Send + 'static,
|
||||
O: IntoEffect,
|
||||
E: IntoHandlerFailure,
|
||||
{
|
||||
let fingerprint = self.fingerprint;
|
||||
self.async_handlers.insert(
|
||||
handle_id,
|
||||
Box::new(move |form| {
|
||||
let handle_id = form.handle_id;
|
||||
let future = handler(C::from_handler_state(state.clone()));
|
||||
Box::pin(async move {
|
||||
match future.await {
|
||||
Ok(effects) => Ok(effects.into_batch(fingerprint)),
|
||||
Err(error) => map_handler_failure(error, handle_id, fingerprint),
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn register_state_typed_async<S, C, T, E, F>(
|
||||
mut self,
|
||||
handle_id: u32,
|
||||
@@ -640,6 +781,7 @@ impl HandlerRegistry {
|
||||
self
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn register_state_typed_async_result<S, C, T, E, O, F>(
|
||||
mut self,
|
||||
handle_id: u32,
|
||||
@@ -652,7 +794,7 @@ impl HandlerRegistry {
|
||||
T: FromInteractionForm,
|
||||
F: Future<Output = Result<O, E>> + Send + 'static,
|
||||
O: IntoEffect,
|
||||
E: fmt::Display,
|
||||
E: IntoHandlerFailure,
|
||||
{
|
||||
let fingerprint = self.fingerprint;
|
||||
self.async_handlers.insert(
|
||||
@@ -672,13 +814,10 @@ impl HandlerRegistry {
|
||||
};
|
||||
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(),
|
||||
})
|
||||
match future.await {
|
||||
Ok(effects) => Ok(effects.into_batch(fingerprint)),
|
||||
Err(error) => map_handler_failure(error, handle_id, fingerprint),
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
@@ -758,6 +897,39 @@ impl HandlerRegistry {
|
||||
self.register_state_typed(handle.id().id, state, handler)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn on_state_result<I, S, C, E, O>(
|
||||
self,
|
||||
handle: Handle<I>,
|
||||
state: S,
|
||||
handler: impl Fn(C) -> Result<O, E> + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
C: FromHandlerState<S>,
|
||||
O: IntoEffect,
|
||||
E: IntoHandlerFailure,
|
||||
{
|
||||
self.register_state_result(handle.id().id, state, handler)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn on_state_form_result<I, S, C, T, E, O>(
|
||||
self,
|
||||
handle: Handle<I>,
|
||||
state: S,
|
||||
handler: impl Fn(C, T) -> Result<O, E> + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
C: FromHandlerState<S>,
|
||||
T: FromInteractionForm,
|
||||
O: IntoEffect,
|
||||
E: IntoHandlerFailure,
|
||||
{
|
||||
self.register_state_typed_result(handle.id().id, state, handler)
|
||||
}
|
||||
|
||||
pub fn on_async<I, E, F>(
|
||||
self,
|
||||
handle: Handle<I>,
|
||||
@@ -798,6 +970,23 @@ impl HandlerRegistry {
|
||||
self.register_state_async(handle.id().id, state, handler)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn on_state_async_result<I, S, C, E, O, 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 = Result<O, E>> + Send + 'static,
|
||||
O: IntoEffect,
|
||||
E: IntoHandlerFailure,
|
||||
{
|
||||
self.register_state_async_result(handle.id().id, state, handler)
|
||||
}
|
||||
|
||||
pub fn on_state_form_async<I, S, C, T, E, F>(
|
||||
self,
|
||||
handle: Handle<I>,
|
||||
@@ -814,6 +1003,7 @@ impl HandlerRegistry {
|
||||
self.register_state_typed_async(handle.id().id, state, handler)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn on_state_form_async_result<I, S, C, T, E, O, F>(
|
||||
self,
|
||||
handle: Handle<I>,
|
||||
@@ -826,7 +1016,7 @@ impl HandlerRegistry {
|
||||
T: FromInteractionForm,
|
||||
F: Future<Output = Result<O, E>> + Send + 'static,
|
||||
O: IntoEffect,
|
||||
E: fmt::Display,
|
||||
E: IntoHandlerFailure,
|
||||
{
|
||||
self.register_state_typed_async_result(handle.id().id, state, handler)
|
||||
}
|
||||
@@ -892,6 +1082,41 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn on_state_result<I, C, E, O>(
|
||||
mut self,
|
||||
handle: Handle<I>,
|
||||
handler: impl Fn(C) -> Result<O, E> + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
C: FromHandlerState<S>,
|
||||
O: IntoEffect,
|
||||
E: IntoHandlerFailure,
|
||||
{
|
||||
self.registry = self
|
||||
.registry
|
||||
.on_state_result(handle, self.state.clone(), handler);
|
||||
self
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn on_result<I, C, T, E, O>(
|
||||
mut self,
|
||||
handle: Handle<I>,
|
||||
handler: impl Fn(C, T) -> Result<O, E> + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
C: FromHandlerState<S>,
|
||||
T: FromInteractionForm,
|
||||
O: IntoEffect,
|
||||
E: IntoHandlerFailure,
|
||||
{
|
||||
self.registry = self
|
||||
.registry
|
||||
.on_state_form_result(handle, self.state.clone(), handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_state_async<I, C, E, F>(
|
||||
mut self,
|
||||
handle: Handle<I>,
|
||||
@@ -908,6 +1133,24 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn on_state_async_result<I, C, E, O, F>(
|
||||
mut self,
|
||||
handle: Handle<I>,
|
||||
handler: impl Fn(C) -> F + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
C: FromHandlerState<S>,
|
||||
F: Future<Output = Result<O, E>> + Send + 'static,
|
||||
O: IntoEffect,
|
||||
E: IntoHandlerFailure,
|
||||
{
|
||||
self.registry = self
|
||||
.registry
|
||||
.on_state_async_result(handle, self.state.clone(), handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_async<I, C, T, E, F>(
|
||||
mut self,
|
||||
handle: Handle<I>,
|
||||
@@ -925,6 +1168,7 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn on_async_result<I, C, T, E, O, F>(
|
||||
mut self,
|
||||
handle: Handle<I>,
|
||||
@@ -935,7 +1179,7 @@ where
|
||||
T: FromInteractionForm,
|
||||
F: Future<Output = Result<O, E>> + Send + 'static,
|
||||
O: IntoEffect,
|
||||
E: fmt::Display,
|
||||
E: IntoHandlerFailure,
|
||||
{
|
||||
self.registry =
|
||||
self.registry
|
||||
@@ -1117,11 +1361,12 @@ impl IntoResponse for DispatchRejection {
|
||||
format!("invalid hemx form for handle id {handle_id}: {message}"),
|
||||
)
|
||||
.into_response(),
|
||||
Self::HandlerError { handle_id, message } => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("hemx handler for handle id {handle_id} failed: {message}"),
|
||||
)
|
||||
.into_response(),
|
||||
Self::HandlerError(HandlerFailure::Response { status, message }) => {
|
||||
(status, message).into_response()
|
||||
}
|
||||
Self::HandlerError(HandlerFailure::Effects(batch)) => {
|
||||
EffectResponse { batch }.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1257,8 +1502,8 @@ mod tests {
|
||||
response::IntoResponse,
|
||||
};
|
||||
use futures_util::stream;
|
||||
use scraper::{Html, Selector};
|
||||
use hemx_core::{EffectBatch, EFFECT_BATCH_ABI_VERSION};
|
||||
use scraper::{Html, Selector};
|
||||
use std::convert::Infallible;
|
||||
|
||||
fn selector(value: &str) -> Selector {
|
||||
|
||||
Reference in New Issue
Block a user