hevy 0.1.0

An async Rust client library for the Hevy public API (https://api.hevyapp.com/docs/)
Documentation
mod common;

use common::{client_for, mock_server};
use hevy::{CreateExerciseTemplateRequest, EquipmentCategory, MuscleGroup, NewCustomExercise, CustomExerciseType};
use serde_json::json;
use wiremock::matchers::{method, path, query_param};
use wiremock::{Mock, ResponseTemplate};

fn sample_template_json() -> serde_json::Value {
    json!({
        "id": "b459cba5-cd6d-463c-abd6-54f8eafcadcb",
        "title": "Bench Press (Barbell)",
        "type": "weight_reps",
        "primary_muscle_group": "chest",
        "secondary_muscle_groups": ["shoulders", "triceps"],
        "equipment_category": "barbell",
        "is_custom": false,
    })
}

#[tokio::test]
async fn list_exercise_templates() {
    let server = mock_server().await;
    let client = client_for(&server);

    Mock::given(method("GET"))
        .and(path("/v1/exercise_templates"))
        .and(query_param("page", "1"))
        .and(query_param("pageSize", "20"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "page": 1,
            "page_count": 3,
            "exercise_templates": [sample_template_json()],
        })))
        .mount(&server)
        .await;

    let page = client
        .list_exercise_templates(Some(1), Some(20))
        .await
        .unwrap();

    assert_eq!(page.page_count, 3);
    assert_eq!(page.exercise_templates[0].title, "Bench Press (Barbell)");
    assert_eq!(
        page.exercise_templates[0].equipment_category,
        Some(EquipmentCategory::Barbell)
    );
}

#[tokio::test]
async fn get_exercise_template_by_id() {
    let server = mock_server().await;
    let client = client_for(&server);

    Mock::given(method("GET"))
        .and(path("/v1/exercise_templates/05293BCA"))
        .respond_with(ResponseTemplate::new(200).set_body_json(sample_template_json()))
        .mount(&server)
        .await;

    let template = client.get_exercise_template("05293BCA").await.unwrap();
    assert_eq!(template.primary_muscle_group, "chest");
    assert!(!template.is_custom);
}

#[tokio::test]
async fn create_exercise_template_returns_new_id() {
    let server = mock_server().await;
    let client = client_for(&server);

    Mock::given(method("POST"))
        .and(path("/v1/exercise_templates"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "id": 123 })))
        .mount(&server)
        .await;

    let exercise = NewCustomExercise::new(
        "Bench Press",
        CustomExerciseType::WeightReps,
        EquipmentCategory::Barbell,
        MuscleGroup::Chest,
    )
    .with_other_muscles([MuscleGroup::Biceps, MuscleGroup::Triceps]);

    let id = client
        .create_exercise_template(&CreateExerciseTemplateRequest::new(exercise))
        .await
        .unwrap();

    assert_eq!(id, 123);
}

#[tokio::test]
async fn create_exercise_template_over_limit_returns_error() {
    let server = mock_server().await;
    let client = client_for(&server);

    Mock::given(method("POST"))
        .and(path("/v1/exercise_templates"))
        .respond_with(ResponseTemplate::new(403).set_body_json(json!({
            "error": "exceeds-custom-exercise-limit"
        })))
        .mount(&server)
        .await;

    let exercise = NewCustomExercise::new(
        "Bench Press",
        CustomExerciseType::WeightReps,
        EquipmentCategory::Barbell,
        MuscleGroup::Chest,
    );

    let err = client
        .create_exercise_template(&CreateExerciseTemplateRequest::new(exercise))
        .await
        .unwrap_err();

    match err {
        hevy::HevyError::Api { status, message } => {
            assert_eq!(status, 403);
            assert_eq!(message, "exceeds-custom-exercise-limit");
        }
        other => panic!("expected HevyError::Api, got {other:?}"),
    }
}