use axum::{ http::{StatusCode, header}, response::{IntoResponse, Response}, }; use serde::Serialize; pub type ApiResult = Result; #[derive(Debug, Serialize)] pub struct ApiError { #[serde(rename = "type")] problem_type: Box, pub title: &'static str, pub status: u16, pub code: &'static str, pub detail: String, #[serde(skip_serializing_if = "Option::is_none")] pub current_revision: Option, #[serde(skip_serializing_if = "Vec::is_empty")] pub issues: Vec, } #[derive(Debug, Clone, Serialize)] pub struct IssueDto { pub severity: &'static str, pub code: String, pub message: String, #[serde(skip_serializing_if = "Option::is_none")] pub field: Option, #[serde(skip_serializing_if = "Option::is_none")] pub object_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub line: Option, #[serde(skip_serializing_if = "Option::is_none")] pub column: Option, #[serde(skip_serializing_if = "Option::is_none")] pub path: Option, #[serde(skip_serializing_if = "Option::is_none")] pub span: Option, #[serde(skip_serializing_if = "Vec::is_empty")] pub related: Vec, } #[derive(Debug, Clone, Serialize)] pub struct SpanDto { pub start: usize, pub end: usize, } impl From for SpanDto { fn from(span: oa_core::language::Span) -> Self { Self { start: span.start, end: span.end, } } } #[derive(Debug, Clone, Serialize)] pub struct SourceLocationDto { pub path: String, pub span: SpanDto, } pub(crate) fn source_issue(issue: oa_core::source_project::SourceDiagnostic) -> IssueDto { let diagnostic = issue.diagnostic; IssueDto { severity: match diagnostic.severity { oa_core::validation::Severity::Error => "error", oa_core::validation::Severity::Warning => "warning", }, code: diagnostic.code, message: diagnostic.message, field: diagnostic.field, object_id: diagnostic.object_id, line: Some(diagnostic.line), column: Some(diagnostic.column), path: Some(issue.path), span: Some(diagnostic.span.into()), related: issue .related .into_iter() .map(|location| SourceLocationDto { path: location.path, span: location.span.into(), }) .collect(), } } pub(crate) fn source_error( diagnostics: Vec, ) -> ApiError { let code = if diagnostics .iter() .any(|d| d.diagnostic.code == "model_limit") { "model_limit" } else { "invalid_source_project" }; let mut error = ApiError::new( StatusCode::UNPROCESSABLE_ENTITY, code, "Source project could not be applied.", ); error.issues = diagnostics.into_iter().map(source_issue).collect(); error } impl ApiError { pub fn new(status: StatusCode, code: &'static str, detail: impl Into) -> Self { Self { problem_type: format!("urn:arcadia:problem:{code}").into_boxed_str(), title: status.canonical_reason().unwrap_or("Request failed"), status: status.as_u16(), code, detail: detail.into(), current_revision: None, issues: vec![], } } pub fn bad_request(detail: impl Into) -> Self { Self::new(StatusCode::BAD_REQUEST, "invalid_request", detail) } pub fn missing() -> Self { Self::new( StatusCode::NOT_FOUND, "project_not_found", "Project does not exist.", ) } pub fn storage(error: std::io::Error) -> Self { eprintln!("arcadia-server storage error: {error}"); Self::new( StatusCode::SERVICE_UNAVAILABLE, "storage_unavailable", "The storage operation failed. Reload the project before retrying a write.", ) } pub fn internal(detail: &str) -> Self { Self::new(StatusCode::INTERNAL_SERVER_ERROR, "internal_error", detail) } } impl IntoResponse for ApiError { fn into_response(self) -> Response { let status = StatusCode::from_u16(self.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); let mut response = (status, axum::Json(self)).into_response(); response.headers_mut().insert( header::CONTENT_TYPE, header::HeaderValue::from_static("application/problem+json"), ); if status == StatusCode::UNAUTHORIZED { response.headers_mut().insert( header::WWW_AUTHENTICATE, header::HeaderValue::from_static("Bearer realm=\"arcadia\""), ); } response } } pub fn issues(model: &oa_core::Project) -> Vec { oa_core::validation::validate(model) .into_iter() .map(|issue| IssueDto { severity: match issue.severity { oa_core::validation::Severity::Error => "error", oa_core::validation::Severity::Warning => "warning", }, message: issue.message, code: issue.code, field: issue.field, object_id: issue.element, line: None, column: None, path: None, span: None, related: vec![], }) .collect() }