//! Starter content for a fresh profile. //! //! A common set of tags and exercises so a new profile is not empty on //! first open. The user is free to edit or delete anything here; the seed //! runs once at profile creation and is never re-applied. use crate::db::Db; use crate::values::LoadUnit; use crate::error::Error; use crate::templates::ResistanceType; struct SeedExercise { name: &'static str, resistance_type: ResistanceType, tags: &'static [&'static str], } const SEED_TAGS: &[&str] = &[ "push", "pull", "squat", "hinge", "chest", "back", "shoulders", "biceps", "triceps", "quads", "hamstrings", "glutes", "calves", "core", ]; const SEED_EXERCISES: &[SeedExercise] = &[ // Barbell staples. SeedExercise { name: "back squat", resistance_type: ResistanceType::Freeweight, tags: &["squat", "quads", "glutes", "core"], }, SeedExercise { name: "front squat", resistance_type: ResistanceType::Freeweight, tags: &["squat", "quads", "core"], }, SeedExercise { name: "deadlift", resistance_type: ResistanceType::Freeweight, tags: &["hinge", "back", "hamstrings", "glutes"], }, SeedExercise { name: "romanian deadlift", resistance_type: ResistanceType::Freeweight, tags: &["hinge", "hamstrings", "glutes"], }, SeedExercise { name: "bench press", resistance_type: ResistanceType::Freeweight, tags: &["push", "chest", "triceps", "shoulders"], }, SeedExercise { name: "incline bench press", resistance_type: ResistanceType::Freeweight, tags: &["push", "chest", "shoulders", "triceps"], }, SeedExercise { name: "overhead press", resistance_type: ResistanceType::Freeweight, tags: &["push", "shoulders", "triceps"], }, SeedExercise { name: "barbell row", resistance_type: ResistanceType::Freeweight, tags: &["pull", "back", "biceps"], }, // Machines. SeedExercise { name: "lat pulldown", resistance_type: ResistanceType::Machine, tags: &["pull", "back", "biceps"], }, SeedExercise { name: "seated row", resistance_type: ResistanceType::Machine, tags: &["pull", "back", "biceps"], }, SeedExercise { name: "leg press", resistance_type: ResistanceType::Machine, tags: &["squat", "quads", "glutes"], }, SeedExercise { name: "leg curl", resistance_type: ResistanceType::Machine, tags: &["hamstrings"], }, SeedExercise { name: "leg extension", resistance_type: ResistanceType::Machine, tags: &["quads"], }, SeedExercise { name: "calf raise", resistance_type: ResistanceType::Machine, tags: &["calves"], }, SeedExercise { name: "tricep pushdown", resistance_type: ResistanceType::Machine, tags: &["push", "triceps"], }, SeedExercise { name: "cable curl", resistance_type: ResistanceType::Machine, tags: &["pull", "biceps"], }, // Bodyweight. SeedExercise { name: "pull-up", resistance_type: ResistanceType::Bodyweight, tags: &["pull", "back", "biceps"], }, SeedExercise { name: "chin-up", resistance_type: ResistanceType::Bodyweight, tags: &["pull", "back", "biceps"], }, SeedExercise { name: "dip", resistance_type: ResistanceType::Bodyweight, tags: &["push", "chest", "triceps"], }, SeedExercise { name: "push-up", resistance_type: ResistanceType::Bodyweight, tags: &["push", "chest", "triceps"], }, SeedExercise { name: "hanging leg raise", resistance_type: ResistanceType::Bodyweight, tags: &["core"], }, ]; /// Insert the starter tag vocabulary and exercise library into a fresh /// profile. Idempotent: pre-existing tags are reused via `upsert_tag`; /// pre-existing exercises by name are skipped (`UNIQUE` constraint on /// `exercises.name` would otherwise error). Load unit and increment are /// derived from the profile's `LoadUnit`. pub fn seed_starter_content(db: &Db, unit: LoadUnit) -> Result<(), Error> { for tag in SEED_TAGS { db.upsert_tag(tag)?; } let existing = db.list_exercises()?; for seed in SEED_EXERCISES { if existing.iter().any(|e| e.name == seed.name) { continue; } let tag_ids = seed .tags .iter() .map(|name| Ok(db.upsert_tag(name)?.id)) .collect::, Error>>()?; let increment = increment_for(seed.resistance_type, unit); db.create_exercise( seed.name, seed.resistance_type, unit, increment, &tag_ids, )?; } Ok(()) } fn increment_for(rt: ResistanceType, unit: LoadUnit) -> f64 { match rt { ResistanceType::Bodyweight => 0.0, ResistanceType::CardioTime | ResistanceType::CardioDistance => 0.0, ResistanceType::Freeweight => match unit { LoadUnit::Kg => 2.5, LoadUnit::Lb => 5.0, }, ResistanceType::Machine => match unit { LoadUnit::Kg => 5.0, LoadUnit::Lb => 10.0, }, } } #[cfg(test)] mod tests { use super::*; fn fresh() -> Db { let db = Db::open_in_memory().unwrap(); db.init_profile("self", LoadUnit::Kg).unwrap(); db } #[test] fn seed_creates_expected_counts() { let db = fresh(); seed_starter_content(&db, LoadUnit::Kg).unwrap(); assert_eq!(db.list_tags().unwrap().len(), SEED_TAGS.len()); assert_eq!(db.list_exercises().unwrap().len(), SEED_EXERCISES.len()); } #[test] fn seed_is_idempotent_when_run_twice() { let db = fresh(); seed_starter_content(&db, LoadUnit::Kg).unwrap(); seed_starter_content(&db, LoadUnit::Kg).unwrap(); assert_eq!(db.list_exercises().unwrap().len(), SEED_EXERCISES.len()); } #[test] fn seed_respects_unit_for_increments() { let db = fresh(); seed_starter_content(&db, LoadUnit::Lb).unwrap(); let squat = db .list_exercises() .unwrap() .into_iter() .find(|e| e.name == "back squat") .unwrap(); assert_eq!(squat.load_unit, LoadUnit::Lb); assert_eq!(squat.increment, 5.0); let leg_press = db .list_exercises() .unwrap() .into_iter() .find(|e| e.name == "leg press") .unwrap(); assert_eq!(leg_press.increment, 10.0); let pullup = db .list_exercises() .unwrap() .into_iter() .find(|e| e.name == "pull-up") .unwrap(); assert_eq!(pullup.increment, 0.0); } #[test] fn seed_wires_up_tags() { let db = fresh(); seed_starter_content(&db, LoadUnit::Kg).unwrap(); let squat = db .list_exercises() .unwrap() .into_iter() .find(|e| e.name == "back squat") .unwrap(); assert_eq!(squat.tag_ids.len(), 4); } }