use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ActivitySummary {
pub activity_id: u64,
#[serde(default)]
pub activity_name: Option<String>,
#[serde(default)]
pub start_time_local: Option<String>,
#[serde(default)]
pub start_time_gmt: Option<String>,
#[serde(default)]
pub activity_type: Option<ActivityType>,
#[serde(default)]
pub distance: Option<f64>,
#[serde(default)]
pub duration: Option<f64>,
#[serde(default)]
pub elapsed_duration: Option<f64>,
#[serde(default)]
pub moving_duration: Option<f64>,
#[serde(default)]
pub calories: Option<f64>,
#[serde(default)]
pub average_hr: Option<f64>,
#[serde(default)]
pub max_hr: Option<f64>,
#[serde(default)]
pub average_speed: Option<f64>,
#[serde(default)]
pub max_speed: Option<f64>,
#[serde(default)]
pub elevation_gain: Option<f64>,
#[serde(default)]
pub elevation_loss: Option<f64>,
#[serde(default)]
pub average_running_cadence_in_steps_per_minute: Option<f64>,
#[serde(default)]
pub steps: Option<u64>,
#[serde(default)]
pub has_polyline: Option<bool>,
#[serde(default)]
pub owner_display_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ActivityType {
pub type_key: String,
#[serde(default)]
pub type_id: Option<u64>,
#[serde(default)]
pub parent_type_id: Option<u64>,
#[serde(default)]
pub is_hidden: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ActivityDetails {
pub activity_id: u64,
#[serde(default)]
pub activity_name: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub start_time_local: Option<String>,
#[serde(default)]
pub start_time_gmt: Option<String>,
#[serde(default)]
pub activity_type: Option<ActivityType>,
#[serde(default)]
pub summary_dto: Option<serde_json::Value>,
#[serde(default)]
pub location_name: Option<String>,
#[serde(default)]
pub time_zone_unit_dto: Option<serde_json::Value>,
#[serde(default)]
pub metadata_dto: Option<serde_json::Value>,
#[serde(flatten)]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UploadResult {
pub detailed_import_result: DetailedImportResult,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DetailedImportResult {
pub upload_id: u64,
#[serde(default)]
pub upload_uuid: Option<UploadUuid>,
#[serde(default)]
pub owner: Option<u64>,
#[serde(default)]
pub file_size: Option<u64>,
#[serde(default)]
pub processing_time: Option<u64>,
#[serde(default)]
pub creation_date: Option<String>,
#[serde(default)]
pub file_name: Option<String>,
#[serde(default)]
pub successes: Vec<UploadSuccess>,
#[serde(default)]
pub failures: Vec<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UploadUuid {
pub uuid: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UploadSuccess {
#[serde(default)]
pub internal_id: Option<u64>,
#[serde(default)]
pub external_id: Option<String>,
#[serde(default)]
pub messages: Vec<serde_json::Value>,
}
impl ActivitySummary {
pub fn display_name(&self) -> String {
self.activity_name
.clone()
.unwrap_or_else(|| "Unnamed Activity".to_string())
}
pub fn type_key(&self) -> String {
self.activity_type
.as_ref()
.map(|t| t.type_key.clone())
.unwrap_or_else(|| "unknown".to_string())
}
pub fn distance_km(&self) -> Option<f64> {
self.distance.map(|d| d / 1000.0)
}
pub fn duration_formatted(&self) -> String {
match self.duration {
Some(secs) => {
let total_secs = secs as u64;
let hours = total_secs / 3600;
let minutes = (total_secs % 3600) / 60;
let seconds = total_secs % 60;
if hours > 0 {
format!("{}:{:02}:{:02}", hours, minutes, seconds)
} else {
format!("{}:{:02}", minutes, seconds)
}
}
None => "-".to_string(),
}
}
pub fn date(&self) -> String {
self.start_time_local
.as_ref()
.map(|s| {
s.split(['T', ' ']).next().unwrap_or(s).to_string()
})
.unwrap_or_else(|| "-".to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_activity_summary_display_name() {
let activity = ActivitySummary {
activity_id: 123,
activity_name: Some("Morning Run".to_string()),
start_time_local: None,
start_time_gmt: None,
activity_type: None,
distance: None,
duration: None,
elapsed_duration: None,
moving_duration: None,
calories: None,
average_hr: None,
max_hr: None,
average_speed: None,
max_speed: None,
elevation_gain: None,
elevation_loss: None,
average_running_cadence_in_steps_per_minute: None,
steps: None,
has_polyline: None,
owner_display_name: None,
};
assert_eq!(activity.display_name(), "Morning Run");
}
#[test]
fn test_activity_summary_duration_formatted() {
let mut activity = ActivitySummary {
activity_id: 123,
activity_name: None,
start_time_local: None,
start_time_gmt: None,
activity_type: None,
distance: None,
duration: Some(3661.0), elapsed_duration: None,
moving_duration: None,
calories: None,
average_hr: None,
max_hr: None,
average_speed: None,
max_speed: None,
elevation_gain: None,
elevation_loss: None,
average_running_cadence_in_steps_per_minute: None,
steps: None,
has_polyline: None,
owner_display_name: None,
};
assert_eq!(activity.duration_formatted(), "1:01:01");
activity.duration = Some(125.0); assert_eq!(activity.duration_formatted(), "2:05");
}
#[test]
fn test_activity_summary_distance_km() {
let activity = ActivitySummary {
activity_id: 123,
activity_name: None,
start_time_local: None,
start_time_gmt: None,
activity_type: None,
distance: Some(10500.0), duration: None,
elapsed_duration: None,
moving_duration: None,
calories: None,
average_hr: None,
max_hr: None,
average_speed: None,
max_speed: None,
elevation_gain: None,
elevation_loss: None,
average_running_cadence_in_steps_per_minute: None,
steps: None,
has_polyline: None,
owner_display_name: None,
};
assert_eq!(activity.distance_km(), Some(10.5));
}
}