use crate::client::ClickUpClient;
use crate::error::CliError;
use crate::Cli;
use serde_json::Value;
const MAX_PAGES: usize = 100;
fn extract_array(resp: &Value, keys: &[&str]) -> Option<Vec<Value>> {
for key in keys {
if let Some(arr) = resp.get(key).and_then(|v| v.as_array()) {
return Some(arr.clone());
}
}
if let Some(arr) = resp.as_array() {
return Some(arr.clone());
}
None
}
pub async fn walk_page<F>(
cli: &Cli,
client: &ClickUpClient,
items_key: &str,
build_path: F,
) -> Result<Vec<Value>, CliError>
where
F: Fn(u32) -> String,
{
let start_page = cli.page.unwrap_or(0);
let mut collected: Vec<Value> = Vec::new();
let mut current_page = start_page;
let mut pages_fetched = 0usize;
loop {
let resp = client.get(&build_path(current_page)).await?;
let items = extract_array(&resp, &[items_key, "data"]).unwrap_or_default();
let last_page = resp
.get("last_page")
.and_then(|v| v.as_bool())
.unwrap_or(items.is_empty());
collected.extend(items);
pages_fetched += 1;
if !cli.all {
break;
}
if last_page || pages_fetched >= MAX_PAGES {
break;
}
if let Some(limit) = cli.limit {
if collected.len() >= limit {
break;
}
}
current_page += 1;
}
if let Some(limit) = cli.limit {
collected.truncate(limit);
}
Ok(collected)
}
pub async fn walk_cursor<F>(
cli: &Cli,
client: &ClickUpClient,
items_keys: &[&str],
build_path: F,
) -> Result<Vec<Value>, CliError>
where
F: Fn(Option<&str>) -> String,
{
let mut cursor = cli.cursor.clone();
let mut collected: Vec<Value> = Vec::new();
let mut pages_fetched = 0usize;
loop {
let resp = client.get(&build_path(cursor.as_deref())).await?;
let items = extract_array(&resp, items_keys).unwrap_or_default();
let next_cursor: Option<String> = resp
.get("next_cursor")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(String::from);
collected.extend(items);
pages_fetched += 1;
if !cli.all {
break;
}
if next_cursor.is_none() || pages_fetched >= MAX_PAGES {
break;
}
if let Some(limit) = cli.limit {
if collected.len() >= limit {
break;
}
}
cursor = next_cursor;
}
if let Some(limit) = cli.limit {
collected.truncate(limit);
}
Ok(collected)
}
pub async fn walk_start_id<F>(
cli: &Cli,
client: &ClickUpClient,
items_key: &str,
build_path: F,
) -> Result<Vec<Value>, CliError>
where
F: Fn(Option<i64>, Option<&str>) -> String,
{
const PAGE_HINT: usize = 25;
let mut current_start = cli.start;
let mut current_start_id = cli.start_id.clone();
let mut collected: Vec<Value> = Vec::new();
let mut pages_fetched = 0usize;
loop {
let resp = client
.get(&build_path(current_start, current_start_id.as_deref()))
.await?;
let items = extract_array(&resp, &[items_key, "data"]).unwrap_or_default();
let count = items.len();
let next_boundary = items.last().and_then(|last| {
let date = last
.get("date")
.and_then(|v| v.as_str())
.and_then(|s| s.parse::<i64>().ok())
.or_else(|| last.get("date").and_then(|v| v.as_i64()));
let id = last.get("id").and_then(|v| v.as_str()).map(String::from);
match (date, id) {
(Some(d), Some(i)) => Some((d, i)),
_ => None,
}
});
collected.extend(items);
pages_fetched += 1;
if !cli.all {
break;
}
if count < PAGE_HINT || pages_fetched >= MAX_PAGES {
break;
}
if let Some(limit) = cli.limit {
if collected.len() >= limit {
break;
}
}
match next_boundary {
Some((d, i)) => {
current_start = Some(d);
current_start_id = Some(i);
}
None => break,
}
}
if let Some(limit) = cli.limit {
collected.truncate(limit);
}
Ok(collected)
}
#[allow(clippy::too_many_arguments)]
pub async fn walk_body<BB, NT>(
cli: &Cli,
client: &ClickUpClient,
path: &str,
items_keys: &[&str],
base_body: BB,
extra_pagination: serde_json::Map<String, Value>,
start_timestamp: Option<i64>,
next_timestamp: NT,
) -> Result<Vec<Value>, CliError>
where
BB: Fn() -> Value,
NT: Fn(&Value) -> Option<i64>,
{
let mut current_timestamp = start_timestamp;
let mut collected: Vec<Value> = Vec::new();
let mut pages_fetched = 0usize;
loop {
let mut body = base_body();
let mut pagination = extra_pagination.clone();
if let Some(t) = current_timestamp {
pagination.insert("pageTimestamp".into(), serde_json::json!(t));
}
if !pagination.is_empty() {
body["pagination"] = Value::Object(pagination);
}
let resp = client.post(path, &body).await?;
let items = extract_array(&resp, items_keys).unwrap_or_default();
let count = items.len();
let next = items.last().and_then(&next_timestamp);
collected.extend(items);
pages_fetched += 1;
if !cli.all {
break;
}
if count == 0 || next.is_none() || pages_fetched >= MAX_PAGES {
break;
}
if let Some(limit) = cli.limit {
if collected.len() >= limit {
break;
}
}
current_timestamp = next;
}
if let Some(limit) = cli.limit {
collected.truncate(limit);
}
Ok(collected)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn extract_array_prefers_first_key() {
let resp = json!({"data": [1, 2], "tasks": [3, 4]});
let arr = extract_array(&resp, &["data", "tasks"]).unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0], json!(1));
}
#[test]
fn extract_array_falls_back_to_second_key() {
let resp = json!({"tasks": [3, 4]});
let arr = extract_array(&resp, &["data", "tasks"]).unwrap();
assert_eq!(arr[0], json!(3));
}
#[test]
fn extract_array_falls_back_to_bare_array() {
let resp = json!([1, 2, 3]);
let arr = extract_array(&resp, &["data"]).unwrap();
assert_eq!(arr.len(), 3);
}
#[test]
fn extract_array_returns_none_when_no_match() {
let resp = json!({"foo": "bar"});
assert!(extract_array(&resp, &["data", "tasks"]).is_none());
}
}