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:
+58
-18
@@ -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,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 {
|
||||
|
||||
Reference in New Issue
Block a user