jirac/crates/libjirac/src/client.rs

155 lines
4 KiB
Rust

pub mod commands;
use http::header::CONTENT_TYPE;
use http::{HeaderMap, HeaderValue};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::fmt::Debug;
use thiserror::Error;
use url::{ParseError, Url};
pub struct JiraClient {
pub url: String,
pub email: String,
pub api_token: String,
}
pub trait JiraCommand {
type TResponse: DeserializeOwned + Clone;
type TPayload: Serialize;
const REQUEST_TYPE: JiraRequestType;
fn endpoint(&self) -> String;
fn request_body(&self) -> Option<&Self::TPayload>;
fn query_params(&self) -> Option<Vec<(String, String)>>;
}
pub enum JiraRequestType {
Read,
Create,
Update,
Delete,
}
impl From<JiraRequestType> for http::Method {
fn from(value: JiraRequestType) -> Self {
match value {
JiraRequestType::Read => Self::GET,
JiraRequestType::Create => Self::POST,
JiraRequestType::Update => Self::PATCH,
JiraRequestType::Delete => Self::DELETE,
}
}
}
#[derive(Debug, Error)]
pub enum JiraClientError {
#[error(transparent)]
UrlParseError(#[from] ParseError),
#[error(transparent)]
ReqwestError(#[from] reqwest::Error),
#[error(transparent)]
SerdeError(#[from] serde_json::Error),
#[error("API Error: {0}")]
ApiError(String),
}
impl JiraClient {
pub fn new(url: &str, email: &str, api_token: &str) -> Self {
Self {
url: url.to_string(),
email: email.to_string(),
api_token: api_token.to_string(),
}
}
pub async fn exec<TCommand>(
&self,
cmd: TCommand,
) -> Result<TCommand::TResponse, JiraClientError>
where
TCommand: JiraCommand + Debug,
{
let client = reqwest::Client::new();
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
let url = self.construct_url(&cmd.endpoint())?;
let mut request_builder = client
.request(TCommand::REQUEST_TYPE.into(), url)
.basic_auth(&self.email, Some(&self.api_token))
.headers(headers);
if let Some(params) = cmd.query_params() {
request_builder = request_builder.query(&params);
}
if let Some(body) = cmd.request_body() {
request_builder = request_builder.json(body);
}
let response = match request_builder.send().await {
Ok(x) => x,
Err(reason) => return Err(JiraClientError::ReqwestError(reason)),
};
if !response.status().is_success() {
let error_text = response.text().await?;
return Err(JiraClientError::ApiError(error_text));
}
let plain = match response.bytes().await {
Ok(x) => x,
Err(reason) => return Err(JiraClientError::ReqwestError(reason)),
};
let value = match is_unit::<TCommand::TResponse>() {
true => b"null",
false => plain.as_ref(),
};
match serde_json::from_slice::<TCommand::TResponse>(value) {
Ok(x) => Ok(x),
Err(reason) => Err(JiraClientError::SerdeError(reason)),
}
}
fn construct_url(&self, endpoint: &str) -> Result<String, JiraClientError> {
let mut url = match Url::parse(&self.url) {
Ok(x) => x,
Err(reason) => {
return Err(JiraClientError::UrlParseError(reason));
}
};
url.set_path(endpoint);
Ok(url.to_string())
}
}
const fn is_unit<T>() -> bool {
size_of::<T>() == 0 && align_of::<T>() == 1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_unit_type() {
assert!(is_unit::<()>());
assert!(!is_unit::<String>());
}
#[test]
fn deserialize_unit_type() {
let value = match is_unit::<()>() {
true => b"null".as_ref(),
false => b"{}".as_ref(),
};
serde_json::from_slice::<()>(value).unwrap();
}
}