1use std::{
2 fs,
3 path::{Path, PathBuf},
4 process::{Command, ExitStatus, Stdio},
5};
6
7use thiserror::Error;
8
9use crate::{Config, ConfigError, GenerationError, refresh_generated_fixtures};
10
11#[derive(Clone, Debug)]
12pub struct CatalogOptions {
13 pub project_root: PathBuf,
14}
15
16impl CatalogOptions {
17 #[must_use]
18 pub fn new(project_root: impl Into<PathBuf>) -> Self {
19 Self {
20 project_root: project_root.into(),
21 }
22 }
23}
24
25pub fn run_list(options: &CatalogOptions) -> Result<(), CatalogError> {
30 let project_root = canonical_project_root(&options.project_root)?;
31 let config = Config::load(&project_root)?;
32 refresh_generated_fixtures(&project_root, &config)?;
33 build_preview(&project_root)?;
34 let output = catalog_output(&project_root)?;
35 print!("{output}");
36 Ok(())
37}
38
39pub(crate) fn fixture_ids(project_root: &Path) -> Result<Vec<String>, CatalogError> {
40 let output = catalog_output(project_root)?;
41 Ok(output
42 .lines()
43 .filter_map(|line| {
44 let mut fields = line.split('\t');
45 (fields.next() == Some("fixture"))
46 .then(|| fields.next().map(str::to_owned))
47 .flatten()
48 })
49 .collect())
50}
51
52pub(crate) fn build_preview(project_root: &Path) -> Result<(), CatalogError> {
53 let manifest = project_root.join(".hblank/Cargo.toml");
54 let target = project_root.join(".hblank/target");
55 let status = Command::new("cargo")
56 .arg("build")
57 .arg("--manifest-path")
58 .arg(&manifest)
59 .arg("--target-dir")
60 .arg(&target)
61 .stdin(Stdio::null())
62 .status()
63 .map_err(CatalogError::Process)?;
64 if status.success() {
65 Ok(())
66 } else {
67 Err(CatalogError::BuildFailed(status))
68 }
69}
70
71pub(crate) fn preview_binary(project_root: &Path) -> Result<PathBuf, CatalogError> {
72 let package_name = preview_package_name(project_root)?;
73 let mut binary = project_root.join(".hblank/target/debug").join(package_name);
74 if cfg!(windows) {
75 binary.set_extension("exe");
76 }
77 Ok(binary)
78}
79
80fn catalog_output(project_root: &Path) -> Result<String, CatalogError> {
81 let binary = preview_binary(project_root)?;
82 let output = Command::new(&binary)
83 .env("HBLANK_PROJECT_ROOT", project_root)
84 .env("HBLANK_LIST_CATALOG", "1")
85 .stdin(Stdio::null())
86 .output()
87 .map_err(CatalogError::Process)?;
88 if !output.status.success() {
89 return Err(CatalogError::ListFailed(output.status));
90 }
91 String::from_utf8(output.stdout).map_err(CatalogError::NonUtf8)
92}
93
94fn canonical_project_root(path: &Path) -> Result<PathBuf, CatalogError> {
95 path.canonicalize()
96 .map_err(|source| CatalogError::ProjectRoot {
97 path: path.to_path_buf(),
98 source,
99 })
100}
101
102fn preview_package_name(project_root: &Path) -> Result<String, CatalogError> {
103 let path = project_root.join(".hblank/Cargo.toml");
104 let source = fs::read_to_string(&path).map_err(|source| CatalogError::ReadManifest {
105 path: path.clone(),
106 source,
107 })?;
108 let manifest =
109 toml::from_str::<toml::Value>(&source).map_err(|source| CatalogError::ParseManifest {
110 path: path.clone(),
111 source,
112 })?;
113 manifest
114 .get("package")
115 .and_then(|package| package.get("name"))
116 .and_then(toml::Value::as_str)
117 .map(str::to_owned)
118 .ok_or(CatalogError::MissingPackageName(path))
119}
120
121#[derive(Debug, Error)]
122pub enum CatalogError {
123 #[error("could not resolve Hblank project root {path}: {source}")]
124 ProjectRoot {
125 path: PathBuf,
126 source: std::io::Error,
127 },
128 #[error(transparent)]
129 Config(#[from] ConfigError),
130 #[error(transparent)]
131 Generation(#[from] GenerationError),
132 #[error("could not run Hblank catalog command: {0}")]
133 Process(std::io::Error),
134 #[error("preview build exited unsuccessfully: {0}")]
135 BuildFailed(ExitStatus),
136 #[error("catalog listing exited unsuccessfully: {0}")]
137 ListFailed(ExitStatus),
138 #[error("catalog listing was not UTF-8: {0}")]
139 NonUtf8(std::string::FromUtf8Error),
140 #[error("could not read preview manifest at {path}: {source}")]
141 ReadManifest {
142 path: PathBuf,
143 source: std::io::Error,
144 },
145 #[error("could not parse preview manifest at {path}: {source}")]
146 ParseManifest {
147 path: PathBuf,
148 source: toml::de::Error,
149 },
150 #[error("preview manifest at {0} has no package name")]
151 MissingPackageName(PathBuf),
152}