use serde::{Deserialize, Serialize};
use crate::shared::ids::RequestId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Artifact {
Body,
RenderedHtml,
Text,
Content,
ContentJson,
Screenshot,
Network,
Console,
Observation,
Storage,
}
impl Artifact {
pub const HTTP_DEFAULT: [Self; 1] = [Self::Body];
pub const BROWSER_DEFAULT: [Self; 9] = [
Self::Body,
Self::RenderedHtml,
Self::Text,
Self::Content,
Self::ContentJson,
Self::Screenshot,
Self::Network,
Self::Console,
Self::Observation,
];
pub const ALL: [Self; 9] = Self::BROWSER_DEFAULT;
#[must_use]
pub const fn filename_template(self) -> &'static str {
match self {
Self::Body => "body",
Self::RenderedHtml => "rendered.html",
Self::Text => "text.txt",
Self::Content => "content.md",
Self::ContentJson => "content.json",
Self::Screenshot => "page.png",
Self::Network => "network.json",
Self::Console => "console.json",
Self::Observation => "observation.json",
Self::Storage => "storage.json",
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Body => "body",
Self::RenderedHtml => "rendered_html",
Self::Text => "text",
Self::Content => "content",
Self::ContentJson => "content_json",
Self::Screenshot => "screenshot",
Self::Network => "network",
Self::Console => "console",
Self::Observation => "observation",
Self::Storage => "storage",
}
}
}
impl std::fmt::Display for Artifact {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct ArtifactPaths {
pub root: std::path::PathBuf,
}
impl ArtifactPaths {
#[must_use]
pub fn new(base: impl Into<std::path::PathBuf>, request_id: &RequestId) -> Self {
let base = crate::shared::path::absolute_lexical(base.into());
Self {
root: base.join(request_id.as_str()),
}
}
#[must_use]
pub fn file_for(&self, artifact: Artifact) -> std::path::PathBuf {
self.root.join(artifact.filename_template())
}
#[must_use]
pub fn network_bodies_dir(&self) -> std::path::PathBuf {
self.root.join("network-bodies")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_tokens_present() {
assert_eq!(Artifact::HTTP_DEFAULT, [Artifact::Body]);
assert_eq!(Artifact::ALL.len(), 9);
assert!(Artifact::ALL.contains(&Artifact::Content));
assert!(Artifact::ALL.contains(&Artifact::ContentJson));
assert!(!Artifact::ALL.contains(&Artifact::Storage));
}
#[test]
fn filename_templates_match_spec_table() {
let table = [
(Artifact::Body, "body"),
(Artifact::RenderedHtml, "rendered.html"),
(Artifact::Text, "text.txt"),
(Artifact::Content, "content.md"),
(Artifact::ContentJson, "content.json"),
(Artifact::Screenshot, "page.png"),
(Artifact::Network, "network.json"),
(Artifact::Console, "console.json"),
(Artifact::Observation, "observation.json"),
(Artifact::Storage, "storage.json"),
];
for (a, f) in table {
assert_eq!(a.filename_template(), f);
}
}
#[test]
fn artifact_paths_compose_under_request_id() {
let rid = RequestId("abc-123".into());
let paths = ArtifactPaths::new("/tmp/out", &rid);
assert_eq!(
paths.file_for(Artifact::Observation),
std::path::PathBuf::from("/tmp/out/abc-123/observation.json"),
);
assert_eq!(
paths.network_bodies_dir(),
std::path::PathBuf::from("/tmp/out/abc-123/network-bodies"),
);
}
#[test]
fn artifact_paths_absolutize_relative_out_dir() {
let rid = RequestId("abc-123".into());
let paths = ArtifactPaths::new("relative-out", &rid);
assert!(
paths.root.is_absolute(),
"artifact root must be absolute: {}",
paths.root.display()
);
}
#[test]
fn artifacts_serialize_to_snake_case() {
let all_including_storage: &[Artifact] = &[
Artifact::Body,
Artifact::RenderedHtml,
Artifact::Text,
Artifact::Content,
Artifact::ContentJson,
Artifact::Screenshot,
Artifact::Network,
Artifact::Console,
Artifact::Observation,
Artifact::Storage,
];
for a in all_including_storage {
let s = serde_json::to_string(a).unwrap_or_default();
assert_eq!(s, format!("\"{}\"", a.as_str()));
}
}
}