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.

started implementing getting names from measurements

+167 -19
+29 -4
src/data_transformer.rs
··· 1 - use crate::user_response::Log; 1 + use std::collections::HashMap; 2 + use crate::json_response::{Log, Measurement, MeasurementsResponse, Name}; 2 3 3 4 #[allow(dead_code, unused)] 4 5 #[derive(Debug)] ··· 15 16 pub struct Exercise { 16 17 pub(crate) id: String, 17 18 pub(crate) sets: Vec<Set>, 19 + pub(crate) name: Name, 18 20 } 19 21 20 22 #[allow(dead_code, unused)] ··· 30 32 31 33 /// A trait for transforming raw API log data into domain-specific Workouts. 32 34 pub trait DataTransformer { 33 - fn transform(&self, logs: &Option<Vec<Log>>) -> Result<Vec<Workout>, serde_json::Error>; 35 + fn get_measurements_from_logs(&self, logs: &Option<Vec<Log>>, measurements_response: &Option<MeasurementsResponse>) -> Result<Vec<Workout>, serde_json::Error>; 34 36 } 35 37 36 38 pub(crate) struct DataTransformerImpl; 37 39 38 40 impl DataTransformer for DataTransformerImpl { 39 - fn transform(&self, logs_option: &Option<Vec<Log>>) -> Result<Vec<Workout>, serde_json::Error> { 40 - let mut logs = match logs_option { 41 + fn get_measurements_from_logs( 42 + &self, 43 + logs_option: &Option<Vec<Log>>, 44 + measurements_response: &Option<MeasurementsResponse> 45 + ) -> Result<Vec<Workout>, serde_json::Error> { 46 + let logs = match logs_option { 41 47 Some(logs) => logs, 42 48 None => return Ok(Vec::new()), 43 49 }; 44 50 51 + //if measurements set, create lookup table 52 + let mut lookup: HashMap<String, Measurement> = HashMap::new(); 53 + if let Some(measurements) = measurements_response { 54 + println!("Measurements count: {}", measurements.embedded.measurements.len()); 55 + for measurement in &measurements.embedded.measurements { 56 + lookup.insert(measurement.id.clone(), measurement.clone()); 57 + } 58 + } 59 + 45 60 let mut workouts = Vec::new(); 46 61 47 62 // Process every log. ··· 105 120 } 106 121 } 107 122 123 + let mut name = Name::default(); 124 + if let Some(exercise) = lookup.get(&cell_set_group.links) { 125 + // get id from links 126 + dbg!(&exercise); 127 + name = exercise.clone().name; 128 + } else { 129 + println!("Exercise not found: {}", cell_set_group.id); 130 + } 131 + 108 132 // Create an Exercise only if there is at least one valid set. 109 133 if !sets.is_empty() { 110 134 exercises.push(Exercise { 111 135 id: cell_set_group.id.clone(), 136 + name, 112 137 sets, 113 138 }); 114 139 }
+31 -7
src/main.rs
··· 1 + use std::collections::HashMap; 1 2 use crate::data_transformer::{DataTransformer, DataTransformerImpl}; 2 3 use crate::strong_api::{Includes, StrongApi}; 3 4 use dotenv::dotenv; 4 5 use reqwest::Url; 5 6 use std::env; 7 + use crate::json_response::{Measurement, MeasurementsResponse, UserResponse}; 6 8 7 9 mod data_transformer; 8 10 mod strong_api; 9 - mod user_response; 11 + mod json_response; 10 12 11 13 #[tokio::main] 12 14 async fn main() -> Result<(), Box<dyn std::error::Error>> { ··· 17 19 18 20 let url = Url::parse(&strong_backend).ok().expect("STRONG_BACKEND is not a valid URL"); 19 21 20 - let mut strong_api = StrongApi::new(url); 22 + let strong_api = StrongApi::new(url); 21 23 24 + /* 22 25 strong_api 23 26 .login(username.as_str(), password.as_str()) 24 27 .await?; 25 - let user = strong_api.get_user("", 500, vec![Includes::Log]).await?; 28 + */ 29 + 30 + let measurements_response; 31 + // check if measurements.json file exist, if not, fetch the data from the API 32 + if !std::path::Path::new("measurements.json").exists() { 33 + println!("{}","Fetching measurements from API"); 34 + measurements_response = strong_api.get_measurements().await?; 35 + let measurements_json = serde_json::to_string(&measurements_response)?; 36 + std::fs::write("measurements.json", measurements_json)?; 37 + } else { 38 + println!("{}","Reading measurements from file"); 39 + let measurements_json = std::fs::read_to_string("measurements.json")?; 40 + measurements_response = serde_json::from_str(&measurements_json)?; 41 + } 42 + 43 + //let user = strong_api.get_user("", 500, vec![Includes::Log]).await?; 44 + 45 + let response_text = std::fs::read_to_string("response.json")?; 46 + let user: UserResponse = serde_json::from_str(&response_text)?; 26 47 27 48 let workouts = DataTransformerImpl 28 - .transform(&user.embedded.log) 49 + .get_measurements_from_logs( 50 + &user.embedded.log, 51 + &Some(measurements_response) 52 + ) 29 53 .expect("Couldn't read workouts"); 30 54 31 - println!("Workout count: {}", workouts.len()); 55 + /*println!("Workout count: {}", workouts.len()); 32 56 33 57 workouts.iter().for_each(|workout| { 34 58 println!("Workout: {}", workout.name); 35 59 workout.exercises.iter().for_each(|exercise| { 36 - println!("Exercise: {}", exercise.id); 60 + println!("Name: {} | Id: {} ", exercise.name, exercise.id); 37 61 exercise.sets.iter().for_each(|set| { 38 62 println!("Set: {:?}", set); 39 63 }); 40 64 }); 41 - }); 65 + });*/ 42 66 43 67 Ok(()) 44 68 }
+31 -8
src/strong_api.rs
··· 1 - use crate::user_response::UserResponse; 1 + use crate::json_response::UserResponse; 2 + use crate::json_response::MeasurementsResponse; 2 3 use reqwest::{ 3 4 Client, Url, 4 5 header::{HeaderMap, HeaderName, HeaderValue}, ··· 234 235 Ok(parsed) 235 236 } 236 237 237 - pub async fn get_measurements(&self) -> Result<(), Box<dyn std::error::Error>> { 238 - let user_id = self.user_id.as_ref().ok_or("Missing user id")?; 239 - let url = self.url.join(&format!("api/measurements/{user_id}"))?; 238 + pub async fn get_measurements(&self) -> Result<MeasurementsResponse, Box<dyn std::error::Error>> { 239 + let mut headers = HeaderMap::new(); 240 + headers.insert( 241 + HeaderName::from_static("user-agent"), 242 + HeaderValue::from_static("Strong Android"), 243 + ); 244 + headers.insert( 245 + HeaderName::from_static("content-type"), 246 + HeaderValue::from_static("application/json"), 247 + ); 248 + headers.insert( 249 + HeaderName::from_static("accept"), 250 + HeaderValue::from_static("application/json"), 251 + ); 252 + headers.insert( 253 + HeaderName::from_static("x-client-build"), 254 + HeaderValue::from_static("600013"), 255 + ); 256 + headers.insert( 257 + HeaderName::from_static("x-client-platform"), 258 + HeaderValue::from_static("android"), 259 + ); 260 + 261 + let url = self.url.join(&"api/measurements?count=500".to_string())?; 240 262 let response = self 241 263 .client 242 264 .get(url) 243 - .bearer_auth(self.access_token.as_ref().ok_or("Missing access token")?) 244 - .headers(self.headers.clone()) 265 + .headers(headers.clone()) 245 266 .send() 246 267 .await?; 247 268 let response_text = response.text().await?; 248 - eprintln!("Measurements response: {}", response_text); 249 - Ok(()) 269 + 270 + let response: MeasurementsResponse = serde_json::from_str(&response_text)?; 271 + Ok(response) 250 272 } 251 273 252 274 pub async fn get_logs(&self) -> Result<(), Box<dyn std::error::Error>> { ··· 260 282 .send() 261 283 .await?; 262 284 let response_text = response.text().await?; 285 + 263 286 eprintln!("Logs response: {}", response_text); 264 287 Ok(()) 265 288 }
+76
src/user_response.rs src/json_response.rs
··· 120 120 pub cell_type: String, 121 121 pub value: Option<String>, 122 122 } 123 + 124 + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 125 + pub struct Instructions { 126 + pub en: String, 127 + } 128 + 129 + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 130 + pub struct Media { 131 + pub url: String, 132 + #[serde(rename = "type")] 133 + pub media_type: String, 134 + #[serde(rename = "contentType")] 135 + pub content_type: String, 136 + } 137 + 138 + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 139 + pub struct CellTypeConfig { 140 + #[serde(rename = "cellType")] 141 + pub cell_type: String, 142 + pub mandatory: Option<bool>, 143 + #[serde(rename = "isExponent")] 144 + pub is_exponent: Option<bool>, 145 + } 146 + 147 + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 148 + pub struct MeasurementLinks { 149 + #[serde(rename = "self")] 150 + pub self_link: Link, 151 + pub tag: Option<Vec<Link>>, 152 + } 153 + 154 + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 155 + pub struct Measurement { 156 + #[serde(rename = "_links")] 157 + pub links: MeasurementLinks, 158 + pub id: String, 159 + pub created: String, 160 + #[serde(rename = "lastChanged")] 161 + pub last_changed: String, 162 + pub name: Name, 163 + pub instructions: Option<Instructions>, 164 + pub media: Vec<Media>, 165 + #[serde(rename = "cellTypeConfigs")] 166 + pub cell_type_configs: Vec<CellTypeConfig>, 167 + #[serde(rename = "isGlobal")] 168 + pub is_global: bool, 169 + #[serde(rename = "measurementType")] 170 + pub measurement_type: String, 171 + } 172 + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 173 + pub struct MeasurementsResponse { 174 + #[serde(rename = "_links")] 175 + pub links: Links, 176 + pub total: u32, 177 + #[serde(rename = "_embedded")] 178 + pub embedded: EmbeddedMeasurements, 179 + } 180 + 181 + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 182 + pub struct Links { 183 + #[serde(rename = "self")] 184 + pub self_link: Link, 185 + pub next: Option<Link>, 186 + } 187 + 188 + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 189 + pub struct Link { 190 + pub href: String, 191 + } 192 + 193 + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 194 + pub struct EmbeddedMeasurements { 195 + #[serde(rename = "measurement")] 196 + pub measurements: Vec<Measurement>, 197 + } 198 +