use anyhow::Result;
use serde_json::{Map, Value};
use std::future::Future;
use crate::api::LinearClient;
use crate::json_path::get_path;
#[derive(Debug, Clone, Default)]
pub struct PaginationOptions {
pub limit: Option<usize>,
pub after: Option<String>,
pub before: Option<String>,
pub page_size: Option<usize>,
pub all: bool,
}
impl PaginationOptions {
pub fn with_default_limit(&self, default_limit: usize) -> Self {
let mut options = self.clone();
if !options.all && options.limit.is_none() {
options.limit = Some(default_limit);
}
options
}
pub fn effective_page_size(&self, default_page_size: usize) -> usize {
self.page_size.unwrap_or(default_page_size).max(1)
}
}
pub async fn paginate_nodes(
client: &LinearClient,
query: &str,
base_variables: Map<String, Value>,
nodes_path: &[&str],
page_info_path: &[&str],
options: &PaginationOptions,
default_page_size: usize,
) -> Result<Vec<Value>> {
let mut items: Vec<Value> = Vec::new();
let remaining = if options.all { None } else { options.limit };
let mut after = options.after.clone();
let mut before = options.before.clone();
let mut forward = before.is_none();
if options.after.is_some() && options.before.is_some() {
before = None;
forward = true;
}
let page_size = options.effective_page_size(default_page_size);
loop {
let batch_size = remaining
.map(|r| r.saturating_sub(items.len()).min(page_size))
.unwrap_or(page_size)
.max(1);
let mut page_vars = Map::with_capacity(base_variables.len() + 2);
if forward {
page_vars.insert(
"first".to_string(),
Value::Number(serde_json::Number::from(batch_size as u64)),
);
if let Some(ref cursor) = after {
page_vars.insert("after".to_string(), Value::String(cursor.clone()));
}
} else {
page_vars.insert(
"last".to_string(),
Value::Number(serde_json::Number::from(batch_size as u64)),
);
if let Some(ref cursor) = before {
page_vars.insert("before".to_string(), Value::String(cursor.clone()));
}
}
for (k, v) in &base_variables {
page_vars.insert(k.clone(), v.clone());
}
let result = client.query(query, Some(Value::Object(page_vars))).await?;
let nodes = get_path(&result, nodes_path)
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
items.extend(nodes);
if let Some(limit) = remaining {
if limit <= items.len() {
items.truncate(limit);
break;
}
}
if !options.all && options.limit.is_none() {
break;
}
let page_info = get_path(&result, page_info_path).and_then(|v| v.as_object());
let Some(page_info) = page_info else { break };
if forward {
let has_next = page_info
.get("hasNextPage")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if !has_next {
break;
}
after = page_info
.get("endCursor")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if after.is_none() {
break;
}
} else {
let has_prev = page_info
.get("hasPreviousPage")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if !has_prev {
break;
}
before = page_info
.get("startCursor")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if before.is_none() {
break;
}
}
}
Ok(items)
}
#[allow(clippy::too_many_arguments)]
pub async fn stream_nodes<F, Fut>(
client: &LinearClient,
query: &str,
base_variables: Map<String, Value>,
nodes_path: &[&str],
page_info_path: &[&str],
options: &PaginationOptions,
default_page_size: usize,
mut handler: F,
) -> Result<usize>
where
F: FnMut(Vec<Value>) -> Fut,
Fut: Future<Output = Result<()>>,
{
let mut total: usize = 0;
let limit = if options.all { None } else { options.limit };
let mut after = options.after.clone();
let page_size = options.effective_page_size(default_page_size);
loop {
let batch_size = limit
.map(|l| l.saturating_sub(total).min(page_size))
.unwrap_or(page_size)
.max(1);
if let Some(l) = limit {
if total >= l {
break;
}
}
let mut page_vars = Map::with_capacity(base_variables.len() + 2);
page_vars.insert(
"first".to_string(),
Value::Number(serde_json::Number::from(batch_size as u64)),
);
if let Some(ref cursor) = after {
page_vars.insert("after".to_string(), Value::String(cursor.clone()));
}
for (k, v) in &base_variables {
page_vars.insert(k.clone(), v.clone());
}
let result = client.query(query, Some(Value::Object(page_vars))).await?;
let mut nodes = get_path(&result, nodes_path)
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
if let Some(l) = limit {
let remaining = l.saturating_sub(total);
if nodes.len() > remaining {
nodes.truncate(remaining);
}
}
let count = nodes.len();
if count == 0 {
break;
}
total += count;
handler(nodes).await?;
if let Some(l) = limit {
if total >= l {
break;
}
}
if !options.all && options.limit.is_none() {
break;
}
let page_info = get_path(&result, page_info_path).and_then(|v| v.as_object());
let Some(page_info) = page_info else { break };
let has_next = page_info
.get("hasNextPage")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if !has_next {
break;
}
after = page_info
.get("endCursor")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if after.is_none() {
break;
}
}
Ok(total)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pagination_options_default() {
let opts = PaginationOptions::default();
assert!(opts.limit.is_none());
assert!(opts.after.is_none());
assert!(opts.before.is_none());
assert!(opts.page_size.is_none());
assert!(!opts.all);
}
#[test]
fn test_with_default_limit_sets_limit_when_none() {
let opts = PaginationOptions::default();
let result = opts.with_default_limit(50);
assert_eq!(result.limit, Some(50));
}
#[test]
fn test_with_default_limit_preserves_existing_limit() {
let opts = PaginationOptions {
limit: Some(10),
..Default::default()
};
let result = opts.with_default_limit(50);
assert_eq!(result.limit, Some(10));
}
#[test]
fn test_with_default_limit_skipped_when_all() {
let opts = PaginationOptions {
all: true,
..Default::default()
};
let result = opts.with_default_limit(50);
assert!(result.limit.is_none());
}
#[test]
fn test_effective_page_size_default() {
let opts = PaginationOptions::default();
assert_eq!(opts.effective_page_size(100), 100);
}
#[test]
fn test_effective_page_size_custom() {
let opts = PaginationOptions {
page_size: Some(25),
..Default::default()
};
assert_eq!(opts.effective_page_size(100), 25);
}
#[test]
fn test_effective_page_size_minimum_one() {
let opts = PaginationOptions {
page_size: Some(0),
..Default::default()
};
assert_eq!(opts.effective_page_size(100), 1);
}
}