use actix_web::{HttpResponse, web}; use serde::Deserialize; use utoipa::IntoParams; use crate::api::response::{ApiErrorResponse, ApiResponse}; use crate::error::AppError; use crate::models::base_info; use crate::models::base_info::UserBaseInfo; use crate::models::prs::{PrReviewComment, PrReviewDetail}; use crate::service::AppService; use crate::service::pr::reviews::{ AddReplyParams, CreateReviewParams, DismissReviewParams, SubmitReviewParams, }; use crate::session::Session; #[derive(Debug, Deserialize, IntoParams)] pub struct PrPath { /// Workspace name (unique identifier) pub workspace_name: String, /// Repository name (unique within the workspace) pub repo_name: String, /// PR number (unique within the repository) pub number: i64, } #[derive(Debug, Deserialize, IntoParams)] pub struct ReviewPath { /// Workspace name (unique identifier) pub workspace_name: String, /// Repository name (unique within the workspace) pub repo_name: String, /// PR number (unique within the repository) pub number: i64, /// Review ID (UUID) pub review_id: uuid::Uuid, } #[derive(Debug, Deserialize, IntoParams)] pub struct CommentPath { /// Workspace name (unique identifier) pub workspace_name: String, /// Repository name (unique within the workspace) pub repo_name: String, /// PR number (unique within the repository) pub number: i64, /// Comment ID (UUID) pub comment_id: uuid::Uuid, } #[derive(Debug, Deserialize, IntoParams)] pub struct QP { pub limit: Option, pub offset: Option, } /// List reviews on a PR #[utoipa::path( get, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/reviews", tag = "Pull Requests", operation_id = "prListReviews", params(PrPath, QP), responses( (status = 200, description = "Reviews listed.", body = ApiResponse>), (status = 401, description = "Authentication required", body = ApiErrorResponse), (status = 404, description = "PR not found", body = ApiErrorResponse), (status = 500, description = "Internal server error", body = ApiErrorResponse), ), security(("session_cookie" = [])) )] pub async fn list_reviews( service: web::Data, session: Session, path: web::Path, query: web::Query, ) -> Result { let reviews = service .pr .pr_list_reviews( &session, &path.workspace_name, &path.repo_name, path.number, query.limit.unwrap_or(50), query.offset.unwrap_or(0), ) .await?; let user_ids: Vec<_> = reviews.iter().map(|r| r.author_id).collect(); let users = base_info::resolve_users(&service.ctx.db, &user_ids).await?; let details: Vec = reviews .into_iter() .map(|r| { let author = users .get(&r.author_id) .cloned() .unwrap_or_else(|| UserBaseInfo::placeholder(r.author_id)); r.into_detail(author) }) .collect(); Ok(HttpResponse::Ok().json(ApiResponse::new(details))) } /// Create a review. States: pending, approved, changes_requested, commented. Authors cannot approve their own PRs. #[utoipa::path( post, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/reviews", tag = "Pull Requests", operation_id = "prCreateReview", params(PrPath), request_body(content = CreateReviewParams, description = "Review parameters", content_type = "application/json"), responses( (status = 201, description = "Review created.", body = ApiResponse), (status = 400, description = "Invalid state or self-approval", body = ApiErrorResponse), (status = 401, description = "Authentication required", body = ApiErrorResponse), (status = 403, description = "Insufficient permissions", body = ApiErrorResponse), (status = 404, description = "PR not found", body = ApiErrorResponse), (status = 500, description = "Internal server error", body = ApiErrorResponse), ), security(("session_cookie" = [])) )] pub async fn create_review( service: web::Data, session: Session, path: web::Path, params: web::Json, ) -> Result { let review = service .pr .pr_create_review( &session, &path.workspace_name, &path.repo_name, path.number, params.into_inner(), ) .await?; let author_id = review.author_id; let users = base_info::resolve_users(&service.ctx.db, &[author_id]).await?; let author = users .get(&author_id) .cloned() .unwrap_or_else(|| UserBaseInfo::placeholder(author_id)); Ok(HttpResponse::Created().json(ApiResponse::new(review.into_detail(author)))) } /// Submit a pending review. Changes its state to approved, changes_requested, or commented. #[utoipa::path( post, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/reviews/{review_id}/submit", tag = "Pull Requests", operation_id = "prSubmitReview", params(ReviewPath), request_body(content = SubmitReviewParams, description = "Submit parameters", content_type = "application/json"), responses( (status = 200, description = "Review submitted.", body = ApiResponse), (status = 400, description = "Invalid state or self-approval", body = ApiErrorResponse), (status = 401, description = "Authentication required", body = ApiErrorResponse), (status = 403, description = "Insufficient permissions", body = ApiErrorResponse), (status = 404, description = "Review not found or already submitted", body = ApiErrorResponse), (status = 500, description = "Internal server error", body = ApiErrorResponse), ), security(("session_cookie" = [])) )] pub async fn submit_review( service: web::Data, session: Session, path: web::Path, params: web::Json, ) -> Result { let review = service .pr .pr_submit_review( &session, &path.workspace_name, &path.repo_name, path.number, path.review_id, params.into_inner(), ) .await?; let author_id = review.author_id; let users = base_info::resolve_users(&service.ctx.db, &[author_id]).await?; let author = users .get(&author_id) .cloned() .unwrap_or_else(|| UserBaseInfo::placeholder(author_id)); Ok(HttpResponse::Ok().json(ApiResponse::new(review.into_detail(author)))) } /// Dismiss a submitted review. Requires Admin role. #[utoipa::path( post, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/reviews/{review_id}/dismiss", tag = "Pull Requests", operation_id = "prDismissReview", params(ReviewPath), request_body(content = DismissReviewParams, description = "Dismiss parameters", content_type = "application/json"), responses( (status = 200, description = "Review dismissed.", body = ApiResponse), (status = 401, description = "Authentication required", body = ApiErrorResponse), (status = 403, description = "Insufficient permissions (Admin required)", body = ApiErrorResponse), (status = 404, description = "Review not found or not submitted", body = ApiErrorResponse), (status = 500, description = "Internal server error", body = ApiErrorResponse), ), security(("session_cookie" = [])) )] pub async fn dismiss_review( service: web::Data, session: Session, path: web::Path, params: web::Json, ) -> Result { let review = service .pr .pr_dismiss_review( &session, &path.workspace_name, &path.repo_name, path.number, path.review_id, params.into_inner(), ) .await?; let author_id = review.author_id; let users = base_info::resolve_users(&service.ctx.db, &[author_id]).await?; let author = users .get(&author_id) .cloned() .unwrap_or_else(|| UserBaseInfo::placeholder(author_id)); Ok(HttpResponse::Ok().json(ApiResponse::new(review.into_detail(author)))) } /// List comments for a specific review #[utoipa::path( get, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/reviews/{review_id}/comments", tag = "Pull Requests", operation_id = "prListReviewComments", params(ReviewPath, QP), responses( (status = 200, description = "Review comments listed.", body = ApiResponse>), (status = 401, description = "Authentication required", body = ApiErrorResponse), (status = 404, description = "Review not found", body = ApiErrorResponse), (status = 500, description = "Internal server error", body = ApiErrorResponse), ), security(("session_cookie" = [])) )] pub async fn list_review_comments( service: web::Data, session: Session, path: web::Path, query: web::Query, ) -> Result { let comments = service .pr .pr_review_comments( &session, &path.workspace_name, &path.repo_name, path.number, path.review_id, query.limit.unwrap_or(50), query.offset.unwrap_or(0), ) .await?; Ok(HttpResponse::Ok().json(ApiResponse::new(comments))) } /// Reply to a review comment #[utoipa::path( post, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/comments/{comment_id}/reply", tag = "Pull Requests", operation_id = "prAddReviewReply", params(CommentPath), request_body(content = AddReplyParams, description = "Reply parameters", content_type = "application/json"), responses( (status = 201, description = "Reply added.", body = ApiResponse), (status = 400, description = "Empty body", body = ApiErrorResponse), (status = 401, description = "Authentication required", body = ApiErrorResponse), (status = 404, description = "Comment not found", body = ApiErrorResponse), (status = 500, description = "Internal server error", body = ApiErrorResponse), ), security(("session_cookie" = [])) )] pub async fn add_review_reply( service: web::Data, session: Session, path: web::Path, params: web::Json, ) -> Result { let comment = service .pr .pr_add_review_reply( &session, &path.workspace_name, &path.repo_name, path.number, path.comment_id, params.into_inner(), ) .await?; Ok(HttpResponse::Created().json(ApiResponse::new(comment))) } /// Update a review comment (own comments only) #[utoipa::path( put, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/comments/{comment_id}", tag = "Pull Requests", operation_id = "prUpdateReviewComment", params(CommentPath), request_body(content = AddReplyParams, description = "Update parameters", content_type = "application/json"), responses( (status = 200, description = "Comment updated.", body = ApiResponse), (status = 401, description = "Authentication required", body = ApiErrorResponse), (status = 403, description = "Cannot edit other users' comments", body = ApiErrorResponse), (status = 404, description = "Comment not found", body = ApiErrorResponse), (status = 500, description = "Internal server error", body = ApiErrorResponse), ), security(("session_cookie" = [])) )] pub async fn update_review_comment( service: web::Data, session: Session, path: web::Path, params: web::Json, ) -> Result { let comment = service .pr .pr_update_review_comment( &session, &path.workspace_name, &path.repo_name, path.number, path.comment_id, params.into_inner(), ) .await?; Ok(HttpResponse::Ok().json(ApiResponse::new(comment))) } /// Delete a review comment. Own comments or Admin role. #[utoipa::path( delete, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/comments/{comment_id}", tag = "Pull Requests", operation_id = "prDeleteReviewComment", params(CommentPath), responses( (status = 200, description = "Comment deleted.", body = ApiResponse), (status = 401, description = "Authentication required", body = ApiErrorResponse), (status = 403, description = "Cannot delete other users' comments (Admin required)", body = ApiErrorResponse), (status = 404, description = "Comment not found", body = ApiErrorResponse), (status = 500, description = "Internal server error", body = ApiErrorResponse), ), security(("session_cookie" = [])) )] pub async fn delete_review_comment( service: web::Data, session: Session, path: web::Path, ) -> Result { service .pr .pr_delete_review_comment( &session, &path.workspace_name, &path.repo_name, path.number, path.comment_id, ) .await?; Ok(HttpResponse::Ok().json(ApiResponse::new("Comment deleted".to_string()))) }