use std::future::Future;
use std::io::{Write, stdout};
use std::{fmt, fs};
use async_stream::try_stream;
use camino::Utf8PathBuf;
use futures_util::{Stream, StreamExt, TryStreamExt, stream};
use reqwest::RequestBuilder;
use serde::{Deserialize, Serialize};
use url::Url;
use crate::Error;
use crate::service::ServiceKind;
use crate::utils::config_dir;
pub trait Contains<T> {
fn contains(&self, obj: &T) -> bool;
}
pub trait RenderSearch<T> {
fn render(&self, fields: &[T]) -> String;
}
pub(crate) trait Api {
fn api(&self) -> String;
}
#[macro_export]
macro_rules! impl_api {
($($type:ty),+) => {$(
impl Api for $type {
fn api(&self) -> String {
self.to_string()
}
}
)+};
}
impl_api!(String, &str, u64, usize, i64);
impl<T: Api> Api for &T {
fn api(&self) -> String {
(*self).api()
}
}
pub trait MergeOption<T> {
fn merge(&mut self, value: Option<T>) -> Self;
}
impl<T> MergeOption<T> for Option<T> {
fn merge(&mut self, value: Option<T>) -> Self {
value.or_else(|| self.take())
}
}
pub trait Merge {
fn merge(&mut self, other: Self);
}
pub trait RequestSend {
type Output;
fn send(&self) -> impl Future<Output = crate::Result<Self::Output>>;
}
pub(crate) trait RequestPagedStream: Clone {
type Item;
fn concurrent(&self) -> Option<usize>;
fn paged(&mut self) -> Option<usize>;
fn paged_requests(self, paged: Option<usize>) -> impl Iterator<Item = Self>;
fn send(self) -> impl Future<Output = crate::Result<Vec<Self::Item>>>;
fn paged_stream(mut self) -> impl Stream<Item = crate::Result<Self::Item>> {
let concurrent = self.concurrent().unwrap_or(1);
let paged = self.paged();
let requests = self.paged_requests(paged);
let mut futures = stream::iter(requests)
.map(|r| r.send())
.buffered(concurrent);
try_stream! {
while let Some(items) = futures.try_next().await? {
let count = items.len();
for item in items {
yield item;
}
match paged {
Some(size) if count == size => (),
_ => break,
}
}
}
}
}
pub trait RequestTemplate: Serialize {
type Params: for<'a> Deserialize<'a> + Merge;
type Service: WebClient;
const TYPE: &'static str;
fn service(&self) -> &Self::Service;
fn params(&mut self) -> &mut Self::Params;
fn config_path(&self, name: &str) -> crate::Result<Utf8PathBuf> {
let service_name = self.service().name();
if service_name.trim().is_empty() || name.contains(std::path::is_separator) {
Ok(Utf8PathBuf::from(name))
} else {
let path = format!("templates/{service_name}/{}/{name}", Self::TYPE);
config_dir().map(|x| x.join(path))
}
}
fn load_template(&mut self, s: &str) -> crate::Result<&mut Self> {
let name = s.trim();
if name.is_empty() {
return Err(Error::InvalidValue(format!("invalid template name: {s:?}")));
}
let path = self.config_path(name)?;
let data = fs::read_to_string(&path)
.map_err(|e| Error::InvalidValue(format!("failed loading template: {name}: {e}")))?;
let params = toml::from_str(&data)
.map_err(|e| Error::InvalidValue(format!("failed parsing template: {name}: {e}")))?;
self.params().merge(params);
Ok(self)
}
fn save_template(&self, s: &str) -> crate::Result<()> {
let name = s.trim();
if name.is_empty() {
return Err(Error::InvalidValue(format!("invalid template name: {s:?}")));
}
let data = toml::to_string(self)
.map_err(|e| Error::InvalidValue(format!("failed serializing template: {e}")))?;
if data.trim().is_empty() {
return Err(Error::InvalidValue(format!(
"empty request template: {name}"
)));
}
if name == "-" {
write!(stdout(), "{data}")?;
} else {
let path = self.config_path(name)?;
fs::create_dir_all(path.parent().expect("invalid template path"))
.map_err(|e| Error::IO(format!("failed creating template dir: {e}")))?;
fs::write(&path, data)
.map_err(|e| Error::IO(format!("failed saving template: {name}: {e}")))?;
}
Ok(())
}
}
pub(crate) trait InjectAuth: Sized {
fn auth<W: WebService>(self, service: &W) -> crate::Result<Self>;
fn auth_optional<W: WebService>(self, service: &W) -> Self;
}
impl InjectAuth for RequestBuilder {
fn auth<W: WebService>(self, service: &W) -> crate::Result<Self> {
service.inject_auth(self, true)
}
fn auth_optional<W: WebService>(self, service: &W) -> Self {
service
.inject_auth(self, false)
.expect("failed injecting optional auth")
}
}
pub(crate) trait WebService: fmt::Display {
#[allow(dead_code)]
const API_VERSION: &'static str;
type Response;
fn inject_auth(&self, request: RequestBuilder, required: bool)
-> crate::Result<RequestBuilder>;
async fn parse_response(&self, response: reqwest::Response) -> crate::Result<Self::Response>;
}
pub trait WebClient {
fn base(&self) -> &Url;
fn kind(&self) -> ServiceKind;
fn name(&self) -> &str;
}
#[cfg(test)]
mod tests {
use std::env;
use tempfile::tempdir;
use crate::service::bugzilla::Bugzilla;
use crate::test::*;
use super::*;
#[tokio::test]
async fn request_template() {
let server = TestServer::new().await;
let service = Bugzilla::new(server.uri()).unwrap();
let mut request1 = service.search();
let mut request2 = service.search();
for name in [" ", "", "\t"] {
let err = request1.save_template(name).unwrap_err();
assert_err_re!(err, "invalid template name: ");
let err = request2.load_template(name).unwrap_err();
assert_err_re!(err, "invalid template name: ");
}
let err = request1.save_template("test").unwrap_err();
assert_err_re!(err, "empty request template: test");
let dir = tempdir().unwrap();
env::set_current_dir(dir.path()).unwrap();
let path = dir.path().join("dir/template");
let path_str = path.to_str().unwrap();
let time = "1d".parse().unwrap();
request1.created(time);
request1.save_template(path_str).unwrap();
assert_eq!(
fs::read_to_string(&path).unwrap().trim(),
r#"created = "1d""#
);
assert_ne!(request1, request2);
request2.load_template(path_str).unwrap();
assert_eq!(request1, request2);
request1.save_template("test").unwrap();
assert_eq!(
fs::read_to_string("test").unwrap().trim(),
r#"created = "1d""#
);
request2.load_template("test").unwrap();
assert_eq!(request1, request2);
let service = Bugzilla::builder(server.uri())
.unwrap()
.name("service")
.build()
.unwrap();
let time = "2d".parse().unwrap();
let mut request1 = service.search();
request1.created(time);
let mut request2 = service.search();
if cfg!(target_os = "linux") {
unsafe {
env::set_var("HOME", dir.path());
env::set_var("XDG_CONFIG_HOME", dir.path());
}
request1.save_template("test").unwrap();
assert_eq!(
fs::read_to_string("bugbite/templates/service/search/test")
.unwrap()
.trim(),
r#"created = "2d""#
);
request2.load_template("test").unwrap();
assert_eq!(request1, request2);
unsafe { env::remove_var("XDG_CONFIG_HOME") };
request1.save_template("test").unwrap();
assert_eq!(
fs::read_to_string(".config/bugbite/templates/service/search/test")
.unwrap()
.trim(),
r#"created = "2d""#
);
request2.load_template("test").unwrap();
assert_eq!(request1, request2);
}
}
}