hevy 0.1.0

An async Rust client library for the Hevy public API (https://api.hevyapp.com/docs/)
Documentation
use serde::{Deserialize, Serialize};

use super::common::SetType;

/// A single logged set, as returned by the API within a [`Workout`] or [`crate::Routine`].
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Set {
    /// Index indicating the order of the set within the exercise.
    #[serde(default)]
    pub index: u32,
    /// The type of set: one of `normal`, `warmup`, `dropset`, `failure`.
    #[serde(rename = "type", default)]
    pub r#type: String,
    /// Weight lifted, in kilograms.
    #[serde(default)]
    pub weight_kg: Option<f64>,
    /// Number of reps logged for the set.
    #[serde(default)]
    pub reps: Option<f64>,
    /// Number of meters logged for the set.
    #[serde(default)]
    pub distance_meters: Option<f64>,
    /// Number of seconds logged for the set.
    #[serde(default)]
    pub duration_seconds: Option<f64>,
    /// Rating of Perceived Exertion logged for the set.
    #[serde(default)]
    pub rpe: Option<f64>,
    /// Custom metric logged for the set (used for e.g. floors/steps on stair machines).
    #[serde(default)]
    pub custom_metric: Option<f64>,
}

/// An exercise entry within a [`Workout`], including all logged sets.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Exercise {
    /// Index indicating the order of the exercise within the workout.
    #[serde(default)]
    pub index: u32,
    /// Title of the exercise.
    #[serde(default)]
    pub title: String,
    /// Notes on the exercise.
    #[serde(default)]
    pub notes: Option<String>,
    /// The id of the exercise template. Can be used to fetch the full template.
    #[serde(default)]
    pub exercise_template_id: String,
    /// The id of the superset this exercise belongs to, if any.
    #[serde(default)]
    pub supersets_id: Option<u32>,
    /// The sets logged for this exercise.
    #[serde(default)]
    pub sets: Vec<Set>,
}

/// A completed workout.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Workout {
    /// The workout ID.
    pub id: String,
    /// The workout title.
    #[serde(default)]
    pub title: String,
    /// The ID of the routine this workout was created from, if any.
    #[serde(default)]
    pub routine_id: Option<String>,
    /// The workout description.
    #[serde(default)]
    pub description: Option<String>,
    /// ISO 8601 timestamp of when the workout was recorded to have started.
    #[serde(default)]
    pub start_time: String,
    /// ISO 8601 timestamp of when the workout was recorded to have ended.
    #[serde(default)]
    pub end_time: String,
    /// ISO 8601 timestamp of when the workout was last updated.
    #[serde(default)]
    pub updated_at: String,
    /// ISO 8601 timestamp of when the workout was created.
    #[serde(default)]
    pub created_at: String,
    /// The exercises logged in this workout.
    #[serde(default)]
    pub exercises: Vec<Exercise>,
}

/// A page of [`Workout`]s, as returned by [`crate::HevyClient::list_workouts`].
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct WorkoutsPage {
    /// The current page number.
    pub page: u32,
    /// The total number of pages available.
    pub page_count: u32,
    /// The workouts on this page.
    #[serde(default)]
    pub workouts: Vec<Workout>,
}

/// The total number of workouts on the account.
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
pub struct WorkoutCount {
    /// The total number of workouts.
    pub workout_count: u64,
}

/// A set to include when creating or updating a workout.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct WorkoutSetInput {
    /// The type of the set.
    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
    pub r#type: Option<SetType>,
    /// The weight in kilograms.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub weight_kg: Option<f64>,
    /// The number of repetitions.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reps: Option<i64>,
    /// The distance in meters.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub distance_meters: Option<i64>,
    /// The duration in seconds.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub duration_seconds: Option<i64>,
    /// A custom metric for the set. Currently used for steps and floors.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub custom_metric: Option<f64>,
    /// The Rating of Perceived Exertion (one of 6, 7, 7.5, 8, 8.5, 9, 9.5, 10).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rpe: Option<f64>,
}

