Skip to main content

s2_common/types/
resources.rs

1use std::{fmt::Debug, num::NonZeroUsize, ops::Deref, str::FromStr};
2
3use compact_str::{CompactString, ToCompactString};
4
5#[derive(Debug, Default, Clone, PartialEq, Eq)]
6pub struct Page<T> {
7    pub values: Vec<T>,
8    pub has_more: bool,
9}
10
11impl<T> Page<T> {
12    pub fn new_empty() -> Self {
13        Self {
14            values: Vec::new(),
15            has_more: false,
16        }
17    }
18
19    pub fn new(values: impl Into<Vec<T>>, has_more: bool) -> Self {
20        Self {
21            values: values.into(),
22            has_more,
23        }
24    }
25}
26
27#[derive(Debug, Clone, Copy)]
28pub struct ListLimit(NonZeroUsize);
29
30impl ListLimit {
31    pub const MAX: ListLimit = Self(NonZeroUsize::new(1000).unwrap());
32
33    pub fn get(&self) -> NonZeroUsize {
34        self.0
35    }
36
37    pub fn as_usize(&self) -> usize {
38        self.0.get()
39    }
40}
41
42impl Default for ListLimit {
43    fn default() -> Self {
44        Self::MAX
45    }
46}
47
48impl From<usize> for ListLimit {
49    fn from(value: usize) -> Self {
50        NonZeroUsize::new(value)
51            .and_then(|n| (n <= Self::MAX.0).then_some(Self(n)))
52            .unwrap_or_default()
53    }
54}
55
56impl From<ListLimit> for usize {
57    fn from(value: ListLimit) -> Self {
58        value.as_usize()
59    }
60}
61
62#[derive(Debug, Clone, Default)]
63pub struct ListItemsRequestParts<P, S> {
64    pub prefix: P,
65    pub start_after: S,
66    pub limit: ListLimit,
67}
68
69#[derive(Debug, Clone, Default)]
70pub struct ListItemsRequest<P, S>(ListItemsRequestParts<P, S>)
71where
72    P: Default,
73    S: Default;
74
75impl<P, S> ListItemsRequest<P, S>
76where
77    P: Default,
78    S: Default,
79{
80    pub fn parts(&self) -> &ListItemsRequestParts<P, S> {
81        &self.0
82    }
83}
84
85impl<P, S> From<ListItemsRequest<P, S>> for ListItemsRequestParts<P, S>
86where
87    P: Default,
88    S: Default,
89{
90    fn from(ListItemsRequest(parts): ListItemsRequest<P, S>) -> Self {
91        parts
92    }
93}
94
95#[derive(Debug, Clone, thiserror::Error)]
96#[error("`start_after` must be greater than or equal to the `prefix`")]
97pub struct StartAfterLessThanPrefixError;
98
99impl<P, S> TryFrom<ListItemsRequestParts<P, S>> for ListItemsRequest<P, S>
100where
101    P: Deref<Target = str> + Default,
102    S: Deref<Target = str> + Default,
103{
104    type Error = StartAfterLessThanPrefixError;
105
106    fn try_from(parts: ListItemsRequestParts<P, S>) -> Result<Self, Self::Error> {
107        let start_after: &str = &parts.start_after;
108        let prefix: &str = &parts.prefix;
109
110        if !start_after.is_empty() && !prefix.is_empty() && start_after < prefix {
111            return Err(StartAfterLessThanPrefixError);
112        }
113
114        Ok(Self(parts))
115    }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub enum CreateMode {
120    /// Create a new resource.
121    ///
122    /// HTTP POST semantics – idempotent if a request token is provided and the resource was
123    /// previously created using the same token.
124    CreateOnly(Option<RequestToken>),
125    /// Create a new resource or reconfigure if the resource already exists.
126    ///
127    /// HTTP PUT semantics – always idempotent.
128    CreateOrReconfigure,
129}
130
131pub static REQUEST_TOKEN_HEADER: http::HeaderName =
132    http::HeaderName::from_static("s2-request-token");
133
134pub const MAX_REQUEST_TOKEN_LENGTH: usize = 36;
135
136#[derive(Debug, PartialEq, Eq, thiserror::Error)]
137#[error("request token was longer than {MAX_REQUEST_TOKEN_LENGTH} bytes in length: {0}")]
138pub struct RequestTokenTooLongError(pub usize);
139
140#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
141pub struct RequestToken(CompactString);
142
143#[cfg(feature = "utoipa")]
144impl utoipa::PartialSchema for RequestToken {
145    fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
146        utoipa::openapi::Object::builder()
147            .schema_type(utoipa::openapi::Type::String)
148            .max_length(Some(MAX_REQUEST_TOKEN_LENGTH))
149            .into()
150    }
151}
152
153#[cfg(feature = "utoipa")]
154impl utoipa::ToSchema for RequestToken {}
155
156impl serde::Serialize for RequestToken {
157    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
158    where
159        S: serde::Serializer,
160    {
161        serializer.serialize_str(&self.0)
162    }
163}
164
165impl<'de> serde::Deserialize<'de> for RequestToken {
166    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
167    where
168        D: serde::Deserializer<'de>,
169    {
170        let s = CompactString::deserialize(deserializer)?;
171        RequestToken::try_from(s).map_err(serde::de::Error::custom)
172    }
173}
174
175impl std::fmt::Display for RequestToken {
176    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177        write!(f, "{}", self.0)
178    }
179}
180
181impl TryFrom<CompactString> for RequestToken {
182    type Error = RequestTokenTooLongError;
183
184    fn try_from(input: CompactString) -> Result<Self, Self::Error> {
185        if input.len() > MAX_REQUEST_TOKEN_LENGTH {
186            return Err(RequestTokenTooLongError(input.len()));
187        }
188        Ok(RequestToken(input))
189    }
190}
191
192impl FromStr for RequestToken {
193    type Err = RequestTokenTooLongError;
194
195    fn from_str(s: &str) -> Result<Self, Self::Err> {
196        s.to_compact_string().try_into()
197    }
198}
199
200impl From<RequestToken> for CompactString {
201    fn from(token: RequestToken) -> Self {
202        token.0
203    }
204}
205
206impl AsRef<str> for RequestToken {
207    fn as_ref(&self) -> &str {
208        &self.0
209    }
210}
211
212impl Deref for RequestToken {
213    type Target = str;
214
215    fn deref(&self) -> &Self::Target {
216        &self.0
217    }
218}
219
220impl crate::http::ParseableHeader for RequestToken {
221    fn name() -> &'static http::HeaderName {
222        &REQUEST_TOKEN_HEADER
223    }
224}