#![deny(non_snake_case)]
use std::{error::Error, fmt};
use anyhow::anyhow;
use http::status::StatusCode;
use lexe_common::api::{
MegaId, auth,
user::{NodePk, UserPk},
};
#[cfg(any(test, feature = "test-utils"))]
use lexe_common::test_utils::arbitrary;
use lexe_enclave::enclave::{self, Measurement};
#[cfg(any(test, feature = "test-utils"))]
use proptest_derive::Arbitrary;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[cfg(feature = "axum")]
use tracing::{error, warn};
#[cfg(feature = "axum")]
use crate::axum_helpers;
pub const CLIENT_400_BAD_REQUEST: StatusCode = StatusCode::BAD_REQUEST;
pub const CLIENT_401_UNAUTHORIZED: StatusCode = StatusCode::UNAUTHORIZED;
pub const CLIENT_404_NOT_FOUND: StatusCode = StatusCode::NOT_FOUND;
pub const CLIENT_409_CONFLICT: StatusCode = StatusCode::CONFLICT;
pub const SERVER_500_INTERNAL_SERVER_ERROR: StatusCode =
StatusCode::INTERNAL_SERVER_ERROR;
pub const SERVER_502_BAD_GATEWAY: StatusCode = StatusCode::BAD_GATEWAY;
pub const SERVER_503_SERVICE_UNAVAILABLE: StatusCode =
StatusCode::SERVICE_UNAVAILABLE;
pub const SERVER_504_GATEWAY_TIMEOUT: StatusCode = StatusCode::GATEWAY_TIMEOUT;
pub type ErrorCode = u16;
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
pub struct ErrorResponse {
pub code: ErrorCode,
#[cfg_attr(
any(test, feature = "test-utils"),
proptest(strategy = "arbitrary::any_string()")
)]
pub msg: String,
#[cfg_attr(
any(test, feature = "test-utils"),
proptest(strategy = "arbitrary::any_json_value()")
)]
#[serde(default)] pub data: serde_json::Value,
#[serde(default)] pub sensitive: bool,
}
pub trait ApiError:
ToHttpStatus
+ From<CommonApiError>
+ From<ErrorResponse>
+ Into<ErrorResponse>
+ Error
+ Clone
{
}
impl<E> ApiError for E where
E: ToHttpStatus
+ From<CommonApiError>
+ From<ErrorResponse>
+ Into<ErrorResponse>
+ Error
+ Clone
{
}
pub trait ApiErrorKind:
Copy
+ Clone
+ Default
+ Eq
+ PartialEq
+ fmt::Debug
+ fmt::Display
+ ToHttpStatus
+ From<CommonErrorKind>
+ From<ErrorCode>
+ Sized
+ 'static
{
const KINDS: &'static [Self];
fn is_unknown(&self) -> bool;
fn to_name(self) -> &'static str;
fn to_msg(self) -> &'static str;
fn to_code(self) -> ErrorCode;
fn from_code(code: ErrorCode) -> Self;
}
pub trait ToHttpStatus {
fn to_http_status(&self) -> StatusCode;
}
#[macro_export]
macro_rules! api_error {
($api_error:ident, $api_error_kind:ident) => {
#[derive(Clone, Debug, Default, Eq, PartialEq, Error)]
pub struct $api_error<D = serde_json::Value> {
pub kind: $api_error_kind,
pub msg: String,
pub data: D,
pub sensitive: bool,
}
impl $api_error {
#[cfg(feature = "axum")]
fn log_and_status(&self) -> StatusCode {
let status = self.to_http_status();
if status.is_server_error() {
tracing::error!("{self}");
} else if status.is_client_error() {
tracing::warn!("{self}");
} else {
tracing::error!(
"Unexpected status code {status} for error: {self}"
);
}
status
}
}
impl fmt::Display for $api_error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let kind_msg = self.kind.to_msg();
let msg = &self.msg;
write!(f, "{kind_msg}: {msg}")
}
}
impl From<ErrorResponse> for $api_error {
fn from(err_resp: ErrorResponse) -> Self {
let ErrorResponse {
code,
msg,
data,
sensitive,
} = err_resp;
let kind = $api_error_kind::from_code(code);
Self {
kind,
msg,
data,
sensitive,
}
}
}
impl From<$api_error> for ErrorResponse {
fn from(api_error: $api_error) -> Self {
let $api_error {
kind,
msg,
data,
sensitive,
} = api_error;
let code = kind.to_code();
Self {
code,
msg,
data,
sensitive,
}
}
}
impl From<CommonApiError> for $api_error {
fn from(common_error: CommonApiError) -> Self {
let CommonApiError { kind, msg } = common_error;
let kind = $api_error_kind::from(kind);
Self {
kind,
msg,
..Default::default()
}
}
}
impl ToHttpStatus for $api_error {
fn to_http_status(&self) -> StatusCode {
self.kind.to_http_status()
}
}
#[cfg(feature = "axum")]
impl axum::response::IntoResponse for $api_error {
fn into_response(self) -> http::Response<axum::body::Body> {
let status = self.log_and_status();
let error_response = ErrorResponse::from(self);
axum_helpers::build_json_response(status, &error_response)
}
}
#[cfg(any(test, feature = "test-utils"))]
impl proptest::arbitrary::Arbitrary for $api_error {
type Parameters = ();
type Strategy = proptest::strategy::BoxedStrategy<Self>;
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
use proptest::{arbitrary::any, strategy::Strategy};
(
any::<$api_error_kind>(),
arbitrary::any_string(),
arbitrary::any_json_value(),
any::<bool>(),
)
.prop_map(|(kind, msg, data, sensitive)| Self {
kind,
msg,
data,
sensitive,
})
.boxed()
}
}
};
}
#[macro_export]
macro_rules! api_error_kind {
{
$(#[$enum_meta:meta])*
pub enum $error_kind_name:ident {
$( #[doc = $unknown_msg:literal] )*
Unknown(ErrorCode),
$(
// use the doc string for the error message
$( #[doc = $item_msg:literal] )*
$item_name:ident = $item_code:literal
),*
$(,)?
}
} => {
$(#[$enum_meta])*
pub enum $error_kind_name {
$( #[doc = $unknown_msg] )*
Unknown(ErrorCode),
$(
$( #[doc = $item_msg] )*
$item_name
),*
}
impl ApiErrorKind for $error_kind_name {
const KINDS: &'static [Self] = &[
$( Self::$item_name, )*
];
#[inline]
fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown(_))
}
fn to_name(self) -> &'static str {
match self {
$( Self::$item_name => stringify!($item_name), )*
Self::Unknown(_) => "Unknown",
}
}
fn to_msg(self) -> &'static str {
let kind_msg = match self {
$( Self::$item_name => concat!($( $item_msg, )*), )*
Self::Unknown(_) => concat!($( $unknown_msg, )*),
};
kind_msg.trim_start()
}
fn to_code(self) -> ErrorCode {
match self {
$( Self::$item_name => $item_code, )*
Self::Unknown(code) => code,
}
}
fn from_code(code: ErrorCode) -> Self {
#[deny(unreachable_patterns)]
match code {
0 => Self::Unknown(0),
$( $item_code => Self::$item_name, )*
_ => Self::Unknown(code),
}
}
}
impl Default for $error_kind_name {
fn default() -> Self {
Self::Unknown(0)
}
}
impl fmt::Display for $error_kind_name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let msg = (*self).to_msg();
write!(f, "{msg}")
}
}
impl From<ErrorCode> for $error_kind_name {
#[inline]
fn from(code: ErrorCode) -> Self {
Self::from_code(code)
}
}
impl From<$error_kind_name> for ErrorCode {
#[inline]
fn from(val: $error_kind_name) -> ErrorCode {
val.to_code()
}
}
impl From<CommonErrorKind> for $error_kind_name {
#[inline]
fn from(common: CommonErrorKind) -> Self {
Self::from_code(common.to_code())
}
}
#[cfg(any(test, feature = "test-utils"))]
impl proptest::arbitrary::Arbitrary for $error_kind_name {
type Parameters = ();
type Strategy = proptest::strategy::BoxedStrategy<Self>;
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
use proptest::{prop_oneof, sample};
use proptest::arbitrary::any;
use proptest::strategy::Strategy;
prop_oneof![
9 => sample::select(Self::KINDS),
1 => any::<ErrorCode>().prop_map(Self::from_code),
].boxed()
}
}
}
}
pub struct CommonApiError {
pub kind: CommonErrorKind,
pub msg: String,
}
api_error!(BackendApiError, BackendErrorKind);
api_error!(GatewayApiError, GatewayErrorKind);
api_error!(LspApiError, LspErrorKind);
api_error!(MegaApiError, MegaErrorKind);
api_error!(NodeApiError, NodeErrorKind);
api_error!(RunnerApiError, RunnerErrorKind);
api_error!(SdkApiError, SdkErrorKind);
#[derive(Copy, Clone, Debug)]
#[repr(u16)]
pub enum CommonErrorKind {
UnknownReqwest = 1,
Building = 2,
Connect = 3,
Timeout = 4,
Decode = 5,
Server = 6,
Rejection = 7,
AtCapacity = 8,
}
impl ToHttpStatus for CommonErrorKind {
fn to_http_status(&self) -> StatusCode {
use CommonErrorKind::*;
match self {
UnknownReqwest => CLIENT_400_BAD_REQUEST,
Building => CLIENT_400_BAD_REQUEST,
Connect => SERVER_503_SERVICE_UNAVAILABLE,
Timeout => SERVER_504_GATEWAY_TIMEOUT,
Decode => SERVER_502_BAD_GATEWAY,
Server => SERVER_500_INTERNAL_SERVER_ERROR,
Rejection => CLIENT_400_BAD_REQUEST,
AtCapacity => SERVER_503_SERVICE_UNAVAILABLE,
}
}
}
api_error_kind! {
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum BackendErrorKind {
Unknown(ErrorCode),
UnknownReqwest = 1,
Building = 2,
Connect = 3,
Timeout = 4,
Decode = 5,
Server = 6,
Rejection = 7,
AtCapacity = 8,
Database = 100,
NotFound = 101,
Duplicate = 102,
Conversion = 103,
Unauthenticated = 104,
Unauthorized = 105,
AuthExpired = 106,
InvalidParsedRequest = 107,
BatchSizeOverLimit = 108,
NotUpdatable = 109,
}
}
impl ToHttpStatus for BackendErrorKind {
fn to_http_status(&self) -> StatusCode {
use BackendErrorKind::*;
match self {
Unknown(_) => SERVER_500_INTERNAL_SERVER_ERROR,
UnknownReqwest => CLIENT_400_BAD_REQUEST,
Building => CLIENT_400_BAD_REQUEST,
Connect => SERVER_503_SERVICE_UNAVAILABLE,
Timeout => SERVER_504_GATEWAY_TIMEOUT,
Decode => SERVER_502_BAD_GATEWAY,
Server => SERVER_500_INTERNAL_SERVER_ERROR,
Rejection => CLIENT_400_BAD_REQUEST,
AtCapacity => SERVER_503_SERVICE_UNAVAILABLE,
Database => SERVER_500_INTERNAL_SERVER_ERROR,
NotFound => CLIENT_404_NOT_FOUND,
Duplicate => CLIENT_409_CONFLICT,
Conversion => SERVER_500_INTERNAL_SERVER_ERROR,
Unauthenticated => CLIENT_401_UNAUTHORIZED,
Unauthorized => CLIENT_401_UNAUTHORIZED,
AuthExpired => CLIENT_401_UNAUTHORIZED,
InvalidParsedRequest => CLIENT_400_BAD_REQUEST,
BatchSizeOverLimit => CLIENT_400_BAD_REQUEST,
NotUpdatable => CLIENT_400_BAD_REQUEST,
}
}
}
api_error_kind! {
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum GatewayErrorKind {
Unknown(ErrorCode),
UnknownReqwest = 1,
Building = 2,
Connect = 3,
Timeout = 4,
Decode = 5,
Server = 6,
Rejection = 7,
AtCapacity = 8,
FiatRatesMissing = 100,
}
}
impl ToHttpStatus for GatewayErrorKind {
fn to_http_status(&self) -> StatusCode {
use GatewayErrorKind::*;
match self {
Unknown(_) => SERVER_500_INTERNAL_SERVER_ERROR,
UnknownReqwest => CLIENT_400_BAD_REQUEST,
Building => CLIENT_400_BAD_REQUEST,
Connect => SERVER_503_SERVICE_UNAVAILABLE,
Timeout => SERVER_504_GATEWAY_TIMEOUT,
Decode => SERVER_502_BAD_GATEWAY,
Server => SERVER_500_INTERNAL_SERVER_ERROR,
Rejection => CLIENT_400_BAD_REQUEST,
AtCapacity => SERVER_503_SERVICE_UNAVAILABLE,
FiatRatesMissing => SERVER_500_INTERNAL_SERVER_ERROR,
}
}
}
api_error_kind! {
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum LspErrorKind {
Unknown(ErrorCode),
UnknownReqwest = 1,
Building = 2,
Connect = 3,
Timeout = 4,
Decode = 5,
Server = 6,
Rejection = 7,
AtCapacity = 8,
Provision = 100,
Scid = 101,
Command = 102,
NotFound = 103,
}
}
impl ToHttpStatus for LspErrorKind {
fn to_http_status(&self) -> StatusCode {
use LspErrorKind::*;
match self {
Unknown(_) => SERVER_500_INTERNAL_SERVER_ERROR,
UnknownReqwest => CLIENT_400_BAD_REQUEST,
Building => CLIENT_400_BAD_REQUEST,
Connect => SERVER_503_SERVICE_UNAVAILABLE,
Timeout => SERVER_504_GATEWAY_TIMEOUT,
Decode => SERVER_502_BAD_GATEWAY,
Server => SERVER_500_INTERNAL_SERVER_ERROR,
Rejection => CLIENT_400_BAD_REQUEST,
AtCapacity => SERVER_503_SERVICE_UNAVAILABLE,
Provision => SERVER_500_INTERNAL_SERVER_ERROR,
Scid => SERVER_500_INTERNAL_SERVER_ERROR,
Command => SERVER_500_INTERNAL_SERVER_ERROR,
NotFound => CLIENT_404_NOT_FOUND,
}
}
}
api_error_kind! {
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum MegaErrorKind {
Unknown(ErrorCode),
UnknownReqwest = 1,
Building = 2,
Connect = 3,
Timeout = 4,
Decode = 5,
Server = 6,
Rejection = 7,
AtCapacity = 8,
WrongMegaId = 100,
RunnerUnreachable = 101,
UnknownUser = 102,
}
}
impl ToHttpStatus for MegaErrorKind {
fn to_http_status(&self) -> StatusCode {
use MegaErrorKind::*;
match self {
Unknown(_) => SERVER_500_INTERNAL_SERVER_ERROR,
UnknownReqwest => CLIENT_400_BAD_REQUEST,
Building => CLIENT_400_BAD_REQUEST,
Connect => SERVER_503_SERVICE_UNAVAILABLE,
Timeout => SERVER_504_GATEWAY_TIMEOUT,
Decode => SERVER_502_BAD_GATEWAY,
Server => SERVER_500_INTERNAL_SERVER_ERROR,
Rejection => CLIENT_400_BAD_REQUEST,
AtCapacity => SERVER_503_SERVICE_UNAVAILABLE,
WrongMegaId => CLIENT_400_BAD_REQUEST,
RunnerUnreachable => SERVER_503_SERVICE_UNAVAILABLE,
UnknownUser => CLIENT_404_NOT_FOUND,
}
}
}
api_error_kind! {
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum NodeErrorKind {
Unknown(ErrorCode),
UnknownReqwest = 1,
Building = 2,
Connect = 3,
Timeout = 4,
Decode = 5,
Server = 6,
Rejection = 7,
AtCapacity = 8,
WrongUserPk = 100,
WrongNodePk = 101,
WrongMeasurement = 102,
Provision = 103,
BadAuth = 104,
Proxy = 105,
Command = 106,
NotFound = 107,
}
}
impl ToHttpStatus for NodeErrorKind {
fn to_http_status(&self) -> StatusCode {
use NodeErrorKind::*;
match self {
Unknown(_) => SERVER_500_INTERNAL_SERVER_ERROR,
UnknownReqwest => CLIENT_400_BAD_REQUEST,
Building => CLIENT_400_BAD_REQUEST,
Connect => SERVER_503_SERVICE_UNAVAILABLE,
Timeout => SERVER_504_GATEWAY_TIMEOUT,
Decode => SERVER_502_BAD_GATEWAY,
Server => SERVER_500_INTERNAL_SERVER_ERROR,
Rejection => CLIENT_400_BAD_REQUEST,
AtCapacity => SERVER_503_SERVICE_UNAVAILABLE,
WrongUserPk => CLIENT_400_BAD_REQUEST,
WrongNodePk => CLIENT_400_BAD_REQUEST,
WrongMeasurement => CLIENT_400_BAD_REQUEST,
Provision => SERVER_500_INTERNAL_SERVER_ERROR,
BadAuth => CLIENT_401_UNAUTHORIZED,
Proxy => SERVER_502_BAD_GATEWAY,
Command => SERVER_500_INTERNAL_SERVER_ERROR,
NotFound => CLIENT_404_NOT_FOUND,
}
}
}
api_error_kind! {
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum RunnerErrorKind {
Unknown(ErrorCode),
UnknownReqwest = 1,
Building = 2,
Connect = 3,
Timeout = 4,
Decode = 5,
Server = 6,
Rejection = 7,
AtCapacity = 8,
Runner = 100,
UnknownMeasurement = 101,
OldVersion = 102,
TemporarilyUnavailable = 103,
ServiceUnavailable = 104,
Boot = 106,
EvictionFailure = 107,
UnknownUser = 108,
LeaseExpired = 109,
WrongLease = 110,
}
}
impl ToHttpStatus for RunnerErrorKind {
fn to_http_status(&self) -> StatusCode {
use RunnerErrorKind::*;
match self {
Unknown(_) => SERVER_500_INTERNAL_SERVER_ERROR,
UnknownReqwest => CLIENT_400_BAD_REQUEST,
Building => CLIENT_400_BAD_REQUEST,
Connect => SERVER_503_SERVICE_UNAVAILABLE,
Timeout => SERVER_504_GATEWAY_TIMEOUT,
Decode => SERVER_502_BAD_GATEWAY,
Server => SERVER_500_INTERNAL_SERVER_ERROR,
Rejection => CLIENT_400_BAD_REQUEST,
AtCapacity => SERVER_503_SERVICE_UNAVAILABLE,
Runner => SERVER_500_INTERNAL_SERVER_ERROR,
UnknownMeasurement => CLIENT_404_NOT_FOUND,
OldVersion => CLIENT_400_BAD_REQUEST,
TemporarilyUnavailable => CLIENT_409_CONFLICT,
ServiceUnavailable => SERVER_503_SERVICE_UNAVAILABLE,
Boot => SERVER_500_INTERNAL_SERVER_ERROR,
EvictionFailure => SERVER_500_INTERNAL_SERVER_ERROR,
UnknownUser => CLIENT_404_NOT_FOUND,
LeaseExpired => CLIENT_400_BAD_REQUEST,
WrongLease => CLIENT_400_BAD_REQUEST,
}
}
}
api_error_kind! {
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum SdkErrorKind {
Unknown(ErrorCode),
UnknownReqwest = 1,
Building = 2,
Connect = 3,
Timeout = 4,
Decode = 5,
Server = 6,
Rejection = 7,
AtCapacity = 8,
Command = 100,
BadAuth = 101,
NotFound = 102,
}
}
impl ToHttpStatus for SdkErrorKind {
fn to_http_status(&self) -> StatusCode {
use SdkErrorKind::*;
match self {
Unknown(_) => SERVER_500_INTERNAL_SERVER_ERROR,
UnknownReqwest => CLIENT_400_BAD_REQUEST,
Building => CLIENT_400_BAD_REQUEST,
Connect => SERVER_503_SERVICE_UNAVAILABLE,
Timeout => SERVER_504_GATEWAY_TIMEOUT,
Decode => SERVER_502_BAD_GATEWAY,
Server => SERVER_500_INTERNAL_SERVER_ERROR,
Rejection => CLIENT_400_BAD_REQUEST,
AtCapacity => SERVER_503_SERVICE_UNAVAILABLE,
Command => SERVER_500_INTERNAL_SERVER_ERROR,
BadAuth => CLIENT_401_UNAUTHORIZED,
NotFound => CLIENT_404_NOT_FOUND,
}
}
}
impl CommonApiError {
pub fn new(kind: CommonErrorKind, msg: String) -> Self {
Self { kind, msg }
}
#[inline]
pub fn to_code(&self) -> ErrorCode {
self.kind.to_code()
}
#[cfg(feature = "axum")]
fn log_and_status(&self) -> StatusCode {
let status = self.kind.to_http_status();
if status.is_server_error() {
error!("{self}");
} else if status.is_client_error() {
warn!("{self}");
} else {
error!("Unexpected status code {status} for error: {self}");
}
status
}
}
impl fmt::Display for CommonApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let kind = &self.kind;
let msg = &self.msg;
write!(f, "{kind:?}: {msg}")
}
}
impl CommonErrorKind {
#[cfg(any(test, feature = "test-utils"))]
const KINDS: &'static [Self] = &[
Self::UnknownReqwest,
Self::Building,
Self::Connect,
Self::Timeout,
Self::Decode,
Self::Server,
Self::Rejection,
Self::AtCapacity,
];
#[inline]
pub fn to_code(self) -> ErrorCode {
self as ErrorCode
}
}
impl From<serde_json::Error> for CommonApiError {
fn from(err: serde_json::Error) -> Self {
let kind = CommonErrorKind::Decode;
let msg = format!("Failed to deserialize response as json: {err:#}");
Self { kind, msg }
}
}
#[cfg(feature = "reqwest")]
impl From<reqwest::Error> for CommonApiError {
fn from(err: reqwest::Error) -> Self {
let msg = format!("{err:?}");
let kind = if err.is_builder() {
CommonErrorKind::Building
} else if err.is_connect() {
CommonErrorKind::Connect
} else if err.is_timeout() {
CommonErrorKind::Timeout
} else if err.is_decode() {
CommonErrorKind::Decode
} else {
CommonErrorKind::UnknownReqwest
};
Self { kind, msg }
}
}
impl From<CommonApiError> for ErrorResponse {
fn from(CommonApiError { kind, msg }: CommonApiError) -> Self {
let code = kind.to_code();
Self {
code,
msg,
..Default::default()
}
}
}
#[cfg(feature = "axum")]
impl axum::response::IntoResponse for CommonApiError {
fn into_response(self) -> http::Response<axum::body::Body> {
let status = self.log_and_status();
let error_response = ErrorResponse::from(self);
axum_helpers::build_json_response(status, &error_response)
}
}
impl BackendApiError {
pub fn database(error: impl fmt::Display) -> Self {
let kind = BackendErrorKind::Database;
let msg = format!("{error:#}");
Self {
kind,
msg,
..Default::default()
}
}
pub fn not_found(error: impl fmt::Display) -> Self {
let kind = BackendErrorKind::NotFound;
let msg = format!("{error:#}");
Self {
kind,
msg,
..Default::default()
}
}
pub fn duplicate(error: impl fmt::Display) -> Self {
let kind = BackendErrorKind::Duplicate;
let msg = format!("{error:#}");
Self {
kind,
msg,
..Default::default()
}
}
pub fn unauthorized(error: impl fmt::Display) -> Self {
let kind = BackendErrorKind::Unauthorized;
let msg = format!("{error:#}");
Self {
kind,
msg,
..Default::default()
}
}
pub fn unauthenticated(error: impl fmt::Display) -> Self {
let kind = BackendErrorKind::Unauthenticated;
let msg = format!("{error:#}");
Self {
kind,
msg,
..Default::default()
}
}
pub fn invalid_parsed_req(error: impl fmt::Display) -> Self {
let kind = BackendErrorKind::InvalidParsedRequest;
let msg = format!("{error:#}");
Self {
kind,
msg,
..Default::default()
}
}
pub fn not_updatable(error: impl fmt::Display) -> Self {
let kind = BackendErrorKind::NotUpdatable;
let msg = format!("{error:#})");
Self {
kind,
msg,
..Default::default()
}
}
pub fn bcs_serialize(err: bcs::Error) -> Self {
let kind = BackendErrorKind::Building;
let msg = format!("Failed to serialize bcs request: {err:#}");
Self {
kind,
msg,
..Default::default()
}
}
pub fn batch_size_too_large() -> Self {
let kind = BackendErrorKind::BatchSizeOverLimit;
let msg = kind.to_msg().to_owned();
Self {
kind,
msg,
..Default::default()
}
}
pub fn conversion(error: impl fmt::Display) -> Self {
let kind = BackendErrorKind::Conversion;
let msg = format!("{error:#}");
Self {
kind,
msg,
..Default::default()
}
}
}
impl From<auth::Error> for BackendApiError {
fn from(error: auth::Error) -> Self {
let kind = match error {
auth::Error::ClockDrift => BackendErrorKind::AuthExpired,
auth::Error::Expired => BackendErrorKind::AuthExpired,
_ => BackendErrorKind::Unauthenticated,
};
let msg = format!("{error:#}");
Self {
kind,
msg,
..Default::default()
}
}
}
impl GatewayApiError {
pub fn fiat_rates_missing() -> Self {
let kind = GatewayErrorKind::FiatRatesMissing;
let msg = kind.to_string();
Self {
kind,
msg,
..Default::default()
}
}
}
impl LspApiError {
pub fn provision(error: impl fmt::Display) -> Self {
let msg = format!("{error:#}");
let kind = LspErrorKind::Provision;
Self {
kind,
msg,
..Default::default()
}
}
pub fn scid(error: impl fmt::Display) -> Self {
let msg = format!("{error:#}");
let kind = LspErrorKind::Scid;
Self {
kind,
msg,
..Default::default()
}
}
pub fn command(error: impl fmt::Display) -> Self {
let msg = format!("{error:#}");
let kind = LspErrorKind::Command;
Self {
kind,
msg,
..Default::default()
}
}
pub fn rejection(error: impl fmt::Display) -> Self {
let msg = format!("{error:#}");
let kind = LspErrorKind::Rejection;
Self {
kind,
msg,
..Default::default()
}
}
pub fn not_found(error: impl fmt::Display) -> Self {
let msg = format!("{error:#}");
let kind = LspErrorKind::NotFound;
Self {
kind,
msg,
..Default::default()
}
}
}
impl MegaApiError {
pub fn at_capacity(msg: impl Into<String>) -> Self {
let kind = MegaErrorKind::AtCapacity;
let msg = msg.into();
Self {
kind,
msg,
..Default::default()
}
}
pub fn wrong_mega_id(
req_mega_id: &MegaId,
actual_mega_id: &MegaId,
) -> Self {
let kind = MegaErrorKind::WrongMegaId;
let msg = format!("Req: {req_mega_id}, Actual: {actual_mega_id}");
Self {
kind,
msg,
..Default::default()
}
}
pub fn unknown_user(user_pk: &UserPk, msg: impl fmt::Display) -> Self {
Self {
kind: MegaErrorKind::UnknownUser,
msg: format!("{user_pk}: {msg}"),
..Default::default()
}
}
}
impl NodeApiError {
pub fn wrong_user_pk(current_pk: UserPk, given_pk: UserPk) -> Self {
let msg =
format!("Node has UserPk '{current_pk}' but received '{given_pk}'");
let kind = NodeErrorKind::WrongUserPk;
Self {
kind,
msg,
..Default::default()
}
}
pub fn wrong_node_pk(derived_pk: NodePk, given_pk: NodePk) -> Self {
let msg =
format!("Derived NodePk '{derived_pk}' but received '{given_pk}'");
let kind = NodeErrorKind::WrongNodePk;
Self {
kind,
msg,
..Default::default()
}
}
pub fn wrong_measurement(
req_measurement: &Measurement,
actual_measurement: &Measurement,
) -> Self {
let kind = NodeErrorKind::WrongMeasurement;
let msg =
format!("Req: {req_measurement}, Actual: {actual_measurement}");
Self {
kind,
msg,
..Default::default()
}
}
pub fn proxy(error: impl fmt::Display) -> Self {
let msg = format!("{error:#}");
let kind = NodeErrorKind::Proxy;
Self {
kind,
msg,
..Default::default()
}
}
pub fn provision(error: impl fmt::Display) -> Self {
let msg = format!("{error:#}");
let kind = NodeErrorKind::Provision;
Self {
kind,
msg,
..Default::default()
}
}
pub fn command(error: impl fmt::Display) -> Self {
let msg = format!("{error:#}");
let kind = NodeErrorKind::Command;
Self {
kind,
msg,
..Default::default()
}
}
pub fn bad_auth(error: impl fmt::Display) -> Self {
let msg = format!("{error:#}");
let kind = NodeErrorKind::BadAuth;
Self {
kind,
msg,
..Default::default()
}
}
pub fn not_found(error: impl fmt::Display) -> Self {
let msg = format!("{error:#}");
let kind = NodeErrorKind::NotFound;
Self {
kind,
msg,
..Default::default()
}
}
}
impl RunnerApiError {
pub fn at_capacity(error: impl fmt::Display) -> Self {
let kind = RunnerErrorKind::AtCapacity;
let msg = format!("{error:#}");
Self {
kind,
msg,
..Default::default()
}
}
pub fn temporarily_unavailable(error: impl fmt::Display) -> Self {
let kind = RunnerErrorKind::TemporarilyUnavailable;
let msg = format!("{error:#}");
Self {
kind,
msg,
..Default::default()
}
}
pub fn service_unavailable(error: impl fmt::Display) -> Self {
let kind = RunnerErrorKind::ServiceUnavailable;
let msg = format!("{error:#}");
Self {
kind,
msg,
..Default::default()
}
}
pub fn unknown_measurement(measurement: enclave::Measurement) -> Self {
let kind = RunnerErrorKind::UnknownMeasurement;
let msg = format!("{measurement}");
Self {
kind,
msg,
..Default::default()
}
}
pub fn unknown_user(user_pk: &UserPk, msg: impl fmt::Display) -> Self {
Self {
kind: RunnerErrorKind::UnknownUser,
msg: format!("{user_pk}: {msg}"),
..Default::default()
}
}
}
impl SdkApiError {
pub fn command(error: impl fmt::Display) -> Self {
let msg = format!("{error:#}");
let kind = SdkErrorKind::Command;
Self {
kind,
msg,
..Default::default()
}
}
pub fn bad_auth(error: impl fmt::Display) -> Self {
let msg = format!("{error:#}");
let kind = SdkErrorKind::BadAuth;
Self {
kind,
msg,
..Default::default()
}
}
pub fn not_found(error: impl fmt::Display) -> Self {
let msg = format!("{error:#}");
let kind = SdkErrorKind::NotFound;
Self {
kind,
msg,
..Default::default()
}
}
}
pub mod error_response {}
pub fn join_results(results: Vec<anyhow::Result<()>>) -> anyhow::Result<()> {
let errors = results
.into_iter()
.filter_map(|res| match res {
Ok(_) => None,
Err(e) => Some(format!("{e:#}")),
})
.collect::<Vec<String>>();
if errors.is_empty() {
Ok(())
} else {
let joined_errs = errors.join("; ");
Err(anyhow!("{joined_errs}"))
}
}
#[cfg(any(test, feature = "test-utils"))]
pub mod invariants {
use proptest::{
arbitrary::{Arbitrary, any},
prop_assert, prop_assert_eq, proptest,
};
use super::*;
pub fn assert_error_kind_invariants<K>()
where
K: ApiErrorKind + Arbitrary,
{
assert!(K::from_code(0).is_unknown());
assert!(K::default().is_unknown());
for common_kind in CommonErrorKind::KINDS {
let common_code = common_kind.to_code();
let common_status = common_kind.to_http_status();
let api_kind = K::from_code(common_kind.to_code());
let api_code = api_kind.to_code();
let api_status = api_kind.to_http_status();
assert_eq!(common_code, api_code, "Error codes must match");
assert_eq!(common_status, api_status, "HTTP statuses must match");
if api_kind.is_unknown() {
panic!(
"all CommonErrorKind's should be covered; \
missing common code: {common_code}, \
common kind: {common_kind:?}",
);
}
}
for kind in K::KINDS {
let code = kind.to_code();
let kind2 = K::from_code(code);
let code2 = kind2.to_code();
assert_eq!(code, code2);
assert_eq!(kind, &kind2);
}
for code in 0_u16..200 {
let kind = K::from_code(code);
let code2 = kind.to_code();
let kind2 = K::from_code(code2);
assert_eq!(code, code2);
assert_eq!(kind, kind2);
}
proptest!(|(kind in any::<K>())| {
let code = kind.to_code();
let kind2 = K::from_code(code);
let code2 = kind2.to_code();
prop_assert_eq!(code, code2);
prop_assert_eq!(kind, kind2);
});
proptest!(|(kind in any::<K>())| {
prop_assert!(!kind.to_msg().is_empty());
prop_assert!(!kind.to_msg().ends_with('.'));
});
}
pub fn assert_api_error_invariants<E, K>()
where
E: ApiError + Arbitrary + PartialEq,
K: ApiErrorKind + Arbitrary,
{
proptest!(|(e1 in any::<E>())| {
let err_resp1 = Into::<ErrorResponse>::into(e1.clone());
let e2 = E::from(err_resp1.clone());
let err_resp2 = Into::<ErrorResponse>::into(e2.clone());
prop_assert_eq!(e1, e2);
prop_assert_eq!(err_resp1, err_resp2);
});
proptest!(|(
kind in any::<K>(),
main_msg in arbitrary::any_string()
)| {
let code = kind.to_code();
let msg = main_msg.clone();
let data = serde_json::Value::String(String::from("dummy"));
let sensitive = false;
let err_resp = ErrorResponse { code, msg, data, sensitive };
let api_error = E::from(err_resp);
let kind_msg = kind.to_msg();
let actual_display = format!("{api_error}");
let expected_display =
format!("{kind_msg}: {main_msg}");
prop_assert_eq!(actual_display, expected_display);
});
}
}
#[cfg(test)]
mod test {
use lexe_common::test_utils::roundtrip;
use proptest::{prelude::any, prop_assert_eq, proptest};
use super::*;
#[test]
fn client_error_kinds_non_zero() {
for kind in CommonErrorKind::KINDS {
assert_ne!(kind.to_code(), 0);
}
}
#[test]
fn error_kind_invariants() {
invariants::assert_error_kind_invariants::<BackendErrorKind>();
invariants::assert_error_kind_invariants::<GatewayErrorKind>();
invariants::assert_error_kind_invariants::<LspErrorKind>();
invariants::assert_error_kind_invariants::<MegaErrorKind>();
invariants::assert_error_kind_invariants::<NodeErrorKind>();
invariants::assert_error_kind_invariants::<RunnerErrorKind>();
invariants::assert_error_kind_invariants::<SdkErrorKind>();
}
#[test]
fn api_error_invariants() {
use invariants::assert_api_error_invariants;
assert_api_error_invariants::<BackendApiError, BackendErrorKind>();
assert_api_error_invariants::<GatewayApiError, GatewayErrorKind>();
assert_api_error_invariants::<LspApiError, LspErrorKind>();
assert_api_error_invariants::<MegaApiError, MegaErrorKind>();
assert_api_error_invariants::<NodeApiError, NodeErrorKind>();
assert_api_error_invariants::<RunnerApiError, RunnerErrorKind>();
assert_api_error_invariants::<SdkApiError, SdkErrorKind>();
}
#[test]
fn node_lsp_command_error_is_concise() {
let err1 = format!("{:#}", NodeApiError::command("Oops!"));
let err2 = format!("{:#}", LspApiError::command("Oops!"));
assert_eq!(err1, "Error: Oops!");
assert_eq!(err2, "Error: Oops!");
}
#[test]
fn error_response_serde_roundtrip() {
roundtrip::json_value_roundtrip_proptest::<ErrorResponse>();
}
#[test]
fn error_response_compat() {
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[derive(Arbitrary)]
pub struct OldErrorResponse {
pub code: ErrorCode,
#[cfg_attr(test, proptest(strategy = "arbitrary::any_string()"))]
pub msg: String,
}
proptest!(|(old in any::<OldErrorResponse>())| {
let json_str = serde_json::to_string(&old).unwrap();
let new =
serde_json::from_str::<ErrorResponse>(&json_str).unwrap();
prop_assert_eq!(old.code, new.code);
prop_assert_eq!(old.msg, new.msg);
prop_assert_eq!(new.data, serde_json::Value::Null);
prop_assert_eq!(new.sensitive, false);
});
}
}