oura_api/
lib.rs

1//! # Oura API Client
2//!
3//! This crate provides a client for the [Oura V2 REST API](https://cloud.ouraring.com/v2/docs).
4//!
5//! Note that this client does not support the Oura V1 REST API.
6//!
7//! ## Usage
8//! ```no_run
9//! use oura_api::{OuraClient, DateQuery};
10//!
11//! // token is the personal access token for the Oura API
12//! let token = std::env::var("OURA_PERSONAL_ACCESS_TOKEN").unwrap();
13//! let client = OuraClient::new(&token);
14//!
15//! let august_date_query = DateQuery::builder().start_date("2023-08-01").end_date("2023-08-31").build();
16//! let august_daily_sleep = client.list_daily_sleep(august_date_query).unwrap();
17//! ```
18
19pub mod models;
20
21use paste::paste;
22use reqwest::blocking::Client;
23use serde::{Deserialize, Serialize};
24use typed_builder::TypedBuilder;
25
26use crate::models::*;
27
28const API_BASE_URL: &str = "https://api.ouraring.com/v2/usercollection";
29
30/// Query parameters for endpoints that accept a date range.
31#[derive(Serialize, TypedBuilder)]
32pub struct DateQuery<'a> {
33    #[builder(default = None, setter(strip_option))]
34    start_date: Option<&'a str>,
35    #[builder(default = None, setter(strip_option))]
36    end_date: Option<&'a str>,
37    #[builder(default = None, setter(strip_option))]
38    next_token: Option<&'a str>,
39}
40
41/// Query parameters for endpoints that accept a datetime range.
42#[derive(Serialize, TypedBuilder)]
43pub struct DatetimeQuery<'a> {
44    #[builder(default = None, setter(strip_option))]
45    start_datetime: Option<&'a str>,
46    #[builder(default = None, setter(strip_option))]
47    end_datetime: Option<&'a str>,
48    #[builder(default = None, setter(strip_option))]
49    next_token: Option<&'a str>,
50}
51
52/// Response from endpoints that return a list of items.
53#[derive(Deserialize, Debug, PartialEq)]
54pub struct ListResponse<T> {
55    /// The list of items returned by the endpoint.
56    pub data: Vec<T>,
57    /// The optional token to use to retrieve the next page of results.
58    pub next_token: Option<String>,
59}
60
61macro_rules! generic_endpoint {
62    ($(#[$m:meta])*, $name: ident, $type: ty, $path: literal) => {
63        $(#[$m])*
64        pub fn $name(&self) -> Result<$type, reqwest::Error> {
65            let url = format!("{}/{}", &self.base_url, $path);
66            let response = self
67                .client
68                .get(&url)
69                .bearer_auth(&self.token)
70                .send()?
71                .error_for_status()?
72                .json::<$type>()?;
73            Ok(response)
74        }
75    };
76}
77
78macro_rules! list_endpoint {
79    ($(#[$m:meta])*, $name: ident, $type: ty, $path: literal, $query: ty) => {
80        $(#[$m])*
81        pub fn $name(&self, query: $query) -> Result<ListResponse<$type>, reqwest::Error> {
82            let url = format!("{}/{}", &self.base_url, $path);
83            let response = self
84                .client
85                .get(&url)
86                .bearer_auth(&self.token)
87                .query(&query)
88                .send()?
89                .error_for_status()?
90                .json::<ListResponse<$type>>()?;
91            Ok(response)
92        }
93    };
94}
95
96macro_rules! get_endpoint {
97    ($(#[$m:meta])*, $name: ident, $type: ty, $path: literal) => {
98        $(#[$m])*
99        pub fn $name(&self, id: &str) -> Result<$type, reqwest::Error> {
100            let url = format!("{}/{}/{}", &self.base_url, $path, id);
101            let response = self
102                .client
103                .get(&url)
104                .bearer_auth(&self.token)
105                .send()?
106                .error_for_status()?
107                .json::<$type>()?;
108            Ok(response)
109        }
110    };
111}
112
113macro_rules! endpoint_set {
114    ($name: ident, $type: ty, $path: literal, $params: ty) => {
115        paste! {
116            get_endpoint!(#[doc = "Gets a single [" $type "] item by id."], [<get_ $name>], $type, $path);
117            list_endpoint!(#[doc = "Returns a [ListResponse] of [" $type "] items based on the supplied query."], [<list_ $name>], $type, $path, $params);
118        }
119    };
120}
121
122/// The Oura API client.
123///
124/// This client is used to make requests to the Oura API.
125pub struct OuraClient<'a> {
126    token: &'a str,
127    base_url: &'a str,
128    client: Client,
129}
130
131impl<'a> OuraClient<'a> {
132    /// Creates a new OuraClient from a personal access token.
133    pub fn new(token: &'a str) -> Self {
134        let client = Client::new();
135        Self {
136            token,
137            client,
138            base_url: API_BASE_URL,
139        }
140    }
141
142    /// Creates a new OuraClient from a personal access token and a base URL.
143    ///
144    /// *Note:* This is only useful for testing against a mock server.
145    pub fn build_with_base_url(token: &'a str, base_url: &'a str) -> Self {
146        let client = Client::new();
147        Self {
148            token,
149            client,
150            base_url,
151        }
152    }
153
154    endpoint_set!(daily_activity, DailyActivity, "daily_activity", DateQuery);
155
156    endpoint_set!(
157        daily_readiness,
158        DailyReadiness,
159        "daily_readiness",
160        DateQuery
161    );
162
163    endpoint_set!(daily_sleep, DailySleep, "daily_sleep", DateQuery);
164
165    endpoint_set!(daily_spo2, DailySpO2, "daily_spo2", DateQuery);
166
167    list_endpoint!(#[doc = "Returns a [ListResponse] of [HeartRate] items based on the supplied query."], list_heart_rate, HeartRate, "heartrate", DatetimeQuery);
168
169    generic_endpoint!(#[doc = "Returns a [PersonalInfo] based on the caller."], get_personal_info, PersonalInfo, "personal_info");
170
171    endpoint_set!(
172        rest_mode_period,
173        RestModePeriod,
174        "rest_mode_period",
175        DateQuery
176    );
177
178    endpoint_set!(
179        ring_configuration,
180        RingConfiguration,
181        "ring_configuration",
182        DateQuery
183    );
184
185    endpoint_set!(session, Session, "session", DateQuery);
186
187    endpoint_set!(sleep, Sleep, "sleep", DateQuery);
188
189    endpoint_set!(sleep_time, SleepTime, "sleep_time", DateQuery);
190
191    endpoint_set!(tag, Tag, "tag", DateQuery);
192
193    endpoint_set!(workout, Workout, "workout", DateQuery);
194
195    endpoint_set!(tag_v2, TagV2, "tag/v2", DateQuery);
196}