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_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)]
173pub struct DirectoryPageCursor {
174 pub head_seq: ChangeSeq,
176 #[serde(rename = "dir_inode_id")]
179 pub directory_inode_id: InodeId,
180 pub last_name_key: NameKey,
182}
183
184impl PageCursor for DirectoryPageCursor {
185 const KIND: &'static str = "directory";
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub struct FileRevisionsPageCursor {
196 pub head_seq: ChangeSeq,
198 pub inode_id: InodeId,
200 pub last_revision_no: RevisionNo,
202 pub last_committed_seq: ChangeSeq,
204 pub last_revision_delta_index: u32,
206}
207
208impl PageCursor for FileRevisionsPageCursor {
209 const KIND: &'static str = "file_revisions";
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
221pub struct TrashPageCursor {
222 pub head_seq: ChangeSeq,
224 pub last_deleted_at_seq: ChangeSeq,
226 pub last_root_inode_id: InodeId,
228}
229
230impl PageCursor for TrashPageCursor {
231 const KIND: &'static str = "trash";
232}
233
234#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
240pub struct GrepPageCursor {
241 pub head_seq: ChangeSeq,
243 pub last_inode_id: InodeId,
245 pub last_byte_offset: u64,
249 pub fingerprint: u64,
253}
254
255impl PageCursor for GrepPageCursor {
256 const KIND: &'static str = "grep";
257}
258
259pub trait PageCursor: Serialize + serde::de::DeserializeOwned {
265 const KIND: &'static str;
267}
268
269#[derive(Serialize, Deserialize)]
270struct CursorEnvelope<C> {
271 #[serde(rename = "v")]
273 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 version: PAGE_CURSOR_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 #[serde(rename = "v")]
295 version: u8,
296 kind: String,
297}
298
299pub fn decode_cursor<C: PageCursor>(value: &str) -> Result<C, PageCursorError> {
301 let bytes =
302 crate::hex::hex_decode_bytes(value).map_err(|_| PageCursorError::InvalidEncoding)?;
303 let header: CursorHeader = serde_json::from_slice(&bytes)
304 .map_err(|error| PageCursorError::InvalidJson(error.to_string()))?;
305 if header.version != PAGE_CURSOR_VERSION {
306 return Err(PageCursorError::UnsupportedVersion {
307 expected: PAGE_CURSOR_VERSION,
308 actual: header.version,
309 });
310 }
311 if header.kind != C::KIND {
312 return Err(PageCursorError::WrongKind {
313 expected: C::KIND,
314 actual: header.kind,
315 });
316 }
317 let envelope: CursorEnvelope<C> = serde_json::from_slice(&bytes)
318 .map_err(|error| PageCursorError::InvalidJson(error.to_string()))?;
319 Ok(envelope.cursor)
320}
321
322pub trait NamespaceCursor: PageCursor {
332 fn namespace_id(&self) -> &NamespaceId;
334
335 fn last_key(&self) -> Option<&str>;
337
338 fn key_prefix(&self) -> String;
340}
341
342pub fn decode_namespace_cursor<C: NamespaceCursor>(
344 token: &str,
345 expected_namespace_id: &NamespaceId,
346) -> Result<C, NamespaceCursorError> {
347 let cursor: C = decode_cursor(token)?;
348 if cursor.namespace_id() != expected_namespace_id {
349 return Err(NamespaceCursorError::ForeignNamespace);
350 }
351 let prefix = cursor.key_prefix();
352 if cursor
353 .last_key()
354 .is_some_and(|key| !key.starts_with(&prefix))
355 {
356 return Err(NamespaceCursorError::OutsideKeyspace);
357 }
358 Ok(cursor)
359}
360
361#[derive(Debug, Clone, PartialEq, Eq, Error)]
363#[non_exhaustive]
364pub enum NamespaceCursorError {
365 #[error(transparent)]
368 Malformed(#[from] PageCursorError),
369 #[error("cursor belongs to a different namespace")]
371 ForeignNamespace,
372 #[error("cursor names a key outside the enumeration it resumes")]
374 OutsideKeyspace,
375}
376
377#[derive(Debug, Clone, PartialEq, Eq, Error)]
379#[non_exhaustive]
380pub enum PageCursorError {
381 #[error("invalid page cursor encoding")]
383 InvalidEncoding,
384 #[error("invalid page cursor JSON: {0}")]
386 InvalidJson(String),
387 #[error("page cursor kind `{actual}` cannot be used as `{expected}` cursor")]
389 WrongKind {
390 expected: &'static str,
392 actual: String,
394 },
395 #[error("unsupported page cursor version `{actual}`; expected `{expected}`")]
397 UnsupportedVersion {
398 expected: u8,
400 actual: u8,
402 },
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn default_policy_resolves_omitted_limit_to_default() {
411 let policy = PaginationPolicy::default();
412 let limit = policy.resolve_limit(None).expect("default limit");
413
414 assert_eq!(limit.get(), DEFAULT_PAGE_LIMIT);
415 assert_eq!(limit.limit_plus_one(), 1_001);
416 }
417
418 #[test]
419 fn policy_rejects_invalid_limits() {
420 let policy = PaginationPolicy::default();
421
422 assert_eq!(policy.resolve_limit(Some(0)), Err(LimitError::Zero));
423 assert_eq!(
424 policy.resolve_limit(Some(DEFAULT_MAX_PAGE_LIMIT + 1)),
425 Err(LimitError::ExceedsMax {
426 requested: DEFAULT_MAX_PAGE_LIMIT + 1,
427 max_limit: DEFAULT_MAX_PAGE_LIMIT,
428 })
429 );
430 }
431
432 #[test]
433 fn policy_exports_capability_limits() {
434 let limits = PaginationPolicy::default().capability_limits();
435
436 assert_eq!(
437 limits.get(LIMIT_PAGINATION_DEFAULT),
438 Some(&u64::from(DEFAULT_PAGE_LIMIT))
439 );
440 assert_eq!(
441 limits.get(LIMIT_PAGINATION_MAX),
442 Some(&u64::from(DEFAULT_MAX_PAGE_LIMIT))
443 );
444 }
445
446 #[test]
447 fn directory_cursor_round_trips() {
448 let cursor = DirectoryPageCursor {
449 head_seq: ChangeSeq(11),
450 directory_inode_id: InodeId(7),
451 last_name_key: NameKey::parse("plan.md").expect("name key"),
452 };
453
454 let encoded = encode_cursor(&cursor).expect("encode cursor");
455 let decoded: DirectoryPageCursor = decode_cursor(&encoded).expect("decode cursor");
456
457 assert_eq!(decoded, cursor);
458 }
459
460 #[test]
461 fn file_revisions_cursor_round_trips() {
462 let cursor = FileRevisionsPageCursor {
463 head_seq: ChangeSeq(11),
464 inode_id: InodeId(7),
465 last_revision_no: RevisionNo(5),
466 last_committed_seq: ChangeSeq(10),
467 last_revision_delta_index: 3,
468 };
469
470 let encoded = encode_cursor(&cursor).expect("encode cursor");
471 let decoded: FileRevisionsPageCursor = decode_cursor(&encoded).expect("decode cursor");
472
473 assert_eq!(decoded, cursor);
474 }
475
476 #[test]
477 fn trash_cursor_round_trips() {
478 let cursor = TrashPageCursor {
479 head_seq: ChangeSeq(11),
480 last_deleted_at_seq: ChangeSeq(10),
481 last_root_inode_id: InodeId(7),
482 };
483
484 let encoded = encode_cursor(&cursor).expect("encode cursor");
485 let decoded: TrashPageCursor = decode_cursor(&encoded).expect("decode cursor");
486
487 assert_eq!(decoded, cursor);
488 }
489
490 #[test]
491 fn grep_cursor_round_trips() {
492 let cursor = GrepPageCursor {
493 head_seq: ChangeSeq(11),
494 last_inode_id: InodeId(7),
495 last_byte_offset: 13,
496 fingerprint: 17,
497 };
498
499 let encoded = encode_cursor(&cursor).expect("encode cursor");
500 let decoded: GrepPageCursor = decode_cursor(&encoded).expect("decode cursor");
501
502 assert_eq!(decoded, cursor);
503 }
504
505 #[test]
506 fn cursor_kind_must_match_decoder() {
507 let cursor = FileRevisionsPageCursor {
508 head_seq: ChangeSeq(11),
509 inode_id: InodeId(7),
510 last_revision_no: RevisionNo(5),
511 last_committed_seq: ChangeSeq(10),
512 last_revision_delta_index: 3,
513 };
514 let encoded = encode_cursor(&cursor).expect("encode cursor");
515
516 assert_eq!(
517 decode_cursor::<DirectoryPageCursor>(&encoded),
518 Err(PageCursorError::WrongKind {
519 expected: "directory",
520 actual: "file_revisions".to_owned(),
521 })
522 );
523 }
524
525 #[test]
526 fn malformed_cursor_is_invalid_encoding() {
527 assert_eq!(
528 decode_cursor::<DirectoryPageCursor>("not-hex"),
529 Err(PageCursorError::InvalidEncoding)
530 );
531 }
532
533 #[test]
534 fn unsupported_cursor_version_is_rejected() {
535 let bytes = serde_json::to_vec(&CursorEnvelope {
536 version: PAGE_CURSOR_VERSION + 1,
537 kind: DirectoryPageCursor::KIND.to_owned(),
538 cursor: DirectoryPageCursor {
539 head_seq: ChangeSeq(11),
540 directory_inode_id: InodeId(7),
541 last_name_key: NameKey::parse("plan.md").expect("name key"),
542 },
543 })
544 .expect("encode cursor");
545 let encoded = crate::hex::hex_encode_bytes(&bytes);
546
547 assert_eq!(
548 decode_cursor::<DirectoryPageCursor>(&encoded),
549 Err(PageCursorError::UnsupportedVersion {
550 expected: PAGE_CURSOR_VERSION,
551 actual: PAGE_CURSOR_VERSION + 1,
552 })
553 );
554 }
555
556 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
557 struct TestNamespaceCursor {
558 namespace_id: NamespaceId,
559 last_key: String,
560 }
561
562 impl PageCursor for TestNamespaceCursor {
563 const KIND: &'static str = "test_namespace";
564 }
565
566 impl NamespaceCursor for TestNamespaceCursor {
567 fn namespace_id(&self) -> &NamespaceId {
568 &self.namespace_id
569 }
570
571 fn last_key(&self) -> Option<&str> {
572 Some(&self.last_key)
573 }
574
575 fn key_prefix(&self) -> String {
576 format!("namespaces/{}/items/", self.namespace_id)
577 }
578 }
579
580 #[test]
581 fn namespace_cursor_accepts_its_namespace_and_keyspace() {
582 let namespace_id = NamespaceId::parse("demo").expect("namespace id");
583 let cursor = TestNamespaceCursor {
584 namespace_id: namespace_id.clone(),
585 last_key: "namespaces/demo/items/item-42".to_owned(),
586 };
587 let encoded = encode_cursor(&cursor).expect("encode cursor");
588
589 assert_eq!(
590 decode_namespace_cursor::<TestNamespaceCursor>(&encoded, &namespace_id)
591 .expect("decode namespace cursor"),
592 cursor
593 );
594 }
595
596 #[test]
597 fn namespace_cursor_rejects_a_different_namespace() {
598 let cursor = TestNamespaceCursor {
599 namespace_id: NamespaceId::parse("demo").expect("namespace id"),
600 last_key: "namespaces/demo/items/item-42".to_owned(),
601 };
602 let encoded = encode_cursor(&cursor).expect("encode cursor");
603
604 assert_eq!(
605 decode_namespace_cursor::<TestNamespaceCursor>(
606 &encoded,
607 &NamespaceId::parse("other").expect("other namespace id")
608 ),
609 Err(NamespaceCursorError::ForeignNamespace)
610 );
611 }
612
613 #[test]
614 fn namespace_cursor_rejects_a_key_outside_its_keyspace() {
615 let namespace_id = NamespaceId::parse("demo").expect("namespace id");
616 let cursor = TestNamespaceCursor {
617 namespace_id: namespace_id.clone(),
618 last_key: "namespaces/demo/checkpoints/checkpoint-42".to_owned(),
619 };
620 let encoded = encode_cursor(&cursor).expect("encode cursor");
621
622 assert_eq!(
623 decode_namespace_cursor::<TestNamespaceCursor>(&encoded, &namespace_id),
624 Err(NamespaceCursorError::OutsideKeyspace)
625 );
626 }
627}