From 134df4b5f7616ef95d11658c2c994bf11e27b1f6 Mon Sep 17 00:00:00 2001 From: Luciano Mammino Date: Tue, 12 Sep 2023 09:56:40 +0100 Subject: [PATCH] BREAKING CHANGE: use typed-builder to implement client builder (#5) --- Cargo.lock | 21 +++++ Cargo.toml | 1 + README.md | 12 +-- examples/customise_client.rs | 6 +- examples/customise_retry_strategy.rs | 6 +- examples/fetch_all.rs | 2 +- src/client.rs | 120 ++++++--------------------- src/lib.rs | 18 ++-- 8 files changed, 73 insertions(+), 113 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 63ea319..75f8cf6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -506,6 +506,7 @@ dependencies = [ "tokio", "tokio-stream", "tracing", + "typed-builder", "url", ] @@ -1167,6 +1168,26 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "typed-builder" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6605aaa56cce0947127ffa0675a8a1b181f87773364390174de60a86ab9085f1" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a6a6884f6a890a012adcc20ce498f30ebdc70fb1ea242c333cc5f435b0b3871" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.14", +] + [[package]] name = "unicode-bidi" version = "0.3.10" diff --git a/Cargo.toml b/Cargo.toml index b98e54c..8f929e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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] diff --git a/README.md b/README.md index b9125c0..11d30fa 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ 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::::from_env("YOUR_USERNAME"); ``` Note: this method will panic if `LASTFM_API_KEY` is not set. @@ -47,7 +47,7 @@ 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::::try_from_env("YOUR_USERNAME"); match maybe_client { Ok(client) => { // do something with the client @@ -58,10 +58,10 @@ 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 @@ -69,7 +69,7 @@ let client = ClientBuilder::new("YOUR_API_KEY", "YOUR_USERNAME").build(); ```rust,no_run #[tokio::main] async fn main() -> Result<(), Box> { - 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); @@ -89,7 +89,7 @@ use futures_util::pin_mut; use futures_util::stream::StreamExt; #[tokio::main] async fn main() -> Result<(), Box> { - 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); diff --git a/examples/customise_client.rs b/examples/customise_client.rs index 200d6a0..4fc59c5 100644 --- a/examples/customise_client.rs +++ b/examples/customise_client.rs @@ -1,8 +1,10 @@ -use lastfm::ClientBuilder; +use lastfm::Client; #[tokio::main] async fn main() -> Result<(), Box> { - 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(); diff --git a/examples/customise_retry_strategy.rs b/examples/customise_retry_strategy.rs index 5824a0a..0483065 100644 --- a/examples/customise_retry_strategy.rs +++ b/examples/customise_retry_strategy.rs @@ -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: @@ -23,7 +23,9 @@ impl RetryStrategy for SimpleRetryStrategy { async fn main() -> Result<(), Box> { 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(); diff --git a/examples/fetch_all.rs b/examples/fetch_all.rs index ed99b32..18ce011 100644 --- a/examples/fetch_all.rs +++ b/examples/fetch_all.rs @@ -8,7 +8,7 @@ use lastfm::Client; async fn main() -> Result<(), Box> { dotenv().ok(); - let client = Client::from_env("loige"); + let client = Client::::from_env("loige"); let now_playing = client.now_playing().await?; if let Some(track) = now_playing { diff --git a/src/client.rs b/src/client.rs index 4b9fe41..faca25a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -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. @@ -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, - base_url: Option, - retry_strategy: Option>, -} - -impl ClientBuilder { - /// Creates a new [`ClientBuilder`] with the given API key and username. - pub fn new, U: AsRef>(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>(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>(username: U) -> Result { - 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) -> 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, U: AsRef> { + 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, } -impl Debug for Client { +impl, U: AsRef> Debug for Client { 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() @@ -269,22 +199,26 @@ async fn get_page(options: GetPageOptions<'_>) -> Result, U: AsRef> Client { /// 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>(username: U) -> Self { - ClientBuilder::try_from_env(username).unwrap().build() + pub fn from_env(username: U) -> Client { + 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>(username: U) -> Result { - Ok(ClientBuilder::try_from_env(username)?.build()) + pub fn try_from_env(username: U) -> Result, 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) @@ -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, @@ -324,8 +258,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: 200, from, to, @@ -333,8 +267,8 @@ impl Client { .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, diff --git a/src/lib.rs b/src/lib.rs index 1b10e6a..9db4079 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,7 +31,7 @@ //! ```rust,no_run //! # use lastfm::Client; //! # -//! let client = Client::from_env("YOUR_USERNAME"); +//! let client = Client::::from_env("YOUR_USERNAME"); //! ``` //! //! Note: this method will panic if `LASTFM_API_KEY` is not set. @@ -41,7 +41,7 @@ //! ```rust,no_run //! # use lastfm::Client; //! # -//! let maybe_client = Client::try_from_env("YOUR_USERNAME"); +//! let maybe_client = Client::::try_from_env("YOUR_USERNAME"); //! match maybe_client { //! Ok(client) => { //! // do something with the client @@ -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> { -//! 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); @@ -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> { -//! 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); //!