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