diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml new file mode 100644 index 00000000..6217b255 --- /dev/null +++ b/.github/workflows/check-links.yml @@ -0,0 +1,52 @@ +name: Links + +on: + pull_request: + branches: + - master + +jobs: + linkChecker: + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CACHE_KEY: cache-lychee-${{ github.sha }} + permissions: + pull-requests: write + steps: + - uses: actions/checkout@v4 + + - name: Restore lychee cache + id: restore-cache + uses: actions/cache@v4 + with: + path: .lycheecache + key: ${{ env.CACHE_KEY }} + restore-keys: cache-lychee- + + - name: Link Checker + id: lychee + uses: lycheeverse/lychee-action@v2 + with: + fail: false + output: ./lychee/out.md + args: "--base . --config ./ci/lychee.toml ." + + - name: Save lychee cache + uses: actions/cache/save@v4 + if: always() + with: + path: .lycheecache + key: ${{ steps.restore-cache.outputs.cache-primary-key || env.CACHE_KEY }} # fallback in case the cache wasn't created yet + + - name: Post Comment on Pull Request + if: github.event_name == 'pull_request' && steps.lychee.outputs.exit_code != 0 + run: | + echo "The link checker found some issues. Please review the report below:" > comment.md + echo "" >> comment.md + cat ./lychee/out.md >> comment.md + gh pr comment ${{ github.event.pull_request.number }} --body-file comment.md + + - name: Fail Workflow + if: github.event_name == 'pull_request' && steps.lychee.outputs.exit_code != 0 + run: exit 1 diff --git a/.github/workflows/deploy-mdbook.yml b/.github/workflows/deploy-mdbook.yml new file mode 100644 index 00000000..8dbf948e --- /dev/null +++ b/.github/workflows/deploy-mdbook.yml @@ -0,0 +1,39 @@ +name: Build and Deploy mdBook + +on: + push: + branches: + - master + workflow_dispatch: + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up cache for mdbook + id: cache-mdbook + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/mdbook + key: mdbook-v0.4.43 + + - name: Install mdBook (if not cached) + if: steps.cache-mdbook.outputs.cache-hit != 'true' + run: | + curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.43/mdbook-v0.4.43-x86_64-unknown-linux-gnu.tar.gz | tar -xz -C ~/.cargo/bin + chmod +x ~/.cargo/bin/mdbook + + - name: Build mdBook + run: | + mdbook build + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./book diff --git a/.github/workflows/mdbook-test.yml b/.github/workflows/mdbook-test.yml new file mode 100644 index 00000000..59b32714 --- /dev/null +++ b/.github/workflows/mdbook-test.yml @@ -0,0 +1,38 @@ +name: Test Rust Project + +on: + pull_request: + branches: + - master + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + cache: 'true' + toolchain: nightly + + - name: Run tests + id: cargo_test + run: | + cargo +nightly test --test skeptic -- -Z unstable-options --format junit --report-time --test-threads=1 | tee ./TEST-cookbook.xml + + - name: List files for debugging + if: always() + run: ls -R + + - name: Publish Test Report + uses: mikepenz/action-junit-report@v5.2.0 + if: always() + with: + fail_on_failure: true + require_tests: true + summary: ${{ steps.cargo_test.outputs.summary }} + report_paths: '**/TEST-*.xml' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a300d36e..101f4c6c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -257,13 +257,11 @@ after the code sample. > generator [`rand::Rng`]. > > The [distributions available are documented here][rand-distributions]. -> An example using the [`Normal`] distribution is shown below. [uniform distribution]: https://en.wikipedia.org/wiki/Uniform_distribution_(continuous) [`Distribution::sample`]: https://docs.rs/rand/*/rand/distributions/trait.Distribution.html#tymethod.sample [`rand::Rng`]: https://docs.rs/rand/*/rand/trait.Rng.html [rand-distributions]: https://docs.rs/rand/*/rand/distributions/index.html -[`Normal`]: https://docs.rs/rand/*/rand/distributions/struct.Normal.html #### Code diff --git a/README.md b/README.md index bc8140ca..c1097ab7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # A Rust Cookbook   [![Build Status travis]][travis] [Build Status travis]: https://api.travis-ci.com/rust-lang-nursery/rust-cookbook.svg?branch=master -[travis]: https://travis-ci.com/rust-lang-nursery/rust-cookbook **[Read it here]**. diff --git a/ci/dictionary.txt b/ci/dictionary.txt index c2daf226..e2547f00 100644 --- a/ci/dictionary.txt +++ b/ci/dictionary.txt @@ -348,4 +348,4 @@ YYYY zurich enum thiserror -tempfile \ No newline at end of file +tempfile diff --git a/ci/lychee.toml b/ci/lychee.toml new file mode 100644 index 00000000..402a61ef --- /dev/null +++ b/ci/lychee.toml @@ -0,0 +1,32 @@ +# Don't show interactive progress bar while checking links. +no_progress = true + +# Enable link caching. This can be helpful to avoid checking the same links on +# multiple runs. +cache = true + +# Discard all cached requests older than this duration. +max_cache_age = "2d" + +user_agent = "curl/7.83. 1" + +# Website timeout from connect to response finished. +timeout = 20 + +# Minimum wait time in seconds between retries of failed requests. +retry_wait_time = 20 + +# Comma-separated list of accepted status codes for valid links. +accept = ["200", "429"] # 429 for ratelimits + +# Request method +method = "get" + +# Custom request headers +headers = [] + +# Exclude loopback IP address range and localhost from checking. +exclude_loopback = false + +# Check mail addresses +include_mail = true diff --git a/src/cryptography/hashing/hmac.md b/src/cryptography/hashing/hmac.md index 704d891a..90f442d0 100644 --- a/src/cryptography/hashing/hmac.md +++ b/src/cryptography/hashing/hmac.md @@ -2,8 +2,9 @@ [![ring-badge]][ring] [![cat-cryptography-badge]][cat-cryptography] -Uses [`ring::hmac`] to creates a [`hmac::Signature`] of a string then verifies the signature is correct. - +The [`hmac::sign`] method is used to calculate the HMAC digest (also called a tag) of the message using the provided key. +The resulting [`hmac::Tag`] structure contains the raw bytes of the HMAC, +which can later be verified with[`hmac::verify`] to ensure the message has not been tampered with and comes from a trusted source. ```rust,edition2018 use ring::{hmac, rand}; @@ -17,12 +18,17 @@ fn main() -> Result<(), Unspecified> { let key = hmac::Key::new(hmac::HMAC_SHA256, &key_value); let message = "Legitimate and important message."; - let signature = hmac::sign(&key, message.as_bytes()); + let signature: hmac::Tag = hmac::sign(&key, message.as_bytes()); hmac::verify(&key, message.as_bytes(), signature.as_ref())?; Ok(()) } ``` -[`hmac::Signature`]: https://briansmith.org/rustdoc/ring/hmac/struct.Signature.html -[`ring::hmac`]: https://briansmith.org/rustdoc/ring/hmac/ +[`ring::hmac`]: https://docs.rs/ring/*/ring/hmac/index.html + +[`hmac::sign`]: https://docs.rs/ring/*/ring/hmac/fn.sign.html + +[`hmac::Tag`]: https://docs.rs/ring/*/ring/hmac/struct.Tag.html + +[`hmac::verify`]: https://docs.rs/ring/*/ring/hmac/fn.verify.html diff --git a/src/development_tools/debugging/config_log/log-custom.md b/src/development_tools/debugging/config_log/log-custom.md index 1bf7b648..5ebac32b 100644 --- a/src/development_tools/debugging/config_log/log-custom.md +++ b/src/development_tools/debugging/config_log/log-custom.md @@ -47,6 +47,6 @@ fn main() -> Result<()> { ``` [`log4rs::append::file::FileAppender`]: https://docs.rs/log4rs/*/log4rs/append/file/struct.FileAppender.html -[`log4rs::config::Config`]: https://docs.rs/log4rs/*/log4rs/config/struct.Config.html +[`log4rs::config::Config`]: https://docs.rs/log4rs/*/log4rs/config/runtime/struct.Config.html [`log4rs::encode::pattern`]: https://docs.rs/log4rs/*/log4rs/encode/pattern/index.html [`log::LevelFilter`]: https://docs.rs/log/*/log/enum.LevelFilter.html diff --git a/src/encoding/string/url-encode.md b/src/encoding/string/url-encode.md index c907e12a..0fc3f726 100644 --- a/src/encoding/string/url-encode.md +++ b/src/encoding/string/url-encode.md @@ -23,7 +23,7 @@ fn main() { } ``` -[`form_urlencoded::byte_serialize`]: https://docs.rs/url/*/url/form_urlencoded/fn.byte_serialize.html -[`form_urlencoded::parse`]: https://docs.rs/url/*/url/form_urlencoded/fn.parse.html +[`form_urlencoded::byte_serialize`]: https://docs.rs/form_urlencoded/*/form_urlencoded/fn.byte_serialize.html +[`form_urlencoded::parse`]: https://docs.rs/form_urlencoded/*/form_urlencoded/fn.parse.html [application/x-www-form-urlencoded]: https://url.spec.whatwg.org/#application/x-www-form-urlencoded diff --git a/src/intro.md b/src/intro.md index 47abbda4..d604e1ad 100644 --- a/src/intro.md +++ b/src/intro.md @@ -4,7 +4,7 @@ This _Rust Cookbook_ is a collection of simple examples that demonstrate good practices to accomplish common programming tasks, using the crates of the Rust ecosystem. -[Read more about _Rust Cookbook_](about.html), including tips for +[Read more about _Rust Cookbook_](about.md), including tips for how to read the book, how to use the examples, and notes on conventions. ## Contributing diff --git a/src/web/clients/api/rate-limited.md b/src/web/clients/api/rate-limited.md deleted file mode 100644 index 635e8e53..00000000 --- a/src/web/clients/api/rate-limited.md +++ /dev/null @@ -1,68 +0,0 @@ -## Handle a rate-limited API - -[![reqwest-badge]][reqwest] [![hyper-badge]][hyper] [![cat-net-badge]][cat-net] - -This example uses the [GitHub API - Rate limiting], as an example of how to -handle remote server errors. This example uses the [`hyper::header!`] macro -to parse the response header and checks for [`reqwest::StatusCode::Forbidden`]. -If the response exceeds the rate limit, the example waits and retries. - -```rust,edition2018,no_run -# use error_chain::error_chain; - -use std::time::{Duration, UNIX_EPOCH}; -use std::thread; -use reqwest::StatusCode; -# -# error_chain! { -# foreign_links { -# Io(std::io::Error); -# Time(std::time::SystemTimeError); -# Reqwest(reqwest::Error); -# } -# } - -header! { (XRateLimitLimit, "X-RateLimit-Limit") => [usize] } -header! { (XRateLimitRemaining, "X-RateLimit-Remaining") => [usize] } -header! { (XRateLimitReset, "X-RateLimit-Reset") => [u64] } - -fn main() -> Result<()> { - loop { - let url = "https://api.github.com/users/rust-lang-nursery "; - let client = reqwest::Client::new(); - let response = client.get(url).send()?; - - let rate_limit = response.headers().get::().ok_or( - "response doesn't include the expected X-RateLimit-Limit header", - )?; - - let rate_remaining = response.headers().get::().ok_or( - "response doesn't include the expected X-RateLimit-Remaining header", - )?; - - let rate_reset_at = response.headers().get::().ok_or( - "response doesn't include the expected X-RateLimit-Reset header", - )?; - - let rate_reset_within = Duration::from_secs(**rate_reset_at) - UNIX_EPOCH.elapsed()?; - - if response.status() == StatusCode::Forbidden && **rate_remaining == 0 { - println!("Sleeping for {} seconds.", rate_reset_within.as_secs()); - thread::sleep(rate_reset_within); - return main(); - } else { - println!( - "Rate limit is currently {}/{}, the reset of this limit will be within {} seconds.", - **rate_remaining, - **rate_limit, - rate_reset_within.as_secs(), - ); - break; - } - } - Ok(()) -} -``` - -[`hyper::header!`]: https://doc.servo.org/hyper/header/index.html#defining-custom-headers -[`reqwest::StatusCode::Forbidden`]: https://docs.rs/reqwest/*/reqwest/struct.StatusCode.html#associatedconstant.FORBIDDEN diff --git a/src/web/scraping/broken.md b/src/web/scraping/broken.md index 79243460..02990003 100644 --- a/src/web/scraping/broken.md +++ b/src/web/scraping/broken.md @@ -6,7 +6,7 @@ Call `get_base_url` to retrieve the base URL. If the document has a base tag, get the href [`attr`] from base tag. [`Position::BeforePath`] of the original URL acts as a default. -Iterates through links in the document and creates a [`tokio::spawn`] task that will +Iterates through links in the document and creates a [`tokio::task::spawn`] task that will parse an individual link with [`url::ParseOptions`] and [`Url::parse`]). The task makes a request to the links with [reqwest] and verifies [`StatusCode`]. Then the tasks `await` completion before ending the program. @@ -28,6 +28,6 @@ fn main() -> anyhow::Result<()> { [`attr`]: https://docs.rs/select/*/select/node/struct.Node.html#method.attr [`Position::BeforePath`]: https://docs.rs/url/*/url/enum.Position.html#variant.BeforePath [`StatusCode`]: https://docs.rs/reqwest/*/reqwest/struct.StatusCode.html -[`tokio::spawn`]: https://docs.rs/tokio/*/tokio/fn.spawn.html +[`tokio::task::spawn`]: https://docs.rs/tokio/*/tokio/task/fn.spawn.html [`url::Parse`]: https://docs.rs/url/*/url/struct.Url.html#method.parse [`url::ParseOptions`]: https://docs.rs/url/*/url/struct.ParseOptions.html diff --git a/xtask/src/tests.rs b/xtask/src/tests.rs index 924851a2..f1612511 100644 --- a/xtask/src/tests.rs +++ b/xtask/src/tests.rs @@ -31,14 +31,17 @@ fn run_all_tests() -> Result<(), Box> { 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}"); + eprintln!("āŒ {name} failed! Re-run with the command:"); + eprintln!(" cargo xtask test {name}"); } + Err(Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + "test failed", + ))) } else { println!("\nšŸŽ‰ All tests passed!"); + Ok(()) } - - Ok(()) } fn cargo_test() -> Result<(), Box> { @@ -87,12 +90,8 @@ fn link_checker() -> Result<(), Box> { .current_dir(project_root()) .args([ "./book", - "--retry-wait-time", - "20", - "--max-retries", - "3", - "--accept", - "429", // accept 429 (ratelimit) errors as valid + "--config", + "./ci/lychee.toml" ]) .status()?;