Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions src/github/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
use crate::github::models::WorkflowRun;
use crate::github::{Client, GitHubError, RepoId};

fn parse_runs(envelope: serde_json::Value) -> Result<Vec<WorkflowRun>, GitHubError> {
let arr = envelope
.get("workflow_runs")
/// Extractor for the `{ workflow_runs: [...] }` envelope each page returns.
fn workflow_runs(page: &serde_json::Value) -> Vec<serde_json::Value> {
page.get("workflow_runs")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or(serde_json::Value::Array(vec![]));
serde_json::from_value(arr).map_err(|e| GitHubError::Parse(e.to_string()))
.unwrap_or_default()
}

fn dispatch_body(git_ref: &str, inputs: Option<&serde_json::Value>) -> serde_json::Value {
Expand All @@ -29,8 +29,8 @@ pub fn list_runs(
if let Some(b) = branch {
path.push_str(&format!("?branch={}", crate::github::encode_query(b)));
}
let envelope = client.request("GET", &path, None)?;
parse_runs(envelope)
let arr = client.get_paginated(&path, workflow_runs)?;
serde_json::from_value(arr).map_err(|e| GitHubError::Parse(e.to_string()))
}

pub fn get_run(client: &Client, repo: &RepoId, run_id: u64) -> Result<WorkflowRun, GitHubError> {
Expand Down Expand Up @@ -68,17 +68,17 @@ pub fn dispatch(
mod tests {
use super::*;
#[test]
fn parse_runs_extracts_the_array() {
fn workflow_runs_extracts_the_array() {
let env = serde_json::json!({ "total_count": 1, "workflow_runs": [{
"id": 1, "status": "completed", "conclusion": "success",
"html_url": "https://github.com/o/r/actions/runs/1" }] });
let runs = parse_runs(env).unwrap();
assert_eq!(runs.len(), 1);
assert_eq!(runs[0].conclusion.as_deref(), Some("success"));
let items = workflow_runs(&env);
assert_eq!(items.len(), 1);
assert_eq!(items[0]["conclusion"], "success");
}
#[test]
fn parse_runs_defaults_to_empty_when_key_absent() {
assert!(parse_runs(serde_json::json!({})).unwrap().is_empty());
fn workflow_runs_defaults_to_empty_when_key_absent() {
assert!(workflow_runs(&serde_json::json!({})).is_empty());
}
#[test]
fn dispatch_body_includes_inputs_only_when_present() {
Expand Down
42 changes: 42 additions & 0 deletions src/github/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ pub struct Client {

const BASE_URL: &str = "https://api.github.com";

/// Max items per page GitHub allows for the list endpoints used here.
const PER_PAGE: usize = 100;

fn map_status(status: u16, ratelimit_remaining: Option<&str>, body: String) -> GitHubError {
match status {
401 => GitHubError::NoAuth(
Expand Down Expand Up @@ -97,6 +100,38 @@ impl Client {
Err(e) => Err(GitHubError::Transport(e.to_string())),
}
}

/// GET every page of a list endpoint, walking `?page=N&per_page=100` until a
/// page comes back shorter than a full page. `extract` pulls the item array
/// out of each response — identity for bare-array endpoints, or the envelope
/// key (e.g. `workflow_runs`) for wrapped ones. Returns the concatenated
/// items as a JSON array so callers deserialize once.
pub fn get_paginated(
&self,
base_path: &str,
extract: impl Fn(&serde_json::Value) -> Vec<serde_json::Value>,
) -> Result<serde_json::Value, GitHubError> {
let sep = if base_path.contains('?') { '&' } else { '?' };
let mut all: Vec<serde_json::Value> = Vec::new();
let mut page = 1usize;
loop {
let path = format!("{base_path}{sep}per_page={PER_PAGE}&page={page}");
let resp = self.request("GET", &path, None)?;
let items = extract(&resp);
let full = items.len() == PER_PAGE;
all.extend(items);
if !full {
break;
}
page += 1;
}
Ok(serde_json::Value::Array(all))
}
}

/// Extractor for bare-array list endpoints (pulls, issues, releases).
pub fn as_array(v: &serde_json::Value) -> Vec<serde_json::Value> {
v.as_array().cloned().unwrap_or_default()
}

#[cfg(test)]
Expand Down Expand Up @@ -126,4 +161,11 @@ mod tests {
GitHubError::Http { status: 500, .. }
));
}

#[test]
fn as_array_unwraps_or_defaults_empty() {
assert_eq!(as_array(&serde_json::json!([1, 2, 3])).len(), 3);
assert!(as_array(&serde_json::json!({})).is_empty());
assert!(as_array(&serde_json::Value::Null).is_empty());
}
}
2 changes: 1 addition & 1 deletion src/github/issues.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub fn list(client: &Client, repo: &RepoId, state: &str) -> Result<Vec<Issue>, G
repo.repo,
crate::github::encode_query(state)
);
let json = client.request("GET", &path, None)?;
let json = client.get_paginated(&path, crate::github::client::as_array)?;
serde_json::from_value(json).map_err(|e| GitHubError::Parse(e.to_string()))
}

Expand Down
2 changes: 1 addition & 1 deletion src/github/pulls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub fn list(client: &Client, repo: &RepoId, state: &str) -> Result<Vec<PullReque
repo.repo,
crate::github::encode_query(state)
);
let json = client.request("GET", &path, None)?;
let json = client.get_paginated(&path, crate::github::client::as_array)?;
serde_json::from_value(json).map_err(|e| GitHubError::Parse(e.to_string()))
}

Expand Down
2 changes: 1 addition & 1 deletion src/github/releases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ fn create_body(

pub fn list(client: &Client, repo: &RepoId) -> Result<Vec<Release>, GitHubError> {
let path = format!("/repos/{}/{}/releases", repo.owner, repo.repo);
let json = client.request("GET", &path, None)?;
let json = client.get_paginated(&path, crate::github::client::as_array)?;
serde_json::from_value(json).map_err(|e| GitHubError::Parse(e.to_string()))
}

Expand Down
Loading