1use crate::capability::{LIMIT_PAGINATION_DEFAULT, LIMIT_PAGINATION_MAX};
5use crate::{ChangeSeq, InodeId, NameKey, NamespaceId, RevisionNo};
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
8use std::num::NonZeroU32;
9use thiserror::Error;
10
11pub const DEFAULT_PAGE_LIMIT: u32 = 1_000;
13pub const DEFAULT_MAX_PAGE_LIMIT: u32 = 1_000;
15
16pub const PAGE_CURSOR_VERSION: u8 = 1;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
21pub struct EffectiveLimit(NonZeroU32);
22
23impl EffectiveLimit {
24 pub fn new(value: NonZeroU32) -> Self {
26 Self(value)
27 }
28
29 pub fn get(self) -> u32 {
31 self.0.get()
32 }
33
34 pub fn as_usize(self) -> usize {
36 self.0.get() as usize
37 }
38
39 pub fn limit_plus_one(self) -> usize {
41 self.as_usize().saturating_add(1)
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct PaginationPolicy {
48 default_limit: NonZeroU32,
49 max_limit: NonZeroU32,
50}
51
52impl PaginationPolicy {
53 pub fn new(
55 default_limit: NonZeroU32,
56 max_limit: NonZeroU32,
57 ) -> Result<Self, PaginationPolicyError> {
58 if default_limit > max_limit {
59 return Err(PaginationPolicyError::DefaultExceedsMax {
60 default_limit: default_limit.get(),
61 max_limit: max_limit.get(),
62 });
63 }
64 Ok(Self {
65 default_limit,
66 max_limit,
67 })
68 }
69
70 pub fn from_values(default_limit: u32, max_limit: u32) -> Result<Self, PaginationPolicyError> {
72 let default_limit =
73 NonZeroU32::new(default_limit).ok_or(PaginationPolicyError::ZeroDefaultLimit)?;
74 let max_limit = NonZeroU32::new(max_limit).ok_or(PaginationPolicyError::ZeroMaxLimit)?;
75 Self::new(default_limit, max_limit)
76 }
77
78 pub fn default_limit(self) -> NonZeroU32 {
80 self.default_limit
81 }
82
83 pub fn max_limit(self) -> NonZeroU32 {
85 self.max_limit
86 }
87
88 pub fn resolve_limit(self, requested: Option<u32>) -> Result<EffectiveLimit, LimitError> {
90 match requested {
91 None => Ok(EffectiveLimit(self.default_limit)),
92 Some(0) => Err(LimitError::Zero),
93 Some(value) if value > self.max_limit.get() => Err(LimitError::ExceedsMax {
94 requested: value,
95 max_limit: self.max_limit.get(),
96 }),
97 Some(value) => NonZeroU32::new(value)
98 .map(EffectiveLimit)
99 .ok_or(LimitError::Zero),
100 }
101 }
102
103 pub fn capability_limits(self) -> BTreeMap<String, u64> {
105 BTreeMap::from([
106 (
107 LIMIT_PAGINATION_DEFAULT.to_owned(),
108 u64::from(self.default_limit.get()),
109 ),
110 (
111 LIMIT_PAGINATION_MAX.to_owned(),
112 u64::from(self.max_limit.get()),
113 ),
114 ])
115 }
116}
117
118impl Default for PaginationPolicy {
119 fn default() -> Self {
120 let default_limit = const { NonZeroU32::new(DEFAULT_PAGE_LIMIT).unwrap() };
121 let max_limit = const { NonZeroU32::new(DEFAULT_MAX_PAGE_LIMIT).unwrap() };
122 Self {
123 default_limit,
124 max_limit,
125 }
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Error)]
131pub enum PaginationPolicyError {
132 #[error("pagination default limit must be greater than zero")]
134 ZeroDefaultLimit,
135 #[error("pagination max limit must be greater than zero")]
137 ZeroMaxLimit,
138 #[error("pagination default limit `{default_limit}` exceeds max limit `{max_limit}`")]
140 DefaultExceedsMax {
141 default_limit: u32,
143 max_limit: u32,
145 },
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Error)]
150pub enum LimitError {
151 #[error("limit must be greater than zero")]
153 Zero,
154 #[error("limit `{requested}` exceeds max limit `{max_limit}`")]
156 ExceedsMax {
157 requested: u32,
159 max_limit: u32,
161 },
162}
163
164#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct PageRequest<C> {
170 pub limit: EffectiveLimit,
172 pub cursor: Option<C>,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct Page<T, C> {
182 pub items: Vec<T>,
184 pub next_cursor: Option<C>,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
201pub struct DirectoryPageCursor {
202 pub head_seq: ChangeSeq,
204 #[serde(rename = "dir_inode_id")]
207 pub directory_inode_id: InodeId,
208 pub last_name_key: NameKey,
210}
211
212impl PageCursor for DirectoryPageCursor {
213 const KIND: &'static str = "directory";
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223pub struct FileRevisionsPageCursor {
224 pub head_seq: ChangeSeq,
226 pub inode_id: InodeId,
228 pub last_revision_no: RevisionNo,
230 pub last_committed_seq: ChangeSeq,
232 pub last_revision_delta_index: u32,
234}
235
236impl PageCursor for FileRevisionsPageCursor {
237 const KIND: &'static str = "file_revisions";
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
249pub struct TrashPageCursor {
250 pub head_seq: ChangeSeq,
252 pub last_deleted_at_seq: ChangeSeq,
254 pub last_root_inode_id: InodeId,
256}
257
258impl PageCursor for TrashPageCursor {
259 const KIND: &'static str = "trash";
260}
261
262#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
268pub struct GrepPageCursor {
269 pub head_seq: ChangeSeq,
271 pub last_inode_id: InodeId,
273 pub last_byte_offset: u64,
277 pub fingerprint: u64,
281}
282
283impl PageCursor for GrepPageCursor {
284 const KIND: &'static str = "grep";
285}
286
287pub trait PageCursor: Serialize + serde::de::DeserializeOwned {
293 const KIND: &'static str;
295}
296
297#[derive(Serialize, Deserialize)]
298struct CursorEnvelope<C> {
299 #[serde(rename = "v")]
301 version: u8,
302 kind: String,
303 #[serde(flatten)]
304 cursor: C,
305}
306
307pub fn encode_cursor<C: PageCursor>(cursor: &C) -> Result<String, PageCursorError> {
309 let bytes = serde_json::to_vec(&CursorEnvelope {
310 version: PAGE_CURSOR_VERSION,
311 kind: C::KIND.to_owned(),
312 cursor,
313 })
314 .map_err(|error| PageCursorError::InvalidJson(error.to_string()))?;
315 Ok(crate::hex::hex_encode_bytes(&bytes))
316}
317
318#[derive(Deserialize)]
321struct CursorHeader {
322 #[serde(rename = "v")]
323 version: u8,
324 kind: String,
325}
326
327pub fn decode_cursor<C: PageCursor>(value: &str) -> Result<C, PageCursorError> {
329 let bytes =
330 crate::hex::hex_decode_bytes(value).map_err(|_| PageCursorError::InvalidEncoding)?;
331 let header: CursorHeader = serde_json::from_slice(&bytes)
332 .map_err(|error| PageCursorError::InvalidJson(error.to_string()))?;
333 if header.version != PAGE_CURSOR_VERSION {
334 return Err(PageCursorError::UnsupportedVersion {
335 expected: PAGE_CURSOR_VERSION,
336 actual: header.version,
337 });
338 }
339 if header.kind != C::KIND {
340 return Err(PageCursorError::WrongKind {
341 expected: C::KIND,
342 actual: header.kind,
343 });
344 }
345 let envelope: CursorEnvelope<C> = serde_json::from_slice(&bytes)
346 .map_err(|error| PageCursorError::InvalidJson(error.to_string()))?;
347 Ok(envelope.cursor)
348}
349
350pub trait NamespaceCursor: PageCursor {
360 fn namespace_id(&self) -> &NamespaceId;
362
363 fn last_key(&self) -> Option<&str>;
365
366 fn key_prefix(&self) -> String;
368}
369
370pub fn decode_namespace_cursor<C: NamespaceCursor>(
372 token: &str,
373 expected_namespace_id: &NamespaceId,
374) -> Result<C, NamespaceCursorError> {
375 let cursor: C = decode_cursor(token)?;
376 if cursor.namespace_id() != expected_namespace_id {
377 return Err(NamespaceCursorError::ForeignNamespace);
378 }
379 let prefix = cursor.key_prefix();
380 if cursor
381 .last_key()
382 .is_some_and(|key| !key.starts_with(&prefix))
383 {
384 return Err(NamespaceCursorError::OutsideKeyspace);
385 }
386 Ok(cursor)
387}
388
389#[derive(Debug, Clone, PartialEq, Eq, Error)]
391pub enum NamespaceCursorError {
392 #[error(transparent)]
395 Malformed(#[from] PageCursorError),
396 #[error("cursor belongs to a different namespace")]
398 ForeignNamespace,
399 #[error("cursor names a key outside the enumeration it resumes")]
401 OutsideKeyspace,
402}
403
404#[derive(Debug, Clone, PartialEq, Eq, Error)]
406pub enum PageCursorError {
407 #[error("invalid page cursor encoding")]
409 InvalidEncoding,
410 #[error("invalid page cursor JSON: {0}")]
412 InvalidJson(String),
413 #[error("page cursor kind `{actual}` cannot be used as `{expected}` cursor")]
415 WrongKind {
416 expected: &'static str,
418 actual: String,
420 },
421 #[error("unsupported page cursor version `{actual}`; expected `{expected}`")]
423 UnsupportedVersion {
424 expected: u8,
426 actual: u8,
428 },
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434
435 #[test]
436 fn default_policy_resolves_omitted_limit_to_default() {
437 let policy = PaginationPolicy::default();
438 let limit = policy.resolve_limit(None).expect("default limit");
439
440 assert_eq!(limit.get(), DEFAULT_PAGE_LIMIT);
441 assert_eq!(limit.limit_plus_one(), 1_001);
442 }
443
444 #[test]
445 fn policy_rejects_invalid_limits() {
446 let policy = PaginationPolicy::default();
447
448 assert_eq!(policy.resolve_limit(Some(0)), Err(LimitError::Zero));
449 assert_eq!(
450 policy.resolve_limit(Some(DEFAULT_MAX_PAGE_LIMIT + 1)),
451 Err(LimitError::ExceedsMax {
452 requested: DEFAULT_MAX_PAGE_LIMIT + 1,
453 max_limit: DEFAULT_MAX_PAGE_LIMIT,
454 })
455 );
456 }
457
458 #[test]
459 fn policy_rejects_default_above_max() {
460 assert_eq!(
461 PaginationPolicy::from_values(10, 5),
462 Err(PaginationPolicyError::DefaultExceedsMax {
463 default_limit: 10,
464 max_limit: 5,
465 })
466 );
467 }
468
469 #[test]
470 fn policy_exports_capability_limits() {
471 let limits = PaginationPolicy::default().capability_limits();
472
473 assert_eq!(
474 limits.get(LIMIT_PAGINATION_DEFAULT),
475 Some(&u64::from(DEFAULT_PAGE_LIMIT))
476 );
477 assert_eq!(
478 limits.get(LIMIT_PAGINATION_MAX),
479 Some(&u64::from(DEFAULT_MAX_PAGE_LIMIT))
480 );
481 }
482
483 #[test]
484 fn directory_cursor_round_trips() {
485 let cursor = DirectoryPageCursor {
486 head_seq: ChangeSeq(11),
487 directory_inode_id: InodeId(7),
488 last_name_key: NameKey::parse("plan.md").expect("name key"),
489 };
490
491 let encoded = encode_cursor(&cursor).expect("encode cursor");
492 let decoded: DirectoryPageCursor = decode_cursor(&encoded).expect("decode cursor");
493
494 assert_eq!(decoded, cursor);
495 }
496
497 #[test]
498 fn file_revisions_cursor_round_trips() {
499 let cursor = FileRevisionsPageCursor {
500 head_seq: ChangeSeq(11),
501 inode_id: InodeId(7),
502 last_revision_no: RevisionNo(5),
503 last_committed_seq: ChangeSeq(10),
504 last_revision_delta_index: 3,
505 };
506
507 let encoded = encode_cursor(&cursor).expect("encode cursor");
508 let decoded: FileRevisionsPageCursor = decode_cursor(&encoded).expect("decode cursor");
509
510 assert_eq!(decoded, cursor);
511 }
512
513 #[test]
514 fn cursor_kind_must_match_decoder() {
515 let cursor = FileRevisionsPageCursor {
516 head_seq: ChangeSeq(11),
517 inode_id: InodeId(7),
518 last_revision_no: RevisionNo(5),
519 last_committed_seq: ChangeSeq(10),
520 last_revision_delta_index: 3,
521 };
522 let encoded = encode_cursor(&cursor).expect("encode cursor");
523
524 assert_eq!(
525 decode_cursor::<DirectoryPageCursor>(&encoded),
526 Err(PageCursorError::WrongKind {
527 expected: "directory",
528 actual: "file_revisions".to_owned(),
529 })
530 );
531 }
532
533 #[test]
534 fn malformed_cursor_is_invalid_encoding() {
535 assert_eq!(
536 decode_cursor::<DirectoryPageCursor>("not-hex"),
537 Err(PageCursorError::InvalidEncoding)
538 );
539 }
540
541 #[test]
542 fn unsupported_cursor_version_is_rejected() {
543 let bytes = serde_json::to_vec(&CursorEnvelope {
544 version: PAGE_CURSOR_VERSION + 1,
545 kind: DirectoryPageCursor::KIND.to_owned(),
546 cursor: DirectoryPageCursor {
547 head_seq: ChangeSeq(11),
548 directory_inode_id: InodeId(7),
549 last_name_key: NameKey::parse("plan.md").expect("name key"),
550 },
551 })
552 .expect("encode cursor");
553 let encoded = crate::hex::hex_encode_bytes(&bytes);
554
555 assert_eq!(
556 decode_cursor::<DirectoryPageCursor>(&encoded),
557 Err(PageCursorError::UnsupportedVersion {
558 expected: PAGE_CURSOR_VERSION,
559 actual: PAGE_CURSOR_VERSION + 1,
560 })
561 );
562 }
563}