use http::StatusCode;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sos_sync::{MaybeConflict, SyncStatus};
use std::error::Error as StdError;
use thiserror::Error;
pub trait AsConflict {
fn is_conflict(&self) -> bool;
fn is_hard_conflict(&self) -> bool;
fn take_conflict(self) -> Option<ConflictError>;
}
#[derive(Debug, Error)]
pub enum Error {
#[error("relay packet end of file")]
EndOfFile,
#[error(transparent)]
Conflict(#[from] ConflictError),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
TryFromSlice(#[from] std::array::TryFromSliceError),
#[error(transparent)]
ProtoBufEncode(#[from] prost::EncodeError),
#[error(transparent)]
ProtoBufDecode(#[from] prost::DecodeError),
#[error(transparent)]
ProtoEnum(#[from] prost::UnknownEnumValue),
#[error(transparent)]
Core(#[from] sos_core::Error),
#[error(transparent)]
Backend(#[from] sos_backend::Error),
#[error(transparent)]
BackendStorage(#[from] sos_backend::StorageError),
#[error(transparent)]
Signer(#[from] sos_signer::Error),
#[error(transparent)]
Sync(#[from] sos_sync::Error),
#[error(transparent)]
Merkle(#[from] rs_merkle::Error),
#[error(transparent)]
Time(#[from] time::error::ComponentRange),
#[error(transparent)]
UrlParse(#[from] url::ParseError),
#[error(transparent)]
Http(#[from] http::Error),
#[error(transparent)]
StatusCode(#[from] http::status::InvalidStatusCode),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(transparent)]
Network(#[from] NetworkError),
#[error(transparent)]
Join(#[from] tokio::task::JoinError),
#[cfg(feature = "network-client")]
#[error(transparent)]
ToStr(#[from] reqwest::header::ToStrError),
#[cfg(feature = "network-client")]
#[error(transparent)]
Request(#[from] reqwest::Error),
#[cfg(feature = "network-client")]
#[error(transparent)]
Base58Decode(#[from] bs58::decode::Error),
#[cfg(feature = "network-client")]
#[error("file download checksum mismatch; expected '{0}' but got '{1}'")]
FileChecksumMismatch(String, String),
#[cfg(feature = "network-client")]
#[error("file transfer canceled")]
TransferCanceled(crate::transfer::CancelReason),
#[cfg(feature = "network-client")]
#[error("retry overflow")]
RetryOverflow,
#[cfg(feature = "network-client")]
#[error("network retry was canceled")]
RetryCanceled(crate::transfer::CancelReason),
#[cfg(feature = "listen")]
#[error("not binary message type on websocket")]
NotBinaryWebsocketMessageType,
#[cfg(feature = "listen")]
#[error(transparent)]
WebSocket(#[from] tokio_tungstenite::tungstenite::Error),
}
#[cfg(feature = "network-client")]
impl Error {
pub fn cancellation_reason(
&self,
) -> Option<&crate::transfer::CancelReason> {
let source = source_error(self);
if let Some(err) = source.downcast_ref::<Error>() {
if let Error::TransferCanceled(reason) = err {
Some(reason)
} else {
None
}
} else {
None
}
}
}
pub(crate) fn source_error<'a>(
error: &'a (dyn StdError + 'static),
) -> &'a (dyn StdError + 'static) {
let mut source = error;
while let Some(next_source) = source.source() {
source = next_source;
}
source
}
#[derive(Debug, Error)]
pub enum NetworkError {
#[error("unexpected response status code {0}")]
ResponseCode(StatusCode),
#[error("unexpected response {1} (code: {0})")]
ResponseJson(StatusCode, Value),
#[error("unexpected content type {0}, expected: {1}")]
ContentType(String, String),
}
#[derive(Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ErrorReply {
code: u16,
#[serde(skip_serializing_if = "Option::is_none")]
value: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
message: Option<String>,
}
impl ErrorReply {
pub fn new_message(
status: StatusCode,
message: impl std::fmt::Display,
) -> Self {
Self {
code: status.into(),
message: Some(message.to_string()),
..Default::default()
}
}
}
impl From<NetworkError> for ErrorReply {
fn from(value: NetworkError) -> Self {
match value {
NetworkError::ResponseCode(status) => ErrorReply {
code: status.into(),
..Default::default()
},
NetworkError::ResponseJson(status, value) => ErrorReply {
code: status.into(),
value: Some(value),
..Default::default()
},
NetworkError::ContentType(_, _) => ErrorReply {
code: StatusCode::BAD_REQUEST.into(),
..Default::default()
},
}
}
}
#[derive(Debug, Error)]
pub enum ConflictError {
#[error("soft conflict")]
Soft {
conflict: MaybeConflict,
local: SyncStatus,
remote: SyncStatus,
},
#[error("hard conflict")]
Hard,
}
impl AsConflict for Error {
fn is_conflict(&self) -> bool {
matches!(self, Error::Conflict(_))
}
fn is_hard_conflict(&self) -> bool {
matches!(self, Error::Conflict(ConflictError::Hard))
}
fn take_conflict(self) -> Option<ConflictError> {
match self {
Self::Conflict(err) => Some(err),
_ => None,
}
}
}