Add rate limiting information to response types
Add the `rate_limit` field to all response types, which contains rate limiting information such as maximum and remaining requests in the current time frame, as well as when the next time frame starts. All response types now also implement the `ResponseType` trait (internal for now). Currently this trait allows converting from `ureq::Response` to the response type, by extracting rate limiting information from the response headers and deserializing the response body into the response type.
This commit is contained in:
parent
fd9c7823d7
commit
508f697a51
|
@ -1,4 +1,3 @@
|
|||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
|
||||
use ureq::Agent;
|
||||
|
@ -36,19 +35,17 @@ impl Client {
|
|||
|
||||
/// Helper method to perform a GET request against an endpoint
|
||||
/// without any query parameters.
|
||||
fn get<R: DeserializeOwned>(&self, endpoint: Endpoint) -> Result<R, Error> {
|
||||
fn get<R: ResponseType>(&self, endpoint: Endpoint) -> Result<R, Error> {
|
||||
let endpoint = format!("{}{}", API_ROOT_URL, endpoint);
|
||||
|
||||
self.agent
|
||||
.get(&endpoint)
|
||||
.call()?
|
||||
.into_json()
|
||||
.map_err(Error::ResponseJson)
|
||||
let response = self.agent.get(&endpoint).call()?;
|
||||
|
||||
R::from_response(response)
|
||||
}
|
||||
|
||||
/// Helper method to perform a GET request against (most) statistics
|
||||
/// endpoints that share common query parameters.
|
||||
fn get_stats<R: DeserializeOwned>(
|
||||
fn get_stats<R: ResponseType>(
|
||||
&self,
|
||||
endpoint: Endpoint,
|
||||
count: Option<u64>,
|
||||
|
@ -75,7 +72,7 @@ impl Client {
|
|||
if response.status() == 204 {
|
||||
Ok(None)
|
||||
} else {
|
||||
response.into_json().map(Some).map_err(Error::ResponseJson)
|
||||
R::from_response(response).map(Some)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,18 +81,18 @@ impl Client {
|
|||
fn post<D, R>(&self, endpoint: Endpoint, token: &str, data: D) -> Result<R, Error>
|
||||
where
|
||||
D: Serialize,
|
||||
R: DeserializeOwned,
|
||||
R: ResponseType,
|
||||
{
|
||||
let data = serde_json::to_value(data).map_err(Error::RequestJson)?;
|
||||
|
||||
let endpoint = format!("{}{}", API_ROOT_URL, endpoint);
|
||||
|
||||
self.agent
|
||||
let response = self.agent
|
||||
.post(&endpoint)
|
||||
.set("Authorization", &format!("Token {}", token))
|
||||
.send_json(data)?
|
||||
.into_json()
|
||||
.map_err(Error::ResponseJson)
|
||||
.send_json(data)?;
|
||||
|
||||
R::from_response(response)
|
||||
}
|
||||
|
||||
/// Endpoint: [`submit-listens`](https://listenbrainz.readthedocs.io/en/production/dev/api/#post--1-submit-listens)
|
||||
|
@ -111,12 +108,12 @@ impl Client {
|
|||
pub fn validate_token(&self, token: &str) -> Result<ValidateTokenResponse, Error> {
|
||||
let endpoint = format!("{}{}", API_ROOT_URL, Endpoint::ValidateToken);
|
||||
|
||||
self.agent
|
||||
let response = self.agent
|
||||
.get(&endpoint)
|
||||
.query("token", token)
|
||||
.call()?
|
||||
.into_json()
|
||||
.map_err(Error::ResponseJson)
|
||||
.call()?;
|
||||
|
||||
ResponseType::from_response(response)
|
||||
}
|
||||
|
||||
/// Endpoint: [`delete-listen`](https://listenbrainz.readthedocs.io/en/production/dev/api/#post--1-delete-listen)
|
||||
|
@ -172,7 +169,9 @@ impl Client {
|
|||
request = request.query("time_range", &time_range.to_string());
|
||||
}
|
||||
|
||||
request.call()?.into_json().map_err(Error::ResponseJson)
|
||||
let response = request.call()?;
|
||||
|
||||
ResponseType::from_response(response)
|
||||
}
|
||||
|
||||
/// Endpoint: [`latest-import`](https://listenbrainz.readthedocs.io/en/production/dev/api/#get--1-latest-import) (`GET`)
|
||||
|
@ -230,7 +229,7 @@ impl Client {
|
|||
if response.status() == 204 {
|
||||
Ok(None)
|
||||
} else {
|
||||
response.into_json().map(Some).map_err(Error::ResponseJson)
|
||||
ResponseType::from_response(response).map(Some)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -258,7 +257,7 @@ impl Client {
|
|||
if response.status() == 204 {
|
||||
Ok(None)
|
||||
} else {
|
||||
response.into_json().map(Some).map_err(Error::ResponseJson)
|
||||
ResponseType::from_response(response).map(Some)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -300,7 +299,9 @@ impl Client {
|
|||
request = request.query("force_recalculate", &force_recalculate.to_string());
|
||||
}
|
||||
|
||||
request.call()?.into_json().map_err(Error::ResponseJson)
|
||||
let response = request.call()?;
|
||||
|
||||
ResponseType::from_response(response)
|
||||
}
|
||||
|
||||
/// Endpoint: [`stats/user/{user_name}/releases`](https://listenbrainz.readthedocs.io/en/production/dev/api/#get--1-stats-user-(user_name)-releases)
|
||||
|
@ -338,6 +339,8 @@ impl Client {
|
|||
request = request.query("id", &id.to_string());
|
||||
}
|
||||
|
||||
request.call()?.into_json().map_err(Error::ResponseJson)
|
||||
let response = request.call()?;
|
||||
|
||||
ResponseType::from_response(response)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,43 +1,133 @@
|
|||
//! Low-level response data models.
|
||||
//!
|
||||
//! Every response type has the `rate_limit` field, which contains rate limiting
|
||||
//! information. See the documentation of the [`RateLimit`] type for more
|
||||
//! details.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Deserialize;
|
||||
use ureq::Response;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
/// Contains rate limiting information.
|
||||
///
|
||||
/// ListenBrainz API rate limiting is described in the [API docs].
|
||||
/// Prefer using the [`RateLimit::reset_in`] field over [`RateLimit::reset`],
|
||||
/// as the former is resilient against clients with incorrect clocks.
|
||||
///
|
||||
/// [API docs]: https://listenbrainz.readthedocs.io/en/production/dev/api/#rate-limiting
|
||||
#[derive(Debug)]
|
||||
pub struct RateLimit {
|
||||
pub limit: u64,
|
||||
pub remaining: u64,
|
||||
pub reset_in: u64,
|
||||
pub reset: i64,
|
||||
}
|
||||
|
||||
impl RateLimit {
|
||||
/// Extract rate limiting information from the `X-RateLimit-` headers.
|
||||
/// Only returns `Some` if all fields are present and valid.
|
||||
fn from_headers(response: &Response) -> Option<Self> {
|
||||
let limit = response.header("X-RateLimit-Limit")?
|
||||
.parse().ok()?;
|
||||
let remaining = response.header("X-RateLimit-Remaining")?
|
||||
.parse().ok()?;
|
||||
let reset_in = response.header("X-RateLimit-Reset-In")?
|
||||
.parse().ok()?;
|
||||
let reset = response.header("X-RateLimit-Reset")?
|
||||
.parse().ok()?;
|
||||
|
||||
Some(Self {
|
||||
limit,
|
||||
remaining,
|
||||
reset_in,
|
||||
reset,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal trait for response types.
|
||||
/// Allows converting the response type from a `ureq::Response`,
|
||||
/// by deserializing the body into the response type and then
|
||||
/// adding the `rate_limit` field from headers.
|
||||
pub(crate) trait ResponseType: DeserializeOwned {
|
||||
fn from_response(response: Response) -> Result<Self, Error>;
|
||||
}
|
||||
|
||||
/// Internal macro for response types.
|
||||
/// Wraps the definition of a response type, adds the `rate_limit` field,
|
||||
/// and implements the `ResponseType` trait.
|
||||
macro_rules! response_type {
|
||||
(
|
||||
$(#[$meta:meta])*
|
||||
pub struct $name:ident {
|
||||
$(pub $field:ident: $field_ty:ty),*
|
||||
$(,)?
|
||||
}
|
||||
) => {
|
||||
$(#[$meta])*
|
||||
pub struct $name {
|
||||
#[serde(skip)]
|
||||
pub rate_limit: Option<RateLimit>,
|
||||
$(pub $field: $field_ty),*
|
||||
}
|
||||
|
||||
impl ResponseType for $name {
|
||||
fn from_response(response: Response) -> Result<Self, Error> {
|
||||
let rate_limit = RateLimit::from_headers(&response);
|
||||
let mut result: Self = response.into_json().map_err(Error::ResponseJson)?;
|
||||
result.rate_limit = rate_limit;
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------- submit-listens
|
||||
|
||||
/// Response type for [`Client::submit_listens`](super::Client::submit_listens).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SubmitListensResponse {
|
||||
pub status: String,
|
||||
response_type! {
|
||||
/// Response type for [`Client::submit_listens`](super::Client::submit_listens).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SubmitListensResponse {
|
||||
pub status: String,
|
||||
}
|
||||
}
|
||||
|
||||
// --------- validate-token
|
||||
|
||||
/// Response type for [`Client::validate_token`](super::Client::validate_token).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ValidateTokenResponse {
|
||||
pub code: u16,
|
||||
pub message: String,
|
||||
response_type! {
|
||||
/// Response type for [`Client::validate_token`](super::Client::validate_token).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ValidateTokenResponse {
|
||||
pub code: u16,
|
||||
pub message: String,
|
||||
|
||||
pub valid: bool,
|
||||
pub user_name: Option<String>,
|
||||
pub valid: bool,
|
||||
pub user_name: Option<String>,
|
||||
}
|
||||
}
|
||||
|
||||
// --------- delete-listen
|
||||
|
||||
/// Response type for [`Client::delete_listen`](super::Client::delete_listen).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DeleteListenResponse {
|
||||
pub status: String,
|
||||
response_type! {
|
||||
/// Response type for [`Client::delete_listen`](super::Client::delete_listen).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DeleteListenResponse {
|
||||
pub status: String,
|
||||
}
|
||||
}
|
||||
|
||||
// --------- users/{user_list}/recent-listens
|
||||
|
||||
/// Response type for [`Client::users_recent_listens`](super::Client::users_recent_listens).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UsersRecentListensResponse {
|
||||
pub payload: UsersRecentListensPayload,
|
||||
response_type! {
|
||||
/// Response type for [`Client::users_recent_listens`](super::Client::users_recent_listens).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UsersRecentListensResponse {
|
||||
pub payload: UsersRecentListensPayload,
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of the [`UsersRecentListensResponse::payload`] field.
|
||||
|
@ -69,10 +159,12 @@ pub struct UsersRecentListensTrackMetadata {
|
|||
|
||||
// --------- user/{user_name}/listen-count
|
||||
|
||||
/// Response type for [`Client::user_listen_count`](super::Client::user_listen_count).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UserListenCountResponse {
|
||||
pub payload: UserListenCountPayload,
|
||||
response_type! {
|
||||
/// Response type for [`Client::user_listen_count`](super::Client::user_listen_count).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UserListenCountResponse {
|
||||
pub payload: UserListenCountPayload,
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of the [`UserListenCountResponse::payload`] field.
|
||||
|
@ -83,10 +175,12 @@ pub struct UserListenCountPayload {
|
|||
|
||||
// -------- user/{user_name}/playing-now
|
||||
|
||||
/// Response type for [`Client::user_playing_now`](super::Client::user_playing_now).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UserPlayingNowResponse {
|
||||
pub payload: UserPlayingNowPayload,
|
||||
response_type! {
|
||||
/// Response type for [`Client::user_playing_now`](super::Client::user_playing_now).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UserPlayingNowResponse {
|
||||
pub payload: UserPlayingNowPayload,
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of the [`UserPlayingNowResponse::payload`] field.
|
||||
|
@ -117,10 +211,12 @@ pub struct UserPlayingNowTrackMetadata {
|
|||
|
||||
// -------- user/{user_name}/listens
|
||||
|
||||
/// Response type for [`Client::user_listens`](super::Client::user_listens).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UserListensResponse {
|
||||
pub payload: UserListensPayload,
|
||||
response_type! {
|
||||
/// Response type for [`Client::user_listens`](super::Client::user_listens).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UserListensResponse {
|
||||
pub payload: UserListensPayload,
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of the [`UserListensResponse::payload`] field.
|
||||
|
@ -153,27 +249,33 @@ pub struct UserListensTrackMetadata {
|
|||
|
||||
// --------- latest-import (GET)
|
||||
|
||||
/// Response type for [`Client::get_latest_import`](super::Client::get_latest_import).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GetLatestImportResponse {
|
||||
pub latest_import: i64,
|
||||
pub musicbrainz_id: String,
|
||||
response_type! {
|
||||
/// Response type for [`Client::get_latest_import`](super::Client::get_latest_import).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GetLatestImportResponse {
|
||||
pub latest_import: i64,
|
||||
pub musicbrainz_id: String,
|
||||
}
|
||||
}
|
||||
|
||||
// --------- latest-import (POST)
|
||||
|
||||
/// Response type for [`Client::update_latest_import`](super::Client::update_latest_import).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdateLatestImportResponse {
|
||||
pub status: String,
|
||||
response_type! {
|
||||
/// Response type for [`Client::update_latest_import`](super::Client::update_latest_import).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdateLatestImportResponse {
|
||||
pub status: String,
|
||||
}
|
||||
}
|
||||
|
||||
// --------- stats/sitewide/artists
|
||||
|
||||
/// Response type for [`Client::stats_sitewide_artists`](super::Client::stats_sitewide_artists).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct StatsSitewideArtistsResponse {
|
||||
pub payload: StatsSitewideArtistsPayload,
|
||||
response_type! {
|
||||
/// Response type for [`Client::stats_sitewide_artists`](super::Client::stats_sitewide_artists).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct StatsSitewideArtistsResponse {
|
||||
pub payload: StatsSitewideArtistsPayload,
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of the [`StatsSitewideArtistsResponse::payload`] field.
|
||||
|
@ -208,10 +310,12 @@ pub struct StatsSitewideArtistsArtist {
|
|||
|
||||
// --------- stats/user/{user_name}/listening-activity
|
||||
|
||||
/// Response type for [`Client::stats_user_listening_activity`](super::Client::stats_user_listening_activity).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct StatsUserListeningActivityResponse {
|
||||
pub payload: StatsUserListeningActivityPayload,
|
||||
response_type! {
|
||||
/// Response type for [`Client::stats_user_listening_activity`](super::Client::stats_user_listening_activity).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct StatsUserListeningActivityResponse {
|
||||
pub payload: StatsUserListeningActivityPayload,
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of the [`StatsUserListeningActivityResponse::payload`] field.
|
||||
|
@ -235,10 +339,12 @@ pub struct StatsUserListeningActivityListeningActivity {
|
|||
|
||||
// --------- stats/user/{user_name}/daily-activity
|
||||
|
||||
/// Response type for [`Client::stats_user_daily_activity`](super::Client::stats_user_daily_activity).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct StatsUserDailyActivityResponse {
|
||||
pub payload: StatsUserDailyActivityPayload,
|
||||
response_type! {
|
||||
/// Response type for [`Client::stats_user_daily_activity`](super::Client::stats_user_daily_activity).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct StatsUserDailyActivityResponse {
|
||||
pub payload: StatsUserDailyActivityPayload,
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of the [`StatsUserDailyActivityResponse::payload`] field.
|
||||
|
@ -267,10 +373,12 @@ pub struct StatsUserDailyActivityHour {
|
|||
|
||||
// --------- stats/user/{user_name}/recordings
|
||||
|
||||
/// Response type of [`Client::stats_user_recordings`](super::Client::stats_user_recordings).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct StatsUserRecordingsResponse {
|
||||
pub payload: StatsUserRecordingsPayload,
|
||||
response_type! {
|
||||
/// Response type of [`Client::stats_user_recordings`](super::Client::stats_user_recordings).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct StatsUserRecordingsResponse {
|
||||
pub payload: StatsUserRecordingsPayload,
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of the [`StatsUserRecordingsResponse::payload`] field.
|
||||
|
@ -303,10 +411,12 @@ pub struct StatsUserRecordingsRecording {
|
|||
|
||||
// --------- stats/user/{user_name}/artist-map
|
||||
|
||||
/// Response type of [`Client::stats_user_artist_map`](super::Client::stats_user_artist_map).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct StatsUserArtistMapResponse {
|
||||
pub payload: StatsUserArtistMapPayload,
|
||||
response_type! {
|
||||
/// Response type of [`Client::stats_user_artist_map`](super::Client::stats_user_artist_map).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct StatsUserArtistMapResponse {
|
||||
pub payload: StatsUserArtistMapPayload,
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of the [`StatsUserArtistMapResponse::payload`] field.
|
||||
|
@ -329,10 +439,12 @@ pub struct StatsUserArtistMapCountry {
|
|||
|
||||
// --------- stats/user/{user_name}/releases
|
||||
|
||||
/// Response type for [`Client::stats_user_releases`](super::Client::stats_user_releases).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct StatsUserReleasesResponse {
|
||||
pub payload: StatsUserReleasesPayload,
|
||||
response_type! {
|
||||
/// Response type for [`Client::stats_user_releases`](super::Client::stats_user_releases).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct StatsUserReleasesResponse {
|
||||
pub payload: StatsUserReleasesPayload,
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of the [`StatsUserReleasesResponse::payload`] field.
|
||||
|
@ -362,10 +474,12 @@ pub struct StatsUserReleasesRelease {
|
|||
|
||||
// --------- stats/user/{user_name}/artists
|
||||
|
||||
/// Response type of [`Client::stats_user_artists`](super::Client::stats_user_artists).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct StatsUserArtistsResponse {
|
||||
pub payload: StatsUserArtistsPayload,
|
||||
response_type! {
|
||||
/// Response type of [`Client::stats_user_artists`](super::Client::stats_user_artists).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct StatsUserArtistsResponse {
|
||||
pub payload: StatsUserArtistsPayload,
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of the [`StatsUserArtistsResponse::payload`] field.
|
||||
|
@ -392,12 +506,14 @@ pub struct StatsUserArtistsArtist {
|
|||
|
||||
// --------- status/get-dump-info
|
||||
|
||||
/// Response type for [`Client::status_get_dump_info`](super::Client::status_get_dump_info).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct StatusGetDumpInfoResponse {
|
||||
pub code: u16,
|
||||
pub message: String,
|
||||
response_type! {
|
||||
/// Response type for [`Client::status_get_dump_info`](super::Client::status_get_dump_info).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct StatusGetDumpInfoResponse {
|
||||
pub code: u16,
|
||||
pub message: String,
|
||||
|
||||
pub id: i64,
|
||||
pub timestamp: String,
|
||||
pub id: i64,
|
||||
pub timestamp: String,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue