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:
slhx agent
2026-06-05 08:36:39 +02:00
parent c33500440e
commit 53480dba5a
8 changed files with 536 additions and 110 deletions
+82 -8
View File
@@ -1,14 +1,14 @@
use axum::extract::State;
use axum::http::{header, HeaderMap};
use axum::response::IntoResponse;
use scraper::{Html, Selector};
use hemx_axum::{
interactions, runtime_js, DispatchRejection, EffectResponse, Form, InteractionForm,
InteractionFormRejection, InteractionRequest, PageMode, PageRequest, PageResponse,
HEMX_CONTENT_TYPE, HEMX_FINGERPRINT_HEADER, HEMX_PARTIAL_HEADER, HEMX_RUNTIME_CONTENT_TYPE,
HEMX_TITLE_HEADER,
interactions, runtime_js, DispatchRejection, EffectResponse, Form, HandlerErrorContext,
HandlerFailure, InteractionForm, InteractionFormRejection, InteractionRequest,
IntoHandlerFailure, PageMode, PageRequest, PageResponse, HEMX_CONTENT_TYPE,
HEMX_FINGERPRINT_HEADER, HEMX_PARTIAL_HEADER, HEMX_RUNTIME_CONTENT_TYPE, HEMX_TITLE_HEADER,
};
use hemx_core::{push, BuildFingerprint, Handle, IntoEffect, SafeHtml, Slot};
use scraper::{Html, Selector};
fn selector(value: &str) -> Selector {
Selector::parse(value).expect("test selector parses")
@@ -306,6 +306,21 @@ impl std::fmt::Display for HandlerBoom {
impl std::error::Error for HandlerBoom {}
impl IntoHandlerFailure for HandlerBoom {
fn into_handler_failure(self, _context: HandlerErrorContext) -> HandlerFailure {
HandlerFailure::internal(self.to_string())
}
}
#[derive(Debug)]
struct UiFailure;
impl IntoHandlerFailure for UiFailure {
fn into_handler_failure(self, context: HandlerErrorContext) -> HandlerFailure {
HandlerFailure::effects(Slot::<String>::new(3).text("try again"), context)
}
}
#[tokio::test]
async fn interaction_request_dispatches_async_typed_state_extractors() {
// req: axum_integration/003 req: form/004 req: canonical_authoring/003
@@ -359,10 +374,69 @@ async fn interaction_request_reports_async_result_handler_errors() {
assert_eq!(
error,
DispatchRejection::HandlerError {
handle_id: 11,
DispatchRejection::HandlerError(HandlerFailure::Response {
status: axum::http::StatusCode::INTERNAL_SERVER_ERROR,
message: "database unavailable".to_owned(),
}
})
);
}
#[tokio::test]
async fn interaction_request_maps_async_result_errors_to_effects() {
// req: axum_integration/003 req: failure/004
async fn open_project(
State(_multiplier): State<u32>,
Form(_input): Form<OpenProject>,
) -> Result<impl IntoEffect, UiFailure> {
Err::<(), _>(UiFailure)
}
let request = InteractionRequest::from(InteractionForm::for_handle(
Handle::<()>::new(12),
vec![("project_id".to_owned(), "21".to_owned())],
));
let response = request
.dispatch_async(
interactions(BuildFingerprint(4))
.with_state(2_u32)
.on_async_result(Handle::<()>::new(12), open_project)
.into_registry(),
)
.await
.unwrap();
assert_eq!(
response.batch.ops,
vec![Slot::<String>::new(3).text("try again")]
);
}
#[test]
fn interaction_request_maps_sync_result_errors_to_effects() {
// req: axum_integration/003 req: failure/004
fn open_project(
State(_multiplier): State<u32>,
Form(_input): Form<OpenProject>,
) -> Result<impl IntoEffect, UiFailure> {
Err::<(), _>(UiFailure)
}
let request = InteractionRequest::from(InteractionForm::for_handle(
Handle::<()>::new(13),
vec![("project_id".to_owned(), "21".to_owned())],
));
let response = request
.dispatch(
interactions(BuildFingerprint(4))
.with_state(2_u32)
.on_result(Handle::<()>::new(13), open_project)
.into_registry(),
)
.unwrap();
assert_eq!(
response.batch.ops,
vec![Slot::<String>::new(3).text("try again")]
);
}