| 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