From d991addbb52becb3fce970cf80c1bbe22dc8e305 Mon Sep 17 00:00:00 2001 From: slhx agent Date: Thu, 11 Jun 2026 21:44:11 +0200 Subject: [PATCH] refactor(examples): shrink workout route wiring Move workout interaction wiring behind an app-owned registry function and use typed form adapters so the runnable binary no longer carries repetitive per-handler closure boilerplate. req: codegen/002 req: examples/001 --- examples/workout/src/lib.rs | 76 +++++++++++++++++++++++++++--------- examples/workout/src/main.rs | 51 +----------------------- 2 files changed, 60 insertions(+), 67 deletions(-) diff --git a/examples/workout/src/lib.rs b/examples/workout/src/lib.rs index 1a25e07..471697f 100644 --- a/examples/workout/src/lib.rs +++ b/examples/workout/src/lib.rs @@ -1,5 +1,6 @@ use hemplate::Hemplate; use hemx::{Html, IntoEffect}; +use hemx_axum::{interactions, Form, HandlerRegistry}; use hemx_host::{ browser_pwa_host_profile, native_shell_host_profile, Capability, CapabilityManifest, CapabilityShape, CapabilityUse, HapticPattern, HostCall, HostCallId, HostEvent, @@ -350,6 +351,37 @@ pub fn render(state: &WorkoutState) -> Html { ui::render(&view(state)) } +#[derive(Clone, Debug)] +#[hemx::form("change_weight")] +pub struct ChangeWeightInput { + kg: f32, +} + +#[derive(Clone, Debug)] +#[hemx::form("record_note")] +pub struct RecordNoteInput { + text: String, +} + +pub fn registry(state: AppState) -> HandlerRegistry { + // req: codegen/002 req: examples/001 + interactions(ui::BUILD_FINGERPRINT) + .with_state(state) + .on_state(ui::workout::complete_set, complete_set) + .on(ui::workout::change_weight, change_weight_form) + .on_state(ui::workout::skip_exercise, skip_exercise) + .on(ui::workout::record_note, record_note_form) + .on_state(ui::workout::replay_export, replay_export) + .on_state(ui::workout::export_log, export_log) + .on_state(ui::workout::record_share_result, record_share_result) + .on_state(ui::workout::request_native_haptic, request_native_haptic) + .on_state( + ui::workout::record_native_haptic_ack, + record_native_haptic_ack, + ) + .into_registry() +} + fn effects(state: &WorkoutState, status: impl Into) -> impl IntoEffect { // req: local/001 req: local/004 ( @@ -361,7 +393,7 @@ fn effects(state: &WorkoutState, status: impl Into) -> impl IntoEffect { ) } -pub fn complete_set(app: &AppState) -> impl IntoEffect { +pub fn complete_set(app: AppState) -> impl IntoEffect { app.update(|state| { let event = state.accept(WorkoutCommand::CompleteSet); effects( @@ -371,7 +403,7 @@ pub fn complete_set(app: &AppState) -> impl IntoEffect { }) } -pub fn change_weight(app: &AppState, kg: f32) -> impl IntoEffect { +pub fn change_weight(app: AppState, kg: f32) -> impl IntoEffect { app.update(|state| { let event = state.accept(WorkoutCommand::ChangeWeight { kg }); effects( @@ -381,7 +413,7 @@ pub fn change_weight(app: &AppState, kg: f32) -> impl IntoEffect { }) } -pub fn skip_exercise(app: &AppState) -> impl IntoEffect { +pub fn skip_exercise(app: AppState) -> impl IntoEffect { app.update(|state| { let event = state.accept(WorkoutCommand::SkipExercise); effects( @@ -391,7 +423,15 @@ pub fn skip_exercise(app: &AppState) -> impl IntoEffect { }) } -pub fn record_note(app: &AppState, text: impl Into) -> impl IntoEffect { +pub fn change_weight_form(app: AppState, Form(input): Form) -> impl IntoEffect { + change_weight(app, input.kg) +} + +pub fn record_note_form(app: AppState, Form(input): Form) -> impl IntoEffect { + record_note(app, input.text) +} + +pub fn record_note(app: AppState, text: impl Into) -> impl IntoEffect { app.update(|state| { let event = state.accept(WorkoutCommand::RecordNote { text: text.into() }); effects( @@ -401,7 +441,7 @@ pub fn record_note(app: &AppState, text: impl Into) -> impl IntoEffect { }) } -pub fn replay_export(app: &AppState) -> impl IntoEffect { +pub fn replay_export(app: AppState) -> impl IntoEffect { app.update(|state| { // req: local/001 req: local/003 req: local/004 let export = state.export_event_log(); @@ -410,7 +450,7 @@ pub fn replay_export(app: &AppState) -> impl IntoEffect { }) } -pub fn export_log(app: &AppState) -> impl IntoEffect { +pub fn export_log(app: AppState) -> impl IntoEffect { app.update(|state| { // req: host/001 req: host/004 req: local/003 let manifest = CapabilityManifest::new([CapabilityUse::new( @@ -429,7 +469,7 @@ pub fn export_log(app: &AppState) -> impl IntoEffect { }) } -pub fn record_share_result(app: &AppState) -> impl IntoEffect { +pub fn record_share_result(app: AppState) -> impl IntoEffect { app.update(|state| { // req: host/002 req: host/005 req: local/003 apply_host_event( @@ -443,7 +483,7 @@ pub fn record_share_result(app: &AppState) -> impl IntoEffect { }) } -pub fn request_native_haptic(app: &AppState) -> impl IntoEffect { +pub fn request_native_haptic(app: AppState) -> impl IntoEffect { app.update(|state| { // req: host/001 req: host/004 let manifest = CapabilityManifest::new([CapabilityUse::new( @@ -467,7 +507,7 @@ pub fn request_native_haptic(app: &AppState) -> impl IntoEffect { }) } -pub fn record_native_haptic_ack(app: &AppState) -> impl IntoEffect { +pub fn record_native_haptic_ack(app: AppState) -> impl IntoEffect { app.update(|state| { // req: host/002 req: host/005 apply_host_event( @@ -520,7 +560,7 @@ mod tests { fn local_command_projects_before_ui_effects() { // req: local/001 req: local/004 let app = AppState::demo(); - let effects = run(|()| complete_set(&app), ()); + let effects = run(|()| complete_set(app.clone()), ()); assert_eq!( app.with_workout(|state| ( @@ -548,13 +588,13 @@ mod tests { fn export_payload_replays_into_projection_before_ui_effects() { // req: local/001 req: local/003 req: local/004 let app = AppState::demo(); - run(|()| complete_set(&app), ()); - run(|()| change_weight(&app, 28.0), ()); + run(|()| complete_set(app.clone()), ()); + run(|()| change_weight(app.clone(), 28.0), ()); let export = app.with_workout(WorkoutState::export_event_log); assert!(export.contains("set_completed\tGoblet squat\t1\t8\t24")); assert!(export.contains("weight_changed\tGoblet squat\t28")); - let replay = run(|()| replay_export(&app), ()); + let replay = run(|()| replay_export(app.clone()), ()); assert!(contains_payload_text( &replay, "Replayed 2 exported workout events" @@ -575,16 +615,16 @@ mod tests { fn host_export_result_returns_through_app_code() { // req: host/001 req: host/002 req: host/005 req: local/003 let app = AppState::demo(); - run(|()| complete_set(&app), ()); + run(|()| complete_set(app.clone()), ()); - let export = run(|()| export_log(&app), ()); + let export = run(|()| export_log(app.clone()), ()); assert!(contains_payload_text( &export, "Export request checked against host manifest" )); assert!(contains_payload_text(&export, "waiting for host result")); - let shared = run(|()| record_share_result(&app), ()); + let shared = run(|()| record_share_result(app.clone()), ()); assert!(app.with_workout(|state| state.projection.exported)); assert!(contains_payload_text( &shared, @@ -601,7 +641,7 @@ mod tests { // req: host/001 req: host/002 req: host/005 let app = AppState::demo(); - let request = run(|()| request_native_haptic(&app), ()); + let request = run(|()| request_native_haptic(app.clone()), ()); assert!(contains_payload_text( &request, "Native-shell host call checked against manifest" @@ -611,7 +651,7 @@ mod tests { "waiting for host acknowledgment" )); - let ack = run(|()| record_native_haptic_ack(&app), ()); + let ack = run(|()| record_native_haptic_ack(app.clone()), ()); assert!(contains_payload_text( &ack, "Native-shell host result accepted by app code" diff --git a/examples/workout/src/main.rs b/examples/workout/src/main.rs index 1572ed7..b5fe00d 100644 --- a/examples/workout/src/main.rs +++ b/examples/workout/src/main.rs @@ -2,8 +2,7 @@ use axum::extract::State; use axum::response::IntoResponse; use axum::routing::get; use axum::Router; -use hemx_axum::{interactions, runtime_js, runtime_js_path, EffectResponse, InteractionRequest}; -use hemx_workout_example::ui::workout; +use hemx_axum::{runtime_js, runtime_js_path, EffectResponse, InteractionRequest}; use hemx_workout_example::{self as workout_app, AppState}; use std::net::SocketAddr; use std::str::FromStr; @@ -39,53 +38,7 @@ async fn interact( State(state): State, request: InteractionRequest, ) -> Result { - request - .dispatch_async( - interactions(hemx_workout_example::ui::BUILD_FINGERPRINT) - .on(workout::complete_set, { - let state = state.clone(); - move |_| workout_app::complete_set(&state) - }) - .on(workout::change_weight, { - let state = state.clone(); - move |form| { - workout_app::change_weight(&state, form.parse::("kg").unwrap_or(0.0)) - } - }) - .on(workout::skip_exercise, { - let state = state.clone(); - move |_| workout_app::skip_exercise(&state) - }) - .on(workout::record_note, { - let state = state.clone(); - move |form| { - workout_app::record_note( - &state, - form.value("text").unwrap_or("").to_owned(), - ) - } - }) - .on(workout::replay_export, { - let state = state.clone(); - move |_| workout_app::replay_export(&state) - }) - .on(workout::export_log, { - let state = state.clone(); - move |_| workout_app::export_log(&state) - }) - .on(workout::record_share_result, { - let state = state.clone(); - move |_| workout_app::record_share_result(&state) - }) - .on(workout::request_native_haptic, { - let state = state.clone(); - move |_| workout_app::request_native_haptic(&state) - }) - .on(workout::record_native_haptic_ack, move |_| { - workout_app::record_native_haptic_ack(&state) - }), - ) - .await + request.dispatch_async(workout_app::registry(state)).await } async fn runtime() -> impl IntoResponse {