use awaken_contract::contract::message::{Message, Visibility};
use serde::Deserialize;
pub fn default_limit() -> usize {
50
}
#[derive(Debug, Deserialize)]
pub struct MessageQueryParams {
#[serde(default)]
pub offset: Option<usize>,
#[serde(default)]
pub cursor: Option<String>,
#[serde(default = "default_limit")]
pub limit: usize,
#[serde(default)]
pub visibility: Option<String>,
}
#[derive(Debug, PartialEq, Eq)]
pub struct CursorPage<T> {
pub items: Vec<T>,
pub total: usize,
pub has_more: bool,
pub next_cursor: Option<String>,
}
impl MessageQueryParams {
pub fn clamped_limit(&self) -> usize {
self.limit.clamp(1, 200)
}
pub fn offset_or_default(&self) -> usize {
self.offset.unwrap_or(0)
}
pub fn cursor_offset(&self) -> Result<usize, String> {
match self
.cursor
.as_deref()
.map(str::trim)
.filter(|cursor| !cursor.is_empty())
{
Some(cursor) => cursor
.parse::<usize>()
.map_err(|_| "cursor must be an unsigned integer offset".to_string()),
None => Ok(self.offset_or_default()),
}
}
pub fn include_internal(&self) -> bool {
self.visibility
.as_deref()
.is_some_and(|value| value.eq_ignore_ascii_case("all"))
}
pub fn filter_messages(&self, messages: Vec<Message>) -> Vec<Message> {
if self.include_internal() {
messages
} else {
messages
.into_iter()
.filter(|message| message.visibility != Visibility::Internal)
.collect()
}
}
pub fn paginate<T>(&self, items: Vec<T>) -> Result<CursorPage<T>, String> {
let offset = self.cursor_offset()?;
let total = items.len();
let start = offset.min(total);
let page_items: Vec<T> = items
.into_iter()
.skip(start)
.take(self.clamped_limit())
.collect();
let next_offset = start + page_items.len();
let has_more = next_offset < total;
Ok(CursorPage {
items: page_items,
total,
has_more,
next_cursor: has_more.then(|| next_offset.to_string()),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults() {
let params: MessageQueryParams = serde_json::from_str("{}").unwrap();
assert_eq!(params.offset, None);
assert_eq!(params.cursor, None);
assert_eq!(params.limit, 50);
assert_eq!(params.visibility, None);
}
#[test]
fn clamped_limit_bounds() {
let low: MessageQueryParams = serde_json::from_str(r#"{"limit": 0}"#).unwrap();
assert_eq!(low.clamped_limit(), 1);
let high: MessageQueryParams = serde_json::from_str(r#"{"limit": 999}"#).unwrap();
assert_eq!(high.clamped_limit(), 200);
let mid: MessageQueryParams = serde_json::from_str(r#"{"limit": 42}"#).unwrap();
assert_eq!(mid.clamped_limit(), 42);
}
#[test]
fn offset_or_default_values() {
let none: MessageQueryParams = serde_json::from_str("{}").unwrap();
assert_eq!(none.offset_or_default(), 0);
let some: MessageQueryParams = serde_json::from_str(r#"{"offset": 10}"#).unwrap();
assert_eq!(some.offset_or_default(), 10);
}
#[test]
fn cursor_offset_uses_cursor_when_present() {
let params: MessageQueryParams =
serde_json::from_str(r#"{"offset":10,"cursor":"25"}"#).unwrap();
assert_eq!(params.cursor_offset().unwrap(), 25);
}
#[test]
fn cursor_offset_falls_back_to_offset() {
let params: MessageQueryParams = serde_json::from_str(r#"{"offset":10}"#).unwrap();
assert_eq!(params.cursor_offset().unwrap(), 10);
}
#[test]
fn cursor_offset_rejects_invalid_cursor() {
let params: MessageQueryParams = serde_json::from_str(r#"{"cursor":"abc"}"#).unwrap();
assert_eq!(
params.cursor_offset().unwrap_err(),
"cursor must be an unsigned integer offset"
);
}
#[test]
fn include_internal_only_when_visibility_is_all() {
let none: MessageQueryParams = serde_json::from_str("{}").unwrap();
assert!(!none.include_internal());
let all: MessageQueryParams = serde_json::from_str(r#"{"visibility":"all"}"#).unwrap();
assert!(all.include_internal());
let case_insensitive: MessageQueryParams =
serde_json::from_str(r#"{"visibility":"ALL"}"#).unwrap();
assert!(case_insensitive.include_internal());
let other: MessageQueryParams = serde_json::from_str(r#"{"visibility":"none"}"#).unwrap();
assert!(!other.include_internal());
}
#[test]
fn filter_messages_hides_internal_by_default() {
let params: MessageQueryParams = serde_json::from_str("{}").unwrap();
let messages = vec![Message::user("visible"), Message::internal_system("hidden")];
let filtered = params.filter_messages(messages);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].text(), "visible");
}
#[test]
fn filter_messages_keeps_internal_when_requested() {
let params: MessageQueryParams = serde_json::from_str(r#"{"visibility":"all"}"#).unwrap();
let messages = vec![Message::user("visible"), Message::internal_system("hidden")];
let filtered = params.filter_messages(messages);
assert_eq!(filtered.len(), 2);
assert_eq!(filtered[1].visibility, Visibility::Internal);
}
#[test]
fn paginate_uses_cursor_and_returns_next_cursor() {
let params: MessageQueryParams =
serde_json::from_str(r#"{"cursor":"2","limit":2}"#).unwrap();
let page = params.paginate(vec!["a", "b", "c", "d", "e"]).unwrap();
assert_eq!(
page,
CursorPage {
items: vec!["c", "d"],
total: 5,
has_more: true,
next_cursor: Some("4".to_string()),
}
);
}
#[test]
fn paginate_uses_offset_when_cursor_absent() {
let params: MessageQueryParams = serde_json::from_str(r#"{"offset":1,"limit":2}"#).unwrap();
let page = params.paginate(vec!["a", "b", "c"]).unwrap();
assert_eq!(
page,
CursorPage {
items: vec!["b", "c"],
total: 3,
has_more: false,
next_cursor: None,
}
);
}
}