use tracing::{Instrument, debug};
use reqwest::header::CONTENT_TYPE;
use reqwest::{Client, Method};
use serde::Serialize;
use serde::de::DeserializeOwned;
use crate::core::{ClientCore, RequestContext};
use crate::rep::Session;
use crate::{Credentials, Error, Result};
#[derive(Clone, Debug)]
pub struct Jira {
pub(crate) core: ClientCore,
client: Client,
}
impl Jira {
#[allow(dead_code)]
pub(crate) fn host(&self) -> &url::Url {
&self.core.host
}
}
impl Jira {
pub fn new<H>(host: H, credentials: Credentials) -> Result<Jira>
where
H: Into<String>,
{
let core = ClientCore::new(host, credentials)?;
Ok(Jira {
core,
client: Client::new(),
})
}
pub fn from_client<H>(host: H, credentials: Credentials, client: Client) -> Result<Jira>
where
H: Into<String>,
{
let core = ClientCore::new(host, credentials)?;
Ok(Jira { core, client })
}
pub fn with_core(core: ClientCore) -> Result<Jira> {
Ok(Jira {
core,
client: Client::new(),
})
}
pub fn with_search_api_version<H>(
host: H,
credentials: Credentials,
search_api_version: crate::core::SearchApiVersion,
) -> Result<Jira>
where
H: Into<String>,
{
let core = ClientCore::with_search_api_version(host, credentials, search_api_version)?;
Ok(Jira {
core,
client: Client::new(),
})
}
#[tracing::instrument]
pub fn search(&self) -> crate::search::AsyncSearch {
crate::search::AsyncSearch::new(self)
}
#[cfg(debug_assertions)]
pub fn get_search_api_version(&self) -> crate::core::SearchApiVersion {
self.core.get_search_api_version()
}
#[tracing::instrument]
pub fn issues(&self) -> crate::issues::AsyncIssues {
crate::issues::AsyncIssues::new(self)
}
#[tracing::instrument]
pub fn issue_links(&self) -> crate::issue_links::AsyncIssueLinks {
crate::issue_links::AsyncIssueLinks::new(self)
}
#[tracing::instrument]
pub fn projects(&self) -> crate::projects::AsyncProjects {
crate::projects::AsyncProjects::new(self)
}
#[tracing::instrument]
pub fn boards(&self) -> crate::boards::AsyncBoards<'_> {
crate::boards::AsyncBoards::new(self)
}
#[tracing::instrument]
pub fn attachments(&self) -> crate::attachments::AsyncAttachments {
crate::attachments::AsyncAttachments::new(self)
}
#[tracing::instrument]
pub fn components(&self) -> crate::components::AsyncComponents {
crate::components::AsyncComponents::new(self)
}
#[tracing::instrument]
pub fn versions(&self) -> crate::versions::AsyncVersions {
crate::versions::AsyncVersions::new(self)
}
#[tracing::instrument]
pub fn sprints(&self) -> crate::sprints::AsyncSprints {
crate::sprints::AsyncSprints::new(self)
}
#[tracing::instrument]
pub fn transitions<K>(&self, issue_key: K) -> crate::transitions::AsyncTransitions
where
K: Into<String> + std::fmt::Debug,
{
crate::transitions::AsyncTransitions::new(self, issue_key)
}
#[tracing::instrument]
pub fn users(&self) -> crate::users::AsyncUsers {
crate::users::AsyncUsers::new(self)
}
#[tracing::instrument]
pub fn groups(&self) -> crate::groups::AsyncGroups {
crate::groups::AsyncGroups::new(self)
}
pub async fn session(&self) -> Result<Session> {
self.get("auth", "/session").await
}
#[cfg(feature = "cache")]
pub fn clear_cache(&self) {
self.core.clear_cache();
}
#[cfg(feature = "cache")]
pub fn cache_stats(&self) -> crate::cache::CacheStats {
self.core.cache_stats()
}
#[cfg(any(feature = "metrics", feature = "cache"))]
pub fn observability_report(&self) -> crate::observability::ObservabilityReport {
let obs = self.create_observability_system();
obs.get_observability_report()
}
#[cfg(any(feature = "metrics", feature = "cache"))]
pub fn health_status(&self) -> crate::observability::HealthStatus {
let obs = self.create_observability_system();
obs.health_status()
}
#[cfg(any(feature = "metrics", feature = "cache"))]
fn create_observability_system(&self) -> crate::observability::ObservabilitySystem {
#[cfg(feature = "cache")]
{
crate::observability::ObservabilitySystem::with_cache(self.core.cache.clone())
}
#[cfg(not(feature = "cache"))]
{
crate::observability::ObservabilitySystem::new()
}
}
#[tracing::instrument]
pub async fn delete<D>(&self, api_name: &str, endpoint: &str) -> Result<D>
where
D: DeserializeOwned,
{
self.request::<D>(Method::DELETE, api_name, endpoint, None)
.await
}
#[tracing::instrument]
pub async fn get<D>(&self, api_name: &str, endpoint: &str) -> Result<D>
where
D: DeserializeOwned,
{
self.request::<D>(Method::GET, api_name, endpoint, None)
.await
}
#[tracing::instrument]
pub async fn get_versioned<D>(
&self,
api_name: &str,
version: Option<&str>,
endpoint: &str,
) -> Result<D>
where
D: DeserializeOwned,
{
self.request_versioned::<D>(Method::GET, api_name, version, endpoint, None)
.await
}
pub async fn post_versioned<D, S>(
&self,
api_name: &str,
version: Option<&str>,
endpoint: &str,
body: S,
) -> Result<D>
where
D: DeserializeOwned,
S: Serialize,
{
let data = self.core.prepare_json_body(body)?;
debug!("Json POST request sent with API version {:?}", version);
self.request_versioned::<D>(Method::POST, api_name, version, endpoint, Some(data))
.await
}
pub async fn get_bytes(&self, api_name: &str, endpoint: &str) -> Result<Vec<u8>> {
let ctx = RequestContext::new("GET", endpoint);
let _span = ctx.create_span().entered();
let url = self.core.build_url(api_name, endpoint)?;
debug!(
correlation_id = %ctx.correlation_id,
url = %url,
"Building request URL for bytes download"
);
let mut req = self
.client
.request(Method::GET, url)
.header("X-Correlation-ID", &ctx.correlation_id);
req = self.core.apply_credentials_async(req);
debug!(
correlation_id = %ctx.correlation_id,
"Sending bytes request"
);
let result = async {
let res = req.send().await?;
let status = res.status();
if !status.is_success() {
let response_body = res.text().await?;
debug!(
correlation_id = %ctx.correlation_id,
status = %status,
response_size = response_body.len(),
"Received error response"
);
return Err(match status {
reqwest::StatusCode::UNAUTHORIZED => Error::Unauthorized,
reqwest::StatusCode::METHOD_NOT_ALLOWED => Error::MethodNotAllowed,
reqwest::StatusCode::NOT_FOUND => Error::NotFound,
client_err if client_err.is_client_error() => Error::Fault {
code: status,
errors: serde_json::from_str(&response_body)?,
},
_ => Error::Fault {
code: status,
errors: serde_json::from_str(&response_body)?,
},
});
}
let bytes = res.bytes().await?.to_vec();
debug!(
correlation_id = %ctx.correlation_id,
status = %status,
bytes_size = bytes.len(),
"Received bytes response"
);
Ok(bytes)
}
.await;
let success = result.is_ok();
ctx.finish(success);
result
}
pub async fn post<D, S>(&self, api_name: &str, endpoint: &str, body: S) -> Result<D>
where
D: DeserializeOwned,
S: Serialize,
{
let data = self.core.prepare_json_body(body)?;
debug!("Json POST request sent");
self.request::<D>(Method::POST, api_name, endpoint, Some(data))
.await
}
pub async fn put<D, S>(&self, api_name: &str, endpoint: &str, body: S) -> Result<D>
where
D: DeserializeOwned,
S: Serialize,
{
let data = self.core.prepare_json_body(body)?;
debug!("Json PUT request sent");
self.request::<D>(Method::PUT, api_name, endpoint, Some(data))
.await
}
pub async fn post_multipart<D>(
&self,
api_name: &str,
endpoint: &str,
form: reqwest::multipart::Form,
) -> Result<D>
where
D: DeserializeOwned,
{
let ctx = RequestContext::new("POST", endpoint);
let _span = ctx.create_span().entered();
let url = self.core.build_url(api_name, endpoint)?;
debug!(
correlation_id = %ctx.correlation_id,
url = %url,
"Building async multipart request URL"
);
#[cfg(feature = "oauth")]
let oauth_header = self.core.get_oauth_header("POST", url.as_str())?;
let mut req = self
.client
.request(Method::POST, url)
.header("X-Atlassian-Token", "no-check")
.header("X-Correlation-ID", &ctx.correlation_id)
.multipart(form);
#[cfg(feature = "oauth")]
if let Some(header) = oauth_header {
req = req.header(reqwest::header::AUTHORIZATION, header);
}
req = self.core.apply_credentials_async(req);
debug!(
correlation_id = %ctx.correlation_id,
"Sending async multipart request"
);
async {
let res = req.send().await?;
let status = res.status();
let response_body = res.text().await?;
debug!(
correlation_id = %ctx.correlation_id,
status = %status,
response_size = response_body.len(),
"Received async response"
);
let response = self.core.process_response(status, &response_body)?;
ctx.finish(false);
Ok(response)
}
.await
}
#[tracing::instrument(skip(self, body))]
async fn request<D>(
&self,
method: Method,
api_name: &str,
endpoint: &str,
body: Option<Vec<u8>>,
) -> Result<D>
where
D: DeserializeOwned,
{
let ctx = RequestContext::new(method.as_ref(), endpoint);
let span = ctx.create_span();
let method_str = method.to_string();
async move {
#[cfg(feature = "cache")]
if method == Method::GET {
if let Some(cached_response) = self.core.check_cache::<D>(&method_str, endpoint) {
debug!(
correlation_id = %ctx.correlation_id,
endpoint = endpoint,
"Returning cached response"
);
ctx.finish(true);
return Ok(cached_response);
}
}
let url = self.core.build_url(api_name, endpoint)?;
debug!(
correlation_id = %ctx.correlation_id,
url = %url,
"Building request URL"
);
#[cfg(feature = "oauth")]
let oauth_header = self.core.get_oauth_header(method.as_str(), url.as_str())?;
let mut req = self
.client
.request(method.clone(), url)
.header(CONTENT_TYPE, "application/json")
.header("X-Correlation-ID", &ctx.correlation_id);
#[cfg(feature = "oauth")]
if let Some(header) = oauth_header {
req = req.header(reqwest::header::AUTHORIZATION, header);
}
req = self.core.apply_credentials_async(req);
if let Some(body) = body {
req = req.body(body);
}
debug!(
correlation_id = %ctx.correlation_id,
"Sending request"
);
let result = async {
let res = req.send().await?;
let status = res.status();
let response_body = res.text().await?;
debug!(
correlation_id = %ctx.correlation_id,
status = %status,
response_size = response_body.len(),
"Received response"
);
let response = self.core.process_response(status, &response_body)?;
#[cfg(feature = "cache")]
if status.is_success() && method == Method::GET {
self.core
.store_raw_response(&method_str, endpoint, &response_body);
}
Ok(response)
}
.await;
let success = result.is_ok();
ctx.finish(success);
result
}
.instrument(span)
.await
}
#[tracing::instrument(skip(self, body))]
async fn request_versioned<D>(
&self,
method: Method,
api_name: &str,
version: Option<&str>,
endpoint: &str,
body: Option<Vec<u8>>,
) -> Result<D>
where
D: DeserializeOwned,
{
let ctx = RequestContext::new(method.as_ref(), endpoint);
let span = ctx.create_span();
#[allow(unused_variables)]
let method_str = method.to_string();
let result = async {
#[cfg(feature = "cache")]
if method == Method::GET {
if let Some(cached_response) = self.core.check_cache::<D>(&method_str, endpoint) {
debug!(
correlation_id = %ctx.correlation_id,
endpoint = endpoint,
"Returning cached response"
);
ctx.finish(true);
return Ok(cached_response);
}
}
let url = self.core.build_versioned_url(api_name, version, endpoint)?;
debug!(
correlation_id = %ctx.correlation_id,
url = %url,
"Building versioned request URL"
);
let mut req = self
.client
.request(method.clone(), url)
.header(CONTENT_TYPE, "application/json")
.header("X-Correlation-ID", &ctx.correlation_id);
req = self.core.apply_credentials_async(req);
if let Some(body) = body {
req = req.body(body);
}
debug!(
correlation_id = %ctx.correlation_id,
"Sending versioned request"
);
let res = req.send().await?;
let status = res.status();
let response_body = res.text().await?;
debug!(
correlation_id = %ctx.correlation_id,
status = %status,
response_size = response_body.len(),
"Received versioned response"
);
let response = self.core.process_response(status, &response_body)?;
#[cfg(feature = "cache")]
if status.is_success() && method == Method::GET {
self.core
.store_raw_response(&method_str, endpoint, &response_body);
}
Ok(response)
}
.instrument(span)
.await;
let success = result.is_ok();
ctx.finish(success);
result
}
}
impl From<&Jira> for crate::sync::Jira {
fn from(async_jira: &Jira) -> Self {
if let Ok(jira) = crate::sync::Jira::with_core(async_jira.core.clone()) {
jira
} else {
crate::sync::Jira::new("http://localhost", Credentials::Anonymous).unwrap()
}
}
}