1use serde::{Deserialize, Serialize};
9
10use crate::shared::ids::RequestId;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum Artifact {
20 Body,
21 RenderedHtml,
22 Text,
23 Content,
24 ContentJson,
25 Screenshot,
26 Network,
27 Console,
28 Observation,
29 Storage,
31}
32
33impl Artifact {
34 pub const ALL: [Self; 9] = [
38 Self::Body,
39 Self::RenderedHtml,
40 Self::Text,
41 Self::Content,
42 Self::ContentJson,
43 Self::Screenshot,
44 Self::Network,
45 Self::Console,
46 Self::Observation,
47 ];
48
49 #[must_use]
53 pub const fn filename_template(self) -> &'static str {
54 match self {
55 Self::Body => "body",
56 Self::RenderedHtml => "rendered.html",
57 Self::Text => "text.txt",
58 Self::Content => "content.md",
59 Self::ContentJson => "content.json",
60 Self::Screenshot => "page.png",
61 Self::Network => "network.json",
62 Self::Console => "console.json",
63 Self::Observation => "observation.json",
64 Self::Storage => "storage.json",
65 }
66 }
67
68 #[must_use]
69 pub const fn as_str(self) -> &'static str {
70 match self {
71 Self::Body => "body",
72 Self::RenderedHtml => "rendered_html",
73 Self::Text => "text",
74 Self::Content => "content",
75 Self::ContentJson => "content_json",
76 Self::Screenshot => "screenshot",
77 Self::Network => "network",
78 Self::Console => "console",
79 Self::Observation => "observation",
80 Self::Storage => "storage",
81 }
82 }
83}
84
85impl std::fmt::Display for Artifact {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 f.write_str(self.as_str())
88 }
89}
90
91#[derive(Debug, Clone)]
94pub struct ArtifactPaths {
95 pub root: std::path::PathBuf,
96}
97
98impl ArtifactPaths {
99 #[must_use]
104 pub fn new(base: impl Into<std::path::PathBuf>, request_id: &RequestId) -> Self {
105 let base = crate::shared::path::absolute_lexical(base.into());
106 Self {
107 root: base.join(request_id.as_str()),
108 }
109 }
110
111 #[must_use]
114 pub fn file_for(&self, artifact: Artifact) -> std::path::PathBuf {
115 self.root.join(artifact.filename_template())
116 }
117
118 #[must_use]
122 pub fn network_bodies_dir(&self) -> std::path::PathBuf {
123 self.root.join("network-bodies")
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn all_default_tokens_present() {
133 assert_eq!(Artifact::ALL.len(), 9);
134 assert!(Artifact::ALL.contains(&Artifact::Content));
135 assert!(Artifact::ALL.contains(&Artifact::ContentJson));
136 assert!(!Artifact::ALL.contains(&Artifact::Storage));
138 }
139
140 #[test]
141 fn filename_templates_match_spec_table() {
142 let table = [
143 (Artifact::Body, "body"),
144 (Artifact::RenderedHtml, "rendered.html"),
145 (Artifact::Text, "text.txt"),
146 (Artifact::Content, "content.md"),
147 (Artifact::ContentJson, "content.json"),
148 (Artifact::Screenshot, "page.png"),
149 (Artifact::Network, "network.json"),
150 (Artifact::Console, "console.json"),
151 (Artifact::Observation, "observation.json"),
152 (Artifact::Storage, "storage.json"),
153 ];
154 for (a, f) in table {
155 assert_eq!(a.filename_template(), f);
156 }
157 }
158
159 #[test]
160 fn artifact_paths_compose_under_request_id() {
161 let rid = RequestId("abc-123".into());
162 let paths = ArtifactPaths::new("/tmp/out", &rid);
163 assert_eq!(
164 paths.file_for(Artifact::Observation),
165 std::path::PathBuf::from("/tmp/out/abc-123/observation.json"),
166 );
167 assert_eq!(
168 paths.network_bodies_dir(),
169 std::path::PathBuf::from("/tmp/out/abc-123/network-bodies"),
170 );
171 }
172
173 #[test]
174 fn artifact_paths_absolutize_relative_out_dir() {
175 let rid = RequestId("abc-123".into());
176 let paths = ArtifactPaths::new("relative-out", &rid);
177 assert!(
178 paths.root.is_absolute(),
179 "artifact root must be absolute: {}",
180 paths.root.display()
181 );
182 }
183
184 #[test]
185 fn artifacts_serialize_to_snake_case() {
186 let all_including_storage: &[Artifact] = &[
187 Artifact::Body,
188 Artifact::RenderedHtml,
189 Artifact::Text,
190 Artifact::Content,
191 Artifact::ContentJson,
192 Artifact::Screenshot,
193 Artifact::Network,
194 Artifact::Console,
195 Artifact::Observation,
196 Artifact::Storage,
197 ];
198 for a in all_including_storage {
199 let s = serde_json::to_string(a).unwrap_or_default();
200 assert_eq!(s, format!("\"{}\"", a.as_str()));
201 }
202 }
203}