Skip to content

Commit

Permalink
Simplify request methods in base client
Browse files Browse the repository at this point in the history
  • Loading branch information
marioortizmanero committed Dec 29, 2022
1 parent cb21a54 commit a759905
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 94 deletions.
3 changes: 2 additions & 1 deletion doc/uml/trait_hierarchy.plantuml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ abstract class BaseClient {
ClientResult<()> auto_reauth()
ClientResult<()> refresh_token()
String endpoint_url()
String auth_url()
ClientResult<Headers> auth_headers()
ClientResult<()> write_token_cache()
ClientResult<Token> fetch_access_token()
Expand Down Expand Up @@ -55,4 +56,4 @@ class ClientCredsSpotify implements BaseClient{
ClientResult<Option<Token>> refetch_token()
---
}
@enduml
@enduml
2 changes: 1 addition & 1 deletion src/auth_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ impl AuthCodeSpotify {
payload.insert(params::SHOW_DIALOG, "true");
}

let request_url = format!("{}/{}", self.config.auth_base_url, auth_urls::AUTHORIZE);
let request_url = self.auth_url(auth_urls::AUTHORIZE);
let parsed = Url::parse_with_params(&request_url, payload)?;
Ok(parsed.into())
}
Expand Down
2 changes: 1 addition & 1 deletion src/auth_code_pkce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ impl AuthCodePkceSpotify {
payload.insert(params::STATE, &self.oauth.state);
payload.insert(params::SCOPE, &scopes);

let request_url = format!("{}/{}", self.config.auth_base_url, auth_urls::AUTHORIZE);
let request_url = self.auth_url(auth_urls::AUTHORIZE);
let parsed = Url::parse_with_params(&request_url, payload)?;
Ok(parsed.into())
}
Expand Down
131 changes: 46 additions & 85 deletions src/clients/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,22 @@ where
/// be mutable (the token is accessed to from every endpoint).
fn get_token(&self) -> Arc<Mutex<Option<Token>>>;

/// If it's a relative URL like "me", the prefix is appended to it.
/// Otherwise, the same URL is returned.
/// Returns the absolute URL for an endpoint in the API
fn endpoint_url(&self, url: &str) -> String {
// Using the client's prefix in case it's a relative route.
if url.starts_with("http") {
url.to_string()
} else {
self.get_config().api_base_url.clone() + url
let mut base = self.get_config().api_base_url.clone();
if !base.ends_with('/') {
base.push('/');
}
base + url
}

/// Returns the absolute URL for an authentication step in the API
fn auth_url(&self, url: &str) -> String {
let mut base = self.get_config().auth_base_url.clone();
if !base.ends_with('/') {
base.push('/');
}
base + url
}

/// Refetch the current access token given a refresh token.
Expand Down Expand Up @@ -100,109 +107,64 @@ where
.auth_headers()
}

// HTTP-related methods for the Spotify client. It wraps the basic HTTP
// client with features needed of higher level.
//
// The Spotify client has two different wrappers to perform requests:
//
// * Basic wrappers: `get`, `post`, `put`, `delete`, `post_form`. These only
// append the configured Spotify API URL to the relative URL provided so
// that it's not forgotten. They're used in the authentication process to
// request an access token and similars.
// * Endpoint wrappers: `endpoint_get`, `endpoint_post`, `endpoint_put`,
// `endpoint_delete`. These append the authentication headers for endpoint
// requests to reduce the code needed for endpoints and make them as
// concise as possible.
// HTTP-related methods for the Spotify client. They wrap up the basic HTTP
// client with its specific usage for endpoints or authentication.

