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:
Koen Bolhuis 2021-01-30 03:06:02 +01:00
parent fd9c7823d7
commit 508f697a51
2 changed files with 217 additions and 98 deletions

View File

@ -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)
}
}

View File

@ -1,19 +1,104 @@
//! 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! {
/// Response type for [`Client::submit_listens`](super::Client::submit_listens).
#[derive(Debug, Deserialize)]
pub struct SubmitListensResponse {
pub status: String,
}
}
// --------- validate-token
response_type! {
/// Response type for [`Client::validate_token`](super::Client::validate_token).
#[derive(Debug, Deserialize)]
pub struct ValidateTokenResponse {
@ -23,22 +108,27 @@ pub struct ValidateTokenResponse {
pub valid: bool,
pub user_name: Option<String>,
}
}
// --------- delete-listen
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! {
/// 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.
#[derive(Debug, Deserialize)]
@ -69,11 +159,13 @@ pub struct UsersRecentListensTrackMetadata {
// --------- user/{user_name}/listen-count
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.
#[derive(Debug, Deserialize)]
@ -83,11 +175,13 @@ pub struct UserListenCountPayload {
// -------- user/{user_name}/playing-now
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.
#[derive(Debug, Deserialize)]
@ -117,11 +211,13 @@ pub struct UserPlayingNowTrackMetadata {
// -------- user/{user_name}/listens
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.
#[derive(Debug, Deserialize)]
@ -153,28 +249,34 @@ pub struct UserListensTrackMetadata {
// --------- latest-import (GET)
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! {
/// 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! {
/// 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.
#[derive(Debug, Deserialize)]
@ -208,11 +310,13 @@ pub struct StatsSitewideArtistsArtist {
// --------- stats/user/{user_name}/listening-activity
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.
#[derive(Debug, Deserialize)]
@ -235,11 +339,13 @@ pub struct StatsUserListeningActivityListeningActivity {
// --------- stats/user/{user_name}/daily-activity
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.
#[derive(Debug, Deserialize)]
@ -267,11 +373,13 @@ pub struct StatsUserDailyActivityHour {
// --------- stats/user/{user_name}/recordings
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.
#[derive(Debug, Deserialize)]
@ -303,11 +411,13 @@ pub struct StatsUserRecordingsRecording {
// --------- stats/user/{user_name}/artist-map
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.
#[derive(Debug, Deserialize)]
@ -329,11 +439,13 @@ pub struct StatsUserArtistMapCountry {
// --------- stats/user/{user_name}/releases
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.
#[derive(Debug, Deserialize)]
@ -362,11 +474,13 @@ pub struct StatsUserReleasesRelease {
// --------- stats/user/{user_name}/artists
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.
#[derive(Debug, Deserialize)]
@ -392,6 +506,7 @@ pub struct StatsUserArtistsArtist {
// --------- status/get-dump-info
response_type! {
/// Response type for [`Client::status_get_dump_info`](super::Client::status_get_dump_info).
#[derive(Debug, Deserialize)]
pub struct StatusGetDumpInfoResponse {
@ -401,3 +516,4 @@ pub struct StatusGetDumpInfoResponse {
pub id: i64,
pub timestamp: String,
}
}