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
This commit is contained in:
slhx agent
2026-06-11 21:44:11 +02:00
parent 5e4f02f419
commit d991addbb5
2 changed files with 60 additions and 67 deletions
+58 -18
View File
@@ -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<String>) -> impl IntoEffect {
// req: local/001 req: local/004
(
@@ -361,7 +393,7 @@ fn effects(state: &WorkoutState, status: impl Into<String>) -> 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<String>) -> impl IntoEffect {
pub fn change_weight_form(app: AppState, Form(input): Form<ChangeWeightInput>) -> impl IntoEffect {
change_weight(app, input.kg)
}
pub fn record_note_form(app: AppState, Form(input): Form<RecordNoteInput>) -> impl IntoEffect {
record_note(app, input.text)
}
pub fn record_note(app: AppState, text: impl Into<String>) -> 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<String>) -> 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"
+2 -49
View File
@@ -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<AppState>,
request: InteractionRequest,
) -> Result<EffectResponse, impl IntoResponse> {
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::<f32>("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 {