chie_shared/types/
api.rs

1//! API-related types for CHIE Protocol.
2//!
3//! This module contains types used for API requests and responses:
4//! - Response wrappers (`ApiResponse`, `ApiError`, `PaginatedResponse`)
5//! - Proof submission results
6//! - Health check responses
7//! - User and reward types
8//! - Leaderboard entries
9
10#[cfg(feature = "schema")]
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13
14use super::core::{PeerIdString, Points};
15use super::enums::{ServiceStatus, UserRole};
16
17/// User account.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[cfg_attr(feature = "schema", derive(JsonSchema))]
20pub struct User {
21    pub id: uuid::Uuid,
22    pub username: String,
23    pub email: String,
24    pub role: UserRole,
25    pub peer_id: Option<PeerIdString>,
26    pub public_key: Option<Vec<u8>>,
27    pub points_balance: Points,
28    pub referrer_id: Option<uuid::Uuid>,
29    pub created_at: chrono::DateTime<chrono::Utc>,
30}
31
32/// Reward distribution result.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34#[cfg_attr(feature = "schema", derive(JsonSchema))]
35pub struct RewardDistribution {
36    pub proof_id: uuid::Uuid,
37    pub provider_reward: Points,
38    pub creator_reward: Points,
39    pub referrer_rewards: Vec<(uuid::Uuid, Points)>,
40    pub platform_fee: Points,
41    pub total_distributed: Points,
42}
43
44/// Leaderboard entry.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46#[cfg_attr(feature = "schema", derive(JsonSchema))]
47pub struct LeaderboardEntry {
48    pub rank: u32,
49    pub user_id: uuid::Uuid,
50    pub username: String,
51    pub total_bandwidth_tb: f64,
52    pub total_earnings: Points,
53    pub badge: Option<String>,
54}
55
56/// Standard API response wrapper for successful operations.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58#[cfg_attr(feature = "schema", derive(JsonSchema))]
59pub struct ApiResponse<T> {
60    /// Indicates success.
61    pub success: bool,
62    /// Response data.
63    pub data: T,
64    /// Optional message.
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub message: Option<String>,
67    /// Response timestamp.
68    pub timestamp: chrono::DateTime<chrono::Utc>,
69}
70
71impl<T> ApiResponse<T> {
72    /// Create a successful response.
73    #[must_use]
74    pub fn success(data: T) -> Self {
75        Self {
76            success: true,
77            data,
78            message: None,
79            timestamp: chrono::Utc::now(),
80        }
81    }
82
83    /// Create a successful response with a message.
84    #[must_use]
85    pub fn success_with_message(data: T, message: impl Into<String>) -> Self {
86        Self {
87            success: true,
88            data,
89            message: Some(message.into()),
90            timestamp: chrono::Utc::now(),
91        }
92    }
93}
94
95/// Standard API error response.
96#[derive(Debug, Clone, Serialize, Deserialize)]
97#[cfg_attr(feature = "schema", derive(JsonSchema))]
98pub struct ApiError {
99    /// Indicates failure.
100    pub success: bool,
101    /// Error code.
102    pub error_code: String,
103    /// Error message.
104    pub message: String,
105    /// Optional detailed errors.
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub details: Option<Vec<String>>,
108    /// Response timestamp.
109    pub timestamp: chrono::DateTime<chrono::Utc>,
110}
111
112impl ApiError {
113    /// Create a new error response.
114    #[must_use]
115    pub fn new(error_code: impl Into<String>, message: impl Into<String>) -> Self {
116        Self {
117            success: false,
118            error_code: error_code.into(),
119            message: message.into(),
120            details: None,
121            timestamp: chrono::Utc::now(),
122        }
123    }
124
125    /// Create an error response with details.
126    #[must_use]
127    pub fn with_details(
128        error_code: impl Into<String>,
129        message: impl Into<String>,
130        details: Vec<String>,
131    ) -> Self {
132        Self {
133            success: false,
134            error_code: error_code.into(),
135            message: message.into(),
136            details: Some(details),
137            timestamp: chrono::Utc::now(),
138        }
139    }
140}
141
142/// Paginated response for list endpoints.
143#[derive(Debug, Clone, Serialize, Deserialize)]
144#[cfg_attr(feature = "schema", derive(JsonSchema))]
145pub struct PaginatedResponse<T> {
146    /// Items in the current page.
147    pub items: Vec<T>,
148    /// Total number of items.
149    pub total: u64,
150    /// Current page offset.
151    pub offset: u64,
152    /// Items per page.
153    pub limit: u64,
154    /// Whether there are more items.
155    pub has_more: bool,
156}
157
158impl<T> PaginatedResponse<T> {
159    /// Create a paginated response.
160    #[must_use]
161    pub fn new(items: Vec<T>, total: u64, offset: u64, limit: u64) -> Self {
162        let has_more = offset + (items.len() as u64) < total;
163        Self {
164            items,
165            total,
166            offset,
167            limit,
168            has_more,
169        }
170    }
171
172    /// Create an empty paginated response.
173    #[must_use]
174    pub fn empty() -> Self {
175        Self {
176            items: Vec::new(),
177            total: 0,
178            offset: 0,
179            limit: 0,
180            has_more: false,
181        }
182    }
183}
184
185/// Proof submission result.
186#[derive(Debug, Clone, Serialize, Deserialize)]
187#[cfg_attr(feature = "schema", derive(JsonSchema))]
188pub struct ProofSubmissionResult {
189    /// Unique proof ID.
190    pub proof_id: uuid::Uuid,
191    /// Whether the proof was accepted.
192    pub accepted: bool,
193    /// Reason for rejection (if not accepted).
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub rejection_reason: Option<String>,
196    /// Reward amount (if accepted).
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub reward_points: Option<Points>,
199}
200
201/// Health check response.
202#[derive(Debug, Clone, Serialize, Deserialize)]
203#[cfg_attr(feature = "schema", derive(JsonSchema))]
204pub struct HealthCheckResponse {
205    /// Service status.
206    pub status: ServiceStatus,
207    /// Uptime in seconds.
208    pub uptime_seconds: u64,
209    /// Service version.
210    pub version: String,
211    /// Database connection status.
212    pub database_ok: bool,
213    /// Cache connection status.
214    pub cache_ok: bool,
215}
216
217/// Cursor for cursor-based pagination (more efficient for large datasets).
218#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
219#[cfg_attr(feature = "schema", derive(JsonSchema))]
220pub struct Cursor {
221    /// Opaque cursor value (typically base64-encoded position data).
222    pub value: String,
223    /// Optional timestamp for time-based cursors.
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub timestamp: Option<i64>,
226}
227
228impl Cursor {
229    /// Create a new cursor from a value.
230    #[must_use]
231    pub fn new(value: impl Into<String>) -> Self {
232        Self {
233            value: value.into(),
234            timestamp: None,
235        }
236    }
237
238    /// Create a cursor with a timestamp.
239    #[must_use]
240    pub fn with_timestamp(value: impl Into<String>, timestamp: i64) -> Self {
241        Self {
242            value: value.into(),
243            timestamp: Some(timestamp),
244        }
245    }
246
247    /// Create a cursor from an ID (encodes as base64).
248    #[must_use]
249    pub fn from_id(id: &uuid::Uuid) -> Self {
250        use std::fmt::Write;
251        let mut buf = String::new();
252        write!(&mut buf, "{id}").expect("UUID formatting failed");
253        Self::new(buf)
254    }
255
256    /// Create a cursor from a timestamp.
257    #[must_use]
258    pub fn from_timestamp(timestamp: i64) -> Self {
259        Self::with_timestamp(timestamp.to_string(), timestamp)
260    }
261}
262
263/// Cursor-based paginated response (more efficient for large datasets).
264#[derive(Debug, Clone, Serialize, Deserialize)]
265#[cfg_attr(feature = "schema", derive(JsonSchema))]
266pub struct CursorPaginatedResponse<T> {
267    /// Items in the current page.
268    pub items: Vec<T>,
269    /// Cursor for the next page (if `has_more` is true).
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub next_cursor: Option<Cursor>,
272    /// Cursor for the previous page (if applicable).
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub prev_cursor: Option<Cursor>,
275    /// Number of items requested.
276    pub limit: u64,
277    /// Whether there are more items available.
278    pub has_more: bool,
279}
280
281impl<T> CursorPaginatedResponse<T> {
282    /// Create a new cursor-based paginated response.
283    #[must_use]
284    pub fn new(
285        items: Vec<T>,
286        next_cursor: Option<Cursor>,
287        prev_cursor: Option<Cursor>,
288        limit: u64,
289    ) -> Self {
290        let has_more = next_cursor.is_some();
291        Self {
292            items,
293            next_cursor,
294            prev_cursor,
295            limit,
296            has_more,
297        }
298    }
299
300    /// Create an empty cursor-based paginated response.
301    #[must_use]
302    pub fn empty(limit: u64) -> Self {
303        Self {
304            items: Vec::new(),
305            next_cursor: None,
306            prev_cursor: None,
307            limit,
308            has_more: false,
309        }
310    }
311
312    /// Create a response without previous cursor.
313    #[must_use]
314    pub fn forward_only(items: Vec<T>, next_cursor: Option<Cursor>, limit: u64) -> Self {
315        Self::new(items, next_cursor, None, limit)
316    }
317}
318
319/// API version for header-based versioning
320#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
321#[cfg_attr(feature = "schema", derive(JsonSchema))]
322pub enum ApiVersion {
323    /// API version 1
324    V1,
325    /// API version 2 (future)
326    V2,
327}
328
329impl ApiVersion {
330    /// Get the version string (e.g., "v1")
331    #[must_use]
332    pub fn as_str(&self) -> &'static str {
333        match self {
334            Self::V1 => "v1",
335            Self::V2 => "v2",
336        }
337    }
338
339    /// Get the version as an Accept/Content-Type header value
340    #[must_use]
341    pub fn as_header_value(&self) -> String {
342        format!("application/vnd.chie.{}+json", self.as_str())
343    }
344
345    /// Get the current/default API version
346    #[must_use]
347    pub const fn current() -> Self {
348        Self::V1
349    }
350}
351
352impl Default for ApiVersion {
353    fn default() -> Self {
354        Self::current()
355    }
356}
357
358impl std::fmt::Display for ApiVersion {
359    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
360        write!(f, "{}", self.as_str())
361    }
362}
363
364impl std::str::FromStr for ApiVersion {
365    type Err = String;
366
367    fn from_str(s: &str) -> Result<Self, Self::Err> {
368        match s {
369            "v1" => Ok(Self::V1),
370            "v2" => Ok(Self::V2),
371            _ => Err(format!("Invalid API version: {s}")),
372        }
373    }
374}
375
376/// HATEOAS link for hypermedia-driven API navigation.
377///
378/// Links allow clients to discover available actions and navigate the API
379/// without hardcoded URLs.
380#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
381#[cfg_attr(feature = "schema", derive(JsonSchema))]
382pub struct Link {
383    /// Link relation type (e.g., "self", "next", "prev", "related")
384    pub rel: String,
385    /// Target URI
386    pub href: String,
387    /// HTTP method for this link (default: GET)
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub method: Option<String>,
390    /// Human-readable title
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub title: Option<String>,
393}
394
395impl Link {
396    /// Create a new link
397    #[must_use]
398    pub fn new(rel: impl Into<String>, href: impl Into<String>) -> Self {
399        Self {
400            rel: rel.into(),
401            href: href.into(),
402            method: None,
403            title: None,
404        }
405    }
406
407    /// Create a link with a specific HTTP method
408    #[must_use]
409    pub fn with_method(mut self, method: impl Into<String>) -> Self {
410        self.method = Some(method.into());
411        self
412    }
413
414    /// Create a link with a title
415    #[must_use]
416    pub fn with_title(mut self, title: impl Into<String>) -> Self {
417        self.title = Some(title.into());
418        self
419    }
420
421    /// Create a "self" link
422    #[must_use]
423    pub fn self_link(href: impl Into<String>) -> Self {
424        Self::new("self", href)
425    }
426
427    /// Create a "next" link for pagination
428    #[must_use]
429    pub fn next(href: impl Into<String>) -> Self {
430        Self::new("next", href).with_title("Next page")
431    }
432
433    /// Create a "prev" link for pagination
434    #[must_use]
435    pub fn prev(href: impl Into<String>) -> Self {
436        Self::new("prev", href).with_title("Previous page")
437    }
438
439    /// Create a "first" link for pagination
440    #[must_use]
441    pub fn first(href: impl Into<String>) -> Self {
442        Self::new("first", href).with_title("First page")
443    }
444
445    /// Create a "last" link for pagination
446    #[must_use]
447    pub fn last(href: impl Into<String>) -> Self {
448        Self::new("last", href).with_title("Last page")
449    }
450}
451
452/// Collection of HATEOAS links
453#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
454#[cfg_attr(feature = "schema", derive(JsonSchema))]
455pub struct Links {
456    /// Links associated with this resource
457    #[serde(skip_serializing_if = "Vec::is_empty", default)]
458    pub links: Vec<Link>,
459}
460
461impl Links {
462    /// Create an empty links collection
463    #[must_use]
464    pub fn new() -> Self {
465        Self { links: Vec::new() }
466    }
467
468    /// Add a link object to the collection
469    #[must_use]
470    pub fn with_link(mut self, link: Link) -> Self {
471        self.links.push(link);
472        self
473    }
474
475    /// Add a link from components
476    #[must_use]
477    pub fn add_link(mut self, rel: impl Into<String>, href: impl Into<String>) -> Self {
478        self.links.push(Link::new(rel, href));
479        self
480    }
481
482    /// Check if the collection is empty
483    #[must_use]
484    pub fn is_empty(&self) -> bool {
485        self.links.is_empty()
486    }
487
488    /// Get the number of links
489    #[must_use]
490    pub fn len(&self) -> usize {
491        self.links.len()
492    }
493
494    /// Find a link by relation type
495    #[must_use]
496    pub fn find(&self, rel: &str) -> Option<&Link> {
497        self.links.iter().find(|link| link.rel == rel)
498    }
499}
500
501impl From<Vec<Link>> for Links {
502    fn from(links: Vec<Link>) -> Self {
503        Self { links }
504    }
505}
506
507/// Rate limit response headers following RFC 6585 and draft standards.
508///
509/// These headers inform clients about their rate limit status.
510#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
511#[cfg_attr(feature = "schema", derive(JsonSchema))]
512pub struct RateLimitHeaders {
513    /// Maximum requests allowed in the time window
514    pub limit: u32,
515    /// Remaining requests in the current window
516    pub remaining: u32,
517    /// Unix timestamp when the rate limit window resets
518    pub reset: i64,
519    /// Time until reset in seconds (for convenience)
520    pub retry_after: Option<u64>,
521}
522
523impl RateLimitHeaders {
524    /// Create new rate limit headers
525    #[must_use]
526    pub fn new(limit: u32, remaining: u32, reset: i64) -> Self {
527        Self {
528            limit,
529            remaining,
530            reset,
531            retry_after: None,
532        }
533    }
534
535    /// Create rate limit headers when limit is exceeded
536    #[must_use]
537    pub fn exceeded(limit: u32, reset: i64) -> Self {
538        let now = chrono::Utc::now().timestamp();
539        let retry_after = if reset > now {
540            Some((reset - now) as u64)
541        } else {
542            Some(0)
543        };
544
545        Self {
546            limit,
547            remaining: 0,
548            reset,
549            retry_after,
550        }
551    }
552
553    /// Check if rate limit is exceeded
554    #[must_use]
555    pub fn is_exceeded(&self) -> bool {
556        self.remaining == 0
557    }
558
559    /// Get the utilization as a percentage (0.0 to 1.0)
560    #[must_use]
561    #[allow(clippy::cast_precision_loss)]
562    pub fn utilization(&self) -> f64 {
563        if self.limit == 0 {
564            return 0.0;
565        }
566        let used = self.limit.saturating_sub(self.remaining);
567        (used as f64) / (self.limit as f64)
568    }
569
570    /// Format as HTTP header pairs (name, value)
571    #[must_use]
572    pub fn as_http_headers(&self) -> Vec<(&'static str, String)> {
573        let mut headers = vec![
574            ("X-RateLimit-Limit", self.limit.to_string()),
575            ("X-RateLimit-Remaining", self.remaining.to_string()),
576            ("X-RateLimit-Reset", self.reset.to_string()),
577        ];
578
579        if let Some(retry_after) = self.retry_after {
580            headers.push(("Retry-After", retry_after.to_string()));
581        }
582
583        headers
584    }
585}
586
587#[cfg(test)]
588mod tests {
589    use super::*;
590
591    // API Response tests
592    #[test]
593    fn test_api_response_success() {
594        let response = ApiResponse::success("test data");
595        assert!(response.success);
596        assert_eq!(response.data, "test data");
597        assert!(response.message.is_none());
598    }
599
600    #[test]
601    fn test_api_response_with_message() {
602        let response = ApiResponse::success_with_message("test data", "Success message");
603        assert!(response.success);
604        assert_eq!(response.data, "test data");
605        assert_eq!(response.message, Some("Success message".to_string()));
606    }
607
608    #[test]
609    fn test_api_error_new() {
610        let error = ApiError::new("TEST_ERROR", "Test error message");
611        assert!(!error.success);
612        assert_eq!(error.error_code, "TEST_ERROR");
613        assert_eq!(error.message, "Test error message");
614        assert!(error.details.is_none());
615    }
616
617    #[test]
618    fn test_api_error_with_details() {
619        let details = vec!["Detail 1".to_string(), "Detail 2".to_string()];
620        let error = ApiError::with_details("TEST_ERROR", "Test error message", details.clone());
621        assert!(!error.success);
622        assert_eq!(error.details, Some(details));
623    }
624
625    // Paginated Response tests
626    #[test]
627    fn test_paginated_response_new() {
628        let items = vec![1, 2, 3];
629        let response = PaginatedResponse::new(items.clone(), 10, 0, 3);
630        assert_eq!(response.items, items);
631        assert_eq!(response.total, 10);
632        assert_eq!(response.offset, 0);
633        assert_eq!(response.limit, 3);
634        assert!(response.has_more);
635    }
636
637    #[test]
638    fn test_paginated_response_no_more() {
639        let items = vec![1, 2, 3];
640        let response = PaginatedResponse::new(items.clone(), 3, 0, 3);
641        assert!(!response.has_more);
642    }
643
644    #[test]
645    fn test_paginated_response_empty() {
646        let response: PaginatedResponse<i32> = PaginatedResponse::empty();
647        assert_eq!(response.items.len(), 0);
648        assert_eq!(response.total, 0);
649        assert!(!response.has_more);
650    }
651
652    // Cursor tests
653    #[test]
654    fn test_cursor_new() {
655        let cursor = Cursor::new("test_cursor_value");
656        assert_eq!(cursor.value, "test_cursor_value");
657        assert!(cursor.timestamp.is_none());
658    }
659
660    #[test]
661    fn test_cursor_with_timestamp() {
662        let timestamp = 1_234_567_890;
663        let cursor = Cursor::with_timestamp("test_value", timestamp);
664        assert_eq!(cursor.value, "test_value");
665        assert_eq!(cursor.timestamp, Some(timestamp));
666    }
667
668    #[test]
669    fn test_cursor_from_id() {
670        let id = uuid::Uuid::new_v4();
671        let cursor = Cursor::from_id(&id);
672        assert_eq!(cursor.value, id.to_string());
673        assert!(cursor.timestamp.is_none());
674    }
675
676    #[test]
677    fn test_cursor_from_timestamp() {
678        let timestamp = 1_234_567_890;
679        let cursor = Cursor::from_timestamp(timestamp);
680        assert_eq!(cursor.timestamp, Some(timestamp));
681        assert_eq!(cursor.value, timestamp.to_string());
682    }
683
684    // CursorPaginatedResponse tests
685    #[test]
686    fn test_cursor_paginated_response_new() {
687        let items = vec![1, 2, 3];
688        let next_cursor = Some(Cursor::new("next"));
689        let prev_cursor = Some(Cursor::new("prev"));
690        let response = CursorPaginatedResponse::new(
691            items.clone(),
692            next_cursor.clone(),
693            prev_cursor.clone(),
694            10,
695        );
696
697        assert_eq!(response.items, items);
698        assert_eq!(response.next_cursor, next_cursor);
699        assert_eq!(response.prev_cursor, prev_cursor);
700        assert_eq!(response.limit, 10);
701        assert!(response.has_more);
702    }
703
704    #[test]
705    fn test_cursor_paginated_response_no_more() {
706        let items = vec![1, 2, 3];
707        let response = CursorPaginatedResponse::new(items, None, None, 10);
708        assert!(!response.has_more);
709        assert!(response.next_cursor.is_none());
710    }
711
712    #[test]
713    fn test_cursor_paginated_response_empty() {
714        let response: CursorPaginatedResponse<i32> = CursorPaginatedResponse::empty(10);
715        assert_eq!(response.items.len(), 0);
716        assert!(response.next_cursor.is_none());
717        assert!(!response.has_more);
718    }
719
720    #[test]
721    fn test_cursor_paginated_response_forward_only() {
722        let items = vec![1, 2, 3];
723        let next_cursor = Some(Cursor::new("next"));
724        let response =
725            CursorPaginatedResponse::forward_only(items.clone(), next_cursor.clone(), 10);
726
727        assert_eq!(response.items, items);
728        assert_eq!(response.next_cursor, next_cursor);
729        assert!(response.prev_cursor.is_none());
730        assert!(response.has_more);
731    }
732
733    #[test]
734    fn test_api_version() {
735        let v1 = ApiVersion::V1;
736        assert_eq!(v1.as_str(), "v1");
737        assert_eq!(v1.as_header_value(), "application/vnd.chie.v1+json");
738
739        let v2 = ApiVersion::V2;
740        assert_eq!(v2.as_str(), "v2");
741    }
742
743    #[test]
744    fn test_api_version_parse() {
745        use std::str::FromStr;
746
747        assert_eq!(ApiVersion::from_str("v1").unwrap(), ApiVersion::V1);
748        assert_eq!(ApiVersion::from_str("v2").unwrap(), ApiVersion::V2);
749        assert!(ApiVersion::from_str("v3").is_err());
750        assert!(ApiVersion::from_str("invalid").is_err());
751    }
752
753    #[test]
754    fn test_api_version_ordering() {
755        assert!(ApiVersion::V1 < ApiVersion::V2);
756        assert!(ApiVersion::V2 > ApiVersion::V1);
757        assert_eq!(ApiVersion::V1, ApiVersion::V1);
758    }
759
760    // RateLimitHeaders tests
761    #[test]
762    fn test_rate_limit_headers_new() {
763        let headers = RateLimitHeaders::new(100, 75, 1_700_000_000);
764        assert_eq!(headers.limit, 100);
765        assert_eq!(headers.remaining, 75);
766        assert_eq!(headers.reset, 1_700_000_000);
767        assert!(!headers.is_exceeded());
768        assert!(headers.retry_after.is_none());
769    }
770
771    #[test]
772    fn test_rate_limit_headers_exceeded() {
773        let reset = chrono::Utc::now().timestamp() + 60;
774        let headers = RateLimitHeaders::exceeded(100, reset);
775        assert_eq!(headers.limit, 100);
776        assert_eq!(headers.remaining, 0);
777        assert!(headers.is_exceeded());
778        assert!(headers.retry_after.is_some());
779    }
780
781    #[test]
782    fn test_rate_limit_headers_utilization() {
783        let headers = RateLimitHeaders::new(100, 25, 0);
784        assert!((headers.utilization() - 0.75).abs() < 0.01);
785
786        let headers = RateLimitHeaders::new(100, 100, 0);
787        assert!((headers.utilization() - 0.0).abs() < 0.01);
788
789        let headers = RateLimitHeaders::new(100, 0, 0);
790        assert!((headers.utilization() - 1.0).abs() < 0.01);
791
792        let headers = RateLimitHeaders::new(0, 0, 0);
793        assert!((headers.utilization() - 0.0).abs() < 0.01);
794    }
795
796    #[test]
797    fn test_rate_limit_headers_as_http_headers() {
798        let headers = RateLimitHeaders::new(100, 75, 1_700_000_000);
799        let http_headers = headers.as_http_headers();
800
801        assert_eq!(http_headers.len(), 3);
802        assert!(http_headers.contains(&("X-RateLimit-Limit", "100".to_string())));
803        assert!(http_headers.contains(&("X-RateLimit-Remaining", "75".to_string())));
804        assert!(http_headers.contains(&("X-RateLimit-Reset", "1700000000".to_string())));
805    }
806
807    #[test]
808    fn test_rate_limit_headers_with_retry_after() {
809        let reset = chrono::Utc::now().timestamp() + 60;
810        let headers = RateLimitHeaders::exceeded(100, reset);
811        let http_headers = headers.as_http_headers();
812
813        assert_eq!(http_headers.len(), 4);
814        assert!(http_headers.iter().any(|(name, _)| *name == "Retry-After"));
815    }
816
817    #[test]
818    fn test_rate_limit_headers_serde() {
819        let headers = RateLimitHeaders::new(100, 50, 1_700_000_000);
820        let json = serde_json::to_string(&headers).unwrap();
821        let decoded: RateLimitHeaders = serde_json::from_str(&json).unwrap();
822
823        assert_eq!(decoded, headers);
824    }
825
826    // HATEOAS Link tests
827    #[test]
828    fn test_link_new() {
829        let link = Link::new("self", "/api/users/123");
830        assert_eq!(link.rel, "self");
831        assert_eq!(link.href, "/api/users/123");
832        assert!(link.method.is_none());
833        assert!(link.title.is_none());
834    }
835
836    #[test]
837    fn test_link_with_method() {
838        let link = Link::new("update", "/api/users/123").with_method("PUT");
839        assert_eq!(link.method, Some("PUT".to_string()));
840    }
841
842    #[test]
843    fn test_link_with_title() {
844        let link = Link::new("related", "/api/content/456").with_title("Related Content");
845        assert_eq!(link.title, Some("Related Content".to_string()));
846    }
847
848    #[test]
849    fn test_link_self_link() {
850        let link = Link::self_link("/api/users/123");
851        assert_eq!(link.rel, "self");
852        assert_eq!(link.href, "/api/users/123");
853    }
854
855    #[test]
856    fn test_link_pagination() {
857        let next = Link::next("/api/users?page=2");
858        assert_eq!(next.rel, "next");
859        assert_eq!(next.title, Some("Next page".to_string()));
860
861        let prev = Link::prev("/api/users?page=1");
862        assert_eq!(prev.rel, "prev");
863
864        let first = Link::first("/api/users?page=1");
865        assert_eq!(first.rel, "first");
866
867        let last = Link::last("/api/users?page=10");
868        assert_eq!(last.rel, "last");
869    }
870
871    #[test]
872    fn test_links_new() {
873        let links = Links::new();
874        assert!(links.is_empty());
875        assert_eq!(links.len(), 0);
876    }
877
878    #[test]
879    fn test_links_add() {
880        let links = Links::new()
881            .with_link(Link::self_link("/api/users/123"))
882            .with_link(Link::next("/api/users?page=2"));
883
884        assert!(!links.is_empty());
885        assert_eq!(links.len(), 2);
886    }
887
888    #[test]
889    fn test_links_add_link() {
890        let links = Links::new()
891            .add_link("self", "/api/users/123")
892            .add_link("next", "/api/users?page=2");
893
894        assert_eq!(links.len(), 2);
895    }
896
897    #[test]
898    fn test_links_find() {
899        let links = Links::new()
900            .with_link(Link::self_link("/api/users/123"))
901            .with_link(Link::next("/api/users?page=2"));
902
903        let self_link = links.find("self");
904        assert!(self_link.is_some());
905        assert_eq!(self_link.unwrap().href, "/api/users/123");
906
907        let missing_link = links.find("prev");
908        assert!(missing_link.is_none());
909    }
910
911    #[test]
912    fn test_links_from_vec() {
913        let vec = vec![
914            Link::self_link("/api/users/123"),
915            Link::next("/api/users?page=2"),
916        ];
917
918        let links = Links::from(vec);
919        assert_eq!(links.len(), 2);
920    }
921
922    #[test]
923    fn test_link_serde() {
924        let link = Link::new("self", "/api/users/123")
925            .with_method("GET")
926            .with_title("User Details");
927
928        let json = serde_json::to_string(&link).unwrap();
929        let decoded: Link = serde_json::from_str(&json).unwrap();
930
931        assert_eq!(decoded, link);
932    }
933
934    #[test]
935    fn test_links_serde() {
936        let links = Links::new()
937            .with_link(Link::self_link("/api/users/123"))
938            .with_link(Link::next("/api/users?page=2"));
939
940        let json = serde_json::to_string(&links).unwrap();
941        let decoded: Links = serde_json::from_str(&json).unwrap();
942
943        assert_eq!(decoded, links);
944    }
945}