use std::time::Duration;
use reqwest::{
Url,
header::{HeaderMap, HeaderName, HeaderValue},
};
use serde::{Deserialize, Serialize};
mod auth;
mod builders;
mod client;
mod error;
pub use crate::auth::Credentials;
pub use builders::{
NoteBuilder, TicketClient, TicketCreateBuilder, TicketSearchBuilder, TicketStatus,
TicketsClient, WorklogBuilder,
};
pub use client::{
Account, Attachment, Condition, CreateTicketData, Criteria, DetailedTicket, EditTicketData,
LogicalOp, Note, NoteData, Priority, Resolution, Status, TemplateInfo, TicketData, TimeEntry,
UserInfo,
};
pub use error::Error;
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Hash, Default)]
pub struct UserID(pub String);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
pub struct TicketID(pub u64);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
pub struct NoteID(pub u64);
struct StringOrNumberU64Visitor;
impl serde::de::Visitor<'_> for StringOrNumberU64Visitor {
type Value = u64;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a u64 or a string containing a u64")
}
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<u64, E> {
Ok(v)
}
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<u64, E> {
u64::try_from(v).map_err(|_| E::custom(format!("negative id: {v}")))
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<u64, E> {
v.parse::<u64>().map_err(serde::de::Error::custom)
}
}
impl<'de> Deserialize<'de> for TicketID {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer
.deserialize_any(StringOrNumberU64Visitor)
.map(TicketID)
}
}
impl<'de> Deserialize<'de> for NoteID {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer
.deserialize_any(StringOrNumberU64Visitor)
.map(NoteID)
}
}
impl From<u64> for NoteID {
fn from(value: u64) -> Self {
NoteID(value)
}
}
impl From<NoteID> for u64 {
fn from(value: NoteID) -> Self {
value.0
}
}
impl std::fmt::Display for NoteID {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<u64> for TicketID {
fn from(value: u64) -> Self {
TicketID(value)
}
}
impl From<TicketID> for u64 {
fn from(value: TicketID) -> Self {
value.0
}
}
impl From<&TicketID> for u64 {
fn from(value: &TicketID) -> Self {
value.0
}
}
impl From<&UserID> for String {
fn from(value: &UserID) -> Self {
value.0.clone()
}
}
impl From<String> for UserID {
fn from(value: String) -> Self {
UserID(value)
}
}
impl From<&str> for UserID {
fn from(value: &str) -> Self {
UserID(value.to_string())
}
}
impl From<u32> for UserID {
fn from(value: u32) -> Self {
UserID(value.to_string())
}
}
impl From<UserID> for u32 {
fn from(value: UserID) -> Self {
value.0.parse().unwrap_or_default()
}
}
impl std::fmt::Display for TicketID {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::fmt::Display for UserID {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone)]
pub struct ServiceDesk {
base_url: Url,
inner: reqwest::Client,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Security {
Unsafe,
NativeTLS,
}
#[derive(Clone, Debug)]
pub struct ServiceDeskOptions {
pub user_agent: Option<String>,
pub timeout: Option<Duration>,
pub security: Option<Security>,
pub default_headers: Option<HeaderMap>,
}
static SDP_HEADER: (HeaderName, HeaderValue) = (
HeaderName::from_static("accept"),
HeaderValue::from_static("application/vnd.manageengine.sdp.v3+json"),
);
impl Default for ServiceDeskOptions {
fn default() -> Self {
ServiceDeskOptions {
user_agent: Some(String::from("servicedesk-rs/0.1.0")),
timeout: Some(Duration::from_secs(5)),
security: Some(Security::Unsafe),
default_headers: Some(HeaderMap::from_iter(vec![SDP_HEADER.clone()])),
}
}
}
impl ServiceDesk {
pub fn new(
base_url: Url,
credentials: Credentials,
options: ServiceDeskOptions,
) -> Result<Self, Error> {
let mut headers = options.default_headers.unwrap_or_default();
if let Credentials::Token { ref token } = credentials {
let value = HeaderValue::from_str(token)
.map_err(|e| Error::Other(format!("invalid auth token header value: {e}")))?;
headers.insert("authtoken", value);
}
let mut builder = reqwest::ClientBuilder::new()
.default_headers(headers)
.user_agent(options.user_agent.unwrap_or_default())
.timeout(options.timeout.unwrap_or_else(|| Duration::from_secs(5)));
if let Some(security) = options.security {
match security {
Security::Unsafe => {
builder = builder.danger_accept_invalid_certs(true);
}
Security::NativeTLS => {}
}
}
let inner = builder
.build()
.map_err(|e| Error::Other(format!("failed to build HTTP client: {e}")))?;
Ok(ServiceDesk { base_url, inner })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn service_desk_options_default() {
let opts = ServiceDeskOptions::default();
assert_eq!(opts.user_agent, Some("servicedesk-rs/0.1.0".to_string()));
assert_eq!(opts.timeout, Some(Duration::from_secs(5)));
assert!(matches!(opts.security, Some(Security::Unsafe)));
assert!(opts.default_headers.is_some());
}
}