warg_api/v1/
fetch.rs

1//! Types relating to the fetch API.
2
3use crate::Status;
4use indexmap::IndexMap;
5use serde::{de::Unexpected, Deserialize, Serialize, Serializer};
6use std::borrow::Cow;
7use std::str::FromStr;
8use thiserror::Error;
9use warg_crypto::hash::AnyHash;
10use warg_protocol::{
11    registry::{LogId, PackageName, RegistryLen},
12    PublishedProtoEnvelopeBody,
13};
14
15/// Wraps the PublishedProtoEnvelopeBody with a fetch token.
16#[derive(Debug, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct PublishedRecord {
19    /// Record proto envelope body with RegistryIndex.
20    #[serde(flatten)]
21    pub envelope: PublishedProtoEnvelopeBody,
22    /// Fetch token for fetch pagination.
23    pub fetch_token: String,
24}
25
26/// Represents a fetch logs request.
27#[derive(Debug, Serialize, Deserialize)]
28#[serde(rename_all = "camelCase", deny_unknown_fields)]
29pub struct FetchLogsRequest<'a> {
30    /// The checkpoint log length.
31    pub log_length: RegistryLen,
32    /// The limit for the number of operator and package records to fetch.
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub limit: Option<u16>,
35    /// The last known operator record fetch token.
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub operator: Option<Cow<'a, str>>,
38    /// The map of package identifiers to last known fetch token.
39    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
40    pub packages: Cow<'a, IndexMap<LogId, Option<String>>>,
41}
42
43/// Represents a fetch logs response.
44#[derive(Debug, Serialize, Deserialize)]
45#[serde(rename_all = "camelCase")]
46pub struct FetchLogsResponse {
47    /// Whether there are more records to fetch.
48    #[serde(default)]
49    pub more: bool,
50    /// The operator records appended since the last known operator record.
51    #[serde(default, skip_serializing_if = "Vec::is_empty")]
52    pub operator: Vec<PublishedRecord>,
53    /// The package records appended since last known package record ids.
54    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
55    pub packages: IndexMap<LogId, Vec<PublishedRecord>>,
56    /// An optional list of warnings.
57    #[serde(default, skip_serializing_if = "Vec::is_empty")]
58    pub warnings: Vec<FetchWarning>,
59}
60
61/// Represents a fetch package names request.
62#[derive(Debug, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct FetchPackageNamesRequest<'a> {
65    /// List of package log IDs to request the package name.
66    pub packages: Cow<'a, Vec<LogId>>,
67}
68
69/// Represents a fetch package names response. If the requested number of packages exceeds the limit
70/// that the server can fulfill on a single request, the client should retry with the log IDs that
71/// are absent in the response body.
72#[derive(Serialize, Deserialize)]
73#[serde(rename_all = "camelCase")]
74pub struct FetchPackageNamesResponse {
75    /// The log ID hash mapping to a package name. If `None`, the package name cannot be provided.
76    pub packages: IndexMap<LogId, Option<PackageName>>,
77}
78
79/// A warning message.
80#[derive(Serialize, Deserialize, Debug)]
81pub struct FetchWarning {
82    /// The warning message itself
83    pub message: String,
84}
85
86/// Represents a fetch API error.
87#[non_exhaustive]
88#[derive(Debug, Error)]
89pub enum FetchError {
90    /// The provided checkpoint was not found.
91    #[error("checkpoint log length `{0}` was not found")]
92    CheckpointNotFound(RegistryLen),
93    /// The provided log was not found.
94    #[error("log `{0}` was not found")]
95    LogNotFound(LogId),
96    /// The provided fetch token was not found.
97    #[error("fetch token `{0}` was not found")]
98    FetchTokenNotFound(String),
99    /// An error with a message occurred.
100    #[error("{message}")]
101    Message {
102        /// The HTTP status code.
103        status: u16,
104        /// The error message
105        message: String,
106    },
107}
108
109impl FetchError {
110    /// Returns the HTTP status code of the error.
111    pub fn status(&self) -> u16 {
112        match self {
113            Self::CheckpointNotFound(_) | Self::LogNotFound(_) | Self::FetchTokenNotFound(_) => 404,
114            Self::Message { status, .. } => *status,
115        }
116    }
117}
118
119#[derive(Serialize, Deserialize)]
120#[serde(rename_all = "camelCase")]
121enum EntityType {
122    LogLength,
123    Log,
124    FetchToken,
125}
126
127#[derive(Serialize, Deserialize)]
128#[serde(untagged, rename_all = "camelCase")]
129enum RawError<'a, T>
130where
131    T: Clone + ToOwned,
132    <T as ToOwned>::Owned: Serialize + for<'b> Deserialize<'b>,
133{
134    CheckpointNotFound {
135        status: Status<404>,
136        #[serde(rename = "type")]
137        ty: EntityType,
138        id: RegistryLen,
139    },
140    NotFound {
141        status: Status<404>,
142        #[serde(rename = "type")]
143        ty: EntityType,
144        id: Cow<'a, T>,
145    },
146    Message {
147        status: u16,
148        message: Cow<'a, T>,
149    },
150}
151
152impl Serialize for FetchError {
153    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
154        match self {
155            Self::CheckpointNotFound(log_length) => RawError::CheckpointNotFound::<RegistryLen> {
156                status: Status::<404>,
157                ty: EntityType::LogLength,
158                id: *log_length,
159            }
160            .serialize(serializer),
161            Self::LogNotFound(log_id) => RawError::NotFound {
162                status: Status::<404>,
163                ty: EntityType::Log,
164                id: Cow::Borrowed(log_id),
165            }
166            .serialize(serializer),
167            Self::FetchTokenNotFound(token) => RawError::NotFound {
168                status: Status::<404>,
169                ty: EntityType::FetchToken,
170                id: Cow::Borrowed(token),
171            }
172            .serialize(serializer),
173            Self::Message { status, message } => RawError::Message {
174                status: *status,
175                message: Cow::Borrowed(message),
176            }
177            .serialize(serializer),
178        }
179    }
180}
181
182impl<'de> Deserialize<'de> for FetchError {
183    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
184    where
185        D: serde::Deserializer<'de>,
186    {
187        match RawError::<String>::deserialize(deserializer)? {
188            RawError::CheckpointNotFound { id, .. } => Ok(Self::CheckpointNotFound(id)),
189            RawError::NotFound { status: _, ty, id } => match ty {
190                EntityType::Log => Ok(Self::LogNotFound(
191                    AnyHash::from_str(&id)
192                        .map_err(|_| {
193                            serde::de::Error::invalid_value(Unexpected::Str(&id), &"a valid log id")
194                        })?
195                        .into(),
196                )),
197                EntityType::FetchToken => Ok(Self::FetchTokenNotFound(id.into_owned())),
198                _ => Err(serde::de::Error::invalid_value(
199                    Unexpected::Str(&id),
200                    &"a valid log length",
201                )),
202            },
203            RawError::Message { status, message } => Ok(Self::Message {
204                status,
205                message: message.into_owned(),
206            }),
207        }
208    }
209}