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
+50 -35
View File
@@ -1,4 +1,5 @@
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use std::path::PathBuf;
use syn::parse::Parser;
@@ -651,20 +652,9 @@ fn add_component_register_helper(mut module: ItemMod, component: &str) -> ItemMo
if handlers.is_empty() {
return module;
}
let calls = handlers.iter().map(|handler| {
let ident = &handler.ident;
if handler.typed_arg_count == 1 && handler.is_async {
quote!(.on_state_async(super::#component_ident::#ident, #ident))
} else if handler.typed_arg_count == 1 {
quote!(.on_state(super::#component_ident::#ident, #ident))
} else if handler.is_async && handler.returns_result {
quote!(.on_async_result(super::#component_ident::#ident, #ident))
} else if handler.is_async {
quote!(.on_async(super::#component_ident::#ident, #ident))
} else {
quote!(.on(super::#component_ident::#ident, #ident))
}
});
let calls = handlers
.iter()
.map(|handler| component_registration_call(handler, &component_ident));
let register: Item = syn::parse2(quote! {
pub fn register(
registry: ::hemx_axum::StateHandlerRegistry<#state_ty>,
@@ -676,20 +666,9 @@ fn add_component_register_helper(mut module: ItemMod, component: &str) -> ItemMo
}
})
.expect("generated component register helper parses");
let calls = handlers.iter().map(|handler| {
let ident = &handler.ident;
if handler.typed_arg_count == 1 && handler.is_async {
quote!(.on_state_async(super::#component_ident::#ident, #ident))
} else if handler.typed_arg_count == 1 {
quote!(.on_state(super::#component_ident::#ident, #ident))
} else if handler.is_async && handler.returns_result {
quote!(.on_async_result(super::#component_ident::#ident, #ident))
} else if handler.is_async {
quote!(.on_async(super::#component_ident::#ident, #ident))
} else {
quote!(.on(super::#component_ident::#ident, #ident))
}
});
let calls = handlers
.iter()
.map(|handler| component_registration_call(handler, &component_ident));
let register_with_state: Item = syn::parse2(quote! {
pub fn register_with_state(
registry: ::hemx_axum::HandlerRegistry,
@@ -710,6 +689,30 @@ fn add_component_register_helper(mut module: ItemMod, component: &str) -> ItemMo
module
}
fn component_registration_call(
handler: &ComponentHandler,
component_ident: &syn::Ident,
) -> TokenStream2 {
let ident = &handler.ident;
if handler.typed_arg_count == 1 && handler.is_async && handler.returns_result {
quote!(.on_state_async_result(super::#component_ident::#ident, #ident))
} else if handler.typed_arg_count == 1 && handler.returns_result {
quote!(.on_state_result(super::#component_ident::#ident, #ident))
} else if handler.typed_arg_count == 1 && handler.is_async {
quote!(.on_state_async(super::#component_ident::#ident, #ident))
} else if handler.typed_arg_count == 1 {
quote!(.on_state(super::#component_ident::#ident, #ident))
} else if handler.is_async && handler.returns_result {
quote!(.on_async_result(super::#component_ident::#ident, #ident))
} else if handler.returns_result {
quote!(.on_result(super::#component_ident::#ident, #ident))
} else if handler.is_async {
quote!(.on_async(super::#component_ident::#ident, #ident))
} else {
quote!(.on(super::#component_ident::#ident, #ident))
}
}
fn component_contract_errors(
path: &PathBuf,
component: Option<&str>,
@@ -1048,18 +1051,28 @@ mod tests {
let module = parse_quote! {
mod handlers {
#[hemx::handler]
fn create(app: super::App, form: super::NewTodo) -> impl hemx::IntoEffect {
hemx::EventName::new("created").emit("")
fn sync_form(app: super::App, form: super::NewTodo) -> impl hemx::IntoEffect {
hemx::EventName::new("sync-form").emit("")
}
#[hemx::handler]
async fn increment(app: super::App) -> impl hemx::IntoEffect {
hemx::EventName::new("incremented").emit("")
async fn async_state(app: super::App) -> impl hemx::IntoEffect {
hemx::EventName::new("async-state").emit("")
}
#[hemx::handler]
async fn save(app: super::App, form: super::NewTodo) -> Result<impl hemx::IntoEffect, super::Error> {
Ok(hemx::EventName::new("saved").emit(""))
fn sync_result(app: super::App) -> Result<impl hemx::IntoEffect, super::Error> {
Ok(hemx::EventName::new("sync-result").emit(""))
}
#[hemx::handler]
async fn async_result(app: super::App) -> Result<impl hemx::IntoEffect, super::Error> {
Ok(hemx::EventName::new("async-result").emit(""))
}
#[hemx::handler]
async fn async_form_result(app: super::App, form: super::NewTodo) -> Result<impl hemx::IntoEffect, super::Error> {
Ok(hemx::EventName::new("async-form-result").emit(""))
}
}
};
@@ -1070,11 +1083,13 @@ mod tests {
assert!(generated.contains("register_with_state"), "{generated}");
assert!(generated.contains("StateHandlerRegistry"), "{generated}");
assert!(
generated.contains("super :: todos :: create"),
generated.contains("super :: todos :: sync_form"),
"{generated}"
);
assert!(generated.contains(". on"), "{generated}");
assert!(generated.contains(". on_state_async"), "{generated}");
assert!(generated.contains(". on_state_result"), "{generated}");
assert!(generated.contains(". on_state_async_result"), "{generated}");
assert!(generated.contains(". on_async_result"), "{generated}");
}
}