use std::fmt::{Debug, Formatter};
use std::time::Duration;
use failure::Fail;
use fedora::{AnonymousSessionBuilder, OpenIDSessionBuilder, Session};
use reqwest::blocking::Response;
use url::Url;
use crate::data::{FEDORA_BODHI_STG_URL, FEDORA_BODHI_URL};
use crate::error::{QueryError, ServiceError};
use crate::{Create, Edit, Query};
pub const DEFAULT_ROWS: u32 = 50;
const REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
const REQUEST_RETRIES: usize = 3;
const USER_AGENT: &str = "bodhi-rs";
#[derive(Debug)]
enum BodhiServiceType {
DEFAULT,
STAGING,
CUSTOM { openid_url: String },
}
#[derive(Debug)]
pub struct BodhiServiceBuilder<'a> {
service_type: BodhiServiceType,
authentication: Option<Authentication<'a>>,
url: String,
timeout: Option<Duration>,
retries: Option<usize>,
}
#[derive(Debug)]
struct Authentication<'a> {
username: &'a str,
password: &'a str,
}
#[derive(Debug, Fail)]
pub enum BuilderError {
#[fail(display = "Failed to parse service URL: {}", error)]
UrlParsingError {
error: url::ParseError,
},
#[fail(display = "Failed to initialize OpenID client: {}", error)]
OpenIDClientError {
error: fedora::openid::OpenIDClientError,
},
#[fail(display = "Failed to initialize the HTTP client: {}", error)]
InitialisationError {
error: fedora::anonymous::InitialisationError,
},
}
impl From<url::ParseError> for BuilderError {
fn from(error: url::ParseError) -> Self {
BuilderError::UrlParsingError { error }
}
}
impl From<fedora::openid::OpenIDClientError> for BuilderError {
fn from(error: fedora::openid::OpenIDClientError) -> Self {
BuilderError::OpenIDClientError { error }
}
}
impl From<fedora::anonymous::InitialisationError> for BuilderError {
fn from(error: fedora::anonymous::InitialisationError) -> Self {
BuilderError::InitialisationError { error }
}
}
impl<'a> BodhiServiceBuilder<'a> {
pub fn default() -> Self {
BodhiServiceBuilder {
service_type: BodhiServiceType::DEFAULT,
authentication: None,
url: FEDORA_BODHI_URL.to_string(),
timeout: None,
retries: None,
}
}
pub fn staging() -> Self {
BodhiServiceBuilder {
service_type: BodhiServiceType::STAGING,
authentication: None,
url: FEDORA_BODHI_STG_URL.to_string(),
timeout: None,
retries: None,
}
}
pub fn custom(url: String, openid_url: String) -> Self {
BodhiServiceBuilder {
service_type: BodhiServiceType::CUSTOM { openid_url },
authentication: None,
url,
timeout: None,
retries: None,
}
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn retries(mut self, retries: usize) -> Self {
self.retries = Some(retries);
self
}
pub fn authentication(mut self, username: &'a str, password: &'a str) -> Self {
self.authentication = Some(Authentication { username, password });
self
}
pub fn build(self) -> Result<BodhiService, BuilderError> {
let url = Url::parse(&self.url)?;
let timeout = match self.timeout {
Some(timeout) => timeout,
None => REQUEST_TIMEOUT,
};
let retries = match self.retries {
Some(retries) => retries,
None => REQUEST_RETRIES,
};
let login_url = url.join("/login")?;
let user_agent = USER_AGENT.to_string();
let session: Box<dyn Session> = if let Some(auth) = self.authentication {
match self.service_type {
BodhiServiceType::DEFAULT => Box::new(
OpenIDSessionBuilder::default(login_url, auth.username, auth.password)
.user_agent(&user_agent)
.timeout(timeout)
.build()?,
),
BodhiServiceType::STAGING => Box::new(
OpenIDSessionBuilder::staging(login_url, auth.username, auth.password)
.user_agent(&user_agent)
.timeout(timeout)
.build()?,
),
BodhiServiceType::CUSTOM { openid_url } => {
let url = Url::parse(&openid_url)?;
Box::new(
OpenIDSessionBuilder::custom(url, login_url, auth.username, auth.password)
.user_agent(&user_agent)
.timeout(timeout)
.build()?,
)
},
}
} else {
Box::new(
AnonymousSessionBuilder::new()
.user_agent(&user_agent)
.timeout(timeout)
.build()?,
)
};
Ok(BodhiService { url, session, retries })
}
}
pub struct BodhiService {
url: Url,
session: Box<dyn Session>,
retries: usize,
}
impl Debug for BodhiService {
fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
writeln!(f, "BodhiService {{ url: {}, retries: {} }}", &self.url, self.retries)
}
}
impl BodhiService {
pub(crate) fn get(&self, path: &str) -> Result<Response, ServiceError> {
let url = self.url.join(path)?;
let qf = || {
match self.session.session().get(url.clone()).send() {
Ok(response) => {
match response.content_length() {
Some(_len) => {
Ok(response)
},
None => {
Err(ServiceError::EmptyResponseError)
},
}
},
Err(error) => {
Err(ServiceError::RequestError { error })
},
}
};
let retries: Vec<Duration> = vec![Duration::from_secs(1); self.retries];
match retry::retry(retries, qf) {
Ok(response) => {
#[cfg(feature = "debug")]
{
dbg!(&response);
};
Ok(response)
},
Err(error) => {
if let retry::Error::Operation { error: inner, .. } = error {
Err(inner)
} else {
Err(ServiceError::RetryError)
}
},
}
}
pub(crate) fn post(&self, path: &str, body: String) -> Result<Response, ServiceError> {
let url = self.url.join(path)?;
let response = self.session.session().post(url).body(body).send()?;
#[cfg(feature = "debug")]
{
dbg!(&response);
}
Ok(response)
}
pub fn query<T>(&self, query: impl Query<T>) -> Result<T, QueryError> {
Query::query(query, self)
}
pub fn create<T>(&self, creator: &dyn Create<T>) -> Result<T, QueryError> {
Create::create(creator, self)
}
pub fn edit<T>(&self, editor: &dyn Edit<T>) -> Result<T, QueryError> {
Edit::edit(editor, self)
}
}