Skip to main content

guts_compat/
lib.rs

1//! # Guts Compatibility Layer
2//!
3//! Git and GitHub compatibility layer for the Guts code collaboration platform.
4//!
5//! This crate provides:
6//! - **User Accounts**: User registration and profile management
7//! - **Personal Access Tokens**: Token-based authentication for API and Git operations
8//! - **SSH Keys**: SSH key management for future SSH protocol support
9//! - **Releases**: Release and asset management
10//! - **Contents API**: Repository file browsing
11//! - **Archive Downloads**: Tarball and zipball generation
12//! - **Rate Limiting**: GitHub-compatible rate limiting
13//! - **Pagination**: GitHub-style Link header pagination
14//!
15//! ## Example
16//!
17//! ```rust
18//! use guts_compat::{CompatStore, TokenScope};
19//!
20//! // Create a store
21//! let store = CompatStore::new();
22//!
23//! // Create a user
24//! let user = store.users.create(
25//!     "alice".to_string(),
26//!     "ed25519_pubkey_hex".to_string(),
27//! ).unwrap();
28//!
29//! // Create a personal access token
30//! let (token, plaintext) = store.tokens.create(
31//!     user.id,
32//!     "CI/CD Token".to_string(),
33//!     vec![TokenScope::RepoRead, TokenScope::RepoWrite],
34//!     None, // No expiration
35//! ).unwrap();
36//!
37//! println!("Token: {}", plaintext);
38//!
39//! // Verify the token later
40//! let (user_id, scopes) = store.tokens.verify(&plaintext).unwrap();
41//! assert_eq!(user_id, user.id);
42//! ```
43//!
44//! ## Authentication
45//!
46//! Tokens can be used in several ways:
47//!
48//! ```text
49//! # Bearer token (recommended)
50//! curl -H "Authorization: Bearer guts_abc12345_XXXXX" https://api.guts.network/user
51//!
52//! # Token header (GitHub-style)
53//! curl -H "Authorization: token guts_abc12345_XXXXX" https://api.guts.network/user
54//!
55//! # Basic auth (username:token)
56//! curl -u "alice:guts_abc12345_XXXXX" https://api.guts.network/user
57//! ```
58//!
59//! ## Rate Limiting
60//!
61//! All API responses include rate limit headers:
62//!
63//! ```text
64//! X-RateLimit-Limit: 5000
65//! X-RateLimit-Remaining: 4999
66//! X-RateLimit-Reset: 1234567890
67//! X-RateLimit-Used: 1
68//! X-RateLimit-Resource: core
69//! ```
70
71pub mod archive;
72pub mod contents;
73pub mod error;
74pub mod middleware;
75pub mod pagination;
76pub mod rate_limit;
77pub mod release;
78pub mod ssh_key;
79pub mod store;
80pub mod token;
81pub mod user;
82
83// Re-export main types
84pub use archive::{create_archive, ArchiveEntry, ArchiveFormat, TarGzBuilder, ZipBuilder};
85pub use contents::{
86    base64_encode, detect_spdx_id, is_readme_file, recognize_license_file, ContentEntry,
87    ContentType, ContentsQuery, LicenseResponse, ReadmeResponse,
88};
89pub use error::{CompatError, Result};
90pub use middleware::{
91    parse_authorization_header, resource_from_path, AuthContext, AuthorizationValue, ErrorResponse,
92    ResponseHeaders, ValidationError, ValidationErrorCode,
93};
94pub use pagination::{
95    paginate, PaginatedResponse, PaginationLinks, PaginationParams, DEFAULT_PER_PAGE, MAX_PER_PAGE,
96};
97pub use rate_limit::{
98    RateLimitHeaders, RateLimitInfo, RateLimitResource, RateLimitResources, RateLimitResponse,
99    RateLimitState, RateLimiter, DEFAULT_RATE_LIMIT, UNAUTHENTICATED_RATE_LIMIT,
100};
101pub use release::{
102    AssetId, AssetResponse, AuthorInfo, CreateReleaseRequest, Release, ReleaseAsset, ReleaseId,
103    ReleaseResponse, UpdateReleaseRequest,
104};
105pub use ssh_key::{AddSshKeyRequest, SshKey, SshKeyId, SshKeyResponse, SshKeyType};
106pub use store::{CompatStats, CompatStore, ReleaseStore, SshKeyStore, TokenStore, UserStore};
107pub use token::{
108    CreateTokenRequest, PersonalAccessToken, TokenId, TokenResponse, TokenScope, TokenValue,
109};
110pub use user::{CreateUserRequest, UpdateUserRequest, User, UserId, UserProfile};
111
112/// Version of the compatibility layer.
113pub const VERSION: &str = env!("CARGO_PKG_VERSION");
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_public_api() {
121        // Verify main types are accessible
122        let store = CompatStore::new();
123        assert_eq!(store.users.count(), 0);
124        assert_eq!(store.tokens.count(), 0);
125    }
126
127    #[test]
128    fn test_full_user_token_flow() {
129        let store = CompatStore::new();
130
131        // Create user
132        let user = store
133            .users
134            .create("alice".to_string(), "pubkey123".to_string())
135            .unwrap();
136        assert_eq!(user.username, "alice");
137
138        // Create token
139        let (token, plaintext) = store
140            .tokens
141            .create(
142                user.id,
143                "My Token".to_string(),
144                vec![TokenScope::RepoRead, TokenScope::RepoWrite],
145                None,
146            )
147            .unwrap();
148
149        assert!(plaintext.starts_with("guts_"));
150
151        // Verify token
152        let (user_id, scopes) = store.tokens.verify(&plaintext).unwrap();
153        assert_eq!(user_id, user.id);
154        assert!(scopes.contains(&TokenScope::RepoRead));
155        assert!(scopes.contains(&TokenScope::RepoWrite));
156
157        // Check token has correct scopes
158        assert!(token.has_scope(TokenScope::RepoRead));
159        assert!(token.has_scope(TokenScope::RepoWrite));
160        assert!(!token.has_scope(TokenScope::Admin));
161    }
162
163    #[test]
164    fn test_rate_limiting() {
165        let limiter = RateLimiter::new();
166
167        // Authenticated users get higher limits
168        let state = limiter.get_state("user1", RateLimitResource::Core, true);
169        assert_eq!(state.limit, 5000);
170
171        // Unauthenticated users get lower limits
172        let state = limiter.get_state("anon", RateLimitResource::Core, false);
173        assert_eq!(state.limit, 60);
174    }
175
176    #[test]
177    fn test_pagination() {
178        let items: Vec<i32> = (1..=100).collect();
179        let params = PaginationParams::new(2, 10);
180        let response = paginate(&items, &params);
181
182        assert_eq!(response.items.len(), 10);
183        assert_eq!(response.total_count, 100);
184        assert_eq!(response.page, 2);
185        assert!(response.has_next_page());
186        assert!(response.has_prev_page());
187    }
188
189    #[test]
190    fn test_release_management() {
191        let store = CompatStore::new();
192
193        // Create a release
194        let release = store
195            .releases
196            .create(
197                "alice/repo".to_string(),
198                "v1.0.0".to_string(),
199                "main".to_string(),
200                "alice".to_string(),
201            )
202            .unwrap();
203
204        // Add an asset
205        let asset = store
206            .releases
207            .add_asset(
208                release.id,
209                "app-linux-amd64.tar.gz".to_string(),
210                "application/gzip".to_string(),
211                b"binary content".to_vec(),
212                "alice".to_string(),
213            )
214            .unwrap();
215
216        assert_eq!(asset.name, "app-linux-amd64.tar.gz");
217
218        // Get asset content
219        let content = store.releases.get_asset_content(&asset.content_hash);
220        assert!(content.is_some());
221    }
222
223    #[test]
224    fn test_archive_generation() {
225        let entries = vec![
226            ArchiveEntry::file("README.md".to_string(), b"# My Project".to_vec()),
227            ArchiveEntry::file("src/main.rs".to_string(), b"fn main() {}".to_vec()),
228        ];
229
230        let archive = create_archive(
231            ArchiveFormat::TarGz,
232            "my-project-v1.0.0".to_string(),
233            entries,
234        );
235        assert!(archive.is_ok());
236    }
237
238    #[test]
239    fn test_ssh_key_management() {
240        let store = CompatStore::new();
241
242        // Add an SSH key
243        let key = store.ssh_keys.add(
244            1,
245            "My Laptop".to_string(),
246            "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl user@laptop".to_string(),
247        ).unwrap();
248
249        assert!(key.fingerprint.starts_with("SHA256:"));
250        assert_eq!(key.key_type, SshKeyType::Ed25519);
251
252        // List keys
253        let keys = store.ssh_keys.list_for_user(1);
254        assert_eq!(keys.len(), 1);
255    }
256}