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::{RepRange, SetType, flexible_f64};

/// A set belonging to an exercise within a [`Routine`], as returned by the API.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct RoutineSet {
    /// 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>,
    /// A target rep range for the set, if applicable.
    #[serde(default)]
    pub rep_range: Option<RepRange>,
    /// 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.
    #[serde(default)]
    pub custom_metric: Option<f64>,
}

/// An exercise entry within a [`Routine`].
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct RoutineExercise {
    /// Index indicating the order of the exercise within the routine.
    #[serde(default)]
    pub index: u32,
    /// Title of the exercise.
    #[serde(default)]
    pub title: String,
    /// The rest time in seconds between sets of the exercise.
    ///
    /// The API documents this as a string but has been observed to also return
    /// a number; both are accepted.
    #[serde(default, deserialize_with = "flexible_f64")]
    pub rest_seconds: Option<f64>,
    /// Routine 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 configured for this exercise.
    #[serde(default)]
    pub sets: Vec<RoutineSet>,
}

/// A saved workout routine/template.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Routine {
    /// The routine ID.
    pub id: String,
    /// The routine title.
    #[serde(default)]
    pub title: String,
    /// The routine folder ID this routine belongs to, if any.
    #[serde(default)]
    pub folder_id: Option<i64>,
    /// ISO 8601 timestamp of when the routine was last updated.
    #[serde(default)]
    pub updated_at: String,
    /// ISO 8601 timestamp of when the routine was created.
    #[serde(default)]
    pub created_at: String,
    /// The exercises configured in this routine.
    #[serde(default)]
    pub exercises: Vec<RoutineExercise>,
}

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

/// Wrapper for the single-routine response returned by `GET /v1/routines/{routineId}`.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub(crate) struct RoutineEnvelope {
    pub routine: Routine,
}

/// A set to include when creating or updating a routine's exercise.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct RoutineSetInput {
    /// 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>,
    /// A target rep range for the set, if applicable.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rep_range: Option<RepRange>,
}

impl RoutineSetInput {
    /// 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()
        }
    }
}

/// An exercise to include when creating or updating a routine.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct RoutineExerciseInput {
    /// 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>,
    /// The rest time in seconds between sets.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rest_seconds: Option<i64>,
    /// Additional notes for the exercise.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub notes: Option<String>,
    /// The sets configured for this exercise.
    pub sets: Vec<RoutineSetInput>,
}

impl RoutineExerciseInput {
    /// 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,
            rest_seconds: None,
            notes: None,
            sets: Vec::new(),
        }
    }

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

/// The routine fields accepted when creating a new routine.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct NewRoutine {
    /// The title of the routine.
    pub title: String,
    /// The folder id the routine should be added to.
    ///
    /// Pass `None` to insert the routine into the default "My Routines" folder.
    #[serde(default)]
    pub folder_id: Option<i64>,
    /// Additional notes for the routine.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub notes: Option<String>,
    /// The exercises configured in this routine.
    pub exercises: Vec<RoutineExerciseInput>,
}

impl NewRoutine {
    /// Creates a new routine with the given title.
    pub fn new(title: impl Into<String>) -> Self {
        Self {
            title: title.into(),
            folder_id: None,
            notes: None,
            exercises: Vec::new(),
        }
    }

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

/// The request body used to create a new routine.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct CreateRoutineRequest {
    /// The routine to create.
    pub routine: NewRoutine,
}

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

/// The routine fields accepted when updating an existing routine.
///
/// Note that, unlike [`NewRoutine`], the folder cannot be changed via this endpoint.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct UpdatedRoutine {
    /// The title of the routine.
    pub title: String,
    /// Additional notes for the routine.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub notes: Option<String>,
    /// The exercises configured in this routine.
    pub exercises: Vec<RoutineExerciseInput>,
}

impl UpdatedRoutine {
    /// Creates a new routine update payload with the given title.
    pub fn new(title: impl Into<String>) -> Self {
        Self {
            title: title.into(),
            notes: None,
            exercises: Vec::new(),
        }
    }

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

/// The request body used to update an existing routine.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct UpdateRoutineRequest {
    /// The new state of the routine.
    pub routine: UpdatedRoutine,
}

impl UpdateRoutineRequest {
    /// Wraps an [`UpdatedRoutine`] in the request body envelope expected by the API.
    pub fn new(routine: UpdatedRoutine) -> Self {
        Self { routine }
    }
}