use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QueryResult<T> {
pub total_size: usize,
pub done: bool,
pub records: Vec<T>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_records_url: Option<String>,
}
impl<T> QueryResult<T> {
#[must_use]
pub const fn new(total_size: usize, done: bool, records: Vec<T>) -> Self {
Self {
total_size,
done,
records,
next_records_url: None,
}
}
#[must_use]
pub fn with_next_page(total_size: usize, records: Vec<T>, next_records_url: String) -> Self {
Self {
total_size,
done: false,
records,
next_records_url: Some(next_records_url),
}
}
#[must_use]
pub const fn is_done(&self) -> bool {
self.done
}
#[must_use]
pub const fn has_more(&self) -> bool {
!self.done && self.next_records_url.is_some()
}
#[must_use]
pub fn len(&self) -> usize {
self.records.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &T> {
self.records.iter()
}
pub fn into_records(self) -> std::vec::IntoIter<T> {
self.records.into_iter()
}
pub fn map<U, F>(self, f: F) -> QueryResult<U>
where
F: FnMut(T) -> U,
{
QueryResult {
total_size: self.total_size,
done: self.done,
records: self.records.into_iter().map(f).collect(),
next_records_url: self.next_records_url,
}
}
pub fn try_map<U, E, F>(self, f: F) -> Result<QueryResult<U>, E>
where
F: FnMut(T) -> Result<U, E>,
{
let records: Result<Vec<U>, E> = self.records.into_iter().map(f).collect();
Ok(QueryResult {
total_size: self.total_size,
done: self.done,
records: records?,
next_records_url: self.next_records_url,
})
}
}
impl<T> Default for QueryResult<T> {
fn default() -> Self {
Self::new(0, true, Vec::new())
}
}
impl<T> IntoIterator for QueryResult<T> {
type Item = T;
type IntoIter = std::vec::IntoIter<T>;
fn into_iter(self) -> Self::IntoIter {
self.records.into_iter()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct QueryLocator(String);
impl QueryLocator {
#[must_use]
pub fn from_url(url: impl Into<String>) -> Self {
Self(url.into())
}
#[must_use]
pub fn url(&self) -> &str {
&self.0
}
#[must_use]
pub fn is_initial(&self) -> bool {
!self.0.contains("/query/")
}
#[must_use]
pub fn is_continuation(&self) -> bool {
self.0.contains("/query/")
}
}
impl From<String> for QueryLocator {
fn from(url: String) -> Self {
Self::from_url(url)
}
}
impl From<&str> for QueryLocator {
fn from(url: &str) -> Self {
Self::from_url(url)
}
}
impl AsRef<str> for QueryLocator {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug)]
pub struct QueryIterator<T> {
pages: std::vec::IntoIter<QueryResult<T>>,
current_page: std::vec::IntoIter<T>,
page_count: usize,
total_count: usize,
yielded: usize,
}
impl<T> QueryIterator<T> {
#[must_use]
pub fn new(pages: Vec<QueryResult<T>>) -> Self {
let page_count = pages.len();
let total_count = pages.iter().map(|p| p.records.len()).sum();
Self {
pages: pages.into_iter(),
current_page: Vec::new().into_iter(),
page_count,
total_count,
yielded: 0,
}
}
#[must_use]
pub fn page_count(&self) -> usize {
self.page_count
}
#[must_use]
pub fn total_count(&self) -> usize {
self.total_count
}
}
impl<T> Iterator for QueryIterator<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
loop {
if let Some(record) = self.current_page.next() {
self.yielded += 1;
return Some(record);
}
let next_page_result = self.pages.next()?;
self.current_page = next_page_result.records.into_iter();
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.total_count.saturating_sub(self.yielded);
(remaining, Some(remaining))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::Must;
use serde_json::json;
#[test]
fn test_query_result_new() {
let result: QueryResult<i32> = QueryResult::new(5, true, vec![1, 2, 3]);
assert_eq!(result.total_size, 5);
assert!(result.is_done());
assert_eq!(result.len(), 3);
assert!(!result.is_empty());
}
#[test]
fn test_query_result_with_next_page() {
let result: QueryResult<i32> =
QueryResult::with_next_page(10, vec![1, 2, 3], "/next".to_string());
assert_eq!(result.total_size, 10);
assert!(!result.is_done());
assert!(result.has_more());
assert_eq!(result.next_records_url, Some("/next".to_string()));
}
#[test]
fn test_query_result_empty() {
let result: QueryResult<i32> = QueryResult::default();
assert_eq!(result.total_size, 0);
assert!(result.is_done());
assert!(result.is_empty());
assert_eq!(result.len(), 0);
}
#[test]
fn test_query_result_iter() {
let result: QueryResult<i32> = QueryResult::new(3, true, vec![1, 2, 3]);
let sum: i32 = result.iter().sum();
assert_eq!(sum, 6);
}
#[test]
fn test_query_result_into_iter() {
let result: QueryResult<i32> = QueryResult::new(3, true, vec![1, 2, 3]);
let collected: Vec<i32> = result.into_iter().collect();
assert_eq!(collected, vec![1, 2, 3]);
}
#[test]
fn test_query_result_for_loop() {
let result: QueryResult<i32> = QueryResult::new(3, true, vec![1, 2, 3]);
let mut sum = 0;
for record in result {
sum += record;
}
assert_eq!(sum, 6);
}
#[test]
fn test_query_result_map() {
let result: QueryResult<i32> = QueryResult::new(3, true, vec![1, 2, 3]);
let mapped: QueryResult<i32> = result.map(|x| x * 2);
assert_eq!(mapped.records, vec![2, 4, 6]);
assert_eq!(mapped.total_size, 3);
}
#[test]
fn test_query_result_try_map_success() {
let result: QueryResult<i32> = QueryResult::new(3, true, vec![1, 2, 3]);
let mapped: Result<QueryResult<i32>, ()> = result.try_map(|x| Ok(x * 2));
assert!(mapped.is_ok());
assert_eq!(mapped.must().records, vec![2, 4, 6]);
}
#[test]
fn test_query_result_try_map_failure() {
let result: QueryResult<i32> = QueryResult::new(3, true, vec![1, 2, 3]);
let mapped: Result<QueryResult<i32>, &str> =
result.try_map(|x| if x == 2 { Err("error") } else { Ok(x) });
assert!(mapped.is_err());
}
#[test]
fn test_query_result_serialize() {
let result: QueryResult<i32> = QueryResult::new(3, true, vec![1, 2, 3]);
let json = serde_json::to_string(&result).must();
assert!(json.contains("\"totalSize\":3"));
assert!(json.contains("\"done\":true"));
assert!(json.contains("\"records\":[1,2,3]"));
}
#[test]
fn test_query_result_deserialize() {
let json = json!({
"totalSize": 5,
"done": true,
"records": [1, 2, 3]
});
let result: QueryResult<i32> = serde_json::from_value(json).must();
assert_eq!(result.total_size, 5);
assert!(result.is_done());
assert_eq!(result.records, vec![1, 2, 3]);
}
#[test]
fn test_query_result_with_next_url_serialize() {
let result: QueryResult<i32> =
QueryResult::with_next_page(10, vec![1, 2], "/next".to_string());
let json = serde_json::to_string(&result).must();
assert!(json.contains("\"nextRecordsUrl\":\"/next\""));
assert!(json.contains("\"done\":false"));
}
#[test]
fn test_query_locator_from_url() {
let locator = QueryLocator::from_url("/services/data/v60.0/query/01gxx-2000");
assert_eq!(locator.url(), "/services/data/v60.0/query/01gxx-2000");
assert!(locator.is_continuation());
assert!(!locator.is_initial());
}
#[test]
fn test_query_locator_from_string() {
let locator: QueryLocator = "/next".to_string().into();
assert_eq!(locator.url(), "/next");
}
#[test]
fn test_query_locator_from_str() {
let locator: QueryLocator = "/next".into();
assert_eq!(locator.url(), "/next");
}
#[test]
fn test_query_locator_as_ref() {
let locator = QueryLocator::from_url("/next");
let s: &str = locator.as_ref();
assert_eq!(s, "/next");
}
#[test]
fn test_query_locator_serialize() {
let locator = QueryLocator::from_url("/next");
let json = serde_json::to_string(&locator).must();
assert!(json.contains("\"/next\""));
}
#[test]
fn test_query_locator_deserialize() {
let json = "\"/services/data/v60.0/query/01gxx\"";
let locator: QueryLocator = serde_json::from_str(json).must();
assert!(locator.is_continuation());
}
#[test]
fn test_query_iterator_new() {
let page1: QueryResult<i32> = QueryResult::new(3, true, vec![1, 2, 3]);
let iter = QueryIterator::new(vec![page1]);
assert_eq!(iter.page_count(), 1);
assert_eq!(iter.total_count(), 3);
}
#[test]
fn test_query_iterator_iterates_records_in_order() {
let page1: QueryResult<i32> = QueryResult::with_next_page(5, vec![1, 2, 3], "/next".into());
let page2: QueryResult<i32> = QueryResult::new(5, true, vec![4, 5]);
let iter = QueryIterator::new(vec![page1, page2]);
let collected: Vec<i32> = iter.collect();
assert_eq!(collected, vec![1, 2, 3, 4, 5]);
}
#[test]
fn test_query_iterator_multiple_pages() {
let page1: QueryResult<i32> = QueryResult::new(5, false, vec![1, 2]);
let page2: QueryResult<i32> = QueryResult::new(5, true, vec![3, 4, 5]);
let iter = QueryIterator::new(vec![page1, page2]);
assert_eq!(iter.page_count(), 2);
assert_eq!(iter.total_count(), 5);
}
#[test]
fn test_query_iterator_empty() {
let iter: QueryIterator<i32> = QueryIterator::new(vec![]);
assert_eq!(iter.page_count(), 0);
assert_eq!(iter.total_count(), 0);
}
#[test]
fn test_query_result_invalid_state_done_with_next_url() {
let result: QueryResult<i32> = QueryResult {
total_size: 10,
done: true,
records: vec![],
next_records_url: Some("/next".to_string()),
};
assert!(result.is_done());
assert!(!result.has_more());
assert!(result.next_records_url.is_some());
}
#[test]
fn test_query_result_invalid_state_not_done_without_next_url() {
let result: QueryResult<i32> = QueryResult {
total_size: 10,
done: false, records: vec![],
next_records_url: None,
};
assert!(!result.is_done());
assert!(!result.has_more()); assert!(result.next_records_url.is_none()); }
mod proptests {
use super::*;
use proptest::prelude::*;
fn arbitrary_query_result() -> impl Strategy<Value = QueryResult<i32>> {
(
any::<usize>(),
any::<bool>(),
prop::collection::vec(any::<i32>(), 0..100),
prop::option::of("[a-z/]{1,50}"),
)
.prop_map(|(total_size, done, records, next_records_url)| {
QueryResult {
total_size,
done,
records,
next_records_url,
}
})
}
proptest! {
#[test]
fn prop_has_more_requires_next_url_and_not_done(result in arbitrary_query_result()) {
prop_assert_eq!(result.has_more(), !result.is_done() && result.next_records_url.is_some());
}
#[test]
fn prop_done_implies_not_has_more(result in arbitrary_query_result()) {
if result.is_done() {
prop_assert!(!result.has_more());
}
}
#[test]
fn prop_len_matches_records(result in arbitrary_query_result()) {
prop_assert_eq!(result.len(), result.records.len());
}
#[test]
fn prop_is_empty_consistent(result in arbitrary_query_result()) {
prop_assert_eq!(result.is_empty(), result.records.is_empty());
prop_assert_eq!(result.is_empty(), result.is_empty());
}
#[test]
fn prop_map_preserves_metadata(result in arbitrary_query_result()) {
let original_total = result.total_size;
let original_done = result.done;
let original_next = result.next_records_url.clone();
let mapped = result.map(|x| x.saturating_add(1));
prop_assert_eq!(mapped.total_size, original_total);
prop_assert_eq!(mapped.done, original_done);
prop_assert_eq!(mapped.next_records_url, original_next);
}
#[test]
fn prop_map_transforms_records(result in arbitrary_query_result()) {
let expected: Vec<i32> = result.records.iter().map(|x| x.saturating_add(1)).collect();
let mapped = result.map(|x| x.saturating_add(1));
prop_assert_eq!(mapped.records, expected);
}
#[test]
fn prop_default_is_empty_and_done(_x in 0..1) {
let default: QueryResult<i32> = QueryResult::default();
prop_assert!(default.is_done());
prop_assert!(default.is_empty());
prop_assert_eq!(default.total_size, 0);
prop_assert_eq!(default.next_records_url, None);
}
#[test]
fn prop_with_next_page_not_done(
total in any::<usize>(),
records in prop::collection::vec(any::<i32>(), 0..50),
url in "[a-z/]{1,50}"
) {
let result = QueryResult::with_next_page(total, records, url.clone());
prop_assert!(!result.is_done());
prop_assert!(result.has_more());
prop_assert_eq!(result.next_records_url, Some(url));
}
}
}
#[test]
fn test_query_result_into_records() {
let result: QueryResult<i32> = QueryResult::new(3, true, vec![10, 20, 30]);
let records: Vec<i32> = result.into_records().collect();
assert_eq!(records, vec![10, 20, 30]);
}
#[test]
fn test_query_iterator_size_hint() {
let page1: QueryResult<i32> = QueryResult::with_next_page(5, vec![1, 2, 3], "/next".into());
let page2: QueryResult<i32> = QueryResult::new(5, true, vec![4, 5]);
let mut iter = QueryIterator::new(vec![page1, page2]);
assert_eq!(iter.size_hint(), (5, Some(5)));
let _ = iter.next(); assert_eq!(iter.size_hint(), (4, Some(4)));
let _ = iter.next(); let _ = iter.next(); assert_eq!(iter.size_hint(), (2, Some(2)));
let _ = iter.next(); let _ = iter.next(); assert_eq!(iter.size_hint(), (0, Some(0)));
assert!(iter.next().is_none());
}
#[test]
fn test_query_iterator_empty_middle_page() {
let page1: QueryResult<i32> = QueryResult::with_next_page(5, vec![1, 2], "/next1".into());
let page2: QueryResult<i32> = QueryResult::with_next_page(5, vec![], "/next2".into());
let page3: QueryResult<i32> = QueryResult::new(5, true, vec![4, 5]);
let iter = QueryIterator::new(vec![page1, page2, page3]);
let collected: Vec<i32> = iter.collect();
assert_eq!(collected, vec![1, 2, 4, 5]);
}
}