1use super::{
8 metadata::{Label, Metadata},
9 serialization_helpers::{ByteUnit, DefaultFactorOne, TimeUnit},
10};
11use crate::proto::gevulot::gevulot;
12use serde::{Deserialize, Serialize};
13
14#[derive(Serialize, Deserialize, Debug)]
68pub struct Pin {
69 pub kind: String,
70 pub version: String,
71 #[serde(default)]
72 pub metadata: Metadata,
73 pub spec: PinSpec,
74 pub status: Option<PinStatus>,
75}
76
77impl From<gevulot::Pin> for Pin {
78 fn from(proto: gevulot::Pin) -> Self {
79 let mut spec: PinSpec = proto.spec.unwrap().into();
80 spec.cid = proto
81 .status
82 .as_ref()
83 .map(|s| s.cid.clone())
84 .or_else(|| proto.metadata.as_ref().map(|m| m.id.clone()));
85 Pin {
86 kind: "Pin".to_string(),
87 version: "v0".to_string(),
88 metadata: Metadata {
89 id: proto.metadata.as_ref().map(|m| m.id.clone()),
90 name: proto
91 .metadata
92 .as_ref()
93 .map(|m| m.name.clone())
94 .unwrap_or_default(),
95 creator: proto.metadata.as_ref().map(|m| m.creator.clone()),
96 description: proto
97 .metadata
98 .as_ref()
99 .map(|m| m.desc.clone())
100 .unwrap_or_default(),
101 tags: proto
102 .metadata
103 .as_ref()
104 .map(|m| m.tags.clone())
105 .unwrap_or_default(),
106 labels: proto
107 .metadata
108 .as_ref()
109 .map(|m| m.labels.clone())
110 .unwrap_or_default()
111 .into_iter()
112 .map(|l| Label {
113 key: l.key,
114 value: l.value,
115 })
116 .collect(),
117 workflow_ref: None,
118 },
119 status: proto.status.map(|s| s.into()),
120 spec,
121 }
122 }
123}
124
125#[derive(Serialize, Debug)]
144pub struct PinSpec {
145 #[serde(default)]
146 pub cid: Option<String>,
147 pub bytes: ByteUnit<DefaultFactorOne>,
148 pub time: TimeUnit,
149 pub redundancy: i64,
150 #[serde(rename = "fallbackUrls", default)]
151 pub fallback_urls: Option<Vec<String>>,
152}
153
154impl<'de> Deserialize<'de> for PinSpec {
155 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
156 where
157 D: serde::Deserializer<'de>,
158 {
159 #[derive(Deserialize)]
161 struct PinSpecHelper {
162 #[serde(default)]
163 cid: Option<String>,
164 bytes: ByteUnit,
165 time: TimeUnit,
166 redundancy: Option<i64>,
167 #[serde(rename = "fallbackUrls", default)]
168 fallback_urls: Option<Vec<String>>,
169 }
170
171 let helper = PinSpecHelper::deserialize(deserializer)?;
173
174 if helper.cid.is_none() {
176 match &helper.fallback_urls {
178 None => {
179 return Err(serde::de::Error::custom(
180 "Either cid or fallbackUrls must be specified",
181 ))
182 }
183 Some(urls) if urls.is_empty() => {
184 return Err(serde::de::Error::custom(
185 "fallbackUrls must contain at least one URL when no cid is specified",
186 ))
187 }
188 _ => {}
189 }
190 }
191
192 let redundancy = helper.redundancy.unwrap_or(1);
193 Ok(PinSpec {
195 cid: helper.cid,
196 bytes: helper.bytes,
197 time: helper.time,
198 redundancy,
199 fallback_urls: helper.fallback_urls,
200 })
201 }
202}
203
204impl From<gevulot::PinSpec> for PinSpec {
205 fn from(proto: gevulot::PinSpec) -> Self {
206 PinSpec {
207 cid: None,
208 bytes: (proto.bytes as i64).into(),
209 time: (proto.time as i64).into(),
210 redundancy: proto.redundancy as i64,
211 fallback_urls: Some(proto.fallback_urls),
212 }
213 }
214}
215
216#[derive(Serialize, Deserialize, Debug)]
239pub struct PinStatus {
240 #[serde(rename = "assignedWorkers", default)]
241 pub assigned_workers: Vec<String>,
242 #[serde(rename = "workerAcks", default)]
243 pub worker_acks: Vec<PinAck>,
244 pub cid: Option<String>,
245}
246
247impl From<gevulot::PinStatus> for PinStatus {
248 fn from(proto: gevulot::PinStatus) -> Self {
249 PinStatus {
250 assigned_workers: proto.assigned_workers,
251 worker_acks: proto
252 .worker_acks
253 .into_iter()
254 .map(|a| PinAck {
255 worker: a.worker,
256 block_height: a.block_height as i64,
257 success: a.success,
258 error: if a.error.is_empty() {
259 None
260 } else {
261 Some(a.error)
262 },
263 })
264 .collect(),
265 cid: Some(proto.cid),
266 }
267 }
268}
269
270#[derive(Serialize, Deserialize, Debug)]
287pub struct PinAck {
288 pub worker: String,
289 #[serde(rename = "blockHeight")]
290 pub block_height: i64,
291 pub success: bool,
292 pub error: Option<String>,
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use serde_json::json;
299
300 #[test]
301 fn test_parse_pin() {
302 let pin = serde_json::from_value::<Pin>(json!({
303 "kind": "Pin",
304 "version": "v0",
305 "metadata": {
306 "name": "Test Pin",
307 "creator": "test",
308 "description": "Test Pin Description",
309 "tags": ["tag1", "tag2"],
310 "labels": [
311 {
312 "key": "label1",
313 "value": "value1"
314 },
315 {
316 "key": "label2",
317 "value": "value2"
318 }
319 ],
320 "workflowRef": "test-workflow"
321 },
322 "spec": {
323 "cid": "test-cid",
324 "bytes": "1234KiB",
325 "time": "24h",
326 "redundancy": 3,
327 "fallbackUrls": ["url1", "url2"]
328 },
329 "status": {
330 "assignedWorkers": ["worker1", "worker2"],
331 "workerAcks": [
332 {
333 "worker": "worker1",
334 "blockHeight": 1000,
335 "success": true,
336 "error": null
337 },
338 {
339 "worker": "worker2",
340 "blockHeight": 1001,
341 "success": false,
342 "error": "Failed to pin"
343 }
344 ],
345 "cid": "test-cid"
346 }
347 }))
348 .unwrap();
349
350 assert_eq!(pin.kind, "Pin");
352 assert_eq!(pin.version, "v0");
353 assert_eq!(pin.metadata.name, "Test Pin");
354 assert_eq!(pin.metadata.creator, Some("test".to_string()));
355 assert_eq!(pin.metadata.description, "Test Pin Description");
356 assert_eq!(pin.metadata.tags, vec!["tag1", "tag2"]);
357 assert_eq!(pin.metadata.labels.len(), 2);
358 assert_eq!(pin.metadata.labels[0].key, "label1");
359 assert_eq!(pin.metadata.labels[0].value, "value1");
360 assert_eq!(pin.metadata.workflow_ref, Some("test-workflow".to_string()));
361
362 assert_eq!(pin.spec.cid, Some("test-cid".to_string()));
364 assert_eq!(pin.spec.bytes.bytes(), Ok(1234 * 1024));
365 assert_eq!(pin.spec.time.seconds(), Ok(24 * 60 * 60));
366 assert_eq!(pin.spec.redundancy, 3);
367 assert_eq!(
368 pin.spec.fallback_urls,
369 Some(vec!["url1".to_string(), "url2".to_string()])
370 );
371
372 let status = pin.status.unwrap();
374 assert_eq!(status.assigned_workers, vec!["worker1", "worker2"]);
375 assert_eq!(status.worker_acks.len(), 2);
376 assert_eq!(status.worker_acks[0].worker, "worker1");
377 assert_eq!(status.worker_acks[0].block_height, 1000);
378 assert_eq!(status.worker_acks[0].success, true);
379 assert_eq!(status.worker_acks[0].error, None);
380 assert_eq!(status.cid, Some("test-cid".to_string()));
381 }
382
383 #[test]
384 fn test_parse_pin_with_the_bare_minimum() {
385 let pin = serde_json::from_value::<Pin>(json!({
386 "kind": "Pin",
387 "version": "v0",
388 "spec": {
389 "cid": "test-cid",
390 "bytes": "1234KiB",
391 "time": "24h",
392 }
393 }))
394 .unwrap();
395
396 assert_eq!(pin.spec.cid, Some("test-cid".to_string()));
397 assert_eq!(pin.spec.bytes.bytes(), Ok(1234 * 1024));
398 assert_eq!(pin.spec.time.seconds(), Ok(24 * 60 * 60));
399 assert_eq!(pin.spec.redundancy, 1);
400 assert_eq!(pin.spec.fallback_urls, None);
401 }
402
403 #[test]
404 fn test_pin_requires_cid_or_fallback_urls() {
405 let result = serde_json::from_value::<Pin>(json!({
407 "kind": "Pin",
408 "version": "v0",
409 "spec": {
410 "bytes": "1234KiB",
411 "time": "24h"
412 }
413 }));
414 assert!(result.is_err());
415
416 let result = serde_json::from_value::<Pin>(json!({
418 "kind": "Pin",
419 "version": "v0",
420 "spec": {
421 "cid": "test-cid",
422 "bytes": "1234KiB",
423 "time": "24h"
424 }
425 }));
426 assert!(result.is_ok());
427
428 let result = serde_json::from_value::<Pin>(json!({
430 "kind": "Pin",
431 "version": "v0",
432 "spec": {
433 "fallbackUrls": ["url1", "url2"],
434 "bytes": "1234KiB",
435 "time": "24h"
436 }
437 }));
438 assert!(result.is_ok());
439
440 let result = serde_json::from_value::<Pin>(json!({
442 "kind": "Pin",
443 "version": "v0",
444 "spec": {
445 "fallbackUrls": [],
446 "bytes": "1234KiB",
447 "time": "24h"
448 }
449 }));
450 assert!(result.is_err());
451 }
452
453 #[test]
454 fn test_pin_with_raw_bytes() {
455 let result = serde_json::from_value::<Pin>(json!({
457 "kind": "Pin",
458 "version": "v0",
459 "spec": {
460 "cid": "test-cid",
461 "bytes": 1234,
462 "time": "24h"
463 }
464 }));
465 assert!(result.is_ok());
466 let pin = result.unwrap();
467 assert_eq!(pin.spec.bytes.bytes(), Ok(1234));
468
469 let result = serde_json::from_value::<Pin>(json!({
471 "kind": "Pin",
472 "version": "v0",
473 "spec": {
474 "cid": "test-cid",
475 "bytes": "1234",
476 "time": "24h"
477 }
478 }));
479 assert!(result.is_ok());
480 let pin = result.unwrap();
481 assert_eq!(pin.spec.bytes.bytes(), Ok(1234));
482 }
483}