1use crate::cli_ops::workspace_ops::{WorkspaceOp, WorkspaceOps};
5use camino::{Utf8Path, Utf8PathBuf};
6use guppy::graph::PackageGraph;
7use include_dir::{include_dir, Dir, DirEntry};
8use std::{borrow::Cow, convert::TryInto, error, fmt, io};
9
10const CRATE_TEMPLATE_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/crate");
11const CONFIG_TEMPLATE: &str = include_str!("../../templates/hakari.toml-in");
12
13#[derive(Clone, Debug)]
15pub struct HakariInit<'g, 'a> {
16 package_graph: &'g PackageGraph,
17 package_name: &'a str,
18 crate_path: &'a Utf8Path,
19 config: Option<(&'a Utf8Path, &'a str)>,
20 cargo_toml_comment: &'a str,
21}
22
23impl<'g, 'a> HakariInit<'g, 'a> {
24 pub fn new(
29 package_graph: &'g PackageGraph,
30 package_name: &'a str,
31 crate_path: &'a Utf8Path,
32 ) -> Result<Self, InitError> {
33 let workspace = package_graph.workspace();
34 let workspace_root = workspace.root();
35
36 if let Ok(existing) = workspace.member_by_name(package_name) {
38 return Err(InitError::PackageNameExists {
39 package_name: package_name.to_owned(),
40 workspace_path: existing
41 .source()
42 .workspace_path()
43 .expect("package returned by workspace")
44 .to_owned(),
45 });
46 }
47
48 let abs_path = workspace_root.join(crate_path);
49 if !abs_path.starts_with(workspace.root()) {
50 return Err(InitError::WorkspacePathNotInRoot {
51 abs_path,
52 workspace_root: workspace.root().to_owned(),
53 });
54 }
55
56 match std::fs::symlink_metadata(&abs_path) {
59 Ok(_) => {
60 return Err(InitError::WorkspacePathExists { abs_path });
62 }
63 #[cfg_attr(guppy_nightly, expect(non_exhaustive_omitted_patterns))]
64 Err(err) => match err.kind() {
65 io::ErrorKind::NotFound => {}
66 _ => {
67 return Err(InitError::Io {
68 path: abs_path,
69 error: err,
70 });
71 }
72 },
73 }
74
75 Ok(Self {
78 package_graph,
79 package_name,
80 crate_path,
81 config: None,
82 cargo_toml_comment: "",
83 })
84 }
85
86 pub fn set_config(
91 &mut self,
92 path: &'a Utf8Path,
93 comment: &'a str,
94 ) -> Result<&mut Self, InitError> {
95 let abs_path = self.package_graph.workspace().root().join(path);
97 if abs_path.exists() {
98 return Err(InitError::ConfigPathExists { abs_path });
99 }
100
101 self.config = Some((path, comment));
102 Ok(self)
103 }
104
105 pub fn set_cargo_toml_comment(&mut self, comment: &'a str) -> &mut Self {
108 self.cargo_toml_comment = comment;
109 self
110 }
111
112 pub fn make_ops(&self) -> WorkspaceOps<'g, 'a> {
114 WorkspaceOps::new(
115 self.package_graph,
116 std::iter::once(self.make_new_crate_op()),
117 )
118 }
119
120 fn make_new_crate_op(&self) -> WorkspaceOp<'g, 'a> {
125 let files = CRATE_TEMPLATE_DIR
126 .find("**/*")
127 .expect("pattern **/* is valid")
128 .flat_map(|entry| {
129 match entry {
130 DirEntry::File(file) => {
131 let path: &Utf8Path = file
132 .path()
133 .try_into()
134 .expect("embedded path is valid UTF-8");
135 if path.extension() == Some("toml-in") {
137 let contents = file
138 .contents_utf8()
139 .expect("embedded .toml-in is valid UTF-8");
140 let contents = contents.replace("%PACKAGE_NAME%", self.package_name);
141 let contents =
142 contents.replace("%CARGO_TOML_COMMENT%\n", self.cargo_toml_comment);
143 Some((
144 Cow::Owned(path.with_extension("toml")),
145 Cow::Owned(contents.into_bytes()),
146 ))
147 } else {
148 Some((Cow::Borrowed(path), Cow::Borrowed(file.contents())))
149 }
150 }
151 DirEntry::Dir(_) => None,
152 }
153 })
154 .collect();
155
156 let root_files = self
157 .config
158 .into_iter()
159 .map(|(path, comment)| {
160 let contents = CONFIG_TEMPLATE.replace("%PACKAGE_NAME%", self.package_name);
161 let contents = contents.replace("%CONFIG_COMMENT%\n", comment);
162 (Cow::Borrowed(path), Cow::Owned(contents.into_bytes()))
163 })
164 .collect();
165
166 WorkspaceOp::NewCrate {
167 crate_path: self.crate_path,
168 files,
169 root_files,
170 }
171 }
172}
173
174#[derive(Debug)]
176#[non_exhaustive]
177pub enum InitError {
178 ConfigPathExists {
180 abs_path: Utf8PathBuf,
182 },
183
184 PackageNameExists {
186 package_name: String,
188
189 workspace_path: Utf8PathBuf,
191 },
192
193 WorkspacePathNotInRoot {
195 abs_path: Utf8PathBuf,
197
198 workspace_root: Utf8PathBuf,
200 },
201
202 WorkspacePathExists {
204 abs_path: Utf8PathBuf,
206 },
207
208 Io {
210 path: Utf8PathBuf,
212
213 error: io::Error,
215 },
216}
217
218impl fmt::Display for InitError {
219 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
220 match self {
221 InitError::ConfigPathExists { abs_path } => {
222 write!(f, "config already exists at path {}", abs_path)
223 }
224 InitError::PackageNameExists {
225 package_name,
226 workspace_path,
227 } => {
228 write!(
229 f,
230 "package name {} already exists at path {}",
231 package_name, workspace_path
232 )
233 }
234 InitError::WorkspacePathNotInRoot {
235 abs_path,
236 workspace_root,
237 } => {
238 write!(
239 f,
240 "path {} is not within workspace {}",
241 abs_path, workspace_root
242 )
243 }
244 InitError::WorkspacePathExists { abs_path } => {
245 write!(f, "workspace path {} already exists", abs_path)
246 }
247 InitError::Io { path, .. } => {
248 write!(f, "IO error while accessing {}", path)
249 }
250 }
251 }
252}
253
254impl error::Error for InitError {
255 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
256 match self {
257 InitError::Io { error, .. } => Some(error),
258 InitError::ConfigPathExists { .. }
259 | InitError::PackageNameExists { .. }
260 | InitError::WorkspacePathNotInRoot { .. }
261 | InitError::WorkspacePathExists { .. } => None,
262 }
263 }
264}