Establish OA/SA prototype baseline and first-release work

This commit is contained in:
2026-09-05 17:04:16 +03:00
commit 93ba83d321
109 changed files with 56512 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
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>,
}
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,
})
.collect()
}