guts_types/
repository.rs

1//! Repository types for Guts.
2
3use commonware_cryptography::{Hasher, Sha256};
4use serde::{Deserialize, Serialize};
5
6/// A unique identifier for a repository.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub struct RepositoryId([u8; 32]);
9
10impl RepositoryId {
11    /// Creates a new repository ID from raw bytes.
12    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
13        Self(bytes)
14    }
15
16    /// Returns the raw bytes.
17    pub const fn as_bytes(&self) -> &[u8; 32] {
18        &self.0
19    }
20
21    /// Generates a repository ID from the repository name and owner.
22    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    /// Returns the ID as a hex string.
36    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/// Visibility of a repository.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
49#[serde(rename_all = "lowercase")]
50pub enum Visibility {
51    /// Public repository.
52    #[default]
53    Public,
54    /// Private repository.
55    Private,
56}
57
58/// A repository in the Guts network.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct Repository {
61    /// Unique identifier.
62    pub id: RepositoryId,
63    /// Human-readable name.
64    pub name: String,
65    /// Owner's public key (hex encoded).
66    pub owner: String,
67    /// Optional description.
68    pub description: Option<String>,
69    /// Default branch name.
70    pub default_branch: String,
71    /// Repository visibility.
72    pub visibility: Visibility,
73    /// Creation timestamp (unix millis).
74    pub created_at: u64,
75}
76
77impl Repository {
78    /// Creates a new repository.
79    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    /// Sets the description.
99    pub fn with_description(mut self, description: impl Into<String>) -> Self {
100        self.description = Some(description.into());
101        self
102    }
103
104    /// Returns the full name (owner/name).
105    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); // 32 bytes = 64 hex chars
146        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        // Verify that repository IDs are deterministic SHA256 hashes
167        let id = RepositoryId::generate("test", "owner");
168        assert_eq!(id.as_bytes().len(), 32); // SHA256 produces 32 bytes
169    }
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); // Duplicate
242
243        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; // Copy
271        assert_eq!(id1, id2);
272    }
273}