1use std::collections::BTreeMap;
2use std::fmt;
3
4use serde::{Deserialize, Serialize};
5use shardline_protocol::{RepositoryProvider, SecretString, TokenScope};
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
9pub struct HealthResponse {
10 pub status: String,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
16pub struct ReadyResponse {
17 pub status: String,
19 pub server_role: String,
21 pub server_frontends: Vec<String>,
23 pub metadata_backend: String,
25 pub object_backend: String,
27 pub cache_backend: String,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33pub struct UploadChunkResult {
34 pub hash: String,
36 pub offset: u64,
38 pub length: u64,
40 pub inserted: bool,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
46pub struct UploadFileResponse {
47 pub file_id: String,
49 pub content_hash: String,
51 pub total_bytes: u64,
53 pub chunk_size: u64,
55 pub inserted_chunks: u64,
57 pub reused_chunks: u64,
59 pub stored_bytes: u64,
61 pub chunks: Vec<UploadChunkResult>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
67pub struct ServerStatsResponse {
68 pub chunks: u64,
70 pub chunk_bytes: u64,
72 pub files: u64,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
78pub struct ProviderTokenIssueRequest {
79 pub subject: String,
81 pub owner: String,
83 pub repo: String,
85 pub revision: Option<String>,
87 pub scope: TokenScope,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
93pub struct ProviderTokenIssueResponse {
94 pub token: SecretString,
96 pub issuer: String,
98 pub subject: String,
100 pub provider: RepositoryProvider,
102 pub owner: String,
104 pub repo: String,
106 pub revision: Option<String>,
108 pub scope: TokenScope,
110 pub expires_at_unix_seconds: u64,
112}
113
114#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
116#[serde(rename_all = "camelCase")]
117pub struct XetCasTokenResponse {
118 pub cas_url: String,
120 pub exp: u64,
122 pub access_token: String,
124}
125
126impl fmt::Debug for XetCasTokenResponse {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 f.debug_struct("XetCasTokenResponse")
129 .field("cas_url", &self.cas_url)
130 .field("exp", &self.exp)
131 .field("access_token", &"<redacted>")
132 .finish()
133 }
134}
135
136#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
138pub struct GitLfsAuthenticateResponse {
139 pub href: String,
141 pub header: BTreeMap<String, String>,
143 pub expires_in: u64,
145}
146
147impl fmt::Debug for GitLfsAuthenticateResponse {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 f.debug_struct("GitLfsAuthenticateResponse")
150 .field("href", &self.href)
151 .field("header", &"<redacted>")
152 .field("expires_in", &self.expires_in)
153 .finish()
154 }
155}
156
157#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
159pub struct OciRegistryTokenResponse {
160 pub token: String,
162 pub access_token: String,
164 pub expires_in: u64,
166}
167
168impl fmt::Debug for OciRegistryTokenResponse {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 f.debug_struct("OciRegistryTokenResponse")
171 .field("token", &"<redacted>")
172 .field("access_token", &"<redacted>")
173 .field("expires_in", &self.expires_in)
174 .finish()
175 }
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
180pub struct ProviderWebhookResponse {
181 pub provider: RepositoryProvider,
183 pub owner: String,
185 pub repo: String,
187 pub delivery_id: String,
189 pub event_kind: String,
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub new_owner: Option<String>,
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub new_repo: Option<String>,
197 #[serde(skip_serializing_if = "Option::is_none")]
199 pub revision: Option<String>,
200 pub affected_file_versions: u64,
202 pub affected_chunks: u64,
204 pub applied_holds: u64,
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub retention_seconds: Option<u64>,
209}
210
211#[cfg(test)]
212mod tests {
213 use std::collections::BTreeMap;
214
215 use shardline_protocol::{RepositoryProvider, SecretString, TokenScope};
216
217 use super::*;
218
219 fn health_response() -> HealthResponse {
220 HealthResponse {
221 status: "ok".to_owned(),
222 }
223 }
224
225 fn ready_response() -> ReadyResponse {
226 ReadyResponse {
227 status: "ok".to_owned(),
228 server_role: "all".to_owned(),
229 server_frontends: vec!["xet".to_owned()],
230 metadata_backend: "local".to_owned(),
231 object_backend: "local".to_owned(),
232 cache_backend: "memory".to_owned(),
233 }
234 }
235
236 #[test]
237 fn health_response_serde_round_trip() {
238 let original = health_response();
239 let json = serde_json::to_string(&original).unwrap();
240 let deserialized: HealthResponse = serde_json::from_str(&json).unwrap();
241 assert_eq!(original, deserialized);
242 assert!(json.contains("ok"));
243 }
244
245 #[test]
246 fn health_response_debug() {
247 let response = health_response();
248 let debug = format!("{response:?}");
249 assert!(debug.contains("HealthResponse"));
250 assert!(debug.contains("ok"));
251 }
252
253 #[test]
254 fn ready_response_serde_round_trip() {
255 let original = ready_response();
256 let json = serde_json::to_string(&original).unwrap();
257 let deserialized: ReadyResponse = serde_json::from_str(&json).unwrap();
258 assert_eq!(original, deserialized);
259 }
260
261 #[test]
262 fn ready_response_debug() {
263 let response = ready_response();
264 let debug = format!("{response:?}");
265 assert!(debug.contains("ReadyResponse"));
266 }
267
268 #[test]
269 fn upload_chunk_result_serde_round_trip() {
270 let original = UploadChunkResult {
271 hash: "abcdef123456".to_owned(),
272 offset: 0,
273 length: 4096,
274 inserted: true,
275 };
276 let json = serde_json::to_string(&original).unwrap();
277 let deserialized: UploadChunkResult = serde_json::from_str(&json).unwrap();
278 assert_eq!(original, deserialized);
279 }
280
281 #[test]
282 fn upload_file_response_serde_round_trip() {
283 let original = UploadFileResponse {
284 file_id: "test-file".to_owned(),
285 content_hash: "content-hash-abc".to_owned(),
286 total_bytes: 8192,
287 chunk_size: 4096,
288 inserted_chunks: 2,
289 reused_chunks: 0,
290 stored_bytes: 8192,
291 chunks: vec![UploadChunkResult {
292 hash: "chunk1".to_owned(),
293 offset: 0,
294 length: 4096,
295 inserted: true,
296 }],
297 };
298 let json = serde_json::to_string(&original).unwrap();
299 let deserialized: UploadFileResponse = serde_json::from_str(&json).unwrap();
300 assert_eq!(original, deserialized);
301 }
302
303 #[test]
304 fn server_stats_response_serde_round_trip() {
305 let original = ServerStatsResponse {
306 chunks: 10,
307 chunk_bytes: 40960,
308 files: 5,
309 };
310 let json = serde_json::to_string(&original).unwrap();
311 let deserialized: ServerStatsResponse = serde_json::from_str(&json).unwrap();
312 assert_eq!(original, deserialized);
313 }
314
315 #[test]
316 fn provider_token_issue_request_serde_round_trip() {
317 let original = ProviderTokenIssueRequest {
318 subject: "user".to_owned(),
319 owner: "org".to_owned(),
320 repo: "repo".to_owned(),
321 revision: Some("main".to_owned()),
322 scope: TokenScope::Read,
323 };
324 let json = serde_json::to_string(&original).unwrap();
325 let deserialized: ProviderTokenIssueRequest = serde_json::from_str(&json).unwrap();
326 assert_eq!(original, deserialized);
327 }
328
329 #[test]
330 fn provider_token_issue_response_serde_round_trip() {
331 let original = ProviderTokenIssueResponse {
332 token: SecretString::from_secret("bearer-token"),
333 issuer: "shardline".to_owned(),
334 subject: "user".to_owned(),
335 provider: RepositoryProvider::GitHub,
336 owner: "org".to_owned(),
337 repo: "repo".to_owned(),
338 revision: Some("main".to_owned()),
339 scope: TokenScope::Write,
340 expires_at_unix_seconds: 1_700_000_000,
341 };
342 let json = serde_json::to_string(&original).unwrap();
343 let deserialized: ProviderTokenIssueResponse = serde_json::from_str(&json).unwrap();
344 assert_eq!(original, deserialized);
345 }
346
347 #[test]
348 fn xet_cas_token_response_serde_round_trip() {
349 let original = XetCasTokenResponse {
350 cas_url: "http://cas.example.com".to_owned(),
351 exp: 1_700_000_000,
352 access_token: "access-token-value".to_owned(),
353 };
354 let json = serde_json::to_string(&original).unwrap();
355 let deserialized: XetCasTokenResponse = serde_json::from_str(&json).unwrap();
356 assert_eq!(original, deserialized);
357 assert!(json.contains("casUrl"));
359 assert!(json.contains("accessToken"));
360 }
361
362 #[test]
363 fn git_lfs_authenticate_response_serde_round_trip() {
364 let mut header = BTreeMap::new();
365 header.insert("X-Custom".to_owned(), "value".to_owned());
366 let original = GitLfsAuthenticateResponse {
367 href: "http://lfs.example.com".to_owned(),
368 header,
369 expires_in: 3600,
370 };
371 let json = serde_json::to_string(&original).unwrap();
372 let deserialized: GitLfsAuthenticateResponse = serde_json::from_str(&json).unwrap();
373 assert_eq!(original, deserialized);
374 }
375
376 #[test]
377 fn oci_registry_token_response_serde_round_trip() {
378 let original = OciRegistryTokenResponse {
379 token: "bearer-token".to_owned(),
380 access_token: "access-token".to_owned(),
381 expires_in: 300,
382 };
383 let json = serde_json::to_string(&original).unwrap();
384 let deserialized: OciRegistryTokenResponse = serde_json::from_str(&json).unwrap();
385 assert_eq!(original, deserialized);
386 }
387
388 #[test]
389 fn provider_webhook_response_serde_round_trip() {
390 let original = ProviderWebhookResponse {
391 provider: RepositoryProvider::GitHub,
392 owner: "org".to_owned(),
393 repo: "repo".to_owned(),
394 delivery_id: "delivery-123".to_owned(),
395 event_kind: "revision_pushed".to_owned(),
396 new_owner: None,
397 new_repo: None,
398 revision: Some("abc123".to_owned()),
399 affected_file_versions: 5,
400 affected_chunks: 20,
401 applied_holds: 3,
402 retention_seconds: Some(86_400),
403 };
404 let json = serde_json::to_string(&original).unwrap();
405 let deserialized: ProviderWebhookResponse = serde_json::from_str(&json).unwrap();
406 assert_eq!(original, deserialized);
407 }
408
409 #[test]
410 fn provider_webhook_response_omits_optional_empty_fields() {
411 let original = ProviderWebhookResponse {
412 provider: RepositoryProvider::GitHub,
413 owner: "org".to_owned(),
414 repo: "repo".to_owned(),
415 delivery_id: "delivery-123".to_owned(),
416 event_kind: "push".to_owned(),
417 new_owner: None,
418 new_repo: None,
419 revision: None,
420 affected_file_versions: 0,
421 affected_chunks: 0,
422 applied_holds: 0,
423 retention_seconds: None,
424 };
425 let json = serde_json::to_string(&original).unwrap();
426 assert!(
428 !json.contains("\"new_owner\""),
429 "unexpected new_owner: {json}"
430 );
431 assert!(
432 !json.contains("\"new_repo\""),
433 "unexpected new_repo: {json}"
434 );
435 assert!(
436 !json.contains("\"revision\""),
437 "unexpected revision: {json}"
438 );
439 assert!(
440 !json.contains("\"retention_seconds\""),
441 "unexpected retention_seconds: {json}"
442 );
443 }
444}