warg_api/v1/
package.rs

1//! Types relating to the package API.
2
3pub use super::ContentSource;
4use crate::Status;
5use indexmap::IndexMap;
6use serde::{de::Unexpected, Deserialize, Serialize, Serializer};
7use std::borrow::Cow;
8use std::str::FromStr;
9use thiserror::Error;
10use warg_crypto::hash::AnyHash;
11use warg_protocol::{
12    registry::{LogId, PackageName, RecordId, RegistryIndex},
13    ProtoEnvelopeBody,
14};
15
16/// Represents the supported kinds of content upload endpoints.
17#[derive(Clone, Debug, Serialize, Deserialize)]
18#[serde(tag = "type", rename_all = "camelCase")]
19pub enum UploadEndpoint {
20    /// Content may be uploaded via HTTP request to the given URL.
21    #[serde(rename_all = "camelCase")]
22    Http {
23        /// The http method for the upload request.
24        /// Only `POST` and `PUT` methods are supported.
25        method: String,
26        /// The URL to POST content to.
27        url: String,
28        /// Optional header names and values for the upload request.
29        /// Only `authorization` and `content-type` headers are valid; any other header should be rejected.
30        #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
31        headers: IndexMap<String, String>,
32    },
33}
34
35/// Information about missing content.
36#[derive(Clone, Debug, Serialize, Deserialize)]
37pub struct MissingContent {
38    /// Upload endpoint(s) that may be used to provide missing content.
39    pub upload: Vec<UploadEndpoint>,
40}
41
42/// Represents a request to publish a record to a package log.
43#[derive(Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct PublishRecordRequest<'a> {
46    /// The package name being published.
47    pub package_name: Cow<'a, PackageName>,
48    /// The publish record to add to the package log.
49    pub record: Cow<'a, ProtoEnvelopeBody>,
50    /// The complete set of content sources for the record.
51    ///
52    /// A registry may not support specifying content sources directly.
53    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
54    pub content_sources: IndexMap<AnyHash, Vec<ContentSource>>,
55}
56
57/// Represents a package record API entity in a registry.
58#[derive(Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct PackageRecord {
61    /// The identifier of the package record.
62    pub record_id: RecordId,
63    /// The current state of the package.
64    #[serde(flatten)]
65    pub state: PackageRecordState,
66}
67
68impl PackageRecord {
69    /// Gets the missing content of the record.
70    pub fn missing_content(&self) -> impl Iterator<Item = (&AnyHash, &MissingContent)> {
71        match &self.state {
72            PackageRecordState::Sourcing {
73                missing_content, ..
74            } => itertools::Either::Left(missing_content.iter()),
75            _ => itertools::Either::Right(std::iter::empty()),
76        }
77    }
78}
79
80/// Represents a package record in one of the following states:
81/// * `sourcing` - The record is sourcing content.
82/// * `processing` - The record is being processed.
83/// * `rejected` - The record was rejected.
84/// * `published` - The record was published to the log.
85#[derive(Serialize, Deserialize)]
86#[serde(tag = "state", rename_all = "camelCase")]
87#[allow(clippy::large_enum_variant)]
88pub enum PackageRecordState {
89    /// The package record needs content sources.
90    #[serde(rename_all = "camelCase")]
91    Sourcing {
92        /// The digests of the missing content.
93        missing_content: IndexMap<AnyHash, MissingContent>,
94    },
95    /// The package record is processing.
96    #[serde(rename_all = "camelCase")]
97    Processing,
98    /// The package record is rejected.
99    #[serde(rename_all = "camelCase")]
100    Rejected {
101        /// The reason the record was rejected.
102        reason: String,
103    },
104    /// The package record was successfully published to the log.
105    #[serde(rename_all = "camelCase")]
106    Published {
107        /// The published index of the record in the registry log.
108        registry_index: RegistryIndex,
109    },
110}
111
112/// Represents a package API error.
113#[non_exhaustive]
114#[derive(Debug, Error)]
115pub enum PackageError {
116    /// The provided log was not found.
117    #[error("log `{0}` was not found")]
118    LogNotFound(LogId),
119    /// The provided record was not found.
120    #[error("record `{0}` was not found")]
121    RecordNotFound(RecordId),
122    /// The record is not currently sourcing content.
123    #[error("the record is not currently sourcing content")]
124    RecordNotSourcing,
125    /// The provided package's namespace was not found in the operator log.
126    #[error("namespace `{0}` is not defined on the registry")]
127    NamespaceNotDefined(String),
128    /// The provided package's namespace is imported from another registry.
129    #[error("namespace `{0}` is an imported namespace from another registry")]
130    NamespaceImported(String),
131    /// The operation was not authorized by the registry.
132    #[error("unauthorized operation: {0}")]
133    Unauthorized(String),
134    /// The operation was not supported by the registry.
135    #[error("the requested operation is not supported: {0}")]
136    NotSupported(String),
137    /// The package was rejected by the registry, due to a conflict with a pending publish.
138    #[error("the package conflicts with pending publish of record `{0}`")]
139    ConflictPendingPublish(RecordId),
140    /// The package was rejected by the registry.
141    #[error("the package was rejected by the registry: {0}")]
142    Rejection(String),
143    /// An error with a message occurred.
144    #[error("{message}")]
145    Message {
146        /// The HTTP status code.
147        status: u16,
148        /// The error message
149        message: String,
150    },
151}
152
153impl PackageError {
154    /// Returns the HTTP status code of the error.
155    pub fn status(&self) -> u16 {
156        match self {
157            Self::Unauthorized { .. } => 401,
158            Self::LogNotFound(_) | Self::RecordNotFound(_) | Self::NamespaceNotDefined(_) => 404,
159            Self::NamespaceImported(_) | Self::ConflictPendingPublish(_) => 409,
160            Self::RecordNotSourcing => 405,
161            Self::Rejection(_) => 422,
162            Self::NotSupported(_) => 501,
163            Self::Message { status, .. } => *status,
164        }
165    }
166}
167
168#[derive(Serialize, Deserialize)]
169#[serde(rename_all = "camelCase")]
170enum EntityType {
171    Log,
172    Record,
173    Namespace,
174    NamespaceImport,
175    Name,
176}
177
178#[derive(Serialize, Deserialize)]
179#[serde(untagged, rename_all = "camelCase")]
180enum RawError<'a, T>
181where
182    T: Clone + ToOwned,
183    <T as ToOwned>::Owned: Serialize + for<'b> Deserialize<'b>,
184{
185    Unauthorized {
186        status: Status<401>,
187        message: Cow<'a, str>,
188    },
189    NotFound {
190        status: Status<404>,
191        #[serde(rename = "type")]
192        ty: EntityType,
193        id: Cow<'a, T>,
194    },
195    Conflict {
196        status: Status<409>,
197        #[serde(rename = "type")]
198        ty: EntityType,
199        id: Cow<'a, T>,
200    },
201    RecordNotSourcing {
202        status: Status<405>,
203    },
204    Rejection {
205        status: Status<422>,
206        message: Cow<'a, str>,
207    },
208    NotSupported {
209        status: Status<501>,
210        message: Cow<'a, str>,
211    },
212    Message {
213        status: u16,
214        message: Cow<'a, str>,
215    },
216}
217
218impl Serialize for PackageError {
219    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
220        match self {
221            Self::Unauthorized(message) => RawError::Unauthorized::<()> {
222                status: Status::<401>,
223                message: Cow::Borrowed(message),
224            }
225            .serialize(serializer),
226            Self::LogNotFound(log_id) => RawError::NotFound {
227                status: Status::<404>,
228                ty: EntityType::Log,
229                id: Cow::Borrowed(log_id),
230            }
231            .serialize(serializer),
232            Self::RecordNotFound(record_id) => RawError::NotFound {
233                status: Status::<404>,
234                ty: EntityType::Record,
235                id: Cow::Borrowed(record_id),
236            }
237            .serialize(serializer),
238            Self::NamespaceNotDefined(namespace) => RawError::NotFound {
239                status: Status::<404>,
240                ty: EntityType::Namespace,
241                id: Cow::Borrowed(namespace),
242            }
243            .serialize(serializer),
244            Self::NamespaceImported(namespace) => RawError::Conflict {
245                status: Status::<409>,
246                ty: EntityType::NamespaceImport,
247                id: Cow::Borrowed(namespace),
248            }
249            .serialize(serializer),
250            Self::ConflictPendingPublish(record_id) => RawError::Conflict {
251                status: Status::<409>,
252                ty: EntityType::Record,
253                id: Cow::Borrowed(record_id),
254            }
255            .serialize(serializer),
256            Self::RecordNotSourcing => RawError::RecordNotSourcing::<()> {
257                status: Status::<405>,
258            }
259            .serialize(serializer),
260            Self::Rejection(message) => RawError::Rejection::<()> {
261                status: Status::<422>,
262                message: Cow::Borrowed(message),
263            }
264            .serialize(serializer),
265            Self::NotSupported(message) => RawError::NotSupported::<()> {
266                status: Status::<501>,
267                message: Cow::Borrowed(message),
268            }
269            .serialize(serializer),
270            Self::Message { status, message } => RawError::Message::<()> {
271                status: *status,
272                message: Cow::Borrowed(message),
273            }
274            .serialize(serializer),
275        }
276    }
277}
278
279impl<'de> Deserialize<'de> for PackageError {
280    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
281    where
282        D: serde::Deserializer<'de>,
283    {
284        match RawError::<String>::deserialize(deserializer)? {
285            RawError::Unauthorized { status: _, message } => {
286                Ok(Self::Unauthorized(message.into_owned()))
287            }
288            RawError::NotFound { status: _, ty, id } => match ty {
289                EntityType::Log => Ok(Self::LogNotFound(
290                    AnyHash::from_str(&id)
291                        .map_err(|_| {
292                            serde::de::Error::invalid_value(Unexpected::Str(&id), &"a valid log id")
293                        })?
294                        .into(),
295                )),
296                EntityType::Record => Ok(Self::RecordNotFound(
297                    AnyHash::from_str(&id)
298                        .map_err(|_| {
299                            serde::de::Error::invalid_value(
300                                Unexpected::Str(&id),
301                                &"a valid record id",
302                            )
303                        })?
304                        .into(),
305                )),
306                EntityType::Namespace => Ok(Self::NamespaceNotDefined(id.into_owned())),
307                _ => Err(serde::de::Error::invalid_value(
308                    Unexpected::Enum,
309                    &"a valid entity type",
310                )),
311            },
312            RawError::Conflict { status: _, ty, id } => match ty {
313                EntityType::NamespaceImport => Ok(Self::NamespaceImported(id.into_owned())),
314                EntityType::Record => Ok(Self::ConflictPendingPublish(
315                    AnyHash::from_str(&id)
316                        .map_err(|_| {
317                            serde::de::Error::invalid_value(
318                                Unexpected::Str(&id),
319                                &"a valid record id",
320                            )
321                        })?
322                        .into(),
323                )),
324                _ => Err(serde::de::Error::invalid_value(
325                    Unexpected::Enum,
326                    &"a valid entity type",
327                )),
328            },
329            RawError::RecordNotSourcing { status: _ } => Ok(Self::RecordNotSourcing),
330            RawError::Rejection { status: _, message } => Ok(Self::Rejection(message.into_owned())),
331            RawError::NotSupported { status: _, message } => {
332                Ok(Self::NotSupported(message.into_owned()))
333            }
334            RawError::Message { status, message } => Ok(Self::Message {
335                status,
336                message: message.into_owned(),
337            }),
338        }
339    }
340}