1#![doc = include_str!("../README.md")]
2
3use std::io::{ErrorKind, Result};
4use std::path::Path;
5
6#[cfg(feature = "tempfile")]
7pub use tempfile;
8
9#[macro_export]
13macro_rules! create {
14 ($($files:tt)+) => {
16 {
17 #[allow(unused_variables)]
18 let path = ::std::path::PathBuf::default();
19 $crate::create_internal!(@entries path $($files)+)
20 }
21 };
22}
23
24#[cfg(feature = "tempfile")]
29#[macro_export]
30macro_rules! create_temp {
31 ($($files:tt)+) => {
33 $crate::tempfile::tempdir()
34 .and_then(|dir| {
35 #[allow(unused_variables)]
36 let path = dir.path();
37 $crate::create_internal!(@entries path $($files)+).and(Ok(dir))
38 })
39 };
40}
41
42#[macro_export]
43#[doc(hidden)]
44macro_rules! create_internal {
45 (@entries $dir_path:ident { $($files:tt)+ }) => {
51 $crate::create_internal!(@entry $dir_path () ($($files)+) ($($files)+))
52 };
53
54 (@entries $dir_path:ident {}) => {
56 Ok::<(), ::std::io::Error>(())
57 };
58
59 (@entry $dir_path:ident ($($file_path:tt)+) (: null $($rest:tt)*) ($($copy:tt)*)) => {
65 $crate::create_internal!(@handle $dir_path [$($file_path)+] (false) $($rest)*)
66 };
67
68 (@entry $dir_path:ident ($($file_path:tt)+) (: false $($rest:tt)*) ($($copy:tt)*)) => {
70 $crate::create_internal!(@handle $dir_path [$($file_path)+] (false) $($rest)*)
71 };
72
73 (@entry $dir_path:ident ($($file_path:tt)+) (: true $($rest:tt)*) ($($copy:tt)*)) => {
75 $crate::create_internal!(@handle $dir_path [$($file_path)+] (true) $($rest)*)
76 };
77
78 (@entry $dir_path:ident ($($file_path:tt)+) (: { $($map:tt)* } , $($rest:tt)*) ($($copy:tt)*)) => {
81 {
82 let $dir_path = &$dir_path.join($($file_path)+);
83 $crate::create_dir(&$dir_path).and_then(|_| {
84 $crate::create_internal!(@entries $dir_path { $($map)* })
85 })
86 }
87 .and_then(|_| $crate::create_internal!(@entries $dir_path { $($rest)* }))
88 };
89
90 (@entry $dir_path:ident ($($file_path:tt)+) (: { $($map:tt)* } $($unexpected:tt)+) (: $($copy:tt)*)) => {
92 $crate::create_expect_map_comma!($($copy)*)
93 };
94
95 (@entry $dir_path:ident ($($file_path:tt)+) (: { $($map:tt)* }) ($($copy:tt)*)) => {
98 {
99 let $dir_path = &$dir_path.join($($file_path)+);
100 $crate::create_dir(&$dir_path).and_then(|_| {
101 $crate::create_internal!(@entries $dir_path { $($map)* })
102 })
103 }
104 };
105
106 (@entry $dir_path:ident ($($file_path:tt)+) (: $contents:expr , $($rest:tt)*) ($($copy:tt)*)) => {
109 $crate::create_internal!(@handle $dir_path [$($file_path)+] ($contents) , $($rest)*)
110 };
111
112 (@entry $dir_path:ident ($($file_path:tt)+) (: $contents:expr) ($($copy:tt)*)) => {
115 $crate::create_internal!(@handle $dir_path [$($file_path)+] ($contents))
116 };
117
118 (@entry $dir_path:ident ($($file_path:tt)+) (:) ($($copy:tt)*)) => {
120 $crate::create_internal!()
122 };
123
124 (@entry $dir_path:ident ($($file_path:tt)+) () ($($copy:tt)*)) => {
126 $crate::create_internal!()
128 };
129
130 (@entry $dir_path:ident () (: $($rest:tt)*) ($colon:tt $($copy:tt)*)) => {
132 $crate::create_unexpected!($colon)
134 };
135
136 (@entry $dir_path:ident ($($file_path:tt)*) (, $($rest:tt)*) ($comma:tt $($copy:tt)*)) => {
138 $crate::create_unexpected!($comma)
140 };
141
142 (@entry $dir_path:ident () (($file_path:expr) : $($rest:tt)*) ($($copy:tt)*)) => {
145 $crate::create_internal!(@entry $dir_path ($file_path) (: $($rest)*) (: $($rest)*))
146 };
147
148 (@entry $dir_path:ident ($($file_path:tt)*) (: $($unexpected:tt)+) ($($copy:tt)*)) => {
150 $crate::create_expect_comma!($($unexpected)+)
152 };
153
154 (@entry $dir_path:ident ($($file_path:tt)+) ({ $($map:tt)* } $($rest:tt)*) ($curly_bracket:tt $($copy:tt)*)) => {
156 $crate::create_unexpected!($curly_bracket)
158 };
159
160 (@entry $dir_path:ident ($($path:tt)*) ($tt:tt $($rest:tt)*) ($($copy:tt)*)) => {
162 $crate::create_internal!(@entry $dir_path ($($path)* $tt) ($($rest)*) ($($rest)*))
163 };
164
165 (@handle $dir_path:ident [$($file_path:tt)+] ($contents:tt) , $($rest:tt)*) => {
171 $crate::create_internal!(@write_file ($dir_path) ($($file_path)+) ($contents))
172 .and_then(|_| $crate::create_internal!(@entries $dir_path { $($rest)* }))
173 };
174
175 (@handle $dir_path:ident [$($file_path:tt)+] ($contents:expr) $unexpected:tt $($rest:tt)*) => {
177 $crate::create_unexpected!($unexpected)
178 };
179
180 (@handle $dir_path:ident [$($file_path:tt)+] ($contents:tt)) => {
182 $crate::create_internal!(@write_file ($dir_path) ($($file_path)+) ($contents))
183 };
184
185 (@write_file ($dir_path:ident) ($($file_path:tt)+) (false)) => {
191 Ok::<(), ::std::io::Error>(())
192 };
193
194 (@write_file ($dir_path:ident) ($($file_path:tt)+) (true)) => {
196 $crate::write_file($dir_path.join($($file_path)+), "")
197 };
198
199 (@write_file ($dir_path:ident) ($($file_path:tt)+) ($contents:expr)) => {
201 $crate::write_file($dir_path.join($($file_path)+), $contents)
202 };
203}
204
205#[macro_export]
206#[doc(hidden)]
207macro_rules! create_expect_comma {
208 ($e:expr , $($tt:tt)*) => {};
209}
210
211#[macro_export]
212#[doc(hidden)]
213macro_rules! create_expect_map_comma {
214 ({$($tt:tt)*} , $($rest:tt)*) => {};
215}
216
217#[macro_export]
218#[doc(hidden)]
219macro_rules! create_unexpected {
220 () => {};
221}
222
223#[cfg(not(test))]
224pub fn create_dir<P: AsRef<Path>>(path: P) -> Result<()> {
225 std::fs::create_dir_all(path)
226}
227
228#[cfg(test)]
229fn create_dir<P: AsRef<Path>>(path: P) -> Result<()> {
230 test_helper::create_dir(path)
231}
232
233#[cfg(not(test))]
234pub fn write_file<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
235 match std::fs::write(&path, &contents) {
236 Err(err) if err.kind() == ErrorKind::NotFound => {
237 let dir_path = path.as_ref().parent().ok_or(err)?;
238 std::fs::create_dir_all(dir_path).and_then(|_| std::fs::write(&path, &contents))
239 }
240 result => result,
241 }
242}
243
244#[cfg(test)]
245fn write_file<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
246 test_helper::write_file(path, contents)
247}
248
249#[cfg(test)]
250mod test_helper {
251 use std::cell::Cell;
252 use std::collections::HashSet;
253 use std::io::{Error, ErrorKind, Result};
254 use std::path::{Path, PathBuf};
255
256 thread_local!(static WRITES: Cell<Option<(Vec<Write>, HashSet<PathBuf>)>> = Cell::new(None));
257
258 pub struct Watcher<T, F>
259 where
260 T: Sized,
261 F: Fn() -> Option<T>,
262 {
263 consume_cb: F,
264 }
265
266 impl<T, F> Watcher<T, F>
267 where
268 T: Sized,
269 F: Fn() -> Option<T>,
270 {
271 pub fn consume(self) -> T {
272 (self.consume_cb)().unwrap()
273 }
274 }
275
276 impl<T, F> Drop for Watcher<T, F>
277 where
278 T: Sized,
279 F: Fn() -> Option<T>,
280 {
281 fn drop(&mut self) {
282 (self.consume_cb)();
283 }
284 }
285
286 pub fn watch_fs() -> Watcher<Vec<Write>, impl Fn() -> Option<Vec<Write>>> {
287 WRITES.with(|cell| cell.set(Some((Default::default(), Default::default()))));
288 Watcher {
289 consume_cb: || {
290 WRITES
291 .with(|cell| cell.replace(None))
292 .map(|(writes, _)| writes)
293 },
294 }
295 }
296
297 #[derive(PartialEq, Eq, Debug)]
298 pub enum Write {
299 Dir(PathBuf),
300 File(PathBuf, Vec<u8>),
301 }
302
303 impl Write {
304 pub fn dir(path: impl AsRef<str>) -> Write {
305 Write::Dir(path.as_ref().into())
306 }
307
308 pub fn file(path: impl AsRef<str>, contents: impl AsRef<str>) -> Write {
309 Write::File(path.as_ref().into(), contents.as_ref().into())
310 }
311 }
312
313 pub fn create_dir<P: AsRef<Path>>(path: P) -> Result<()> {
314 WRITES.with(|cell| {
315 if let Some(mut writes) = cell.take() {
316 let path = path.as_ref().to_owned();
317 if writes.1.contains(&path) {
318 cell.replace(Some(writes));
319 return Err(Error::from(ErrorKind::Other));
320 }
321 writes.0.push(Write::Dir(path));
322 cell.replace(Some(writes));
323 }
324 Ok(())
325 })
326 }
327
328 pub fn write_file<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
329 WRITES.with(|cell| {
330 if let Some(mut writes) = cell.take() {
331 let path = path.as_ref().to_owned();
332 if writes.1.contains(&path) {
333 cell.replace(Some(writes));
334 return Err(Error::from(ErrorKind::Other));
335 }
336 let contents = contents.as_ref().to_owned();
337 writes.0.push(Write::File(path, contents));
338 cell.replace(Some(writes));
339 }
340 Ok(())
341 })
342 }
343
344 pub fn fail_fs<P: AsRef<Path>>(path: P) {
345 WRITES.with(|cell| {
346 if let Some(mut writes) = cell.take() {
347 writes.1.insert(path.as_ref().to_owned());
348 cell.replace(Some(writes));
349 }
350 });
351 }
352}
353
354#[cfg(test)]
355mod fs_tests {
356 use super::test_helper::Write;
357 use super::*;
358
359 #[test]
360 fn test_1() {
361 let watcher = test_helper::watch_fs();
362 create!({
363 "README.md": "# Project",
364 ("LICENSE"): "MIT"
365 })
366 .unwrap();
367 let expected = vec![
368 Write::file("README.md", "# Project"),
369 Write::file("LICENSE", "MIT"),
370 ];
371 assert_eq!(watcher.consume(), expected);
372 }
373
374 #[test]
375 fn test_2() {
376 let watcher = test_helper::watch_fs();
377 create!({
378 "directory": {
379 "README.md": "# Project"
380 },
381 "sibling": {}
382 })
383 .unwrap();
384 let expected = vec![
385 Write::dir("directory"),
386 Write::file("directory/README.md", "# Project"),
387 Write::dir("sibling"),
388 ];
389 assert_eq!(watcher.consume(), expected);
390 }
391
392 #[test]
393 fn test_3() {
394 let watcher = test_helper::watch_fs();
395
396 let project_name = String::from("Rust project");
397 let adr_directory = "adr";
398 let adr_template = ["# NUMBER. TITLE", "", "Date: DATE"].join("\n");
399 fn license() -> &'static str {
400 "MIT License..."
401 }
402 fn markdown(name: &str) -> String {
403 format!("{}.md", name)
404 }
405
406 create!({
407 ["long", "path"].join("/"): {
408 markdown("README"): format!("# {}", project_name),
409 "docs": {
410 markdown("README"): "# Documentation",
411 "assets": {},
412 "examples": {}
413 },
414 adr_directory: {
415 "templates": {
416 markdown("template"): adr_template,
417 }
418 },
419 "LICENSE": license(),
420 ".adr-dir": adr_directory,
421 },
422 "other": {
423 "not-create-1": false,
424 "not-create-2": null,
425 ".gitkeep": true,
426 "path/as/name": true
427 },
428 })
429 .unwrap();
430 let expected = vec![
431 Write::dir("long/path"),
432 Write::file("long/path/README.md", "# Rust project"),
433 Write::dir("long/path/docs"),
434 Write::file("long/path/docs/README.md", "# Documentation"),
435 Write::dir("long/path/docs/assets"),
436 Write::dir("long/path/docs/examples"),
437 Write::dir("long/path/adr"),
438 Write::dir("long/path/adr/templates"),
439 Write::file(
440 "long/path/adr/templates/template.md",
441 "# NUMBER. TITLE\n\nDate: DATE",
442 ),
443 Write::file("long/path/LICENSE", "MIT License..."),
444 Write::file("long/path/.adr-dir", "adr"),
445 Write::dir("other"),
446 Write::file("other/.gitkeep", ""),
447 Write::file("other/path/as/name", ""),
448 ];
449 assert_eq!(watcher.consume(), expected);
450 }
451
452 #[test]
453 fn directory_fails() {
454 let watcher = test_helper::watch_fs();
455 test_helper::fail_fs("second-error");
456 let result = create!({
457 "first-success": {},
458 "second-error": {},
459 "third-not-attempted": {},
460 });
461 let expected = vec![Write::dir("first-success")];
462 assert_eq!(result.unwrap_err().kind(), ErrorKind::Other);
463 assert_eq!(watcher.consume(), expected);
464
465 let watcher = test_helper::watch_fs();
466 test_helper::fail_fs("second-error");
467 let result = create!({
468 "first-success": {
469 "README.md": "# Project 1",
470 },
471 "second-error": {
472 "README.md": "# Project 2",
473 }
474 });
475 let expected = vec![
476 Write::dir("first-success"),
477 Write::file("first-success/README.md", "# Project 1"),
478 ];
479 assert_eq!(result.unwrap_err().kind(), ErrorKind::Other);
480 assert_eq!(watcher.consume(), expected);
481 }
482
483 #[test]
484 fn file_fails() {
485 let watcher = test_helper::watch_fs();
486 test_helper::fail_fs("second-success/README.md");
487 let result = create!({
488 "first-success": {
489 "README.md": "# Project 1",
490 "LICENSE": "MIT"
491 },
492 "second-success": {
493 "README.md": "# Project error",
494 "LICENSE": "MIT"
495 },
496 "third-not-attempted": {
497 "README.md": "# Project 3",
498 "LICENSE": "MIT"
499 },
500 });
501 let expected = vec![
502 Write::dir("first-success"),
503 Write::file("first-success/README.md", "# Project 1"),
504 Write::file("first-success/LICENSE", "MIT"),
505 Write::dir("second-success"),
506 ];
507 assert_eq!(result.unwrap_err().kind(), ErrorKind::Other);
508 assert_eq!(watcher.consume(), expected);
509 }
510}