1use s2_common::{
2 self,
3 access::{AccessTokenId, AccessTokenIdPrefix, AccessTokenIdStartAfter},
4 basin::{BasinName, BasinNamePrefix},
5 stream::{StreamName, StreamNamePrefix},
6};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone)]
10#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
11pub enum MaybeEmpty<T> {
12 Empty,
13 NonEmpty(T),
14}
15
16impl<T: Serialize> Serialize for MaybeEmpty<T> {
17 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
18 where
19 S: serde::Serializer,
20 {
21 match self {
22 Self::NonEmpty(v) => v.serialize(serializer),
23 Self::Empty => serializer.serialize_str(""),
24 }
25 }
26}
27
28impl<'de, T> Deserialize<'de> for MaybeEmpty<T>
29where
30 T: Deserialize<'de>,
31{
32 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
33 where
34 D: serde::Deserializer<'de>,
35 {
36 let s = String::deserialize(deserializer)?;
37 if s.is_empty() {
38 Ok(MaybeEmpty::Empty)
39 } else {
40 T::deserialize(serde::de::value::StringDeserializer::new(s)).map(MaybeEmpty::NonEmpty)
41 }
42 }
43}
44
45use time::OffsetDateTime;
46
47#[rustfmt::skip]
48#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
49#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
50#[serde(rename_all = "kebab-case")]
51pub enum Operation {
52 ListBasins,
54 CreateBasin,
56 DeleteBasin,
58 ReconfigureBasin,
60 GetBasinConfig,
62 IssueAccessToken,
64 RevokeAccessToken,
66 ListAccessTokens,
68 ListStreams,
70 CreateStream,
72 DeleteStream,
74 GetStreamConfig,
76 ReconfigureStream,
78 CheckTail,
80 Append,
82 Read,
84 Trim,
86 Fence,
88 AccountMetrics,
90 BasinMetrics,
92 StreamMetrics,
94 ListLocations,
96 GetDefaultLocation,
98 SetDefaultLocation,
100}
101
102impl From<Operation> for s2_common::access::Operation {
103 fn from(value: Operation) -> Self {
104 match value {
105 Operation::ListBasins => Self::ListBasins,
106 Operation::CreateBasin => Self::CreateBasin,
107 Operation::DeleteBasin => Self::DeleteBasin,
108 Operation::ReconfigureBasin => Self::ReconfigureBasin,
109 Operation::GetBasinConfig => Self::GetBasinConfig,
110 Operation::IssueAccessToken => Self::IssueAccessToken,
111 Operation::RevokeAccessToken => Self::RevokeAccessToken,
112 Operation::ListAccessTokens => Self::ListAccessTokens,
113 Operation::ListStreams => Self::ListStreams,
114 Operation::CreateStream => Self::CreateStream,
115 Operation::DeleteStream => Self::DeleteStream,
116 Operation::GetStreamConfig => Self::GetStreamConfig,
117 Operation::ReconfigureStream => Self::ReconfigureStream,
118 Operation::CheckTail => Self::CheckTail,
119 Operation::Append => Self::Append,
120 Operation::Read => Self::Read,
121 Operation::Trim => Self::Trim,
122 Operation::Fence => Self::Fence,
123 Operation::AccountMetrics => Self::AccountMetrics,
124 Operation::BasinMetrics => Self::BasinMetrics,
125 Operation::StreamMetrics => Self::StreamMetrics,
126 Operation::ListLocations => Self::ListLocations,
127 Operation::GetDefaultLocation => Self::GetDefaultLocation,
128 Operation::SetDefaultLocation => Self::SetDefaultLocation,
129 }
130 }
131}
132
133impl From<s2_common::access::Operation> for Operation {
134 fn from(value: s2_common::access::Operation) -> Self {
135 use s2_common::access::Operation::*;
136 match value {
137 ListBasins => Self::ListBasins,
138 CreateBasin => Self::CreateBasin,
139 DeleteBasin => Self::DeleteBasin,
140 ReconfigureBasin => Self::ReconfigureBasin,
141 GetBasinConfig => Self::GetBasinConfig,
142 IssueAccessToken => Self::IssueAccessToken,
143 RevokeAccessToken => Self::RevokeAccessToken,
144 ListAccessTokens => Self::ListAccessTokens,
145 ListStreams => Self::ListStreams,
146 CreateStream => Self::CreateStream,
147 DeleteStream => Self::DeleteStream,
148 GetStreamConfig => Self::GetStreamConfig,
149 ReconfigureStream => Self::ReconfigureStream,
150 CheckTail => Self::CheckTail,
151 Append => Self::Append,
152 Read => Self::Read,
153 Trim => Self::Trim,
154 Fence => Self::Fence,
155 AccountMetrics => Self::AccountMetrics,
156 BasinMetrics => Self::BasinMetrics,
157 StreamMetrics => Self::StreamMetrics,
158 ListLocations => Self::ListLocations,
159 GetDefaultLocation => Self::GetDefaultLocation,
160 SetDefaultLocation => Self::SetDefaultLocation,
161 }
162 }
163}
164
165#[rustfmt::skip]
166#[derive(Debug, Clone, Serialize, Deserialize)]
167#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
168pub struct AccessTokenInfo {
169 pub id: AccessTokenId,
171 #[serde(default, with = "time::serde::rfc3339::option")]
173 pub expires_at: Option<OffsetDateTime>,
174 pub auto_prefix_streams: bool,
176 pub scope: AccessTokenScope,
178}
179
180impl From<s2_common::access::AccessTokenInfo> for AccessTokenInfo {
181 fn from(value: s2_common::access::AccessTokenInfo) -> Self {
182 Self {
183 id: value.id,
184 expires_at: value.expires_at,
185 auto_prefix_streams: value.auto_prefix_streams,
186 scope: value.scope.into(),
187 }
188 }
189}
190
191#[rustfmt::skip]
192#[derive(Debug, Clone, Serialize, Deserialize)]
193#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
194pub struct IssueAccessTokenRequest {
195 pub id: AccessTokenId,
199 #[serde(default, with = "time::serde::rfc3339::option")]
202 pub expires_at: Option<OffsetDateTime>,
203 #[cfg_attr(feature = "utoipa", schema(value_type = bool, default = false, required = false))]
206 pub auto_prefix_streams: Option<bool>,
207 pub scope: AccessTokenScope,
209}
210
211impl TryFrom<IssueAccessTokenRequest> for s2_common::access::IssueAccessTokenRequest {
212 type Error = s2_common::ValidationError;
213
214 fn try_from(value: IssueAccessTokenRequest) -> Result<Self, Self::Error> {
215 Ok(Self {
216 id: value.id,
217 expires_at: value.expires_at,
218 auto_prefix_streams: value.auto_prefix_streams.unwrap_or_default(),
219 scope: value.scope.try_into()?,
220 })
221 }
222}
223
224#[rustfmt::skip]
225#[derive(Debug, Clone, Serialize, Deserialize)]
226#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
227pub struct AccessTokenScope {
228 pub basins: Option<ResourceSet<MaybeEmpty<BasinName>, BasinNamePrefix>>,
230 pub streams: Option<ResourceSet<MaybeEmpty<StreamName>, StreamNamePrefix>>,
232 pub access_tokens: Option<ResourceSet<MaybeEmpty<AccessTokenId>, AccessTokenIdPrefix>>,
234 pub op_groups: Option<PermittedOperationGroups>,
236 #[cfg_attr(feature = "utoipa", schema(required = false))]
239 pub ops: Option<Vec<Operation>>,
240}
241
242impl TryFrom<AccessTokenScope> for s2_common::access::AccessTokenScope {
243 type Error = s2_common::ValidationError;
244
245 fn try_from(value: AccessTokenScope) -> Result<Self, Self::Error> {
246 let AccessTokenScope {
247 basins,
248 streams,
249 access_tokens,
250 op_groups,
251 ops,
252 } = value;
253
254 Ok(Self {
255 basins: basins.map(Into::into).unwrap_or_default(),
256 streams: streams.map(Into::into).unwrap_or_default(),
257 access_tokens: access_tokens.map(Into::into).unwrap_or_default(),
258 op_groups: op_groups.map(Into::into).unwrap_or_default(),
259 ops: ops
260 .map(|o| {
261 o.into_iter()
262 .map(s2_common::access::Operation::from)
263 .collect()
264 })
265 .unwrap_or_default(),
266 })
267 }
268}
269
270impl From<s2_common::access::AccessTokenScope> for AccessTokenScope {
271 fn from(value: s2_common::access::AccessTokenScope) -> Self {
272 let s2_common::access::AccessTokenScope {
273 basins,
274 streams,
275 access_tokens,
276 op_groups,
277 ops,
278 } = value;
279
280 Self {
281 basins: ResourceSet::to_opt(basins),
282 streams: ResourceSet::to_opt(streams),
283 access_tokens: ResourceSet::to_opt(access_tokens),
284 op_groups: Some(op_groups.into()),
285 ops: Some(ops.into_iter().map(Operation::from).collect()),
286 }
287 }
288}
289
290#[rustfmt::skip]
291#[derive(Debug, Clone, Serialize, Deserialize)]
292#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
293#[serde(rename_all = "kebab-case")]
294pub enum ResourceSet<E, P> {
295 #[cfg_attr(feature = "utoipa", schema(title = "exact", value_type = String))]
298 Exact(E),
299 #[cfg_attr(feature = "utoipa", schema(title = "prefix", value_type = String))]
302 Prefix(P),
303}
304
305impl<E, P> ResourceSet<MaybeEmpty<E>, P> {
306 pub fn to_opt(rs: s2_common::access::ResourceSet<E, P>) -> Option<Self> {
307 match rs {
308 s2_common::access::ResourceSet::None => None,
309 s2_common::access::ResourceSet::Exact(e) => {
310 Some(ResourceSet::Exact(MaybeEmpty::NonEmpty(e)))
311 }
312 s2_common::access::ResourceSet::Prefix(p) => Some(ResourceSet::Prefix(p)),
313 }
314 }
315}
316
317impl<E, P> From<ResourceSet<MaybeEmpty<E>, P>> for s2_common::access::ResourceSet<E, P> {
318 fn from(value: ResourceSet<MaybeEmpty<E>, P>) -> Self {
319 match value {
320 ResourceSet::Exact(MaybeEmpty::Empty) => Self::None,
321 ResourceSet::Exact(MaybeEmpty::NonEmpty(e)) => Self::Exact(e),
322 ResourceSet::Prefix(p) => Self::Prefix(p),
323 }
324 }
325}
326
327#[rustfmt::skip]
328#[derive(Debug, Clone, Serialize, Deserialize)]
329#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
330pub struct PermittedOperationGroups {
331 pub account: Option<ReadWritePermissions>,
333 pub basin: Option<ReadWritePermissions>,
335 pub stream: Option<ReadWritePermissions>,
337}
338
339impl From<PermittedOperationGroups> for s2_common::access::PermittedOperationGroups {
340 fn from(value: PermittedOperationGroups) -> Self {
341 let PermittedOperationGroups {
342 account,
343 basin,
344 stream,
345 } = value;
346
347 Self {
348 account: account.map(Into::into).unwrap_or_default(),
349 basin: basin.map(Into::into).unwrap_or_default(),
350 stream: stream.map(Into::into).unwrap_or_default(),
351 }
352 }
353}
354
355impl From<s2_common::access::PermittedOperationGroups> for PermittedOperationGroups {
356 fn from(value: s2_common::access::PermittedOperationGroups) -> Self {
357 let s2_common::access::PermittedOperationGroups {
358 account,
359 basin,
360 stream,
361 } = value;
362
363 Self {
364 account: Some(account.into()),
365 basin: Some(basin.into()),
366 stream: Some(stream.into()),
367 }
368 }
369}
370
371#[rustfmt::skip]
372#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
373#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
374pub struct ReadWritePermissions {
375 #[cfg_attr(feature = "utoipa", schema(value_type = bool, default = false, required = false))]
377 pub read: Option<bool>,
378 #[cfg_attr(feature = "utoipa", schema(value_type = bool, default = false, required = false))]
380 pub write: Option<bool>,
381}
382
383impl From<ReadWritePermissions> for s2_common::access::ReadWritePermissions {
384 fn from(value: ReadWritePermissions) -> Self {
385 let ReadWritePermissions { read, write } = value;
386
387 Self {
388 read: read.unwrap_or_default(),
389 write: write.unwrap_or_default(),
390 }
391 }
392}
393
394impl From<s2_common::access::ReadWritePermissions> for ReadWritePermissions {
395 fn from(value: s2_common::access::ReadWritePermissions) -> Self {
396 let s2_common::access::ReadWritePermissions { read, write } = value;
397
398 Self {
399 read: Some(read),
400 write: Some(write),
401 }
402 }
403}
404
405#[rustfmt::skip]
406#[derive(Debug, Clone, Serialize, Deserialize)]
407#[cfg_attr(feature = "utoipa", derive(utoipa::IntoParams))]
408#[cfg_attr(feature = "utoipa", into_params(parameter_in = Query))]
409pub struct ListAccessTokensRequest {
410 #[cfg_attr(feature = "utoipa", param(value_type = String, default = "", required = false))]
413 pub prefix: Option<AccessTokenIdPrefix>,
414 #[cfg_attr(feature = "utoipa", param(value_type = String, default = "", required = false))]
417 pub start_after: Option<AccessTokenIdStartAfter>,
418 #[cfg_attr(feature = "utoipa", param(value_type = usize, maximum = 1000, default = 1000, required = false))]
420 pub limit: Option<usize>,
421}
422
423super::impl_list_request_conversions!(
424 ListAccessTokensRequest,
425 AccessTokenIdPrefix,
426 AccessTokenIdStartAfter
427);
428
429#[rustfmt::skip]
430#[derive(Debug, Clone, Serialize, Deserialize)]
431#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
432pub struct ListAccessTokensResponse {
433 #[cfg_attr(feature = "utoipa", schema(max_items = 1000))]
435 pub access_tokens: Vec<AccessTokenInfo>,
436 pub has_more: bool,
438}
439
440#[rustfmt::skip]
441#[derive(Debug, Clone, Serialize, Deserialize)]
442#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
443pub struct IssueAccessTokenResponse {
444 pub access_token: String,
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 #[test]
453 fn empty_exact_converts_to_resource_set_none() {
454 let json = serde_json::json!({
455 "id": "test-token",
456 "scope": {
457 "streams": {"exact": ""},
458 "basins": {"exact": ""},
459 "access_tokens": {"exact": ""}
460 }
461 });
462
463 let parsed: IssueAccessTokenRequest = serde_json::from_value(json).unwrap();
464 let internal: s2_common::access::IssueAccessTokenRequest = parsed.try_into().unwrap();
465
466 assert!(matches!(
467 internal.scope.streams,
468 s2_common::access::ResourceSet::None
469 ));
470 assert!(matches!(
471 internal.scope.basins,
472 s2_common::access::ResourceSet::None
473 ));
474 assert!(matches!(
475 internal.scope.access_tokens,
476 s2_common::access::ResourceSet::None
477 ));
478 }
479
480 #[test]
481 fn missing_scope_fields_default_to_resource_set_none() {
482 let json = serde_json::json!({
483 "id": "test-token",
484 "scope": {}
485 });
486
487 let parsed: IssueAccessTokenRequest = serde_json::from_value(json).unwrap();
488 let internal: s2_common::access::IssueAccessTokenRequest = parsed.try_into().unwrap();
489
490 assert!(matches!(
491 internal.scope.streams,
492 s2_common::access::ResourceSet::None
493 ));
494 assert!(matches!(
495 internal.scope.basins,
496 s2_common::access::ResourceSet::None
497 ));
498 assert!(matches!(
499 internal.scope.access_tokens,
500 s2_common::access::ResourceSet::None
501 ));
502 }
503}