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 hemplate::Hemplate;
|
||||||
use hemx::{Html, IntoEffect};
|
use hemx::{Html, IntoEffect};
|
||||||
|
use hemx_axum::{interactions, Form, HandlerRegistry};
|
||||||
use hemx_host::{
|
use hemx_host::{
|
||||||
browser_pwa_host_profile, native_shell_host_profile, Capability, CapabilityManifest,
|
browser_pwa_host_profile, native_shell_host_profile, Capability, CapabilityManifest,
|
||||||
CapabilityShape, CapabilityUse, HapticPattern, HostCall, HostCallId, HostEvent,
|
CapabilityShape, CapabilityUse, HapticPattern, HostCall, HostCallId, HostEvent,
|
||||||
@@ -350,6 +351,37 @@ pub fn render(state: &WorkoutState) -> Html {
|
|||||||
ui::render(&view(state))
|
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 {
|
fn effects(state: &WorkoutState, status: impl Into<String>) -> impl IntoEffect {
|
||||||
// req: local/001 req: local/004
|
// 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| {
|
app.update(|state| {
|
||||||
let event = state.accept(WorkoutCommand::CompleteSet);
|
let event = state.accept(WorkoutCommand::CompleteSet);
|
||||||
effects(
|
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| {
|
app.update(|state| {
|
||||||
let event = state.accept(WorkoutCommand::ChangeWeight { kg });
|
let event = state.accept(WorkoutCommand::ChangeWeight { kg });
|
||||||
effects(
|
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| {
|
app.update(|state| {
|
||||||
let event = state.accept(WorkoutCommand::SkipExercise);
|
let event = state.accept(WorkoutCommand::SkipExercise);
|
||||||
effects(
|
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| {
|
app.update(|state| {
|
||||||
let event = state.accept(WorkoutCommand::RecordNote { text: text.into() });
|
let event = state.accept(WorkoutCommand::RecordNote { text: text.into() });
|
||||||
effects(
|
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| {
|
app.update(|state| {
|
||||||
// req: local/001 req: local/003 req: local/004
|
// req: local/001 req: local/003 req: local/004
|
||||||
let export = state.export_event_log();
|
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| {
|
app.update(|state| {
|
||||||
// req: host/001 req: host/004 req: local/003
|
// req: host/001 req: host/004 req: local/003
|
||||||
let manifest = CapabilityManifest::new([CapabilityUse::new(
|
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| {
|
app.update(|state| {
|
||||||
// req: host/002 req: host/005 req: local/003
|
// req: host/002 req: host/005 req: local/003
|
||||||
apply_host_event(
|
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| {
|
app.update(|state| {
|
||||||
// req: host/001 req: host/004
|
// req: host/001 req: host/004
|
||||||
let manifest = CapabilityManifest::new([CapabilityUse::new(
|
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| {
|
app.update(|state| {
|
||||||
// req: host/002 req: host/005
|
// req: host/002 req: host/005
|
||||||
apply_host_event(
|
apply_host_event(
|
||||||
@@ -520,7 +560,7 @@ mod tests {
|
|||||||
fn local_command_projects_before_ui_effects() {
|
fn local_command_projects_before_ui_effects() {
|
||||||
// req: local/001 req: local/004
|
// req: local/001 req: local/004
|
||||||
let app = AppState::demo();
|
let app = AppState::demo();
|
||||||
let effects = run(|()| complete_set(&app), ());
|
let effects = run(|()| complete_set(app.clone()), ());
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.with_workout(|state| (
|
app.with_workout(|state| (
|
||||||
@@ -548,13 +588,13 @@ mod tests {
|
|||||||
fn export_payload_replays_into_projection_before_ui_effects() {
|
fn export_payload_replays_into_projection_before_ui_effects() {
|
||||||
// req: local/001 req: local/003 req: local/004
|
// req: local/001 req: local/003 req: local/004
|
||||||
let app = AppState::demo();
|
let app = AppState::demo();
|
||||||
run(|()| complete_set(&app), ());
|
run(|()| complete_set(app.clone()), ());
|
||||||
run(|()| change_weight(&app, 28.0), ());
|
run(|()| change_weight(app.clone(), 28.0), ());
|
||||||
let export = app.with_workout(WorkoutState::export_event_log);
|
let export = app.with_workout(WorkoutState::export_event_log);
|
||||||
assert!(export.contains("set_completed\tGoblet squat\t1\t8\t24"));
|
assert!(export.contains("set_completed\tGoblet squat\t1\t8\t24"));
|
||||||
assert!(export.contains("weight_changed\tGoblet squat\t28"));
|
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(
|
assert!(contains_payload_text(
|
||||||
&replay,
|
&replay,
|
||||||
"Replayed 2 exported workout events"
|
"Replayed 2 exported workout events"
|
||||||
@@ -575,16 +615,16 @@ mod tests {
|
|||||||
fn host_export_result_returns_through_app_code() {
|
fn host_export_result_returns_through_app_code() {
|
||||||
// req: host/001 req: host/002 req: host/005 req: local/003
|
// req: host/001 req: host/002 req: host/005 req: local/003
|
||||||
let app = AppState::demo();
|
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(
|
assert!(contains_payload_text(
|
||||||
&export,
|
&export,
|
||||||
"Export request checked against host manifest"
|
"Export request checked against host manifest"
|
||||||
));
|
));
|
||||||
assert!(contains_payload_text(&export, "waiting for host result"));
|
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!(app.with_workout(|state| state.projection.exported));
|
||||||
assert!(contains_payload_text(
|
assert!(contains_payload_text(
|
||||||
&shared,
|
&shared,
|
||||||
@@ -601,7 +641,7 @@ mod tests {
|
|||||||
// req: host/001 req: host/002 req: host/005
|
// req: host/001 req: host/002 req: host/005
|
||||||
let app = AppState::demo();
|
let app = AppState::demo();
|
||||||
|
|
||||||
let request = run(|()| request_native_haptic(&app), ());
|
let request = run(|()| request_native_haptic(app.clone()), ());
|
||||||
assert!(contains_payload_text(
|
assert!(contains_payload_text(
|
||||||
&request,
|
&request,
|
||||||
"Native-shell host call checked against manifest"
|
"Native-shell host call checked against manifest"
|
||||||
@@ -611,7 +651,7 @@ mod tests {
|
|||||||
"waiting for host acknowledgment"
|
"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(
|
assert!(contains_payload_text(
|
||||||
&ack,
|
&ack,
|
||||||
"Native-shell host result accepted by app code"
|
"Native-shell host result accepted by app code"
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ use axum::extract::State;
|
|||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use hemx_axum::{interactions, runtime_js, runtime_js_path, EffectResponse, InteractionRequest};
|
use hemx_axum::{runtime_js, runtime_js_path, EffectResponse, InteractionRequest};
|
||||||
use hemx_workout_example::ui::workout;
|
|
||||||
use hemx_workout_example::{self as workout_app, AppState};
|
use hemx_workout_example::{self as workout_app, AppState};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@@ -39,53 +38,7 @@ async fn interact(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
request: InteractionRequest,
|
request: InteractionRequest,
|
||||||
) -> Result<EffectResponse, impl IntoResponse> {
|
) -> Result<EffectResponse, impl IntoResponse> {
|
||||||
request
|
request.dispatch_async(workout_app::registry(state)).await
|
||||||
.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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn runtime() -> impl IntoResponse {
|
async fn runtime() -> impl IntoResponse {
|
||||||
|
|||||||
Reference in New Issue
Block a user