use crate::prelude::*;
use axum::{
extract::{Path, Query, State},
Json,
};
use crate::web::github::{self, Refusal};
use crate::web::state::WebState;
use wsb_github::client::{CreateIssueRequest, GitHubComment, GitHubIssue, UpdateIssueRequest};
use wsb_github::service::IssueState;
#[derive(Deserialize)]
pub struct ListIssuesQuery {
pub state: Option<String>,
pub repo: Option<String>,
}
fn issue_state(query: &ListIssuesQuery) -> Result<IssueState, Refusal> {
query
.state
.as_deref()
.unwrap_or("open")
.parse()
.map_err(|error: String| {
(
axum::http::StatusCode::BAD_REQUEST,
Json(super::sessions::ErrorResponse { error }),
)
})
}
pub async fn list_issues(
State(state): State<WebState>,
Query(query): Query<ListIssuesQuery>,
) -> Result<Json<wsb_github::service::IssueList>, Refusal> {
let wanted = issue_state(&query)?;
let github = github::of(&state)?;
let mut repos: Vec<String> = github::repositories(&state)?
.into_iter()
.map(|repository| repository.name)
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
if let Some(repo) = &query.repo {
repos.retain(|held| held == repo);
}
let service = github.service.clone();
let list = tokio::task::spawn_blocking(move || service.issues(&repos, wanted))
.await
.map_err(|e| github::failure(wsb_github::Failure::Mirror(anyhow::anyhow!("join error: {e}"))))?
.map_err(github::failure)?;
Ok(Json(list))
}
pub async fn list_repo_issues(
State(state): State<WebState>,
Path((owner, repo)): Path<(String, String)>,
Query(query): Query<ListIssuesQuery>,
) -> Result<Json<serde_json::Value>, Refusal> {
let wanted = issue_state(&query)?;
let github = github::of(&state)?;
let name = format!("{owner}/{repo}");
let service = github.service.clone();
let repos = vec![name.clone()];
let mut list = tokio::task::spawn_blocking(move || service.issues(&repos, wanted))
.await
.map_err(|e| github::failure(wsb_github::Failure::Mirror(anyhow::anyhow!("join error: {e}"))))?
.map_err(github::failure)?;
Ok(Json(serde_json::json!({
"repo": name,
"issues": list.issues,
"sync": list.sync.remove(&name),
})))
}
pub async fn refresh_issues(
State(state): State<WebState>,
) -> Result<Json<wsb_github::sync::SyncReport>, Refusal> {
let github = github::of(&state)?;
let prj = github.prj.clone();
let (targets, token) = tokio::task::spawn_blocking(move || {
(wsb_github::targets::Targets::of(&prj), wsb_github::token::resolve())
})
.await
.map_err(|e| github::failure(wsb_github::Failure::Mirror(anyhow::anyhow!("join error: {e}"))))?;
let targets = targets.map_err(|e| github::failure(wsb_github::Failure::Mirror(e)))?;
let client = token.ok().map(|token| wsb_github::GitHubClient::new(token.value()));
let report =
wsb_github::sync::sync(github.service.mirror(), client.as_ref(), &targets, true).await;
Ok(Json(report))
}
#[derive(Debug, Clone, Serialize)]
pub struct RootIssue {
pub id: String,
pub title: String,
}
pub async fn root_issues(
State(state): State<WebState>,
) -> Result<Json<serde_json::Value>, Refusal> {
let issues = load_root_issues(&state.db, &state.db_service)
.await
.map_err(|e| github::failure(wsb_github::Failure::Mirror(e)))?;
Ok(Json(serde_json::json!({ "root_issues": issues })))
}
async fn load_root_issues(
repos: &crate::persist::global::WsbRepos,
db_service: &crate::persist::global::DatabaseService,
) -> anyhow::Result<Vec<RootIssue>> {
let projects = repos.project_repo.list(None, None).await?;
let Some(project) = projects.into_iter().next() else {
return Ok(Vec::new());
};
let snapshot =
crate::persist::local::database::create_context_snapshot(db_service, &project.id).await?;
Ok(snapshot
.active_issues
.into_iter()
.enumerate()
.map(|(index, title)| RootIssue {
id: format!("workspace-active-issue-{}", index + 1),
title,
})
.collect())
}
pub async fn create_issue(
State(state): State<WebState>,
Path((owner, repo)): Path<(String, String)>,
Json(body): Json<CreateIssueRequest>,
) -> Result<Json<GitHubIssue>, Refusal> {
let github = github::of(&state)?;
let client = github::client()?;
github
.service
.create_issue(&client, &owner, &repo, &body)
.await
.map(Json)
.map_err(github::failure)
}
pub async fn update_issue(
State(state): State<WebState>,
Path((owner, repo, number)): Path<(String, String, i64)>,
Json(body): Json<UpdateIssueRequest>,
) -> Result<Json<GitHubIssue>, Refusal> {
let github = github::of(&state)?;
let client = github::client()?;
github
.service
.update_issue(&client, &owner, &repo, number, &body)
.await
.map(Json)
.map_err(github::failure)
}
#[derive(Deserialize)]
pub struct CommentRequest {
pub body: String,
}
pub async fn list_comments(
State(state): State<WebState>,
Path((owner, repo, number)): Path<(String, String, i64)>,
) -> Result<Json<serde_json::Value>, Refusal> {
let github = github::of(&state)?;
let client = github::client().ok();
let listed = github
.service
.comments(client.as_ref(), &format!("{owner}/{repo}"), number)
.await
.map_err(github::failure)?;
Ok(Json(serde_json::json!({ "comments": listed.items, "sync": listed.sync })))
}
pub async fn add_comment(
State(state): State<WebState>,
Path((owner, repo, number)): Path<(String, String, i64)>,
Json(body): Json<CommentRequest>,
) -> Result<Json<GitHubComment>, Refusal> {
let github = github::of(&state)?;
let client = github::client()?;
github
.service
.add_comment(&client, &owner, &repo, number, &body.body)
.await
.map(Json)
.map_err(github::failure)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_unknown_issue_state_is_a_bad_request() {
let refused = issue_state(&ListIssuesQuery {
state: Some("sometimes".to_string()),
repo: None,
})
.expect_err("a state nobody has");
assert_eq!(refused.0, axum::http::StatusCode::BAD_REQUEST);
assert!(issue_state(&ListIssuesQuery { state: None, repo: None }).is_ok(), "open is the default");
}
#[test]
fn github_refusals_keep_their_meaning() {
let (status, _) = github::failure(wsb_github::Failure::Invalid("no title".to_string()));
assert_eq!(status, axum::http::StatusCode::BAD_REQUEST);
let (status, body) = github::failure(wsb_github::Failure::GitHub(anyhow::anyhow!(
"failed with GitHub API status 401"
)));
assert_eq!(status, axum::http::StatusCode::UNAUTHORIZED);
assert!(body.0.error.contains("refused the token"));
let (status, _) = github::failure(wsb_github::Failure::GitHub(anyhow::anyhow!("connection reset")));
assert_eq!(status, axum::http::StatusCode::BAD_GATEWAY);
}
}