gemini_adk_fluent_rs/compose/
artifacts.rs

1//! A — Artifact composition.
2//!
3//! Compose artifact schemas and transforms with `+`.
4
5use std::sync::Arc;
6
7use serde::{Deserialize, Serialize};
8
9/// An artifact schema describing expected artifact structure.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ArtifactSchema {
12    /// Artifact name/key.
13    pub name: String,
14    /// MIME type.
15    pub mime_type: String,
16    /// Description of what this artifact contains.
17    pub description: String,
18}
19
20/// An artifact transform — a pipeline step that produces or consumes artifacts.
21#[derive(Debug, Clone)]
22pub struct ArtifactTransform {
23    /// Artifacts consumed (input).
24    pub inputs: Vec<ArtifactSchema>,
25    /// Artifacts produced (output).
26    pub outputs: Vec<ArtifactSchema>,
27}
28
29impl ArtifactTransform {
30    /// Create a transform that only produces artifacts.
31    pub fn produces(schemas: Vec<ArtifactSchema>) -> Self {
32        Self {
33            inputs: Vec::new(),
34            outputs: schemas,
35        }
36    }
37
38    /// Create a transform that only consumes artifacts.
39    pub fn consumes(schemas: Vec<ArtifactSchema>) -> Self {
40        Self {
41            inputs: schemas,
42            outputs: Vec::new(),
43        }
44    }
45
46    /// Number of input + output schemas.
47    pub fn len(&self) -> usize {
48        self.inputs.len() + self.outputs.len()
49    }
50
51    /// Whether empty.
52    pub fn is_empty(&self) -> bool {
53        self.inputs.is_empty() && self.outputs.is_empty()
54    }
55}
56
57/// An artifact composite — multiple transforms composed together.
58#[derive(Debug, Clone)]
59pub struct ArtifactComposite {
60    /// The list of artifact transforms in this composite.
61    pub transforms: Vec<ArtifactTransform>,
62}
63
64impl ArtifactComposite {
65    /// Create from a single transform.
66    pub fn from_transform(transform: ArtifactTransform) -> Self {
67        Self {
68            transforms: vec![transform],
69        }
70    }
71
72    /// All input schemas across all transforms.
73    pub fn all_inputs(&self) -> Vec<&ArtifactSchema> {
74        self.transforms.iter().flat_map(|t| &t.inputs).collect()
75    }
76
77    /// All output schemas across all transforms.
78    pub fn all_outputs(&self) -> Vec<&ArtifactSchema> {
79        self.transforms.iter().flat_map(|t| &t.outputs).collect()
80    }
81
82    /// Total number of transforms.
83    pub fn len(&self) -> usize {
84        self.transforms.len()
85    }
86
87    /// Whether empty.
88    pub fn is_empty(&self) -> bool {
89        self.transforms.is_empty()
90    }
91}
92
93/// Compose two artifact composites with `+`.
94impl 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
103/// The `A` namespace — static factory methods for artifact composition.
104pub struct A;
105
106impl A {
107    /// Declare an artifact that this agent produces.
108    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    /// Declare an artifact that this agent consumes.
121    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    /// Declare a JSON artifact output.
134    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    /// Declare a JSON artifact input.
142    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    /// Declare a text artifact output.
150    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    /// Declare a text artifact input.
158    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    /// Publish an artifact with the given name and MIME type.
166    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    /// Save an artifact to storage.
174    pub fn save(name: impl Into<String>) -> ArtifactOp {
175        ArtifactOp::Save { name: name.into() }
176    }
177
178    /// Load an artifact from storage.
179    pub fn load(name: impl Into<String>) -> ArtifactOp {
180        ArtifactOp::Load { name: name.into() }
181    }
182
183    /// List available artifacts.
184    pub fn list() -> ArtifactOp {
185        ArtifactOp::List
186    }
187
188    /// Delete an artifact.
189    pub fn delete(name: impl Into<String>) -> ArtifactOp {
190        ArtifactOp::Delete { name: name.into() }
191    }
192
193    /// Get a specific version of an artifact.
194    pub fn version(name: impl Into<String>, version: u32) -> ArtifactOp {
195        ArtifactOp::Version {
196            name: name.into(),
197            version,
198        }
199    }
200
201    /// Convert an artifact to JSON format.
202    pub fn as_json(name: impl Into<String>) -> ArtifactOp {
203        ArtifactOp::AsJson { name: name.into() }
204    }
205
206    /// Convert an artifact to text format.
207    pub fn as_text(name: impl Into<String>) -> ArtifactOp {
208        ArtifactOp::AsText { name: name.into() }
209    }
210
211    /// Create an artifact from a JSON string.
212    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    /// Create an artifact from a text string.
220    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    /// Conditional artifact operation — executes `inner` only when `predicate` returns true.
228    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/// A runtime artifact operation.
240///
241/// These represent deferred operations on artifacts that can be composed
242/// into pipelines using the `+` operator.
243#[derive(Clone)]
244pub enum ArtifactOp {
245    /// Publish an artifact with a given MIME type.
246    Publish {
247        /// Artifact name.
248        name: String,
249        /// MIME type.
250        mime_type: String,
251    },
252    /// Save an artifact to storage.
253    Save {
254        /// Artifact name.
255        name: String,
256    },
257    /// Load an artifact from storage.
258    Load {
259        /// Artifact name.
260        name: String,
261    },
262    /// List available artifacts.
263    List,
264    /// Delete an artifact.
265    Delete {
266        /// Artifact name.
267        name: String,
268    },
269    /// Get a specific version of an artifact.
270    Version {
271        /// Artifact name.
272        name: String,
273        /// Version number.
274        version: u32,
275    },
276    /// Convert an artifact to JSON format.
277    AsJson {
278        /// Artifact name.
279        name: String,
280    },
281    /// Convert an artifact to text format.
282    AsText {
283        /// Artifact name.
284        name: String,
285    },
286    /// Create an artifact from a JSON string.
287    FromJson {
288        /// Artifact name.
289        name: String,
290        /// JSON data.
291        data: String,
292    },
293    /// Create an artifact from a text string.
294    FromText {
295        /// Artifact name.
296        name: String,
297        /// Text data.
298        data: String,
299    },
300    /// Conditional operation — execute inner only when predicate is true.
301    When {
302        /// Predicate function.
303        #[allow(clippy::type_complexity)]
304        predicate: Arc<dyn Fn() -> bool + Send + Sync>,
305        /// Inner operation to conditionally execute.
306        inner: Box<ArtifactOp>,
307    },
308    /// A sequence of operations composed with `+`.
309    Sequence(Vec<ArtifactOp>),
310}
311
312impl ArtifactOp {
313    /// Returns the artifact name associated with this operation, if any.
314    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    /// Returns true if this operation should execute (always true unless `When`).
332    pub fn should_execute(&self) -> bool {
333        match self {
334            ArtifactOp::When { predicate, .. } => predicate(),
335            _ => true,
336        }
337    }
338
339    /// Flatten this operation into a list of leaf operations.
340    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
379/// Compose two artifact operations with `+`.
380impl 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}