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}