1use std::sync::Arc;
6
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ArtifactSchema {
12 pub name: String,
14 pub mime_type: String,
16 pub description: String,
18}
19
20#[derive(Debug, Clone)]
22pub struct ArtifactTransform {
23 pub inputs: Vec<ArtifactSchema>,
25 pub outputs: Vec<ArtifactSchema>,
27}
28
29impl ArtifactTransform {
30 pub fn produces(schemas: Vec<ArtifactSchema>) -> Self {
32 Self {
33 inputs: Vec::new(),
34 outputs: schemas,
35 }
36 }
37
38 pub fn consumes(schemas: Vec<ArtifactSchema>) -> Self {
40 Self {
41 inputs: schemas,
42 outputs: Vec::new(),
43 }
44 }
45
46 pub fn len(&self) -> usize {
48 self.inputs.len() + self.outputs.len()
49 }
50
51 pub fn is_empty(&self) -> bool {
53 self.inputs.is_empty() && self.outputs.is_empty()
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct ArtifactComposite {
60 pub transforms: Vec<ArtifactTransform>,
62}
63
64impl ArtifactComposite {
65 pub fn from_transform(transform: ArtifactTransform) -> Self {
67 Self {
68 transforms: vec![transform],
69 }
70 }
71
72 pub fn all_inputs(&self) -> Vec<&ArtifactSchema> {
74 self.transforms.iter().flat_map(|t| &t.inputs).collect()
75 }
76
77 pub fn all_outputs(&self) -> Vec<&ArtifactSchema> {
79 self.transforms.iter().flat_map(|t| &t.outputs).collect()
80 }
81
82 pub fn len(&self) -> usize {
84 self.transforms.len()
85 }
86
87 pub fn is_empty(&self) -> bool {
89 self.transforms.is_empty()
90 }
91}
92
93impl std::ops::Add for ArtifactComposite {
95 type Output = ArtifactComposite;
96
97 fn add(mut self, rhs: ArtifactComposite) -> Self::Output {
98 self.transforms.extend(rhs.transforms);
99 self
100 }
101}
102
103pub struct A;
105
106impl A {
107 pub fn output(
109 name: impl Into<String>,
110 mime_type: impl Into<String>,
111 description: impl Into<String>,
112 ) -> ArtifactComposite {
113 ArtifactComposite::from_transform(ArtifactTransform::produces(vec![ArtifactSchema {
114 name: name.into(),
115 mime_type: mime_type.into(),
116 description: description.into(),
117 }]))
118 }
119
120 pub fn input(
122 name: impl Into<String>,
123 mime_type: impl Into<String>,
124 description: impl Into<String>,
125 ) -> ArtifactComposite {
126 ArtifactComposite::from_transform(ArtifactTransform::consumes(vec![ArtifactSchema {
127 name: name.into(),
128 mime_type: mime_type.into(),
129 description: description.into(),
130 }]))
131 }
132
133 pub fn json_output(
135 name: impl Into<String>,
136 description: impl Into<String>,
137 ) -> ArtifactComposite {
138 Self::output(name, "application/json", description)
139 }
140
141 pub fn json_input(
143 name: impl Into<String>,
144 description: impl Into<String>,
145 ) -> ArtifactComposite {
146 Self::input(name, "application/json", description)
147 }
148
149 pub fn text_output(
151 name: impl Into<String>,
152 description: impl Into<String>,
153 ) -> ArtifactComposite {
154 Self::output(name, "text/plain", description)
155 }
156
157 pub fn text_input(
159 name: impl Into<String>,
160 description: impl Into<String>,
161 ) -> ArtifactComposite {
162 Self::input(name, "text/plain", description)
163 }
164
165 pub fn publish(name: impl Into<String>, mime_type: impl Into<String>) -> ArtifactOp {
167 ArtifactOp::Publish {
168 name: name.into(),
169 mime_type: mime_type.into(),
170 }
171 }
172
173 pub fn save(name: impl Into<String>) -> ArtifactOp {
175 ArtifactOp::Save { name: name.into() }
176 }
177
178 pub fn load(name: impl Into<String>) -> ArtifactOp {
180 ArtifactOp::Load { name: name.into() }
181 }
182
183 pub fn list() -> ArtifactOp {
185 ArtifactOp::List
186 }
187
188 pub fn delete(name: impl Into<String>) -> ArtifactOp {
190 ArtifactOp::Delete { name: name.into() }
191 }
192
193 pub fn version(name: impl Into<String>, version: u32) -> ArtifactOp {
195 ArtifactOp::Version {
196 name: name.into(),
197 version,
198 }
199 }
200
201 pub fn as_json(name: impl Into<String>) -> ArtifactOp {
203 ArtifactOp::AsJson { name: name.into() }
204 }
205
206 pub fn as_text(name: impl Into<String>) -> ArtifactOp {
208 ArtifactOp::AsText { name: name.into() }
209 }
210
211 pub fn from_json(name: impl Into<String>, data: impl Into<String>) -> ArtifactOp {
213 ArtifactOp::FromJson {
214 name: name.into(),
215 data: data.into(),
216 }
217 }
218
219 pub fn from_text(name: impl Into<String>, data: impl Into<String>) -> ArtifactOp {
221 ArtifactOp::FromText {
222 name: name.into(),
223 data: data.into(),
224 }
225 }
226
227 pub fn when(
229 predicate: impl Fn() -> bool + Send + Sync + 'static,
230 inner: ArtifactOp,
231 ) -> ArtifactOp {
232 ArtifactOp::When {
233 predicate: Arc::new(predicate),
234 inner: Box::new(inner),
235 }
236 }
237}
238
239#[derive(Clone)]
244pub enum ArtifactOp {
245 Publish {
247 name: String,
249 mime_type: String,
251 },
252 Save {
254 name: String,
256 },
257 Load {
259 name: String,
261 },
262 List,
264 Delete {
266 name: String,
268 },
269 Version {
271 name: String,
273 version: u32,
275 },
276 AsJson {
278 name: String,
280 },
281 AsText {
283 name: String,
285 },
286 FromJson {
288 name: String,
290 data: String,
292 },
293 FromText {
295 name: String,
297 data: String,
299 },
300 When {
302 #[allow(clippy::type_complexity)]
304 predicate: Arc<dyn Fn() -> bool + Send + Sync>,
305 inner: Box<ArtifactOp>,
307 },
308 Sequence(Vec<ArtifactOp>),
310}
311
312impl ArtifactOp {
313 pub fn name(&self) -> Option<&str> {
315 match self {
316 ArtifactOp::Publish { name, .. }
317 | ArtifactOp::Save { name }
318 | ArtifactOp::Load { name }
319 | ArtifactOp::Delete { name }
320 | ArtifactOp::Version { name, .. }
321 | ArtifactOp::AsJson { name }
322 | ArtifactOp::AsText { name }
323 | ArtifactOp::FromJson { name, .. }
324 | ArtifactOp::FromText { name, .. } => Some(name),
325 ArtifactOp::List => None,
326 ArtifactOp::When { inner, .. } => inner.name(),
327 ArtifactOp::Sequence(_) => None,
328 }
329 }
330
331 pub fn should_execute(&self) -> bool {
333 match self {
334 ArtifactOp::When { predicate, .. } => predicate(),
335 _ => true,
336 }
337 }
338
339 pub fn flatten(&self) -> Vec<&ArtifactOp> {
341 match self {
342 ArtifactOp::Sequence(ops) => ops.iter().flat_map(|op| op.flatten()).collect(),
343 other => vec![other],
344 }
345 }
346}
347
348impl std::fmt::Debug for ArtifactOp {
349 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
350 match self {
351 ArtifactOp::Publish { name, mime_type } => f
352 .debug_struct("Publish")
353 .field("name", name)
354 .field("mime_type", mime_type)
355 .finish(),
356 ArtifactOp::Save { name } => f.debug_struct("Save").field("name", name).finish(),
357 ArtifactOp::Load { name } => f.debug_struct("Load").field("name", name).finish(),
358 ArtifactOp::List => write!(f, "List"),
359 ArtifactOp::Delete { name } => f.debug_struct("Delete").field("name", name).finish(),
360 ArtifactOp::Version { name, version } => f
361 .debug_struct("Version")
362 .field("name", name)
363 .field("version", version)
364 .finish(),
365 ArtifactOp::AsJson { name } => f.debug_struct("AsJson").field("name", name).finish(),
366 ArtifactOp::AsText { name } => f.debug_struct("AsText").field("name", name).finish(),
367 ArtifactOp::FromJson { name, .. } => {
368 f.debug_struct("FromJson").field("name", name).finish()
369 }
370 ArtifactOp::FromText { name, .. } => {
371 f.debug_struct("FromText").field("name", name).finish()
372 }
373 ArtifactOp::When { inner, .. } => f.debug_struct("When").field("inner", inner).finish(),
374 ArtifactOp::Sequence(ops) => f.debug_struct("Sequence").field("ops", ops).finish(),
375 }
376 }
377}
378
379impl std::ops::Add for ArtifactOp {
381 type Output = ArtifactOp;
382
383 fn add(self, rhs: ArtifactOp) -> Self::Output {
384 match self {
385 ArtifactOp::Sequence(mut ops) => {
386 match rhs {
387 ArtifactOp::Sequence(rhs_ops) => ops.extend(rhs_ops),
388 other => ops.push(other),
389 }
390 ArtifactOp::Sequence(ops)
391 }
392 other => match rhs {
393 ArtifactOp::Sequence(mut rhs_ops) => {
394 rhs_ops.insert(0, other);
395 ArtifactOp::Sequence(rhs_ops)
396 }
397 rhs_other => ArtifactOp::Sequence(vec![other, rhs_other]),
398 },
399 }
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 #[test]
408 fn artifact_schema() {
409 let schema = ArtifactSchema {
410 name: "report".into(),
411 mime_type: "application/json".into(),
412 description: "Analysis report".into(),
413 };
414 assert_eq!(schema.name, "report");
415 }
416
417 #[test]
418 fn artifact_transform_produces() {
419 let t = ArtifactTransform::produces(vec![ArtifactSchema {
420 name: "output".into(),
421 mime_type: "text/plain".into(),
422 description: "Result".into(),
423 }]);
424 assert_eq!(t.outputs.len(), 1);
425 assert!(t.inputs.is_empty());
426 assert_eq!(t.len(), 1);
427 }
428
429 #[test]
430 fn artifact_transform_consumes() {
431 let t = ArtifactTransform::consumes(vec![ArtifactSchema {
432 name: "input".into(),
433 mime_type: "text/plain".into(),
434 description: "Source".into(),
435 }]);
436 assert!(t.outputs.is_empty());
437 assert_eq!(t.inputs.len(), 1);
438 }
439
440 #[test]
441 fn a_json_output() {
442 let comp = A::json_output("report", "Analysis results");
443 assert_eq!(comp.len(), 1);
444 let outputs = comp.all_outputs();
445 assert_eq!(outputs.len(), 1);
446 assert_eq!(outputs[0].mime_type, "application/json");
447 }
448
449 #[test]
450 fn a_text_input() {
451 let comp = A::text_input("source", "Source document");
452 let inputs = comp.all_inputs();
453 assert_eq!(inputs.len(), 1);
454 assert_eq!(inputs[0].mime_type, "text/plain");
455 }
456
457 #[test]
458 fn compose_with_add() {
459 let comp = A::json_output("report", "Report")
460 + A::text_input("source", "Source")
461 + A::json_output("summary", "Summary");
462 assert_eq!(comp.len(), 3);
463 assert_eq!(comp.all_inputs().len(), 1);
464 assert_eq!(comp.all_outputs().len(), 2);
465 }
466
467 #[test]
468 fn empty_composite() {
469 let comp = ArtifactComposite { transforms: vec![] };
470 assert!(comp.is_empty());
471 assert_eq!(comp.len(), 0);
472 }
473
474 #[test]
475 fn publish_op() {
476 let op = A::publish("report", "application/json");
477 assert_eq!(op.name(), Some("report"));
478 assert!(op.should_execute());
479 }
480
481 #[test]
482 fn save_and_load_ops() {
483 let save = A::save("report");
484 let load = A::load("report");
485 assert_eq!(save.name(), Some("report"));
486 assert_eq!(load.name(), Some("report"));
487 }
488
489 #[test]
490 fn list_op() {
491 let op = A::list();
492 assert_eq!(op.name(), None);
493 assert!(op.should_execute());
494 }
495
496 #[test]
497 fn delete_op() {
498 let op = A::delete("old_report");
499 assert_eq!(op.name(), Some("old_report"));
500 }
501
502 #[test]
503 fn version_op() {
504 let op = A::version("report", 3);
505 assert_eq!(op.name(), Some("report"));
506 if let ArtifactOp::Version { version, .. } = &op {
507 assert_eq!(*version, 3);
508 } else {
509 panic!("Expected Version variant");
510 }
511 }
512
513 #[test]
514 fn as_json_and_as_text() {
515 let json_op = A::as_json("data");
516 let text_op = A::as_text("data");
517 assert_eq!(json_op.name(), Some("data"));
518 assert_eq!(text_op.name(), Some("data"));
519 }
520
521 #[test]
522 fn from_json_and_from_text() {
523 let json_op = A::from_json("config", r#"{"key": "value"}"#);
524 let text_op = A::from_text("note", "hello world");
525 assert_eq!(json_op.name(), Some("config"));
526 assert_eq!(text_op.name(), Some("note"));
527 }
528
529 #[test]
530 fn when_op_true() {
531 let op = A::when(|| true, A::save("report"));
532 assert!(op.should_execute());
533 assert_eq!(op.name(), Some("report"));
534 }
535
536 #[test]
537 fn when_op_false() {
538 let op = A::when(|| false, A::save("report"));
539 assert!(!op.should_execute());
540 }
541
542 #[test]
543 fn compose_ops_with_add() {
544 let pipeline = A::load("source") + A::as_json("source") + A::save("output");
545 let ops = pipeline.flatten();
546 assert_eq!(ops.len(), 3);
547 }
548
549 #[test]
550 fn op_debug_format() {
551 let op = A::publish("report", "application/json");
552 let debug = format!("{:?}", op);
553 assert!(debug.contains("Publish"));
554 assert!(debug.contains("report"));
555 }
556}