225 lines
7.6 KiB
Rust
225 lines
7.6 KiB
Rust
use axum::{
|
|
Json,
|
|
http::StatusCode,
|
|
response::{IntoResponse, Response},
|
|
};
|
|
use crank_mapping::MappingError;
|
|
use crank_registry::RegistryError;
|
|
use crank_runtime::RuntimeError;
|
|
use crank_schema::SchemaError;
|
|
use serde_json::{Value, json};
|
|
use thiserror::Error;
|
|
use tracing::{error, warn};
|
|
|
|
use crate::storage::StorageError;
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum ApiError {
|
|
#[error("{message}")]
|
|
Unauthorized { message: String },
|
|
#[error("{message}")]
|
|
Forbidden { message: String },
|
|
#[error("{message}")]
|
|
Validation { message: String },
|
|
#[error("{message}")]
|
|
NotFound { message: String },
|
|
#[error("{message}")]
|
|
Conflict { message: String },
|
|
#[error("{message}")]
|
|
Internal { message: String },
|
|
}
|
|
|
|
impl ApiError {
|
|
pub fn unauthorized(message: impl Into<String>) -> Self {
|
|
Self::Unauthorized {
|
|
message: message.into(),
|
|
}
|
|
}
|
|
|
|
pub fn forbidden(message: impl Into<String>) -> Self {
|
|
Self::Forbidden {
|
|
message: message.into(),
|
|
}
|
|
}
|
|
|
|
pub fn validation(message: impl Into<String>) -> Self {
|
|
Self::Validation {
|
|
message: message.into(),
|
|
}
|
|
}
|
|
|
|
pub fn not_found(message: impl Into<String>) -> Self {
|
|
Self::NotFound {
|
|
message: message.into(),
|
|
}
|
|
}
|
|
|
|
pub fn conflict(message: impl Into<String>) -> Self {
|
|
Self::Conflict {
|
|
message: message.into(),
|
|
}
|
|
}
|
|
|
|
pub fn internal(message: impl Into<String>) -> Self {
|
|
Self::Internal {
|
|
message: message.into(),
|
|
}
|
|
}
|
|
|
|
fn status_code(&self) -> StatusCode {
|
|
match self {
|
|
Self::Unauthorized { .. } => StatusCode::UNAUTHORIZED,
|
|
Self::Forbidden { .. } => StatusCode::FORBIDDEN,
|
|
Self::Validation { .. } => StatusCode::BAD_REQUEST,
|
|
Self::NotFound { .. } => StatusCode::NOT_FOUND,
|
|
Self::Conflict { .. } => StatusCode::CONFLICT,
|
|
Self::Internal { .. } => StatusCode::INTERNAL_SERVER_ERROR,
|
|
}
|
|
}
|
|
|
|
fn code(&self) -> &'static str {
|
|
match self {
|
|
Self::Unauthorized { .. } => "unauthorized",
|
|
Self::Forbidden { .. } => "forbidden",
|
|
Self::Validation { .. } => "validation_error",
|
|
Self::NotFound { .. } => "not_found",
|
|
Self::Conflict { .. } => "conflict",
|
|
Self::Internal { .. } => "internal_error",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl IntoResponse for ApiError {
|
|
fn into_response(self) -> Response {
|
|
match &self {
|
|
Self::Internal { message } => {
|
|
error!(error_code = self.code(), error_message = %message)
|
|
}
|
|
Self::Unauthorized { message }
|
|
| Self::Forbidden { message }
|
|
| Self::Validation { message }
|
|
| Self::NotFound { message }
|
|
| Self::Conflict { message } => {
|
|
warn!(error_code = self.code(), error_message = %message)
|
|
}
|
|
}
|
|
|
|
let body = Json(json!({
|
|
"error": {
|
|
"code": self.code(),
|
|
"message": self.to_string(),
|
|
}
|
|
}));
|
|
|
|
(self.status_code(), body).into_response()
|
|
}
|
|
}
|
|
|
|
impl From<RegistryError> for ApiError {
|
|
fn from(value: RegistryError) -> Self {
|
|
match value {
|
|
RegistryError::WorkspaceNotFound { workspace_id } => {
|
|
Self::not_found(format!("workspace {workspace_id} was not found"))
|
|
}
|
|
RegistryError::UserNotFound { user_id } => {
|
|
Self::not_found(format!("user {user_id} was not found"))
|
|
}
|
|
RegistryError::AgentNotFound { agent_id } => {
|
|
Self::not_found(format!("agent {agent_id} was not found"))
|
|
}
|
|
RegistryError::InvitationNotFound { invitation_id } => {
|
|
Self::not_found(format!("invitation {invitation_id} was not found"))
|
|
}
|
|
RegistryError::PlatformApiKeyNotFound { key_id } => {
|
|
Self::not_found(format!("platform api key {key_id} was not found"))
|
|
}
|
|
RegistryError::InvocationLogNotFound { log_id } => {
|
|
Self::not_found(format!("invocation log {log_id} was not found"))
|
|
}
|
|
RegistryError::PublishedAgentNotFound {
|
|
workspace_slug,
|
|
agent_slug,
|
|
} => Self::not_found(format!(
|
|
"published agent {workspace_slug}/{agent_slug} was not found"
|
|
)),
|
|
RegistryError::OperationNotFound { operation_id } => {
|
|
Self::not_found(format!("operation {operation_id} was not found"))
|
|
}
|
|
RegistryError::OperationVersionNotFound {
|
|
operation_id,
|
|
version,
|
|
} => Self::not_found(format!(
|
|
"operation version {version} for {operation_id} was not found"
|
|
)),
|
|
RegistryError::OperationHasPublishedAgentBindings { operation_id } => {
|
|
Self::conflict(format!(
|
|
"operation {operation_id} cannot be deleted while it is bound to a published agent"
|
|
))
|
|
}
|
|
RegistryError::AuthProfileNotFound { auth_profile_id } => {
|
|
Self::not_found(format!("auth profile {auth_profile_id} was not found"))
|
|
}
|
|
RegistryError::OperationAlreadyExists { operation_id } => {
|
|
Self::conflict(format!("operation {operation_id} already exists"))
|
|
}
|
|
RegistryError::WorkspaceSlugAlreadyExists { slug } => {
|
|
Self::conflict(format!("workspace with slug {slug} already exists"))
|
|
}
|
|
RegistryError::UserEmailAlreadyExists { email } => {
|
|
Self::conflict(format!("user with email {email} already exists"))
|
|
}
|
|
RegistryError::InvalidInitialVersion { .. }
|
|
| RegistryError::InvalidVersionSequence { .. }
|
|
| RegistryError::ImmutableOperationFieldChanged { .. }
|
|
| RegistryError::InvalidEnumRepresentation { .. }
|
|
| RegistryError::InvalidNumericValue { .. }
|
|
| RegistryError::YamlImportJobNotFound { .. } => Self::validation(value.to_string()),
|
|
RegistryError::Storage(_) | RegistryError::Serialization(_) => {
|
|
Self::internal(value.to_string())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<MappingError> for ApiError {
|
|
fn from(value: MappingError) -> Self {
|
|
Self::validation(value.to_string())
|
|
}
|
|
}
|
|
|
|
impl From<SchemaError> for ApiError {
|
|
fn from(value: SchemaError) -> Self {
|
|
Self::validation(value.to_string())
|
|
}
|
|
}
|
|
|
|
impl From<StorageError> for ApiError {
|
|
fn from(value: StorageError) -> Self {
|
|
match value {
|
|
StorageError::InvalidStorageRef { details } => Self::validation(details),
|
|
StorageError::Io(_) | StorageError::Serialization(_) => {
|
|
Self::internal(value.to_string())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn runtime_test_failure(error: &RuntimeError) -> Value {
|
|
json!({
|
|
"code": runtime_test_failure_code(error),
|
|
"message": error.to_string()
|
|
})
|
|
}
|
|
|
|
fn runtime_test_failure_code(error: &RuntimeError) -> &'static str {
|
|
match error {
|
|
RuntimeError::Schema(_) => "runtime_schema_error",
|
|
RuntimeError::Mapping(_) => "runtime_mapping_error",
|
|
RuntimeError::GraphqlAdapter(_) => "runtime_graphql_error",
|
|
RuntimeError::GrpcAdapter(_) => "runtime_grpc_error",
|
|
RuntimeError::RestAdapter(_) => "runtime_rest_error",
|
|
RuntimeError::UnsupportedProtocol { .. } => "runtime_protocol_error",
|
|
RuntimeError::InvalidPreparedRequest { .. } => "runtime_request_error",
|
|
}
|
|
}
|