1use crate::abac::AttrSet;
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use serde_with::skip_serializing_none;
9use std::collections::HashMap;
10use std::time::SystemTime;
11
12#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
16pub struct TnId(pub u32);
17
18impl std::fmt::Display for TnId {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 write!(f, "{}", self.0)
21 }
22}
23
24impl Serialize for TnId {
25 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
26 where
27 S: serde::Serializer,
28 {
29 serializer.serialize_u32(self.0)
30 }
31}
32
33impl<'de> Deserialize<'de> for TnId {
34 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
35 where
36 D: serde::Deserializer<'de>,
37 {
38 Ok(TnId(u32::deserialize(deserializer)?))
39 }
40}
41
42#[derive(Clone, Copy, Debug, Default)]
46pub struct Timestamp(pub i64);
47
48impl Timestamp {
49 pub fn now() -> Timestamp {
50 let res = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default();
51 Timestamp(res.as_secs().cast_signed())
52 }
53
54 pub fn from_now(delta: i64) -> Timestamp {
55 let res = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default();
56 Timestamp(res.as_secs().cast_signed() + delta)
57 }
58
59 pub fn add_seconds(&self, seconds: i64) -> Timestamp {
61 Timestamp(self.0 + seconds)
62 }
63
64 pub fn to_iso_string(&self) -> String {
66 use chrono::{DateTime, SecondsFormat};
67 DateTime::from_timestamp(self.0, 0)
68 .unwrap_or_default()
69 .to_rfc3339_opts(SecondsFormat::Secs, true)
70 }
71}
72
73impl std::fmt::Display for Timestamp {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 write!(f, "{}", self.0)
76 }
77}
78
79impl std::cmp::PartialEq for Timestamp {
80 fn eq(&self, other: &Self) -> bool {
81 self.0 == other.0
82 }
83}
84
85impl std::cmp::PartialOrd for Timestamp {
86 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
87 Some(self.cmp(other))
88 }
89}
90
91impl std::cmp::Eq for Timestamp {}
92
93impl std::cmp::Ord for Timestamp {
94 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
95 self.0.cmp(&other.0)
96 }
97}
98
99impl Serialize for Timestamp {
100 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
101 where
102 S: serde::Serializer,
103 {
104 serializer.serialize_i64(self.0)
105 }
106}
107
108impl<'de> Deserialize<'de> for Timestamp {
109 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
110 where
111 D: serde::Deserializer<'de>,
112 {
113 use serde::de::{Error, Visitor};
114
115 struct TimestampVisitor;
116
117 impl Visitor<'_> for TimestampVisitor {
118 type Value = Timestamp;
119
120 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
121 write!(f, "an integer timestamp or ISO 8601 string")
122 }
123
124 fn visit_i64<E: Error>(self, v: i64) -> Result<Self::Value, E> {
125 Ok(Timestamp(v))
126 }
127
128 fn visit_u64<E: Error>(self, v: u64) -> Result<Self::Value, E> {
129 Ok(Timestamp(v.cast_signed()))
130 }
131
132 fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
133 use chrono::DateTime;
134 DateTime::parse_from_rfc3339(v)
135 .map(|dt| Timestamp(dt.timestamp()))
136 .map_err(|_| E::custom("invalid ISO 8601 timestamp"))
137 }
138 }
139
140 deserializer.deserialize_any(TimestampVisitor)
141 }
142}
143
144pub fn serialize_timestamp_iso<S>(ts: &Timestamp, serializer: S) -> Result<S::Ok, S::Error>
146where
147 S: Serializer,
148{
149 use chrono::{DateTime, SecondsFormat};
150 let dt = DateTime::from_timestamp(ts.0, 0).unwrap_or_default();
151 serializer.serialize_str(&dt.to_rfc3339_opts(SecondsFormat::Secs, true))
152}
153
154pub fn serialize_timestamp_iso_opt<S>(
156 ts: &Option<Timestamp>,
157 serializer: S,
158) -> Result<S::Ok, S::Error>
159where
160 S: Serializer,
161{
162 match ts {
163 Some(ts) => serialize_timestamp_iso(ts, serializer),
164 None => serializer.serialize_none(),
165 }
166}
167
168#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
175pub enum Patch<T> {
176 #[default]
178 Undefined,
179 Null,
181 Value(T),
183}
184
185impl<T> Patch<T> {
186 pub fn is_undefined(&self) -> bool {
188 matches!(self, Patch::Undefined)
189 }
190
191 pub fn is_null(&self) -> bool {
193 matches!(self, Patch::Null)
194 }
195
196 pub fn is_value(&self) -> bool {
198 matches!(self, Patch::Value(_))
199 }
200
201 pub fn value(&self) -> Option<&T> {
203 match self {
204 Patch::Value(v) => Some(v),
205 _ => None,
206 }
207 }
208
209 pub fn as_option(&self) -> Option<Option<&T>> {
211 match self {
212 Patch::Undefined => None,
213 Patch::Null => Some(None),
214 Patch::Value(v) => Some(Some(v)),
215 }
216 }
217
218 pub fn map<U, F>(self, f: F) -> Patch<U>
220 where
221 F: FnOnce(T) -> U,
222 {
223 match self {
224 Patch::Undefined => Patch::Undefined,
225 Patch::Null => Patch::Null,
226 Patch::Value(v) => Patch::Value(f(v)),
227 }
228 }
229}
230
231impl<T> Serialize for Patch<T>
232where
233 T: Serialize,
234{
235 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
236 where
237 S: Serializer,
238 {
239 match self {
240 Patch::Undefined | Patch::Null => serializer.serialize_none(),
241 Patch::Value(v) => v.serialize(serializer),
242 }
243 }
244}
245
246impl<'de, T> Deserialize<'de> for Patch<T>
247where
248 T: Deserialize<'de>,
249{
250 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
251 where
252 D: Deserializer<'de>,
253 {
254 Option::<T>::deserialize(deserializer).map(|opt| match opt {
255 None => Patch::Null,
256 Some(v) => Patch::Value(v),
257 })
258 }
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
266#[serde(rename_all = "camelCase")]
267pub struct RegisterVerifyCheckRequest {
268 #[serde(rename = "type")]
269 pub typ: String, pub id_tag: String,
271 pub app_domain: Option<String>,
272 pub token: Option<String>, }
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
277#[serde(rename_all = "camelCase")]
278pub struct RegisterRequest {
279 #[serde(rename = "type")]
280 pub typ: String, pub id_tag: String,
282 pub app_domain: Option<String>,
283 pub email: String,
284 pub token: String,
285 pub lang: Option<String>,
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
290#[serde(rename_all = "camelCase")]
291pub struct RegisterVerifyRequest {
292 pub id_tag: String,
293 pub token: String,
294}
295
296#[serde_with::skip_serializing_none]
298#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
299#[serde(rename_all = "camelCase")]
300pub struct Profile {
301 pub id_tag: String,
302 pub name: String,
303 #[serde(rename = "type")]
304 pub r#type: String,
305 pub profile_pic: Option<String>,
306 pub cover_pic: Option<String>,
307 pub keys: Vec<crate::auth_adapter::AuthKey>,
308 pub x: Option<HashMap<String, String>>,
310}
311
312#[serde_with::skip_serializing_none]
318#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
319#[serde(rename_all = "camelCase")]
320pub struct ProfileBase {
321 pub id_tag: String,
322 pub name: String,
323 #[serde(rename = "type")]
324 pub r#type: String,
325 pub profile_pic: Option<String>,
326 pub keys: Vec<crate::auth_adapter::AuthKey>,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
332#[serde(rename_all = "camelCase")]
333pub struct AppDomainRes {
334 pub app_domain: String,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
339#[serde(rename_all = "camelCase")]
340pub struct ProfilePatch {
341 #[serde(default)]
342 pub name: Patch<String>,
343 #[serde(default)]
345 pub x: Option<HashMap<String, Option<String>>>,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
350#[serde(rename_all = "camelCase")]
351pub struct AdminProfilePatch {
352 #[serde(default)]
354 pub name: Patch<String>,
355
356 #[serde(default)]
358 pub roles: Patch<Option<Vec<String>>>,
359 #[serde(default)]
360 pub status: Patch<crate::meta_adapter::ProfileStatus>,
361}
362
363#[skip_serializing_none]
365#[derive(Debug, Clone, Default, Serialize, Deserialize)]
366#[serde(rename_all = "camelCase")]
367pub struct ProfileInfo {
368 pub id_tag: String,
369 pub name: String,
370 #[serde(rename = "type")]
371 pub r#type: Option<String>,
372 pub profile_pic: Option<String>, pub status: Option<crate::meta_adapter::ProfileStatus>,
374 pub connected: Option<bool>,
375 pub following: Option<bool>,
376 pub follower: Option<bool>,
377 pub trust: Option<crate::meta_adapter::ProfileTrust>,
380 pub roles: Option<Vec<String>>,
381 #[serde(
382 serialize_with = "serialize_timestamp_iso_opt",
383 skip_serializing_if = "Option::is_none"
384 )]
385 pub created_at: Option<Timestamp>,
386 #[serde(
389 serialize_with = "serialize_timestamp_iso_opt",
390 skip_serializing_if = "Option::is_none"
391 )]
392 pub feed_read_at: Option<Timestamp>,
393 #[serde(
396 serialize_with = "serialize_timestamp_iso_opt",
397 skip_serializing_if = "Option::is_none"
398 )]
399 pub msg_read_at: Option<Timestamp>,
400 pub hidden_in_home: Option<bool>,
403 pub x: Option<HashMap<String, String>>,
405}
406
407#[derive(Debug, Clone, Deserialize)]
409#[serde(rename_all = "camelCase")]
410pub struct CreateCommunityRequest {
411 #[serde(rename = "type")]
412 pub typ: String, pub name: Option<String>,
414 pub profile_pic: Option<String>,
415 pub app_domain: Option<String>, pub invite_ref: Option<String>, }
418
419#[skip_serializing_none]
421#[derive(Debug, Clone, Serialize)]
422#[serde(rename_all = "camelCase")]
423pub struct CommunityProfileResponse {
424 pub id_tag: String,
425 pub name: String,
426 #[serde(rename = "type")]
427 pub r#type: String,
428 pub profile_pic: Option<String>,
429 #[serde(serialize_with = "serialize_timestamp_iso")]
430 pub created_at: Timestamp,
431 pub onboarding: Option<String>,
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize)]
444#[serde(rename_all = "camelCase")]
445pub struct CreateActionRequest {
446 #[serde(rename = "type")]
447 pub r#type: String, pub sub_type: Option<String>, pub parent_id: Option<String>,
450 pub content: String,
452 pub attachments: Option<Vec<String>>, pub audience: Option<Vec<String>>,
454}
455
456#[skip_serializing_none]
458#[derive(Debug, Clone, Serialize, Deserialize)]
459#[serde(rename_all = "camelCase")]
460pub struct ActionResponse {
461 pub action_id: String,
462 pub action_token: String,
463 #[serde(rename = "type")]
464 pub r#type: String,
465 pub sub_type: Option<String>,
466 pub parent_id: Option<String>,
467 pub root_id: Option<String>,
468 pub content: String,
469 pub attachments: Vec<String>,
470 pub issuer_tag: String,
471 #[serde(serialize_with = "serialize_timestamp_iso")]
472 pub created_at: Timestamp,
473}
474
475#[derive(Debug, Clone, Default, Deserialize)]
477#[serde(rename_all = "camelCase")]
478pub struct ListActionsQuery {
479 #[serde(rename = "type")]
480 pub r#type: Option<String>,
481 pub parent_id: Option<String>,
482 pub offset: Option<usize>,
483 pub limit: Option<usize>,
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize)]
488#[serde(rename_all = "camelCase")]
489pub struct FileUploadResponse {
490 pub file_id: String,
491 pub descriptor: String,
492 pub variants: Vec<FileVariantInfo>,
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize)]
497#[serde(rename_all = "camelCase")]
498pub struct FileVariantInfo {
499 pub variant_id: String,
500 pub format: String,
501 pub size: u64,
502 pub resolution: Option<(u32, u32)>,
503}
504
505#[derive(Debug, Clone, Serialize, Deserialize)]
507pub struct TagInfo {
508 pub tag: String,
509 #[serde(skip_serializing_if = "Option::is_none")]
510 pub count: Option<u32>,
511}
512
513#[derive(Debug, Clone, Serialize, Deserialize)]
518#[serde(rename_all = "camelCase")]
519pub struct PaginationInfo {
520 pub offset: usize,
521 pub limit: usize,
522 pub total: usize,
523}
524
525#[derive(Debug, Clone, Serialize, Deserialize)]
530#[serde(rename_all = "camelCase")]
531pub struct CursorPaginationInfo {
532 pub next_cursor: Option<String>,
534 pub has_more: bool,
536 #[serde(skip_serializing_if = "Option::is_none")]
539 pub count: Option<i64>,
540}
541
542#[derive(Debug, Clone, Serialize, Deserialize)]
544pub struct CursorData {
545 pub s: String,
547 pub v: serde_json::Value,
549 pub id: String,
551}
552
553impl CursorData {
554 pub fn new(sort_field: &str, sort_value: serde_json::Value, item_id: &str) -> Self {
556 Self { s: sort_field.to_string(), v: sort_value, id: item_id.to_string() }
557 }
558
559 pub fn encode(&self) -> String {
561 use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
562 let json = serde_json::to_string(self).unwrap_or_default();
563 URL_SAFE_NO_PAD.encode(json.as_bytes())
564 }
565
566 pub fn decode(cursor: &str) -> Option<Self> {
568 use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
569 let bytes = URL_SAFE_NO_PAD.decode(cursor).ok()?;
570 let json = String::from_utf8(bytes).ok()?;
571 serde_json::from_str(&json).ok()
572 }
573
574 pub fn timestamp(&self) -> Option<i64> {
576 self.v.as_i64()
577 }
578
579 pub fn string_value(&self) -> Option<&str> {
581 self.v.as_str()
582 }
583}
584
585#[derive(Debug, Serialize, Deserialize)]
587#[serde(rename_all = "camelCase")]
588pub struct ApiResponse<T> {
589 pub data: T,
590 #[serde(skip_serializing_if = "Option::is_none")]
591 pub pagination: Option<PaginationInfo>,
592 #[serde(skip_serializing_if = "Option::is_none")]
593 pub cursor_pagination: Option<CursorPaginationInfo>,
594 #[serde(serialize_with = "serialize_timestamp_iso")]
595 pub time: Timestamp,
596 #[serde(skip_serializing_if = "Option::is_none")]
597 pub req_id: Option<String>,
598}
599
600impl<T> ApiResponse<T> {
601 pub fn new(data: T) -> Self {
603 Self {
604 data,
605 pagination: None,
606 cursor_pagination: None,
607 time: Timestamp::now(),
608 req_id: None,
609 }
610 }
611
612 pub fn with_pagination(data: T, offset: usize, limit: usize, total: usize) -> Self {
614 Self {
615 data,
616 pagination: Some(PaginationInfo { offset, limit, total }),
617 cursor_pagination: None,
618 time: Timestamp::now(),
619 req_id: None,
620 }
621 }
622
623 pub fn with_cursor_pagination(data: T, next_cursor: Option<String>, has_more: bool) -> Self {
625 Self {
626 data,
627 pagination: None,
628 cursor_pagination: Some(CursorPaginationInfo { next_cursor, has_more, count: None }),
629 time: Timestamp::now(),
630 req_id: None,
631 }
632 }
633
634 pub fn with_count(count: i64) -> Self
638 where
639 T: Default,
640 {
641 Self {
642 data: T::default(),
643 pagination: None,
644 cursor_pagination: Some(CursorPaginationInfo {
645 next_cursor: None,
646 has_more: false,
647 count: Some(count),
648 }),
649 time: Timestamp::now(),
650 req_id: None,
651 }
652 }
653
654 pub fn with_req_id(mut self, req_id: String) -> Self {
656 self.req_id = Some(req_id);
657 self
658 }
659}
660
661#[derive(Debug, Serialize, Deserialize)]
663#[serde(rename_all = "camelCase")]
664pub struct ErrorResponse {
665 pub error: ErrorDetails,
666}
667
668#[derive(Debug, Serialize, Deserialize)]
670#[serde(rename_all = "camelCase")]
671pub struct ErrorDetails {
672 pub code: String,
673 pub message: String,
674 #[serde(skip_serializing_if = "Option::is_none")]
675 pub details: Option<serde_json::Value>,
676}
677
678impl ErrorResponse {
679 pub fn new(code: String, message: String) -> Self {
681 Self { error: ErrorDetails { code, message, details: None } }
682 }
683
684 pub fn with_details(mut self, details: serde_json::Value) -> Self {
686 self.error.details = Some(details);
687 self
688 }
689}
690
691#[derive(
699 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
700)]
701#[serde(rename_all = "lowercase")]
702pub enum AccessLevel {
703 None,
704 Read,
705 Comment,
706 Write,
707 Admin,
708}
709
710impl AccessLevel {
711 pub fn as_str(&self) -> &'static str {
712 match self {
713 Self::None => "none",
714 Self::Read => "read",
715 Self::Comment => "comment",
716 Self::Write => "write",
717 Self::Admin => "admin",
718 }
719 }
720
721 pub fn from_str_name(s: &str) -> Option<Self> {
725 match s {
726 "none" => Some(Self::None),
727 "read" => Some(Self::Read),
728 "comment" => Some(Self::Comment),
729 "write" => Some(Self::Write),
730 "admin" => Some(Self::Admin),
731 _ => Option::None,
732 }
733 }
734
735 pub fn from_perm_char(c: char) -> Self {
742 match c {
743 'A' => Self::Admin,
744 'W' => Self::Write,
745 'C' => Self::Comment,
746 _ => Self::Read,
747 }
748 }
749
750 pub fn to_perm_char(self) -> Option<char> {
755 match self {
756 Self::None => Option::None,
757 Self::Read => Some('R'),
758 Self::Comment => Some('C'),
759 Self::Write => Some('W'),
760 Self::Admin => Some('A'),
761 }
762 }
763
764 pub fn to_scope_char(self) -> Option<char> {
774 match self {
775 Self::Admin | Self::Write => Some('W'),
776 Self::Comment => Some('C'),
777 Self::Read => Some('R'),
778 Self::None => Option::None,
779 }
780 }
781
782 pub fn can_read(self) -> bool {
784 self >= Self::Read
785 }
786
787 pub fn can_comment(self) -> bool {
789 self >= Self::Comment
790 }
791
792 pub fn can_write(self) -> bool {
794 self >= Self::Write
795 }
796
797 pub fn can_manage_shares(self) -> bool {
799 self == Self::Admin
800 }
801}
802
803#[derive(Debug, Clone, PartialEq, Eq)]
808pub enum TokenScope {
809 File { file_id: String, access: AccessLevel },
811 ApkgPublish,
813}
814
815impl TokenScope {
816 pub fn parse(s: &str) -> Option<Self> {
823 if s == "apkg:publish" {
824 return Some(Self::ApkgPublish);
825 }
826 let parts: Vec<&str> = s.split(':').collect();
827 if parts.len() == 3 && parts[0] == "file" {
828 let access = match parts[2] {
832 "R" => AccessLevel::Read,
833 "C" => AccessLevel::Comment,
834 "W" => AccessLevel::Write,
835 _ => return None,
836 };
837 return Some(Self::File { file_id: parts[1].to_string(), access });
838 }
839 None
840 }
841
842 pub fn file_id(&self) -> Option<&str> {
844 match self {
845 Self::File { file_id, .. } => Some(file_id),
846 Self::ApkgPublish => None,
847 }
848 }
849
850 pub fn file_access(&self) -> Option<AccessLevel> {
852 match self {
853 Self::File { access, .. } => Some(*access),
854 Self::ApkgPublish => None,
855 }
856 }
857
858 pub fn matches_file(&self, target_file_id: &str) -> bool {
860 match self {
861 Self::File { file_id, .. } => file_id == target_file_id,
862 Self::ApkgPublish => false,
863 }
864 }
865}
866
867#[derive(Debug, Clone)]
869pub struct ProfileAttrs {
870 pub id_tag: Box<str>,
871 pub profile_type: Box<str>,
872 pub tenant_tag: Box<str>,
873 pub roles: Vec<Box<str>>,
874 pub status: Box<str>,
875 pub following: bool,
876 pub connected: bool,
877 pub visibility: Box<str>,
878}
879
880impl AttrSet for ProfileAttrs {
881 fn get(&self, key: &str) -> Option<&str> {
882 match key {
883 "id_tag" => Some(&self.id_tag),
884 "profile_type" => Some(&self.profile_type),
885 "tenant_tag" | "owner_id_tag" => Some(&self.tenant_tag),
886 "status" => Some(&self.status),
887 "following" => Some(if self.following { "true" } else { "false" }),
888 "connected" => Some(if self.connected { "true" } else { "false" }),
889 "visibility" => Some(&self.visibility),
890 _ => None,
891 }
892 }
893
894 fn get_list(&self, key: &str) -> Option<Vec<&str>> {
895 match key {
896 "roles" => Some(self.roles.iter().map(AsRef::as_ref).collect()),
897 _ => None,
898 }
899 }
900}
901
902#[derive(Debug, Clone)]
904pub struct ActionAttrs {
905 pub typ: Box<str>,
906 pub sub_typ: Option<Box<str>>,
907 pub tenant_id_tag: Box<str>,
909 pub issuer_id_tag: Box<str>,
911 pub parent_id: Option<Box<str>>,
912 pub root_id: Option<Box<str>>,
913 pub audience_tag: Vec<Box<str>>,
914 pub tags: Vec<Box<str>>,
915 pub visibility: Box<str>,
916 pub following: bool,
918 pub connected: bool,
920}
921
922impl AttrSet for ActionAttrs {
923 fn get(&self, key: &str) -> Option<&str> {
924 match key {
925 "type" => Some(&self.typ),
926 "sub_type" => self.sub_typ.as_deref(),
927 "tenant_id_tag" | "owner_id_tag" => Some(&self.tenant_id_tag),
929 "issuer_id_tag" => Some(&self.issuer_id_tag),
930 "parent_id" => self.parent_id.as_deref(),
931 "root_id" => self.root_id.as_deref(),
932 "visibility" => Some(&self.visibility),
933 "following" => Some(if self.following { "true" } else { "false" }),
934 "connected" => Some(if self.connected { "true" } else { "false" }),
935 _ => None,
936 }
937 }
938
939 fn get_list(&self, key: &str) -> Option<Vec<&str>> {
940 match key {
941 "audience_tag" => Some(self.audience_tag.iter().map(AsRef::as_ref).collect()),
942 "tags" => Some(self.tags.iter().map(AsRef::as_ref).collect()),
943 _ => None,
944 }
945 }
946}
947
948#[derive(Debug, Clone)]
950pub struct FileAttrs {
951 pub file_id: Box<str>,
952 pub owner_id_tag: Box<str>,
953 pub mime_type: Box<str>,
954 pub tags: Vec<Box<str>>,
955 pub visibility: Box<str>,
956 pub access_level: AccessLevel,
957 pub following: bool,
959 pub connected: bool,
961}
962
963impl AttrSet for FileAttrs {
964 fn get(&self, key: &str) -> Option<&str> {
965 match key {
966 "file_id" => Some(&self.file_id),
967 "owner_id_tag" => Some(&self.owner_id_tag),
968 "mime_type" => Some(&self.mime_type),
969 "visibility" => Some(&self.visibility),
970 "access_level" => Some(self.access_level.as_str()),
971 "following" => Some(if self.following { "true" } else { "false" }),
972 "connected" => Some(if self.connected { "true" } else { "false" }),
973 _ => None,
974 }
975 }
976
977 fn get_list(&self, key: &str) -> Option<Vec<&str>> {
978 match key {
979 "tags" => Some(self.tags.iter().map(AsRef::as_ref).collect()),
980 _ => None,
981 }
982 }
983}
984
985#[derive(Debug, Clone)]
990pub struct SubjectAttrs {
991 pub id_tag: Box<str>,
992 pub roles: Vec<Box<str>>,
993 pub tier: Box<str>, pub quota_remaining_bytes: Box<str>, pub rate_limit_remaining: Box<str>, pub banned: bool,
997 pub email_verified: bool,
998}
999
1000impl AttrSet for SubjectAttrs {
1001 fn get(&self, key: &str) -> Option<&str> {
1002 match key {
1003 "id_tag" => Some(&self.id_tag),
1004 "tier" => Some(&self.tier),
1005 "quota_remaining" | "quota_remaining_bytes" => Some(&self.quota_remaining_bytes),
1006 "rate_limit_remaining" => Some(&self.rate_limit_remaining),
1007 "banned" => Some(if self.banned { "true" } else { "false" }),
1008 "email_verified" => Some(if self.email_verified { "true" } else { "false" }),
1009 _ => None,
1010 }
1011 }
1012
1013 fn get_list(&self, key: &str) -> Option<Vec<&str>> {
1014 match key {
1015 "roles" => Some(self.roles.iter().map(AsRef::as_ref).collect()),
1016 _ => None,
1017 }
1018 }
1019}
1020
1021#[cfg(test)]
1022mod access_level_tests {
1023 use super::{AccessLevel, TokenScope};
1024
1025 #[test]
1026 fn perm_char_keeps_admin_distinct() {
1027 assert_eq!(AccessLevel::from_perm_char('A'), AccessLevel::Admin);
1028 assert_eq!(AccessLevel::from_perm_char('W'), AccessLevel::Write);
1029 assert_eq!(AccessLevel::from_perm_char('C'), AccessLevel::Comment);
1030 assert_eq!(AccessLevel::from_perm_char('R'), AccessLevel::Read);
1031 assert_eq!(AccessLevel::from_perm_char('?'), AccessLevel::Read);
1033 }
1034
1035 #[test]
1036 fn to_perm_char_is_the_injective_inverse() {
1037 for level in
1038 [AccessLevel::Read, AccessLevel::Comment, AccessLevel::Write, AccessLevel::Admin]
1039 {
1040 let c = level.to_perm_char().expect("every level above None has a char");
1041 assert_eq!(AccessLevel::from_perm_char(c), level);
1042 }
1043 assert_eq!(AccessLevel::None.to_perm_char(), None);
1045 }
1046
1047 #[test]
1048 fn str_name_round_trips_every_variant() {
1049 for level in [
1052 AccessLevel::None,
1053 AccessLevel::Read,
1054 AccessLevel::Comment,
1055 AccessLevel::Write,
1056 AccessLevel::Admin,
1057 ] {
1058 assert_eq!(AccessLevel::from_str_name(level.as_str()), Some(level));
1059 }
1060 assert_eq!(AccessLevel::from_str_name("bogus"), None);
1063 assert_eq!(AccessLevel::from_str_name(""), None);
1064 assert_eq!(AccessLevel::from_str_name("Write"), None);
1066 }
1067
1068 #[test]
1069 fn scope_char_caps_admin_at_write() {
1070 assert_eq!(AccessLevel::Admin.to_scope_char(), Some('W'));
1071 assert_eq!(AccessLevel::Write.to_scope_char(), Some('W'));
1072 assert_eq!(AccessLevel::Comment.to_scope_char(), Some('C'));
1073 assert_eq!(AccessLevel::Read.to_scope_char(), Some('R'));
1074 assert_eq!(AccessLevel::None.to_scope_char(), None);
1077 }
1078
1079 #[test]
1080 fn predicates_follow_the_ordering() {
1081 assert!(AccessLevel::Admin.can_read());
1082 assert!(AccessLevel::Admin.can_comment());
1083 assert!(AccessLevel::Admin.can_write());
1084 assert!(AccessLevel::Admin.can_manage_shares());
1085
1086 assert!(AccessLevel::Write.can_write());
1087 assert!(!AccessLevel::Write.can_manage_shares());
1088 assert!(!AccessLevel::Comment.can_write());
1089 assert!(AccessLevel::Comment.can_comment());
1090 assert!(!AccessLevel::Read.can_comment());
1091 assert!(AccessLevel::Read.can_read());
1092 assert!(!AccessLevel::None.can_read());
1093 }
1094
1095 #[test]
1096 fn scope_parse_rejects_an_admin_level_char() {
1097 assert_eq!(TokenScope::parse("file:f1~abc:A"), None);
1100 assert_eq!(TokenScope::parse("file:f1~abc:X"), None);
1101 assert_eq!(
1102 TokenScope::parse("file:f1~abc:W"),
1103 Some(TokenScope::File { file_id: "f1~abc".to_string(), access: AccessLevel::Write })
1104 );
1105 assert_eq!(
1106 TokenScope::parse("file:f1~abc:R"),
1107 Some(TokenScope::File { file_id: "f1~abc".to_string(), access: AccessLevel::Read })
1108 );
1109 }
1110}
1111
1112#[derive(Debug, Clone, Copy, Default)]
1118pub struct CompactReport {
1119 pub files: usize,
1120 pub bytes_before: u64,
1121 pub bytes_after: u64,
1122}
1123
1124