1use std::time::Duration;
37
38use reqwest::{
39 Url,
40 header::{HeaderMap, HeaderName, HeaderValue},
41};
42use serde::{Deserialize, Serialize};
43
44mod auth;
45mod builders;
46mod client;
47mod error;
48
49pub use crate::auth::Credentials;
50pub use builders::{
51 NoteBuilder, TicketClient, TicketCreateBuilder, TicketSearchBuilder, TicketStatus,
52 TicketsClient,
53};
54pub use client::{
55 Account, Attachment, Condition, CreateTicketData, Criteria, DetailedTicket, EditTicketData,
56 LogicalOp, Note, NoteData, Priority, Resolution, Status, TemplateInfo, TicketData, TimeEntry,
57 UserInfo,
58};
59pub use error::Error;
60
61#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Hash, Default)]
63pub struct UserID(pub String);
64
65#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
70pub struct TicketID(pub u64);
71
72#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
77pub struct NoteID(pub u64);
78
79struct StringOrNumberU64Visitor;
81
82impl<'de> serde::de::Visitor<'de> for StringOrNumberU64Visitor {
83 type Value = u64;
84
85 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
86 formatter.write_str("a u64 or a string containing a u64")
87 }
88
89 fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<u64, E> {
90 Ok(v)
91 }
92
93 fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<u64, E> {
94 u64::try_from(v).map_err(|_| E::custom(format!("negative id: {v}")))
95 }
96
97 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<u64, E> {
98 v.parse::<u64>().map_err(serde::de::Error::custom)
99 }
100}
101
102impl<'de> Deserialize<'de> for TicketID {
103 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
104 deserializer
105 .deserialize_any(StringOrNumberU64Visitor)
106 .map(TicketID)
107 }
108}
109
110impl<'de> Deserialize<'de> for NoteID {
111 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
112 deserializer
113 .deserialize_any(StringOrNumberU64Visitor)
114 .map(NoteID)
115 }
116}
117
118impl From<u64> for NoteID {
119 fn from(value: u64) -> Self {
120 NoteID(value)
121 }
122}
123
124impl From<NoteID> for u64 {
125 fn from(value: NoteID) -> Self {
126 value.0
127 }
128}
129
130impl std::fmt::Display for NoteID {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 write!(f, "{}", self.0)
133 }
134}
135
136impl From<u64> for TicketID {
137 fn from(value: u64) -> Self {
138 TicketID(value)
139 }
140}
141
142impl From<TicketID> for u64 {
143 fn from(value: TicketID) -> Self {
144 value.0
145 }
146}
147
148impl From<&TicketID> for u64 {
149 fn from(value: &TicketID) -> Self {
150 value.0
151 }
152}
153
154impl From<&UserID> for String {
155 fn from(value: &UserID) -> Self {
156 value.0.clone()
157 }
158}
159
160impl From<String> for UserID {
161 fn from(value: String) -> Self {
162 UserID(value)
163 }
164}
165
166impl From<&str> for UserID {
167 fn from(value: &str) -> Self {
168 UserID(value.to_string())
169 }
170}
171
172impl From<u32> for UserID {
173 fn from(value: u32) -> Self {
174 UserID(value.to_string())
175 }
176}
177
178impl From<UserID> for u32 {
179 fn from(value: UserID) -> Self {
180 value.0.parse().unwrap_or_default()
181 }
182}
183
184impl std::fmt::Display for TicketID {
185 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186 write!(f, "{}", self.0)
187 }
188}
189
190impl std::fmt::Display for UserID {
191 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 write!(f, "{}", self.0)
193 }
194}
195
196#[derive(Clone)]
201pub struct ServiceDesk {
202 base_url: Url,
203 inner: reqwest::Client,
204}
205
206#[derive(Clone, Debug, PartialEq, Eq)]
210pub enum Security {
211 Unsafe,
212 NativeTLS,
213}
214
215#[derive(Clone, Debug)]
217pub struct ServiceDeskOptions {
218 pub user_agent: Option<String>,
219 pub timeout: Option<Duration>,
221 pub security: Option<Security>,
222 pub default_headers: Option<HeaderMap>,
223}
224
225static SDP_HEADER: (HeaderName, HeaderValue) = (
226 HeaderName::from_static("accept"),
227 HeaderValue::from_static("application/vnd.manageengine.sdp.v3+json"),
228);
229
230impl Default for ServiceDeskOptions {
231 fn default() -> Self {
232 ServiceDeskOptions {
233 user_agent: Some(String::from("servicedesk-rs/0.1.0")),
234 timeout: Some(Duration::from_secs(5)),
235 security: Some(Security::Unsafe),
236 default_headers: Some(HeaderMap::from_iter(vec![SDP_HEADER.clone()])),
237 }
238 }
239}
240
241impl ServiceDesk {
242 pub fn new(
249 base_url: Url,
250 credentials: Credentials,
251 options: ServiceDeskOptions,
252 ) -> Result<Self, Error> {
253 let mut headers = options.default_headers.unwrap_or_default();
254
255 if let Credentials::Token { ref token } = credentials {
256 let value = HeaderValue::from_str(token)
257 .map_err(|e| Error::Other(format!("invalid auth token header value: {e}")))?;
258 headers.insert("authtoken", value);
259 }
260
261 let mut builder = reqwest::ClientBuilder::new()
262 .default_headers(headers)
263 .user_agent(options.user_agent.unwrap_or_default())
264 .timeout(options.timeout.unwrap_or_else(|| Duration::from_secs(5)));
265
266 if let Some(security) = options.security {
267 match security {
268 Security::Unsafe => {
269 builder = builder.danger_accept_invalid_certs(true);
270 }
271 Security::NativeTLS => {}
272 }
273 }
274
275 let inner = builder
276 .build()
277 .map_err(|e| Error::Other(format!("failed to build HTTP client: {e}")))?;
278
279 Ok(ServiceDesk { base_url, inner })
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn service_desk_options_default() {
289 let opts = ServiceDeskOptions::default();
290 assert_eq!(opts.user_agent, Some("servicedesk-rs/0.1.0".to_string()));
291 assert_eq!(opts.timeout, Some(Duration::from_secs(5)));
292 assert!(matches!(opts.security, Some(Security::Unsafe)));
293 assert!(opts.default_headers.is_some());
294 }
295}