siderite_core/pagination/
cursor.rs1use serde::ser::SerializeStruct;
15use serde::{Deserialize, Serialize, Serializer};
16use serde_json::{Number, Value, json};
17use siderite_orm::{Model, OrmError, QuerySet};
18use siderite_validation::dump::DumpSerialize;
19use siderite_validation::model::{Constraint, check};
20use siderite_validation::{Dump, DumpError, DumpOptions, Schema};
21use siderite_validation::{SchemaObject, SchemaRegistry};
22use siderite_validation::{Validate, ValidationContext};
23
24pub type Fetched<M> = Result<CursorPage<M>, OrmError>;
26
27#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
36pub struct CursorParams<const MAX: u64 = 100, const DEFAULT: u64 = 20> {
37 #[serde(default)]
39 pub after: Option<String>,
40 #[serde(default)]
42 pub before: Option<String>,
43 #[serde(default)]
45 pub limit: Option<u64>,
46}
47
48impl<const MAX: u64, const DEFAULT: u64> CursorParams<MAX, DEFAULT> {
49 const BOUNDS: () = assert!(
51 1 <= DEFAULT && DEFAULT <= MAX,
52 "CursorParams needs 1 <= DEFAULT <= MAX"
53 );
54
55 pub fn limit(&self) -> u64 {
57 let () = Self::BOUNDS;
58 self.limit.unwrap_or(DEFAULT)
59 }
60
61 pub async fn fetch<M: Model>(&self, queryset: QuerySet<M>) -> Fetched<M> {
67 let mut queryset = queryset;
68 if let Some(token) = &self.after {
69 queryset = queryset.after(token);
70 }
71 if let Some(token) = &self.before {
72 queryset = queryset.before(token);
73 }
74 let page = queryset.cursor_page(self.limit()).await?;
75 Ok(page.into())
76 }
77}
78
79impl<const MAX: u64, const D: u64> Validate for CursorParams<MAX, D> {
80 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
81 let Value::Object(fields) = input else {
82 return;
83 };
84 for key in ["after", "before"] {
85 if let Some(value) = fields.get_mut(key) {
86 ctx.at(key, |ctx| Option::<String>::prepare(value, ctx));
87 }
88 }
89 let given = |key| fields.get(key).is_some_and(|v| !v.is_null());
90 if given("after") && given("before") {
91 ctx.at("before", |ctx| {
92 ctx.error("exclusive", "cannot be combined with `after`");
93 });
94 }
95 let Some(value) = fields.get_mut("limit") else {
96 return;
97 };
98 ctx.at("limit", |ctx| {
99 let before = ctx.error_count();
100 Option::<u64>::prepare(value, ctx);
101 if ctx.error_count() > before {
102 return;
103 }
104 check(value, Constraint::Ge(&Number::from(1_u64)), ctx);
105 check(value, Constraint::Le(&Number::from(MAX)), ctx);
106 });
107 }
108}
109
110impl<const MAX: u64, const D: u64> Schema for CursorParams<MAX, D> {
111 fn schema(_: &mut SchemaRegistry) -> SchemaObject {
112 let properties = json!({
113 "after": {
114 "type": "string",
115 "description": "The `next` cursor of the previous page. \
116 Not combined with `before`.",
117 },
118 "before": {
119 "type": "string",
120 "description": "The `previous` cursor of a page, to read \
121 the page before it. Not combined with `after`.",
122 },
123 "limit": {
124 "type": "integer",
125 "minimum": 1,
126 "maximum": MAX,
127 "default": D,
128 "description": "Items per page.",
129 },
130 });
131 SchemaObject::of_type("object").with("properties", properties)
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Serialize)]
141pub struct CursorPage<T> {
142 pub items: Vec<T>,
144 pub next: Option<String>,
146 pub previous: Option<String>,
148}
149
150impl<T> CursorPage<T> {
151 pub fn map<U>(self, f: impl FnMut(T) -> U) -> CursorPage<U> {
153 CursorPage {
154 items: self.items.into_iter().map(f).collect(),
155 next: self.next,
156 previous: self.previous,
157 }
158 }
159
160 pub fn try_map<U, E, F>(self, f: F) -> Result<CursorPage<U>, E>
166 where
167 F: FnMut(T) -> Result<U, E>,
168 {
169 let items = self.items.into_iter().map(f).collect::<Result<_, _>>()?;
170 Ok(CursorPage {
171 items,
172 next: self.next,
173 previous: self.previous,
174 })
175 }
176}
177
178impl<M> From<siderite_orm::CursorPage<M>> for CursorPage<M> {
179 fn from(page: siderite_orm::CursorPage<M>) -> Self {
180 Self {
181 items: page.items,
182 next: page.next,
183 previous: page.previous,
184 }
185 }
186}
187
188impl<T: Dump> Dump for CursorPage<T> {
189 fn serialize_dump<S: Serializer>(
190 &self,
191 serializer: S,
192 opts: &DumpOptions,
193 ) -> Result<S::Ok, S::Error> {
194 let mut out = serializer.serialize_struct("CursorPage", 3)?;
195 out.serialize_field("items", &DumpSerialize(&self.items[..], opts))?;
196 out.serialize_field("next", &self.next)?;
197 out.serialize_field("previous", &self.previous)?;
198 out.end()
199 }
200
201 fn dump(&self, opts: &DumpOptions) -> Result<Value, DumpError> {
202 Ok(serde_json::to_value(DumpSerialize(self, opts))?)
203 }
204}
205
206impl<T: Schema + 'static> Schema for CursorPage<T> {
207 fn schema(registry: &mut SchemaRegistry) -> SchemaObject {
208 let items = registry.subschema::<Vec<T>>().into_value();
209 let cursor = registry.subschema::<Option<String>>().into_value();
210 let properties = json!({
211 "items": items,
212 "next": cursor,
213 "previous": cursor,
214 });
215 let required = ["items", "next", "previous"];
216 SchemaObject::of_type("object")
217 .with("properties", properties)
218 .with("required", json!(required))
219 }
220}