/// Convenience method to send GET requests related to an endpoint in the
/// API.
#[doc(hidden)]
#[inline]
async fn get(
&self,
url: &str,
headers: Option<&Headers>,
payload: &Query<'_>,
) -> ClientResult<String> {
async fn endpoint_get(&self, url: &str, payload: &Query<'_>) -> ClientResult<String> {
let url = self.endpoint_url(url);
Ok(self.get_http().get(&url, headers, payload).await?)
let headers = self.auth_headers().await;
Ok(self.get_http().get(&url, Some(&headers), payload).await?)
}

/// Convenience method to send POST requests related to an endpoint in the
/// API.
#[doc(hidden)]
#[inline]
async fn post(
&self,
url: &str,
headers: Option<&Headers>,
payload: &Value,
) -> ClientResult<String> {
async fn endpoint_post(&self, url: &str, payload: &Value) -> ClientResult<String> {
let url = self.endpoint_url(url);
Ok(self.get_http().post(&url, headers, payload).await?)
let headers = self.auth_headers().await;
Ok(self.get_http().post(&url, Some(&headers), payload).await?)
}

/// Convenience method to send PUT requests related to an endpoint in the
/// API.
#[doc(hidden)]
#[inline]
async fn post_form(
&self,
url: &str,
headers: Option<&Headers>,
payload: &Form<'_>,
) -> ClientResult<String> {
async fn endpoint_put(&self, url: &str, payload: &Value) -> ClientResult<String> {
let url = self.endpoint_url(url);
Ok(self.get_http().post_form(&url, headers, payload).await?)
let headers = self.auth_headers().await;
Ok(self.get_http().put(&url, Some(&headers), payload).await?)
}

/// Convenience method to send DELETE requests related to an endpoint in the
/// API.
#[doc(hidden)]
#[inline]
async fn put(
&self,
url: &str,
headers: Option<&Headers>,
payload: &Value,
) -> ClientResult<String> {
async fn endpoint_delete(&self, url: &str, payload: &Value) -> ClientResult<String> {
let url = self.endpoint_url(url);
Ok(self.get_http().put(&url, headers, payload).await?)
let headers = self.auth_headers().await;
Ok(self
.get_http()
.delete(&url, Some(&headers), payload)
.await?)
}

/// Convenience method to send POST requests related to the authentication
/// process.
#[doc(hidden)]
#[inline]
async fn delete(
async fn auth_post(
&self,
url: &str,
headers: Option<&Headers>,
payload: &Value,
payload: &Form<'_>,
) -> ClientResult<String> {
let url = self.endpoint_url(url);
Ok(self.get_http().delete(&url, headers, payload).await?)
}

// The wrappers for the endpoints, which also includes the required
// autentication.

#[doc(hidden)]
#[inline]
async fn endpoint_get(&self, url: &str, payload: &Query<'_>) -> ClientResult<String> {
let headers = self.auth_headers().await;
self.get(url, Some(&headers), payload).await
}

#[doc(hidden)]
#[inline]
async fn endpoint_post(&self, url: &str, payload: &Value) -> ClientResult<String> {
let headers = self.auth_headers().await;
self.post(url, Some(&headers), payload).await
}

#[doc(hidden)]
#[inline]
async fn endpoint_put(&self, url: &str, payload: &Value) -> ClientResult<String> {
let headers = self.auth_headers().await;
self.put(url, Some(&headers), payload).await
}

#[doc(hidden)]
#[inline]
async fn endpoint_delete(&self, url: &str, payload: &Value) -> ClientResult<String> {
let headers = self.auth_headers().await;
self.delete(url, Some(&headers), payload).await
let url = self.auth_url(url);
Ok(self.get_http().post_form(&url, headers, payload).await?)
}

/// Updates the cache file at the internal cache path.
Expand Down Expand Up @@ -230,8 +192,7 @@ where
payload: &Form<'_>,
headers: Option<&Headers>,
) -> ClientResult<Token> {
let request_url = format!("{}/{}", self.get_config().auth_base_url, auth_urls::TOKEN);
let response = self.post_form(&request_url, headers, payload).await?;
let response = self.auth_post(auth_urls::TOKEN, headers, payload).await?;

let mut tok = serde_json::from_str::<Token>(&response)?;
tok.expires_at = Utc::now().checked_add_signed(tok.expires_in);
Expand Down
51 changes: 45 additions & 6 deletions src/clients/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub(crate) fn append_device_id(path: &str, device_id: Option<&str>) -> String {
#[cfg(test)]
mod test {
use super::*;
use crate::{model::Token, scopes, ClientCredsSpotify};
use crate::{model::Token, scopes, ClientCredsSpotify, Config};
use chrono::{prelude::*, Duration};

#[test]
Expand All @@ -56,18 +56,57 @@ mod test {

#[test]
fn test_endpoint_url() {
let spotify = ClientCredsSpotify::default();
let mut spotify = ClientCredsSpotify::default();
assert_eq!(
spotify.endpoint_url("me/player/play"),
"https://api.spotify.com/v1/me/player/play"
);

spotify.config = Config {
api_base_url: String::from("http://localhost:8080/api/v1/"),
..Default::default()
};
assert_eq!(
spotify.endpoint_url("http://api.spotify.com/v1/me/player/play"),
"http://api.spotify.com/v1/me/player/play"
spotify.endpoint_url("me/player/play"),
"http://localhost:8080/api/v1/me/player/play"
);

// Also works without trailing character
spotify.config = Config {
api_base_url: String::from("http://localhost:8080/api/v1"),
..Default::default()
};
assert_eq!(
spotify.endpoint_url("https://api.spotify.com/v1/me/player/play"),
"https://api.spotify.com/v1/me/player/play"
spotify.endpoint_url("me/player/play"),
"http://localhost:8080/api/v1/me/player/play"
);
}

#[test]
fn test_auth_url() {
let mut spotify = ClientCredsSpotify::default();
assert_eq!(
spotify.auth_url("api/token"),
"https://accounts.spotify.com/api/token"
);

spotify.config = Config {
auth_base_url: String::from("http://localhost:8080/accounts/"),
..Default::default()
};
assert_eq!(
spotify.auth_url("api/token"),
"http://localhost:8080/accounts/api/token"
);

// Also works without trailing character
spotify.config = Config {
auth_base_url: String::from("http://localhost:8080/accounts"),
..Default::default()
};
assert_eq!(
spotify.auth_url("api/token"),
"http://localhost:8080/accounts/api/token"
);
}

Expand Down

0 comments on commit a759905

Please sign in to comment.