use http::StatusCode;
use knot_types::{AtUri, RecordCid};
use serde::{Deserialize, Serialize, Serializer};
use serde_json::json;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone)]
pub struct WriteError {
status: StatusCode,
error: &'static str,
message: String,
private: bool,
stale: Option<(AtUri<String>, RecordCid)>,
}
impl WriteError {
fn new(status: StatusCode, error: &'static str, message: impl Into<String>) -> Self {
Self {
status,
error,
message: message.into(),
private: false,
stale: None,
}
}
pub fn auth_required(message: impl Into<String>) -> Self {
Self::new(StatusCode::UNAUTHORIZED, "AuthenticationRequired", message)
}
pub fn upstream_unavailable(message: impl Into<String>) -> Self {
Self::new(
StatusCode::SERVICE_UNAVAILABLE,
"UpstreamUnavailable",
message,
)
}
pub fn unsupported(
status: StatusCode,
error: &'static str,
message: impl Into<String>,
) -> Self {
Self::new(status, error, message)
}
pub fn invalid_request(message: impl Into<String>) -> Self {
Self::new(StatusCode::BAD_REQUEST, "InvalidRequest", message)
}
pub fn forbidden(message: impl Into<String>) -> Self {
Self::new(StatusCode::FORBIDDEN, "Forbidden", message)
}
pub fn not_found(message: impl Into<String>) -> Self {
Self::new(StatusCode::NOT_FOUND, "NotFound", message)
}
pub fn conflict(message: impl Into<String>) -> Self {
Self::new(StatusCode::CONFLICT, "Conflict", message)
}
pub fn request_too_large(message: impl Into<String>) -> Self {
Self::new(StatusCode::PAYLOAD_TOO_LARGE, "RequestTooLarge", message)
}
pub fn warming(message: impl Into<String>) -> Self {
Self::new(
StatusCode::SERVICE_UNAVAILABLE,
"ProjectionWarming",
message,
)
}
pub fn stale(uri: AtUri<String>, cid: RecordCid, message: impl Into<String>) -> Self {
Self {
stale: Some((uri, cid)),
..Self::new(StatusCode::CONFLICT, "InvalidSwap", message)
}
}
pub fn bad_gateway(message: impl Into<String>) -> Self {
Self::new(StatusCode::BAD_GATEWAY, "UpstreamFailure", message)
}
/// the message names paths and git's stderr, so it goes to the log and never over http.
pub fn internal(message: impl Into<String>) -> Self {
Self {
private: true,
..Self::internal_notice(message)
}
}
pub fn internal_notice(message: impl Into<String>) -> Self {
Self::new(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", message)
}
pub fn status(&self) -> StatusCode {
self.status
}
pub fn body(&self) -> serde_json::Value {
match &self.stale {
Some((uri, cid)) => json!({
"error": self.error,
"message": self.message,
"uri": uri.as_str(),
"cid": cid.to_string(),
}),
None => json!({ "error": self.error, "message": self.message }),
}
}
pub fn response_body(&self) -> serde_json::Value {
if self.private {
return json!({ "error": self.error, "message": "Internal Server Error" });
}
self.body()
}
}
impl fmt::Display for WriteError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.error, self.message)
}
}
impl std::error::Error for WriteError {}
impl From<knot_git::GitError> for WriteError {
fn from(error: knot_git::GitError) -> Self {
use knot_git::GitError;
let message = error.to_string();
match error {
GitError::AlreadyExists(_) | GitError::AtomicRefs(_) => Self::conflict(message),
GitError::UnsafeRepoDid(_) | GitError::ReservedDid(_) | GitError::DepthExceeded(_) => {
Self::invalid_request(message)
}
_ => Self::internal(message),
}
}
}
impl From<knot_cob::CobError> for WriteError {
fn from(error: knot_cob::CobError) -> Self {
use knot_cob::CobError;
let message = error.to_string();
match error {
CobError::Contended(_) | CobError::StaleTip { .. } => Self::conflict(message),
CobError::StaleRecord { uri, current, .. } => Self::stale(uri, current, message),
CobError::Impersonation { .. }
| CobError::CreateRejected { .. }
| CobError::AppendRejected { .. } => Self::forbidden(message),
CobError::ChangeTooLarge { .. } => Self::request_too_large(message),
_ => Self::internal(message),
}
}
}
impl From<knot_record::RecordError> for WriteError {
fn from(error: knot_record::RecordError) -> Self {
Self::internal(error.to_string())
}
}
impl From<knot_types::RecordBodyError> for WriteError {
fn from(error: knot_types::RecordBodyError) -> Self {
match error {
knot_types::RecordBodyError::Oversized(_) => Self::request_too_large(error.to_string()),
other => Self::internal(other.to_string()),
}
}
}
impl From<knot_index::IndexError> for WriteError {
fn from(error: knot_index::IndexError) -> Self {
use knot_index::IndexError;
match error {
IndexError::Git(git) => git.into(),
IndexError::Cob(cob) => cob.into(),
other => Self::internal(other.to_string()),
}
}
}
impl From<knot_secrets::SecretsError> for WriteError {
fn from(error: knot_secrets::SecretsError) -> Self {
Self::internal(error.to_string())
}
}
impl From<knot_cobs::RegistryError> for WriteError {
fn from(error: knot_cobs::RegistryError) -> Self {
use knot_cobs::RegistryError;
match error {
RegistryError::AlreadyRegistered { .. }
| RegistryError::RepoMismatch { .. }
| RegistryError::RkeyTaken { .. }
| RegistryError::OwnerMoved { .. } => Self::conflict(error.to_string()),
RegistryError::NotRegistered { .. } | RegistryError::NotHosted { .. } => {
Self::not_found(error.to_string())
}
RegistryError::Cob(cob) => cob.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct CidString(cid::Cid);
impl CidString {
pub(super) fn record(self) -> Option<RecordCid> {
RecordCid::new(self.0)
}
}
impl From<RecordCid> for CidString {
fn from(cid: RecordCid) -> Self {
Self(cid.cid())
}
}
impl Serialize for CidString {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.0.to_string())
}
}
impl<'de> Deserialize<'de> for CidString {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = String::deserialize(deserializer)?;
cid::Cid::from_str(&raw)
.map(Self)
.map_err(serde::de::Error::custom)
}
}