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 ListItemsRequest<P, S> {
64    pub prefix: P,
65    pub start_after: S,
66    pub limit: ListLimit,
67}
68
69/// Mode for provisioning a resource.
70///
71/// Provisioning either creates a new resource with create-only semantics, or ensures that
72/// a resource exists with the requested config.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub enum ProvisionMode {
75    /// Create a new resource only.
76    ///
77    /// HTTP POST semantics: idempotent if a request token is provided and the resource was
78    /// previously created using the same token and config.
79    CreateOnly {
80        /// Optional request token used to make create retries idempotent.
81        request_token: Option<RequestToken>,
82    },
83    /// Ensure a resource exists with the requested config.
84    ///
85    /// HTTP PUT semantics: always idempotent. Defaults are applied before validation. When the
86    /// resource already exists, its stored config is set to the effective requested config unless
87    /// it already matches.
88    Ensure,
89}
90
91/// Result of provisioning a resource.
92///
93/// Indicates whether provisioning created, updated, or skipped writing a resource.
94/// All variants hold the resource's current state.
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub enum ProvisionResult<T> {
97    /// Resource was newly created.
98    Created(T),
99    /// Resource already existed and now matches the requested config.
100    Updated(T),
101    /// Resource already existed and no write was performed.
102    Noop(T),
103}
104
105impl<T> ProvisionResult<T> {
106    /// Borrow the inner value regardless of variant.
107    pub fn inner(&self) -> &T {
108        match self {
109            Self::Created(t) | Self::Updated(t) | Self::Noop(t) => t,
110        }
111    }
112
113    /// Unwrap the inner value regardless of variant.
114    pub fn into_inner(self) -> T {
115        match self {
116            Self::Created(t) | Self::Updated(t) | Self::Noop(t) => t,
117        }
118    }
119
120    /// Map the inner value while preserving the provisioning outcome.
121    pub fn map<U>(self, f: impl FnOnce(T) -> U) -> ProvisionResult<U> {
122        match self {
123            Self::Created(t) => ProvisionResult::Created(f(t)),
124            Self::Updated(t) => ProvisionResult::Updated(f(t)),
125            Self::Noop(t) => ProvisionResult::Noop(f(t)),
126        }
127    }
128
129    /// Fallibly map the inner value while preserving the provisioning outcome.
130    pub fn try_map<U, E>(self, f: impl FnOnce(T) -> Result<U, E>) -> Result<ProvisionResult<U>, E> {
131        match self {
132            Self::Created(t) => Ok(ProvisionResult::Created(f(t)?)),
133            Self::Updated(t) => Ok(ProvisionResult::Updated(f(t)?)),
134            Self::Noop(t) => Ok(ProvisionResult::Noop(f(t)?)),
135        }
136    }
137}
138pub static REQUEST_TOKEN_HEADER: http::HeaderName =
139    http::HeaderName::from_static("s2-request-token");
140
141pub static PROVISION_RESULT_HEADER: http::HeaderName =
142    http::HeaderName::from_static("s2-provision-result");
143
144pub const MAX_REQUEST_TOKEN_LENGTH: usize = 36;
145
146#[derive(Debug, PartialEq, Eq, thiserror::Error)]
147#[error("request token was longer than {MAX_REQUEST_TOKEN_LENGTH} bytes in length: {0}")]
148pub struct RequestTokenTooLongError(pub usize);
149
150#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
151pub struct RequestToken(CompactString);
152
153#[cfg(feature = "utoipa")]
154impl utoipa::PartialSchema for RequestToken {
155    fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
156        utoipa::openapi::Object::builder()
157            .schema_type(utoipa::openapi::Type::String)
158            .max_length(Some(MAX_REQUEST_TOKEN_LENGTH))
159            .into()
160    }
161}
162
163#[cfg(feature = "utoipa")]
164impl utoipa::ToSchema for RequestToken {}
165
166impl serde::Serialize for RequestToken {
167    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
168    where
169        S: serde::Serializer,
170    {
171        serializer.serialize_str(&self.0)
172    }
173}
174
175impl<'de> serde::Deserialize<'de> for RequestToken {
176    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
177    where
178        D: serde::Deserializer<'de>,
179    {
180        let s = CompactString::deserialize(deserializer)?;
181        RequestToken::try_from(s).map_err(serde::de::Error::custom)
182    }
183}
184
185impl std::fmt::Display for RequestToken {
186    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187        write!(f, "{}", self.0)
188    }
189}
190
191impl TryFrom<CompactString> for RequestToken {
192    type Error = RequestTokenTooLongError;
193
194    fn try_from(input: CompactString) -> Result<Self, Self::Error> {
195        if input.len() > MAX_REQUEST_TOKEN_LENGTH {
196            return Err(RequestTokenTooLongError(input.len()));
197        }
198        Ok(RequestToken(input))
199    }
200}
201
202impl FromStr for RequestToken {
203    type Err = RequestTokenTooLongError;
204
205    fn from_str(s: &str) -> Result<Self, Self::Err> {
206        s.to_compact_string().try_into()
207    }
208}
209
210impl From<RequestToken> for CompactString {
211    fn from(token: RequestToken) -> Self {
212        token.0
213    }
214}
215
216impl AsRef<str> for RequestToken {
217    fn as_ref(&self) -> &str {
218        &self.0
219    }
220}
221
222impl Deref for RequestToken {
223    type Target = str;
224
225    fn deref(&self) -> &Self::Target {
226        &self.0
227    }
228}
229
230impl crate::http::ParseableHeader for RequestToken {
231    fn name() -> &'static http::HeaderName {
232        &REQUEST_TOKEN_HEADER
233    }
234}