siderite_core/pagination/
mod.rs1mod cursor;
16
17pub use cursor::{CursorPage, CursorParams};
18
19use serde::ser::SerializeStruct;
20use serde::{Deserialize, Serialize, Serializer};
21use serde_json::{Number, Value, json};
22use siderite_orm::{Model, OrmError, QuerySet};
23use siderite_validation::dump::DumpSerialize;
24use siderite_validation::model::{Constraint, check};
25use siderite_validation::{Dump, DumpError, DumpOptions, Schema};
26use siderite_validation::{SchemaObject, SchemaRegistry};
27use siderite_validation::{Validate, ValidationContext};
28
29pub type Fetched<M> = Result<Page<M>, OrmError>;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
40pub struct PageParams<const MAX: u64 = 100, const DEFAULT: u64 = 20> {
41 #[serde(default)]
43 pub page: Option<u64>,
44 #[serde(default)]
46 pub per_page: Option<u64>,
47}
48
49impl<const MAX: u64, const DEFAULT: u64> PageParams<MAX, DEFAULT> {
50 const BOUNDS: () = assert!(
52 1 <= DEFAULT && DEFAULT <= MAX,
53 "PageParams needs 1 <= DEFAULT <= MAX"
54 );
55
56 pub fn page(&self) -> u64 {
58 self.page.unwrap_or(1)
59 }
60
61 pub fn per_page(&self) -> u64 {
63 let () = Self::BOUNDS;
64 self.per_page.unwrap_or(DEFAULT)
65 }
66
67 pub async fn fetch<M: Model>(&self, queryset: QuerySet<M>) -> Fetched<M> {
72 let page = queryset.paginate(self.page(), self.per_page()).await?;
73 Ok(page.into())
74 }
75}
76
77impl<const MAX: u64, const D: u64> Validate for PageParams<MAX, D> {
78 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
79 let Value::Object(fields) = input else {
80 return;
81 };
82 let one = Number::from(1_u64);
83 let max = Number::from(MAX);
84 for (key, upper) in [("page", None), ("per_page", Some(&max))] {
85 let Some(value) = fields.get_mut(key) else {
86 continue;
87 };
88 ctx.at(key, |ctx| {
89 let before = ctx.error_count();
90 Option::<u64>::prepare(value, ctx);
91 if ctx.error_count() > before {
92 return;
93 }
94 check(value, Constraint::Ge(&one), ctx);
95 if let Some(upper) = upper {
96 check(value, Constraint::Le(upper), ctx);
97 }
98 });
99 }
100 }
101}
102
103impl<const MAX: u64, const D: u64> Schema for PageParams<MAX, D> {
104 fn schema(_: &mut SchemaRegistry) -> SchemaObject {
105 let properties = json!({
106 "page": {
107 "type": "integer",
108 "minimum": 1,
109 "default": 1,
110 "description": "1-based page number.",
111 },
112 "per_page": {
113 "type": "integer",
114 "minimum": 1,
115 "maximum": MAX,
116 "default": D,
117 "description": "Items per page.",
118 },
119 });
120 SchemaObject::of_type("object").with("properties", properties)
121 }
122}
123
124#[derive(Debug, Clone, PartialEq, Serialize)]
129pub struct Page<T> {
130 pub items: Vec<T>,
132 pub total: u64,
134 pub page: u64,
136 pub per_page: u64,
138 pub total_pages: u64,
140}
141
142impl<T> Page<T> {
143 pub fn map<U>(self, f: impl FnMut(T) -> U) -> Page<U> {
145 Page {
146 items: self.items.into_iter().map(f).collect(),
147 total: self.total,
148 page: self.page,
149 per_page: self.per_page,
150 total_pages: self.total_pages,
151 }
152 }
153
154 pub fn try_map<U, E, F>(self, f: F) -> Result<Page<U>, E>
160 where
161 F: FnMut(T) -> Result<U, E>,
162 {
163 let items = self.items.into_iter().map(f).collect::<Result<_, _>>()?;
164 Ok(Page {
165 items,
166 total: self.total,
167 page: self.page,
168 per_page: self.per_page,
169 total_pages: self.total_pages,
170 })
171 }
172}
173
174impl<M> From<siderite_orm::Page<M>> for Page<M> {
175 fn from(page: siderite_orm::Page<M>) -> Self {
176 let total_pages = page.total_pages();
177 Self {
178 items: page.items,
179 total: page.total,
180 page: page.page,
181 per_page: page.per_page,
182 total_pages,
183 }
184 }
185}
186
187impl<T: Dump> Dump for Page<T> {
188 fn serialize_dump<S: Serializer>(
189 &self,
190 serializer: S,
191 opts: &DumpOptions,
192 ) -> Result<S::Ok, S::Error> {
193 let mut out = serializer.serialize_struct("Page", 5)?;
194 out.serialize_field("items", &DumpSerialize(&self.items[..], opts))?;
195 out.serialize_field("total", &self.total)?;
196 out.serialize_field("page", &self.page)?;
197 out.serialize_field("per_page", &self.per_page)?;
198 out.serialize_field("total_pages", &self.total_pages)?;
199 out.end()
200 }
201
202 fn dump(&self, opts: &DumpOptions) -> Result<Value, DumpError> {
203 Ok(serde_json::to_value(DumpSerialize(self, opts))?)
204 }
205}
206
207impl<T: Schema + 'static> Schema for Page<T> {
208 fn schema(registry: &mut SchemaRegistry) -> SchemaObject {
209 let items = registry.subschema::<Vec<T>>().into_value();
210 let count = registry.subschema::<u64>().into_value();
211 let properties = json!({
212 "items": items,
213 "total": count,
214 "page": count,
215 "per_page": count,
216 "total_pages": count,
217 });
218 let required = ["items", "total", "page", "per_page", "total_pages"];
219 SchemaObject::of_type("object")
220 .with("properties", properties)
221 .with("required", json!(required))
222 }
223}
224
225#[cfg(test)]
226mod cursor_tests;
227#[cfg(test)]
228mod tests;