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,
198 #[serde(default, with = "time::serde::rfc3339::option")]
201 pub expires_at: Option<OffsetDateTime>,
202 #[cfg_attr(feature = "utoipa", schema(value_type = bool, default = false, required = false))]
205 pub auto_prefix_streams: Option<bool>,
206 pub scope: AccessTokenScope,
208}
209
210impl TryFrom<IssueAccessTokenRequest> for s2_common::access::IssueAccessTokenRequest {
211 type Error = s2_common::ValidationError;
212
213 fn try_from(value: IssueAccessTokenRequest) -> Result<Self, Self::Error> {
214 Ok(Self {
215 id: value.id,
216 expires_at: value.expires_at,
217 auto_prefix_streams: value.auto_prefix_streams.unwrap_or_default(),
218 scope: value.scope.try_into()?,
219 })
220 }
221}
222
223#[rustfmt::skip]
224#[derive(Debug, Clone, Serialize, Deserialize)]
225#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
226pub struct AccessTokenScope {
227 pub basins: Option<ResourceSet<MaybeEmpty<BasinName>, BasinNamePrefix>>,
229 pub streams: Option<ResourceSet<MaybeEmpty<StreamName>, StreamNamePrefix>>,
231 pub access_tokens: Option<ResourceSet<MaybeEmpty<AccessTokenId>, AccessTokenIdPrefix>>,
233 pub op_groups: Option<PermittedOperationGroups>,
235 #[cfg_attr(feature = "utoipa", schema(required = false))]
238 pub ops: Option<Vec<Operation>>,
239}
240
241impl TryFrom<AccessTokenScope> for s2_common::access::AccessTokenScope {
242 type Error = s2_common::ValidationError;
243
244 fn try_from(value: AccessTokenScope) -> Result<Self, Self::Error> {
245 let AccessTokenScope {
246 basins,
247 streams,
248 access_tokens,
249 op_groups,
250 ops,
251 } = value;
252
253 Ok(Self {
254 basins: basins.map(Into::into).unwrap_or_default(),
255 streams: streams.map(Into::into).unwrap_or_default(),
256 access_tokens: access_tokens.map(Into::into).unwrap_or_default(),
257 op_groups: op_groups.map(Into::into).unwrap_or_default(),
258 ops: ops
259 .map(|o| {
260 o.into_iter()
261 .map(s2_common::access::Operation::from)
262 .collect()
263 })
264 .unwrap_or_default(),
265 })
266 }
267}
268
269impl From<s2_common::access::AccessTokenScope> for AccessTokenScope {
270 fn from(value: s2_common::access::AccessTokenScope) -> Self {
271 let s2_common::access::AccessTokenScope {
272 basins,
273 streams,
274 access_tokens,
275 op_groups,
276 ops,
277 } = value;
278
279 Self {
280 basins: ResourceSet::to_opt(basins),
281 streams: ResourceSet::to_opt(streams),
282 access_tokens: ResourceSet::to_opt(access_tokens),
283 op_groups: Some(op_groups.into()),
284 ops: Some(ops.into_iter().map(Operation::from).collect()),
285 }
286 }
287}
288
289#[rustfmt::skip]
290#[derive(Debug, Clone, Serialize, Deserialize)]
291#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
292#[serde(rename_all = "kebab-case")]
293pub enum ResourceSet<E, P> {
294 #[cfg_attr(feature = "utoipa", schema(title = "exact", value_type = String))]
297 Exact(E),
298 #[cfg_attr(feature = "utoipa", schema(title = "prefix", value_type = String))]
301 Prefix(P),
302}
303
304impl<E, P> ResourceSet<MaybeEmpty<E>, P> {
305 pub fn to_opt(rs: s2_common::access::ResourceSet<E, P>) -> Option<Self> {
306 match rs {
307 s2_common::access::ResourceSet::None => None,
308 s2_common::access::ResourceSet::Exact(e) => {
309 Some(ResourceSet::Exact(MaybeEmpty::NonEmpty(e)))
310 }
311 s2_common::access::ResourceSet::Prefix(p) => Some(ResourceSet::Prefix(p)),
312 }
313 }
314}
315
316impl<E, P> From<ResourceSet<MaybeEmpty<E>, P>> for s2_common::access::ResourceSet<E, P> {
317 fn from(value: ResourceSet<MaybeEmpty<E>, P>) -> Self {
318 match value {
319 ResourceSet::Exact(MaybeEmpty::Empty) => Self::None,
320 ResourceSet::Exact(MaybeEmpty::NonEmpty(e)) => Self::Exact(e),
321 ResourceSet::Prefix(p) => Self::Prefix(p),
322 }
323 }
324}
325
326#[rustfmt::skip]
327#[derive(Debug, Clone, Serialize, Deserialize)]
328#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
329pub struct PermittedOperationGroups {
330 pub account: Option<ReadWritePermissions>,
332 pub basin: Option<ReadWritePermissions>,
334 pub stream: Option<ReadWritePermissions>,
336}
337
338impl From<PermittedOperationGroups> for s2_common::access::PermittedOperationGroups {
339 fn from(value: PermittedOperationGroups) -> Self {
340 let PermittedOperationGroups {
341 account,
342 basin,
343 stream,
344 } = value;
345
346 Self {
347 account: account.map(Into::into).unwrap_or_default(),
348 basin: basin.map(Into::into).unwrap_or_default(),
349 stream: stream.map(Into::into).unwrap_or_default(),
350 }
351 }
352}
353
354impl From<s2_common::access::PermittedOperationGroups> for PermittedOperationGroups {
355 fn from(value: s2_common::access::PermittedOperationGroups) -> Self {
356 let s2_common::access::PermittedOperationGroups {
357 account,
358 basin,
359 stream,
360 } = value;
361
362 Self {
363 account: Some(account.into()),
364 basin: Some(basin.into()),
365 stream: Some(stream.into()),
366 }
367 }
368}
369
370#[rustfmt::skip]
371#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
372#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
373pub struct ReadWritePermissions {
374 #[cfg_attr(feature = "utoipa", schema(value_type = bool, default = false, required = false))]
376 pub read: Option<bool>,
377 #[cfg_attr(feature = "utoipa", schema(value_type = bool, default = false, required = false))]
379 pub write: Option<bool>,
380}
381
382impl From<ReadWritePermissions> for s2_common::access::ReadWritePermissions {
383 fn from(value: ReadWritePermissions) -> Self {
384 let ReadWritePermissions { read, write } = value;
385
386 Self {
387 read: read.unwrap_or_default(),
388 write: write.unwrap_or_default(),
389 }
390 }
391}
392
393impl From<s2_common::access::ReadWritePermissions> for ReadWritePermissions {
394 fn from(value: s2_common::access::ReadWritePermissions) -> Self {
395 let s2_common::access::ReadWritePermissions { read, write } = value;
396
397 Self {
398 read: Some(read),
399 write: Some(write),
400 }
401 }
402}
403
404#[rustfmt::skip]
405#[derive(Debug, Clone, Serialize, Deserialize)]
406#[cfg_attr(feature = "utoipa", derive(utoipa::IntoParams))]
407#[cfg_attr(feature = "utoipa", into_params(parameter_in = Query))]
408pub struct ListAccessTokensRequest {
409 #[cfg_attr(feature = "utoipa", param(value_type = String, default = "", required = false))]
411 pub prefix: Option<AccessTokenIdPrefix>,
412 #[cfg_attr(feature = "utoipa", param(value_type = String, default = "", required = false))]
414 pub start_after: Option<AccessTokenIdStartAfter>,
415 #[cfg_attr(feature = "utoipa", param(value_type = usize, maximum = 1000, default = 1000, required = false))]
417 pub limit: Option<usize>,
418}
419
420super::impl_list_request_conversions!(
421 ListAccessTokensRequest,
422 AccessTokenIdPrefix,
423 AccessTokenIdStartAfter
424);
425
426#[rustfmt::skip]
427#[derive(Debug, Clone, Serialize, Deserialize)]
428#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
429pub struct ListAccessTokensResponse {
430 #[cfg_attr(feature = "utoipa", schema(max_items = 1000))]
432 pub access_tokens: Vec<AccessTokenInfo>,
433 pub has_more: bool,
435}
436
437#[rustfmt::skip]
438#[derive(Debug, Clone, Serialize, Deserialize)]
439#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
440pub struct IssueAccessTokenResponse {
441 pub access_token: String,
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448
449 #[test]
450 fn empty_exact_converts_to_resource_set_none() {
451 let json = serde_json::json!({
452 "id": "test-token",
453 "scope": {
454 "streams": {"exact": ""},
455 "basins": {"exact": ""},
456 "access_tokens": {"exact": ""}
457 }
458 });
459
460 let parsed: IssueAccessTokenRequest = serde_json::from_value(json).unwrap();
461 let internal: s2_common::access::IssueAccessTokenRequest = parsed.try_into().unwrap();
462
463 assert!(matches!(
464 internal.scope.streams,
465 s2_common::access::ResourceSet::None
466 ));
467 assert!(matches!(
468 internal.scope.basins,
469 s2_common::access::ResourceSet::None
470 ));
471 assert!(matches!(
472 internal.scope.access_tokens,
473 s2_common::access::ResourceSet::None
474 ));
475 }
476
477 #[test]
478 fn missing_scope_fields_default_to_resource_set_none() {
479 let json = serde_json::json!({
480 "id": "test-token",
481 "scope": {}
482 });
483
484 let parsed: IssueAccessTokenRequest = serde_json::from_value(json).unwrap();
485 let internal: s2_common::access::IssueAccessTokenRequest = parsed.try_into().unwrap();
486
487 assert!(matches!(
488 internal.scope.streams,
489 s2_common::access::ResourceSet::None
490 ));
491 assert!(matches!(
492 internal.scope.basins,
493 s2_common::access::ResourceSet::None
494 ));
495 assert!(matches!(
496 internal.scope.access_tokens,
497 s2_common::access::ResourceSet::None
498 ));
499 }
500}