use axum::extract::{Path, State};
use axum::Json;
use serde::Deserialize;
use serde_json::json;
use crate::web::github::{self, Refusal};
use crate::web::state::WebState;
use wsb_github::client::{GitHubProjectItem, ProjectFieldWrite};
pub async fn list_projects(State(state): State<WebState>) -> Result<Json<serde_json::Value>, Refusal> {
let github = github::of(&state)?;
let references = github::board_references(github, &state)?;
let service = github.service.clone();
let all = github::blocking(move || service.all_boards(&references)).await?;
Ok(Json(json!({
"projects": all.boards,
"owners": all.owners,
"referenced": all.referenced,
"account": all.account,
"problems": all.problems,
})))
}
pub async fn refresh_projects(
State(state): State<WebState>,
) -> Result<Json<wsb_github::sync::SyncReport>, Refusal> {
let github = github::of(&state)?;
let references = github::board_references(github, &state)?;
let client = match github::client() {
Ok(client) => client,
Err(_) => {
return Ok(Json(wsb_github::sync::SyncReport {
no_token: true,
..Default::default()
}))
}
};
Ok(Json(github.service.refresh_boards(&client, &references).await))
}
pub async fn project_items(
State(state): State<WebState>,
Path(project): Path<String>,
) -> Result<Json<serde_json::Value>, Refusal> {
let github = github::of(&state)?;
let service = github.service.clone();
let board = project.clone();
let mut cards = github::blocking(move || service.cards(&board)).await?;
if !cards.sync.ever_synced {
if let Ok(client) = github::client() {
let board = known_board(&state, &project).await?;
let report = github.service.refresh_board(&client, &board, true).await;
if let Some((scope, error)) = report.failed.first() {
tracing::debug!(%scope, %error, "a board opened for the first time could not be read");
}
let service = github.service.clone();
let board = project.clone();
cards = github::blocking(move || service.cards(&board)).await?;
}
}
Ok(Json(json!({ "items": cards.items, "sync": cards.sync })))
}
async fn known_board(state: &WebState, id: &str) -> Result<wsb_github::client::GitHubProject, Refusal> {
let github = github::of(state)?;
let references = github::board_references(github, state)?;
let service = github.service.clone();
let all = github::blocking(move || service.all_boards(&references)).await?;
all.boards
.into_iter()
.find(|board| board.id == id)
.ok_or_else(|| {
github::failure(wsb_github::Failure::Invalid(format!(
"board {id} is not one the mirror lists for this account or its organisations"
)))
})
}
fn same_board(path: &str, body: &str) -> Result<(), Refusal> {
if path == body {
return Ok(());
}
Err(github::failure(wsb_github::Failure::Invalid(format!(
"the path names project {path} and the body names {body}"
))))
}
fn item(card: GitHubProjectItem) -> Json<serde_json::Value> {
Json(json!({ "item": card }))
}
#[derive(Debug, Deserialize)]
pub struct FieldRequest {
pub project: String,
pub field: String,
pub value: Option<ProjectFieldWrite>,
}
pub async fn set_field(
State(state): State<WebState>,
Path((project, card)): Path<(String, String)>,
Json(body): Json<FieldRequest>,
) -> Result<Json<serde_json::Value>, Refusal> {
same_board(&project, &body.project)?;
let github = github::of(&state)?;
let client = github::client()?;
github
.service
.set_field(&client, &project, &card, &body.field, body.value.as_ref())
.await
.map(item)
.map_err(github::failure)
}
#[derive(Debug, Deserialize)]
pub struct PositionRequest {
pub project: String,
pub after: Option<String>,
}
pub async fn set_position(
State(state): State<WebState>,
Path((project, card)): Path<(String, String)>,
Json(body): Json<PositionRequest>,
) -> Result<Json<serde_json::Value>, Refusal> {
same_board(&project, &body.project)?;
let github = github::of(&state)?;
let client = github::client()?;
github
.service
.move_card(&client, &project, &card, body.after.as_deref())
.await
.map(item)
.map_err(github::failure)
}
#[derive(Debug, Deserialize)]
pub struct ArchiveRequest {
pub project: String,
pub archived: bool,
}
pub async fn set_archived(
State(state): State<WebState>,
Path((project, card)): Path<(String, String)>,
Json(body): Json<ArchiveRequest>,
) -> Result<Json<serde_json::Value>, Refusal> {
same_board(&project, &body.project)?;
let github = github::of(&state)?;
let client = github::client()?;
github
.service
.set_archived(&client, &project, &card, body.archived)
.await
.map(item)
.map_err(github::failure)
}
#[derive(Debug, Deserialize)]
pub struct DraftRequest {
pub project: String,
pub content: String,
pub title: String,
#[serde(default)]
pub body: String,
}
pub async fn edit_draft(
State(state): State<WebState>,
Path((project, card)): Path<(String, String)>,
Json(body): Json<DraftRequest>,
) -> Result<Json<serde_json::Value>, Refusal> {
same_board(&project, &body.project)?;
let github = github::of(&state)?;
let client = github::client()?;
github
.service
.edit_draft(&client, &project, &card, &body.content, &body.title, &body.body)
.await
.map(item)
.map_err(github::failure)
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum AddRequest {
Existing { project: String, url: String },
Draft {
project: String,
title: String,
#[serde(default)]
body: String,
},
}
impl AddRequest {
fn project(&self) -> &str {
match self {
Self::Existing { project, .. } | Self::Draft { project, .. } => project,
}
}
}
pub async fn add_item(
State(state): State<WebState>,
Path(project): Path<String>,
Json(body): Json<AddRequest>,
) -> Result<Json<serde_json::Value>, Refusal> {
same_board(&project, body.project())?;
let github = github::of(&state)?;
let client = github::client()?;
let added = match &body {
AddRequest::Existing { url, .. } => github.service.add_existing(&client, &project, url).await,
AddRequest::Draft { title, body, .. } => {
github.service.add_draft(&client, &project, title, body).await
}
};
added.map(item).map_err(github::failure)
}
#[derive(Debug, Deserialize)]
pub struct RemoveRequest {
pub project: String,
}
pub async fn remove_item(
State(state): State<WebState>,
Path((project, card)): Path<(String, String)>,
Json(body): Json<RemoveRequest>,
) -> Result<Json<serde_json::Value>, Refusal> {
same_board(&project, &body.project)?;
let github = github::of(&state)?;
let client = github::client()?;
github
.service
.remove_card(&client, &project, &card)
.await
.map_err(github::failure)?;
Ok(Json(json!({ "removed": card })))
}
#[derive(Debug, Deserialize)]
pub struct AddColumnRequest {
pub project: String,
pub field: String,
pub name: String,
pub color: String,
pub description: String,
}
pub async fn add_column(
State(state): State<WebState>,
Path(project): Path<String>,
Json(body): Json<AddColumnRequest>,
) -> Result<Json<serde_json::Value>, Refusal> {
same_board(&project, &body.project)?;
let github = github::of(&state)?;
let client = github::client()?;
let (board, items) = github
.service
.add_column(&client, &project, &body.field, &body.name, &body.color, &body.description)
.await
.map_err(github::failure)?;
Ok(Json(json!({ "project": board, "items": items })))
}
#[derive(Debug, Deserialize)]
pub struct RemoveColumnRequest {
pub project: String,
pub field: String,
}
pub async fn remove_column(
State(state): State<WebState>,
Path((project, option)): Path<(String, String)>,
Json(body): Json<RemoveColumnRequest>,
) -> Result<Json<serde_json::Value>, Refusal> {
same_board(&project, &body.project)?;
let github = github::of(&state)?;
let client = github::client()?;
let (board, items) = github
.service
.remove_column(&client, &project, &body.field, &option)
.await
.map_err(github::failure)?;
Ok(Json(json!({ "project": board, "items": items })))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_write_naming_two_boards_is_refused() {
assert!(same_board("PVT_1", "PVT_1").is_ok());
let (status, body) = same_board("PVT_1", "PVT_2").expect_err("two boards");
assert_eq!(status, axum::http::StatusCode::BAD_REQUEST);
assert!(body.0.error.contains("PVT_1") && body.0.error.contains("PVT_2"));
}
}