1use crate::capability::{LIMIT_PAGINATION_DEFAULT, LIMIT_PAGINATION_MAX};
4use crate::{ChangeSeq, InodeId, NameKey, NamespaceId, RevisionNo};
5use serde::{Deserialize, Serialize};
6use std::collections::BTreeMap;
7use std::future::Future;
8use std::num::NonZeroU32;
9use std::pin::Pin;
10use thiserror::Error;
11
12pub trait PagedResponse: Send + 'static {
14 type Item: Send + 'static;
16 type Cursor: Clone + Send + 'static;
18
19 fn items_mut(&mut self) -> &mut Vec<Self::Item>;
21
22 fn items(&self) -> &[Self::Item];
24
25 fn next_cursor(&self) -> Option<Self::Cursor>;
27
28 fn absorb(&mut self, later: Self);
30}
31
32enum PagerState<C> {
33 NotStarted,
34 More(C),
35 Done,
36}
37
38type PageFuture<P, E> = Pin<Box<dyn Future<Output = Result<P, E>> + Send>>;
39type PageFetcher<P, E> =
40 Box<dyn FnMut(Option<<P as PagedResponse>::Cursor>) -> PageFuture<P, E> + Send>;
41
42#[must_use]
44pub struct Pager<P: PagedResponse, E> {
45 fetch: PageFetcher<P, E>,
46 state: PagerState<P::Cursor>,
47 pending: Option<P>,
48}
49
50impl<P: PagedResponse, E> Pager<P, E> {
51 pub fn new<F, Fut>(cursor: Option<P::Cursor>, mut fetch: F) -> Self
53 where
54 F: FnMut(Option<P::Cursor>) -> Fut + Send + 'static,
55 Fut: Future<Output = Result<P, E>> + Send + 'static,
56 {
57 let state = match cursor {
58 Some(cursor) => PagerState::More(cursor),
59 None => PagerState::NotStarted,
60 };
61 Self {
62 fetch: Box::new(move |cursor| Box::pin(fetch(cursor))),
63 state,
64 pending: None,
65 }
66 }
67
68 pub async fn next(&mut self) -> Option<Result<P, E>> {
70 if let Some(page) = self.pending.take() {
71 return Some(Ok(page));
72 }
73 let cursor = match &self.state {
74 PagerState::NotStarted => None,
75 PagerState::More(cursor) => Some(cursor.clone()),
76 PagerState::Done => return None,
77 };
78 let page = (self.fetch)(cursor).await;
79 if let Ok(page) = &page {
80 self.state = match page.next_cursor() {
81 Some(cursor) => PagerState::More(cursor),
82 None => PagerState::Done,
83 };
84 }
85 Some(page)
86 }
87
88 pub async fn collect_up_to(&mut self, max_items: usize) -> Result<Vec<P::Item>, E> {
90 let mut items = Vec::new();
91 while items.len() < max_items {
92 let Some(page) = self.next().await else {
93 break;
94 };
95 let mut page = page?;
96 let page_items = page.items_mut();
97 let take = (max_items - items.len()).min(page_items.len());
98 if take < page_items.len() {
99 let remaining = page_items.split_off(take);
100 items.append(page_items);
101 *page.items_mut() = remaining;
102 self.pending = Some(page);
103 break;
104 }
105 items.append(page_items);
106 }
107 Ok(items)
108 }
109}
110
111macro_rules! string_cursor_response {
112 ($response:path, $item:ty, $field:ident $(, $metadata:ident)*) => {
113 impl PagedResponse for $response {
114 type Item = $item;
115 type Cursor = String;
116
117 fn items_mut(&mut self) -> &mut Vec<Self::Item> {
118 &mut self.$field
119 }
120
121 fn items(&self) -> &[Self::Item] {
122 &self.$field
123 }
124
125 fn next_cursor(&self) -> Option<Self::Cursor> {
126 self.next_cursor.clone()
127 }
128
129 fn absorb(&mut self, mut later: Self) {
130 $(self.$metadata = later.$metadata;)*
131 self.$field.append(&mut later.$field);
132 self.next_cursor = later.next_cursor;
133 }
134 }
135 };
136}
137
138string_cursor_response!(
139 crate::ListPathEntriesResponse,
140 crate::PathEntry,
141 entries,
142 head_seq
143);
144string_cursor_response!(
145 crate::ListInodeChildrenResponse,
146 crate::PathEntry,
147 entries,
148 head_seq
149);
150string_cursor_response!(
151 crate::ListFileRevisionsResponse,
152 crate::FileRevision,
153 revisions,
154 head_seq
155);
156string_cursor_response!(
157 crate::ListTrashResponse,
158 crate::TrashEntry,
159 entries,
160 head_seq
161);
162string_cursor_response!(
163 crate::ListCheckpointsResponse,
164 crate::Checkpoint,
165 checkpoints
166);
167string_cursor_response!(
168 crate::v0::ListSnapshotsResponse,
169 crate::v0::SnapshotSummary,
170 snapshots
171);
172
173impl PagedResponse for crate::v0::ListChangesResponse {
174 type Item = crate::v0::Commit;
175 type Cursor = ChangeSeq;
176
177 fn items_mut(&mut self) -> &mut Vec<Self::Item> {
178 &mut self.changes
179 }
180
181 fn items(&self) -> &[Self::Item] {
182 &self.changes
183 }
184
185 fn next_cursor(&self) -> Option<Self::Cursor> {
186 self.next_after_seq
187 }
188
189 fn absorb(&mut self, mut later: Self) {
190 self.through_seq = later.through_seq;
191 self.next_after_seq = later.next_after_seq;
192 self.changes.append(&mut later.changes);
193 }
194}
195
196pub const DEFAULT_PAGE_LIMIT: u32 = 1_000;
200pub const DEFAULT_MAX_PAGE_LIMIT: u32 = 1_000;
204
205pub const PAGE_CURSOR_FORMAT_VERSION: u8 = 1;
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
210pub struct EffectiveLimit(NonZeroU32);
211
212impl EffectiveLimit {
213 pub fn new(value: NonZeroU32) -> Self {
215 Self(value)
216 }
217
218 pub fn get(self) -> u32 {
220 self.0.get()
221 }
222
223 pub fn as_usize(self) -> usize {
225 self.0.get() as usize
226 }
227
228 pub fn limit_plus_one(self) -> usize {
230 self.as_usize().saturating_add(1)
231 }
232
233 pub fn finish_page<R, C>(self, rows: &mut Vec<R>, cursor: impl FnOnce(&R) -> C) -> Option<C> {
235 if rows.len() <= self.as_usize() {
236 return None;
237 }
238 rows.truncate(self.as_usize());
239 rows.last().map(cursor)
240 }
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub struct PaginationPolicy {
246 default_limit: NonZeroU32,
247 max_limit: NonZeroU32,
248}
249
250impl PaginationPolicy {
251 pub fn default_limit(self) -> NonZeroU32 {
253 self.default_limit
254 }
255
256 pub fn max_limit(self) -> NonZeroU32 {
258 self.max_limit
259 }
260
261 pub fn resolve_limit(self, requested: Option<u32>) -> Result<EffectiveLimit, LimitError> {
263 match requested {
264 None => Ok(EffectiveLimit(self.default_limit)),
265 Some(value) if value > self.max_limit.get() => Err(LimitError::ExceedsMax {
266 requested: value,
267 max_limit: self.max_limit.get(),
268 }),
269 Some(value) => NonZeroU32::new(value)
270 .map(EffectiveLimit)
271 .ok_or(LimitError::Zero),
272 }
273 }
274
275 pub fn capability_limits(self) -> BTreeMap<String, u64> {
277 BTreeMap::from([
278 (
279 LIMIT_PAGINATION_DEFAULT.to_owned(),
280 u64::from(self.default_limit.get()),
281 ),
282 (
283 LIMIT_PAGINATION_MAX.to_owned(),
284 u64::from(self.max_limit.get()),
285 ),
286 ])
287 }
288}
289
290impl Default for PaginationPolicy {
291 fn default() -> Self {
292 let default_limit = const { NonZeroU32::new(DEFAULT_PAGE_LIMIT).unwrap() };
296 let max_limit = const { NonZeroU32::new(DEFAULT_MAX_PAGE_LIMIT).unwrap() };
297 Self {
298 default_limit,
299 max_limit,
300 }
301 }
302}
303
304#[derive(Debug, Clone, PartialEq, Eq, Error)]
306#[non_exhaustive]
307pub enum LimitError {
308 #[error("limit must be greater than zero")]
310 Zero,
311 #[error("limit `{requested}` exceeds max limit `{max_limit}`")]
313 ExceedsMax {
314 requested: u32,
316 max_limit: u32,
318 },
319}
320
321#[derive(Debug, Clone, PartialEq, Eq)]
323pub struct PageRequest<C> {
324 pub limit: EffectiveLimit,
326 pub cursor: Option<C>,
328}
329
330#[derive(Debug, Clone, PartialEq, Eq)]
332pub struct Page<T, C> {
333 pub items: Vec<T>,
335 pub next_cursor: Option<C>,
337}
338
339#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
343pub struct DirectoryPageCursor {
344 pub head_seq: ChangeSeq,
346 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub snapshot_id: Option<crate::SnapshotId>,
349 pub directory_inode_id: InodeId,
351 pub last_name_key: NameKey,
353}
354
355impl PageCursor for DirectoryPageCursor {
356 const KIND: &'static str = "directory";
357}
358
359#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
361pub struct FileRevisionsPageCursor {
362 pub head_seq: ChangeSeq,
364 pub inode_id: InodeId,
366 pub last_revision_no: RevisionNo,
368 pub last_committed_seq: ChangeSeq,
370 pub last_revision_delta_index: u32,
372}
373
374impl PageCursor for FileRevisionsPageCursor {
375 const KIND: &'static str = "file_revisions";
376}
377
378#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
380pub struct TrashPageCursor {
381 pub head_seq: ChangeSeq,
383 pub last_deletion_seq: ChangeSeq,
385 pub last_root_inode_id: InodeId,
387}
388
389impl PageCursor for TrashPageCursor {
390 const KIND: &'static str = "trash";
391}
392
393#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
395pub struct GrepPageCursor {
396 pub head_seq: ChangeSeq,
398 pub last_inode_id: InodeId,
400 pub last_byte_offset: u64,
402 pub fingerprint: u64,
404}
405
406impl PageCursor for GrepPageCursor {
407 const KIND: &'static str = "grep";
408}
409
410pub trait PageCursor: Serialize + serde::de::DeserializeOwned {
412 const KIND: &'static str;
414}
415
416#[derive(Serialize, Deserialize)]
417struct OpaqueTokenEnvelope<T> {
418 format_version: u8,
419 kind: String,
420 #[serde(flatten)]
421 token: T,
422}
423
424pub trait OpaqueToken: Serialize + serde::de::DeserializeOwned {
426 const KIND: &'static str;
428}
429
430impl<C: PageCursor> OpaqueToken for C {
431 const KIND: &'static str = C::KIND;
432}
433
434pub fn encode_token<T: OpaqueToken>(
436 token: &T,
437 format_version: u8,
438) -> Result<String, serde_json::Error> {
439 serde_json::to_vec(&OpaqueTokenEnvelope {
440 format_version,
441 kind: T::KIND.to_owned(),
442 token,
443 })
444 .map(|bytes| crate::hex::hex_encode_bytes(&bytes))
445}
446
447pub fn encode_cursor<C: PageCursor>(cursor: &C) -> Result<String, PageCursorError> {
449 encode_token(cursor, PAGE_CURSOR_FORMAT_VERSION)
450 .map_err(|error| PageCursorError::InvalidJson(error.to_string()))
451}
452
453#[derive(Deserialize)]
456struct CursorHeader {
457 format_version: u8,
458 kind: String,
459}
460
461pub fn decode_token<T: OpaqueToken>(
463 value: &str,
464 supported_version: u8,
465) -> Result<T, OpaqueTokenError> {
466 let bytes =
467 crate::hex::hex_decode_bytes(value).map_err(|_| OpaqueTokenError::InvalidEncoding)?;
468 let header: CursorHeader = serde_json::from_slice(&bytes)
469 .map_err(|error| OpaqueTokenError::InvalidJson(error.to_string()))?;
470 if header.format_version != supported_version {
471 return Err(OpaqueTokenError::UnsupportedVersion {
472 expected: supported_version,
473 actual: header.format_version,
474 });
475 }
476 if header.kind != T::KIND {
477 return Err(OpaqueTokenError::WrongKind {
478 expected: T::KIND,
479 actual: header.kind,
480 });
481 }
482 let envelope: OpaqueTokenEnvelope<T> = serde_json::from_slice(&bytes)
483 .map_err(|error| OpaqueTokenError::InvalidJson(error.to_string()))?;
484 Ok(envelope.token)
485}
486
487pub fn decode_cursor<C: PageCursor>(value: &str) -> Result<C, PageCursorError> {
489 decode_token(value, PAGE_CURSOR_FORMAT_VERSION).map_err(PageCursorError::from)
490}
491
492pub trait NamespaceCursor: PageCursor {
494 fn namespace_id(&self) -> &NamespaceId;
496
497 fn last_key(&self) -> Option<&str>;
499
500 fn key_prefix(&self) -> String;
502}
503
504pub fn decode_namespace_cursor<C: NamespaceCursor>(
506 token: &str,
507 expected_namespace_id: &NamespaceId,
508) -> Result<C, NamespaceCursorError> {
509 let cursor: C = decode_cursor(token)?;
510 if cursor.namespace_id() != expected_namespace_id {
511 return Err(NamespaceCursorError::ForeignNamespace);
512 }
513 let prefix = cursor.key_prefix();
514 if cursor
515 .last_key()
516 .is_some_and(|key| !key.starts_with(&prefix))
517 {
518 return Err(NamespaceCursorError::OutsideKeyspace);
519 }
520 Ok(cursor)
521}
522
523#[derive(Debug, Clone, PartialEq, Eq, Error)]
525#[non_exhaustive]
526pub enum NamespaceCursorError {
527 #[error(transparent)]
529 Malformed(#[from] PageCursorError),
530 #[error("cursor belongs to a different namespace")]
532 ForeignNamespace,
533 #[error("cursor names a key outside the enumeration it resumes")]
535 OutsideKeyspace,
536}
537
538#[derive(Debug, Clone, PartialEq, Eq, Error)]
540#[non_exhaustive]
541pub enum PageCursorError {
542 #[error("invalid page cursor encoding")]
544 InvalidEncoding,
545 #[error("invalid page cursor JSON: {0}")]
547 InvalidJson(String),
548 #[error("page cursor kind `{actual}` cannot be used as `{expected}` cursor")]
550 WrongKind {
551 expected: &'static str,
553 actual: String,
555 },
556 #[error("unsupported page cursor version `{actual}`; expected `{expected}`")]
558 UnsupportedVersion {
559 expected: u8,
561 actual: u8,
563 },
564}
565
566#[derive(Debug, Clone, PartialEq, Eq, Error)]
568#[non_exhaustive]
569pub enum OpaqueTokenError {
570 #[error("invalid opaque token encoding")]
572 InvalidEncoding,
573 #[error("invalid opaque token JSON: {0}")]
575 InvalidJson(String),
576 #[error("opaque token kind `{actual}` cannot be used as `{expected}` token")]
578 WrongKind {
579 expected: &'static str,
581 actual: String,
583 },
584 #[error("unsupported opaque token version `{actual}`; expected `{expected}`")]
586 UnsupportedVersion {
587 expected: u8,
589 actual: u8,
591 },
592}
593
594impl From<OpaqueTokenError> for PageCursorError {
595 fn from(error: OpaqueTokenError) -> Self {
596 match error {
597 OpaqueTokenError::InvalidEncoding => Self::InvalidEncoding,
598 OpaqueTokenError::InvalidJson(message) => Self::InvalidJson(message),
599 OpaqueTokenError::WrongKind { expected, actual } => {
600 Self::WrongKind { expected, actual }
601 }
602 OpaqueTokenError::UnsupportedVersion { expected, actual } => {
603 Self::UnsupportedVersion { expected, actual }
604 }
605 }
606 }
607}
608
609#[cfg(test)]
610mod tests {
611 use super::*;
612
613 #[test]
614 fn default_policy_resolves_omitted_limit_to_default() {
615 let policy = PaginationPolicy::default();
616 let limit = policy.resolve_limit(None).expect("default limit");
617
618 assert_eq!(limit.get(), DEFAULT_PAGE_LIMIT);
619 assert_eq!(limit.limit_plus_one(), 1_001);
620 }
621
622 #[test]
623 fn policy_rejects_invalid_limits() {
624 let policy = PaginationPolicy::default();
625
626 assert_eq!(policy.resolve_limit(Some(0)), Err(LimitError::Zero));
627 assert_eq!(
628 policy.resolve_limit(Some(DEFAULT_MAX_PAGE_LIMIT + 1)),
629 Err(LimitError::ExceedsMax {
630 requested: DEFAULT_MAX_PAGE_LIMIT + 1,
631 max_limit: DEFAULT_MAX_PAGE_LIMIT,
632 })
633 );
634 }
635
636 #[test]
637 fn policy_exports_capability_limits() {
638 let limits = PaginationPolicy::default().capability_limits();
639
640 assert_eq!(
641 limits.get("pagination.default_limit"),
642 Some(&u64::from(DEFAULT_PAGE_LIMIT))
643 );
644 assert_eq!(
645 limits.get("pagination.max_limit"),
646 Some(&u64::from(DEFAULT_MAX_PAGE_LIMIT))
647 );
648 }
649
650 #[test]
651 fn directory_cursor_round_trips() {
652 let cursor = DirectoryPageCursor {
653 head_seq: ChangeSeq(11),
654 snapshot_id: Some(
655 crate::SnapshotId::parse("pin_00000000000000000001-0000000000000001")
656 .expect("snapshot id"),
657 ),
658 directory_inode_id: InodeId(7),
659 last_name_key: NameKey::parse("plan.md").expect("name key"),
660 };
661
662 let encoded = encode_cursor(&cursor).expect("encode cursor");
663 let decoded: DirectoryPageCursor = decode_cursor(&encoded).expect("decode cursor");
664
665 assert_eq!(decoded, cursor);
666 }
667
668 #[test]
669 fn live_directory_cursor_omits_the_additive_snapshot_field() {
670 let cursor = DirectoryPageCursor {
671 head_seq: ChangeSeq(11),
672 snapshot_id: None,
673 directory_inode_id: InodeId(7),
674 last_name_key: NameKey::parse("plan.md").expect("name key"),
675 };
676
677 let encoded = encode_cursor(&cursor).expect("encode cursor");
678 let bytes = crate::hex::hex_decode_bytes(&encoded).expect("decode hex");
679 let json: serde_json::Value = serde_json::from_slice(&bytes).expect("decode JSON");
680
681 assert!(json.get("snapshot_id").is_none());
682 assert_eq!(
683 decode_cursor::<DirectoryPageCursor>(&encoded).expect("decode cursor"),
684 cursor
685 );
686 }
687
688 #[test]
689 fn file_revisions_cursor_round_trips() {
690 let cursor = FileRevisionsPageCursor {
691 head_seq: ChangeSeq(11),
692 inode_id: InodeId(7),
693 last_revision_no: RevisionNo(5),
694 last_committed_seq: ChangeSeq(10),
695 last_revision_delta_index: 3,
696 };
697
698 let encoded = encode_cursor(&cursor).expect("encode cursor");
699 let decoded: FileRevisionsPageCursor = decode_cursor(&encoded).expect("decode cursor");
700
701 assert_eq!(decoded, cursor);
702 }
703
704 #[test]
705 fn trash_cursor_round_trips() {
706 let cursor = TrashPageCursor {
707 head_seq: ChangeSeq(11),
708 last_deletion_seq: ChangeSeq(10),
709 last_root_inode_id: InodeId(7),
710 };
711
712 let encoded = encode_cursor(&cursor).expect("encode cursor");
713 let decoded: TrashPageCursor = decode_cursor(&encoded).expect("decode cursor");
714
715 assert_eq!(decoded, cursor);
716 }
717
718 #[test]
719 fn grep_cursor_round_trips() {
720 let cursor = GrepPageCursor {
721 head_seq: ChangeSeq(11),
722 last_inode_id: InodeId(7),
723 last_byte_offset: 13,
724 fingerprint: 17,
725 };
726
727 let encoded = encode_cursor(&cursor).expect("encode cursor");
728 let decoded: GrepPageCursor = decode_cursor(&encoded).expect("decode cursor");
729
730 assert_eq!(decoded, cursor);
731 }
732
733 #[test]
734 fn cursor_kind_must_match_decoder() {
735 let cursor = FileRevisionsPageCursor {
736 head_seq: ChangeSeq(11),
737 inode_id: InodeId(7),
738 last_revision_no: RevisionNo(5),
739 last_committed_seq: ChangeSeq(10),
740 last_revision_delta_index: 3,
741 };
742 let encoded = encode_cursor(&cursor).expect("encode cursor");
743
744 assert_eq!(
745 decode_cursor::<DirectoryPageCursor>(&encoded),
746 Err(PageCursorError::WrongKind {
747 expected: "directory",
748 actual: "file_revisions".to_owned(),
749 })
750 );
751 }
752
753 #[test]
754 fn malformed_cursor_is_invalid_encoding() {
755 assert_eq!(
756 decode_cursor::<DirectoryPageCursor>("not-hex"),
757 Err(PageCursorError::InvalidEncoding)
758 );
759 }
760
761 #[test]
762 fn unsupported_cursor_version_is_rejected() {
763 let bytes = serde_json::to_vec(&OpaqueTokenEnvelope {
764 format_version: PAGE_CURSOR_FORMAT_VERSION + 1,
765 kind: <DirectoryPageCursor as PageCursor>::KIND.to_owned(),
766 token: DirectoryPageCursor {
767 head_seq: ChangeSeq(11),
768 snapshot_id: None,
769 directory_inode_id: InodeId(7),
770 last_name_key: NameKey::parse("plan.md").expect("name key"),
771 },
772 })
773 .expect("encode cursor");
774 let encoded = crate::hex::hex_encode_bytes(&bytes);
775
776 assert_eq!(
777 decode_cursor::<DirectoryPageCursor>(&encoded),
778 Err(PageCursorError::UnsupportedVersion {
779 expected: PAGE_CURSOR_FORMAT_VERSION,
780 actual: PAGE_CURSOR_FORMAT_VERSION + 1,
781 })
782 );
783 }
784
785 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
786 struct TestNamespaceCursor {
787 namespace_id: NamespaceId,
788 last_key: String,
789 }
790
791 impl PageCursor for TestNamespaceCursor {
792 const KIND: &'static str = "test_namespace";
793 }
794
795 impl NamespaceCursor for TestNamespaceCursor {
796 fn namespace_id(&self) -> &NamespaceId {
797 &self.namespace_id
798 }
799
800 fn last_key(&self) -> Option<&str> {
801 Some(&self.last_key)
802 }
803
804 fn key_prefix(&self) -> String {
805 format!("namespaces/{}/items/", self.namespace_id)
806 }
807 }
808
809 #[test]
810 fn namespace_cursor_accepts_its_namespace_and_keyspace() {
811 let namespace_id = NamespaceId::parse("demo").expect("namespace id");
812 let cursor = TestNamespaceCursor {
813 namespace_id: namespace_id.clone(),
814 last_key: "namespaces/demo/items/item-42".to_owned(),
815 };
816 let encoded = encode_cursor(&cursor).expect("encode cursor");
817
818 assert_eq!(
819 decode_namespace_cursor::<TestNamespaceCursor>(&encoded, &namespace_id)
820 .expect("decode namespace cursor"),
821 cursor
822 );
823 }
824
825 #[test]
826 fn namespace_cursor_rejects_a_different_namespace() {
827 let cursor = TestNamespaceCursor {
828 namespace_id: NamespaceId::parse("demo").expect("namespace id"),
829 last_key: "namespaces/demo/items/item-42".to_owned(),
830 };
831 let encoded = encode_cursor(&cursor).expect("encode cursor");
832
833 assert_eq!(
834 decode_namespace_cursor::<TestNamespaceCursor>(
835 &encoded,
836 &NamespaceId::parse("other").expect("other namespace id")
837 ),
838 Err(NamespaceCursorError::ForeignNamespace)
839 );
840 }
841
842 #[test]
843 fn namespace_cursor_rejects_a_key_outside_its_keyspace() {
844 let namespace_id = NamespaceId::parse("demo").expect("namespace id");
845 let cursor = TestNamespaceCursor {
846 namespace_id: namespace_id.clone(),
847 last_key: "namespaces/demo/pins/checkpoint-42".to_owned(),
848 };
849 let encoded = encode_cursor(&cursor).expect("encode cursor");
850
851 assert_eq!(
852 decode_namespace_cursor::<TestNamespaceCursor>(&encoded, &namespace_id),
853 Err(NamespaceCursorError::OutsideKeyspace)
854 );
855 }
856}