1use std::{marker::PhantomData, ops::Deref, str::FromStr};
2
3use compact_str::{CompactString, ToCompactString};
4use enumset::{EnumSet, EnumSetType};
5
6use super::{
7 ValidationError,
8 basin::{BasinName, BasinNamePrefix},
9 stream::{StreamName, StreamNamePrefix},
10 strings::{IdProps, PrefixProps, StartAfterProps, StrProps},
11};
12use crate::{caps, types::resources::ListItemsRequest};
13
14#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
15#[cfg_attr(
16 feature = "rkyv",
17 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
18)]
19pub struct AccessTokenIdStr<T: StrProps>(CompactString, PhantomData<T>);
20
21impl<T: StrProps> AccessTokenIdStr<T> {
22 fn validate_str(id: &str) -> Result<(), ValidationError> {
23 if !T::IS_PREFIX && id.is_empty() {
24 return Err(format!("access token {} must not be empty", T::FIELD_NAME).into());
25 }
26
27 if !T::IS_PREFIX && (id == "." || id == "..") {
28 return Err(
29 format!("access token {} must not be \".\" or \"..\"", T::FIELD_NAME).into(),
30 );
31 }
32
33 if id.len() > caps::MAX_ACCESS_TOKEN_ID_LEN {
34 return Err(format!(
35 "access token {} must not exceed {} bytes in length",
36 T::FIELD_NAME,
37 caps::MAX_ACCESS_TOKEN_ID_LEN
38 )
39 .into());
40 }
41
42 Ok(())
43 }
44}
45
46#[cfg(feature = "utoipa")]
47impl<T> utoipa::PartialSchema for AccessTokenIdStr<T>
48where
49 T: StrProps,
50{
51 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
52 utoipa::openapi::Object::builder()
53 .schema_type(utoipa::openapi::Type::String)
54 .min_length((!T::IS_PREFIX).then_some(1))
55 .max_length(Some(caps::MAX_ACCESS_TOKEN_ID_LEN))
56 .into()
57 }
58}
59
60#[cfg(feature = "utoipa")]
61impl<T> utoipa::ToSchema for AccessTokenIdStr<T> where T: StrProps {}
62
63impl<T: StrProps> serde::Serialize for AccessTokenIdStr<T> {
64 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
65 where
66 S: serde::Serializer,
67 {
68 serializer.serialize_str(&self.0)
69 }
70}
71
72impl<'de, T: StrProps> serde::Deserialize<'de> for AccessTokenIdStr<T> {
73 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
74 where
75 D: serde::Deserializer<'de>,
76 {
77 let s = CompactString::deserialize(deserializer)?;
78 s.try_into().map_err(serde::de::Error::custom)
79 }
80}
81
82impl<T: StrProps> AsRef<str> for AccessTokenIdStr<T> {
83 fn as_ref(&self) -> &str {
84 &self.0
85 }
86}
87
88impl<T: StrProps> Deref for AccessTokenIdStr<T> {
89 type Target = str;
90
91 fn deref(&self) -> &Self::Target {
92 &self.0
93 }
94}
95
96impl<T: StrProps> TryFrom<CompactString> for AccessTokenIdStr<T> {
97 type Error = ValidationError;
98
99 fn try_from(name: CompactString) -> Result<Self, Self::Error> {
100 Self::validate_str(&name)?;
101 Ok(Self(name, PhantomData))
102 }
103}
104
105impl<T: StrProps> FromStr for AccessTokenIdStr<T> {
106 type Err = ValidationError;
107
108 fn from_str(s: &str) -> Result<Self, Self::Err> {
109 Self::validate_str(s)?;
110 Ok(Self(s.to_compact_string(), PhantomData))
111 }
112}
113
114impl<T: StrProps> std::fmt::Debug for AccessTokenIdStr<T> {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 f.write_str(&self.0)
117 }
118}
119
120impl<T: StrProps> std::fmt::Display for AccessTokenIdStr<T> {
121 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 f.write_str(&self.0)
123 }
124}
125
126impl<T: StrProps> From<AccessTokenIdStr<T>> for CompactString {
127 fn from(value: AccessTokenIdStr<T>) -> Self {
128 value.0
129 }
130}
131
132pub type AccessTokenId = AccessTokenIdStr<IdProps>;
133
134pub type AccessTokenIdPrefix = AccessTokenIdStr<PrefixProps>;
135
136impl Default for AccessTokenIdPrefix {
137 fn default() -> Self {
138 AccessTokenIdStr(CompactString::default(), PhantomData)
139 }
140}
141
142impl From<AccessTokenId> for AccessTokenIdPrefix {
143 fn from(value: AccessTokenId) -> Self {
144 Self(value.0, PhantomData)
145 }
146}
147
148pub type AccessTokenIdStartAfter = AccessTokenIdStr<StartAfterProps>;
149
150impl Default for AccessTokenIdStartAfter {
151 fn default() -> Self {
152 AccessTokenIdStr(CompactString::default(), PhantomData)
153 }
154}
155
156impl From<AccessTokenId> for AccessTokenIdStartAfter {
157 fn from(value: AccessTokenId) -> Self {
158 Self(value.0, PhantomData)
159 }
160}
161
162#[derive(Debug, Hash, EnumSetType, strum::EnumCount)]
163pub enum Operation {
164 ListBasins = 1,
165 CreateBasin = 2,
166 DeleteBasin = 3,
167 ReconfigureBasin = 4,
168 GetBasinConfig = 5,
169 IssueAccessToken = 6,
170 RevokeAccessToken = 7,
171 ListAccessTokens = 8,
172 ListStreams = 9,
173 CreateStream = 10,
174 DeleteStream = 11,
175 GetStreamConfig = 12,
176 ReconfigureStream = 13,
177 CheckTail = 14,
178 Append = 15,
179 Read = 16,
180 Trim = 17,
181 Fence = 18,
182 AccountMetrics = 19,
183 BasinMetrics = 20,
184 StreamMetrics = 21,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
188#[cfg_attr(
189 feature = "rkyv",
190 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
191)]
192pub enum ResourceSet<E, P> {
193 #[default]
194 None,
195 Exact(E),
196 Prefix(P),
197}
198
199pub type BasinResourceSet = ResourceSet<BasinName, BasinNamePrefix>;
200pub type StreamResourceSet = ResourceSet<StreamName, StreamNamePrefix>;
201pub type AccessTokenResourceSet = ResourceSet<AccessTokenId, AccessTokenIdPrefix>;
202
203#[derive(Debug, Clone, Copy, Default)]
204pub struct ReadWritePermissions {
205 pub read: bool,
206 pub write: bool,
207}
208
209#[derive(Debug, Clone, Default)]
210pub struct PermittedOperationGroups {
211 pub account: ReadWritePermissions,
212 pub basin: ReadWritePermissions,
213 pub stream: ReadWritePermissions,
214}
215
216#[derive(Debug, Clone, Default)]
217pub struct AccessTokenScope {
218 pub basins: BasinResourceSet,
219 pub streams: StreamResourceSet,
220 pub access_tokens: AccessTokenResourceSet,
221 pub op_groups: PermittedOperationGroups,
222 pub ops: EnumSet<Operation>,
223}
224
225#[derive(Debug, Clone)]
226pub struct AccessTokenInfo {
227 pub id: AccessTokenId,
228 pub expires_at: time::OffsetDateTime,
229 pub auto_prefix_streams: bool,
230 pub scope: AccessTokenScope,
231}
232
233#[derive(Debug, Clone)]
234pub struct IssueAccessTokenRequest {
235 pub id: AccessTokenId,
236 pub expires_at: Option<time::OffsetDateTime>,
237 pub auto_prefix_streams: bool,
238 pub scope: AccessTokenScope,
239}
240
241pub type ListAccessTokensRequest = ListItemsRequest<AccessTokenIdPrefix, AccessTokenIdStartAfter>;
242
243#[cfg(test)]
244mod test {
245 use rstest::rstest;
246
247 use super::{
248 super::strings::{IdProps, PrefixProps, StartAfterProps},
249 AccessTokenIdStr,
250 };
251
252 #[rstest]
253 #[case::normal("my-token".to_owned())]
254 #[case::max_len("a".repeat(crate::caps::MAX_ACCESS_TOKEN_ID_LEN))]
255 fn validate_id_ok(#[case] id: String) {
256 assert_eq!(AccessTokenIdStr::<IdProps>::validate_str(&id), Ok(()));
257 }
258
259 #[rstest]
260 #[case::empty("".to_owned())]
261 #[case::dot(".".to_owned())]
262 #[case::dot_dot("..".to_owned())]
263 #[case::too_long("a".repeat(crate::caps::MAX_ACCESS_TOKEN_ID_LEN + 1))]
264 fn validate_id_err(#[case] id: String) {
265 AccessTokenIdStr::<IdProps>::validate_str(&id).expect_err("expected validation error");
266 }
267
268 #[rstest]
269 #[case::empty("".to_owned())]
270 #[case::dot(".".to_owned())]
271 #[case::dot_dot("..".to_owned())]
272 #[case::max_len("a".repeat(crate::caps::MAX_ACCESS_TOKEN_ID_LEN))]
273 fn validate_prefix_ok(#[case] prefix: String) {
274 assert_eq!(
275 AccessTokenIdStr::<PrefixProps>::validate_str(&prefix),
276 Ok(())
277 );
278 }
279
280 #[rstest]
281 #[case::too_long("a".repeat(crate::caps::MAX_ACCESS_TOKEN_ID_LEN + 1))]
282 fn validate_prefix_err(#[case] prefix: String) {
283 AccessTokenIdStr::<PrefixProps>::validate_str(&prefix)
284 .expect_err("expected validation error");
285 }
286
287 #[rstest]
288 #[case::empty("".to_owned())]
289 #[case::dot(".".to_owned())]
290 #[case::dot_dot("..".to_owned())]
291 #[case::max_len("a".repeat(crate::caps::MAX_ACCESS_TOKEN_ID_LEN))]
292 fn validate_start_after_ok(#[case] start_after: String) {
293 assert_eq!(
294 AccessTokenIdStr::<StartAfterProps>::validate_str(&start_after),
295 Ok(())
296 );
297 }
298
299 #[rstest]
300 #[case::too_long("a".repeat(crate::caps::MAX_ACCESS_TOKEN_ID_LEN + 1))]
301 fn validate_start_after_err(#[case] start_after: String) {
302 AccessTokenIdStr::<StartAfterProps>::validate_str(&start_after)
303 .expect_err("expected validation error");
304 }
305}