use std::path::Path;
use chrono::{DateTime, Local};
use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{
Priority, ServiceDesk, TicketID, UserInfo,
client::{
Condition, CreateTicketData, Criteria, DetailedTicket, EditTicketData, ListInfo, LogicalOp,
Note, NoteData, SearchRequest, TicketData, TicketSearchResponse,
},
error::Error,
};
pub struct TicketsClient<'a> {
pub(crate) client: &'a ServiceDesk,
}
impl<'a> TicketsClient<'a> {
#[must_use]
pub fn search(self) -> TicketSearchBuilder<'a> {
TicketSearchBuilder {
client: self.client,
root_criteria: None,
children: vec![],
row_count: 100,
}
}
#[must_use]
pub fn create(self) -> TicketCreateBuilder<'a> {
TicketCreateBuilder {
client: self.client,
subject: None,
description: None,
requester: None,
priority: Priority::low(),
account: None,
template: None,
udf_fields: None,
}
}
}
pub struct TicketClient<'a> {
pub(crate) client: &'a ServiceDesk,
pub(crate) id: TicketID,
}
impl<'a> TicketClient<'a> {
pub async fn get(&self) -> Result<DetailedTicket, Error> {
self.client.ticket_details(self.id).await
}
pub async fn close(&self, comment: &str) -> Result<(), Error> {
self.client.close_ticket(self.id, comment).await
}
pub async fn assign(&self, technician: &str) -> Result<(), Error> {
self.client.assign_ticket(self.id, technician).await
}
pub async fn conversations(&self) -> Result<Value, Error> {
self.client.get_conversations(self.id).await
}
pub async fn conversation_content(&self, content_url: &str) -> Result<Value, Error> {
self.client.get_conversation_content(content_url).await
}
pub async fn add_attachment(&self, file_path: impl AsRef<Path>) -> Result<(), Error> {
self.client.add_attachment(self.id, file_path).await
}
pub async fn all_attachment_links(&self) -> Result<Vec<String>, Error> {
let ticket = self.client.ticket(self.id).get().await?;
let mut links = Vec::new();
if let Some(attachments) = ticket.attachments {
for attachment in attachments {
links.push(format!(
"{}{}",
self.client.base_url, attachment.content_url
));
}
}
if let Ok(attachments) = self.client.get_conversation_attachment_urls(self.id).await {
for url in attachments {
links.push(url);
}
}
Ok(links)
}
pub async fn add_note(&self, description: &str) -> Result<Note, Error> {
self.client
.add_note(
self.id,
&NoteData {
description: description.to_string(),
..Default::default()
},
)
.await
}
pub async fn add_worklog(&self, worklog: &WorklogData) -> Result<Value, Error> {
self.client.add_worklog(self.id, worklog).await
}
#[must_use]
pub fn note(&self) -> NoteBuilder<'a> {
NoteBuilder {
client: self.client,
id: self.id,
description: String::new(),
mark_first_response: false,
add_to_linked_requests: false,
notify_technician: false,
show_to_requester: false,
}
}
#[must_use]
pub fn worklog(&self) -> WorklogBuilder<'a> {
WorklogBuilder {
client: self.client,
id: self.id,
owner: None,
description: None,
start_time: None,
end_time: None,
exchange_rate: None,
mark_first_response: None,
include_nonoperational_hours: None,
}
}
pub async fn merge(&self, ticket_ids: &[TicketID]) -> Result<(), Error> {
self.client.merge(self.id, ticket_ids).await
}
pub async fn merged_ticket_ids(&self) -> Result<Vec<TicketID>, Error> {
self.client.merged_ticket_ids(self.id).await
}
pub async fn edit(&self, data: &EditTicketData) -> Result<(), Error> {
self.client.edit(self.id, data).await
}
pub async fn close_with_note(&self, comment: &str) -> Result<(), Error> {
self.client
.add_note(
self.id,
&NoteData {
description: comment.to_string(),
..Default::default()
},
)
.await?;
self.client.close_ticket(self.id, comment).await
}
}
pub struct TicketSearchBuilder<'a> {
client: &'a ServiceDesk,
root_criteria: Option<Criteria>,
children: Vec<Criteria>,
row_count: u32,
}
#[derive(Debug, PartialEq, Eq)]
pub enum TicketStatus {
Open,
Closed,
Cancelled,
OnHold,
}
impl std::fmt::Display for TicketStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let status_str = match self {
TicketStatus::Open => "Open",
TicketStatus::Closed => "Closed",
TicketStatus::Cancelled => "Cancelled",
TicketStatus::OnHold => "On Hold",
};
write!(f, "{status_str}")
}
}
impl TicketSearchBuilder<'_> {
#[must_use]
pub fn status(mut self, status: &str) -> Self {
self.root_criteria = Some(Criteria {
field: "status.name".to_string(),
condition: Condition::Is,
value: status.into(),
children: vec![],
logical_operator: None,
});
self
}
#[must_use]
pub fn filter(self, filter: &TicketStatus) -> Self {
self.status(&filter.to_string())
}
#[must_use]
pub fn open(self) -> Self {
self.status("Open")
}
#[must_use]
pub fn closed(self) -> Self {
self.status("Closed")
}
#[must_use]
pub fn created_after(mut self, time: DateTime<Local>) -> Self {
self.children.push(Criteria {
field: "created_time".to_string(),
condition: Condition::GreaterThan,
value: time.timestamp_millis().to_string().into(),
children: vec![],
logical_operator: Some(LogicalOp::And),
});
self
}
#[must_use]
pub fn updated_after(mut self, time: DateTime<Local>) -> Self {
self.children.push(Criteria {
field: "last_updated_time".to_string(),
condition: Condition::GreaterThan,
value: time.timestamp_millis().to_string().into(),
children: vec![],
logical_operator: Some(LogicalOp::And),
});
self
}
#[must_use]
pub fn subject_contains(mut self, value: &str) -> Self {
self.children.push(Criteria {
field: "subject".to_string(),
condition: Condition::Contains,
value: value.into(),
children: vec![],
logical_operator: Some(LogicalOp::And),
});
self
}
pub fn field_contains(mut self, field: &str, value: impl Into<Value>) -> Self {
self.children.push(Criteria {
field: field.to_string(),
condition: Condition::Contains,
value: value.into(),
children: vec![],
logical_operator: Some(LogicalOp::And),
});
self
}
pub fn field_equals(mut self, field: &str, value: impl Into<Value>) -> Self {
self.children.push(Criteria {
field: field.to_string(),
condition: Condition::Is,
value: value.into(),
children: vec![],
logical_operator: Some(LogicalOp::And),
});
self
}
#[must_use]
pub fn limit(mut self, count: u32) -> Self {
self.row_count = count;
self
}
#[must_use]
pub fn criteria(mut self, criteria: Criteria) -> Self {
if self.root_criteria.is_none() {
self.root_criteria = Some(criteria);
} else {
self.children.push(criteria);
}
self
}
pub async fn fetch(self) -> Result<Vec<DetailedTicket>, Error> {
let mut root = self.root_criteria.unwrap_or_else(|| Criteria {
field: "id".to_string(),
condition: Condition::GreaterThan,
value: "0".into(),
children: vec![],
logical_operator: None,
});
root.children = self.children;
let body = SearchRequest {
list_info: ListInfo {
row_count: self.row_count,
search_criteria: root,
},
};
let resp: Value = self
.client
.request_input_data(Method::GET, "/api/v3/requests", &body)
.await?;
let ticket_response: TicketSearchResponse = serde_json::from_value(resp)?;
Ok(ticket_response.requests)
}
pub async fn first(mut self) -> Result<Option<DetailedTicket>, Error> {
self.row_count = 1;
let results = self.fetch().await?;
Ok(results.into_iter().next())
}
}
pub struct TicketCreateBuilder<'a> {
client: &'a ServiceDesk,
subject: Option<String>,
description: Option<String>,
requester: Option<String>,
priority: Priority,
account: Option<String>,
template: Option<String>,
udf_fields: Option<Value>,
}
impl TicketCreateBuilder<'_> {
pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.subject = Some(subject.into());
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn requester(mut self, requester: impl Into<String>) -> Self {
self.requester = Some(requester.into());
self
}
#[must_use]
pub fn priority(mut self, priority: Priority) -> Self {
self.priority = priority;
self
}
pub fn account(mut self, account: impl Into<String>) -> Self {
self.account = Some(account.into());
self
}
pub fn template(mut self, template: impl Into<String>) -> Self {
self.template = Some(template.into());
self
}
#[must_use]
pub fn udf_fields(mut self, fields: Value) -> Self {
self.udf_fields = Some(fields);
self
}
pub async fn send(self) -> Result<TicketData, Error> {
let subject = self
.subject
.ok_or_else(|| Error::Other("subject is required".to_string()))?;
let requester = self
.requester
.ok_or_else(|| Error::Other("requester is required".to_string()))?;
let data = CreateTicketData {
subject,
description: self.description.unwrap_or_default(),
requester,
priority: self.priority,
account: self.account.unwrap_or_default(),
template: self.template.unwrap_or_default(),
udf_fields: self.udf_fields.unwrap_or(serde_json::json!({})),
};
self.client.create_ticket(&data).await
}
}
pub struct NoteBuilder<'a> {
client: &'a ServiceDesk,
id: TicketID,
description: String,
mark_first_response: bool,
add_to_linked_requests: bool,
notify_technician: bool,
show_to_requester: bool,
}
impl NoteBuilder<'_> {
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
#[must_use]
pub fn mark_first_response(mut self) -> Self {
self.mark_first_response = true;
self
}
#[must_use]
pub fn add_to_linked_requests(mut self) -> Self {
self.add_to_linked_requests = true;
self
}
#[must_use]
pub fn notify_technician(mut self) -> Self {
self.notify_technician = true;
self
}
#[must_use]
pub fn show_to_requester(mut self) -> Self {
self.show_to_requester = true;
self
}
#[must_use]
pub fn build(self) -> NoteData {
NoteData {
description: self.description,
mark_first_response: self.mark_first_response,
add_to_linked_requests: self.add_to_linked_requests,
notify_technician: self.notify_technician,
show_to_requester: self.show_to_requester,
}
}
pub async fn send(self) -> Result<Note, Error> {
let client = self.client;
let id = self.id;
let note = self.build();
client.add_note(id, ¬e).await
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WorklogData {
owner: UserInfo,
description: String,
#[serde(serialize_with = "serialize_sdp_time")]
start_time: DateTime<Local>,
#[serde(serialize_with = "serialize_sdp_time")]
end_time: DateTime<Local>,
#[serde(skip_serializing_if = "Option::is_none")]
exchange_rate: Option<f64>,
mark_first_response: bool,
include_nonoperational_hours: bool,
}
fn serialize_sdp_time<S>(dt: &DateTime<Local>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut s = serializer.serialize_struct("SdpTime", 1)?;
s.serialize_field("value", &dt.timestamp_millis())?;
s.end()
}
pub struct WorklogBuilder<'a> {
client: &'a ServiceDesk,
id: TicketID,
owner: Option<UserInfo>,
description: Option<String>,
start_time: Option<DateTime<Local>>,
end_time: Option<DateTime<Local>>,
exchange_rate: Option<f64>,
mark_first_response: Option<bool>,
include_nonoperational_hours: Option<bool>,
}
impl WorklogBuilder<'_> {
#[must_use]
pub fn owner(mut self, owner: UserInfo) -> Self {
self.owner = Some(owner);
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn start_time(mut self, start_time: DateTime<Local>) -> Self {
self.start_time = Some(start_time);
self
}
#[must_use]
pub fn end_time(mut self, end_time: DateTime<Local>) -> Self {
self.end_time = Some(end_time);
self
}
#[must_use]
pub fn exchange_rate(mut self, exchange_rate: f64) -> Self {
self.exchange_rate = Some(exchange_rate);
self
}
#[must_use]
pub fn mark_first_response(mut self) -> Self {
self.mark_first_response = Some(true);
self
}
#[must_use]
pub fn include_nonoperational_hours(mut self) -> Self {
self.include_nonoperational_hours = Some(true);
self
}
pub fn build(self) -> Result<WorklogData, Error> {
Ok(WorklogData {
owner: self
.owner
.ok_or_else(|| Error::FieldRequired("owner".to_string()))?,
description: self.description.unwrap_or_default(),
start_time: self.start_time.unwrap_or_else(Local::now),
end_time: self.end_time.unwrap_or_else(Local::now),
exchange_rate: self.exchange_rate,
mark_first_response: self.mark_first_response.unwrap_or(false),
include_nonoperational_hours: self.include_nonoperational_hours.unwrap_or(false),
})
}
pub async fn send(self) -> Result<Value, Error> {
let client = self.client;
let id = self.id;
let worklog = self.build()?;
client.add_worklog(id, &worklog).await
}
}
impl ServiceDesk {
#[must_use]
pub fn tickets(&self) -> TicketsClient<'_> {
TicketsClient { client: self }
}
pub fn ticket(&self, id: impl Into<TicketID>) -> TicketClient<'_> {
TicketClient {
client: self,
id: id.into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ticket_status_display() {
assert_eq!(TicketStatus::Open.to_string(), "Open");
assert_eq!(TicketStatus::Closed.to_string(), "Closed");
assert_eq!(TicketStatus::Cancelled.to_string(), "Cancelled");
assert_eq!(TicketStatus::OnHold.to_string(), "On Hold");
}
}