1use commonware_cryptography::{Hasher, Sha256};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub struct RepositoryId([u8; 32]);
9
10impl RepositoryId {
11 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
13 Self(bytes)
14 }
15
16 pub const fn as_bytes(&self) -> &[u8; 32] {
18 &self.0
19 }
20
21 pub fn generate(name: &str, owner: &str) -> Self {
23 let mut hasher = Sha256::new();
24 hasher.update(name.as_bytes());
25 hasher.update(b":");
26 hasher.update(owner.as_bytes());
27 let digest = hasher.finalize();
28 let bytes: [u8; 32] = digest
29 .as_ref()
30 .try_into()
31 .expect("SHA256 produces 32 bytes");
32 Self(bytes)
33 }
34
35 pub fn to_hex(&self) -> String {
37 commonware_utils::hex(&self.0)
38 }
39}
40
41impl std::fmt::Display for RepositoryId {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 write!(f, "{}", self.to_hex())
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
49#[serde(rename_all = "lowercase")]
50pub enum Visibility {
51 #[default]
53 Public,
54 Private,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct Repository {
61 pub id: RepositoryId,
63 pub name: String,
65 pub owner: String,
67 pub description: Option<String>,
69 pub default_branch: String,
71 pub visibility: Visibility,
73 pub created_at: u64,
75}
76
77impl Repository {
78 pub fn new(name: impl Into<String>, owner: impl Into<String>) -> Self {
80 let name = name.into();
81 let owner = owner.into();
82 let id = RepositoryId::generate(&name, &owner);
83
84 Self {
85 id,
86 name,
87 owner,
88 description: None,
89 default_branch: "main".to_string(),
90 visibility: Visibility::Public,
91 created_at: std::time::SystemTime::now()
92 .duration_since(std::time::UNIX_EPOCH)
93 .unwrap_or_default()
94 .as_millis() as u64,
95 }
96 }
97
98 pub fn with_description(mut self, description: impl Into<String>) -> Self {
100 self.description = Some(description.into());
101 self
102 }
103
104 pub fn full_name(&self) -> String {
106 format!("{}/{}", self.owner, self.name)
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn test_repository_id_generation() {
116 let id1 = RepositoryId::generate("my-repo", "alice");
117 let id2 = RepositoryId::generate("my-repo", "alice");
118 let id3 = RepositoryId::generate("other-repo", "alice");
119
120 assert_eq!(id1, id2);
121 assert_ne!(id1, id3);
122 }
123
124 #[test]
125 fn test_repository_creation() {
126 let repo = Repository::new("test-repo", "user123");
127 assert_eq!(repo.name, "test-repo");
128 assert_eq!(repo.owner, "user123");
129 assert_eq!(repo.default_branch, "main");
130 assert_eq!(repo.full_name(), "user123/test-repo");
131 }
132
133 #[test]
134 fn test_repository_id_from_bytes() {
135 let bytes = [0u8; 32];
136 let id = RepositoryId::from_bytes(bytes);
137 assert_eq!(*id.as_bytes(), bytes);
138 }
139
140 #[test]
141 fn test_repository_id_to_hex() {
142 let bytes = [0xab; 32];
143 let id = RepositoryId::from_bytes(bytes);
144 let hex = id.to_hex();
145 assert_eq!(hex.len(), 64); assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
147 }
148
149 #[test]
150 fn test_repository_id_display() {
151 let id = RepositoryId::generate("repo", "owner");
152 let display = format!("{}", id);
153 let hex = id.to_hex();
154 assert_eq!(display, hex);
155 }
156
157 #[test]
158 fn test_repository_id_different_owners_same_name() {
159 let id1 = RepositoryId::generate("repo", "alice");
160 let id2 = RepositoryId::generate("repo", "bob");
161 assert_ne!(id1, id2);
162 }
163
164 #[test]
165 fn test_repository_id_is_hash() {
166 let id = RepositoryId::generate("test", "owner");
168 assert_eq!(id.as_bytes().len(), 32); }
170
171 #[test]
172 fn test_repository_visibility_default() {
173 let repo = Repository::new("test", "owner");
174 assert_eq!(repo.visibility, Visibility::Public);
175 }
176
177 #[test]
178 fn test_visibility_serde() {
179 let public: Visibility = serde_json::from_str(r#""public""#).unwrap();
180 let private: Visibility = serde_json::from_str(r#""private""#).unwrap();
181
182 assert_eq!(public, Visibility::Public);
183 assert_eq!(private, Visibility::Private);
184
185 assert_eq!(
186 serde_json::to_string(&Visibility::Public).unwrap(),
187 r#""public""#
188 );
189 assert_eq!(
190 serde_json::to_string(&Visibility::Private).unwrap(),
191 r#""private""#
192 );
193 }
194
195 #[test]
196 fn test_repository_with_description() {
197 let repo = Repository::new("test", "owner").with_description("A test repository");
198
199 assert_eq!(repo.description, Some("A test repository".to_string()));
200 }
201
202 #[test]
203 fn test_repository_created_at_is_set() {
204 let repo = Repository::new("test", "owner");
205 assert!(repo.created_at > 0);
206 }
207
208 #[test]
209 fn test_repository_serialization() {
210 let repo = Repository::new("test-repo", "alice").with_description("Test description");
211
212 let json = serde_json::to_string(&repo).unwrap();
213 let parsed: Repository = serde_json::from_str(&json).unwrap();
214
215 assert_eq!(parsed.name, repo.name);
216 assert_eq!(parsed.owner, repo.owner);
217 assert_eq!(parsed.description, repo.description);
218 assert_eq!(parsed.id, repo.id);
219 }
220
221 #[test]
222 fn test_repository_id_equality() {
223 let id1 = RepositoryId::from_bytes([1u8; 32]);
224 let id2 = RepositoryId::from_bytes([1u8; 32]);
225 let id3 = RepositoryId::from_bytes([2u8; 32]);
226
227 assert_eq!(id1, id2);
228 assert_ne!(id1, id3);
229 }
230
231 #[test]
232 fn test_repository_id_hash_trait() {
233 use std::collections::HashSet;
234
235 let id1 = RepositoryId::generate("repo1", "owner");
236 let id2 = RepositoryId::generate("repo2", "owner");
237
238 let mut set = HashSet::new();
239 set.insert(id1);
240 set.insert(id2);
241 set.insert(id1); assert_eq!(set.len(), 2);
244 }
245
246 #[test]
247 fn test_repository_full_name_format() {
248 let repo = Repository::new("my-project", "org-name");
249 assert_eq!(repo.full_name(), "org-name/my-project");
250 }
251
252 #[test]
253 fn test_repository_accepts_string_types() {
254 let repo1 = Repository::new(String::from("name"), String::from("owner"));
255 let repo2 = Repository::new("name", "owner");
256
257 assert_eq!(repo1.name, repo2.name);
258 assert_eq!(repo1.owner, repo2.owner);
259 }
260
261 #[test]
262 fn test_visibility_default_trait() {
263 let visibility: Visibility = Default::default();
264 assert_eq!(visibility, Visibility::Public);
265 }
266
267 #[test]
268 fn test_repository_id_copy_trait() {
269 let id1 = RepositoryId::generate("repo", "owner");
270 let id2 = id1; assert_eq!(id1, id2);
272 }
273}