1use serde::{Deserialize, Serialize};
2
3#[cfg_attr(feature = "with-utoipa", derive(utoipa::ToSchema))]
4#[derive(Clone, Debug, Serialize, Deserialize)]
5pub struct PageInfo {
6 pub next_cursor: Option<String>,
7 pub prev_cursor: Option<String>,
8 pub limit: u64,
9}
10
11#[derive(Clone, Debug, Serialize, Deserialize)]
12pub struct Page<T> {
13 pub items: Vec<T>,
14 pub page_info: PageInfo,
15}
16
17#[cfg(feature = "with-utoipa")]
23impl<T> utoipa::PartialSchema for Page<T>
24where
25 T: utoipa::ToSchema + utoipa::PartialSchema,
26{
27 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
28 use utoipa::openapi::schema::{ArrayBuilder, ObjectBuilder};
29
30 ObjectBuilder::new()
31 .property(
32 "items",
33 ArrayBuilder::new().items(
34 utoipa::openapi::RefOr::<utoipa::openapi::schema::Schema>::Ref(
35 utoipa::openapi::Ref::from_schema_name(T::name().to_string()),
36 ),
37 ),
38 )
39 .required("items")
40 .property(
41 "page_info",
42 utoipa::openapi::RefOr::<utoipa::openapi::schema::Schema>::Ref(
43 utoipa::openapi::Ref::from_schema_name(
44 <PageInfo as utoipa::ToSchema>::name().to_string(),
45 ),
46 ),
47 )
48 .required("page_info")
49 .into()
50 }
51}
52
53#[cfg(feature = "with-utoipa")]
54impl<T> utoipa::ToSchema for Page<T>
55where
56 T: utoipa::ToSchema + utoipa::PartialSchema,
57{
58 fn name() -> std::borrow::Cow<'static, str> {
59 std::borrow::Cow::Owned(format!("Page_{}", T::name()))
60 }
61
62 fn schemas(
63 schemas: &mut Vec<(
64 String,
65 utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
66 )>,
67 ) {
68 schemas.push((
70 <PageInfo as utoipa::ToSchema>::name().to_string(),
71 <PageInfo as utoipa::PartialSchema>::schema(),
72 ));
73 <PageInfo as utoipa::ToSchema>::schemas(schemas);
74
75 schemas.push((
77 <T as utoipa::ToSchema>::name().to_string(),
78 <T as utoipa::PartialSchema>::schema(),
79 ));
80 <T as utoipa::ToSchema>::schemas(schemas);
81 }
82}
83
84impl<T> Page<T> {
85 #[must_use]
87 pub fn new(items: Vec<T>, page_info: PageInfo) -> Self {
88 Self { items, page_info }
89 }
90
91 #[must_use]
93 pub fn empty(limit: u64) -> Self {
94 Self {
95 items: Vec::new(),
96 page_info: PageInfo {
97 next_cursor: None,
98 prev_cursor: None,
99 limit,
100 },
101 }
102 }
103
104 pub fn map_items<U>(self, mut f: impl FnMut(T) -> U) -> Page<U> {
106 Page {
107 items: self.items.into_iter().map(&mut f).collect(),
108 page_info: self.page_info,
109 }
110 }
111}
112
113#[cfg(all(test, feature = "with-utoipa"))]
114#[cfg_attr(coverage_nightly, coverage(off))]
115mod tests {
116 use super::*;
117
118 #[derive(utoipa::ToSchema)]
120 struct DummyItem {
121 #[allow(dead_code)]
122 pub value: String,
123 }
124
125 #[test]
126 fn test_page_name_includes_generic() {
127 use utoipa::ToSchema;
128 let name = <Page<DummyItem> as ToSchema>::name();
129 assert_eq!(name.as_ref(), "Page_DummyItem");
130 }
131
132 #[test]
133 fn test_page_schemas_includes_inner_type() {
134 use utoipa::ToSchema;
135 let mut schemas = Vec::new();
136 <Page<DummyItem> as ToSchema>::schemas(&mut schemas);
137
138 let names: Vec<&str> = schemas.iter().map(|(n, _)| n.as_str()).collect();
139 assert!(
140 names.contains(&"DummyItem"),
141 "Expected DummyItem in schemas, got: {names:?}"
142 );
143 assert!(
144 names.contains(&"PageInfo"),
145 "Expected PageInfo in schemas, got: {names:?}"
146 );
147 }
148}