188 lines
5.5 KiB
Rust
188 lines
5.5 KiB
Rust
use axum::{
|
|
http::{StatusCode, header},
|
|
response::{IntoResponse, Response},
|
|
};
|
|
use serde::Serialize;
|
|
|
|
pub type ApiResult<T> = Result<T, ApiError>;
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ApiError {
|
|
#[serde(rename = "type")]
|
|
problem_type: Box<str>,
|
|
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<u64>,
|
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
pub issues: Vec<IssueDto>,
|
|
}
|
|
|
|
#[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<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub object_id: Option<oa_core::Id>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub line: Option<usize>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub column: Option<usize>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub path: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub span: Option<SpanDto>,
|
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
pub related: Vec<SourceLocationDto>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct SpanDto {
|
|
pub start: usize,
|
|
pub end: usize,
|
|
}
|
|
|
|
impl From<oa_core::language::Span> 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<oa_core::source_project::SourceDiagnostic>,
|
|
) -> 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<String>) -> 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<String>) -> 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<IssueDto> {
|
|
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()
|
|
}
|