use axum::{
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
routing::{get, post},
Json, Router,
};
use mlua_swarm::blueprint::store::BlueprintId;
use mlua_swarm::store::issue::{IssueId, IssuePayload, IssueStatus, IssueStore, IssueStoreError};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostIssueRequest {
pub blueprint_id: String,
pub intent: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostIssueResponse {
pub issue_id: String,
pub status: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetIssueResponse {
pub issue_id: String,
pub status: String, pub blueprint_id: Option<String>,
pub intent: Option<String>,
pub reason: Option<String>,
pub new_version: Option<String>,
}
pub fn build_issues_router(store: Arc<dyn IssueStore>) -> Router {
Router::new()
.route("/v1/issues", post(post_issue))
.route("/v1/issues/:issue_id", get(get_issue))
.with_state(store)
}
async fn post_issue(
State(store): State<Arc<dyn IssueStore>>,
Json(req): Json<PostIssueRequest>,
) -> Result<(StatusCode, Json<PostIssueResponse>), (StatusCode, String)> {
if req.intent.trim().is_empty() {
return Err((StatusCode::BAD_REQUEST, "intent must be non-empty".into()));
}
let issue_id = format!("h-{}", uuid::Uuid::new_v4());
let payload = IssuePayload {
issue_id: IssueId::new(issue_id.clone()),
blueprint_id: BlueprintId::new(req.blueprint_id),
intent: req.intent,
};
store
.create(payload)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok((
StatusCode::CREATED,
Json(PostIssueResponse {
issue_id,
status: "pending".into(),
}),
))
}
async fn get_issue(
State(store): State<Arc<dyn IssueStore>>,
Path(issue_id): Path<String>,
) -> Result<Json<GetIssueResponse>, (StatusCode, String)> {
let id = IssueId::new(issue_id.clone());
let status = match store.status(&id).await {
Ok(s) => s,
Err(IssueStoreError::NotFound(_)) => {
return Err((
StatusCode::NOT_FOUND,
format!("issue not found: {issue_id}"),
));
}
Err(e) => return Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())),
};
let payload = store.get(&id).await.ok();
let (status_str, reason, new_version) = match &status {
IssueStatus::Pending => ("pending".to_string(), None, None),
IssueStatus::InFlight => ("in_flight".to_string(), None, None),
IssueStatus::Applied { new_version } => {
("applied".to_string(), None, Some(new_version.clone()))
}
IssueStatus::Rejected { reason } => ("rejected".to_string(), Some(reason.clone()), None),
};
Ok(Json(GetIssueResponse {
issue_id,
status: status_str,
blueprint_id: payload
.as_ref()
.map(|p| p.blueprint_id.as_str().to_string()),
intent: payload.as_ref().map(|p| p.intent.clone()),
reason,
new_version,
}))
}
pub struct IssueStoreErrorResponse(
pub IssueStoreError,
);
impl IntoResponse for IssueStoreErrorResponse {
fn into_response(self) -> axum::response::Response {
(StatusCode::INTERNAL_SERVER_ERROR, self.0.to_string()).into_response()
}
}