1use rustapi_openapi::Schema;
33use serde::{Deserialize, Serialize};
34use std::collections::HashMap;
35
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Schema)]
49pub struct Link {
50 pub href: String,
52
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub templated: Option<bool>,
56
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub title: Option<String>,
60
61 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
63 pub media_type: Option<String>,
64
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub deprecation: Option<String>,
68
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub name: Option<String>,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub profile: Option<String>,
76
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub hreflang: Option<String>,
80}
81
82impl Link {
83 pub fn new(href: impl Into<String>) -> Self {
85 Self {
86 href: href.into(),
87 templated: None,
88 title: None,
89 media_type: None,
90 deprecation: None,
91 name: None,
92 profile: None,
93 hreflang: None,
94 }
95 }
96
97 pub fn templated(href: impl Into<String>) -> Self {
106 Self {
107 href: href.into(),
108 templated: Some(true),
109 ..Self::new("")
110 }
111 }
112
113 pub fn set_templated(mut self, templated: bool) -> Self {
115 self.templated = Some(templated);
116 self
117 }
118
119 pub fn title(mut self, title: impl Into<String>) -> Self {
121 self.title = Some(title.into());
122 self
123 }
124
125 pub fn media_type(mut self, media_type: impl Into<String>) -> Self {
127 self.media_type = Some(media_type.into());
128 self
129 }
130
131 pub fn deprecation(mut self, deprecation_url: impl Into<String>) -> Self {
133 self.deprecation = Some(deprecation_url.into());
134 self
135 }
136
137 pub fn name(mut self, name: impl Into<String>) -> Self {
139 self.name = Some(name.into());
140 self
141 }
142
143 pub fn profile(mut self, profile: impl Into<String>) -> Self {
145 self.profile = Some(profile.into());
146 self
147 }
148
149 pub fn hreflang(mut self, hreflang: impl Into<String>) -> Self {
151 self.hreflang = Some(hreflang.into());
152 self
153 }
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, Schema)]
177pub struct Resource<T: rustapi_openapi::schema::RustApiSchema> {
178 #[serde(flatten)]
180 pub data: T,
181
182 #[serde(rename = "_links")]
184 pub links: HashMap<String, LinkOrArray>,
185
186 #[serde(rename = "_embedded", skip_serializing_if = "Option::is_none")]
188 pub embedded: Option<HashMap<String, serde_json::Value>>,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Schema)]
193#[serde(untagged)]
194pub enum LinkOrArray {
195 Single(Link),
197 Array(Vec<Link>),
199}
200
201impl From<Link> for LinkOrArray {
202 fn from(link: Link) -> Self {
203 LinkOrArray::Single(link)
204 }
205}
206
207impl From<Vec<Link>> for LinkOrArray {
208 fn from(links: Vec<Link>) -> Self {
209 LinkOrArray::Array(links)
210 }
211}
212
213impl<T: rustapi_openapi::schema::RustApiSchema> Resource<T> {
214 pub fn new(data: T) -> Self {
216 Self {
217 data,
218 links: HashMap::new(),
219 embedded: None,
220 }
221 }
222
223 pub fn link(mut self, rel: impl Into<String>, href: impl Into<String>) -> Self {
225 self.links
226 .insert(rel.into(), LinkOrArray::Single(Link::new(href)));
227 self
228 }
229
230 pub fn link_object(mut self, rel: impl Into<String>, link: Link) -> Self {
232 self.links.insert(rel.into(), LinkOrArray::Single(link));
233 self
234 }
235
236 pub fn links(mut self, rel: impl Into<String>, links: Vec<Link>) -> Self {
238 self.links.insert(rel.into(), LinkOrArray::Array(links));
239 self
240 }
241
242 pub fn self_link(self, href: impl Into<String>) -> Self {
244 self.link("self", href)
245 }
246
247 pub fn embed<E: Serialize>(
249 mut self,
250 rel: impl Into<String>,
251 resources: E,
252 ) -> Result<Self, serde_json::Error> {
253 let embedded = self.embedded.get_or_insert_with(HashMap::new);
254 embedded.insert(rel.into(), serde_json::to_value(resources)?);
255 Ok(self)
256 }
257
258 pub fn embed_raw(mut self, rel: impl Into<String>, value: serde_json::Value) -> Self {
260 let embedded = self.embedded.get_or_insert_with(HashMap::new);
261 embedded.insert(rel.into(), value);
262 self
263 }
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize, Schema)]
293pub struct ResourceCollection<T: rustapi_openapi::schema::RustApiSchema> {
294 #[serde(rename = "_embedded")]
296 pub embedded: HashMap<String, Vec<T>>,
297
298 #[serde(rename = "_links")]
300 pub links: HashMap<String, LinkOrArray>,
301
302 #[serde(skip_serializing_if = "Option::is_none")]
304 pub page: Option<PageInfo>,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize, Schema)]
309pub struct PageInfo {
310 pub size: usize,
312 #[serde(rename = "totalElements")]
314 pub total_elements: usize,
315 #[serde(rename = "totalPages")]
317 pub total_pages: usize,
318 pub number: usize,
320}
321
322impl PageInfo {
323 pub fn new(size: usize, total_elements: usize, total_pages: usize, number: usize) -> Self {
325 Self {
326 size,
327 total_elements,
328 total_pages,
329 number,
330 }
331 }
332
333 pub fn calculate(total_elements: usize, page_size: usize, current_page: usize) -> Self {
335 let total_pages = total_elements.div_ceil(page_size);
336 Self {
337 size: page_size,
338 total_elements,
339 total_pages,
340 number: current_page,
341 }
342 }
343}
344
345impl<T: rustapi_openapi::schema::RustApiSchema> ResourceCollection<T> {
346 pub fn new(rel: impl Into<String>, items: Vec<T>) -> Self {
348 let mut embedded = HashMap::new();
349 embedded.insert(rel.into(), items);
350
351 Self {
352 embedded,
353 links: HashMap::new(),
354 page: None,
355 }
356 }
357
358 pub fn link(mut self, rel: impl Into<String>, href: impl Into<String>) -> Self {
360 self.links
361 .insert(rel.into(), LinkOrArray::Single(Link::new(href)));
362 self
363 }
364
365 pub fn self_link(self, href: impl Into<String>) -> Self {
367 self.link("self", href)
368 }
369
370 pub fn first_link(self, href: impl Into<String>) -> Self {
372 self.link("first", href)
373 }
374
375 pub fn last_link(self, href: impl Into<String>) -> Self {
377 self.link("last", href)
378 }
379
380 pub fn next_link(self, href: impl Into<String>) -> Self {
382 self.link("next", href)
383 }
384
385 pub fn prev_link(self, href: impl Into<String>) -> Self {
387 self.link("prev", href)
388 }
389
390 pub fn page_info(mut self, page: PageInfo) -> Self {
392 self.page = Some(page);
393 self
394 }
395
396 pub fn with_pagination(mut self, base_url: &str) -> Self {
398 let page_info = self.page.clone();
400
401 if let Some(page) = page_info {
402 self = self.self_link(format!(
403 "{}?page={}&size={}",
404 base_url, page.number, page.size
405 ));
406 self = self.first_link(format!("{}?page=0&size={}", base_url, page.size));
407
408 if page.total_pages > 0 {
409 self = self.last_link(format!(
410 "{}?page={}&size={}",
411 base_url,
412 page.total_pages - 1,
413 page.size
414 ));
415 }
416
417 if page.number > 0 {
418 self = self.prev_link(format!(
419 "{}?page={}&size={}",
420 base_url,
421 page.number - 1,
422 page.size
423 ));
424 }
425
426 if page.number < page.total_pages.saturating_sub(1) {
427 self = self.next_link(format!(
428 "{}?page={}&size={}",
429 base_url,
430 page.number + 1,
431 page.size
432 ));
433 }
434 }
435 self
436 }
437}
438
439pub trait Linkable: Sized + Serialize + rustapi_openapi::schema::RustApiSchema {
441 fn with_links(self) -> Resource<Self> {
443 Resource::new(self)
444 }
445}
446
447impl<T: Serialize + rustapi_openapi::schema::RustApiSchema> Linkable for T {}
449
450#[derive(Debug, Clone)]
471pub struct Paginated<T> {
472 pub items: Vec<T>,
474 pub page: u64,
476 pub per_page: u64,
478 pub total: u64,
480}
481
482impl<T> Paginated<T> {
483 pub fn new(items: Vec<T>, page: u64, per_page: u64, total: u64) -> Self {
485 Self {
486 items,
487 page,
488 per_page,
489 total,
490 }
491 }
492
493 pub fn total_pages(&self) -> u64 {
495 if self.per_page == 0 {
496 return 0;
497 }
498 self.total.div_ceil(self.per_page)
499 }
500
501 pub fn has_next(&self) -> bool {
503 self.page < self.total_pages()
504 }
505
506 pub fn has_prev(&self) -> bool {
508 self.page > 1
509 }
510
511 pub fn map<U, F: FnMut(T) -> U>(self, f: F) -> Paginated<U> {
513 Paginated {
514 items: self.items.into_iter().map(f).collect(),
515 page: self.page,
516 per_page: self.per_page,
517 total: self.total,
518 }
519 }
520}
521
522#[derive(Serialize)]
524struct PaginatedBody<T: Serialize> {
525 items: Vec<T>,
526 meta: PaginationMeta,
527 #[serde(rename = "_links")]
528 links: PaginationLinks,
529}
530
531#[derive(Serialize)]
532struct PaginationMeta {
533 page: u64,
534 per_page: u64,
535 total: u64,
536 total_pages: u64,
537}
538
539#[derive(Serialize)]
540struct PaginationLinks {
541 #[serde(rename = "self")]
542 self_link: String,
543 first: String,
544 last: String,
545 #[serde(skip_serializing_if = "Option::is_none")]
546 next: Option<String>,
547 #[serde(skip_serializing_if = "Option::is_none")]
548 prev: Option<String>,
549}
550
551impl<T: Serialize> Paginated<T> {
552 fn link_header(&self, base_path: &str) -> String {
554 let total_pages = self.total_pages();
555 let mut links = Vec::new();
556
557 links.push(format!(
559 "<{}?page=1&per_page={}>; rel=\"first\"",
560 base_path, self.per_page
561 ));
562
563 if total_pages > 0 {
565 links.push(format!(
566 "<{}?page={}&per_page={}>; rel=\"last\"",
567 base_path, total_pages, self.per_page
568 ));
569 }
570
571 if self.has_prev() {
573 links.push(format!(
574 "<{}?page={}&per_page={}>; rel=\"prev\"",
575 base_path,
576 self.page - 1,
577 self.per_page
578 ));
579 }
580
581 if self.has_next() {
583 links.push(format!(
584 "<{}?page={}&per_page={}>; rel=\"next\"",
585 base_path,
586 self.page + 1,
587 self.per_page
588 ));
589 }
590
591 links.join(", ")
592 }
593
594 fn to_body_with_path(&self, base_path: &str) -> PaginatedBody<&T> {
596 let total_pages = self.total_pages();
597
598 let links = PaginationLinks {
599 self_link: format!(
600 "{}?page={}&per_page={}",
601 base_path, self.page, self.per_page
602 ),
603 first: format!("{}?page=1&per_page={}", base_path, self.per_page),
604 last: format!(
605 "{}?page={}&per_page={}",
606 base_path,
607 total_pages.max(1),
608 self.per_page
609 ),
610 next: if self.has_next() {
611 Some(format!(
612 "{}?page={}&per_page={}",
613 base_path,
614 self.page + 1,
615 self.per_page
616 ))
617 } else {
618 None
619 },
620 prev: if self.has_prev() {
621 Some(format!(
622 "{}?page={}&per_page={}",
623 base_path,
624 self.page - 1,
625 self.per_page
626 ))
627 } else {
628 None
629 },
630 };
631
632 PaginatedBody {
633 items: self.items.iter().collect(),
634 meta: PaginationMeta {
635 page: self.page,
636 per_page: self.per_page,
637 total: self.total,
638 total_pages,
639 },
640 links,
641 }
642 }
643}
644
645impl<T: Serialize + Send> crate::response::IntoResponse for Paginated<T> {
646 fn into_response(self) -> crate::response::Response {
647 let base_path = "";
650 let link_header = self.link_header(base_path);
651 let body = self.to_body_with_path(base_path);
652
653 let total_count = self.total.to_string();
654
655 match crate::json::to_vec_with_capacity(&body, 512) {
656 Ok(json_bytes) => {
657 let mut response = http::Response::builder()
658 .status(http::StatusCode::OK)
659 .header(http::header::CONTENT_TYPE, "application/json")
660 .header("X-Total-Count", &total_count)
661 .header("X-Total-Pages", self.total_pages().to_string())
662 .body(crate::response::Body::from(json_bytes))
663 .unwrap();
664
665 if !link_header.is_empty() {
666 response.headers_mut().insert(
667 http::header::LINK,
668 http::HeaderValue::from_str(&link_header)
669 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
670 );
671 }
672
673 response
674 }
675 Err(err) => crate::error::ApiError::internal(format!(
676 "Failed to serialize paginated response: {}",
677 err
678 ))
679 .into_response(),
680 }
681 }
682}
683
684#[derive(Debug, Clone)]
703pub struct CursorPaginated<T> {
704 pub items: Vec<T>,
706 pub next_cursor: Option<String>,
708 pub has_more: bool,
710}
711
712impl<T> CursorPaginated<T> {
713 pub fn new(items: Vec<T>, next_cursor: Option<String>, has_more: bool) -> Self {
715 Self {
716 items,
717 next_cursor,
718 has_more,
719 }
720 }
721
722 pub fn map<U, F: FnMut(T) -> U>(self, f: F) -> CursorPaginated<U> {
724 CursorPaginated {
725 items: self.items.into_iter().map(f).collect(),
726 next_cursor: self.next_cursor,
727 has_more: self.has_more,
728 }
729 }
730}
731
732#[derive(Serialize)]
733struct CursorPaginatedBody<T: Serialize> {
734 items: Vec<T>,
735 meta: CursorMeta,
736}
737
738#[derive(Serialize)]
739struct CursorMeta {
740 #[serde(skip_serializing_if = "Option::is_none")]
741 next_cursor: Option<String>,
742 has_more: bool,
743}
744
745impl<T: Serialize + Send> crate::response::IntoResponse for CursorPaginated<T> {
746 fn into_response(self) -> crate::response::Response {
747 let body = CursorPaginatedBody {
748 items: self.items,
749 meta: CursorMeta {
750 next_cursor: self.next_cursor,
751 has_more: self.has_more,
752 },
753 };
754
755 match crate::json::to_vec_with_capacity(&body, 512) {
756 Ok(json_bytes) => http::Response::builder()
757 .status(http::StatusCode::OK)
758 .header(http::header::CONTENT_TYPE, "application/json")
759 .body(crate::response::Body::from(json_bytes))
760 .unwrap(),
761 Err(err) => crate::error::ApiError::internal(format!(
762 "Failed to serialize cursor-paginated response: {}",
763 err
764 ))
765 .into_response(),
766 }
767 }
768}
769
770#[cfg(test)]
771mod tests {
772 use super::*;
773 use rustapi_openapi::schema::{JsonSchema2020, RustApiSchema, SchemaCtx, SchemaRef};
774 use serde::Serialize;
775
776 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
777 struct User {
778 id: i64,
779 name: String,
780 }
781
782 impl RustApiSchema for User {
783 fn schema(_: &mut SchemaCtx) -> SchemaRef {
784 let mut s = JsonSchema2020::object();
785 let mut props = std::collections::BTreeMap::new();
786 props.insert("id".to_string(), JsonSchema2020::integer());
787 props.insert("name".to_string(), JsonSchema2020::string());
788 s.properties = Some(props);
789 SchemaRef::Schema(Box::new(s))
790 }
791 fn name() -> std::borrow::Cow<'static, str> {
792 std::borrow::Cow::Borrowed("User")
793 }
794 }
795
796 #[test]
797 fn test_link_creation() {
798 let link = Link::new("/users/1")
799 .title("Get user")
800 .media_type("application/json");
801
802 assert_eq!(link.href, "/users/1");
803 assert_eq!(link.title, Some("Get user".to_string()));
804 assert_eq!(link.media_type, Some("application/json".to_string()));
805 }
806
807 #[test]
808 fn test_templated_link() {
809 let link = Link::templated("/users/{id}");
810 assert!(link.templated.unwrap());
811 }
812
813 #[test]
814 fn test_resource_with_links() {
815 let user = User {
816 id: 1,
817 name: "John".to_string(),
818 };
819 let resource = Resource::new(user)
820 .self_link("/users/1")
821 .link("orders", "/users/1/orders");
822
823 assert!(resource.links.contains_key("self"));
824 assert!(resource.links.contains_key("orders"));
825
826 let json = serde_json::to_string_pretty(&resource).unwrap();
827 assert!(json.contains("_links"));
828 assert!(json.contains("/users/1"));
829 }
830
831 #[test]
832 fn test_resource_collection() {
833 let users = vec![
834 User {
835 id: 1,
836 name: "John".to_string(),
837 },
838 User {
839 id: 2,
840 name: "Jane".to_string(),
841 },
842 ];
843
844 let page = PageInfo::calculate(100, 20, 2);
845 let collection = ResourceCollection::new("users", users)
846 .page_info(page)
847 .with_pagination("/api/users");
848
849 assert!(collection.links.contains_key("self"));
850 assert!(collection.links.contains_key("first"));
851 assert!(collection.links.contains_key("prev"));
852 assert!(collection.links.contains_key("next"));
853 }
854
855 #[test]
856 fn test_page_info_calculation() {
857 let page = PageInfo::calculate(95, 20, 0);
858 assert_eq!(page.total_pages, 5);
859 assert_eq!(page.size, 20);
860 }
861
862 #[test]
863 fn test_linkable_trait() {
864 let user = User {
865 id: 1,
866 name: "Test".to_string(),
867 };
868 let resource = user.with_links().self_link("/users/1");
869 assert!(resource.links.contains_key("self"));
870 }
871}