use crate::capability::{LIMIT_PAGINATION_DEFAULT, LIMIT_PAGINATION_MAX};
use crate::{ChangeSeq, InodeId, NameKey, NamespaceId, RevisionNo};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::num::NonZeroU32;
use thiserror::Error;
pub const DEFAULT_PAGE_LIMIT: u32 = 1_000;
pub const DEFAULT_MAX_PAGE_LIMIT: u32 = 1_000;
pub const PAGE_CURSOR_VERSION: u8 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct EffectiveLimit(NonZeroU32);
impl EffectiveLimit {
pub fn new(value: NonZeroU32) -> Self {
Self(value)
}
pub fn get(self) -> u32 {
self.0.get()
}
pub fn as_usize(self) -> usize {
self.0.get() as usize
}
pub fn limit_plus_one(self) -> usize {
self.as_usize().saturating_add(1)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PaginationPolicy {
default_limit: NonZeroU32,
max_limit: NonZeroU32,
}
impl PaginationPolicy {
pub fn new(
default_limit: NonZeroU32,
max_limit: NonZeroU32,
) -> Result<Self, PaginationPolicyError> {
if default_limit > max_limit {
return Err(PaginationPolicyError::DefaultExceedsMax {
default_limit: default_limit.get(),
max_limit: max_limit.get(),
});
}
Ok(Self {
default_limit,
max_limit,
})
}
pub fn from_values(default_limit: u32, max_limit: u32) -> Result<Self, PaginationPolicyError> {
let default_limit =
NonZeroU32::new(default_limit).ok_or(PaginationPolicyError::ZeroDefaultLimit)?;
let max_limit = NonZeroU32::new(max_limit).ok_or(PaginationPolicyError::ZeroMaxLimit)?;
Self::new(default_limit, max_limit)
}
pub fn default_limit(self) -> NonZeroU32 {
self.default_limit
}
pub fn max_limit(self) -> NonZeroU32 {
self.max_limit
}
pub fn resolve_limit(self, requested: Option<u32>) -> Result<EffectiveLimit, LimitError> {
match requested {
None => Ok(EffectiveLimit(self.default_limit)),
Some(0) => Err(LimitError::Zero),
Some(value) if value > self.max_limit.get() => Err(LimitError::ExceedsMax {
requested: value,
max_limit: self.max_limit.get(),
}),
Some(value) => NonZeroU32::new(value)
.map(EffectiveLimit)
.ok_or(LimitError::Zero),
}
}
pub fn capability_limits(self) -> BTreeMap<String, u64> {
BTreeMap::from([
(
LIMIT_PAGINATION_DEFAULT.to_owned(),
u64::from(self.default_limit.get()),
),
(
LIMIT_PAGINATION_MAX.to_owned(),
u64::from(self.max_limit.get()),
),
])
}
}
impl Default for PaginationPolicy {
fn default() -> Self {
let default_limit = const { NonZeroU32::new(DEFAULT_PAGE_LIMIT).unwrap() };
let max_limit = const { NonZeroU32::new(DEFAULT_MAX_PAGE_LIMIT).unwrap() };
Self {
default_limit,
max_limit,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum PaginationPolicyError {
#[error("pagination default limit must be greater than zero")]
ZeroDefaultLimit,
#[error("pagination max limit must be greater than zero")]
ZeroMaxLimit,
#[error("pagination default limit `{default_limit}` exceeds max limit `{max_limit}`")]
DefaultExceedsMax {
default_limit: u32,
max_limit: u32,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum LimitError {
#[error("limit must be greater than zero")]
Zero,
#[error("limit `{requested}` exceeds max limit `{max_limit}`")]
ExceedsMax {
requested: u32,
max_limit: u32,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PageRequest<C> {
pub limit: EffectiveLimit,
pub cursor: Option<C>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Page<T, C> {
pub items: Vec<T>,
pub next_cursor: Option<C>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DirectoryPageCursor {
pub head_seq: ChangeSeq,
#[serde(rename = "dir_inode_id")]
pub directory_inode_id: InodeId,
pub last_name_key: NameKey,
}
impl PageCursor for DirectoryPageCursor {
const KIND: &'static str = "directory";
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FileRevisionsPageCursor {
pub head_seq: ChangeSeq,
pub inode_id: InodeId,
pub last_revision_no: RevisionNo,
pub last_committed_seq: ChangeSeq,
pub last_revision_delta_index: u32,
}
impl PageCursor for FileRevisionsPageCursor {
const KIND: &'static str = "file_revisions";
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrashPageCursor {
pub head_seq: ChangeSeq,
pub last_deleted_at_seq: ChangeSeq,
pub last_root_inode_id: InodeId,
}
impl PageCursor for TrashPageCursor {
const KIND: &'static str = "trash";
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GrepPageCursor {
pub head_seq: ChangeSeq,
pub last_inode_id: InodeId,
pub last_byte_offset: u64,
pub fingerprint: u64,
}
impl PageCursor for GrepPageCursor {
const KIND: &'static str = "grep";
}
pub trait PageCursor: Serialize + serde::de::DeserializeOwned {
const KIND: &'static str;
}
#[derive(Serialize, Deserialize)]
struct CursorEnvelope<C> {
#[serde(rename = "v")]
version: u8,
kind: String,
#[serde(flatten)]
cursor: C,
}
pub fn encode_cursor<C: PageCursor>(cursor: &C) -> Result<String, PageCursorError> {
let bytes = serde_json::to_vec(&CursorEnvelope {
version: PAGE_CURSOR_VERSION,
kind: C::KIND.to_owned(),
cursor,
})
.map_err(|error| PageCursorError::InvalidJson(error.to_string()))?;
Ok(crate::hex::hex_encode_bytes(&bytes))
}
#[derive(Deserialize)]
struct CursorHeader {
#[serde(rename = "v")]
version: u8,
kind: String,
}
pub fn decode_cursor<C: PageCursor>(value: &str) -> Result<C, PageCursorError> {
let bytes =
crate::hex::hex_decode_bytes(value).map_err(|_| PageCursorError::InvalidEncoding)?;
let header: CursorHeader = serde_json::from_slice(&bytes)
.map_err(|error| PageCursorError::InvalidJson(error.to_string()))?;
if header.version != PAGE_CURSOR_VERSION {
return Err(PageCursorError::UnsupportedVersion {
expected: PAGE_CURSOR_VERSION,
actual: header.version,
});
}
if header.kind != C::KIND {
return Err(PageCursorError::WrongKind {
expected: C::KIND,
actual: header.kind,
});
}
let envelope: CursorEnvelope<C> = serde_json::from_slice(&bytes)
.map_err(|error| PageCursorError::InvalidJson(error.to_string()))?;
Ok(envelope.cursor)
}
pub trait NamespaceCursor: PageCursor {
fn namespace_id(&self) -> &NamespaceId;
fn last_key(&self) -> Option<&str>;
fn key_prefix(&self) -> String;
}
pub fn decode_namespace_cursor<C: NamespaceCursor>(
token: &str,
expected_namespace_id: &NamespaceId,
) -> Result<C, NamespaceCursorError> {
let cursor: C = decode_cursor(token)?;
if cursor.namespace_id() != expected_namespace_id {
return Err(NamespaceCursorError::ForeignNamespace);
}
let prefix = cursor.key_prefix();
if cursor
.last_key()
.is_some_and(|key| !key.starts_with(&prefix))
{
return Err(NamespaceCursorError::OutsideKeyspace);
}
Ok(cursor)
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum NamespaceCursorError {
#[error(transparent)]
Malformed(#[from] PageCursorError),
#[error("cursor belongs to a different namespace")]
ForeignNamespace,
#[error("cursor names a key outside the enumeration it resumes")]
OutsideKeyspace,
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum PageCursorError {
#[error("invalid page cursor encoding")]
InvalidEncoding,
#[error("invalid page cursor JSON: {0}")]
InvalidJson(String),
#[error("page cursor kind `{actual}` cannot be used as `{expected}` cursor")]
WrongKind {
expected: &'static str,
actual: String,
},
#[error("unsupported page cursor version `{actual}`; expected `{expected}`")]
UnsupportedVersion {
expected: u8,
actual: u8,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_policy_resolves_omitted_limit_to_default() {
let policy = PaginationPolicy::default();
let limit = policy.resolve_limit(None).expect("default limit");
assert_eq!(limit.get(), DEFAULT_PAGE_LIMIT);
assert_eq!(limit.limit_plus_one(), 1_001);
}
#[test]
fn policy_rejects_invalid_limits() {
let policy = PaginationPolicy::default();
assert_eq!(policy.resolve_limit(Some(0)), Err(LimitError::Zero));
assert_eq!(
policy.resolve_limit(Some(DEFAULT_MAX_PAGE_LIMIT + 1)),
Err(LimitError::ExceedsMax {
requested: DEFAULT_MAX_PAGE_LIMIT + 1,
max_limit: DEFAULT_MAX_PAGE_LIMIT,
})
);
}
#[test]
fn policy_rejects_default_above_max() {
assert_eq!(
PaginationPolicy::from_values(10, 5),
Err(PaginationPolicyError::DefaultExceedsMax {
default_limit: 10,
max_limit: 5,
})
);
}
#[test]
fn policy_exports_capability_limits() {
let limits = PaginationPolicy::default().capability_limits();
assert_eq!(
limits.get(LIMIT_PAGINATION_DEFAULT),
Some(&u64::from(DEFAULT_PAGE_LIMIT))
);
assert_eq!(
limits.get(LIMIT_PAGINATION_MAX),
Some(&u64::from(DEFAULT_MAX_PAGE_LIMIT))
);
}
#[test]
fn directory_cursor_round_trips() {
let cursor = DirectoryPageCursor {
head_seq: ChangeSeq(11),
directory_inode_id: InodeId(7),
last_name_key: NameKey::parse("plan.md").expect("name key"),
};
let encoded = encode_cursor(&cursor).expect("encode cursor");
let decoded: DirectoryPageCursor = decode_cursor(&encoded).expect("decode cursor");
assert_eq!(decoded, cursor);
}
#[test]
fn file_revisions_cursor_round_trips() {
let cursor = FileRevisionsPageCursor {
head_seq: ChangeSeq(11),
inode_id: InodeId(7),
last_revision_no: RevisionNo(5),
last_committed_seq: ChangeSeq(10),
last_revision_delta_index: 3,
};
let encoded = encode_cursor(&cursor).expect("encode cursor");
let decoded: FileRevisionsPageCursor = decode_cursor(&encoded).expect("decode cursor");
assert_eq!(decoded, cursor);
}
#[test]
fn cursor_kind_must_match_decoder() {
let cursor = FileRevisionsPageCursor {
head_seq: ChangeSeq(11),
inode_id: InodeId(7),
last_revision_no: RevisionNo(5),
last_committed_seq: ChangeSeq(10),
last_revision_delta_index: 3,
};
let encoded = encode_cursor(&cursor).expect("encode cursor");
assert_eq!(
decode_cursor::<DirectoryPageCursor>(&encoded),
Err(PageCursorError::WrongKind {
expected: "directory",
actual: "file_revisions".to_owned(),
})
);
}
#[test]
fn malformed_cursor_is_invalid_encoding() {
assert_eq!(
decode_cursor::<DirectoryPageCursor>("not-hex"),
Err(PageCursorError::InvalidEncoding)
);
}
#[test]
fn unsupported_cursor_version_is_rejected() {
let bytes = serde_json::to_vec(&CursorEnvelope {
version: PAGE_CURSOR_VERSION + 1,
kind: DirectoryPageCursor::KIND.to_owned(),
cursor: DirectoryPageCursor {
head_seq: ChangeSeq(11),
directory_inode_id: InodeId(7),
last_name_key: NameKey::parse("plan.md").expect("name key"),
},
})
.expect("encode cursor");
let encoded = crate::hex::hex_encode_bytes(&bytes);
assert_eq!(
decode_cursor::<DirectoryPageCursor>(&encoded),
Err(PageCursorError::UnsupportedVersion {
expected: PAGE_CURSOR_VERSION,
actual: PAGE_CURSOR_VERSION + 1,
})
);
}
}