Skip to main content

sdp_request_client/
lib.rs

1//! Async Rust client for the ManageEngine ServiceDesk Plus REST API v3.
2//!
3//! # Quick Start
4//!
5//! ```no_run
6//! use sdp_request_client::{ServiceDesk, ServiceDeskOptions, Credentials};
7//! use reqwest::Url;
8//!
9//! # async fn example() -> Result<(), sdp_request_client::Error> {
10//! let client = ServiceDesk::new(
11//!     Url::parse("https://sdp.example.com")?,
12//!     Credentials::Token { token: "your-token".into() },
13//!     ServiceDeskOptions::default(),
14//! );
15//!
16//! // Search tickets
17//! let tickets = client.tickets().search().open().limit(10).fetch().await?;
18//!
19//! // Create a ticket
20//! let response = client.tickets()
21//!     .create()
22//!     .subject("Server issue")
23//!     .requester("John Doe")
24//!     .send()
25//!     .await?;
26//!
27//! // Ticket operations
28//! client.ticket(12345).add_note("Investigating...").await?;
29//! client.ticket(12345).close("Resolved").await?;
30//! # Ok(())
31//! # }
32//! ```
33//!
34//! See [`ServiceDesk`] for the main entry point.
35
36use chrono::Duration;
37use reqwest::{
38    Url,
39    header::{HeaderMap, HeaderName, HeaderValue},
40};
41use serde::{Deserialize, Serialize};
42
43mod auth;
44mod builders;
45mod client;
46mod error;
47
48pub use crate::auth::Credentials;
49pub use builders::{
50    NoteBuilder, TicketClient, TicketCreateBuilder, TicketSearchBuilder, TicketStatus,
51    TicketsClient,
52};
53pub use client::{
54    Attachment, Condition, CreateTicketData, Criteria, DetailedTicket, EditTicketData, LogicalOp,
55    NameWrapper, Note, NoteData, NoteResponse, Priority, Resolution, SizeInfo, Status, TicketData,
56    TicketResponse, TimeEntry, UserInfo,
57};
58pub use error::{Error, SdpErrorCode};
59
60/// Type-safe wrapper for User ID in SDP
61#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Hash, Default)]
62pub struct UserID(pub String);
63
64/// Type-safe wrapper for Ticket ID in SDP
65#[derive(Clone, Debug)]
66pub struct TicketID(pub u64);
67
68/// Type-safe wrapper for Note ID in SDP
69#[derive(Clone, Debug)]
70pub struct NoteID(pub u64);
71
72impl From<u64> for NoteID {
73    fn from(value: u64) -> Self {
74        NoteID(value)
75    }
76}
77
78impl From<NoteID> for u64 {
79    fn from(value: NoteID) -> Self {
80        value.0
81    }
82}
83
84impl std::fmt::Display for NoteID {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        write!(f, "{}", self.0)
87    }
88}
89
90impl From<u64> for TicketID {
91    fn from(value: u64) -> Self {
92        TicketID(value)
93    }
94}
95
96impl From<TicketID> for u64 {
97    fn from(value: TicketID) -> Self {
98        value.0
99    }
100}
101
102impl From<&TicketID> for u64 {
103    fn from(value: &TicketID) -> Self {
104        value.0
105    }
106}
107
108impl From<&UserID> for String {
109    fn from(value: &UserID) -> Self {
110        value.0.clone()
111    }
112}
113
114impl From<String> for UserID {
115    fn from(value: String) -> Self {
116        UserID(value)
117    }
118}
119
120impl From<&str> for UserID {
121    fn from(value: &str) -> Self {
122        UserID(value.to_string())
123    }
124}
125
126impl From<u32> for UserID {
127    fn from(value: u32) -> Self {
128        UserID(value.to_string())
129    }
130}
131
132impl From<UserID> for u32 {
133    fn from(value: UserID) -> Self {
134        value.0.parse().unwrap_or_default()
135    }
136}
137
138impl std::fmt::Display for TicketID {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        write!(f, "{}", self.0)
141    }
142}
143
144impl std::fmt::Display for UserID {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        write!(f, "{}", self.0)
147    }
148}
149
150/// Main client for interacting with ServiceDesk Plus API.
151///
152/// Use [`tickets()`](Self::tickets) for search/create operations,
153/// or [`ticket(id)`](Self::ticket) for single-ticket operations.
154#[derive(Clone)]
155pub struct ServiceDesk {
156    pub base_url: Url,
157    pub credentials: Credentials,
158    inner: reqwest::Client,
159}
160
161/// Security options for the ServiceDesk client
162/// Not finished yet!!
163#[derive(Clone, Debug)]
164pub enum Security {
165    Unsafe,
166    NativeTlS,
167}
168
169/// Configuration options for the ServiceDesk client
170#[derive(Clone, Debug)]
171pub struct ServiceDeskOptions {
172    pub user_agent: Option<String>,
173    /// Request timeout duration
174    pub timeout: Option<Duration>,
175    pub security: Option<Security>,
176    pub default_headers: Option<HeaderMap>,
177}
178
179static SDP_HEADER: (HeaderName, HeaderValue) = (
180    HeaderName::from_static("accept"),
181    HeaderValue::from_static("application/vnd.manageengine.sdp.v3+json"),
182);
183
184impl Default for ServiceDeskOptions {
185    fn default() -> Self {
186        ServiceDeskOptions {
187            user_agent: Some(String::from("servicedesk-rs/0.1.0")),
188            timeout: Some(Duration::seconds(5)),
189            security: Some(Security::Unsafe),
190            default_headers: Some(HeaderMap::from_iter(vec![SDP_HEADER.clone()])),
191        }
192    }
193}
194
195impl ServiceDesk {
196    /// Create a new ServiceDesk client instance
197    pub fn new(base_url: Url, credentials: Credentials, options: ServiceDeskOptions) -> Self {
198        let mut headers = options.default_headers.unwrap_or_default();
199
200        #[allow(clippy::single_match)]
201        match credentials {
202            Credentials::Token { ref token } => {
203                headers.insert("authtoken", HeaderValue::from_str(token).unwrap());
204            }
205            _ => {}
206        }
207        let mut inner = reqwest::ClientBuilder::new()
208            .default_headers(headers)
209            .user_agent(options.user_agent.unwrap_or_default())
210            .timeout(options.timeout.unwrap_or_default().to_std().unwrap());
211
212        if let Some(security) = options.security {
213            match security {
214                Security::Unsafe => {
215                    inner = inner.danger_accept_invalid_certs(true);
216                }
217                Security::NativeTlS => {
218                    // Default behavior, do nothing
219                }
220            }
221        };
222
223        let inner = inner.build().expect("failed to build sdp client");
224
225        ServiceDesk {
226            base_url,
227            credentials,
228            inner,
229        }
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn service_desk_options_default() {
239        let opts = ServiceDeskOptions::default();
240        assert_eq!(opts.user_agent, Some("servicedesk-rs/0.1.0".to_string()));
241        assert_eq!(opts.timeout, Some(Duration::seconds(5)));
242        assert!(matches!(opts.security, Some(Security::Unsafe)));
243        assert!(opts.default_headers.is_some());
244    }
245}