1#[cfg(feature = "schema")]
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13
14use super::core::{PeerIdString, Points};
15use super::enums::{ServiceStatus, UserRole};
16
17#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
58#[cfg_attr(feature = "schema", derive(JsonSchema))]
59pub struct ApiResponse<T> {
60 pub success: bool,
62 pub data: T,
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub message: Option<String>,
67 pub timestamp: chrono::DateTime<chrono::Utc>,
69}
70
71impl<T> ApiResponse<T> {
72 #[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 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
97#[cfg_attr(feature = "schema", derive(JsonSchema))]
98pub struct ApiError {
99 pub success: bool,
101 pub error_code: String,
103 pub message: String,
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub details: Option<Vec<String>>,
108 pub timestamp: chrono::DateTime<chrono::Utc>,
110}
111
112impl ApiError {
113 #[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 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
144#[cfg_attr(feature = "schema", derive(JsonSchema))]
145pub struct PaginatedResponse<T> {
146 pub items: Vec<T>,
148 pub total: u64,
150 pub offset: u64,
152 pub limit: u64,
154 pub has_more: bool,
156}
157
158impl<T> PaginatedResponse<T> {
159 #[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 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
187#[cfg_attr(feature = "schema", derive(JsonSchema))]
188pub struct ProofSubmissionResult {
189 pub proof_id: uuid::Uuid,
191 pub accepted: bool,
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub rejection_reason: Option<String>,
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub reward_points: Option<Points>,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203#[cfg_attr(feature = "schema", derive(JsonSchema))]
204pub struct HealthCheckResponse {
205 pub status: ServiceStatus,
207 pub uptime_seconds: u64,
209 pub version: String,
211 pub database_ok: bool,
213 pub cache_ok: bool,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
219#[cfg_attr(feature = "schema", derive(JsonSchema))]
220pub struct Cursor {
221 pub value: String,
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub timestamp: Option<i64>,
226}
227
228impl Cursor {
229 #[must_use]
231 pub fn new(value: impl Into<String>) -> Self {
232 Self {
233 value: value.into(),
234 timestamp: None,
235 }
236 }
237
238 #[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 #[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 #[must_use]
258 pub fn from_timestamp(timestamp: i64) -> Self {
259 Self::with_timestamp(timestamp.to_string(), timestamp)
260 }
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
265#[cfg_attr(feature = "schema", derive(JsonSchema))]
266pub struct CursorPaginatedResponse<T> {
267 pub items: Vec<T>,
269 #[serde(skip_serializing_if = "Option::is_none")]
271 pub next_cursor: Option<Cursor>,
272 #[serde(skip_serializing_if = "Option::is_none")]
274 pub prev_cursor: Option<Cursor>,
275 pub limit: u64,
277 pub has_more: bool,
279}
280
281impl<T> CursorPaginatedResponse<T> {
282 #[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 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
321#[cfg_attr(feature = "schema", derive(JsonSchema))]
322pub enum ApiVersion {
323 V1,
325 V2,
327}
328
329impl ApiVersion {
330 #[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 #[must_use]
341 pub fn as_header_value(&self) -> String {
342 format!("application/vnd.chie.{}+json", self.as_str())
343 }
344
345 #[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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
381#[cfg_attr(feature = "schema", derive(JsonSchema))]
382pub struct Link {
383 pub rel: String,
385 pub href: String,
387 #[serde(skip_serializing_if = "Option::is_none")]
389 pub method: Option<String>,
390 #[serde(skip_serializing_if = "Option::is_none")]
392 pub title: Option<String>,
393}
394
395impl Link {
396 #[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 #[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 #[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 #[must_use]
423 pub fn self_link(href: impl Into<String>) -> Self {
424 Self::new("self", href)
425 }
426
427 #[must_use]
429 pub fn next(href: impl Into<String>) -> Self {
430 Self::new("next", href).with_title("Next page")
431 }
432
433 #[must_use]
435 pub fn prev(href: impl Into<String>) -> Self {
436 Self::new("prev", href).with_title("Previous page")
437 }
438
439 #[must_use]
441 pub fn first(href: impl Into<String>) -> Self {
442 Self::new("first", href).with_title("First page")
443 }
444
445 #[must_use]
447 pub fn last(href: impl Into<String>) -> Self {
448 Self::new("last", href).with_title("Last page")
449 }
450}
451
452#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
454#[cfg_attr(feature = "schema", derive(JsonSchema))]
455pub struct Links {
456 #[serde(skip_serializing_if = "Vec::is_empty", default)]
458 pub links: Vec<Link>,
459}
460
461impl Links {
462 #[must_use]
464 pub fn new() -> Self {
465 Self { links: Vec::new() }
466 }
467
468 #[must_use]
470 pub fn with_link(mut self, link: Link) -> Self {
471 self.links.push(link);
472 self
473 }
474
475 #[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 #[must_use]
484 pub fn is_empty(&self) -> bool {
485 self.links.is_empty()
486 }
487
488 #[must_use]
490 pub fn len(&self) -> usize {
491 self.links.len()
492 }
493
494 #[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#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
511#[cfg_attr(feature = "schema", derive(JsonSchema))]
512pub struct RateLimitHeaders {
513 pub limit: u32,
515 pub remaining: u32,
517 pub reset: i64,
519 pub retry_after: Option<u64>,
521}
522
523impl RateLimitHeaders {
524 #[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 #[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 #[must_use]
555 pub fn is_exceeded(&self) -> bool {
556 self.remaining == 0
557 }
558
559 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}