impl WorkoutSetInput {
    /// Creates a normal set with the given weight and reps.
    pub fn normal(weight_kg: f64, reps: i64) -> Self {
        Self {
            r#type: Some(SetType::Normal),
            weight_kg: Some(weight_kg),
            reps: Some(reps),
            ..Default::default()
        }
    }

    /// Creates a warmup set with the given weight and reps.
    pub fn warmup(weight_kg: f64, reps: i64) -> Self {
        Self {
            r#type: Some(SetType::Warmup),
            weight_kg: Some(weight_kg),
            reps: Some(reps),
            ..Default::default()
        }
    }
}

/// An exercise to include when creating or updating a workout.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct WorkoutExerciseInput {
    /// The ID of the exercise template.
    pub exercise_template_id: String,
    /// The ID of the superset this exercise belongs to, if any.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub superset_id: Option<i64>,
    /// Additional notes for the exercise.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub notes: Option<String>,
    /// The sets performed for this exercise.
    pub sets: Vec<WorkoutSetInput>,
}

impl WorkoutExerciseInput {
    /// Creates a new exercise input for the given exercise template id.
    pub fn new(exercise_template_id: impl Into<String>) -> Self {
        Self {
            exercise_template_id: exercise_template_id.into(),
            superset_id: None,
            notes: None,
            sets: Vec::new(),
        }
    }

    /// Appends a set to this exercise.
    pub fn with_set(mut self, set: WorkoutSetInput) -> Self {
        self.sets.push(set);
        self
    }
}

/// The workout fields accepted by the create/update workout endpoints.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct NewWorkout {
    /// The title of the workout.
    pub title: String,
    /// A description for the workout.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    /// ISO 8601 timestamp of when the workout started.
    pub start_time: String,
    /// ISO 8601 timestamp of when the workout ended.
    pub end_time: String,
    /// Whether the workout is private.
    #[serde(default)]
    pub is_private: bool,
    /// The exercises performed in this workout.
    pub exercises: Vec<WorkoutExerciseInput>,
}

impl NewWorkout {
    /// Creates a new workout with the given title and start/end times.
    ///
    /// Timestamps should be ISO 8601 strings, e.g. `"2024-08-14T12:00:00Z"`.
    pub fn new(
        title: impl Into<String>,
        start_time: impl Into<String>,
        end_time: impl Into<String>,
    ) -> Self {
        Self {
            title: title.into(),
            description: None,
            start_time: start_time.into(),
            end_time: end_time.into(),
            is_private: false,
            exercises: Vec::new(),
        }
    }

    /// Appends an exercise to this workout.
    pub fn with_exercise(mut self, exercise: WorkoutExerciseInput) -> Self {
        self.exercises.push(exercise);
        self
    }
}

/// The request body used to create or update a workout.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct CreateWorkoutRequest {
    /// The workout to create or the new state of the workout to update.
    pub workout: NewWorkout,
}

impl CreateWorkoutRequest {
    /// Wraps a [`NewWorkout`] in the request body envelope expected by the API.
    pub fn new(workout: NewWorkout) -> Self {
        Self { workout }
    }
}

/// A workout event indicating either an update or a deletion, as returned by
/// [`crate::HevyClient::workout_events`].
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum WorkoutEvent {
    /// A workout was created or updated.
    Updated {
        /// The full, up-to-date workout.
        workout: Workout,
    },
    /// A workout was deleted.
    Deleted {
        /// The id of the deleted workout.
        id: String,
        /// When the workout was deleted, if known.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        deleted_at: Option<String>,
    },
}

/// A paginated list of [`WorkoutEvent`]s, as returned by [`crate::HevyClient::workout_events`].
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct PaginatedWorkoutEvents {
    /// The current page number.
    pub page: u32,
    /// The total number of pages available.
    pub page_count: u32,
    /// The events on this page, ordered from newest to oldest.
    #[serde(default)]
    pub events: Vec<WorkoutEvent>,
}