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) -> Self { Self::Unauthorized { message: message.into(), } } pub fn forbidden(message: impl Into) -> Self { Self::Forbidden { message: message.into(), } } pub fn validation(message: impl Into) -> Self { Self::Validation { message: message.into(), } } pub fn not_found(message: impl Into) -> Self { Self::NotFound { message: message.into(), } } pub fn conflict(message: impl Into) -> Self { Self::Conflict { message: message.into(), } } pub fn internal(message: impl Into) -> 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 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 for ApiError { fn from(value: MappingError) -> Self { Self::validation(value.to_string()) } } impl From for ApiError { fn from(value: SchemaError) -> Self { Self::validation(value.to_string()) } } impl From 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", } }