| 1 | //! Row types shared across crates. |
| 2 | //! |
| 3 | //! Only the types more than one crate needs live here. Query-specific shapes |
| 4 | //! stay next to their queries. |
| 5 | |
| 6 | use chrono::{DateTime, Utc}; |
| 7 | use serde::{Deserialize, Serialize}; |
| 8 | use uuid::Uuid; |
| 9 | |
| 10 | #[derive(Debug, Clone, sqlx::FromRow)] |
| 11 | pub struct User { |
| 12 | pub id: Uuid, |
| 13 | pub subject: String, |
| 14 | pub handle: String, |
| 15 | pub display_name: Option<String>, |
| 16 | pub email: Option<String>, |
| 17 | pub avatar_url: Option<String>, |
| 18 | pub is_admin: bool, |
| 19 | pub created_at: DateTime<Utc>, |
| 20 | } |
| 21 | |
| 22 | impl User { |
| 23 | /// What to show in the UI: the display name if we have one, else the handle. |
| 24 | pub fn label(&self) -> &str { |
| 25 | self.display_name |
| 26 | .as_deref() |
| 27 | .filter(|s| !s.trim().is_empty()) |
| 28 | .unwrap_or(&self.handle) |
| 29 | } |
| 30 | } |
| 31 | |
| 32 | #[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type, Serialize, Deserialize)] |
| 33 | #[sqlx(type_name = "owner_kind", rename_all = "lowercase")] |
| 34 | #[serde(rename_all = "lowercase")] |
| 35 | pub enum OwnerKind { |
| 36 | User, |
| 37 | Org, |
| 38 | } |
| 39 | |
| 40 | #[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type, Serialize, Deserialize)] |
| 41 | #[sqlx(type_name = "visibility", rename_all = "lowercase")] |
| 42 | #[serde(rename_all = "lowercase")] |
| 43 | pub enum Visibility { |
| 44 | Public, |
| 45 | Private, |
| 46 | } |
| 47 | |
| 48 | /// Repository access level. |
| 49 | /// |
| 50 | /// Ordered least- to most-privileged; `PartialOrd` is derived from that order so |
| 51 | /// permission checks read as `role >= RepoRole::Write`. |
| 52 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, sqlx::Type, Serialize, Deserialize)] |
| 53 | #[sqlx(type_name = "repo_role", rename_all = "lowercase")] |
| 54 | #[serde(rename_all = "lowercase")] |
| 55 | pub enum RepoRole { |
| 56 | Read, |
| 57 | Write, |
| 58 | Maintain, |
| 59 | Admin, |
| 60 | } |
| 61 | |
| 62 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, sqlx::Type, Serialize, Deserialize)] |
| 63 | #[sqlx(type_name = "org_role", rename_all = "lowercase")] |
| 64 | #[serde(rename_all = "lowercase")] |
| 65 | pub enum OrgRole { |
| 66 | Member, |
| 67 | Admin, |
| 68 | } |
| 69 | |
| 70 | #[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type, Serialize, Deserialize)] |
| 71 | #[sqlx(type_name = "change_state", rename_all = "lowercase")] |
| 72 | #[serde(rename_all = "lowercase")] |
| 73 | pub enum ChangeState { |
| 74 | Draft, |
| 75 | Open, |
| 76 | Merged, |
| 77 | Abandoned, |
| 78 | } |
| 79 | |
| 80 | #[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type, Serialize, Deserialize)] |
| 81 | #[sqlx(type_name = "issue_state", rename_all = "lowercase")] |
| 82 | #[serde(rename_all = "lowercase")] |
| 83 | pub enum IssueState { |
| 84 | Open, |
| 85 | Closed, |
| 86 | } |
| 87 | |
| 88 | #[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type, Serialize, Deserialize)] |
| 89 | #[sqlx(type_name = "anchor_state", rename_all = "lowercase")] |
| 90 | #[serde(rename_all = "lowercase")] |
| 91 | pub enum AnchorState { |
| 92 | Current, |
| 93 | Outdated, |
| 94 | Orphaned, |
| 95 | } |
| 96 | |
| 97 | #[derive(Debug, Clone, sqlx::FromRow)] |
| 98 | pub struct Repo { |
| 99 | pub id: Uuid, |
| 100 | pub owner_kind: OwnerKind, |
| 101 | pub owner_user_id: Option<Uuid>, |
| 102 | pub owner_org_id: Option<Uuid>, |
| 103 | pub name: String, |
| 104 | pub description: Option<String>, |
| 105 | pub visibility: Visibility, |
| 106 | pub default_bookmark: String, |
| 107 | pub fork_of_repo_id: Option<Uuid>, |
| 108 | pub size_bytes: i64, |
| 109 | pub pushed_at: Option<DateTime<Utc>>, |
| 110 | pub archived: bool, |
| 111 | pub created_at: DateTime<Utc>, |
| 112 | } |
| 113 | |
| 114 | impl Repo { |
| 115 | pub fn is_public(&self) -> bool { |
| 116 | matches!(self.visibility, Visibility::Public) |
| 117 | } |
| 118 | |
| 119 | /// Whether this repo has ever received a push. An empty repo shows clone |
| 120 | /// instructions rather than a file listing. |
| 121 | pub fn is_empty(&self) -> bool { |
| 122 | self.pushed_at.is_none() |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | #[derive(Debug, Clone)] |
| 127 | pub struct Session { |
| 128 | pub id: Uuid, |
| 129 | pub user_id: Uuid, |
| 130 | pub expires_at: DateTime<Utc>, |
| 131 | } |
| 132 | |
| 133 | #[cfg(test)] |
| 134 | mod tests { |
| 135 | use super::*; |
| 136 | |
| 137 | #[test] |
| 138 | fn repo_roles_are_ordered_by_privilege() { |
| 139 | // Permission checks rely on this ordering, so it is worth pinning. |
| 140 | assert!(RepoRole::Read < RepoRole::Write); |
| 141 | assert!(RepoRole::Write < RepoRole::Maintain); |
| 142 | assert!(RepoRole::Maintain < RepoRole::Admin); |
| 143 | assert_eq!( |
| 144 | RepoRole::Admin.max(RepoRole::Read), |
| 145 | RepoRole::Admin, |
| 146 | "effective role is the maximum of the roles a user holds" |
| 147 | ); |
| 148 | } |
| 149 | |
| 150 | #[test] |
| 151 | fn user_label_falls_back_to_handle() { |
| 152 | let mut u = User { |
| 153 | id: Uuid::nil(), |
| 154 | subject: "s".into(), |
| 155 | handle: "alice".into(), |
| 156 | display_name: None, |
| 157 | email: None, |
| 158 | avatar_url: None, |
| 159 | is_admin: false, |
| 160 | created_at: Utc::now(), |
| 161 | }; |
| 162 | assert_eq!(u.label(), "alice"); |
| 163 | u.display_name = Some(" ".into()); |
| 164 | assert_eq!(u.label(), "alice", "blank display names must not be shown"); |
| 165 | u.display_name = Some("Alice Liddell".into()); |
| 166 | assert_eq!(u.label(), "Alice Liddell"); |
| 167 | } |
| 168 | } |
168 lines · Rust