diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..c8ebfa02 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +xtask = "run --manifest-path ./xtask/Cargo.toml --" \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8561e7fe..e97057b6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,13 +7,10 @@ No worries if anything in these lists is unclear. Just submit the PR and ask awa -------------------------- ### Things to check before submitting a PR -- [ ] the tests are passing locally with `cargo test` -- [ ] cookbook renders correctly in `mdbook serve -o` +- [ ] the tests are passing locally with `cargo xtask test all` - [ ] commits are squashed into one and rebased to latest master - [ ] PR contains correct "fixes #ISSUE_ID" clause to autoclose the issue on PR merge - if issue does not exist consider creating it or remove the clause -- [ ] spell check runs without errors `./ci/spellcheck.sh` -- [ ] link check runs without errors `link-checker ./book` - [ ] non rendered items are in sorted order (links, reference, identifiers, Cargo.toml) - [ ] links to docs.rs have wildcard version `https://docs.rs/tar/*/tar/struct.Entry.html` - [ ] example has standard [error handling](https://rust-lang-nursery.github.io/rust-cookbook/about.html#a-note-about-error-handling) @@ -25,4 +22,4 @@ No worries if anything in these lists is unclear. Just submit the PR and ask awa ### Things to do after submitting PR - [ ] check if CI is happy with your PR -Thank you for reading, you may now delete this text! Thank you! :smile: +Thank you for reading, you may now delete this text! Thank you! :smile: \ No newline at end of file diff --git a/README.md b/README.md index bc8140ca..87fdcc00 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ If you'd like to read it locally: $ git clone https://github.com/rust-lang-nursery/rust-cookbook $ cd rust-cookbook $ cargo install mdbook --vers "0.4.43" -$ mdbook serve --open +$ cargo xtask book build ``` The output can also be opened from the `book` subdirectory in your web browser. diff --git a/ci/dictionary.txt b/ci/dictionary.txt index 408eaecb..c2daf226 100644 --- a/ci/dictionary.txt +++ b/ci/dictionary.txt @@ -346,3 +346,6 @@ XRateLimitReset YAML YYYY zurich +enum +thiserror +tempfile \ No newline at end of file diff --git a/src/database/postgres/aggregate_data.md b/src/database/postgres/aggregate_data.md index 9667dd30..8d799d0d 100644 --- a/src/database/postgres/aggregate_data.md +++ b/src/database/postgres/aggregate_data.md @@ -40,4 +40,4 @@ fn main() -> Result<(), Error> { } ``` -[`Museum of Modern Art`]: https://github.com/MuseumofModernArt/collection/blob/master/Artists.csv +[`Museum of Modern Art`]: https://github.com/MuseumofModernArt/collection/blob/main/Artists.csv diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 00000000..337bfe3d --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "xtask" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 00000000..66d0bcd9 --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,57 @@ +mod tests; +mod mdbook; + +use std::path::{Path, PathBuf}; +use std::{env, error::Error}; + +fn main() { + if let Err(e) = try_main() { + eprintln!("{}", e); + std::process::exit(-1); + } +} + +fn try_main() -> Result<(), Box> { + let task = env::args().nth(1); + match task.as_deref() { + Some("test") => { + let sub_task = env::args().nth(2).unwrap_or_else(|| "all".to_string()); + tests::run_test(&sub_task)? + } + Some("book") => { + let sub_task = env::args().nth(2).unwrap_or_else(|| "build".to_string()); + mdbook::run_book(&sub_task)? + } + _ => print_help(), + } + Ok(()) +} + +fn project_root() -> PathBuf { + Path::new(&env!("CARGO_MANIFEST_DIR")) + .ancestors() + .nth(1) + .unwrap() + .to_path_buf() +} + +fn print_help() { + eprintln!("Available tasks:"); + eprintln!( + " test [all|cargo|spellcheck|link] - Run the tests. Use 'all' to run all tests (default), or specify individual tests." + ); + eprintln!( + " book [build] - Build the book using mdbook. Default if no subcommand is specified." + ); + eprintln!(" book serve - Serve the book using mdbook and open it in a browser."); + eprintln!(); + eprintln!("Usage:"); + eprintln!(" cargo xtask [subcommand]"); + eprintln!(); + eprintln!("Examples:"); + eprintln!(" cargo xtask test"); + eprintln!(" cargo xtask test all"); + eprintln!(" cargo xtask test cargo"); + eprintln!(" cargo xtask book"); + eprintln!(" cargo xtask book serve"); +} \ No newline at end of file diff --git a/xtask/src/mdbook.rs b/xtask/src/mdbook.rs new file mode 100644 index 00000000..22e5fc62 --- /dev/null +++ b/xtask/src/mdbook.rs @@ -0,0 +1,44 @@ +use crate::project_root; +use std::{error::Error, path::PathBuf, process::Command}; + +pub fn run_book(task: &str) -> Result<(), Box> { + let args: &[&str] = if task == "serve" { &["--open"] } else { &[] }; + + execute_mdbook_command(task, args)?; + + Ok(()) +} + +fn execute_mdbook_command(command: &str, additional_args: &[&str]) -> Result<(), Box> { + check_mdbook_version()?; + + let book_dest = project_root().join("book").to_str().unwrap().to_string(); + + let mut args = vec![command, "--dest-dir", &book_dest]; + args.extend_from_slice(additional_args); + + let status = Command::new("mdbook") + .current_dir(project_root()) + .args(&args) + .status()?; + + if !status.success() { + return Err(format!("`mdbook {command}` failed to run successfully!").into()); + } + + Ok(()) +} + +fn check_mdbook_version() -> Result<(), Box> { + if Command::new("mdbook").arg("--version").status().is_err() { + println!("Error: `mdbook` not found. Please ensure it is installed!"); + println!("You can install it using:"); + println!(" cargo install mdbook"); + return Err(Box::new(std::io::Error::new( + std::io::ErrorKind::NotFound, + "`mdbook` is not installed", + ))); + } + + Ok(()) +} \ No newline at end of file diff --git a/xtask/src/tests.rs b/xtask/src/tests.rs new file mode 100644 index 00000000..e43298bb --- /dev/null +++ b/xtask/src/tests.rs @@ -0,0 +1,104 @@ +use crate::project_root; +use std::error::Error; +use std::process::Command; + +pub fn run_test(task: &str) -> Result<(), Box> { + match task { + "all" => run_all_tests()?, + "cargo" => cargo_test()?, + "spellcheck" => spellcheck()?, + "link" => link_checker()?, + _ => run_all_tests()?, + } + Ok(()) +} + +fn run_all_tests() -> Result<(), Box> { + let mut failures = Vec::new(); + + if cargo_test().is_err() { + failures.push("cargo_test".to_string()); + } + + if spellcheck().is_err() { + failures.push("spellcheck".to_string()); + } + + if link_checker().is_err() { + failures.push("link".to_string()); + } + + if !failures.is_empty() { + println!("\n--- Test Summary ---"); + for name in failures { + println!("āŒ {name} failed! Re-run with the command:"); + println!(" cargo xtask test {name}"); + } + } else { + println!("\nšŸŽ‰ All tests passed!"); + } + + Ok(()) +} + +fn cargo_test() -> Result<(), Box> { + let status = Command::new("cargo") + .current_dir(project_root()) + .args(["test", "--package", "rust-cookbook"]) + .status()?; + + if !status.success() { + return Err("failed to run cargo test!".into()); + } + + Ok(()) +} + +fn spellcheck() -> Result<(), Box> { + let status = Command::new("./ci/spellcheck.sh") + .current_dir(project_root()) + .status()?; + + if !status.success() { + return Err("failed to run spellcheck!".into()); + } + + Ok(()) +} + +fn link_checker() -> Result<(), Box> { + if Command::new("lychee").arg("--version").status().is_err() { + return Err( + "The `lychee` tool is not installed. Please install it using:\n cargo install lychee".into(), + ); + } + + let book_dir = project_root().join("book"); + if !book_dir.is_dir() { + return Err(format!( + "The book directory could not be found in the root directory: {:?}\n\ + You can build it using:\n cargo xtask book build", + book_dir + ) + .into()); + } + + let status = Command::new("lychee") + .current_dir(project_root()) + .args([ + "./book", + "--retry-wait-time", + "20", + "--max-retries", + "3", + "--accept", + "429", // accept 429 (ratelimit) errors as valid + ]) + .status()?; + + if !status.success() { + return Err("Failed to run link checker!".into()); + } + + Ok(()) +}