feat(openapi): complete upload preview and UI evidence

This commit is contained in:
2026-08-28 15:38:14 +03:00
parent d2849ea3fe
commit 2c94af6791
35 changed files with 3655 additions and 537 deletions
+109 -4
View File
@@ -11,6 +11,7 @@ use serde_json::{Value, json};
use thiserror::Error;
use tracing::{error, warn};
use crate::dto::OpenApiUploadLocale;
use crate::storage::StorageError;
#[derive(Debug, Error)]
@@ -96,6 +97,112 @@ impl ApiError {
}
}
pub(crate) fn openapi_upload(locale: OpenApiUploadLocale, code: &'static str) -> Self {
let russian = locale == OpenApiUploadLocale::Ru;
let message = match (russian, code) {
(_, "file_too_large") => {
if russian {
"файл OpenAPI превышает лимит 256 КиБ"
} else {
"OpenAPI file exceeds the 256 KiB limit"
}
}
(_, "empty_file") => {
if russian {
"файл OpenAPI не должен быть пустым"
} else {
"OpenAPI file must not be empty"
}
}
(_, "invalid_utf8") => {
if russian {
"файл OpenAPI должен быть в UTF-8"
} else {
"OpenAPI file must be UTF-8"
}
}
(_, "invalid_media_type") => {
if russian {
"тип файла OpenAPI не поддерживается"
} else {
"OpenAPI file type is not supported"
}
}
(_, "invalid_document") => {
if russian {
"некорректный или неподдерживаемый документ OpenAPI"
} else {
"OpenAPI document is invalid or unsupported"
}
}
(_, "no_methods") => {
if russian {
"документ OpenAPI не содержит поддерживаемых методов"
} else {
"OpenAPI document contains no supported methods"
}
}
(_, "source_integrity") => {
if russian {
"проверка целостности источника OpenAPI не пройдена"
} else {
"OpenAPI source integrity verification failed"
}
}
(_, "source_unavailable") => {
if russian {
"источник OpenAPI недоступен"
} else {
"OpenAPI source is unavailable"
}
}
(_, "parser_unavailable") | (_, "storage_unavailable") => {
if russian {
"обработка OpenAPI временно недоступна"
} else {
"OpenAPI processing is temporarily unavailable"
}
}
_ => {
if russian {
"некорректная multipart-загрузка OpenAPI"
} else {
"invalid OpenAPI multipart upload"
}
}
};
let context = json!({ "error_code": format!("openapi_upload.{code}") });
if code == "file_too_large" {
Self::PayloadTooLarge {
message: message.to_owned(),
context: Some(context),
}
} else if matches!(code, "storage_unavailable" | "parser_unavailable") {
Self::Internal {
message: message.to_owned(),
context: Some(context),
}
} else if matches!(code, "source_integrity" | "source_unavailable") {
Self::Unprocessable {
message: message.to_owned(),
context: Some(context),
}
} else {
Self::Validation {
message: message.to_owned(),
context: Some(context),
}
}
}
pub(crate) fn source_unavailable() -> Self {
Self::openapi_upload(OpenApiUploadLocale::En, "source_unavailable")
}
pub(crate) fn source_integrity() -> Self {
Self::openapi_upload(OpenApiUploadLocale::En, "source_integrity")
}
pub(crate) fn rate_limited_with_context(message: impl Into<String>, context: Value) -> Self {
Self::RateLimited {
message: message.into(),
@@ -588,17 +695,15 @@ impl From<RegistryError> for ApiError {
format!("import job {job_id} was already applied with different parameters"),
json!({ "job_id": job_id }),
),
RegistryError::SourceNotFound { source_id } => Self::not_found_with_context(
RegistryError::SourceNotFound { .. } => Self::not_found_with_context(
"artifact source was not found",
json!({
"source_id": source_id,
"error_code": "artifact_source_not_found"
}),
),
RegistryError::SourceConflict { source_id } => Self::conflict_with_context(
RegistryError::SourceConflict { .. } => Self::conflict_with_context(
"artifact source metadata or lifecycle conflicts with the request",
json!({
"source_id": source_id,
"error_code": "artifact_source_conflict",
"recovery": "reload"
}),