use std::{error::Error as StdError, fmt};
use url::Url;
use crate::{
client::{AuthType, OAuth2Client},
http::HttpClient,
};
pub struct OAuth2ClientBuilder {
client_id: Option<String>,
client_secret: Option<String>,
auth_url: Option<String>,
token_url: Option<String>,
redirect_url: Option<String>,
scopes: Vec<String>,
auth_type: AuthType,
http: Option<HttpClient>,
}
impl OAuth2ClientBuilder {
pub(crate) fn new() -> Self {
Self {
client_id: None,
client_secret: None,
auth_url: None,
token_url: None,
redirect_url: None,
scopes: Vec::new(),
auth_type: AuthType::default(),
http: None,
}
}
pub fn client_id(mut self, client_id: impl Into<String>) -> Self {
self.client_id = Some(client_id.into());
self
}
pub fn client_secret(mut self, client_secret: impl Into<String>) -> Self {
self.client_secret = Some(client_secret.into());
self
}
pub fn auth_url(mut self, auth_url: impl Into<String>) -> Self {
self.auth_url = Some(auth_url.into());
self
}
pub fn token_url(mut self, token_url: impl Into<String>) -> Self {
self.token_url = Some(token_url.into());
self
}
pub fn redirect_url(mut self, redirect_url: impl Into<String>) -> Self {
self.redirect_url = Some(redirect_url.into());
self
}
pub fn scopes(mut self, scopes: &[&str]) -> Self {
self.scopes = scopes.iter().map(|scope| scope.to_string()).collect();
self
}
pub fn basic_auth(mut self) -> Self {
self.auth_type = AuthType::BasicAuth;
self
}
pub fn request_body(mut self) -> Self {
self.auth_type = AuthType::RequestBody;
self
}
#[cfg_attr(not(feature = "reqwest"), allow(unreachable_code, unused_mut))]
pub fn http_client(mut self, http_client: impl Into<HttpClient>) -> Self {
self.http = Some(http_client.into());
self
}
pub fn set_client_id(&mut self, client_id: impl Into<String>) {
self.client_id = Some(client_id.into());
}
pub fn set_client_secret(&mut self, client_secret: impl Into<String>) {
self.client_secret = Some(client_secret.into());
}
pub fn set_auth_url(&mut self, auth_url: impl Into<String>) {
self.auth_url = Some(auth_url.into());
}
pub fn set_token_url(&mut self, token_url: impl Into<String>) {
self.token_url = Some(token_url.into());
}
pub fn set_redirect_url(&mut self, redirect_url: impl Into<String>) {
self.redirect_url = Some(redirect_url.into());
}
pub fn set_scopes(&mut self, scopes: &[&str]) {
self.scopes = scopes.iter().map(|scope| scope.to_string()).collect();
}
#[cfg_attr(not(feature = "reqwest"), allow(unreachable_code))]
pub fn set_http_client(&mut self, http_client: impl Into<HttpClient>) {
self.http = Some(http_client.into());
}
pub fn build(self) -> OAuth2Client {
self.try_build().unwrap()
}
pub fn try_build(self) -> Result<OAuth2Client, ConfigError> {
let client_id = self.client_id.ok_or(ConfigError::MissingClientId)?;
let auth_url = Url::parse(&self.auth_url.ok_or(ConfigError::MissingAuthUrl)?)
.map_err(ConfigError::InvalidAuthUrl)?;
let token_url = Url::parse(&self.token_url.ok_or(ConfigError::MissingTokenUrl)?)
.map_err(ConfigError::InvalidTokenUrl)?;
let redirect_url = self
.redirect_url
.map(|url| Url::parse(&url))
.transpose()
.map_err(ConfigError::InvalidRedirectUrl)?;
let http = self.http;
#[cfg(feature = "reqwest")]
let http = http.or_else(|| {
Some(HttpClient::Reqwest(
crate::http::dep_reqwest::default_client(),
))
});
let http = http.ok_or(ConfigError::NoHttpClient)?;
Ok(OAuth2Client {
client_id,
client_secret: self.client_secret,
auth_url,
token_url,
redirect_url,
scopes: self.scopes,
auth_type: self.auth_type,
http,
})
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ConfigError {
MissingClientId,
MissingAuthUrl,
MissingTokenUrl,
InvalidAuthUrl(url::ParseError),
InvalidTokenUrl(url::ParseError),
InvalidRedirectUrl(url::ParseError),
NoHttpClient,
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigError::MissingClientId => f.write_str("client id is missing"),
ConfigError::MissingAuthUrl => f.write_str("authorization url is missing"),
ConfigError::MissingTokenUrl => f.write_str("token url is missing"),
ConfigError::InvalidAuthUrl(parse_error) => {
write!(f, "could not parse authorization url: {parse_error}")
}
ConfigError::InvalidTokenUrl(parse_error) => {
write!(f, "could not parse token url: {parse_error}")
}
ConfigError::InvalidRedirectUrl(parse_error) => {
write!(f, "could not parse redirect url: {parse_error}")
}
ConfigError::NoHttpClient => f.write_str(
"no HTTP client available (enable a backend feature such as `reqwest`, or set one with `http_client`)",
),
}
}
}
impl StdError for ConfigError {}
#[cfg(all(test, not(feature = "reqwest")))]
mod tests {
use super::*;
#[test]
fn no_backend_is_a_config_error() {
let result = OAuth2ClientBuilder::new()
.client_id("id")
.auth_url("https://provider.example/authorize")
.token_url("https://provider.example/token")
.try_build();
assert!(matches!(result, Err(ConfigError::NoHttpClient)));
}
}