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;
15pub const DEFAULT_MAX_PAGE_LIMIT: u32 = 1_000;
19
20pub const PAGE_CURSOR_FORMAT_VERSION: u8 = 1;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
25pub struct EffectiveLimit(NonZeroU32);
26
27impl EffectiveLimit {
28 pub fn new(value: NonZeroU32) -> Self {
30 Self(value)
31 }
32
33 pub fn get(self) -> u32 {
35 self.0.get()
36 }
37
38 pub fn as_usize(self) -> usize {
40 self.0.get() as usize
41 }
42
43 pub fn limit_plus_one(self) -> usize {
45 self.as_usize().saturating_add(1)
46 }
47
48 pub fn finish_page<R, C>(self, rows: &mut Vec<R>, cursor: impl FnOnce(&R) -> C) -> Option<C> {
50 if rows.len() <= self.as_usize() {
51 return None;
52 }
53 rows.truncate(self.as_usize());
54 rows.last().map(cursor)
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub struct PaginationPolicy {
61 default_limit: NonZeroU32,
62 max_limit: NonZeroU32,
63}
64
65impl PaginationPolicy {
66 pub fn default_limit(self) -> NonZeroU32 {
68 self.default_limit
69 }
70
71 pub fn max_limit(self) -> NonZeroU32 {
73 self.max_limit
74 }
75
76 pub fn resolve_limit(self, requested: Option<u32>) -> Result<EffectiveLimit, LimitError> {
78 match requested {
79 None => Ok(EffectiveLimit(self.default_limit)),
80 Some(value) if value > self.max_limit.get() => Err(LimitError::ExceedsMax {
81 requested: value,
82 max_limit: self.max_limit.get(),
83 }),
84 Some(value) => NonZeroU32::new(value)
85 .map(EffectiveLimit)
86 .ok_or(LimitError::Zero),
87 }
88 }
89
90 pub fn capability_limits(self) -> BTreeMap<String, u64> {
92 BTreeMap::from([
93 (
94 LIMIT_PAGINATION_DEFAULT.to_owned(),
95 u64::from(self.default_limit.get()),
96 ),
97 (
98 LIMIT_PAGINATION_MAX.to_owned(),
99 u64::from(self.max_limit.get()),
100 ),
101 ])
102 }
103}
104
105impl Default for PaginationPolicy {
106 fn default() -> Self {
107 let default_limit = const { NonZeroU32::new(DEFAULT_PAGE_LIMIT).unwrap() };
111 let max_limit = const { NonZeroU32::new(DEFAULT_MAX_PAGE_LIMIT).unwrap() };
112 Self {
113 default_limit,
114 max_limit,
115 }
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Error)]
121#[non_exhaustive]
122pub enum LimitError {
123 #[error("limit must be greater than zero")]
125 Zero,
126 #[error("limit `{requested}` exceeds max limit `{max_limit}`")]
128 ExceedsMax {
129 requested: u32,
131 max_limit: u32,
133 },
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct PageRequest<C> {
142 pub limit: EffectiveLimit,
144 pub cursor: Option<C>,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct Page<T, C> {
154 pub items: Vec<T>,
156 pub next_cursor: Option<C>,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175pub struct DirectoryPageCursor {
176 pub head_seq: ChangeSeq,
178 #[serde(rename = "dir_inode_id")]
181 pub directory_inode_id: InodeId,
182 pub last_name_key: NameKey,
184}
185
186impl PageCursor for DirectoryPageCursor {
187 const KIND: &'static str = "directory";
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
197pub struct FileRevisionsPageCursor {
198 pub head_seq: ChangeSeq,
200 pub inode_id: InodeId,
202 pub last_revision_no: RevisionNo,
204 pub last_committed_seq: ChangeSeq,
206 pub last_revision_delta_index: u32,
208}
209
210impl PageCursor for FileRevisionsPageCursor {
211 const KIND: &'static str = "file_revisions";
212}
213
214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223pub struct TrashPageCursor {
224 pub head_seq: ChangeSeq,
226 pub last_deletion_seq: ChangeSeq,
228 pub last_root_inode_id: InodeId,
230}
231
232impl PageCursor for TrashPageCursor {
233 const KIND: &'static str = "trash";
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242pub struct GrepPageCursor {
243 pub head_seq: ChangeSeq,
245 pub last_inode_id: InodeId,
247 pub last_byte_offset: u64,
251 pub fingerprint: u64,
255}
256
257impl PageCursor for GrepPageCursor {
258 const KIND: &'static str = "grep";
259}
260
261pub trait PageCursor: Serialize + serde::de::DeserializeOwned {
267 const KIND: &'static str;
269}
270
271#[derive(Serialize, Deserialize)]
272struct CursorEnvelope<C> {
273 format_version: u8,
274 kind: String,
275 #[serde(flatten)]
276 cursor: C,
277}
278
279pub fn encode_cursor<C: PageCursor>(cursor: &C) -> Result<String, PageCursorError> {
281 let bytes = serde_json::to_vec(&CursorEnvelope {
282 format_version: PAGE_CURSOR_FORMAT_VERSION,
283 kind: C::KIND.to_owned(),
284 cursor,
285 })
286 .map_err(|error| PageCursorError::InvalidJson(error.to_string()))?;
287 Ok(crate::hex::hex_encode_bytes(&bytes))
288}
289
290#[derive(Deserialize)]
293struct CursorHeader {
294 format_version: u8,
295 kind: String,
296}
297
298pub fn decode_cursor<C: PageCursor>(value: &str) -> Result<C, PageCursorError> {
300 let bytes =
301 crate::hex::hex_decode_bytes(value).map_err(|_| PageCursorError::InvalidEncoding)?;
302 let header: CursorHeader = serde_json::from_slice(&bytes)
303 .map_err(|error| PageCursorError::InvalidJson(error.to_string()))?;
304 if header.format_version != PAGE_CURSOR_FORMAT_VERSION {
305 return Err(PageCursorError::UnsupportedVersion {
306 expected: PAGE_CURSOR_FORMAT_VERSION,
307 actual: header.format_version,
308 });
309 }
310 if header.kind != C::KIND {
311 return Err(PageCursorError::WrongKind {
312 expected: C::KIND,
313 actual: header.kind,
314 });
315 }
316 let envelope: CursorEnvelope<C> = serde_json::from_slice(&bytes)
317 .map_err(|error| PageCursorError::InvalidJson(error.to_string()))?;
318 Ok(envelope.cursor)
319}
320
321pub trait NamespaceCursor: PageCursor {
331 fn namespace_id(&self) -> &NamespaceId;
333
334 fn last_key(&self) -> Option<&str>;
336
337 fn key_prefix(&self) -> String;
339}
340
341pub fn decode_namespace_cursor<C: NamespaceCursor>(
343 token: &str,
344 expected_namespace_id: &NamespaceId,
345) -> Result<C, NamespaceCursorError> {
346 let cursor: C = decode_cursor(token)?;
347 if cursor.namespace_id() != expected_namespace_id {
348 return Err(NamespaceCursorError::ForeignNamespace);
349 }
350 let prefix = cursor.key_prefix();
351 if cursor
352 .last_key()
353 .is_some_and(|key| !key.starts_with(&prefix))
354 {
355 return Err(NamespaceCursorError::OutsideKeyspace);
356 }
357 Ok(cursor)
358}
359
360#[derive(Debug, Clone, PartialEq, Eq, Error)]
362#[non_exhaustive]
363pub enum NamespaceCursorError {
364 #[error(transparent)]
367 Malformed(#[from] PageCursorError),
368 #[error("cursor belongs to a different namespace")]
370 ForeignNamespace,
371 #[error("cursor names a key outside the enumeration it resumes")]
373 OutsideKeyspace,
374}
375
376#[derive(Debug, Clone, PartialEq, Eq, Error)]
378#[non_exhaustive]
379pub enum PageCursorError {
380 #[error("invalid page cursor encoding")]
382 InvalidEncoding,
383 #[error("invalid page cursor JSON: {0}")]
385 InvalidJson(String),
386 #[error("page cursor kind `{actual}` cannot be used as `{expected}` cursor")]
388 WrongKind {
389 expected: &'static str,
391 actual: String,
393 },
394 #[error("unsupported page cursor version `{actual}`; expected `{expected}`")]
396 UnsupportedVersion {
397 expected: u8,
399 actual: u8,
401 },
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 #[test]
409 fn default_policy_resolves_omitted_limit_to_default() {
410 let policy = PaginationPolicy::default();
411 let limit = policy.resolve_limit(None).expect("default limit");
412
413 assert_eq!(limit.get(), DEFAULT_PAGE_LIMIT);
414 assert_eq!(limit.limit_plus_one(), 1_001);
415 }
416
417 #[test]
418 fn policy_rejects_invalid_limits() {
419 let policy = PaginationPolicy::default();
420
421 assert_eq!(policy.resolve_limit(Some(0)), Err(LimitError::Zero));
422 assert_eq!(
423 policy.resolve_limit(Some(DEFAULT_MAX_PAGE_LIMIT + 1)),
424 Err(LimitError::ExceedsMax {
425 requested: DEFAULT_MAX_PAGE_LIMIT + 1,
426 max_limit: DEFAULT_MAX_PAGE_LIMIT,
427 })
428 );
429 }
430
431 #[test]
432 fn policy_exports_capability_limits() {
433 let limits = PaginationPolicy::default().capability_limits();
434
435 assert_eq!(
436 limits.get("pagination.default_limit"),
437 Some(&u64::from(DEFAULT_PAGE_LIMIT))
438 );
439 assert_eq!(
440 limits.get("pagination.max_limit"),
441 Some(&u64::from(DEFAULT_MAX_PAGE_LIMIT))
442 );
443 }
444
445 #[test]
446 fn directory_cursor_round_trips() {
447 let cursor = DirectoryPageCursor {
448 head_seq: ChangeSeq(11),
449 directory_inode_id: InodeId(7),
450 last_name_key: NameKey::parse("plan.md").expect("name key"),
451 };
452
453 let encoded = encode_cursor(&cursor).expect("encode cursor");
454 let decoded: DirectoryPageCursor = decode_cursor(&encoded).expect("decode cursor");
455
456 assert_eq!(decoded, cursor);
457 }
458
459 #[test]
460 fn file_revisions_cursor_round_trips() {
461 let cursor = FileRevisionsPageCursor {
462 head_seq: ChangeSeq(11),
463 inode_id: InodeId(7),
464 last_revision_no: RevisionNo(5),
465 last_committed_seq: ChangeSeq(10),
466 last_revision_delta_index: 3,
467 };
468
469 let encoded = encode_cursor(&cursor).expect("encode cursor");
470 let decoded: FileRevisionsPageCursor = decode_cursor(&encoded).expect("decode cursor");
471
472 assert_eq!(decoded, cursor);
473 }
474
475 #[test]
476 fn trash_cursor_round_trips() {
477 let cursor = TrashPageCursor {
478 head_seq: ChangeSeq(11),
479 last_deletion_seq: ChangeSeq(10),
480 last_root_inode_id: InodeId(7),
481 };
482
483 let encoded = encode_cursor(&cursor).expect("encode cursor");
484 let decoded: TrashPageCursor = decode_cursor(&encoded).expect("decode cursor");
485
486 assert_eq!(decoded, cursor);
487 }
488
489 #[test]
490 fn grep_cursor_round_trips() {
491 let cursor = GrepPageCursor {
492 head_seq: ChangeSeq(11),
493 last_inode_id: InodeId(7),
494 last_byte_offset: 13,
495 fingerprint: 17,
496 };
497
498 let encoded = encode_cursor(&cursor).expect("encode cursor");
499 let decoded: GrepPageCursor = decode_cursor(&encoded).expect("decode cursor");
500
501 assert_eq!(decoded, cursor);
502 }
503
504 #[test]
505 fn cursor_kind_must_match_decoder() {
506 let cursor = FileRevisionsPageCursor {
507 head_seq: ChangeSeq(11),
508 inode_id: InodeId(7),
509 last_revision_no: RevisionNo(5),
510 last_committed_seq: ChangeSeq(10),
511 last_revision_delta_index: 3,
512 };
513 let encoded = encode_cursor(&cursor).expect("encode cursor");
514
515 assert_eq!(
516 decode_cursor::<DirectoryPageCursor>(&encoded),
517 Err(PageCursorError::WrongKind {
518 expected: "directory",
519 actual: "file_revisions".to_owned(),
520 })
521 );
522 }
523
524 #[test]
525 fn malformed_cursor_is_invalid_encoding() {
526 assert_eq!(
527 decode_cursor::<DirectoryPageCursor>("not-hex"),
528 Err(PageCursorError::InvalidEncoding)
529 );
530 }
531
532 #[test]
533 fn unsupported_cursor_version_is_rejected() {
534 let bytes = serde_json::to_vec(&CursorEnvelope {
535 format_version: PAGE_CURSOR_FORMAT_VERSION + 1,
536 kind: DirectoryPageCursor::KIND.to_owned(),
537 cursor: DirectoryPageCursor {
538 head_seq: ChangeSeq(11),
539 directory_inode_id: InodeId(7),
540 last_name_key: NameKey::parse("plan.md").expect("name key"),
541 },
542 })
543 .expect("encode cursor");
544 let encoded = crate::hex::hex_encode_bytes(&bytes);
545
546 assert_eq!(
547 decode_cursor::<DirectoryPageCursor>(&encoded),
548 Err(PageCursorError::UnsupportedVersion {
549 expected: PAGE_CURSOR_FORMAT_VERSION,
550 actual: PAGE_CURSOR_FORMAT_VERSION + 1,
551 })
552 );
553 }
554
555 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
556 struct TestNamespaceCursor {
557 namespace_id: NamespaceId,
558 last_key: String,
559 }
560
561 impl PageCursor for TestNamespaceCursor {
562 const KIND: &'static str = "test_namespace";
563 }
564
565 impl NamespaceCursor for TestNamespaceCursor {
566 fn namespace_id(&self) -> &NamespaceId {
567 &self.namespace_id
568 }
569
570 fn last_key(&self) -> Option<&str> {
571 Some(&self.last_key)
572 }
573
574 fn key_prefix(&self) -> String {
575 format!("namespaces/{}/items/", self.namespace_id)
576 }
577 }
578
579 #[test]
580 fn namespace_cursor_accepts_its_namespace_and_keyspace() {
581 let namespace_id = NamespaceId::parse("demo").expect("namespace id");
582 let cursor = TestNamespaceCursor {
583 namespace_id: namespace_id.clone(),
584 last_key: "namespaces/demo/items/item-42".to_owned(),
585 };
586 let encoded = encode_cursor(&cursor).expect("encode cursor");
587
588 assert_eq!(
589 decode_namespace_cursor::<TestNamespaceCursor>(&encoded, &namespace_id)
590 .expect("decode namespace cursor"),
591 cursor
592 );
593 }
594
595 #[test]
596 fn namespace_cursor_rejects_a_different_namespace() {
597 let cursor = TestNamespaceCursor {
598 namespace_id: NamespaceId::parse("demo").expect("namespace id"),
599 last_key: "namespaces/demo/items/item-42".to_owned(),
600 };
601 let encoded = encode_cursor(&cursor).expect("encode cursor");
602
603 assert_eq!(
604 decode_namespace_cursor::<TestNamespaceCursor>(
605 &encoded,
606 &NamespaceId::parse("other").expect("other namespace id")
607 ),
608 Err(NamespaceCursorError::ForeignNamespace)
609 );
610 }
611
612 #[test]
613 fn namespace_cursor_rejects_a_key_outside_its_keyspace() {
614 let namespace_id = NamespaceId::parse("demo").expect("namespace id");
615 let cursor = TestNamespaceCursor {
616 namespace_id: namespace_id.clone(),
617 last_key: "namespaces/demo/checkpoints/checkpoint-42".to_owned(),
618 };
619 let encoded = encode_cursor(&cursor).expect("encode cursor");
620
621 assert_eq!(
622 decode_namespace_cursor::<TestNamespaceCursor>(&encoded, &namespace_id),
623 Err(NamespaceCursorError::OutsideKeyspace)
624 );
625 }
626}