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