diff --git a/examples/default.rs b/examples/default.rs index b0a65cb..137db8d 100644 --- a/examples/default.rs +++ b/examples/default.rs @@ -32,7 +32,7 @@ async fn main() { .expect("Unable to insert data"); // query page 1, 2 at a time - let mut options = create_options(2, 0); + let mut options = create_options(2, 0, doc! { "name": 1 }); let mut find_results: FindResult = PaginatedCursor::new(Some(options), None, None) .find(&db.collection("myfruits"), None) .await @@ -44,7 +44,7 @@ async fn main() { print_details("First page", &find_results); // get the second page - options = create_options(2, 0); + options = create_options(2, 0, doc! { "name": 1 }); let mut cursor = find_results.page_info.next_cursor; find_results = PaginatedCursor::new(Some(options), cursor, Some(CursorDirections::Next)) .find(&db.collection("myfruits"), None) @@ -57,7 +57,7 @@ async fn main() { print_details("Second page", &find_results); // get previous page - options = create_options(2, 0); + options = create_options(2, 0, doc! { "name": 1 }); cursor = find_results.page_info.start_cursor; find_results = PaginatedCursor::new(Some(options), cursor, Some(CursorDirections::Previous)) .find(&db.collection("myfruits"), None) @@ -70,7 +70,7 @@ async fn main() { print_details("Previous page", &find_results); // with a skip - options = create_options(2, 3); + options = create_options(2, 3, doc! { "name": 1 }); find_results = PaginatedCursor::new(Some(options), None, None) .find(&db.collection("myfruits"), None) .await @@ -85,7 +85,7 @@ async fn main() { ); // backwards from skipping - options = create_options(2, 0); + options = create_options(2, 0, doc! { "name": 1 }); cursor = find_results.page_info.start_cursor; find_results = PaginatedCursor::new(Some(options), cursor, Some(CursorDirections::Previous)) .find(&db.collection("myfruits"), None) @@ -98,7 +98,7 @@ async fn main() { print_details("Previous from skip", &find_results); // backwards one more time and we are all the way back - options = create_options(2, 0); + options = create_options(2, 0, doc! { "name": 1 }); cursor = find_results.page_info.start_cursor; find_results = PaginatedCursor::new(Some(options), cursor, Some(CursorDirections::Previous)) .find(&db.collection("myfruits"), None) diff --git a/examples/helper/mod.rs b/examples/helper/mod.rs index 418b31a..02d6fdf 100644 --- a/examples/helper/mod.rs +++ b/examples/helper/mod.rs @@ -1,4 +1,4 @@ -use bson::doc; +use bson::{doc, Document}; use mongodb::options::FindOptions; use mongodb_cursor_pagination::FindResult; use serde::Deserialize; @@ -20,11 +20,11 @@ impl MyFruit { } } -pub fn create_options(limit: i64, skip: u64) -> FindOptions { +pub fn create_options(limit: i64, skip: u64, sort: Document) -> FindOptions { FindOptions::builder() .limit(limit) .skip(skip) - .sort(doc! { "name": 1 }) + .sort(sort) .build() } diff --git a/examples/multisort.rs b/examples/multisort.rs index 22e6904..8da8963 100644 --- a/examples/multisort.rs +++ b/examples/multisort.rs @@ -43,7 +43,7 @@ async fn main() { .expect("Unable to insert data"); // query page 1, 2 at a time - let mut options = create_options(3, 0); + let mut options = create_options(3, 0, doc! { "how_many": 1, "name": -1, "non_existent": 1 }); let mut find_results: FindResult = PaginatedCursor::new(Some(options), None, None) .find(&db.collection("myfruits"), None) .await @@ -51,15 +51,15 @@ async fn main() { assert_eq!( find_results.items, vec![ - MyFruit::new("Apple", 5), + MyFruit::new("Orange", 3), MyFruit::new("Avocado", 5), - MyFruit::new("Bananas", 10) + MyFruit::new("Apple", 5) ] ); print_details("First page", &find_results); // get the second page - options = create_options(3, 0); + options = create_options(3, 0, doc! { "how_many": 1, "name": -1, "non_existent": 1 }); let mut cursor = find_results.page_info.next_cursor; find_results = PaginatedCursor::new(Some(options), cursor, Some(CursorDirections::Next)) .find(&db.collection("myfruits"), None) @@ -68,15 +68,15 @@ async fn main() { assert_eq!( find_results.items, vec![ - MyFruit::new("Blackberry", 12), MyFruit::new("Blueberry", 10), + MyFruit::new("Bananas", 10), MyFruit::new("Grapes", 12) ] ); print_details("Second page", &find_results); // get previous page - options = create_options(3, 0); + options = create_options(3, 0, doc! { "how_many": 1, "name": -1, "non_existent": 1 }); cursor = find_results.page_info.start_cursor; find_results = PaginatedCursor::new(Some(options), cursor, Some(CursorDirections::Previous)) .find(&db.collection("myfruits"), None) @@ -85,15 +85,15 @@ async fn main() { assert_eq!( find_results.items, vec![ - MyFruit::new("Apple", 5), + MyFruit::new("Orange", 3), MyFruit::new("Avocado", 5), - MyFruit::new("Bananas", 10) + MyFruit::new("Apple", 5) ] ); print_details("Previous page", &find_results); // with a skip - options = create_options(3, 4); + options = create_options(3, 4, doc! { "how_many": 1, "name": -1, "non_existent": 1 }); find_results = PaginatedCursor::new(Some(options), None, None) .find(&db.collection("myfruits"), None) .await @@ -101,9 +101,9 @@ async fn main() { assert_eq!( find_results.items, vec![ - MyFruit::new("Blueberry", 10), + MyFruit::new("Bananas", 10), MyFruit::new("Grapes", 12), - MyFruit::new("Orange", 3) + MyFruit::new("Blackberry", 12) ] ); print_details( @@ -112,7 +112,7 @@ async fn main() { ); // backwards from skipping - options = create_options(3, 0); + options = create_options(3, 0, doc! { "how_many": 1, "name": -1, "non_existent": 1 }); cursor = find_results.page_info.start_cursor; find_results = PaginatedCursor::new(Some(options), cursor, Some(CursorDirections::Previous)) .find(&db.collection("myfruits"), None) @@ -122,20 +122,20 @@ async fn main() { find_results.items, vec![ MyFruit::new("Avocado", 5), - MyFruit::new("Bananas", 10), - MyFruit::new("Blackberry", 12), + MyFruit::new("Apple", 5), + MyFruit::new("Blueberry", 10), ] ); print_details("Previous from skip", &find_results); // backwards one more time and we are all the way back - options = create_options(3, 0); + options = create_options(3, 0, doc! { "how_many": 1, "name": -1, "non_existent": 1 }); cursor = find_results.page_info.start_cursor; find_results = PaginatedCursor::new(Some(options), cursor, Some(CursorDirections::Previous)) .find(&db.collection("myfruits"), None) .await .expect("Unable to find data"); - assert_eq!(find_results.items, vec![MyFruit::new("Apple", 5),]); + assert_eq!(find_results.items, vec![MyFruit::new("Orange", 3),]); print_details( "Previous again - at beginning, but cursor was for before Avocado (so only Apple)", &find_results, diff --git a/examples/regex.rs b/examples/regex.rs index 0337aab..ce63145 100644 --- a/examples/regex.rs +++ b/examples/regex.rs @@ -47,7 +47,7 @@ async fn main() { .expect("Unable to insert data"); // query page 1, 2 at a time - let mut options = create_options(2, 0); + let mut options = create_options(2, 0, doc! { "name": 1 }); let filter = doc! { "$or": [ { "name": Bson::RegularExpression(Regex { pattern: String::from("berry"), options: String::from("i") })}, { "spanish": Bson::RegularExpression(Regex { pattern: String::from("ana"), options: String::from("i") })}, @@ -63,7 +63,7 @@ async fn main() { print_details("First page", &find_results); // get the second page - options = create_options(2, 0); + options = create_options(2, 0, doc! { "name": 1 }); let mut cursor = find_results.page_info.next_cursor; find_results = PaginatedCursor::new(Some(options), cursor, Some(CursorDirections::Next)) .find(&db.collection("myfruits"), Some(&filter)) @@ -79,7 +79,7 @@ async fn main() { print_details("Second page", &find_results); // get previous page - options = create_options(2, 0); + options = create_options(2, 0, doc! { "name": 1 }); cursor = find_results.page_info.start_cursor; find_results = PaginatedCursor::new(Some(options), cursor, Some(CursorDirections::Previous)) .find(&db.collection("myfruits"), Some(&filter)) @@ -92,7 +92,7 @@ async fn main() { print_details("Previous (first) page", &find_results); // get the second page again - options = create_options(2, 0); + options = create_options(2, 0, doc! { "name": 1 }); cursor = find_results.page_info.next_cursor; find_results = PaginatedCursor::new(Some(options), cursor, Some(CursorDirections::Next)) .find(&db.collection("myfruits"), Some(&filter)) @@ -108,7 +108,7 @@ async fn main() { print_details("Second page (again)", &find_results); // get the third page - options = create_options(2, 0); + options = create_options(2, 0, doc! { "name": 1 }); cursor = find_results.page_info.next_cursor; find_results = PaginatedCursor::new(Some(options), cursor, Some(CursorDirections::Next)) .find(&db.collection("myfruits"), Some(&filter)) @@ -121,7 +121,7 @@ async fn main() { print_details("Third page", &find_results); // get previous page - options = create_options(2, 0); + options = create_options(2, 0, doc! { "name": 1 }); cursor = find_results.page_info.start_cursor; find_results = PaginatedCursor::new(Some(options), cursor, Some(CursorDirections::Previous)) .find(&db.collection("myfruits"), Some(&filter)) diff --git a/src/error/mod.rs b/src/error/mod.rs index 69ecf7e..ebe916f 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -10,32 +10,32 @@ pub enum CursorError { } impl From for CursorError { - fn from(err: io::Error) -> CursorError { - CursorError::IoError(err) + fn from(err: io::Error) -> Self { + Self::IoError(err) } } impl From for CursorError { - fn from(err: Utf8Error) -> CursorError { - CursorError::InvalidCursor(err.to_string()) + fn from(err: Utf8Error) -> Self { + Self::InvalidCursor(err.to_string()) } } impl From for CursorError { - fn from(err: base64::DecodeError) -> CursorError { - CursorError::InvalidCursor(err.to_string()) + fn from(err: base64::DecodeError) -> Self { + Self::InvalidCursor(err.to_string()) } } impl fmt::Display for CursorError { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { - CursorError::IoError(ref inner) => inner.fmt(fmt), - CursorError::InvalidCursor(ref cursor) => { - write!(fmt, "Invalid cursor - unable to parse: {:?}", cursor) + Self::IoError(ref inner) => inner.fmt(fmt), + Self::InvalidCursor(ref cursor) => { + write!(fmt, "Invalid cursor - unable to parse: {cursor:?}") } - CursorError::Unknown(ref inner) => inner.fmt(fmt), - CursorError::InvalidId(ref id) => write!(fmt, "Invalid id - {:?}", id), + Self::Unknown(ref inner) => inner.fmt(fmt), + Self::InvalidId(ref id) => write!(fmt, "Invalid id - {id:?}"), } } } @@ -44,16 +44,16 @@ impl fmt::Display for CursorError { impl error::Error for CursorError { fn description(&self) -> &str { match *self { - CursorError::IoError(ref inner) => inner.description(), - CursorError::InvalidCursor(_) => "Invalid cursor value", - CursorError::Unknown(ref inner) => inner, - CursorError::InvalidId(_) => "Invalid mongodbid", + Self::IoError(ref inner) => inner.description(), + Self::InvalidCursor(_) => "Invalid cursor value", + Self::Unknown(ref inner) => inner, + Self::InvalidId(_) => "Invalid mongodbid", } } fn cause(&self) -> Option<&dyn error::Error> { match *self { - CursorError::IoError(ref inner) => Some(inner), + Self::IoError(ref inner) => Some(inner), _ => None, } } diff --git a/src/lib.rs b/src/lib.rs index 6ff27c5..1b7f799 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,23 @@ #![doc = include_str!("../README.md")] +#![forbid(unsafe_code)] +#![warn( + clippy::cast_lossless, + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + clippy::cast_precision_loss, + clippy::cast_sign_loss, + clippy::checked_conversions, + clippy::implicit_saturating_sub, + clippy::integer_arithmetic, + clippy::mod_module_files, + clippy::panic, + clippy::panic_in_result_fn, + clippy::unwrap_used, + missing_docs, + rust_2018_idioms, + unused_lifetimes, + unused_qualifications +)] //! ### Usage: //! The usage is a bit different than the node version. See the examples for more details and a working example. @@ -129,19 +148,21 @@ pub mod error; mod options; +use crate::options::CursorOptions; use base64::engine::general_purpose::STANDARD; use base64::Engine; use bson::{doc, oid::ObjectId, Bson, Document}; use error::CursorError; use futures_util::stream::StreamExt; use log::warn; +use mongodb::options::{CountOptions, EstimatedDocumentCountOptions}; use mongodb::{options::FindOptions, Collection}; -use options::CursorOptions; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; +use std::ops::Neg; /// Provides details about if there are more pages and the cursor to the start of the list and end -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, Default)] pub struct PageInfo { pub has_next_page: bool, pub has_previous_page: bool, @@ -193,7 +214,7 @@ impl From<&Edge> for Edge { } /// The result of a find method with the items, edges, pagination info, and total count of objects -#[derive(Debug)] +#[derive(Debug, Default)] pub struct FindResult { pub page_info: PageInfo, pub edges: Vec, @@ -202,7 +223,7 @@ pub struct FindResult { } /// The direction of the list, ie. you are sending a cursor for the next or previous items. Defaults to Next -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum CursorDirections { Previous, Next, @@ -217,7 +238,7 @@ pub struct PaginatedCursor { options: CursorOptions, } -impl<'a> PaginatedCursor { +impl PaginatedCursor { /// Updates or creates all of the find options to help with pagination and returns a `PaginatedCursor` object. /// /// # Arguments @@ -225,25 +246,20 @@ impl<'a> PaginatedCursor { /// * `cursor` - An optional existing cursor in base64. This would have come from a previous `FindResult` /// * `direction` - Determines whether the cursor supplied is for a previous page or the next page. Defaults to Next /// + #[must_use] pub fn new( options: Option, cursor: Option, direction: Option, ) -> Self { - PaginatedCursor { + Self { // parse base64 for keys has_cursor: cursor.is_some(), - cursor_doc: if let Some(b64) = cursor { + cursor_doc: cursor.map_or_else(Document::new, |b64| { map_from_base64(b64).expect("Unable to parse cursor") - } else { - Document::new() - }, - direction: if let Some(d) = direction { - d - } else { - CursorDirections::Next - }, - options: CursorOptions::from(options), + }), + direction: direction.unwrap_or(CursorDirections::Next), + options: CursorOptions::from(options.unwrap_or_default()), } } @@ -252,9 +268,10 @@ impl<'a> PaginatedCursor { &self, collection: &Collection, ) -> Result { - let count_options = self.options.clone(); let total_count = collection - .estimated_document_count(count_options) + .estimated_document_count(Some(EstimatedDocumentCountOptions::from( + self.options.clone(), + ))) .await .unwrap(); Ok(total_count) @@ -271,13 +288,9 @@ impl<'a> PaginatedCursor { let mut count_options = self.options.clone(); count_options.limit = None; count_options.skip = None; - let count_query = if let Some(q) = query { - q.clone() - } else { - Document::new() - }; + let count_query = query.map_or_else(Document::new, Clone::clone); let total_count = collection - .count_documents(count_query, count_options) + .count_documents(count_query, Some(CountOptions::from(count_options))) .await .unwrap(); Ok(total_count) @@ -304,86 +317,89 @@ impl<'a> PaginatedCursor { let mut start_cursor: Option = None; let mut next_cursor: Option = None; - // make the query if we have some docs - if total_count > 0 { - // build the cursor - let query_doc = self.get_query(filter)?; - let mut options = self.options.clone(); - let skip_value = options.skip.unwrap_or(0); - if self.has_cursor || skip_value == 0 { - options.skip = None; - } else { - has_skip = true; - } - // let has_previous - let is_previous_query = self.has_cursor && self.direction == CursorDirections::Previous; - // if it's a previous query we need to reverse the sort we were doing - if is_previous_query { - if let Some(sort) = options.sort { - let keys: Vec<&String> = sort.keys().collect(); - let mut new_sort = Document::new(); - for key in keys { - let bson_value = sort.get(key).unwrap(); - match bson_value { - Bson::Int32(value) => { - new_sort.insert(key, Bson::Int32(-*value)); - } - Bson::Int64(value) => { - new_sort.insert(key, Bson::Int64(-*value)); - } - _ => {} - }; - } - options.sort = Some(new_sort); - } - } - let mut cursor = collection.find(query_doc, options).await.unwrap(); - while let Some(result) = cursor.next().await { - match result { - Ok(doc) => { - let item = bson::from_bson(bson::Bson::Document(doc.clone())).unwrap(); - edges.push(Edge { - cursor: self.create_from_doc(&doc), - }); - items.push(item); + // return if we if have no docs + if total_count == 0 { + return Ok(FindResult { + page_info: PageInfo::default(), + edges: vec![], + total_count: 0, + items: vec![], + }); + } + + // build the cursor + let query_doc = self.get_query(filter.cloned()); + let mut options = self.options.clone(); + let skip_value = options.skip.unwrap_or(0); + if self.has_cursor || skip_value == 0 { + options.skip = None; + } else { + has_skip = true; + } + // let has_previous + let is_previous_query = self.has_cursor && self.direction == CursorDirections::Previous; + // if it's a previous query we need to reverse the sort we were doing + if is_previous_query { + if let Some(sort) = options.sort.as_mut() { + sort.iter_mut().for_each(|(_key, value)| { + if let Bson::Int32(num) = value { + *value = Bson::Int32(num.neg()); } - Err(error) => { - warn!("Error to find doc: {}", error); + if let Bson::Int64(num) = value { + *value = Bson::Int64(num.neg()); } - } + }); } - let has_more: bool; - if has_skip { - has_more = (items.len() as u64 + skip_value) < total_count; - has_previous_page = true; - has_next_page = has_more; - } else { - has_more = items.len() > (self.options.limit.unwrap() - 1) as usize; - has_previous_page = (self.has_cursor && self.direction == CursorDirections::Next) - || (is_previous_query && has_more); - has_next_page = (self.direction == CursorDirections::Next && has_more) - || (is_previous_query && self.has_cursor); + } + let mut cursor = collection + .find(query_doc, Some(options.into())) + .await + .unwrap(); + while let Some(result) = cursor.next().await { + match result { + Ok(doc) => { + let item = bson::from_bson(Bson::Document(doc.clone())).unwrap(); + edges.push(Edge { + cursor: self.create_from_doc(&doc), + }); + items.push(item); + } + Err(error) => { + warn!("Error to find doc: {}", error); + } } + } + let has_more: bool; + if has_skip { + has_more = (items.len() as u64).saturating_add(skip_value) < total_count; + has_previous_page = true; + has_next_page = has_more; + } else { + has_more = items.len() as i64 > self.options.limit.unwrap().saturating_sub(1); + has_previous_page = (self.has_cursor && self.direction == CursorDirections::Next) + || (is_previous_query && has_more); + has_next_page = (self.direction == CursorDirections::Next && has_more) + || (is_previous_query && self.has_cursor); + } - // reorder if we are going backwards - if is_previous_query { - items.reverse(); - edges.reverse(); - } - // remove the extra item to check if we have more - if has_more && !is_previous_query { - items.pop(); - edges.pop(); - } else if has_more { - items.remove(0); - edges.remove(0); - } + // reorder if we are going backwards + if is_previous_query { + items.reverse(); + edges.reverse(); + } + // remove the extra item to check if we have more + if has_more && !is_previous_query { + items.pop(); + edges.pop(); + } else if has_more { + items.remove(0); + edges.remove(0); + } - // create the next cursor - if !items.is_empty() && edges.len() == items.len() { - start_cursor = Some(edges[0].cursor.to_owned()); - next_cursor = Some(edges[items.len() - 1].cursor.to_owned()); - } + // create the next cursor + if !items.is_empty() && edges.len() == items.len() { + start_cursor = Some(edges[0].cursor.clone()); + next_cursor = Some(edges[items.len().saturating_sub(1)].cursor.clone()); } let page_info = PageInfo { @@ -394,34 +410,26 @@ impl<'a> PaginatedCursor { }; Ok(FindResult { page_info, - total_count, edges, + total_count, items, }) } fn get_value_from_doc(&self, key: &str, doc: Bson) -> Option<(String, Bson)> { - let parts: Vec<&str> = key.splitn(2, ".").collect(); + let parts: Vec<&str> = key.splitn(2, '.').collect(); match doc { - Bson::Document(d) => { - let some_value = d.get(parts[0]); - match some_value { - Some(value) => match value { - Bson::Document(d) => { - self.get_value_from_doc(parts[1], Bson::Document(d.clone())) - } - _ => Some((parts[0].to_string(), value.clone())), - }, - None => None, - } - } + Bson::Document(d) => d.get(parts[0]).and_then(|value| match value { + Bson::Document(d) => self.get_value_from_doc(parts[1], Bson::Document(d.clone())), + _ => Some((parts[0].to_string(), value.clone())), + }), _ => Some((parts[0].to_string(), doc)), } } fn create_from_doc(&self, doc: &Document) -> String { let mut only_sort_keys = Document::new(); - if let Some(sort) = &self.options.sort { + self.options.sort.as_ref().map_or_else(String::new, |sort| { for key in sort.keys() { if let Some((_, value)) = self.get_value_from_doc(key, Bson::Document(doc.clone())) { @@ -430,9 +438,7 @@ impl<'a> PaginatedCursor { } let buf = bson::to_vec(&only_sort_keys).unwrap(); STANDARD.encode(buf) - } else { - "".to_owned() - } + }) } /* @@ -444,56 +450,52 @@ impl<'a> PaginatedCursor { _id: { $lt: nextId } }] */ - fn get_query(&self, query: Option<&Document>) -> Result { + fn get_query(&self, query: Option) -> Document { // now create the filter - let mut query_doc = match query { - Some(doc) => doc.clone(), - None => Document::new(), - }; + let mut query_doc = query.unwrap_or_default(); + // Don't do anything if no cursor is provided if self.cursor_doc.is_empty() { - return Ok(query_doc); - } else if let Some(sort) = &self.options.sort { - if sort.len() > 1 { - let keys: Vec<&String> = sort.keys().collect(); - let mut queries: Vec = Vec::new(); - #[allow(clippy::needless_range_loop)] - for i in 0..keys.len() { - let mut query = query_doc.clone(); - #[allow(clippy::needless_range_loop)] - for j in 0..i { - let value = self.cursor_doc.get(keys[j]).unwrap_or(&Bson::Null); - query.insert(keys[j], value.clone()); - } - // insert the directional sort (ie. < or >) - let value = self.cursor_doc.get(keys[i]).unwrap_or(&Bson::Null); - let direction = self.get_direction_from_key(&sort, keys[i]); - query.insert(keys[i], doc! { direction: value.clone() }); - queries.push(query); - } - if queries.len() > 1 { - query_doc = doc! { "$or": [] }; - let or_array = query_doc - .get_array_mut("$or") - .map_err(|_| CursorError::Unknown("Unable to process".into()))?; - for d in queries.iter() { - or_array.push(Bson::Document(d.clone())); - } - } else { - query_doc = queries[0].clone(); - } - } else { - // this is the simplest form, it's just a sort by _id - let object_id = self.cursor_doc.get("_id").unwrap().clone(); - let direction = self.get_direction_from_key(&sort, "_id"); - query_doc.insert("_id", doc! { direction: object_id }); - } + return query_doc; + } + let Some(sort) = &self.options.sort else { + return query_doc; + }; + + // this is the simplest form, it's just a sort by _id + if sort.len() <= 1 { + let object_id = self.cursor_doc.get("_id").unwrap().clone(); + let direction = self.get_direction_from_key(sort, "_id"); + query_doc.insert("_id", doc! { direction: object_id }); + return query_doc; + } + + let mut queries: Vec = Vec::new(); + let mut previous_conditions: Vec<(String, Bson)> = Vec::new(); + + // Add each sort condition with it's direction and all previous condition with fixed values + for key in sort.keys() { + let mut query = query_doc.clone(); + query.extend(previous_conditions.clone().into_iter()); // Add previous conditions + + let value = self.cursor_doc.get(key).unwrap_or(&Bson::Null); + let direction = self.get_direction_from_key(sort, key); + query.insert(key, doc! { direction: value.clone() }); + previous_conditions.push((key.clone(), value.clone())); // Add self without direction to previous conditions + + queries.push(query); } - Ok(query_doc) + + query_doc = if queries.len() > 1 { + doc! { "$or": queries.iter().as_ref() } + } else { + queries.pop().unwrap_or_default() + }; + query_doc } fn get_direction_from_key(&self, sort: &Document, key: &str) -> &'static str { - let value = sort.get(key).unwrap().as_i32().unwrap(); + let value = sort.get(key).and_then(Bson::as_i32).unwrap_or(0); match self.direction { CursorDirections::Next => { if value >= 0 { @@ -515,13 +517,13 @@ impl<'a> PaginatedCursor { fn map_from_base64(base64_string: String) -> Result { // change from base64 - let decoded = STANDARD.decode(&base64_string)?; + let decoded = STANDARD.decode(base64_string)?; // decode from bson let cursor_doc = bson::from_slice(decoded.as_slice()).unwrap(); Ok(cursor_doc) } -/// Converts an id into a MongoDb ObjectId +/// Converts an id into a `MongoDb` `ObjectId` pub fn get_object_id(id: &str) -> Result { let object_id = match ObjectId::parse_str(id) { Ok(object_id) => object_id, diff --git a/src/options/mod.rs b/src/options/mod.rs index c18383f..c1799b4 100644 --- a/src/options/mod.rs +++ b/src/options/mod.rs @@ -1,125 +1,61 @@ -use bson::Document; use mongodb::options::{CountOptions, EstimatedDocumentCountOptions, FindOptions}; +use serde::{Deserialize, Serialize}; +use std::ops::{Deref, DerefMut}; const DEFAULT_LIMIT: i64 = 25; -// FIX: This isn't the best but I can't figure out the Rc or other options to make it work yet -#[derive(Clone, Debug)] -pub struct CursorOptions { - pub allow_partial_results: Option, - pub batch_size: Option, - pub collation: Option, - pub comment: Option, - pub cursor_type: Option, - pub hint: Option, - pub limit: Option, - pub max: Option, - pub max_await_time: Option, - pub max_scan: Option, - pub max_time: Option, - pub min: Option, - pub no_cursor_timeout: Option, - pub projection: Option, - pub read_concern: Option, - pub return_key: Option, - pub selection_criteria: Option, - pub show_record_id: Option, - pub skip: Option, - pub sort: Option, +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct CursorOptions(FindOptions); + +impl Deref for CursorOptions { + type Target = FindOptions; + + fn deref(&self) -> &Self::Target { + &self.0 + } } -impl From> for CursorOptions { - fn from(options: Option) -> CursorOptions { - let old_opts = match options { - Some(o) => o, - None => FindOptions::builder().build(), - }; - let limit = match old_opts.limit { - Some(l) => Some(l + 1), - None => Some(DEFAULT_LIMIT + 1), - }; - let limit = limit.map(|l| l as u64); - // check the sort - let mut sort = match &old_opts.sort { - Some(s) => s.clone(), - None => Document::new(), - }; +impl DerefMut for CursorOptions { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} +impl From for CursorOptions { + fn from(mut options: FindOptions) -> Self { + options.limit = Some(options.limit.unwrap_or(DEFAULT_LIMIT).saturating_add(1)); + let mut sort = options.sort.unwrap_or_default(); if !sort.contains_key("_id") { sort.insert("_id", -1); } - CursorOptions { - allow_partial_results: old_opts.allow_partial_results, - batch_size: old_opts.batch_size, - collation: old_opts.collation.clone(), - comment: old_opts.comment.clone(), - cursor_type: old_opts.cursor_type, - hint: old_opts.hint.clone(), - limit, - max: old_opts.max.clone(), - max_await_time: old_opts.max_await_time, - max_scan: old_opts.max_scan, - max_time: old_opts.max_time, - min: old_opts.min.clone(), - no_cursor_timeout: old_opts.no_cursor_timeout, - projection: old_opts.projection.clone(), - read_concern: old_opts.read_concern, - return_key: old_opts.return_key, - selection_criteria: old_opts.selection_criteria, - show_record_id: old_opts.show_record_id, - skip: old_opts.skip, - sort: Some(sort), - } + options.sort = Some(sort); + Self(options) } } -impl From for Option { - fn from(options: CursorOptions) -> Option { - let find_options = FindOptions::builder() - .allow_partial_results(options.allow_partial_results) - .batch_size(options.batch_size) - .collation(options.collation) - .comment(options.comment) - .cursor_type(options.cursor_type) - .hint(options.hint) - .limit(options.limit.map(|l| l as i64)) - .max(options.max) - .max_await_time(options.max_await_time) - .max_scan(options.max_scan) - .max_time(options.max_time) - .min(options.min) - .no_cursor_timeout(options.no_cursor_timeout) - .projection(options.projection) - .read_concern(options.read_concern) - .return_key(options.return_key) - .selection_criteria(options.selection_criteria) - .show_record_id(options.show_record_id) - .skip(options.skip) - .sort(options.sort) - .build(); - Some(find_options) +impl From for FindOptions { + fn from(value: CursorOptions) -> Self { + value.0 } } -impl From for Option { - fn from(options: CursorOptions) -> Option { - let count_options = CountOptions::builder() - .collation(options.collation) - .hint(options.hint) - .limit(options.limit) - .max_time(options.max_time) - .skip(options.skip) - .build(); - Some(count_options) +impl From for CountOptions { + fn from(value: CursorOptions) -> Self { + Self::builder() + .collation(value.collation.clone()) + .hint(value.hint.clone()) + .limit(value.limit.map(|i| i as u64)) + .max_time(value.max_time) + .skip(value.skip) + .build() } } -impl From for Option { - fn from(options: CursorOptions) -> Option { - let count_options = EstimatedDocumentCountOptions::builder() +impl From for EstimatedDocumentCountOptions { + fn from(options: CursorOptions) -> Self { + Self::builder() .max_time(options.max_time) - .selection_criteria(options.selection_criteria) - .read_concern(options.read_concern) - .build(); - Some(count_options) + .selection_criteria(options.selection_criteria.clone()) + .read_concern(options.read_concern.clone()) + .build() } }