Skip to content

Commit

Permalink
BREAKING CHANGE: use typed-builder to implement client builder (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
lmammino committed Sep 12, 2023
1 parent ca8e8d5 commit 134df4b
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 113 deletions.
21 changes: 21 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ thiserror = "1.0.40"
tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1.11"
tracing = "0.1.37"
typed-builder = "0.16.0"
url = "2.3.1"

[dev-dependencies]
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@ If you have already registered an application, you can find your API key in the
If you have your API key exposed through the `LASTFM_API_KEY` environment variable, you can use the `from_env` method:

```rust,no_run
let client = Client::from_env("YOUR_USERNAME");
let client = Client::<String, &str>::from_env("YOUR_USERNAME");
```

Note: this method will panic if `LASTFM_API_KEY` is not set.

Alternatively, you can use `try_from_env` which will return a `Result`.

```rust,no_run
let maybe_client = Client::try_from_env("YOUR_USERNAME");
let maybe_client = Client::<String, &str>::try_from_env("YOUR_USERNAME");
match maybe_client {
Ok(client) => {
// do something with the client
Expand All @@ -58,18 +58,18 @@ match maybe_client {
}
```

Finally, for more advanced configurations you can use a `ClientBuilder`:
Finally, for more advanced configurations you can use a `Client::builder()`:
```rust
let client = ClientBuilder::new("YOUR_API_KEY", "YOUR_USERNAME").build();
let client = Client::builder().api_key("YOUR_API_KEY").username("YOUR_USERNAME").build();
```
### Fetch the track you are currently playing
```rust,no_run
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = ClientBuilder::new("YOUR_API_KEY", "YOUR_USERNAME").build();
let client = Client::builder().api_key("YOUR_API_KEY").username("YOUR_USERNAME").build();
let now_playing = client.now_playing().await?;
if let Some(track) = now_playing {
println!("Now playing: {} - {}", track.artist.name, track.name);
Expand All @@ -89,7 +89,7 @@ use futures_util::pin_mut;
use futures_util::stream::StreamExt;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = ClientBuilder::new("YOUR_API_KEY", "YOUR_USERNAME").build();
let client = Client::builder().api_key("YOUR_API_KEY").username("YOUR_USERNAME").build();
let tracks = client.all_tracks().await?;
println!("Total tracks: {}", tracks.total_tracks);

Expand Down
6 changes: 4 additions & 2 deletions examples/customise_client.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use lastfm::ClientBuilder;
use lastfm::Client;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = ClientBuilder::new("some-api-key", "loige")
let client = Client::builder()
.api_key("some-api-key")
.username("loige")
.reqwest_client(reqwest::Client::new())
.base_url("http://localhost:8080".parse().unwrap())
.build();
Expand Down
6 changes: 4 additions & 2 deletions examples/customise_retry_strategy.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use lastfm::{retry_strategy::RetryStrategy, ClientBuilder};
use lastfm::{retry_strategy::RetryStrategy, Client};
use std::time::Duration;

/// A retry strategy that will retry 3 times with the following delays:
Expand All @@ -23,7 +23,9 @@ impl RetryStrategy for SimpleRetryStrategy {
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let retry_strategy = SimpleRetryStrategy {};

let client = ClientBuilder::new("some-api-key", "loige")
let client = Client::builder()
.api_key("some-api-key".to_string())
.username("loige".to_string())
.retry_strategy(Box::from(retry_strategy))
.build();

Expand Down
2 changes: 1 addition & 1 deletion examples/fetch_all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use lastfm::Client;
async fn main() -> Result<(), Box<dyn std::error::Error>> {
dotenv().ok();

let client = Client::from_env("loige");
let client = Client::<String, &str>::from_env("loige");

let now_playing = client.now_playing().await?;
if let Some(track) = now_playing {
Expand Down
120 changes: 27 additions & 93 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use std::{
time::Duration,
};
use tokio_stream::Stream;
use typed_builder::TypedBuilder;
use url::Url;

/// The default base URL for the Last.fm API.
Expand All @@ -40,101 +41,30 @@ fn mask_api_key(api_key: &str) -> String {
.chars()
.enumerate()
.map(|(i, c)| match i {
0 | 1 | 2 => c,
0..=2 => c,
_ => '*',
})
.collect()
}

/// A builder for the [`Client`] struct.
pub struct ClientBuilder {
api_key: String,
username: String,
reqwest_client: Option<reqwest::Client>,
base_url: Option<Url>,
retry_strategy: Option<Box<dyn RetryStrategy>>,
}

impl ClientBuilder {
/// Creates a new [`ClientBuilder`] with the given API key and username.
pub fn new<A: AsRef<str>, U: AsRef<str>>(api_key: A, username: U) -> Self {
Self {
api_key: api_key.as_ref().to_string(),
username: username.as_ref().to_string(),
reqwest_client: None,
base_url: None,
retry_strategy: None,
}
}

/// Creates a new [`ClientBuilder`] with the given username.
/// This is a shortcut for [`ClientBuilder::try_from_env`] that panics instead of returning an error.
///
/// # Panics
/// This methods expects the `LASTFM_API_KEY` environment variable to be set and it would panic otherwise.
pub fn from_env<U: AsRef<str>>(username: U) -> Self {
Self::try_from_env(username).expect("Cannot read LASTFM_API_KEY from environment")
}

/// Creates a new [`ClientBuilder`] with the given username.
/// This methods expects the `LASTFM_API_KEY` environment variable to be set and it would return an error otherwise.
pub fn try_from_env<U: AsRef<str>>(username: U) -> Result<Self, VarError> {
let api_key = env::var("LASTFM_API_KEY")?;
Ok(ClientBuilder::new(api_key, username))
}

/// Sets the [`reqwest::Client`] to use for the requests.
pub fn reqwest_client(mut self, client: reqwest::Client) -> Self {
self.reqwest_client = Some(client);
self
}

/// Sets the base URL for the Last.fm API.
pub fn base_url(mut self, base_url: Url) -> Self {
self.base_url = Some(base_url);
self
}

/// Sets the retry strategy to use for the requests.
///
/// For more details on how you can create a custom retry strategy, consult the [`crate::retry_strategy::RetryStrategy`] trait.
pub fn retry_strategy(mut self, retry_strategy: Box<dyn RetryStrategy>) -> Self {
self.retry_strategy = Some(retry_strategy);
self
}

/// Builds the [`Client`] instance.
pub fn build(self) -> Client {
Client {
api_key: self.api_key,
username: self.username,
reqwest_client: self
.reqwest_client
.unwrap_or_else(|| DEFAULT_CLIENT.clone()),
base_url: self
.base_url
.unwrap_or_else(|| DEFAULT_BASE_URL.parse().unwrap()),
retry_strategy: self
.retry_strategy
.unwrap_or_else(|| Box::from(JitteredBackoff::default())),
}
}
}

/// A client for the Last.fm API.
pub struct Client {
api_key: String,
username: String,
#[derive(TypedBuilder)]
pub struct Client<A: AsRef<str>, U: AsRef<str>> {
api_key: A,
username: U,
#[builder(default = DEFAULT_CLIENT.clone())]
reqwest_client: reqwest::Client,
#[builder(default = DEFAULT_BASE_URL.parse().unwrap())]
base_url: Url,
#[builder(default = Box::from(JitteredBackoff::default()))]
retry_strategy: Box<dyn RetryStrategy>,
}

impl Debug for Client {
impl<A: AsRef<str>, U: AsRef<str>> Debug for Client<A, U> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Client")
.field("api_key", &mask_api_key(&self.api_key).as_str())
.field("username", &self.username)
.field("api_key", &mask_api_key(&self.api_key.as_ref()).as_str())
.field("username", &self.username.as_ref())
.field("reqwest_client", &self.reqwest_client)
.field("base_url", &self.base_url)
.finish()
Expand Down Expand Up @@ -269,22 +199,26 @@ async fn get_page(options: GetPageOptions<'_>) -> Result<RecentTracksPage, Error
Err(Error::TooManyRetry(errors))
}

impl Client {
impl<A: AsRef<str>, U: AsRef<str>> Client<A, U> {
/// Creates a new [`Client`] with the given username.
/// The API key is read from the `LASTFM_API_KEY` environment variable.
/// This method is a shortcut for [`ClientBuilder::from_env`] but, in case of failure, it will panic rather than returning an error.
///
/// # Panics
/// If the environment variable is not set, this function will panic.
pub fn from_env<U: AsRef<str>>(username: U) -> Self {
ClientBuilder::try_from_env(username).unwrap().build()
pub fn from_env(username: U) -> Client<String, U> {
Self::try_from_env(username).expect("Missing LASTFM_API_KEY environment variable")
}

/// Creates a new [`Client`] with the given username.
/// The API key is read from the `LASTFM_API_KEY` environment variable.
/// If the environment variable is not set, this function will return an error.
pub fn try_from_env<U: AsRef<str>>(username: U) -> Result<Self, VarError> {
Ok(ClientBuilder::try_from_env(username)?.build())
pub fn try_from_env(username: U) -> Result<Client<String, U>, VarError> {
let api_key = env::var("LASTFM_API_KEY")?;
Ok(Client::builder()
.username(username)
.api_key(api_key)
.build())
}

/// Fetches the currently playing track for the user (if any)
Expand All @@ -293,8 +227,8 @@ impl Client {
client: &self.reqwest_client,
retry_strategy: &*self.retry_strategy,
base_url: &self.base_url,
api_key: &self.api_key,
username: &self.username,
api_key: &self.api_key.as_ref(),
username: &self.username.as_ref(),
limit: 1,
from: None,
to: None,
Expand Down Expand Up @@ -324,17 +258,17 @@ impl Client {
client: &self.reqwest_client,
retry_strategy: &*self.retry_strategy,
base_url: &self.base_url,
api_key: &self.api_key,
username: &self.username,
api_key: &self.api_key.as_ref(),
username: &self.username.as_ref(),
limit: 200,
from,
to,
})
.await?;

let mut fetcher = RecentTracksFetcher {
api_key: self.api_key.clone(),
username: self.username.clone(),
api_key: self.api_key.as_ref().to_string(),
username: self.username.as_ref().to_string(),
current_page: vec![],
from,
to,
Expand Down
18 changes: 9 additions & 9 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
//! ```rust,no_run
//! # use lastfm::Client;
//! #
//! let client = Client::from_env("YOUR_USERNAME");
//! let client = Client::<String, &str>::from_env("YOUR_USERNAME");
//! ```
//!
//! Note: this method will panic if `LASTFM_API_KEY` is not set.
Expand All @@ -41,7 +41,7 @@
//! ```rust,no_run
//! # use lastfm::Client;
//! #
//! let maybe_client = Client::try_from_env("YOUR_USERNAME");
//! let maybe_client = Client::<String, &str>::try_from_env("YOUR_USERNAME");
//! match maybe_client {
//! Ok(client) => {
//! // do something with the client
Expand All @@ -52,22 +52,22 @@
//! }
//! ```
//!
//! Finally, for more advanced configurations you can use a `ClientBuilder`:
//! Finally, for more advanced configurations you can use a `Client::builder()`:
//!
//! ```rust
//! # use lastfm::ClientBuilder;
//! # use lastfm::Client;
//! #
//! let client = ClientBuilder::new("YOUR_API_KEY", "YOUR_USERNAME").build();
//! let client = Client::builder().api_key("YOUR_API_KEY").username("YOUR_USERNAME").build();
//! ```
//!
//! ### Fetch the track you are currently playing
//!
//! ```rust,no_run
//! # use lastfm::ClientBuilder;
//! # use lastfm::Client;
//! #
//! #[tokio::main]
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let client = ClientBuilder::new("YOUR_API_KEY", "YOUR_USERNAME").build();
//! let client = Client::builder().api_key("YOUR_API_KEY").username("YOUR_USERNAME").build();
//! let now_playing = client.now_playing().await?;
//! if let Some(track) = now_playing {
//! println!("Now playing: {} - {}", track.artist.name, track.name);
Expand All @@ -85,11 +85,11 @@
//! ```rust,no_run
//! use futures_util::pin_mut;
//! use futures_util::stream::StreamExt;
//! # use lastfm::ClientBuilder;
//! # use lastfm::Client;
//! #
//! #[tokio::main]
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let client = ClientBuilder::new("YOUR_API_KEY", "YOUR_USERNAME").build();
//! let client = Client::builder().api_key("YOUR_API_KEY").username("YOUR_USERNAME").build();
//! let tracks = client.all_tracks().await?;
//! println!("Total tracks: {}", tracks.total_tracks);
//!
Expand Down

0 comments on commit 134df4b

Please sign in to comment.