pub use super::ContentSource;
use crate::Status;
use indexmap::IndexMap;
use serde::{de::Unexpected, Deserialize, Serialize, Serializer};
use std::borrow::Cow;
use std::str::FromStr;
use thiserror::Error;
use warg_crypto::hash::AnyHash;
use warg_protocol::{
registry::{LogId, PackageName, RecordId, RegistryIndex},
ProtoEnvelopeBody,
};
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum UploadEndpoint {
#[serde(rename_all = "camelCase")]
Http {
method: String,
url: String,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
headers: IndexMap<String, String>,
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MissingContent {
pub upload: Vec<UploadEndpoint>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PublishRecordRequest<'a> {
pub package_name: Cow<'a, PackageName>,
pub record: Cow<'a, ProtoEnvelopeBody>,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub content_sources: IndexMap<AnyHash, Vec<ContentSource>>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackageRecord {
pub record_id: RecordId,
#[serde(flatten)]
pub state: PackageRecordState,
}
impl PackageRecord {
pub fn missing_content(&self) -> impl Iterator<Item = (&AnyHash, &MissingContent)> {
match &self.state {
PackageRecordState::Sourcing {
missing_content, ..
} => itertools::Either::Left(missing_content.iter()),
_ => itertools::Either::Right(std::iter::empty()),
}
}
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "camelCase")]
#[allow(clippy::large_enum_variant)]
pub enum PackageRecordState {
#[serde(rename_all = "camelCase")]
Sourcing {
missing_content: IndexMap<AnyHash, MissingContent>,
},
#[serde(rename_all = "camelCase")]
Processing,
#[serde(rename_all = "camelCase")]
Rejected {
reason: String,
},
#[serde(rename_all = "camelCase")]
Published {
registry_index: RegistryIndex,
},
}
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum PackageError {
#[error("log `{0}` was not found")]
LogNotFound(LogId),
#[error("record `{0}` was not found")]
RecordNotFound(RecordId),
#[error("the record is not currently sourcing content")]
RecordNotSourcing,
#[error("namespace `{0}` is not defined on the registry")]
NamespaceNotDefined(String),
#[error("namespace `{0}` is an imported namespace from another registry")]
NamespaceImported(String),
#[error("unauthorized operation: {0}")]
Unauthorized(String),
#[error("the requested operation is not supported: {0}")]
NotSupported(String),
#[error("the package conflicts with pending publish of record `{0}`")]
ConflictPendingPublish(RecordId),
#[error("the package was rejected by the registry: {0}")]
Rejection(String),
#[error("{message}")]
Message {
status: u16,
message: String,
},
}
impl PackageError {
pub fn status(&self) -> u16 {
match self {
Self::Unauthorized { .. } => 401,
Self::LogNotFound(_) | Self::RecordNotFound(_) | Self::NamespaceNotDefined(_) => 404,
Self::NamespaceImported(_) | Self::ConflictPendingPublish(_) => 409,
Self::RecordNotSourcing => 405,
Self::Rejection(_) => 422,
Self::NotSupported(_) => 501,
Self::Message { status, .. } => *status,
}
}
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
enum EntityType {
Log,
Record,
Namespace,
NamespaceImport,
Name,
}
#[derive(Serialize, Deserialize)]
#[serde(untagged, rename_all = "camelCase")]
enum RawError<'a, T>
where
T: Clone + ToOwned,
<T as ToOwned>::Owned: Serialize + for<'b> Deserialize<'b>,
{
Unauthorized {
status: Status<401>,
message: Cow<'a, str>,
},
NotFound {
status: Status<404>,
#[serde(rename = "type")]
ty: EntityType,
id: Cow<'a, T>,
},
Conflict {
status: Status<409>,
#[serde(rename = "type")]
ty: EntityType,
id: Cow<'a, T>,
},
RecordNotSourcing {
status: Status<405>,
},
Rejection {
status: Status<422>,
message: Cow<'a, str>,
},
NotSupported {
status: Status<501>,
message: Cow<'a, str>,
},
Message {
status: u16,
message: Cow<'a, str>,
},
}
impl Serialize for PackageError {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::Unauthorized(message) => RawError::Unauthorized::<()> {
status: Status::<401>,
message: Cow::Borrowed(message),
}
.serialize(serializer),
Self::LogNotFound(log_id) => RawError::NotFound {
status: Status::<404>,
ty: EntityType::Log,
id: Cow::Borrowed(log_id),
}
.serialize(serializer),
Self::RecordNotFound(record_id) => RawError::NotFound {
status: Status::<404>,
ty: EntityType::Record,
id: Cow::Borrowed(record_id),
}
.serialize(serializer),
Self::NamespaceNotDefined(namespace) => RawError::NotFound {
status: Status::<404>,
ty: EntityType::Namespace,
id: Cow::Borrowed(namespace),
}
.serialize(serializer),
Self::NamespaceImported(namespace) => RawError::Conflict {
status: Status::<409>,
ty: EntityType::NamespaceImport,
id: Cow::Borrowed(namespace),
}
.serialize(serializer),
Self::ConflictPendingPublish(record_id) => RawError::Conflict {
status: Status::<409>,
ty: EntityType::Record,
id: Cow::Borrowed(record_id),
}
.serialize(serializer),
Self::RecordNotSourcing => RawError::RecordNotSourcing::<()> {
status: Status::<405>,
}
.serialize(serializer),
Self::Rejection(message) => RawError::Rejection::<()> {
status: Status::<422>,
message: Cow::Borrowed(message),
}
.serialize(serializer),
Self::NotSupported(message) => RawError::NotSupported::<()> {
status: Status::<501>,
message: Cow::Borrowed(message),
}
.serialize(serializer),
Self::Message { status, message } => RawError::Message::<()> {
status: *status,
message: Cow::Borrowed(message),
}
.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for PackageError {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
match RawError::<String>::deserialize(deserializer)? {
RawError::Unauthorized { status: _, message } => {
Ok(Self::Unauthorized(message.into_owned()))
}
RawError::NotFound { status: _, ty, id } => match ty {
EntityType::Log => Ok(Self::LogNotFound(
AnyHash::from_str(&id)
.map_err(|_| {
serde::de::Error::invalid_value(Unexpected::Str(&id), &"a valid log id")
})?
.into(),
)),
EntityType::Record => Ok(Self::RecordNotFound(
AnyHash::from_str(&id)
.map_err(|_| {
serde::de::Error::invalid_value(
Unexpected::Str(&id),
&"a valid record id",
)
})?
.into(),
)),
EntityType::Namespace => Ok(Self::NamespaceNotDefined(id.into_owned())),
_ => Err(serde::de::Error::invalid_value(
Unexpected::Enum,
&"a valid entity type",
)),
},
RawError::Conflict { status: _, ty, id } => match ty {
EntityType::NamespaceImport => Ok(Self::NamespaceImported(id.into_owned())),
EntityType::Record => Ok(Self::ConflictPendingPublish(
AnyHash::from_str(&id)
.map_err(|_| {
serde::de::Error::invalid_value(
Unexpected::Str(&id),
&"a valid record id",
)
})?
.into(),
)),
_ => Err(serde::de::Error::invalid_value(
Unexpected::Enum,
&"a valid entity type",
)),
},
RawError::RecordNotSourcing { status: _ } => Ok(Self::RecordNotSourcing),
RawError::Rejection { status: _, message } => Ok(Self::Rejection(message.into_owned())),
RawError::NotSupported { status: _, message } => {
Ok(Self::NotSupported(message.into_owned()))
}
RawError::Message { status, message } => Ok(Self::Message {
status,
message: message.into_owned(),
}),
}
}
}