use super::funejson::Value; /// Take the 1-based page `page` of `items`, `per_page` items at a time. /// /// Page numbers are 1-based because every pagination UI, query string and API /// contract in the wild is 1-based; page 0 can only come from a caller that has /// the contract wrong, so it panics. /// /// A page past the end is deliberately not an error. Between two requests a /// collection can shrink under a client that is already on page 9, and that /// client should render an empty page, not a 500. /// /// Generic over the item type, like the TypeScript and Python versions: a page /// of `Invoice` is a `Page`, and a page of open JSON records is a /// `Page`. /// /// # Panics /// Panics if `per_page` is less than 1 or `page` is less than 1. pub fn paginate(items: &[T], page: i64, per_page: i64) -> Page { // A page size of zero cannot mean "no limit": that would quietly hand back // the entire collection, which is the outage pagination exists to prevent. if per_page < 1 { panic!("per_page must be at least 1, received {}", per_page); } if page < 1 { panic!("page must be 1 or greater, received {}", page); } let total = items.len() as i64; // Zero items is zero pages, not one empty page, so `page <= total_pages` is // the one correct test for "does this page exist". let total_pages = (total + per_page - 1) / per_page; // Clamp rather than index: a start past the end is the documented // empty-page case, not a slice panic. let start = ((page - 1) * per_page).min(total) as usize; let end = (start as i64 + per_page).min(total) as usize; Page { items: items[start..end].to_vec(), page, per_page, total, total_pages, has_next: page < total_pages, // On page 9 of a 2-page collection there really are earlier pages. has_prev: page > 1, } } /// The 1-based page number a zero-based item index falls on. /// /// # Panics /// Panics if `index` is negative or `per_page` is less than 1. pub fn page_of_index(index: i64, per_page: i64) -> i64 { if index < 0 { panic!("index must be a whole number of 0 or greater, received {}", index); } if per_page < 1 { panic!("per_page must be at least 1, received {}", per_page); } index / per_page + 1 } pub fn page_to_value(page: &Page) -> Value { Value::obj(vec![ ("items", Value::Arr(page.items.clone())), ("page", Value::Int(page.page)), ("perPage", Value::Int(page.per_page)), ("total", Value::Int(page.total)), ("totalPages", Value::Int(page.total_pages)), ("hasNext", Value::Bool(page.has_next)), ("hasPrev", Value::Bool(page.has_prev)), ]) } pub fn fune_vector(args: &[Value]) -> Value { // Refuse what the typed signature cannot hold, with the wording TypeScript // and Python use, rather than let the conversion below quietly change it. if let Value::Float(f) = args[1] { if f.fract() != 0.0 { panic!("page must be a whole number, received {}", f); } } if let Value::Float(f) = args[2] { if f.fract() != 0.0 { panic!("perPage must be a whole number, received {}", f); } } page_to_value(&paginate( args[0].as_arr(), args[1].as_i64(), args[2].as_i64(), )) }