darkstrata_credential_check/
types.rs1use chrono::{DateTime, NaiveDate, Utc};
4use serde::{Deserialize, Serialize};
5use std::time::Duration;
6
7#[derive(Debug, Clone)]
9pub struct ClientOptions {
10 pub api_key: String,
12 pub base_url: Option<String>,
14 pub timeout: Option<Duration>,
16 pub retries: Option<u32>,
18 pub enable_caching: Option<bool>,
20 pub cache_ttl: Option<Duration>,
22}
23
24impl ClientOptions {
25 pub fn new(api_key: impl Into<String>) -> Self {
27 Self {
28 api_key: api_key.into(),
29 base_url: None,
30 timeout: None,
31 retries: None,
32 enable_caching: None,
33 cache_ttl: None,
34 }
35 }
36
37 pub fn base_url(mut self, url: impl Into<String>) -> Self {
39 self.base_url = Some(url.into());
40 self
41 }
42
43 pub fn timeout(mut self, timeout: Duration) -> Self {
45 self.timeout = Some(timeout);
46 self
47 }
48
49 pub fn retries(mut self, retries: u32) -> Self {
51 self.retries = Some(retries);
52 self
53 }
54
55 pub fn enable_caching(mut self, enable: bool) -> Self {
57 self.enable_caching = Some(enable);
58 self
59 }
60
61 pub fn cache_ttl(mut self, ttl: Duration) -> Self {
63 self.cache_ttl = Some(ttl);
64 self
65 }
66}
67
68#[derive(Debug, Clone)]
70pub(crate) struct ResolvedConfig {
71 pub api_key: String,
72 pub base_url: String,
73 pub timeout: Duration,
74 pub retries: u32,
75 pub enable_caching: bool,
76 pub cache_ttl: Duration,
77}
78
79#[derive(Debug, Clone, Default)]
81pub struct CheckOptions {
82 pub client_hmac: Option<String>,
85 pub since: Option<SinceFilter>,
88}
89
90impl CheckOptions {
91 pub fn new() -> Self {
93 Self::default()
94 }
95
96 pub fn client_hmac(mut self, hmac: impl Into<String>) -> Self {
98 self.client_hmac = Some(hmac.into());
99 self
100 }
101
102 pub fn since_datetime(mut self, datetime: DateTime<Utc>) -> Self {
104 self.since = Some(SinceFilter::DateTime(datetime));
105 self
106 }
107
108 pub fn since_date(mut self, date: NaiveDate) -> Self {
110 self.since = Some(SinceFilter::Date(date));
111 self
112 }
113
114 pub fn since_epoch_day(mut self, epoch_day: u32) -> Self {
116 self.since = Some(SinceFilter::EpochDay(epoch_day));
117 self
118 }
119
120 pub fn since_timestamp(mut self, timestamp: i64) -> Self {
122 self.since = Some(SinceFilter::Timestamp(timestamp));
123 self
124 }
125}
126
127#[derive(Debug, Clone)]
129pub enum SinceFilter {
130 DateTime(DateTime<Utc>),
132 Date(NaiveDate),
134 EpochDay(u32),
136 Timestamp(i64),
138}
139
140impl SinceFilter {
141 pub fn to_epoch_day(&self) -> u32 {
143 match self {
144 SinceFilter::DateTime(dt) => (dt.timestamp() / 86400) as u32,
145 SinceFilter::Date(date) => {
146 let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap();
147 date.signed_duration_since(epoch).num_days() as u32
148 }
149 SinceFilter::EpochDay(day) => *day,
150 SinceFilter::Timestamp(ts) => (*ts / 86400) as u32,
151 }
152 }
153}
154
155#[derive(Debug, Clone)]
157pub struct Credential {
158 pub email: String,
160 pub password: String,
162}
163
164impl Credential {
165 pub fn new(email: impl Into<String>, password: impl Into<String>) -> Self {
167 Self {
168 email: email.into(),
169 password: password.into(),
170 }
171 }
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct CheckResult {
177 pub found: bool,
179 pub credential: CredentialInfo,
181 pub metadata: CheckMetadata,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct CredentialInfo {
188 pub email: String,
190 pub masked: bool,
192}
193
194impl CredentialInfo {
195 pub fn new(email: impl Into<String>) -> Self {
197 Self {
198 email: email.into(),
199 masked: true,
200 }
201 }
202
203 pub fn masked() -> Self {
205 Self {
206 email: "[hash]".to_string(),
207 masked: true,
208 }
209 }
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct CheckMetadata {
215 pub prefix: String,
217 pub total_results: usize,
219 pub hmac_source: HmacSource,
221 #[serde(skip_serializing_if = "Option::is_none")]
223 pub time_window: Option<i64>,
224 #[serde(skip_serializing_if = "Option::is_none")]
226 pub filter_since: Option<u32>,
227 pub cached_result: bool,
229 pub checked_at: DateTime<Utc>,
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
235#[serde(rename_all = "lowercase")]
236pub enum HmacSource {
237 Server,
239 Client,
241}
242
243impl std::fmt::Display for HmacSource {
244 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245 match self {
246 HmacSource::Server => write!(f, "server"),
247 HmacSource::Client => write!(f, "client"),
248 }
249 }
250}
251
252impl std::str::FromStr for HmacSource {
253 type Err = String;
254
255 fn from_str(s: &str) -> Result<Self, Self::Err> {
256 match s.to_lowercase().as_str() {
257 "server" => Ok(HmacSource::Server),
258 "client" => Ok(HmacSource::Client),
259 _ => Err(format!("Invalid HMAC source: {}", s)),
260 }
261 }
262}
263
264#[derive(Debug, Clone)]
266pub(crate) struct ApiResponse {
267 pub hashes: Vec<String>,
269 #[allow(dead_code)]
271 pub prefix: String,
272 pub hmac_key: String,
274 pub hmac_source: HmacSource,
276 pub time_window: Option<i64>,
278 pub filter_since: Option<u32>,
280}
281
282#[derive(Debug, Clone)]
284pub(crate) struct HashedCredential {
285 pub credential: Option<Credential>,
287 pub hash: String,
289 pub prefix: String,
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn test_client_options_builder() {
299 let options = ClientOptions::new("test-key")
300 .base_url("https://custom.api.com/")
301 .timeout(Duration::from_secs(60))
302 .retries(5)
303 .enable_caching(false)
304 .cache_ttl(Duration::from_secs(1800));
305
306 assert_eq!(options.api_key, "test-key");
307 assert_eq!(
308 options.base_url,
309 Some("https://custom.api.com/".to_string())
310 );
311 assert_eq!(options.timeout, Some(Duration::from_secs(60)));
312 assert_eq!(options.retries, Some(5));
313 assert_eq!(options.enable_caching, Some(false));
314 assert_eq!(options.cache_ttl, Some(Duration::from_secs(1800)));
315 }
316
317 #[test]
318 fn test_check_options_builder() {
319 let options = CheckOptions::new()
320 .client_hmac("abc123")
321 .since_epoch_day(19724);
322
323 assert_eq!(options.client_hmac, Some("abc123".to_string()));
324 assert!(matches!(options.since, Some(SinceFilter::EpochDay(19724))));
325 }
326
327 #[test]
328 fn test_since_filter_to_epoch_day() {
329 let filter = SinceFilter::EpochDay(19723);
331 assert_eq!(filter.to_epoch_day(), 19723);
332
333 let filter = SinceFilter::Timestamp(1704067200);
336 assert_eq!(filter.to_epoch_day(), 19723);
337 }
338
339 #[test]
340 fn test_hmac_source_parsing() {
341 assert_eq!("server".parse::<HmacSource>().unwrap(), HmacSource::Server);
342 assert_eq!("client".parse::<HmacSource>().unwrap(), HmacSource::Client);
343 assert_eq!("SERVER".parse::<HmacSource>().unwrap(), HmacSource::Server);
344 assert!("invalid".parse::<HmacSource>().is_err());
345 }
346
347 #[test]
348 fn test_credential_info() {
349 let info = CredentialInfo::new("test@example.com");
350 assert_eq!(info.email, "test@example.com");
351 assert!(info.masked);
352
353 let masked = CredentialInfo::masked();
354 assert_eq!(masked.email, "[hash]");
355 assert!(masked.masked);
356 }
357}