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