Sync your own workout data from your "Strong" app
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor data_transformer; removed prints from library

+115 -99
+13 -2
strong-api-dump/src/main.rs
··· 16 16 let strong_backend = env::var("STRONG_BACKEND").expect("STRONG_BACKEND must be set"); 17 17 let url = Url::parse(&strong_backend).expect("STRONG_BACKEND is not a valid URL"); 18 18 19 + let start = std::time::Instant::now(); 20 + 19 21 let strong_api = StrongApi::new(url); 20 22 21 23 /* ··· 44 46 let response_text = std::fs::read_to_string("response.json")?; 45 47 let user: UserResponse = serde_json::from_str(&response_text)?; 46 48 47 - let data_transformer = DataTransformer::new() 48 - .with_measurements_response(measurements_response); 49 + println!( 50 + "Measurements count: {}/{}", 51 + &measurements_response.embedded.measurements.len(), 52 + &measurements_response.total 53 + ); 54 + 55 + let data_transformer = DataTransformer::new().with_measurements_response(measurements_response); 49 56 50 57 let workouts = data_transformer 51 58 .get_measurements_from_logs(&user.embedded.log) ··· 62 69 }); 63 70 }); 64 71 }); 72 + 73 + let end = start.elapsed(); 74 + 75 + println!("Time elapsed: {:?}", end); 65 76 66 77 Ok(()) 67 78 }
+100 -90
strong-api-lib/src/data_transformer.rs
··· 1 - use crate::json_response::{CellSetGroupLinks, Log, Measurement, MeasurementsResponse}; 1 + use crate::json_response::{CellSet, CellSetGroup, CellSetGroupLinks, Log, Measurement, MeasurementsResponse}; 2 2 use std::collections::HashMap; 3 3 4 4 #[derive(Debug)] ··· 27 27 } 28 28 29 29 pub struct DataTransformer { 30 - measurements_response: Option<MeasurementsResponse> 30 + measurements_response: Option<MeasurementsResponse>, 31 31 } 32 32 33 33 impl DataTransformer { 34 34 pub fn new() -> Self { 35 35 Self { 36 - measurements_response: None 36 + measurements_response: None, 37 37 } 38 38 } 39 39 40 - pub fn with_measurements_response(mut self, with_measurements_response: MeasurementsResponse) -> Self { 40 + pub fn with_measurements_response( 41 + mut self, 42 + with_measurements_response: MeasurementsResponse, 43 + ) -> Self { 41 44 self.measurements_response = Some(with_measurements_response); 42 45 self 43 46 } 44 47 45 48 pub fn get_measurements_from_logs( 46 49 &self, 47 - logs_option: &Option<Vec<Log>> 50 + logs_option: &Option<Vec<Log>>, 48 51 ) -> Result<Vec<Workout>, serde_json::Error> { 49 52 let logs = match logs_option { 50 53 Some(logs) => logs, 51 54 None => return Ok(Vec::new()), 52 55 }; 53 56 54 - //if measurements set, create lookup table 57 + let lookup = self.create_measurement_lookup(); 58 + let workouts = logs.iter() 59 + .map(|log| self.process_log_to_workout(log, &lookup)) 60 + .collect(); 61 + 62 + Ok(workouts) 63 + } 64 + 65 + fn create_measurement_lookup(&self) -> HashMap<String, Measurement> { 55 66 let mut lookup: HashMap<String, Measurement> = HashMap::new(); 67 + 56 68 if let Some(measurements) = &self.measurements_response { 57 - println!( 58 - "Measurements count: {}/{}", 59 - measurements.embedded.measurements.len(), 60 - measurements.total 61 - ); 62 69 for measurement in &measurements.embedded.measurements { 63 70 lookup.insert(measurement.id.clone(), measurement.clone()); 64 71 } 65 72 } 66 73 67 - let mut workouts = Vec::new(); 74 + lookup 75 + } 68 76 69 - // Process every log. 70 - for log in logs { 71 - let workout_id = log.id.clone(); 72 - // Assuming `name` can be converted to a String. 73 - let workout_name = log.name.clone().unwrap_or_default().to_string(); 74 - let timezone = log.timezone_id.clone(); 75 - let start_date = log.start_date.clone(); 76 - let end_date = log.end_date.clone(); 77 + fn process_log_to_workout(&self, log: &Log, lookup: &HashMap<String, Measurement>) -> Workout { 78 + let exercises = log 79 + .embedded 80 + .cell_set_group 81 + .iter() 82 + .filter_map(|cell_set_group| { 83 + self.process_cell_set_group_to_exercise(cell_set_group, lookup) 84 + }) 85 + .collect(); 77 86 78 - let mut exercises = Vec::new(); 87 + Workout { 88 + id: log.id.clone(), 89 + name: log.name.clone().unwrap_or_default().to_string(), 90 + timezone: log.timezone_id.clone(), 91 + start_date: log.start_date.clone(), 92 + end_date: log.end_date.clone(), 93 + exercises, 94 + } 95 + } 79 96 80 - // Iterate over each cellSetGroup in the log. 81 - for cell_set_group in &log.embedded.cell_set_group { 82 - let mut sets = Vec::new(); 97 + fn process_cell_set_group_to_exercise( 98 + &self, 99 + cell_set_group: &CellSetGroup, 100 + lookup: &HashMap<String, Measurement>, 101 + ) -> Option<Exercise> { 102 + let sets: Vec<Set> = cell_set_group 103 + .cell_sets 104 + .iter() 105 + .filter_map(|cell_set| self.process_cell_set_to_set(cell_set)) 106 + .collect(); 83 107 84 - // Process each cell set in the group. 85 - for cell_set in &cell_set_group.cell_sets { 86 - // Skip any cell set that represents a rest timer or a note. 87 - if !cell_set 88 - .cells 89 - .iter() 90 - .any(|cell| cell.cell_type == "REST_TIMER" || cell.cell_type == "NOTE") 91 - { 92 - let weight = cell_set 93 - .cells 94 - .iter() 95 - .find(|cell| { 96 - cell.cell_type == "OTHER_WEIGHT" 97 - || cell.cell_type == "DUMBBELL_WEIGHT" 98 - || cell.cell_type == "BARBELL_WEIGHT" 99 - || cell.cell_type == "WEIGHTED_BODYWEIGHT" 100 - }) 101 - .and_then(|cell| cell.value.as_ref()) 102 - .and_then(|s| s.parse::<f32>().ok()); 108 + if sets.is_empty() { 109 + return None; 110 + } 103 111 104 - let reps = cell_set 105 - .cells 106 - .iter() 107 - .find(|cell| cell.cell_type == "REPS") 108 - .and_then(|cell| cell.value.as_ref()) 109 - .and_then(|s| s.parse::<u32>().ok()) 110 - .unwrap_or(0); 112 + let workout_id = Self::get_workout_id_from_link(&cell_set_group.links); 111 113 112 - let rpe = cell_set 113 - .cells 114 - .iter() 115 - .find(|cell| cell.cell_type == "RPE") 116 - .and_then(|cell| cell.value.as_ref()) 117 - .and_then(|s| s.parse::<f32>().ok()); 114 + // Get workout name from measurements if available 115 + let name = lookup 116 + .get(&workout_id) 117 + .map(|measurement| measurement.name.to_string()) 118 + .unwrap_or_default(); 118 119 119 - let id = cell_set.id.clone(); 120 + Some(Exercise { 121 + id: cell_set_group.id.clone(), 122 + name, 123 + sets, 124 + }) 125 + } 120 126 121 - sets.push(Set { 122 - id, 123 - weight, 124 - reps, 125 - rpe, 126 - }); 127 - } 128 - } 127 + fn process_cell_set_to_set(&self, cell_set: &CellSet) -> Option<Set> { 128 + // Skip rest timers or notes 129 + if cell_set.cells.iter() 130 + .any(|cell| matches!(cell.cell_type.as_str(), "REST_TIMER" | "NOTE")) 131 + { 132 + return None; 133 + } 129 134 130 - let mut name = String::new(); 131 - let workout_id = DataTransformer::get_workout_id_from_link(&cell_set_group.links); 132 - // get workout name from measurements if available 133 - if let Some(measurement) = lookup.get(&workout_id) { 134 - name = (measurement.name).to_string(); 135 - } 135 + let weight = cell_set 136 + .cells 137 + .iter() 138 + .find(|cell| { 139 + matches!( 140 + cell.cell_type.as_str(), 141 + "OTHER_WEIGHT" | "DUMBBELL_WEIGHT" | "BARBELL_WEIGHT" | "WEIGHTED_BODYWEIGHT" 142 + ) 143 + }) 144 + .and_then(|cell| cell.value.as_ref()) 145 + .and_then(|s| s.parse::<f32>().ok()); 136 146 137 - // Create an Exercise only if there is at least one valid set. 138 - if !sets.is_empty() { 139 - exercises.push(Exercise { 140 - id: cell_set_group.id.clone(), 141 - name, 142 - sets, 143 - }); 144 - } 145 - } 147 + let reps = cell_set 148 + .cells 149 + .iter() 150 + .find(|cell| cell.cell_type == "REPS") 151 + .and_then(|cell| cell.value.as_ref()) 152 + .and_then(|s| s.parse::<u32>().ok()) 153 + .unwrap_or(0); 146 154 147 - workouts.push(Workout { 148 - id: workout_id, 149 - exercises, 150 - name: workout_name, 151 - timezone, 152 - start_date, 153 - end_date, 154 - }); 155 - } 155 + let rpe = cell_set 156 + .cells 157 + .iter() 158 + .find(|cell| cell.cell_type == "RPE") 159 + .and_then(|cell| cell.value.as_ref()) 160 + .and_then(|s| s.parse::<f32>().ok()); 156 161 157 - Ok(workouts) 162 + Some(Set { 163 + id: cell_set.id.clone(), 164 + weight, 165 + reps, 166 + rpe, 167 + }) 158 168 } 159 169 160 170 fn get_workout_id_from_link(links: &CellSetGroupLinks) -> String {
+2 -2
strong-api-lib/src/json_response.rs
··· 118 118 pub embedded: CellSetGroupEmbedded, 119 119 pub id: String, 120 120 #[serde(rename = "cellSets")] 121 - pub cell_sets: Vec<CallSet>, 121 + pub cell_sets: Vec<CellSet>, 122 122 } 123 123 124 124 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] ··· 130 130 pub struct CellSetGroupEmbedded {} 131 131 132 132 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 133 - pub struct CallSet { 133 + pub struct CellSet { 134 134 pub id: String, 135 135 pub cells: Vec<Cell>, 136 136 #[serde(rename = "isCompleted")]
-5
strong-api-lib/src/strong_api.rs
··· 152 152 .send() 153 153 .await?; 154 154 155 - // Log the status (consider replacing with proper logging) 156 - eprintln!("Refresh status: {}", response.status()); 157 155 let response_text = response.text().await?; 158 156 let parsed: LoginResponse = serde_json::from_str(&response_text)?; 159 157 ··· 215 213 query_pairs.append_pair("include", &include.to_string()); 216 214 } 217 215 } 218 - // Drop the mutable borrow here. 219 - eprintln!("Request URL: {}", url); 220 216 221 217 let response = self 222 218 .client ··· 275 271 let mut query_pairs = url.query_pairs_mut(); 276 272 query_pairs.append_pair("page", &page.to_string()); 277 273 } 278 - eprintln!("Request URL: {}", url); 279 274 280 275 let response = self.client.get(url).headers(headers.clone()).send().await?; 281 276 let response_text = response.text().await?;