Skip to main content

standout_dispatch/
artifact.rs

1use serde::ser::SerializeStruct;
2use serde::{Serialize, Serializer};
3use std::path::{Path, PathBuf};
4const STDOUT_LABEL: &str = "-";
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum ArtifactDestination {
7    Stdout,
8    File(PathBuf),
9}
10impl ArtifactDestination {
11    pub fn path(&self) -> Option<&Path> {
12        match self {
13            ArtifactDestination::Stdout => None,
14            ArtifactDestination::File(path) => Some(path),
15        }
16    }
17    pub fn is_stdout(&self) -> bool {
18        matches!(self, ArtifactDestination::Stdout)
19    }
20    pub fn label(&self) -> String {
21        match self {
22            ArtifactDestination::Stdout => STDOUT_LABEL.to_string(),
23            ArtifactDestination::File(path) => path.display().to_string(),
24        }
25    }
26}
27impl Serialize for ArtifactDestination {
28    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
29        serializer.serialize_str(&self.label())
30    }
31}
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct ArtifactReceipt {
34    destination: ArtifactDestination,
35    byte_count: usize,
36}
37impl ArtifactReceipt {
38    pub fn new(destination: ArtifactDestination, byte_count: usize) -> Self {
39        Self {
40            destination,
41            byte_count,
42        }
43    }
44    pub fn destination(&self) -> &ArtifactDestination {
45        &self.destination
46    }
47    pub fn path(&self) -> Option<&Path> {
48        self.destination.path()
49    }
50    pub fn is_stdout(&self) -> bool {
51        self.destination.is_stdout()
52    }
53    pub fn byte_count(&self) -> usize {
54        self.byte_count
55    }
56}
57impl Serialize for ArtifactReceipt {
58    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
59        let mut state = serializer.serialize_struct("ArtifactReceipt", 3)?;
60        state.serialize_field("destination", &self.destination)?;
61        state.serialize_field("stdout", &self.destination.is_stdout())?;
62        state.serialize_field("byte_count", &self.byte_count)?;
63        state.end()
64    }
65}
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct Artifact<T> {
68    bytes: Vec<u8>,
69    suggested_destination: Option<PathBuf>,
70    stdout_fallback: bool,
71    report: Option<T>,
72}
73impl<T> Artifact<T> {
74    pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
75        Self {
76            bytes: bytes.into(),
77            suggested_destination: None,
78            stdout_fallback: false,
79            report: None,
80        }
81    }
82    pub fn suggest_destination(mut self, destination: impl Into<PathBuf>) -> Self {
83        self.suggested_destination = Some(destination.into());
84        self
85    }
86    pub fn allow_stdout(mut self) -> Self {
87        self.stdout_fallback = true;
88        self
89    }
90    pub fn with_report(mut self, report: T) -> Self {
91        self.report = Some(report);
92        self
93    }
94    pub fn bytes(&self) -> &[u8] {
95        &self.bytes
96    }
97    pub fn suggested_destination(&self) -> Option<&Path> {
98        self.suggested_destination.as_deref()
99    }
100    pub fn stdout_allowed(&self) -> bool {
101        self.stdout_fallback
102    }
103    pub fn report(&self) -> Option<&T> {
104        self.report.as_ref()
105    }
106    pub fn into_parts(self) -> (Vec<u8>, Option<PathBuf>, bool, Option<T>) {
107        (
108            self.bytes,
109            self.suggested_destination,
110            self.stdout_fallback,
111            self.report,
112        )
113    }
114}
115#[derive(Debug, Clone)]
116pub struct ArtifactRun {
117    bytes: Vec<u8>,
118    suggested_destination: Option<PathBuf>,
119    receipt: ArtifactReceipt,
120    report: Option<String>,
121}
122impl ArtifactRun {
123    pub fn new(
124        bytes: Vec<u8>,
125        suggested_destination: Option<PathBuf>,
126        receipt: ArtifactReceipt,
127        report: Option<String>,
128    ) -> Self {
129        Self {
130            bytes,
131            suggested_destination,
132            receipt,
133            report,
134        }
135    }
136    pub fn bytes(&self) -> &[u8] {
137        &self.bytes
138    }
139    pub fn suggested_destination(&self) -> Option<&Path> {
140        self.suggested_destination.as_deref()
141    }
142    pub fn receipt(&self) -> &ArtifactReceipt {
143        &self.receipt
144    }
145    pub fn destination(&self) -> &ArtifactDestination {
146        self.receipt.destination()
147    }
148    pub fn report(&self) -> Option<&str> {
149        self.report.as_deref()
150    }
151}
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use serde::Serialize;
156    #[derive(Serialize, Debug, PartialEq, Eq, Clone)]
157    struct Report {
158        entries: usize,
159    }
160    #[test]
161    fn artifact_defaults_to_no_destination_authorization() {
162        let artifact = Artifact::<Report>::new(vec![1, 2, 3]);
163        assert_eq!(artifact.bytes(), &[1, 2, 3]);
164        assert_eq!(artifact.suggested_destination(), None);
165        assert!(!artifact.stdout_allowed());
166        assert!(artifact.report().is_none());
167    }
168    #[test]
169    fn artifact_builder_records_every_opt_in() {
170        let artifact = Artifact::new(b"data".to_vec())
171            .suggest_destination("out.bin")
172            .allow_stdout()
173            .with_report(Report { entries: 2 });
174        assert_eq!(artifact.bytes(), b"data");
175        assert_eq!(artifact.suggested_destination(), Some(Path::new("out.bin")));
176        assert!(artifact.stdout_allowed());
177        assert_eq!(artifact.report(), Some(&Report { entries: 2 }));
178    }
179    #[test]
180    fn artifact_into_parts_round_trips() {
181        let artifact = Artifact::new(vec![7u8])
182            .suggest_destination("a.bin")
183            .with_report(Report { entries: 1 });
184        let (bytes, suggested, stdout, report) = artifact.into_parts();
185        assert_eq!(bytes, vec![7u8]);
186        assert_eq!(suggested, Some(PathBuf::from("a.bin")));
187        assert!(!stdout);
188        assert_eq!(report, Some(Report { entries: 1 }));
189    }
190    #[test]
191    fn file_destination_exposes_path() {
192        let dest = ArtifactDestination::File(PathBuf::from("/tmp/x.zip"));
193        assert_eq!(dest.path(), Some(Path::new("/tmp/x.zip")));
194        assert!(!dest.is_stdout());
195        assert_eq!(dest.label(), "/tmp/x.zip");
196    }
197    #[test]
198    fn stdout_destination_has_no_path_and_dash_label() {
199        let dest = ArtifactDestination::Stdout;
200        assert_eq!(dest.path(), None);
201        assert!(dest.is_stdout());
202        assert_eq!(dest.label(), "-");
203    }
204    #[test]
205    fn file_receipt_serializes_destination_and_count() {
206        let receipt = ArtifactReceipt::new(ArtifactDestination::File(PathBuf::from("/tmp/x")), 12);
207        let value = serde_json::to_value(&receipt).unwrap();
208        assert_eq!(
209            value,
210            serde_json::json!({"destination": "/tmp/x", "stdout": false, "byte_count": 12})
211        );
212        assert_eq!(receipt.path(), Some(Path::new("/tmp/x")));
213        assert_eq!(receipt.byte_count(), 12);
214        assert!(!receipt.is_stdout());
215    }
216    #[test]
217    fn stdout_receipt_serializes_dash_and_flag() {
218        let receipt = ArtifactReceipt::new(ArtifactDestination::Stdout, 3);
219        let value = serde_json::to_value(&receipt).unwrap();
220        assert_eq!(
221            value,
222            serde_json::json!({"destination": "-", "stdout": true, "byte_count": 3})
223        );
224        assert!(receipt.is_stdout());
225    }
226    #[test]
227    fn artifact_run_exposes_bytes_suggestion_receipt_and_report() {
228        let run = ArtifactRun::new(
229            vec![1, 2],
230            Some(PathBuf::from("s.bin")),
231            ArtifactReceipt::new(ArtifactDestination::File(PathBuf::from("o.bin")), 2),
232            Some("wrote 2 bytes".to_string()),
233        );
234        assert_eq!(run.bytes(), &[1, 2]);
235        assert_eq!(run.suggested_destination(), Some(Path::new("s.bin")));
236        assert_eq!(
237            run.destination(),
238            &ArtifactDestination::File(PathBuf::from("o.bin"))
239        );
240        assert_eq!(run.receipt().byte_count(), 2);
241        assert_eq!(run.report(), Some("wrote 2 bytes"));
242    }
243    #[test]
244    fn artifact_run_without_report_is_none() {
245        let run = ArtifactRun::new(
246            vec![],
247            None,
248            ArtifactReceipt::new(ArtifactDestination::Stdout, 0),
249            None,
250        );
251        assert_eq!(run.report(), None);
252        assert_eq!(run.suggested_destination(), None);
253    }
254}