1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
//! Types relating to the package API.

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,
};

/// Represents the supported kinds of content upload endpoints.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum UploadEndpoint {
    /// Content may be uploaded via HTTP request to the given URL.
    #[serde(rename_all = "camelCase")]
    Http {
        /// The http method for the upload request.
        /// Only `POST` and `PUT` methods are supported.
        method: String,
        /// The URL to POST content to.
        url: String,
        /// Optional header names and values for the upload request.
        /// Only `authorization` and `content-type` headers are valid; any other header should be rejected.
        #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
        headers: IndexMap<String, String>,
    },
}

/// Information about missing content.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MissingContent {
    /// Upload endpoint(s) that may be used to provide missing content.
    pub upload: Vec<UploadEndpoint>,
}

/// Represents a request to publish a record to a package log.
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PublishRecordRequest<'a> {
    /// The package name being published.
    pub package_name: Cow<'a, PackageName>,
    /// The publish record to add to the package log.
    pub record: Cow<'a, ProtoEnvelopeBody>,
    /// The complete set of content sources for the record.
    ///
    /// A registry may not support specifying content sources directly.
    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
    pub content_sources: IndexMap<AnyHash, Vec<ContentSource>>,
}

/// Represents a package record API entity in a registry.
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackageRecord {
    /// The identifier of the package record.
    pub record_id: RecordId,
    /// The current state of the package.
    #[serde(flatten)]
    pub state: PackageRecordState,
}

impl PackageRecord {
    /// Gets the missing content of the record.
    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()),
        }
    }
}

/// Represents a package record in one of the following states:
/// * `sourcing` - The record is sourcing content.
/// * `processing` - The record is being processed.
/// * `rejected` - The record was rejected.
/// * `published` - The record was published to the log.
#[derive(Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "camelCase")]
#[allow(clippy::large_enum_variant)]
pub enum PackageRecordState {
    /// The package record needs content sources.
    #[serde(rename_all = "camelCase")]
    Sourcing {
        /// The digests of the missing content.
        missing_content: IndexMap<AnyHash, MissingContent>,
    },
    /// The package record is processing.
    #[serde(rename_all = "camelCase")]
    Processing,
    /// The package record is rejected.
    #[serde(rename_all = "camelCase")]
    Rejected {
        /// The reason the record was rejected.
        reason: String,
    },
    /// The package record was successfully published to the log.
    #[serde(rename_all = "camelCase")]
    Published {
        /// The published index of the record in the registry log.
        registry_index: RegistryIndex,
    },
}

/// Represents a package API error.
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum PackageError {
    /// The provided log was not found.
    #[error("log `{0}` was not found")]
    LogNotFound(LogId),
    /// The provided record was not found.
    #[error("record `{0}` was not found")]
    RecordNotFound(RecordId),
    /// The record is not currently sourcing content.
    #[error("the record is not currently sourcing content")]
    RecordNotSourcing,
    /// The provided package's namespace was not found in the operator log.
    #[error("namespace `{0}` is not defined on the registry")]
    NamespaceNotDefined(String),
    /// The provided package's namespace is imported from another registry.
    #[error("namespace `{0}` is an imported namespace from another registry")]
    NamespaceImported(String),
    /// The operation was not authorized by the registry.
    #[error("unauthorized operation: {0}")]
    Unauthorized(String),
    /// The operation was not supported by the registry.
    #[error("the requested operation is not supported: {0}")]
    NotSupported(String),
    /// The package was rejected by the registry, due to a conflict with a pending publish.
    #[error("the package conflicts with pending publish of record `{0}`")]
    ConflictPendingPublish(RecordId),
    /// The package was rejected by the registry.
    #[error("the package was rejected by the registry: {0}")]
    Rejection(String),
    /// An error with a message occurred.
    #[error("{message}")]
    Message {
        /// The HTTP status code.
        status: u16,
        /// The error message
        message: String,
    },
}

impl PackageError {
    /// Returns the HTTP status code of the error.
    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(),
            }),
        }
    }
}