diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 00000000..356cdbf1 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,37 @@ +name: Deploy to GitHub Pages + +on: + # Trigger the workflow every time you push to the `main` branch + # Using a different branch name? Replace `main` with your branch’s name + push: + branches: [ development ] + # Allows you to run this workflow manually from the Actions tab on GitHub. + workflow_dispatch: + +# Allow this job to clone the repo and create a page deployment +permissions: + contents: read + pages: write + id-token: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout your repository using git + uses: actions/checkout@v4 + - name: Install, build, and upload your site + uses: withastro/action@v2 + with: + path: ./docs/website # The root location of your Astro project inside the repository. (optional) + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index d80d35be..518b2731 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Generated by Cargo # will have compiled files and executables /target/ +/tools/target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html @@ -13,4 +14,12 @@ Cargo.lock *.iml # OSX -.DS_Store \ No newline at end of file +.DS_Store + +# LLVM coverage and profiling tool files +*.profraw + +# Coverage report files +cobertura.xml +tarpaulin-report.html + diff --git a/CHANGELOG.md b/CHANGELOG.md index 84eb0c5e..36192755 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,17 @@ # Changelog -## Version 0.7.1 - -- A new [MockServer::reset](https://docs.rs/httpmock/latest/httpmock/struct.MockServer.html#method.reset) method was added that resets a mock server. Thanks for providing the [pull request](https://github.com/alexliesenfeld/httpmock/pull/100) for this feature, [@dax](https://github.com/dax). +## Version 0.8.0 + +### BREAKING CHANGES +- A new [MockServer::reset](https://docs.rs/httpmock/latest/httpmock/struct.MockServer.html#method.reset) method was added that resets a mock server. Thanks for providing the [pull request](https://github.com/alexliesenfeld/httpmock/pull/100) for this feature, [@dax](https://github.com/dax). +- The default port for standalone server was changed from `5000` to `5050` due to conflicts with system services on macOS. +- [Custom matcher functions](https://docs.rs/httpmock/latest/httpmock/struct.When.html#method.matches) are now closures rather than functions. +- [When::json_body_partial](https://docs.rs/httpmock/0.7.0/httpmock/struct.When.html#method.json_body_partial) was renamed to `json_body_includes`. +- [When::x_www_form_urlencoded_tuple](https://docs.rs/httpmock/0.7.0/httpmock/struct.When.html#method.x_www_form_urlencoded) was renamed to `form_urlencoded_tuple`. +- [When::x_www_form_urlencoded_key_exists](https://docs.rs/httpmock/0.7.0/httpmock/struct.When.html#method.x_www_form_urlencoded) was renamed to `form_urlencoded_key_exists`. + +### Improvements +- The algorithm to find the most similar request in case of mock assertion failures has been improved. ## Version 0.7.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..f95d6907 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 8f97ff83..5cda4271 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "httpmock" -version = "0.7.0" +version = "0.8.0-alpha.1" authors = ["Alexander Liesenfeld "] edition = "2018" description = "HTTP mocking library for Rust" @@ -18,45 +18,62 @@ lazy_static = "1.4" base64 = "0.22" regex = "1.10" log = "0.4" -url = "2.4" +url = "2.5" +stringmetrics = "2" assert-json-diff = "2.0" async-trait = "0.1" async-object-pool = "0.1" crossbeam-utils = "0.8" futures-util = "0.3" -similar = "2.3" -levenshtein = "1.0" +similar = "2.6" form_urlencoded = "1.2" - -hyper = { version = "0.14", features = ["server", "http1", "tcp"] } -tokio = { version = "1.33", features = ["sync", "macros", "rt-multi-thread", "signal"] } - -isahc = { version = "1.7", optional = true } -basic-cookies = { version = "0.1", optional = true } -colored = { version = "2.0", optional = true } -clap = { version = "4.4", features = ["derive", "env"], optional = true } +thiserror = "1.0" +path-tree = "0.8" +http = "1" +bytes = { version = "1", features = ["serde"] } +hyper = { version = "1.4", features = ["server", "http1", "client"] } +hyper-util = { version = "0.1", features = ["tokio", "server", "http1", "server-auto"] } +http-body-util = "0.1" +tokio = { version = "1.36", features = ["sync", "macros", "rt-multi-thread", "signal"] } +tabwriter = "1.4" +colored = { version = "2.1", optional = true } +clap = { version = "4.5", features = ["derive", "env"], optional = true } env_logger = { version = "0.11", optional = true } serde_yaml = { version = "0.9", optional = true } async-std = { version = "1.12", features = ["attributes", "unstable"] } +headers = { version = "0.4", optional = true } + +### TLS / HTTPS / PROXY +rustls = { version = "0.23", default-features = false, features = ["std", "tls12"], optional = true } +rcgen = { version = "0.12", features = ["pem", "x509-parser"], optional = true } +tokio-rustls = { version = "0.26", optional = true } +rustls-pemfile = { version = "2", optional = true } +tls-detect = { version = "0.1", optional = true } +hyper-rustls = { version = "0.27", optional = true } +futures-timer = "3" [dev-dependencies] env_logger = "0.11" tokio-test = "0.4" quote = "1.0" actix-rt = "2.9" -colored = "2.0" -ureq = "2.8" - -isahc = { version = "1.7", features = ["json"] } +colored = "2.1" +reqwest = { version = "0.12", features = ["blocking", "cookies", "rustls-tls", "rustls-tls-native-roots"] } syn = { version = "2.0", features = ["full"] } +urlencoding = "2.1.2" -reqwest = "0.11.22" [features] default = ["cookies"] -standalone = ["clap", "env_logger", "serde_yaml", "remote"] -color = ["colored"] -cookies = ["basic-cookies"] -remote = ["isahc"] +standalone = ["clap", "env_logger", "record", "http2", "cookies", "remote", "remote-https"] # enables standalone mode +color = ["colored"] # enables colorful output in standalone mode +cookies = ["headers"] # enables support for matching cookies +remote = ["hyper-util/client-legacy", "hyper-util/http2"] # allows to connect to remote mock servers +remote-https = ["remote", "rustls", "hyper-rustls", "hyper-rustls/http2"] # allows to connect to remote mock servers via HTTPS +proxy = ["remote-https", "hyper-util/client-legacy", "hyper-util/http2", "hyper-rustls", "hyper-rustls/http2"] # enables proxy functionality +https = ["rustls", "rcgen", "tokio-rustls", "rustls-pemfile", "rustls/ring", "tls-detect"] # enables httpmock server support for TLS/HTTPS +http2 = ["hyper/http2", "hyper-util/http2"] # enables httpmocks server support for HTTP2 +record = ["proxy", "serde_yaml"] +experimental = [] # marker feature for experimental features [[bin]] name = "httpmock" diff --git a/Dockerfile b/Dockerfile index be79fbf6..a07fada6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,27 +1,15 @@ -# ================================================================================ -# Builder -# ================================================================================ +# First stage: build the application FROM rust:1.74 as builder -WORKDIR /usr/src/httpmock -COPY Cargo.toml . - -COPY src/ ./src/ - -RUN cargo install --features="standalone" --path . - -# ================================================================================ -# Runner -# ================================================================================ -FROM debian:bullseye-slim -RUN apt-get update && apt-get install -y openssl && rm -rf /var/lib/apt/lists/* -COPY --from=builder /usr/local/cargo/bin/httpmock /usr/local/bin/httpmock +RUN apt-get update && apt-get install -y \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* # Log level (refer to env_logger crate for more information) ENV RUST_LOG httpmock=info # The TCP port on which the mock server will listen to. -ENV HTTPMOCK_PORT 5000 +ENV HTTPMOCK_PORT 5050 # Container internal directory path that contains file bases mock specs (YAML-fies). # ENV HTTPMOCK_MOCK_FILES_DIR /mocks @@ -32,6 +20,16 @@ ENV HTTPMOCK_PORT 5000 # Request history limit. ENV HTTPMOCK_REQUEST_HISTORY_LIMIT 100 +WORKDIR /httpmock + +COPY Cargo.toml . +# COPY Cargo.lock . + +COPY src/ ./src/ +COPY certs/ ./certs/ + +RUN cargo install --all-features --path . + ENTRYPOINT ["httpmock", "--expose"] EXPOSE ${HTTPMOCK_PORT} \ No newline at end of file diff --git a/Makefile b/Makefile index ccf9a77a..8430e275 100644 --- a/Makefile +++ b/Makefile @@ -1,23 +1,68 @@ -.PHONY: build -build: - cargo build +.PHONY: setup +setup: + cargo install cargo-audit + cargo install --locked cargo-deny + cargo install cargo-tarpaulin + cargo install cargo-hack -.PHONY: test-local -test-local: - cargo test +.PHONY: test-full +test-full: + docker compose up -d + HTTPMOCK_TESTS_DISABLE_SIMULATED_STANDALONE_SERVER=1 cargo hack test --feature-powerset --exclude-features https -.PHONY: test-remote -test-local: - cargo test --features=remote +.PHONY: check +check: + cargo fmt --check + cargo clippy + cargo audit + cargo deny check -.PHONY: test-standalone -test-standalone: - cargo test --features standalone +.PHONY: coverage +coverage: + cargo tarpaulin --out -.PHONY: test-all -test-all: - cargo test --all-features +.PHONY: coverage-full +coverage-full: + cargo tarpaulin --config tarpaulin.full.toml --out -.PHONY: build-docker -build-docker: - docker build . \ No newline at end of file +.PHONY: coverage-debug +coverage-debug: + RUST_BACKTRACE=1 RUST_LOG=trace cargo tarpaulin --out -- --nocapture + +.PHONY: clean-coverage +clean-coverage: + rm -f *.profraw + rm -f cobertura.xml + rm -f tarpaulin-report.html + +.PHONY: clean-coverage +clean: clean-coverage + cargo clean + +.PHONY: certs +certs: + rm -rf certs + mkdir certs + cd certs && openssl genrsa -out ca.key 2048 + cd certs && openssl req -x509 -new -nodes -key ca.key -sha256 -days 36525 -out ca.pem -subj "/CN=httpmock" + +.PHONY: docker +docker: + docker-compose build --no-cache + docker-compose up + +.PHONY: docs +docs: + rm -rf tools/target/generated && mkdir -p tools/target/generated + cd tools && cargo run --bin extract_docs + cd tools && cargo run --bin extract_code + cd tools && cargo run --bin extract_groups + cd tools && cargo run --bin extract_example_tests + rm -rf docs/website/generated && cp -r tools/target/generated docs/website/generated + cd docs/website && npm install && npm run generate-docs + + +.PHONY: fmt +fmt: + cargo fmt + cargo fix --allow-dirty \ No newline at end of file diff --git a/README.md b/README.md index 42da9b1d..d4ddf4f8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

httpmock

-

HTTP mocking library for Rust.

+

Simple yet powerful HTTP mocking library for Rust

[![Build](https://github.com/alexliesenfeld/httpmock/actions/workflows/build.yml/badge.svg)](https://github.com/alexliesenfeld/httpmock/actions/workflows/build.yml) @@ -10,11 +10,16 @@ [![crates.io](https://img.shields.io/crates/d/httpmock.svg)](https://crates.io/crates/httpmock) [![Mentioned in Awesome](https://awesome.re/badge.svg)](https://github.com/rust-unofficial/awesome-rust#testing) [![Rust](https://img.shields.io/badge/rust-1.70%2B-blue.svg?maxAge=3600)](https://github.com/rust-lang/rust/blob/master/RELEASES.md#version-1700-2023-06-01) +[![Discord](https://img.shields.io/badge/Chat-Discord-%235865F2.svg)](https://discord.gg/QrjhRh7A)

- Documentation + Documentation + · + API Reference + · + Chat · Crate · @@ -32,13 +37,16 @@ * Simple, expressive, fluent API. * Many built-in helpers for easy request matching ([Regex](https://docs.rs/regex/), JSON, [serde](https://crates.io/crates/serde), cookies, and more). -* Parallel test execution. -* Extensible request matching. -* Fully asynchronous core with synchronous and asynchronous APIs. -* [Advanced verification and debugging support](https://alexliesenfeld.github.io/posts/mocking-http--services-in-rust/#creating-mocks) (including diff generation between actual and expected HTTP request values) +* Record and Playback +* Forward and Proxy Mode +* HTTPS support * Fault and network delay simulation. -* Support for [Regex](https://docs.rs/regex/) matching, JSON, [serde](https://crates.io/crates/serde), cookies, and more. +* Custom request matchers. * Standalone mode with an accompanying [Docker image](https://hub.docker.com/r/alexliesenfeld/httpmock). +* Helpful error messages +* [Advanced verification and debugging support](https://alexliesenfeld.github.io/posts/mocking-http--services-in-rust/#creating-mocks) (including diff generation between actual and expected HTTP request values) +* Parallel test execution. +* Fully asynchronous core with synchronous and asynchronous APIs. * Support for [mock configuration using YAML files](https://github.com/alexliesenfeld/httpmock/tree/master#file-based-mock-specification). ## Getting Started @@ -47,7 +55,7 @@ Add `httpmock` to `Cargo.toml`: ```toml [dev-dependencies] -httpmock = "0.7.0" +httpmock = "0.8.0-alpha.1" ``` You can then use `httpmock` as follows: @@ -83,32 +91,39 @@ The above example will spin up a lightweight HTTP mock server and configure it t to path `/translate` with query parameter `word=hello`. The corresponding HTTP response will contain the text body `Привет`. -In case the request fails, `httpmock` would show you a detailed error description including a diff between the -expected and the actual HTTP request: +When the specified expectations do not match the received request, `httpmock` provides a detailed error description, +including a diff that shows the differences between the expected and actual HTTP requests. Example: -![colored-diff.png](https://raw.githubusercontent.com/alexliesenfeld/httpmock/master/docs/diff.png) +```bash +0 of 1 expected requests matched the mock specification. +Here is a comparison with the most similar unmatched request (request number 1): -# Usage +------------------------------------------------------------ +1 : Query Parameter Mismatch +------------------------------------------------------------ +Expected: + key [equals] word + value [equals] hello-rustaceans -See the [reference docs](https://docs.rs/httpmock/) for detailed API documentation. +Received (most similar query parameter): + word=hello -## Examples +All received query parameter values: + 1. word=hello -You can find examples in the -[`httpmock` test directory](https://github.com/alexliesenfeld/httpmock/blob/master/tests/). -The [reference docs](https://docs.rs/httpmock/) also contain _**a lot**_ of examples. There is an [online tutorial](https://alexliesenfeld.com/mocking-http-services-in-rust) as well. +Matcher: query_param +Docs: https://docs.rs/httpmock/0.8.0/httpmock/struct.When.html#method.query_param +``` -## Standalone Mock Server +# Usage -You can use `httpmock` to run a standalone mock server that is executed in a separate process. There is a -[Docker image](https://hub.docker.com/r/alexliesenfeld/httpmock) available at Dockerhub to get started quickly. +See the [official documentation](http://alexliesenfeld.github.io/httpmock) for detailed API documentation. -The standalone mode allows you to mock HTTP based APIs for many API clients, not only the ones -inside your Rust tests, but also completely different programs running on remote hosts. -This is especially useful if you want to use `httpmock` in system or end-to-end tests that require mocked services -(such as REST APIs, data stores, authentication providers, etc.). +## Examples -Please refer to [the docs](https://docs.rs/httpmock/0.5.8/httpmock/#standalone-mode) for more information +You can find examples in the +[`httpmock` test directory](https://github.com/alexliesenfeld/httpmock/blob/master/tests/). +The [official documentation](http://alexliesenfeld.github.io/httpmock) and [reference docs](https://docs.rs/httpmock/) also contain _**a lot**_ of examples. ## License diff --git a/certs/ca.key b/certs/ca.key new file mode 100644 index 00000000..72bb8fbf --- /dev/null +++ b/certs/ca.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC/XRExWtLyoTUW +ZuDro/XhD4qguNA3nrcFWbxTeg7uuFkS5HvXhnh35awAWx3PpwGzxSknJCMGKoWF +kThR1aWbkcK/veudcBmtlVxk8fYy8YD87Q1I+Or73uQiqgkuJHHWYi8vk/p5hjEZ +q4Ce6+81wNsZWx7yBcX8YZ9E6BdByCGic6gnt3wuLQ3UErHgtFjWOV/hhgZPmYq5 +hfI8Tg5U42+mZ+3RVG9dpLUG/3YaxT3HpguGT2Un8iK0jgeh+LU/XGu1DOxyPo6C +Mg+MTRIKfTteMriE/AoY7GmKjtUYeWovs3QQqUq8VramlgO9i7zMM+IPcPlPscrr +sajissOTAgMBAAECggEAPNtyHkoOEA9ofKlXGllYVqzEn3xm62lUNyVkmP+WRCDo +YvO61r3zDd5CpxJTFri799nZzpLVeJ6JPAME8DGLqz/duXDCv5zo7aU0bv3sGCNp +rAYPVYej41ntH4EHzl7UvSMYcn8TBxbAArPiAahyuJuOki/CVaG0ZyD8r8NHsilQ +SNmhWJpr3tebwkLXpzIHDRNv4CNN7eOkdHXVvuqH5NEL+aqrg0dRY4/mQ8Dya3YF +Om4M7lS7si8sDbEn0DbDJskVVxdVzjqvEc24NBmydKWrXGJXhzqdvqtVVsrsrwNF ++iEmVgkmeD9uQyH4JTHLcZi5C6BqSs8ZGAmcuUB+8QKBgQDr10+37K4YUXTeToS9 +KV6JIMn8k181Jk5WiIUsWwh3Ln6jfM9Ui39ROqGwDnqDDNPcwU2rmXUx4oJK52OE +MEuW64jQpEmC9jwi/1Mqksd6joVZHmofg8r27A7TZf68FGAamq/zvxgvdkCY09px +Qx7ahZU0GrCxm5DWcnMnEDBtlwKBgQDPuH76Ddan6x8VlPMrAY1mlAUH7QEItuVs +ch+Tb+wDfjq7LcOsMGajIAYEwQKmgKKWjE+vr4QF76R8CaHV5QFpVROZGRt+ccJv +SdG6Iln0wZxaAbWiS7j1+a6K/QiGjjuXsKLnIuw2/cpfuesdp1USYhUUJTnbbVy5 +hr8mFUORZQKBgQCbuqomLf/riOYd4UUfT1DgRal6walthCTYWP9vAZF+eVIgDEsv +bYmdjpSzl2voWzEOpQnvlL5hOUuFwHLjF6ziNBc8hi8Qbh3ZkjVNeGyGDdQZu86h +jroYAFnt13y0ntOy3Y/v6LBErtYK1GF6xrJ54xlZtYIVVT73i81j7vm7cwKBgQC7 +wYJUt8l9QpN4SIh8KQ0M2WKqxVmX7On3WjicZiApECI6KqWhsKY1cK7AAU5J/h/4 +gJ9OqBFn5DMDQxmbY0IhWZs7WWx2oJElUs5VttMk3xRabw0kw9lNzQAt9YWNSmcn +N6wnzHNDSadxW3Xf+e51jV6MNRHU+0dDEz8YR0Qp2QKBgH3eXlWq3tBQrwccxUjh +aN3qziwJan0dWFUrclXdu2EgY8T8aOR8D4iHkGPaGR2BPMOxJZO9whFmNex8UYOa +JSvx/Zpno9UheVQkYOeVGyStdD/biN1BJ0rQp72cBqKawaqujzmkuuQhdK0PHodG +W6oGu2c+Yw1vuF12lyWuGXaM +-----END PRIVATE KEY----- diff --git a/certs/ca.pem b/certs/ca.pem new file mode 100644 index 00000000..c7ca12ea --- /dev/null +++ b/certs/ca.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUNR4A1PGeh9qcMWItrv3HvzfkR10wDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIaHR0cG1vY2swIBcNMjQwMzE5MTk1OTMyWhgPMjEyNDAz +MjAxOTU5MzJaMBMxETAPBgNVBAMMCGh0dHBtb2NrMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAv10RMVrS8qE1Fmbg66P14Q+KoLjQN563BVm8U3oO7rhZ +EuR714Z4d+WsAFsdz6cBs8UpJyQjBiqFhZE4UdWlm5HCv73rnXAZrZVcZPH2MvGA +/O0NSPjq+97kIqoJLiRx1mIvL5P6eYYxGauAnuvvNcDbGVse8gXF/GGfROgXQcgh +onOoJ7d8Li0N1BKx4LRY1jlf4YYGT5mKuYXyPE4OVONvpmft0VRvXaS1Bv92GsU9 +x6YLhk9lJ/IitI4Hofi1P1xrtQzscj6OgjIPjE0SCn07XjK4hPwKGOxpio7VGHlq +L7N0EKlKvFa2ppYDvYu8zDPiD3D5T7HK67Go4rLDkwIDAQABo1MwUTAdBgNVHQ4E +FgQUaU+BjH/1Nv4tQUFUcLtoab/zLu4wHwYDVR0jBBgwFoAUaU+BjH/1Nv4tQUFU +cLtoab/zLu4wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAbP+j +e38tpmk5W2aaVuFfGrVAY9XmNVhpAS6Ma7K8REVeSHvPFlZbNXSgkeIhkPKw2f9b +Y5hr9tvTxykL+ZuL6XgULSkTP8R2Ds1s3POGKuJKZ8czQxo1vbeuSS2quGFx8c3a +blMtXMbrhFWlq4qeTSZ94qJR6aXnSJJT0veZcaTNXg93VKmUnQDsAfF7Cg7b41P5 +WWBGJVLHtvfN4MoHLHHM+Uj730lnCN3Td7mIfeiwLpzcsws5aI+5aeDST2Hpv+De +iofRlpJMC6aGtsbE5PpyOJowTp1n2A3XgaFaIDZ1+yonqVnUTkGeNf603ctruEit +a+EcK/GxLDywZUeRKQ== +-----END CERTIFICATE----- diff --git a/deny.toml b/deny.toml new file mode 100644 index 00000000..db146b38 --- /dev/null +++ b/deny.toml @@ -0,0 +1,237 @@ +# This template contains all of the possible sections and their default values + +# Note that all fields that take a lint level have these possible values: +# * deny - An error will be produced and the check will fail +# * warn - A warning will be produced, but the check will not fail +# * allow - No warning or error will be produced, though in some cases a note +# will be + +# The values provided in this template are the default values that will be used +# when any section or field is not specified in your own configuration + +# Root options + +# The graph table configures how the dependency graph is constructed and thus +# which crates the checks are performed against +[graph] +# If 1 or more target triples (and optionally, target_features) are specified, +# only the specified targets will be checked when running `cargo deny check`. +# This means, if a particular package is only ever used as a target specific +# dependency, such as, for example, the `nix` crate only being used via the +# `target_family = "unix"` configuration, that only having windows targets in +# this list would mean the nix crate, as well as any of its exclusive +# dependencies not shared by any other crates, would be ignored, as the target +# list here is effectively saying which targets you are building for. +targets = [ + # The triple can be any string, but only the target triples built in to + # rustc (as of 1.40) can be checked against actual config expressions + #"x86_64-unknown-linux-musl", + # You can also specify which target_features you promise are enabled for a + # particular target. target_features are currently not validated against + # the actual valid features supported by the target architecture. + #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, +] +# When creating the dependency graph used as the source of truth when checks are +# executed, this field can be used to prune crates from the graph, removing them +# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate +# is pruned from the graph, all of its dependencies will also be pruned unless +# they are connected to another crate in the graph that hasn't been pruned, +# so it should be used with care. The identifiers are [Package ID Specifications] +# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html) +#exclude = [] +# If true, metadata will be collected with `--all-features`. Note that this can't +# be toggled off if true, if you want to conditionally enable `--all-features` it +# is recommended to pass `--all-features` on the cmd line instead +all-features = false +# If true, metadata will be collected with `--no-default-features`. The same +# caveat with `all-features` applies +no-default-features = false +# If set, these feature will be enabled when collecting metadata. If `--features` +# is specified on the cmd line they will take precedence over this option. +#features = [] + +# The output table provides options for how/if diagnostics are outputted +[output] +# When outputting inclusion graphs in diagnostics that include features, this +# option can be used to specify the depth at which feature edges will be added. +# This option is included since the graphs can be quite large and the addition +# of features from the crate(s) to all of the graph roots can be far too verbose. +# This option can be overridden via `--feature-depth` on the cmd line +feature-depth = 1 + +# This section is considered when running `cargo deny check advisories` +# More documentation for the advisories section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html +[advisories] +# The path where the advisory databases are cloned/fetched into +#db-path = "$CARGO_HOME/advisory-dbs" +# The url(s) of the advisory databases to use +#db-urls = ["https://github.com/rustsec/advisory-db"] +# A list of advisory IDs to ignore. Note that ignored advisories will still +# output a note when they are encountered. +ignore = [ + #"RUSTSEC-0000-0000", + #{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" }, + #"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish + #{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" }, +] +# If this is true, then cargo deny will use the git executable to fetch advisory database. +# If this is false, then it uses a built-in git library. +# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. +# See Git Authentication for more information about setting up git authentication. +#git-fetch-with-cli = true + +# This section is considered when running `cargo deny check licenses` +# More documentation for the licenses section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html +[licenses] +# List of explicitly allowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. +allow = [ + "MIT", + "Apache-2.0", + #"Apache-2.0 WITH LLVM-exception", +] +# The confidence threshold for detecting a license from license text. +# The higher the value, the more closely the license text must be to the +# canonical license text of a valid SPDX license file. +# [possible values: any between 0.0 and 1.0]. +confidence-threshold = 1.0 +# Allow 1 or more licenses on a per-crate basis, so that particular licenses +# aren't accepted for every possible crate as with the normal allow list +exceptions = [ + # Each entry is the crate and version constraint, and its specific allow + # list + { allow = ["BSD-3-Clause"], crate = "instant" }, + { allow = ["CC0-1.0"], crate = "tiny-keccak" }, + { allow = ["Unicode-DFS-2016"], crate = "unicode-ident" }, +] + +# Some crates don't have (easily) machine readable licensing information, +# adding a clarification entry for it allows you to manually specify the +# licensing information +#[[licenses.clarify]] +# The package spec the clarification applies to +#crate = "ring" +# The SPDX expression for the license requirements of the crate +#expression = "MIT AND ISC AND OpenSSL" +# One or more files in the crate's source used as the "source of truth" for +# the license expression. If the contents match, the clarification will be used +# when running the license check, otherwise the clarification will be ignored +# and the crate will be checked normally, which may produce warnings or errors +# depending on the rest of your configuration +#license-files = [ +# Each entry is a crate relative path, and the (opaque) hash of its contents +#{ path = "LICENSE", hash = 0xbd0eed23 } +#] + +[licenses.private] +# If true, ignores workspace crates that aren't published, or are only +# published to private registries. +# To see how to mark a crate as unpublished (to the official registry), +# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. +ignore = false +# One or more private registries that you might publish crates to, if a crate +# is only published to private registries, and ignore is true, the crate will +# not have its license(s) checked +registries = [ + #"https://sekretz.com/registry +] + +# This section is considered when running `cargo deny check bans`. +# More documentation about the 'bans' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html +[bans] +# Lint level for when multiple versions of the same crate are detected +multiple-versions = "warn" +# Lint level for when a crate version requirement is `*` +wildcards = "allow" +# The graph highlighting used when creating dotgraphs for crates +# with multiple versions +# * lowest-version - The path to the lowest versioned duplicate is highlighted +# * simplest-path - The path to the version with the fewest edges is highlighted +# * all - Both lowest-version and simplest-path are used +highlight = "all" +# The default lint level for `default` features for crates that are members of +# the workspace that is being checked. This can be overridden by allowing/denying +# `default` on a crate-by-crate basis if desired. +workspace-default-features = "allow" +# The default lint level for `default` features for external crates that are not +# members of the workspace. This can be overridden by allowing/denying `default` +# on a crate-by-crate basis if desired. +external-default-features = "allow" +# List of crates that are allowed. Use with care! +allow = [ + #"ansi_term@0.11.0", + #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" }, +] +# List of crates to deny +deny = [ + #"ansi_term@0.11.0", + #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" }, + # Wrapper crates can optionally be specified to allow the crate when it + # is a direct dependency of the otherwise banned crate + #{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] }, +] + +# List of features to allow/deny +# Each entry the name of a crate and a version range. If version is +# not specified, all versions will be matched. +#[[bans.features]] +#crate = "reqwest" +# Features to not allow +#deny = ["json"] +# Features to allow +#allow = [ +# "rustls", +# "__rustls", +# "__tls", +# "hyper-rustls", +# "rustls", +# "rustls-pemfile", +# "rustls-tls-webpki-roots", +# "tokio-rustls", +# "webpki-roots", +#] +# If true, the allowed features must exactly match the enabled feature set. If +# this is set there is no point setting `deny` +#exact = true + +# Certain crates/versions that will be skipped when doing duplicate detection. +skip = [ + #"ansi_term@0.11.0", + #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" }, +] +# Similarly to `skip` allows you to skip certain crates during duplicate +# detection. Unlike skip, it also includes the entire tree of transitive +# dependencies starting at the specified crate, up to a certain depth, which is +# by default infinite. +skip-tree = [ + #"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies + #{ crate = "ansi_term@0.11.0", depth = 20 }, +] + +# This section is considered when running `cargo deny check sources`. +# More documentation about the 'sources' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html +[sources] +# Lint level for what to happen when a crate from a crate registry that is not +# in the allow list is encountered +unknown-registry = "deny" +# Lint level for what to happen when a crate from a git repository that is not +# in the allow list is encountered +unknown-git = "deny" +# List of URLs for allowed crate registries. Defaults to the crates.io index +# if not specified. If it is specified but empty, no registries are allowed. +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +# List of URLs for allowed Git repositories +allow-git = [] + +[sources.allow-org] +# 1 or more github.com organizations to allow git sources for +# github = [""] +# 1 or more gitlab.com organizations to allow git sources for +# gitlab = [""] +# 1 or more bitbucket.org organizations to allow git sources for +# bitbucket = [""] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..c77757bf --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3.8' + +services: + server: + build: + context: . + dockerfile: Dockerfile + ports: + - "5050:5050" + volumes: + - ./tests/resources/simple_static_mock.yaml:/static-mocks/simple_static_mock.yaml + environment: + - HTTPMOCK_MOCK_FILES_DIR=/static-mocks diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..a8e405d4 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,23 @@ +# build output +dist/ +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store +/website/src/content/docs/matching_requests +/website/src/content/docs/mocking_responses diff --git a/docs/.vscode/extensions.json b/docs/.vscode/extensions.json new file mode 100644 index 00000000..22a15055 --- /dev/null +++ b/docs/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + "recommendations": ["astro-build.astro-vscode"], + "unwantedRecommendations": [] +} diff --git a/docs/.vscode/launch.json b/docs/.vscode/launch.json new file mode 100644 index 00000000..d6422097 --- /dev/null +++ b/docs/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "command": "./node_modules/.bin/astro dev", + "name": "Development server", + "request": "launch", + "type": "node-terminal" + } + ] +} diff --git a/docs/assets/Logo-design.afdesign b/docs/assets/Logo-design.afdesign new file mode 100644 index 00000000..abdb0efd Binary files /dev/null and b/docs/assets/Logo-design.afdesign differ diff --git a/docs/assets/Logo.afdesign b/docs/assets/Logo.afdesign new file mode 100644 index 00000000..5c7c246a Binary files /dev/null and b/docs/assets/Logo.afdesign differ diff --git a/docs/diff.png b/docs/assets/diff.png similarity index 100% rename from docs/diff.png rename to docs/assets/diff.png diff --git a/docs/website/README.md b/docs/website/README.md new file mode 100644 index 00000000..b51abaab --- /dev/null +++ b/docs/website/README.md @@ -0,0 +1,54 @@ +# Starlight Starter Kit: Basics + +[![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build) + +``` +npm create astro@latest -- --template starlight +``` + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics) +[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs) + +> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! + +## 🚀 Project Structure + +Inside of your Astro + Starlight project, you'll see the following folders and files: + +``` +. +├── public/ +├── src/ +│ ├── assets/ +│ ├── content/ +│ │ ├── docs/ +│ │ └── config.ts +│ └── env.d.ts +├── astro.config.mjs +├── package.json +└── tsconfig.json +``` + +Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name. + +Images can be added to `src/assets/` and embedded in Markdown with a relative link. + +Static assets, like favicons, can be placed in the `public/` directory. + +## 🧞 Commands + +All commands are run from the root of the project, from a terminal: + +| Command | Action | +| :------------------------ | :----------------------------------------------- | +| `npm install` | Installs dependencies | +| `npm run dev` | Starts local dev server at `localhost:4321` | +| `npm run build` | Build your production site to `./dist/` | +| `npm run preview` | Preview your build locally, before deploying | +| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | +| `npm run astro -- --help` | Get help using the Astro CLI | + +## 👀 Want to learn more? + +Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat). diff --git a/docs/website/astro.config.mjs b/docs/website/astro.config.mjs new file mode 100644 index 00000000..6513ca72 --- /dev/null +++ b/docs/website/astro.config.mjs @@ -0,0 +1,87 @@ +import {defineConfig} from 'astro/config'; +import starlight from '@astrojs/starlight'; + +// https://astro.build/config +export default defineConfig({ + site: 'https://alexliesenfeld.github.io/httpmock', + base: 'httpmock', // so that 'https://alexliesenfeld.github.io/httpmock' will be set as the base path + integrations: [ + starlight({ + title: 'httpmock Tutorial', + logo: { + light: './src/assets/logo-light.svg', + dark: './src/assets/logo-dark.svg', + replacesTitle: true, + }, + social: { + github: 'https://github.com/withastro/starlight', + discord: 'https://discord.gg/QrjhRh7A' + }, + sidebar: [ + { + label: 'Getting Started', + items: [ + // Each item here is one entry in the navigation menu. + {label: 'Quick Introduction', link: '/getting_started/quick_introduction/'}, + {label: 'Fundamentals', link: '/getting_started/fundamentals/'}, + {label: 'Resources', link: '/getting_started/resources/'}, + ], + }, + { + label: 'Mocking', + items: [ + // Each item here is one entry in the navigation menu. + { + label: 'Matching Requests', + items: [ + // Each item here is one entry in the navigation menu. + {label: 'Path', link: '/matching_requests/path/'}, + {label: 'Method', link: '/matching_requests/method/'}, + {label: 'Query Parameters', link: '/matching_requests/query/'}, + {label: 'Headers', link: '/matching_requests/headers/'}, + {label: 'Body', link: '/matching_requests/body/'}, + {label: 'Cookie', link: '/matching_requests/cookies/'}, + {label: 'Host', link: '/matching_requests/host/'}, + {label: 'Port', link: '/matching_requests/port/'}, + {label: 'Scheme', link: '/matching_requests/scheme/'}, + {label: 'Custom Matchers', link: '/matching_requests/custom/'}, + ], + + }, + { + label: 'Mocking Responses', items: [ + // Each item here is one entry in the navigation menu. + {label: 'Response Values', link: '/mocking_responses/all/'}, + {label: 'Network Delay', link: '/mocking_responses/delay/'}, + ], + }, + ], + }, + { + label: 'Record and Playback', + badge: 'New', + items: [ + {label: 'Recording', link: '/record-and-playback/recording/'}, + {label: 'Playback', link: '/record-and-playback/playback/'}, + ], + }, + { + label: 'Server', + items: [ + {label: 'Standalone Server', link: '/server/standalone/'}, + {label: 'HTTPS', link: '/server/https/', badge: 'New'}, + {label: 'Debugging', link: '/server/debugging/'}, + ], + }, + { + label: 'Miscellaneous', + items: [ + {label: 'FAQ', link: '/miscellaneous/faq/'}, + {label: 'License', link: 'https://github.com/alexliesenfeld/httpmock/blob/master/LICENSE'}, + ], + }, + ], + customCss: ['./src/assets/landing.css'], + }), + ], +}); diff --git a/docs/website/generated/code_examples.json b/docs/website/generated/code_examples.json new file mode 100644 index 00000000..86534c4a --- /dev/null +++ b/docs/website/generated/code_examples.json @@ -0,0 +1,104 @@ +{ + "then": { + "and": "```rust\n use std::time::Duration;\n use http::{StatusCode, header::HeaderValue};\n use httpmock::{Then, MockServer};\n\n // Function that configures a response with JSON content and a delay\n fn ok_json_with_delay(then: Then) -> Then {\n then.status(StatusCode::OK.as_u16())\n .header(\"content-type\", \"application/json\")\n .delay(Duration::from_secs_f32(0.5))\n }\n\n // Usage within a method chain\n let server = MockServer::start();\n let then = server.mock(|when, then| {\n when.path(\"/example\");\n then.header(\"general-vibe\", \"much better\")\n .and(ok_json_with_delay);\n });\n\n // The `and` method keeps the setup intuitively readable as a continuous chain\n```\n", + "body": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Initialize the mock server\n let server = MockServer::start();\n\n // Configure the mock\n let m = server.mock(|when, then| {\n when.path(\"/hello\");\n then.status(200)\n .body(\"ohi!\");\n });\n\n // Send a request and verify the response\n let response = Client::new()\n .get(server.url(\"/hello\"))\n .send()\n .unwrap();\n\n // Check that the mock was called as expected and the response body is as configured\n m.assert();\n assert_eq!(response.status(), 200);\n assert_eq!(response.text().unwrap(), \"ohi!\");\n```\n", + "body_from_file": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Initialize the mock server\n let server = MockServer::start();\n\n // Configure the mock\n let m = server.mock(|when, then| {\n when.path(\"/hello\");\n then.status(200)\n .body_from_file(\"tests/resources/simple_body.txt\");\n });\n\n // Send a request and verify the response\n let response = Client::new()\n .get(server.url(\"/hello\"))\n .send()\n .unwrap();\n\n // Check that the mock was called as expected and the response body matches the file contents\n m.assert();\n assert_eq!(response.status(), 200);\n assert_eq!(response.text().unwrap(), \"ohi!\");\n```\n", + "delay": "```rust\n use std::time::{SystemTime, Duration};\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Arrange\n let _ = env_logger::try_init();\n let start_time = SystemTime::now();\n let three_seconds = Duration::from_secs(3);\n let server = MockServer::start();\n\n // Configure the mock\n let mock = server.mock(|when, then| {\n when.path(\"/delay\");\n then.status(200)\n .delay(three_seconds);\n });\n\n // Act\n let response = Client::new()\n .get(server.url(\"/delay\"))\n .send()\n .unwrap();\n\n // Assert\n mock.assert();\n assert!(start_time.elapsed().unwrap() >= three_seconds);\n```\n", + "header": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Arrange\n let _ = env_logger::try_init();\n let server = MockServer::start();\n\n // Configure the mock\n let m = server.mock(|when, then| {\n when.path(\"/\");\n then.status(200)\n .header(\"Expires\", \"Wed, 21 Oct 2050 07:28:00 GMT\");\n });\n\n // Act\n let response = Client::new()\n .get(server.url(\"/\"))\n .send()\n .unwrap();\n\n // Assert\n m.assert();\n assert_eq!(response.status(), 200);\n assert_eq!(\n response.headers().get(\"Expires\").unwrap().to_str().unwrap(),\n \"Wed, 21 Oct 2050 07:28:00 GMT\"\n );\n```\n", + "json_body": "```rust\n use httpmock::prelude::*;\n use serde_json::{Value, json};\n use reqwest::blocking::Client;\n\n // Arrange\n let _ = env_logger::try_init();\n let server = MockServer::start();\n\n // Configure the mock\n let m = server.mock(|when, then| {\n when.path(\"/user\");\n then.status(200)\n .header(\"content-type\", \"application/json\")\n .json_body(json!({ \"name\": \"Hans\" }));\n });\n\n // Act\n let response = Client::new()\n .get(server.url(\"/user\"))\n .send()\n .unwrap();\n\n // Get the status code first\n let status = response.status();\n\n // Extract the text from the response\n let response_text = response.text().unwrap();\n\n // Deserialize the JSON response\n let user: Value =\n serde_json::from_str(&response_text).expect(\"cannot deserialize JSON\");\n\n // Assert\n m.assert();\n assert_eq!(status, 200);\n assert_eq!(user[\"name\"], \"Hans\");\n```\n", + "json_body_obj": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n use serde::{Serialize, Deserialize};\n\n #[derive(Serialize, Deserialize)]\n struct TestUser {\n name: String,\n }\n\n // Arrange\n let _ = env_logger::try_init();\n let server = MockServer::start();\n\n // Configure the mock\n let m = server.mock(|when, then| {\n when.path(\"/user\");\n then.status(200)\n .header(\"content-type\", \"application/json\")\n .json_body_obj(&TestUser {\n name: String::from(\"Hans\"),\n });\n });\n\n // Act\n let response = Client::new()\n .get(server.url(\"/user\"))\n .send()\n .unwrap();\n\n // Get the status code first\n let status = response.status();\n\n // Extract the text from the response\n let response_text = response.text().unwrap();\n\n // Deserialize the JSON response into a TestUser object\n let user: TestUser =\n serde_json::from_str(&response_text).unwrap();\n\n // Assert\n m.assert();\n assert_eq!(status, 200);\n assert_eq!(user.name, \"Hans\");\n```\n", + "status": "```rust\n use httpmock::prelude::*;\n\n // Initialize the mock server\n let server = MockServer::start();\n\n // Configure the mock\n let m = server.mock(|when, then| {\n when.path(\"/hello\");\n then.status(200);\n });\n\n // Send a request and verify the response\n let response = reqwest::blocking::get(server.url(\"/hello\")).unwrap();\n\n // Check that the mock was called as expected and the response status is as configured\n m.assert();\n assert_eq!(response.status(), 200);\n```\n" + }, + "when": { + "and": "```rust\n use httpmock::{prelude::*, When};\n use httpmock::Method::POST;\n\n // Function to apply a standard authorization and content type setup for JSON POST requests\n fn is_authorized_json_post_request(when: When) -> When {\n when.method(POST)\n .header(\"Authorization\", \"SOME API KEY\")\n .header(\"Content-Type\", \"application/json\")\n }\n\n // Usage example demonstrating how to maintain fluent interface style with complex setups.\n // This approach keeps the chain of conditions clear and readable, enhancing test legibility\n let server = MockServer::start();\n let m = server.mock(|when, then| {\n when.query_param(\"user_id\", \"12345\")\n .and(is_authorized_json_post_request) // apply the function to include common setup\n .json_body_includes(r#\"{\"key\": \"value\"}\"#); // additional specific condition\n then.status(200);\n });\n```\n", + "any_request": "```rust\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Configure the mock server to respond to any request\n let mock = server.mock(|when, then| {\n when.any_request(); // Explicitly specify that any request should match\n then.status(200); // Respond with status code 200 for all matched requests\n });\n\n // Make a request to the server's URL and ensure the mock is triggered\n let response = reqwest::blocking::get(server.url(\"/anyPath\")).unwrap();\n\n // Ensure the request was successful\n assert_eq!(response.status(), 200);\n\n // Assert that the mock was called at least once\n mock.assert();\n```\n", + "body": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the request body to be \"The Great Gatsby\"\n let mock = server.mock(|when, then| {\n when.body(\"The Great Gatsby\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request with the required body content\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .body(\"The Great Gatsby\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "body_excludes": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the request body to not contain the substring \"Gatsby\"\n let mock = server.mock(|when, then| {\n when.body_excludes(\"Gatsby\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request with a different body content\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .body(\"A Tale of Two Cities is a novel.\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "body_includes": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the request body to contain the substring \"Gatsby\"\n let mock = server.mock(|when, then| {\n when.body_includes(\"Gatsby\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request with the required substring in the body content\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .body(\"The Great Gatsby is a novel.\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "body_matches": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the request body to match the regex pattern \"^The Great Gatsby.*\"\n let mock = server.mock(|when, then| {\n when.body_matches(\"^The Great Gatsby.*\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request with a body that matches the regex pattern\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .body(\"The Great Gatsby is a novel by F. Scott Fitzgerald.\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "body_not": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the request body to not be \"The Great Gatsby\"\n let mock = server.mock(|when, then| {\n when.body_not(\"The Great Gatsby\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request with a different body content\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .body(\"A Tale of Two Cities\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "body_prefix": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the request body to begin with the substring \"The Great\"\n let mock = server.mock(|when, then| {\n when.body_prefix(\"The Great\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request with the required prefix in the body content\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .body(\"The Great Gatsby is a novel.\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "body_prefix_not": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the request body to not begin with the substring \"Error:\"\n let mock = server.mock(|when, then| {\n when.body_prefix_not(\"Error:\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request with a different body content\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .body(\"Success: Operation completed.\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "body_suffix": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the request body to end with the substring \"a novel.\"\n let mock = server.mock(|when, then| {\n when.body_suffix(\"a novel.\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request with the required suffix in the body content\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .body(\"The Great Gatsby is a novel.\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "body_suffix_not": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the request body to not end with the substring \"a novel.\"\n let mock = server.mock(|when, then| {\n when.body_suffix_not(\"a novel.\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request with a different body content\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .body(\"The Great Gatsby is a story.\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "cookie": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects a cookie named \"SESSIONID\" with the value \"1234567890\"\n let mock = server.mock(|when, then| {\n when.cookie(\"SESSIONID\", \"1234567890\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request that includes the required cookie\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; SESSIONID=1234567890; CONSENT=1\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "cookie_count": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects a cookie with a name matching the regex \"^SESSION\"\n // and a value matching the regex \"^[0-9]{10}$\" to appear exactly twice\n let mock = server.mock(|when, then| {\n when.cookie_count(r\"^SESSION\", r\"^[0-9]{10}$\", 2);\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request that includes the required cookies\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"SESSIONID=1234567890; TRACK=12345; SESSIONTOKEN=0987654321; CONSENT=1\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "cookie_excludes": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects a cookie named \"SESSIONID\" with a value not containing \"1234\"\n let mock = server.mock(|when, then| {\n when.cookie_excludes(\"SESSIONID\", \"1234\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request that includes the required cookie\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; SESSIONID=abcdef; CONSENT=1\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "cookie_exists": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects a cookie named \"SESSIONID\"\n let mock = server.mock(|when, then| {\n when.cookie_exists(\"SESSIONID\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request that includes the required cookie\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; SESSIONID=1234567890; CONSENT=1\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "cookie_includes": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects a cookie named \"SESSIONID\" with a value containing \"1234\"\n let mock = server.mock(|when, then| {\n when.cookie_includes(\"SESSIONID\", \"1234\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request that includes the required cookie\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; SESSIONID=abc1234def; CONSENT=1\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "cookie_matches": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects a cookie with a name matching the regex \"^SESSION\"\n // and a value matching the regex \"^[0-9]{10}$\"\n let mock = server.mock(|when, then| {\n when.cookie_matches(r\"^SESSION\", r\"^[0-9]{10}$\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request that includes the required cookie\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; SESSIONID=1234567890; CONSENT=1\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "cookie_missing": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects a cookie named \"SESSIONID\" not to exist\n let mock = server.mock(|when, then| {\n when.cookie_missing(\"SESSIONID\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request that does not include the excluded cookie\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; CONSENT=1\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "cookie_not": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects a cookie named \"SESSIONID\" to not have the value \"1234567890\"\n let mock = server.mock(|when, then| {\n when.cookie_not(\"SESSIONID\", \"1234567890\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request that includes the required cookie\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; SESSIONID=0987654321; CONSENT=1\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "cookie_prefix": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects a cookie named \"SESSIONID\" with a value starting with \"1234\"\n let mock = server.mock(|when, then| {\n when.cookie_prefix(\"SESSIONID\", \"1234\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request that includes the required cookie\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; SESSIONID=1234abcdef; CONSENT=1\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "cookie_prefix_not": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects a cookie named \"SESSIONID\" with a value not starting with \"1234\"\n let mock = server.mock(|when, then| {\n when.cookie_prefix_not(\"SESSIONID\", \"1234\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request that includes the required cookie\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; SESSIONID=abcd1234; CONSENT=1\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "cookie_suffix": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects a cookie named \"SESSIONID\" with a value ending with \"7890\"\n let mock = server.mock(|when, then| {\n when.cookie_suffix(\"SESSIONID\", \"7890\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request that includes the required cookie\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; SESSIONID=abcdef7890; CONSENT=1\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "cookie_suffix_not": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects a cookie named \"SESSIONID\" with a value not ending with \"7890\"\n let mock = server.mock(|when, then| {\n when.cookie_suffix_not(\"SESSIONID\", \"7890\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request that includes the required cookie\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; SESSIONID=abcdef1234; CONSENT=1\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "form_urlencoded_tuple": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Arrange\n let server = MockServer::start();\n\n let m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple(\"name\", \"Peter Griffin\")\n .form_urlencoded_tuple(\"town\", \"Quahog\");\n then.status(202);\n });\n\n let response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Peter%20Griffin&town=Quahog\")\n .send()\n .unwrap();\n\n // Assert\n m.assert();\n assert_eq!(response.status(), 202);\n```\n", + "form_urlencoded_tuple_count": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n use regex::Regex;\n\n // Arrange\n let server = MockServer::start();\n\n let m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_count(\n Regex::new(r\"^name$\").unwrap(),\n Regex::new(r\".*Griffin$\").unwrap(),\n 2\n );\n then.status(202);\n });\n\n // Act\n let response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Peter%20Griffin&name=Lois%20Griffin&town=Quahog\")\n .send()\n .unwrap();\n\n // Assert\n m.assert();\n assert_eq!(response.status(), 202);\n```\n", + "form_urlencoded_tuple_excludes": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Arrange\n let server = MockServer::start();\n\n let m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_excludes(\"name\", \"Griffin\");\n then.status(202);\n });\n\n let response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Lois%20Smith&city=Quahog\")\n .send()\n .unwrap();\n\n // Assert\n m.assert();\n assert_eq!(response.status(), 202);\n```\n", + "form_urlencoded_tuple_exists": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Arrange\n let server = MockServer::start();\n\n let m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_exists(\"name\")\n .form_urlencoded_tuple_exists(\"town\");\n then.status(202);\n });\n\n let response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Peter%20Griffin&town=Quahog\")\n .send()\n .unwrap();\n\n // Assert\n m.assert();\n assert_eq!(response.status(), 202);\n```\n", + "form_urlencoded_tuple_includes": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Arrange\n let server = MockServer::start();\n\n let m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_includes(\"name\", \"Griffin\")\n .form_urlencoded_tuple_includes(\"town\", \"Quahog\");\n then.status(202);\n });\n\n let response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Peter%20Griffin&town=Quahog\")\n .send()\n .unwrap();\n\n // Assert\n m.assert();\n assert_eq!(response.status(), 202);\n```\n", + "form_urlencoded_tuple_matches": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n use regex::Regex;\n\n // Arrange\n let server = MockServer::start();\n\n let key_regex = Regex::new(r\"^name$\").unwrap();\n let value_regex = Regex::new(r\"^Peter\\sGriffin$\").unwrap();\n\n let m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_matches(key_regex, value_regex);\n then.status(202);\n });\n\n let response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Peter%20Griffin&town=Quahog\")\n .send()\n .unwrap();\n\n // Assert\n m.assert();\n assert_eq!(response.status(), 202);\n```\n", + "form_urlencoded_tuple_missing": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Arrange\n let server = MockServer::start();\n\n let m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_missing(\"name\")\n .form_urlencoded_tuple_missing(\"town\");\n then.status(202);\n });\n\n let response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"city=Quahog&occupation=Cartoonist\")\n .send()\n .unwrap();\n\n // Assert\n m.assert();\n assert_eq!(response.status(), 202);\n```\n", + "form_urlencoded_tuple_not": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Arrange\n let server = MockServer::start();\n\n let m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_not(\"name\", \"Peter Griffin\");\n then.status(202);\n });\n\n let response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Lois%20Griffin&town=Quahog\")\n .send()\n .unwrap();\n\n // Assert\n m.assert();\n assert_eq!(response.status(), 202);\n```\n", + "form_urlencoded_tuple_prefix": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Arrange\n let server = MockServer::start();\n\n let m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_prefix(\"name\", \"Pete\")\n .form_urlencoded_tuple_prefix(\"town\", \"Qua\");\n then.status(202);\n });\n\n let response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Peter%20Griffin&town=Quahog\")\n .send()\n .unwrap();\n\n // Assert\n m.assert();\n assert_eq!(response.status(), 202);\n```\n", + "form_urlencoded_tuple_prefix_not": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Arrange\n let server = MockServer::start();\n\n let m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_prefix_not(\"name\", \"Lois\")\n .form_urlencoded_tuple_prefix_not(\"town\", \"Hog\");\n then.status(202);\n });\n\n let response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Peter%20Griffin&town=Quahog\")\n .send()\n .unwrap();\n\n // Assert\n m.assert();\n assert_eq!(response.status(), 202);\n```\n", + "form_urlencoded_tuple_suffix": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Arrange\n let server = MockServer::start();\n\n let m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_suffix(\"name\", \"Griffin\")\n .form_urlencoded_tuple_suffix(\"town\", \"hog\");\n then.status(202);\n });\n\n let response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Peter%20Griffin&town=Quahog\")\n .send()\n .unwrap();\n\n // Assert\n m.assert();\n assert_eq!(response.status(), 202);\n```\n", + "form_urlencoded_tuple_suffix_not": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Arrange\n let server = MockServer::start();\n\n let m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_suffix_not(\"name\", \"Smith\")\n .form_urlencoded_tuple_suffix_not(\"town\", \"ville\");\n then.status(202);\n });\n\n let response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Peter%20Griffin&town=Quahog\")\n .send()\n .unwrap();\n\n // Assert\n m.assert();\n assert_eq!(response.status(), 202);\n```\n", + "header": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the \"Authorization\" header with a specific value\n let mock = server.mock(|when, then| {\n when.header(\"Authorization\", \"token 1234567890\");\n then.status(200); // Respond with a 200 status code if the header and value are present\n });\n\n // Make a request that includes the \"Authorization\" header with the specified value\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Authorization\", \"token 1234567890\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "header_count": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects at least 2 headers whose keys match the regex \"^X-Custom-Header.*\"\n // and values match the regex \"value.*\"\n let mock = server.mock(|when, then| {\n when.header_count(\"^X-Custom-Header.*\", \"value.*\", 2);\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request that includes the required headers\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"x-custom-header-1\", \"value1\")\n .header(\"X-Custom-Header-2\", \"value2\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "header_excludes": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the \"Authorization\" header's value to not contain \"Bearer\"\n let mock = server.mock(|when, then| {\n when.header_excludes(\"Authorization\", \"Bearer\");\n then.status(200); // Respond with a 200 status code if the header value does not contain the substring\n });\n\n // Make a request that includes the \"Authorization\" header without the forbidden substring in its value\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Authorization\", \"token 1234567890\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "header_exists": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the \"Authorization\" header to be present in the request\n let mock = server.mock(|when, then| {\n when.header_exists(\"Authorization\");\n then.status(200); // Respond with a 200 status code if the header is present\n });\n\n // Make a request that includes the \"Authorization\" header\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Authorization\", \"token 1234567890\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "header_includes": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the \"Authorization\" header's value to contain \"token\"\n let mock = server.mock(|when, then| {\n when.header_includes(\"Authorization\", \"token\");\n then.status(200); // Respond with a 200 status code if the header value contains the substring\n });\n\n // Make a request that includes the \"Authorization\" header with the specified substring in its value\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Authorization\", \"token 1234567890\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "header_matches": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the \"Authorization\" header's key to match the regex \"^Auth.*\"\n // and its value to match the regex \"token .*\"\n let mock = server.mock(|when, then| {\n when.header_matches(\"^Auth.*\", \"token .*\");\n then.status(200); // Respond with a 200 status code if the header key and value match the patterns\n });\n\n // Make a request that includes the \"Authorization\" header with a value matching the regex\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Authorization\", \"token 1234567890\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "header_missing": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the \"Authorization\" header to be absent in the request\n let mock = server.mock(|when, then| {\n when.header_missing(\"Authorization\");\n then.status(200); // Respond with a 200 status code if the header is absent\n });\n\n // Make a request that does not include the \"Authorization\" header\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "header_not": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the \"Authorization\" header with a specific value to be absent\n let mock = server.mock(|when, then| {\n when.header_not(\"Authorization\", \"token 1234567890\");\n then.status(200); // Respond with a 200 status code if the header and value are absent\n });\n\n // Make a request that includes the \"Authorization\" header with a different value\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Authorization\", \"token abcdefg\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "header_prefix": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the \"Authorization\" header's value to start with \"token\"\n let mock = server.mock(|when, then| {\n when.header_prefix(\"Authorization\", \"token\");\n then.status(200); // Respond with a 200 status code if the header value starts with the prefix\n });\n\n // Make a request that includes the \"Authorization\" header with the specified prefix in its value\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Authorization\", \"token 1234567890\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "header_prefix_not": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the \"Authorization\" header's value to not start with \"Bearer\"\n let mock = server.mock(|when, then| {\n when.header_prefix_not(\"Authorization\", \"Bearer\");\n then.status(200); // Respond with a 200 status code if the header value does not start with the prefix\n });\n\n // Make a request that includes the \"Authorization\" header without the \"Bearer\" prefix in its value\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Authorization\", \"token 1234567890\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "header_suffix": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the \"Authorization\" header's value to end with \"7890\"\n let mock = server.mock(|when, then| {\n when.header_suffix(\"Authorization\", \"7890\");\n then.status(200); // Respond with a 200 status code if the header value ends with the suffix\n });\n\n // Make a request that includes the \"Authorization\" header with the specified suffix in its value\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Authorization\", \"token 1234567890\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "header_suffix_not": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the \"Authorization\" header's value to not end with \"abc\"\n let mock = server.mock(|when, then| {\n when.header_suffix_not(\"Authorization\", \"abc\");\n then.status(200); // Respond with a 200 status code if the header value does not end with the suffix\n });\n\n // Make a request that includes the \"Authorization\" header without the \"abc\" suffix in its value\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Authorization\", \"token 1234567890\")\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "host": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n let server = MockServer::start();\n\n server.mock(|when, then| {\n when.host(\"github.com\");\n then.body(\"This is a mock response\");\n });\n\n let client = Client::builder()\n .proxy(reqwest::Proxy::all(&server.base_url()).unwrap())\n .build()\n .unwrap();\n\n let response = client.get(\"http://github.com\").send().unwrap();\n\n assert_eq!(response.text().unwrap(), \"This is a mock response\");\n```\n", + "host_excludes": "```rust\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that excludes any request where the host name contains \"www.google.com\"\n let mock = server.mock(|when, then| {\n when.host_excludes(\"www.google.com\"); // Exclude hosts containing \"www.google.com\"\n then.status(200); // Respond with status code 200 for other matched requests\n });\n\n // Make a request to a URL whose host name will be \"localhost\" and trigger the mock\n let response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n // Ensure the request was successful\n assert_eq!(response.status(), 200);\n\n // Ensure that the mock was called at least once\n mock.assert();\n```\n", + "host_includes": "```rust\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that matches any request where the host name contains \"localhost\"\n let mock = server.mock(|when, then| {\n when.host_includes(\"0.0\"); // Only match hosts containing \"0.0\" (e.g., 127.0.0.1)\n then.status(200); // Respond with status code 200 for all matched requests\n });\n\n // Make a request to a URL whose host name is \"localhost\" to trigger the mock\n let response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n // Ensure the request was successful\n assert_eq!(response.status(), 200);\n\n // Ensure that the mock was called at least once\n mock.assert();\n```\n", + "host_matches": "```rust\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that matches requests where the host name is exactly \"localhost\"\n let mock = server.mock(|when, then| {\n when.host_matches(r\"^127.0.0.1$\");\n then.status(200);\n });\n\n // Make a request with \"127.0.0.1\" as the host name to trigger the mock response.\n let response = reqwest::blocking::get(server.url(\"/\")).unwrap();\n\n // Ensure the request was successful\n assert_eq!(response.status(), 200);\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "host_not": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n let server = MockServer::start();\n\n server.mock(|when, then| {\n when.host(\"github.com\");\n then.body(\"This is a mock response\");\n });\n\n let client = Client::builder()\n .proxy(reqwest::Proxy::all(&server.base_url()).unwrap())\n .build()\n .unwrap();\n\n let response = client.get(\"http://github.com\").send().unwrap();\n\n assert_eq!(response.text().unwrap(), \"This is a mock response\");\n```\n", + "host_prefix": "```rust\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that matches any request where the host name starts with \"local\"\n let mock = server.mock(|when, then| {\n when.host_prefix(\"127.0\"); // Only match hosts starting with \"127.0\"\n then.status(200); // Respond with status code 200 for all matched requests\n });\n\n // Make a request to the mock server with a host name of \"127.0.0.1\" to trigger the mock response.\n let response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n // Ensure the request was successful\n assert_eq!(response.status(), 200);\n\n // Ensure that the mock was called at least once\n mock.assert();\n```\n", + "host_prefix_not": "```rust\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that matches any request where the host name does not start with \"www.\"\n let mock = server.mock(|when, then| {\n when.host_prefix_not(\"www.\"); // Exclude hosts starting with \"www\"\n then.status(200); // Respond with status code 200 for all other requests\n });\n\n // Make a request with host name \"localhost\" that does not start with \"www\" and therefore\n // triggers the mock response.\n let response = reqwest::blocking::get(server.url(\"/example\")).unwrap();\n\n // Ensure the request was successful\n assert_eq!(response.status(), 200);\n\n // Ensure that the mock was called at least once\n mock.assert();\n```\n", + "host_suffix": "```rust\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that matches any request where the host name ends with \"host\" (e.g., \"localhost\").\n let mock = server.mock(|when, then| {\n when.host_suffix(\"0.1\"); // Only match hosts ending with \"0.1\"\n then.status(200); // Respond with status code 200 for all matched requests\n });\n\n // Make a request to the mock server with a host name of \"127.0.0.1\" to trigger the mock response.\n let response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n // Ensure the request was successful\n assert_eq!(response.status(), 200);\n\n // Ensure that the mock was called at least once\n mock.assert();\n```\n", + "host_suffix_not": "```rust\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that matches any request where the host name does not end with \"host\".\n let mock = server.mock(|when, then| {\n when.host_suffix_not(\"host\"); // Exclude hosts ending with \"host\"\n then.status(200); // Respond with status code 200 for all other requests\n });\n\n // Make a request with a host name that does not end with \"host\" to trigger the mock response.\n let response = reqwest::blocking::get(server.url(\"/example\")).unwrap();\n\n // Ensure the request was successful\n assert_eq!(response.status(), 200);\n\n // Ensure that the mock was called at least once\n mock.assert();\n```\n", + "is_false": "```rust\n use httpmock::prelude::*;\n\n // Arrange\n let server = MockServer::start();\n\n let m = server.mock(|when, then| {\n when.is_false(|req: &HttpMockRequest| {\n req.uri().path().contains(\"es\")\n });\n then.status(404);\n });\n\n // Act: Send the HTTP request\n let response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n // Assert\n m.assert();\n assert_eq!(response.status(), 404);\n```\n", + "is_true": "```rust\n use httpmock::prelude::*;\n\n // Arrange\n let server = MockServer::start();\n\n let m = server.mock(|when, then| {\n when.is_true(|req: &HttpMockRequest| {\n req.uri().path().contains(\"es\")\n });\n then.status(200);\n });\n\n // Act: Send the HTTP request\n let response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n // Assert\n m.assert();\n assert_eq!(response.status(), 200);\n```\n", + "json_body": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n use serde_json::json;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the request body to match a specific JSON structure\n let mock = server.mock(|when, then| {\n when.json_body(json!({\n \"title\": \"The Great Gatsby\",\n \"author\": \"F. Scott Fitzgerald\"\n }));\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Make a request with a JSON body that matches the expected structure\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Content-Type\", \"application/json\") // It's important to set the Content-Type header manually\n .body(r#\"{\"title\":\"The Great Gatsby\",\"author\":\"F. Scott Fitzgerald\"}\"#)\n .send()\n .unwrap();\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "json_body_excludes": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n let server = MockServer::start();\n\n let mock = server.mock(|when, then| {\n when.json_body_excludes(r#\"\n {\n \"child\": {\n \"target_attribute\": \"Example\"\n }\n }\n \"#);\n then.status(200);\n });\n\n // Send a POST request with a JSON body\n let response = Client::new()\n .post(&format!(\"http://{}/some/path\", server.address()))\n .header(\"content-type\", \"application/json\")\n .body(r#\"\n {\n \"parent_attribute\": \"Some parent data goes here\",\n \"child\": {\n \"other_attribute\": \"Another value\"\n }\n }\n \"#)\n .send()\n .unwrap();\n\n // Assert the mock was called and the response status is as expected\n mock.assert();\n assert_eq!(response.status(), 200);\n```\n", + "json_body_includes": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n let server = MockServer::start();\n\n let mock = server.mock(|when, then| {\n when.json_body_includes(r#\"\n {\n \"child\": {\n \"target_attribute\": \"Example\"\n }\n }\n \"#);\n then.status(200);\n });\n\n // Send a POST request with a JSON body\n let response = Client::new()\n .post(&format!(\"http://{}/some/path\", server.address()))\n .header(\"content-type\", \"application/json\")\n .body(r#\"\n {\n \"parent_attribute\": \"Some parent data goes here\",\n \"child\": {\n \"target_attribute\": \"Example\",\n \"other_attribute\": \"Another value\"\n }\n }\n \"#)\n .send()\n .unwrap();\n\n // Assert the mock was called and the response status is as expected\n mock.assert();\n assert_eq!(response.status(), 200);\n```\n", + "json_body_obj": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n use serde_json::json;\n use serde::{Serialize, Deserialize};\n\n #[derive(Serialize, Deserialize)]\n struct TestUser {\n name: String,\n }\n\n // Initialize logging (optional, for debugging purposes)\n let _ = env_logger::try_init();\n\n // Start the mock server\n let server = MockServer::start();\n\n // Set up a mock endpoint\n let m = server.mock(|when, then| {\n when.path(\"/user\")\n .header(\"content-type\", \"application/json\")\n .json_body_obj(&TestUser { name: String::from(\"Fred\") });\n then.status(200);\n });\n\n // Send a POST request with a JSON body\n let response = Client::new()\n .post(&format!(\"http://{}/user\", server.address()))\n .header(\"content-type\", \"application/json\")\n .body(json!(&TestUser { name: \"Fred\".to_string() }).to_string())\n .send()\n .unwrap();\n\n // Assert the mock was called and the response status is as expected\n m.assert();\n assert_eq!(response.status(), 200);\n```\n", + "matches": "```rust\n use httpmock::prelude::*;\n\n // Arrange\n let server = MockServer::start();\n\n let m = server.mock(|when, then| {\n when.matches(|req: &HttpMockRequest| {\n req.uri().path().contains(\"es\")\n });\n then.status(200);\n });\n\n // Act: Send the HTTP request\n let response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n // Assert\n m.assert();\n assert_eq!(response.status(), 200);\n```\n", + "method": "```rust\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that matches only `GET` requests\n let mock = server.mock(|when, then| {\n when.method(GET); // Match only `GET` HTTP method\n then.status(200); // Respond with status code 200 for all matched requests\n });\n\n // Make a GET request to the server's URL to trigger the mock\n let response = reqwest::blocking::get(server.url(\"/\")).unwrap();\n\n // Ensure the request was successful\n assert_eq!(response.status(), 200);\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "method_not": "```rust\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that matches any request except those using the `POST` method\n let mock = server.mock(|when, then| {\n when.method_not(POST); // Exclude the `POST` HTTP method from matching\n then.status(200); // Respond with status code 200 for all other matched requests\n });\n\n // Make a GET request to the server's URL, which will trigger the mock\n let response = reqwest::blocking::get(server.url(\"/\")).unwrap();\n\n // Ensure the request was successful\n assert_eq!(response.status(), 200);\n\n // Ensure that the mock was called at least once\n mock.assert();\n```\n", + "path": "```rust\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that matches requests to `/test`\n let mock = server.mock(|when, then| {\n when.path(\"/test\");\n then.status(200); // Respond with a 200 status code\n });\n\n // Make a request to the mock server using the specified path\n let response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n // Ensure the request was successful\n assert_eq!(response.status(), 200);\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "path_excludes": "```rust\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that matches any path not containing the substring \"xyz\"\n let mock = server.mock(|when, then| {\n when.path_excludes(\"xyz\");\n then.status(200); // Respond with status 200 for paths excluding \"xyz\"\n });\n\n // Make a request to a path that does not contain \"xyz\"\n let response = reqwest::blocking::get(server.url(\"/testpath\")).unwrap();\n\n // Ensure the request was successful\n assert_eq!(response.status(), 200);\n\n // Ensure the mock server returned the expected response\n mock.assert();\n```\n", + "path_includes": "```rust\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that matches any path containing the substring \"es\"\n let mock = server.mock(|when, then| {\n when.path_includes(\"es\");\n then.status(200); // Respond with a 200 status code for matched requests\n });\n\n // Make a request to a path containing \"es\" to trigger the mock response\n let response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n // Ensure the request was successful\n assert_eq!(response.status(), 200);\n\n // Ensure that the mock was called at least once\n mock.assert();\n```\n", + "path_matches": "```rust\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that matches paths ending with the suffix \"le\"\n let mock = server.mock(|when, then| {\n when.path_matches(r\"le$\");\n then.status(200); // Respond with a 200 status code for paths matching the pattern\n });\n\n // Make a request to a path ending with \"le\"\n let response = reqwest::blocking::get(server.url(\"/example\")).unwrap();\n\n // Ensure the request was successful\n assert_eq!(response.status(), 200);\n\n // Verify that the mock server returned the expected response\n mock.assert();\n```\n", + "path_not": "```rust\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that will not match requests to `/exclude`\n let mock = server.mock(|when, then| {\n when.path_not(\"/exclude\");\n then.status(200); // Respond with status 200 for all other paths\n });\n\n // Make a request to a path that does not match the exclusion\n let response = reqwest::blocking::get(server.url(\"/include\")).unwrap();\n\n // Ensure the request was successful\n assert_eq!(response.status(), 200);\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "path_prefix": "```rust\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that matches any path starting with the prefix \"/api\"\n let mock = server.mock(|when, then| {\n when.path_prefix(\"/api\");\n then.status(200); // Respond with a 200 status code for matched requests\n });\n\n // Make a request to a path starting with \"/api\"\n let response = reqwest::blocking::get(server.url(\"/api/v1/resource\")).unwrap();\n\n // Ensure the request was successful\n assert_eq!(response.status(), 200);\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "path_prefix_not": "```rust\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that matches any path not starting with the prefix \"/admin\"\n let mock = server.mock(|when, then| {\n when.path_prefix_not(\"/admin\");\n then.status(200); // Respond with status 200 for paths excluding \"/admin\"\n });\n\n // Make a request to a path that does not start with \"/admin\"\n let response = reqwest::blocking::get(server.url(\"/public/home\")).unwrap();\n\n // Ensure the request was successful\n assert_eq!(response.status(), 200);\n\n // Verify that the mock server returned the expected response\n mock.assert();\n```\n", + "path_suffix": "```rust\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that matches any path ending with the suffix \".html\"\n let mock = server.mock(|when, then| {\n when.path_suffix(\".html\");\n then.status(200); // Respond with a 200 status code for matched requests\n });\n\n // Make a request to a path ending with \".html\"\n let response = reqwest::blocking::get(server.url(\"/about/index.html\")).unwrap();\n\n // Ensure the request was successful\n assert_eq!(response.status(), 200);\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "path_suffix_not": "```rust\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that matches any path not ending with the suffix \".json\"\n let mock = server.mock(|when, then| {\n when.path_suffix_not(\".json\");\n then.status(200); // Respond with a 200 status code for paths excluding \".json\"\n });\n\n // Make a request to a path that does not end with \".json\"\n let response = reqwest::blocking::get(server.url(\"/about/index.html\")).unwrap();\n\n // Ensure the request was successful\n assert_eq!(response.status(), 200);\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "port": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Configure a mock to respond to requests made to `github.com`\n // with a specific port\n server.mock(|when, then| {\n when.port(80); // Specify the expected port\n then.body(\"This is a mock response\");\n });\n\n // Set up an HTTP client to use the mock server as a proxy\n let client = Client::builder()\n // Proxy all requests to the mock server\n .proxy(reqwest::Proxy::all(&server.base_url()).unwrap())\n .build()\n .unwrap();\n\n // Send a GET request to `github.com` on port 80.\n // The request will be sent to our mock server due to the HTTP client proxy settings.\n let response = client.get(\"http://github.com:80\").send().unwrap();\n\n // Validate that the mock server returned the expected response\n assert_eq!(response.text().unwrap(), \"This is a mock response\");\n```\n", + "port_not": "```rust\n use httpmock::prelude::*;\n use reqwest::blocking::Client;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Configure a mock to respond to requests not using port 81\n server.mock(|when, then| {\n when.port_not(81); // Exclude requests on port 81\n then.body(\"This is a mock response\");\n });\n\n // Set up an HTTP client to use the mock server as a proxy\n let client = Client::builder()\n .proxy(reqwest::Proxy::all(&server.base_url()).unwrap())\n .build()\n .unwrap();\n\n // Make a request to `github.com` on port 80, which will trigger\n // the mock response\n let response = client.get(\"http://github.com:80\").send().unwrap();\n\n // Validate that the mock server returned the expected response\n assert_eq!(response.text().unwrap(), \"This is a mock response\");\n```\n", + "query_param": "```rust\n // Arrange\n use reqwest::blocking::get;\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the query parameter `query` to have the value \"This is cool\"\n let m = server.mock(|when, then| {\n when.query_param(\"query\", \"This is cool\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Act: Make a request that includes the specified query parameter and value\n get(&server.url(\"/search?query=This+is+cool\")).unwrap();\n\n // Assert: Verify that the mock was called at least once\n m.assert();\n```\n", + "query_param_count": "```rust\n // Arrange\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects exactly two query parameters with keys matching the regex \"user.*\"\n // and values matching the regex \"admin.*\"\n let m = server.mock(|when, then| {\n when.query_param_count(r\"user.*\", r\"admin.*\", 2);\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Act: Make a request that matches the conditions\n reqwest::blocking::get(&server.url(\"/search?user1=admin1&user2=admin2\")).unwrap();\n\n // Assert: Verify that the mock was called at least once\n m.assert();\n```\n", + "query_param_excludes": "```rust\n // Arrange\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the query parameter `query`\n // to have a value that does not contain \"uncool\"\n let m = server.mock(|when, then| {\n when.query_param_excludes(\"query\", \"uncool\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Act: Make a request that includes a value not containing the substring \"uncool\"\n reqwest::blocking::get(&server.url(\"/search?query=Something+cool\")).unwrap();\n\n // Assert: Verify that the mock was called at least once\n m.assert();\n```\n", + "query_param_exists": "```rust\n // Arrange\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the query parameter `query` to exist, regardless of its value\n let m = server.mock(|when, then| {\n when.query_param_exists(\"query\");\n then.status(200); // Respond with a 200 status code if the parameter exists\n });\n\n // Act: Make a request with the specified query parameter\n reqwest::blocking::get(&server.url(\"/search?query=restaurants+near+me\")).unwrap();\n\n // Assert: Verify that the mock was called at least once\n m.assert();\n```\n", + "query_param_includes": "```rust\n // Arrange\n use reqwest::blocking::get;\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the query parameter `query`\n // to have a value containing \"cool\"\n let m = server.mock(|when, then| {\n when.query_param_includes(\"query\", \"cool\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Act: Make a request that includes a value containing the substring \"cool\"\n get(server.url(\"/search?query=Something+cool\")).unwrap();\n\n // Assert: Verify that the mock was called at least once\n m.assert();\n```\n", + "query_param_matches": "```rust\n // Arrange\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the query parameter key to match the regex \"user.*\"\n // and the value to match the regex \"admin.*\"\n let m = server.mock(|when, then| {\n when.query_param_matches(r\"user.*\", r\"admin.*\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Act: Make a request that matches the regex patterns for both key and value\n reqwest::blocking::get(&server.url(\"/search?user=admin_user\")).unwrap();\n\n // Assert: Verify that the mock was called at least once\n m.assert();\n```\n", + "query_param_missing": "```rust\n // Arrange\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the query parameter `query` to be missing\n let m = server.mock(|when, then| {\n when.query_param_missing(\"query\");\n then.status(200); // Respond with a 200 status code if the parameter is absent\n });\n\n // Act: Make a request without the specified query parameter\n reqwest::blocking::get(&server.url(\"/search\")).unwrap();\n\n // Assert: Verify that the mock was called at least once\n m.assert();\n```\n", + "query_param_not": "```rust\n // Arrange\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the query parameter `query` to NOT have the value \"This is cool\"\n let m = server.mock(|when, then| {\n when.query_param_not(\"query\", \"This is cool\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Act: Make a request that does not include the specified query parameter and value\n let response = reqwest::blocking::get(&server.url(\"/search?query=awesome\")).unwrap();\n\n // Assert: Verify that the mock was called\n assert_eq!(response.status(), 200);\n m.assert();\n```\n", + "query_param_prefix": "```rust\n // Arrange\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the query parameter `query`\n // to have a value starting with \"cool\"\n let m = server.mock(|when, then| {\n when.query_param_prefix(\"query\", \"cool\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Act: Make a request that includes a value starting with the prefix \"cool\"\n reqwest::blocking::get(&server.url(\"/search?query=cool+stuff\")).unwrap();\n\n // Assert: Verify that the mock was called at least once\n m.assert();\n```\n", + "query_param_prefix_not": "```rust\n // Arrange\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the query parameter `query`\n // to have a value not starting with \"cool\"\n let m = server.mock(|when, then| {\n when.query_param_prefix_not(\"query\", \"cool\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Act: Make a request that does not start with the prefix \"cool\"\n reqwest::blocking::get(&server.url(\"/search?query=warm_stuff\")).unwrap();\n\n // Assert: Verify that the mock was called at least once\n m.assert();\n```\n", + "query_param_suffix": "```rust\n // Arrange\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the query parameter `query`\n // to have a value ending with \"cool\"\n let m = server.mock(|when, then| {\n when.query_param_suffix(\"query\", \"cool\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Act: Make a request that includes a value ending with the suffix \"cool\"\n reqwest::blocking::get(&server.url(\"/search?query=really_cool\")).unwrap();\n\n // Assert: Verify that the mock was called at least once\n m.assert();\n```\n", + "query_param_suffix_not": "```rust\n // Arrange\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that expects the query parameter `query`\n // to have a value not ending with \"cool\"\n let m = server.mock(|when, then| {\n when.query_param_suffix_not(\"query\", \"cool\");\n then.status(200); // Respond with a 200 status code if the condition is met\n });\n\n // Act: Make a request that doesn't end with the suffix \"cool\"\n reqwest::blocking::get(&server.url(\"/search?query=uncool_stuff\")).unwrap();\n\n // Assert: Verify that the mock was called at least once\n m.assert();\n```\n", + "scheme": "```rust\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that only matches requests with the \"http\" scheme\n let mock = server.mock(|when, then| {\n when.scheme(\"http\"); // Restrict to the \"http\" scheme\n then.status(200); // Respond with status code 200 for all matched requests\n });\n\n // Make an \"http\" request to the server's URL to trigger the mock\n let response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n // Ensure the request was successful\n assert_eq!(response.status(), 200);\n\n // Verify that the mock was called at least once\n mock.assert();\n```\n", + "scheme_not": "```rust\n use httpmock::prelude::*;\n\n // Start a new mock server\n let server = MockServer::start();\n\n // Create a mock that will only match requests that do not use the \"https\" scheme\n let mock = server.mock(|when, then| {\n when.scheme_not(\"https\"); // Exclude the \"https\" scheme from matching\n then.status(200); // Respond with status code 200 for all matched requests\n });\n\n // Make a request to the server's URL with the \"http\" scheme to trigger the mock\n let response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n // Ensure the request was successful\n assert_eq!(response.status(), 200);\n\n // Ensure that the mock was called at least once\n mock.assert();\n```\n" + } +} \ No newline at end of file diff --git a/docs/website/generated/docs.json b/docs/website/generated/docs.json new file mode 100644 index 00000000..4b390cde --- /dev/null +++ b/docs/website/generated/docs.json @@ -0,0 +1,104 @@ +{ + "then": { + "and": "Applies a custom function to modify a `Then` instance, enhancing flexibility and readability\nin setting up mock server responses.\n\nThis method allows you to encapsulate complex configurations into reusable functions,\nand apply them without breaking the chain of method calls on a `Then` object.\n\n# Parameters\n- `func`: A function that takes a `Then` instance and returns it after applying some modifications.\n\n# Returns\nReturns `self` to allow chaining of method calls on the `Then` object.\n\n# Example\nDemonstrates how to use the `and` method to maintain readability while applying multiple\nmodifications from an external function.\n\n```rust\nuse std::time::Duration;\nuse http::{StatusCode, header::HeaderValue};\nuse httpmock::{Then, MockServer};\n\n// Function that configures a response with JSON content and a delay\nfn ok_json_with_delay(then: Then) -> Then {\n then.status(StatusCode::OK.as_u16())\n .header(\"content-type\", \"application/json\")\n .delay(Duration::from_secs_f32(0.5))\n}\n\n// Usage within a method chain\nlet server = MockServer::start();\nlet then = server.mock(|when, then| {\n when.path(\"/example\");\n then.header(\"general-vibe\", \"much better\")\n .and(ok_json_with_delay);\n});\n\n// The `and` method keeps the setup intuitively readable as a continuous chain\n```\n", + "body": "Configures the HTTP response body that the mock server will return.\n\n# Parameters\n- `body`: The content of the response body, provided as a type that can be referenced as a byte slice.\n\n# Returns\nReturns `self` to allow chaining of method calls on the `Mock` object.\n\n# Example\nDemonstrates setting a response body for a request to the path `/hello` with a 200 OK status.\n\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Initialize the mock server\nlet server = MockServer::start();\n\n// Configure the mock\nlet m = server.mock(|when, then| {\n when.path(\"/hello\");\n then.status(200)\n .body(\"ohi!\");\n});\n\n// Send a request and verify the response\nlet response = Client::new()\n .get(server.url(\"/hello\"))\n .send()\n .unwrap();\n\n// Check that the mock was called as expected and the response body is as configured\nm.assert();\nassert_eq!(response.status(), 200);\nassert_eq!(response.text().unwrap(), \"ohi!\");\n```\n", + "body_from_file": "Configures the HTTP response body with content loaded from a specified file on the mock server.\n\n# Parameters\n- `resource_file_path`: A string representing the path to the file whose contents will be used as the response body. The path can be absolute or relative to the server's running directory.\n\n# Returns\nReturns `self` to allow chaining of method calls on the `Mock` object.\n\n# Panics\nPanics if the specified file cannot be read, or if the path provided cannot be resolved to an absolute path.\n\n# Example\nDemonstrates setting the response body from a file for a request to the path `/hello` with a 200 OK status.\n\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Initialize the mock server\nlet server = MockServer::start();\n\n// Configure the mock\nlet m = server.mock(|when, then| {\n when.path(\"/hello\");\n then.status(200)\n .body_from_file(\"tests/resources/simple_body.txt\");\n});\n\n// Send a request and verify the response\nlet response = Client::new()\n .get(server.url(\"/hello\"))\n .send()\n .unwrap();\n\n// Check that the mock was called as expected and the response body matches the file contents\nm.assert();\nassert_eq!(response.status(), 200);\nassert_eq!(response.text().unwrap(), \"ohi!\");\n```\n", + "delay": "Sets a delay for the mock server response.\n\nThis method configures the server to wait for a specified duration before sending a response,\nwhich can be useful for testing timeout scenarios or asynchronous operations.\n\n# Parameters\n- `duration`: The length of the delay as a `std::time::Duration`.\n\n# Returns\nReturns `self` to allow chaining of method calls on the `Mock` object.\n\n# Panics\nPanics if the specified duration results in a delay that cannot be represented as a 64-bit\nunsigned integer of milliseconds (more than approximately 584 million years).\n\n# Example\nDemonstrates setting a 3-second delay for a request to the path `/delay`.\n\n```rust\nuse std::time::{SystemTime, Duration};\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Arrange\nlet _ = env_logger::try_init();\nlet start_time = SystemTime::now();\nlet three_seconds = Duration::from_secs(3);\nlet server = MockServer::start();\n\n// Configure the mock\nlet mock = server.mock(|when, then| {\n when.path(\"/delay\");\n then.status(200)\n .delay(three_seconds);\n});\n\n// Act\nlet response = Client::new()\n .get(server.url(\"/delay\"))\n .send()\n .unwrap();\n\n// Assert\nmock.assert();\nassert!(start_time.elapsed().unwrap() >= three_seconds);\n```\n", + "header": "Sets an HTTP header that the mock server will return in the response.\n\nThis method configures a response header to be included when the mock server handles a request.\n\n# Parameters\n- `name`: The name of the header to set.\n- `value`: The value of the header.\n\n# Returns\nReturns `self` to allow chaining of method calls on the `Mock` object.\n\n# Example\nDemonstrates setting the \"Expires\" header for a response to a request to the root path.\n\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Arrange\nlet _ = env_logger::try_init();\nlet server = MockServer::start();\n\n// Configure the mock\nlet m = server.mock(|when, then| {\n when.path(\"/\");\n then.status(200)\n .header(\"Expires\", \"Wed, 21 Oct 2050 07:28:00 GMT\");\n});\n\n// Act\nlet response = Client::new()\n .get(server.url(\"/\"))\n .send()\n .unwrap();\n\n// Assert\nm.assert();\nassert_eq!(response.status(), 200);\nassert_eq!(\n response.headers().get(\"Expires\").unwrap().to_str().unwrap(),\n \"Wed, 21 Oct 2050 07:28:00 GMT\"\n);\n```\n", + "json_body": "Sets the JSON body for the HTTP response that will be returned by the mock server.\n\nThis function accepts a JSON object that must be serializable and deserializable by serde.\nNote that this method does not automatically set the \"Content-Type\" header to \"application/json\".\nYou will need to set this header manually if required.\n\n# Parameters\n- `body`: The HTTP response body in the form of a `serde_json::Value` object.\n\n# Returns\nReturns `self` to allow chaining of method calls on the `Mock` object.\n\n# Example\nDemonstrates how to set a JSON body and a matching \"Content-Type\" header for a mock response.\n\n```rust\nuse httpmock::prelude::*;\nuse serde_json::{Value, json};\nuse reqwest::blocking::Client;\n\n// Arrange\nlet _ = env_logger::try_init();\nlet server = MockServer::start();\n\n// Configure the mock\nlet m = server.mock(|when, then| {\n when.path(\"/user\");\n then.status(200)\n .header(\"content-type\", \"application/json\")\n .json_body(json!({ \"name\": \"Hans\" }));\n});\n\n// Act\nlet response = Client::new()\n .get(server.url(\"/user\"))\n .send()\n .unwrap();\n\n// Get the status code first\nlet status = response.status();\n\n// Extract the text from the response\nlet response_text = response.text().unwrap();\n\n// Deserialize the JSON response\nlet user: Value =\n serde_json::from_str(&response_text).expect(\"cannot deserialize JSON\");\n\n// Assert\nm.assert();\nassert_eq!(status, 200);\nassert_eq!(user[\"name\"], \"Hans\");\n```\n", + "json_body_obj": "Sets the JSON body that will be returned by the mock server using a serializable serde object.\n\nThis method converts the provided object into a JSON string. It does not automatically set\nthe \"Content-Type\" header to \"application/json\", so you must set this header manually if it's\nneeded.\n\n# Parameters\n- `body`: A reference to an object that implements the `serde::Serialize` trait.\n\n# Returns\nReturns `self` to allow chaining of method calls on the `Mock` object.\n\n# Panics\nPanics if the object cannot be serialized into a JSON string.\n\n# Example\nDemonstrates setting a JSON body and the corresponding \"Content-Type\" header for a user object.\n\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\nuse serde::{Serialize, Deserialize};\n\n#[derive(Serialize, Deserialize)]\nstruct TestUser {\n name: String,\n}\n\n// Arrange\nlet _ = env_logger::try_init();\nlet server = MockServer::start();\n\n// Configure the mock\nlet m = server.mock(|when, then| {\n when.path(\"/user\");\n then.status(200)\n .header(\"content-type\", \"application/json\")\n .json_body_obj(&TestUser {\n name: String::from(\"Hans\"),\n });\n});\n\n// Act\nlet response = Client::new()\n .get(server.url(\"/user\"))\n .send()\n .unwrap();\n\n// Get the status code first\nlet status = response.status();\n\n// Extract the text from the response\nlet response_text = response.text().unwrap();\n\n// Deserialize the JSON response into a TestUser object\nlet user: TestUser =\n serde_json::from_str(&response_text).unwrap();\n\n// Assert\nm.assert();\nassert_eq!(status, 200);\nassert_eq!(user.name, \"Hans\");\n```\n", + "status": "Configures the HTTP response status code that the mock server will return.\n\n# Parameters\n- `status`: A `u16` HTTP status code that the mock server should return for the configured request.\n\n# Returns\nReturns `self` to allow chaining of method calls on the `Mock` object.\n\n# Example\nDemonstrates setting a 200 OK status for a request to the path `/hello`.\n\n```rust\nuse httpmock::prelude::*;\n\n// Initialize the mock server\nlet server = MockServer::start();\n\n// Configure the mock\nlet m = server.mock(|when, then| {\n when.path(\"/hello\");\n then.status(200);\n});\n\n// Send a request and verify the response\nlet response = reqwest::blocking::get(server.url(\"/hello\")).unwrap();\n\n// Check that the mock was called as expected and the response status is as configured\nm.assert();\nassert_eq!(response.status(), 200);\n```\n" + }, + "when": { + "and": "Applies a specified function to enhance or modify the `When` instance. This method allows for the\nencapsulation of multiple matching conditions into a single function, maintaining a clear and fluent\ninterface for setting up HTTP request expectations.\n\nThis method is particularly useful for reusing common setup patterns across multiple test scenarios,\npromoting cleaner and more maintainable test code.\n\n# Parameters\n- `func`: A function that takes a `When` instance and returns it after applying some conditions.\n\n## Example\n```rust\nuse httpmock::{prelude::*, When};\nuse httpmock::Method::POST;\n\n// Function to apply a standard authorization and content type setup for JSON POST requests\nfn is_authorized_json_post_request(when: When) -> When {\n when.method(POST)\n .header(\"Authorization\", \"SOME API KEY\")\n .header(\"Content-Type\", \"application/json\")\n}\n\n// Usage example demonstrating how to maintain fluent interface style with complex setups.\n// This approach keeps the chain of conditions clear and readable, enhancing test legibility\nlet server = MockServer::start();\nlet m = server.mock(|when, then| {\n when.query_param(\"user_id\", \"12345\")\n .and(is_authorized_json_post_request) // apply the function to include common setup\n .json_body_includes(r#\"{\"key\": \"value\"}\"#); // additional specific condition\n then.status(200);\n});\n```\n\n# Returns\n`When`: The modified `When` instance with additional conditions applied, suitable for further chaining.\n", + "any_request": "Configures the mock server to respond to any incoming request, regardless of the URL path,\nquery parameters, headers, or method.\n\nThis method doesn't directly alter the behavior of the mock server, as it already responds to any\nrequest by default. However, it serves as an explicit indication in your code that the\nserver will respond to any request.\n\n# Example\n\n```rust\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Configure the mock server to respond to any request\nlet mock = server.mock(|when, then| {\n when.any_request(); // Explicitly specify that any request should match\n then.status(200); // Respond with status code 200 for all matched requests\n});\n\n// Make a request to the server's URL and ensure the mock is triggered\nlet response = reqwest::blocking::get(server.url(\"/anyPath\")).unwrap();\n\n// Ensure the request was successful\nassert_eq!(response.status(), 200);\n\n// Assert that the mock was called at least once\nmock.assert();\n```\n\n# Note\nThis is the default behavior as of now, but it may change in future versions.\n\n# Returns\nThe updated `When` instance to enable method chaining.\n\n", + "body": "Sets the required HTTP request body content.\nThis method specifies that the HTTP request body must match the provided content exactly.\n\n**Note**: The body content is case-sensitive and must be an exact match.\n\n# Parameters\n- `body`: The required HTTP request body content. This parameter accepts any type that can be converted into a `String`.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the request body to be \"The Great Gatsby\"\nlet mock = server.mock(|when, then| {\n when.body(\"The Great Gatsby\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request with the required body content\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .body(\"The Great Gatsby\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n", + "body_excludes": "Sets the condition that the HTTP request body content must not contain the specified substring.\nThis method ensures that the request body does not include the provided content as a substring.\n\n**Note**: The body content is case-sensitive.\n\n# Parameters\n- `substring`: The substring that the HTTP request body must not contain. This parameter accepts any type that can be converted into a `String`.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the request body to not contain the substring \"Gatsby\"\nlet mock = server.mock(|when, then| {\n when.body_excludes(\"Gatsby\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request with a different body content\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .body(\"A Tale of Two Cities is a novel.\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n", + "body_includes": "Sets the condition that the HTTP request body content must contain the specified substring.\nThis method ensures that the request body includes the provided content as a substring.\n\n**Note**: The body content is case-sensitive.\n\n# Parameters\n- `substring`: The substring that the HTTP request body must contain. This parameter accepts any type that can be converted into a `String`.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the request body to contain the substring \"Gatsby\"\nlet mock = server.mock(|when, then| {\n when.body_includes(\"Gatsby\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request with the required substring in the body content\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .body(\"The Great Gatsby is a novel.\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n", + "body_matches": "Sets the condition that the HTTP request body content must match the specified regular expression.\nThis method ensures that the request body fully conforms to the provided regex pattern.\n\n**Note**: The regex matching is case-sensitive unless the regex is explicitly defined to be case-insensitive.\n\n# Parameters\n- `pattern`: The regular expression pattern that the HTTP request body must match. This parameter accepts any type that can be converted into a `Regex`.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the request body to match the regex pattern \"^The Great Gatsby.*\"\nlet mock = server.mock(|when, then| {\n when.body_matches(\"^The Great Gatsby.*\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request with a body that matches the regex pattern\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .body(\"The Great Gatsby is a novel by F. Scott Fitzgerald.\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When’ instance to allow method chaining for additional configuration.\n", + "body_not": "Sets the condition that the HTTP request body content must not match the specified value.\nThis method ensures that the request body does not contain the provided content exactly.\n\n**Note**: The body content is case-sensitive and must be an exact mismatch.\n\n# Parameters\n- `body`: The body content that the HTTP request must not contain. This parameter accepts any type that can be converted into a `String`.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the request body to not be \"The Great Gatsby\"\nlet mock = server.mock(|when, then| {\n when.body_not(\"The Great Gatsby\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request with a different body content\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .body(\"A Tale of Two Cities\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n", + "body_prefix": "Sets the condition that the HTTP request body content must begin with the specified substring.\nThis method ensures that the request body starts with the provided content as a substring.\n\n**Note**: The body content is case-sensitive.\n\n# Parameters\n- `prefix`: The substring that the HTTP request body must begin with. This parameter accepts any type that can be converted into a `String`.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the request body to begin with the substring \"The Great\"\nlet mock = server.mock(|when, then| {\n when.body_prefix(\"The Great\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request with the required prefix in the body content\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .body(\"The Great Gatsby is a novel.\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n", + "body_prefix_not": "Sets the condition that the HTTP request body content must not begin with the specified substring.\nThis method ensures that the request body does not start with the provided content as a substring.\n\n**Note**: The body content is case-sensitive.\n\n# Parameters\n- `prefix`: The substring that the HTTP request body must not begin with. This parameter accepts any type that can be converted into a `String`.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the request body to not begin with the substring \"Error:\"\nlet mock = server.mock(|when, then| {\n when.body_prefix_not(\"Error:\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request with a different body content\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .body(\"Success: Operation completed.\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When’ instance to allow method chaining for additional configuration.\n", + "body_suffix": "Sets the condition that the HTTP request body content must end with the specified substring.\nThis method ensures that the request body concludes with the provided content as a substring.\n\n**Note**: The body content is case-sensitive.\n\n# Parameters\n- `suffix`: The substring that the HTTP request body must end with. This parameter accepts any type that can be converted into a `String`.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the request body to end with the substring \"a novel.\"\nlet mock = server.mock(|when, then| {\n when.body_suffix(\"a novel.\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request with the required suffix in the body content\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .body(\"The Great Gatsby is a novel.\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When’ instance to allow method chaining for additional configuration.\n", + "body_suffix_not": "Sets the condition that the HTTP request body content must not end with the specified substring.\nThis method ensures that the request body does not conclude with the provided content as a substring.\n\n**Note**: The body content is case-sensitive.\n\n# Parameters\n- `suffix`: The substring that the HTTP request body must not end with. This parameter accepts any type that can be converted into a `String`.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the request body to not end with the substring \"a novel.\"\nlet mock = server.mock(|when, then| {\n when.body_suffix_not(\"a novel.\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request with a different body content\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .body(\"The Great Gatsby is a story.\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When’ instance to allow method chaining for additional configuration.\n", + "cookie": "Sets the cookie that needs to exist in the HTTP request.\nCookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html).\n**Attention**: Cookie names are **case-sensitive**.\n\n# Parameters\n- `name`: The name of the cookie. Must be a case-sensitive match.\n- `value`: The expected value of the cookie.\n\n> Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects a cookie named \"SESSIONID\" with the value \"1234567890\"\nlet mock = server.mock(|when, then| {\n when.cookie(\"SESSIONID\", \"1234567890\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request that includes the required cookie\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; SESSIONID=1234567890; CONSENT=1\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n", + "cookie_count": "Sets the requirement that a cookie with a name and value matching the specified regexes must appear a specified number of times in the HTTP request.\nCookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html).\n**Attention**: Cookie names are **case-sensitive**.\n\n# Parameters\n- `key_regex`: The regex pattern that the cookie name must match.\n- `value_regex`: The regex pattern that the cookie value must match.\n- `count`: The number of times a cookie with a matching name and value must appear.\n\n> Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects a cookie with a name matching the regex \"^SESSION\"\n// and a value matching the regex \"^[0-9]{10}$\" to appear exactly twice\nlet mock = server.mock(|when, then| {\n when.cookie_count(r\"^SESSION\", r\"^[0-9]{10}$\", 2);\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request that includes the required cookies\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"SESSIONID=1234567890; TRACK=12345; SESSIONTOKEN=0987654321; CONSENT=1\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n", + "cookie_excludes": "Sets the requirement that a cookie with the specified name must exist and its value must not contain the specified substring.\nCookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html).\n**Attention**: Cookie names are **case-sensitive**.\n\n# Parameters\n- `name`: The name of the cookie that must exist.\n- `value_substring`: The substring that must not be present in the cookie value.\n\n> Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects a cookie named \"SESSIONID\" with a value not containing \"1234\"\nlet mock = server.mock(|when, then| {\n when.cookie_excludes(\"SESSIONID\", \"1234\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request that includes the required cookie\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; SESSIONID=abcdef; CONSENT=1\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n", + "cookie_exists": "Sets the requirement that a cookie with the specified name must exist in the HTTP request.\nCookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html).\n**Attention**: Cookie names are **case-sensitive**.\n\n# Parameters\n- `name`: The name of the cookie that must exist.\n\n> Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects a cookie named \"SESSIONID\"\nlet mock = server.mock(|when, then| {\n when.cookie_exists(\"SESSIONID\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request that includes the required cookie\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; SESSIONID=1234567890; CONSENT=1\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n", + "cookie_includes": "Sets the requirement that a cookie with the specified name must exist and its value must contain the specified substring.\nCookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html).\n**Attention**: Cookie names are **case-sensitive**.\n\n# Parameters\n- `name`: The name of the cookie that must exist.\n- `value_substring`: The substring that must be present in the cookie value.\n\n> Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects a cookie named \"SESSIONID\" with a value containing \"1234\"\nlet mock = server.mock(|when, then| {\n when.cookie_includes(\"SESSIONID\", \"1234\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request that includes the required cookie\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; SESSIONID=abc1234def; CONSENT=1\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n", + "cookie_matches": "Sets the requirement that a cookie with a name matching the specified regex must exist and its value must match the specified regex.\nCookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html).\n**Attention**: Cookie names are **case-sensitive**.\n\n# Parameters\n- `key_regex`: The regex pattern that the cookie name must match.\n- `value_regex`: The regex pattern that the cookie value must match.\n\n> Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects a cookie with a name matching the regex \"^SESSION\"\n// and a value matching the regex \"^[0-9]{10}$\"\nlet mock = server.mock(|when, then| {\n when.cookie_matches(r\"^SESSION\", r\"^[0-9]{10}$\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request that includes the required cookie\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; SESSIONID=1234567890; CONSENT=1\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n", + "cookie_missing": "Sets the requirement that a cookie with the specified name must not exist in the HTTP request.\nCookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html).\n**Attention**: Cookie names are **case-sensitive**.\n\n# Parameters\n- `name`: The name of the cookie that must not exist.\n\n> Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects a cookie named \"SESSIONID\" not to exist\nlet mock = server.mock(|when, then| {\n when.cookie_missing(\"SESSIONID\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request that does not include the excluded cookie\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; CONSENT=1\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n", + "cookie_not": "Sets the cookie that should not exist or should not have a specific value in the HTTP request.\nCookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html).\n**Attention**: Cookie names are **case-sensitive**.\n\n# Parameters\n- `name`: The name of the cookie. Must be a case-sensitive match.\n- `value`: The value that the cookie should not have.\n\n> Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects a cookie named \"SESSIONID\" to not have the value \"1234567890\"\nlet mock = server.mock(|when, then| {\n when.cookie_not(\"SESSIONID\", \"1234567890\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request that includes the required cookie\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; SESSIONID=0987654321; CONSENT=1\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n", + "cookie_prefix": "Sets the requirement that a cookie with the specified name must exist and its value must start with the specified substring.\nCookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html).\n**Attention**: Cookie names are **case-sensitive**.\n\n# Parameters\n- `name`: The name of the cookie that must exist.\n- `value_prefix`: The substring that must be at the start of the cookie value.\n\n> Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects a cookie named \"SESSIONID\" with a value starting with \"1234\"\nlet mock = server.mock(|when, then| {\n when.cookie_prefix(\"SESSIONID\", \"1234\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request that includes the required cookie\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; SESSIONID=1234abcdef; CONSENT=1\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n", + "cookie_prefix_not": "Sets the requirement that a cookie with the specified name must exist and its value must not start with the specified substring.\nCookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html).\n**Attention**: Cookie names are **case-sensitive**.\n\n# Parameters\n- `name`: The name of the cookie that must exist.\n- `value_prefix`: The substring that must not be at the start of the cookie value.\n\n> Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects a cookie named \"SESSIONID\" with a value not starting with \"1234\"\nlet mock = server.mock(|when, then| {\n when.cookie_prefix_not(\"SESSIONID\", \"1234\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request that includes the required cookie\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; SESSIONID=abcd1234; CONSENT=1\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n", + "cookie_suffix": "Sets the requirement that a cookie with the specified name must exist and its value must end with the specified substring.\nCookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html).\n**Attention**: Cookie names are **case-sensitive**.\n\n# Parameters\n- `name`: The name of the cookie that must exist.\n- `value_suffix`: The substring that must be at the end of the cookie value.\n\n> Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects a cookie named \"SESSIONID\" with a value ending with \"7890\"\nlet mock = server.mock(|when, then| {\n when.cookie_suffix(\"SESSIONID\", \"7890\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request that includes the required cookie\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; SESSIONID=abcdef7890; CONSENT=1\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n", + "cookie_suffix_not": "Sets the requirement that a cookie with the specified name must exist and its value must not end with the specified substring.\nCookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html).\n**Attention**: Cookie names are **case-sensitive**.\n\n# Parameters\n- `name`: The name of the cookie that must exist.\n- `value_suffix`: The substring that must not be at the end of the cookie value.\n\n> Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects a cookie named \"SESSIONID\" with a value not ending with \"7890\"\nlet mock = server.mock(|when, then| {\n when.cookie_suffix_not(\"SESSIONID\", \"7890\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request that includes the required cookie\n Client::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Cookie\", \"TRACK=12345; SESSIONID=abcdef1234; CONSENT=1\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n", + "form_urlencoded_tuple": "Adds a key-value pair to the requirements for an `application/x-www-form-urlencoded` request body.\n\nThis method sets an expectation for a specific key-value pair to be included in the request body\nof an `application/x-www-form-urlencoded` POST request. Each key and value are URL-encoded as specified\nby the [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded).\n\n**Note**: The mock server does not automatically verify that the HTTP method is POST as per spec.\nIf you want to verify that the request method is POST, you must explicitly set it in your mock configuration.\n\n# Parameters\n- `key`: The key of the key-value pair to set as a requirement.\n- `value`: The value of the key-value pair to set as a requirement.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Arrange\nlet server = MockServer::start();\n\nlet m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple(\"name\", \"Peter Griffin\")\n .form_urlencoded_tuple(\"town\", \"Quahog\");\n then.status(202);\n});\n\nlet response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Peter%20Griffin&town=Quahog\")\n .send()\n .unwrap();\n\n// Assert\nm.assert();\nassert_eq!(response.status(), 202);\n```\n\n# Returns\n`When`: Returns the modified `When` object with the new key-value pair added to the `application/x-www-form-urlencoded` expectations.\n", + "form_urlencoded_tuple_count": "Sets a requirement for the number of times a key-value pair matching specific regular expressions appears in an `application/x-www-form-urlencoded` request body.\n\nThis method sets an expectation that the key-value pair must appear a specific number of times in the request body of an\n`application/x-www-form-urlencoded` POST request. The key and value regular expressions are URL-encoded as specified by the\n[URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded).\n\n**Note**: The mock server does not automatically verify that the HTTP method is POST as per spec.\nIf you want to verify that the request method is POST, you must explicitly set it in your mock configuration.\n\n# Parameters\n- `key_regex`: The regular expression that the key must match in the `application/x-www-form-urlencoded` request body.\n- `value_regex`: The regular expression that the value must match in the `application/x-www-form-urlencoded` request body.\n- `count`: The number of times the key-value pair matching the regular expressions must appear.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\nuse regex::Regex;\n\n// Arrange\nlet server = MockServer::start();\n\nlet m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_count(\n Regex::new(r\"^name$\").unwrap(),\n Regex::new(r\".*Griffin$\").unwrap(),\n 2\n );\n then.status(202);\n});\n\n// Act\nlet response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Peter%20Griffin&name=Lois%20Griffin&town=Quahog\")\n .send()\n .unwrap();\n\n// Assert\nm.assert();\nassert_eq!(response.status(), 202);\n```\n\n# Returns\n`When`: Returns the modified `When` object with the new key-value count requirement added to the\n`application/x-www-form-urlencoded` expectations.\n", + "form_urlencoded_tuple_excludes": "Sets a requirement that a key's value in an `application/x-www-form-urlencoded` request body must not contain a specific substring.\n\nThis method sets an expectation that the value associated with a specific key must not contain a specified substring\nin the request body of an `application/x-www-form-urlencoded` POST request. The key and the substring are URL-encoded\nas specified by the [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded).\n\n**Note**: The mock server does not automatically verify that the HTTP method is POST as per spec.\nIf you want to verify that the request method is POST, you must explicitly set it in your mock configuration.\n\n# Parameters\n- `key`: The key in the `application/x-www-form-urlencoded` request body.\n- `substring`: The substring that must not be present in the value associated with the key.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Arrange\nlet server = MockServer::start();\n\nlet m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_excludes(\"name\", \"Griffin\");\n then.status(202);\n});\n\nlet response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Lois%20Smith&city=Quahog\")\n .send()\n .unwrap();\n\n// Assert\nm.assert();\nassert_eq!(response.status(), 202);\n```\n\n# Returns\n`When`: Returns the modified `When` object with the new key-value substring exclusion requirement added to the\n`application/x-www-form-urlencoded` expectations.\n", + "form_urlencoded_tuple_exists": "Sets a requirement for the existence of a key in an `application/x-www-form-urlencoded` request body.\n\nThis method sets an expectation that a specific key must be present in the request body of an\n`application/x-www-form-urlencoded` POST request, regardless of its value. The key is URL-encoded\nas specified by the [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded).\n\n**Note**: The mock server does not automatically verify that the HTTP method is POST as per spec.\nIf you want to verify that the request method is POST, you must explicitly set it in your mock configuration.\n\n# Parameters\n- `key`: The key that must exist in the `application/x-www-form-urlencoded` request body.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Arrange\nlet server = MockServer::start();\n\nlet m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_exists(\"name\")\n .form_urlencoded_tuple_exists(\"town\");\n then.status(202);\n});\n\nlet response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Peter%20Griffin&town=Quahog\")\n .send()\n .unwrap();\n\n// Assert\nm.assert();\nassert_eq!(response.status(), 202);\n```\n\n# Returns\n`When`: Returns the modified `When` object with the new key existence requirement added to the\n`application/x-www-form-urlencoded` expectations.\n", + "form_urlencoded_tuple_includes": "Sets a requirement that a key's value in an `application/x-www-form-urlencoded` request body must contain a specific substring.\n\nThis method sets an expectation that the value associated with a specific key must contain a specified substring\nin the request body of an `application/x-www-form-urlencoded` POST request. The key and the substring are URL-encoded\nas specified by the [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded).\n\n**Note**: The mock server does not automatically verify that the HTTP method is POST as per spec.\nIf you want to verify that the request method is POST, you must explicitly set it in your mock configuration.\n\n# Parameters\n- `key`: The key in the `application/x-www-form-urlencoded` request body.\n- `substring`: The substring that must be present in the value associated with the key.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Arrange\nlet server = MockServer::start();\n\nlet m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_includes(\"name\", \"Griffin\")\n .form_urlencoded_tuple_includes(\"town\", \"Quahog\");\n then.status(202);\n});\n\nlet response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Peter%20Griffin&town=Quahog\")\n .send()\n .unwrap();\n\n// Assert\nm.assert();\nassert_eq!(response.status(), 202);\n```\n\n# Returns\n`When`: Returns the modified `When` object with the new key-value substring requirement added to the\n`application/x-www-form-urlencoded` expectations.\n", + "form_urlencoded_tuple_matches": "Sets a requirement that a key-value pair in an `application/x-www-form-urlencoded` request body must match specific regular expressions.\n\nThis method sets an expectation that the key and the value in a key-value pair must match the specified regular expressions\nin the request body of an `application/x-www-form-urlencoded` POST request. The key and value regular expressions are URL-encoded\nas specified by the [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded).\n\n**Note**: The mock server does not automatically verify that the HTTP method is POST as per spec.\nIf you want to verify that the request method is POST, you must explicitly set it in your mock configuration.\n\n# Parameters\n- `key_regex`: The regular expression that the key must match in the `application/x-www-form-urlencoded` request body.\n- `value_regex`: The regular expression that the value must match in the `application/x-www-form-urlencoded` request body.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\nuse regex::Regex;\n\n// Arrange\nlet server = MockServer::start();\n\nlet key_regex = Regex::new(r\"^name$\").unwrap();\nlet value_regex = Regex::new(r\"^Peter\\sGriffin$\").unwrap();\n\nlet m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_matches(key_regex, value_regex);\n then.status(202);\n});\n\nlet response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Peter%20Griffin&town=Quahog\")\n .send()\n .unwrap();\n\n// Assert\nm.assert();\nassert_eq!(response.status(), 202);\n```\n\n# Returns\n`When`: Returns the modified `When` object with the new key-value regex matching requirement added to the\n`application/x-www-form-urlencoded` expectations.\n", + "form_urlencoded_tuple_missing": "Sets a requirement that a key must be absent in an `application/x-www-form-urlencoded` request body.\n\nThis method sets an expectation that a specific key must not be present in the request body of an\n`application/x-www-form-urlencoded` POST request. The key is URL-encoded as specified by the\n[URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded).\n\n**Note**: The mock server does not automatically verify that the HTTP method is POST as per spec.\nIf you want to verify that the request method is POST, you must explicitly set it in your mock configuration.\n\n# Parameters\n- `key`: The key that must be absent in the `application/x-www-form-urlencoded` request body.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Arrange\nlet server = MockServer::start();\n\nlet m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_missing(\"name\")\n .form_urlencoded_tuple_missing(\"town\");\n then.status(202);\n});\n\nlet response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"city=Quahog&occupation=Cartoonist\")\n .send()\n .unwrap();\n\n// Assert\nm.assert();\nassert_eq!(response.status(), 202);\n```\n\n# Returns\n`When`: Returns the modified `When` object with the new key absence requirement added to the\n`application/x-www-form-urlencoded` expectations.\n", + "form_urlencoded_tuple_not": "Adds a key-value pair to the negative requirements for an `application/x-www-form-urlencoded` request body.\n\nThis method sets an expectation for a specific key-value pair to be excluded from the request body\nof an `application/x-www-form-urlencoded` POST request. Each key and value are URL-encoded as specified\nby the [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded).\n\n**Note**: The mock server does not automatically verify that the HTTP method is POST as per spec.\nIf you want to verify that the request method is POST, you must explicitly set it in your mock configuration.\n\n# Parameters\n- `key`: The key of the key-value pair to set as a requirement.\n- `value`: The value of the key-value pair to set as a requirement.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Arrange\nlet server = MockServer::start();\n\nlet m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_not(\"name\", \"Peter Griffin\");\n then.status(202);\n});\n\nlet response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Lois%20Griffin&town=Quahog\")\n .send()\n .unwrap();\n\n// Assert\nm.assert();\nassert_eq!(response.status(), 202);\n```\n\n# Returns\n`When`: Returns the modified `When` object with the new key-value pair added to the negative `application/x-www-form-urlencoded` expectations.\n", + "form_urlencoded_tuple_prefix": "Sets a requirement that a key's value in an `application/x-www-form-urlencoded` request body must start with a specific prefix.\n\nThis method sets an expectation that the value associated with a specific key must start with a specified prefix\nin the request body of an `application/x-www-form-urlencoded` POST request. The key and the prefix are URL-encoded\nas specified by the [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded).\n\n**Note**: The mock server does not automatically verify that the HTTP method is POST as per spec.\nIf you want to verify that the request method is POST, you must explicitly set it in your mock configuration.\n\n# Parameters\n- `key`: The key in the `application/x-www-form-urlencoded` request body.\n- `prefix`: The prefix that must appear at the start of the value associated with the key.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Arrange\nlet server = MockServer::start();\n\nlet m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_prefix(\"name\", \"Pete\")\n .form_urlencoded_tuple_prefix(\"town\", \"Qua\");\n then.status(202);\n});\n\nlet response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Peter%20Griffin&town=Quahog\")\n .send()\n .unwrap();\n\n// Assert\nm.assert();\nassert_eq!(response.status(), 202);\n```\n\n# Returns\n`When`: Returns the modified `When` object with the new key-value prefix requirement added to the\n`application/x-www-form-urlencoded` expectations.\n", + "form_urlencoded_tuple_prefix_not": "Sets a requirement that a key's value in an `application/x-www-form-urlencoded` request body must not start with a specific prefix.\n\nThis method sets an expectation that the value associated with a specific key must not start with a specified prefix\nin the request body of an `application/x-www-form-urlencoded` POST request. The key and the prefix are URL-encoded\nas specified by the [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded).\n\n**Note**: The mock server does not automatically verify that the HTTP method is POST as per spec.\nIf you want to verify that the request method is POST, you must explicitly set it in your mock configuration.\n\n# Parameters\n- `key`: The key in the `application/x-www-form-urlencoded` request body.\n- `prefix`: The prefix that must not appear at the start of the value associated with the key.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Arrange\nlet server = MockServer::start();\n\nlet m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_prefix_not(\"name\", \"Lois\")\n .form_urlencoded_tuple_prefix_not(\"town\", \"Hog\");\n then.status(202);\n});\n\nlet response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Peter%20Griffin&town=Quahog\")\n .send()\n .unwrap();\n\n// Assert\nm.assert();\nassert_eq!(response.status(), 202);\n```\n\n# Returns\n`When`: Returns the modified `When` object with the new key-value prefix exclusion requirement added to the\n`application/x-www-form-urlencoded` expectations.\n", + "form_urlencoded_tuple_suffix": "Sets a requirement that a key's value in an `application/x-www-form-urlencoded` request body must end with a specific suffix.\n\nThis method sets an expectation that the value associated with a specific key must end with a specified suffix\nin the request body of an `application/x-www-form-urlencoded` POST request. The key and the suffix are URL-encoded\nas specified by the [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded).\n\n**Note**: The mock server does not automatically verify that the HTTP method is POST as per spec.\nIf you want to verify that the request method is POST, you must explicitly set it in your mock configuration.\n\n# Parameters\n- `key`: The key in the `application/x-www-form-urlencoded` request body.\n- `suffix`: The suffix that must appear at the end of the value associated with the key.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Arrange\nlet server = MockServer::start();\n\nlet m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_suffix(\"name\", \"Griffin\")\n .form_urlencoded_tuple_suffix(\"town\", \"hog\");\n then.status(202);\n});\n\nlet response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Peter%20Griffin&town=Quahog\")\n .send()\n .unwrap();\n\n// Assert\nm.assert();\nassert_eq!(response.status(), 202);\n```\n\n# Returns\n`When`: Returns the modified `When` object with the new key-value suffix requirement added to the\n`application/x-www-form-urlencoded` expectations.\n", + "form_urlencoded_tuple_suffix_not": "Sets a requirement that a key's value in an `application/x-www-form-urlencoded` request body must not end with a specific suffix.\n\nThis method sets an expectation that the value associated with a specific key must not end with a specified suffix\nin the request body of an `application/x-www-form-urlencoded` POST request. The key and the suffix are URL-encoded\nas specified by the [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded).\n\n**Note**: The mock server does not automatically verify that the HTTP method is POST as per spec.\nIf you want to verify that the request method is POST, you must explicitly set it in your mock configuration.\n\n# Parameters\n- `key`: The key in the `application/x-www-form-urlencoded` request body.\n- `suffix`: The suffix that must not appear at the end of the value associated with the key.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Arrange\nlet server = MockServer::start();\n\nlet m = server.mock(|when, then| {\n when.method(POST)\n .path(\"/example\")\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .form_urlencoded_tuple_suffix_not(\"name\", \"Smith\")\n .form_urlencoded_tuple_suffix_not(\"town\", \"ville\");\n then.status(202);\n});\n\nlet response = Client::new()\n .post(server.url(\"/example\"))\n .header(\"content-type\", \"application/x-www-form-urlencoded\")\n .body(\"name=Peter%20Griffin&town=Quahog\")\n .send()\n .unwrap();\n\n// Assert\nm.assert();\nassert_eq!(response.status(), 202);\n```\n\n# Returns\n`When`: Returns the modified `When` object with the new key-value suffix exclusion requirement added to the\n`application/x-www-form-urlencoded` expectations.\n", + "header": "Sets the expected HTTP header and its value for the request to match.\nThis function ensures that the specified header with the given value is present in the request.\nHeader names are case-insensitive, as per RFC 2616.\n\n# Parameters\n- `name`: The HTTP header name. Header names are case-insensitive.\n- `value`: The expected value of the HTTP header.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the \"Authorization\" header with a specific value\nlet mock = server.mock(|when, then| {\n when.header(\"Authorization\", \"token 1234567890\");\n then.status(200); // Respond with a 200 status code if the header and value are present\n});\n\n// Make a request that includes the \"Authorization\" header with the specified value\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Authorization\", \"token 1234567890\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "header_count": "Sets the requirement that the HTTP request must contain a specific number of headers whose keys and values match specified patterns.\nThis function ensures that the specified number of headers with keys and values matching the given patterns are present in the request.\nHeader names are case-insensitive, as per RFC 2616.\n\nThis function may be called multiple times to check multiple patterns and counts.\n\n# Parameters\n- `key_pattern`: The pattern that the header keys must match.\n- `value_pattern`: The pattern that the header values must match.\n- `count`: The number of headers with keys and values matching the patterns that must be present.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects at least 2 headers whose keys match the regex \"^X-Custom-Header.*\"\n// and values match the regex \"value.*\"\nlet mock = server.mock(|when, then| {\n when.header_count(\"^X-Custom-Header.*\", \"value.*\", 2);\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request that includes the required headers\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"x-custom-header-1\", \"value1\")\n .header(\"X-Custom-Header-2\", \"value2\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "header_excludes": "Sets the requirement that the HTTP request must contain a specific header whose value does not contain a specified substring.\nThis function ensures that the specified header is present and its value does not contain the given substring.\nHeader names are case-insensitive, as per RFC 2616.\n\nThis function may be called multiple times to check multiple headers and substrings.\n\n# Parameters\n- `name`: The HTTP header name. Header names are case-insensitive.\n- `substring`: The substring that the header value must not contain.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the \"Authorization\" header's value to not contain \"Bearer\"\nlet mock = server.mock(|when, then| {\n when.header_excludes(\"Authorization\", \"Bearer\");\n then.status(200); // Respond with a 200 status code if the header value does not contain the substring\n});\n\n// Make a request that includes the \"Authorization\" header without the forbidden substring in its value\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Authorization\", \"token 1234567890\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "header_exists": "Sets the requirement that the HTTP request must contain a specific header.\nThe presence of the header is checked, but its value is not validated.\nFor value validation, refer to [Mock::expect_header](struct.Mock.html#method.expect_header).\n\n# Parameters\n- `name`: The HTTP header name. Header names are case-insensitive, as per RFC 2616.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the \"Authorization\" header to be present in the request\nlet mock = server.mock(|when, then| {\n when.header_exists(\"Authorization\");\n then.status(200); // Respond with a 200 status code if the header is present\n});\n\n// Make a request that includes the \"Authorization\" header\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Authorization\", \"token 1234567890\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "header_includes": "Sets the requirement that the HTTP request must contain a specific header whose value contains a specified substring.\nThis function ensures that the specified header is present and its value contains the given substring.\nHeader names are case-insensitive, as per RFC 2616.\n\nThis function may be called multiple times to check multiple headers and substrings.\n\n# Parameters\n- `name`: The HTTP header name. Header names are case-insensitive.\n- `substring`: The substring that the header value must contain.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the \"Authorization\" header's value to contain \"token\"\nlet mock = server.mock(|when, then| {\n when.header_includes(\"Authorization\", \"token\");\n then.status(200); // Respond with a 200 status code if the header value contains the substring\n});\n\n// Make a request that includes the \"Authorization\" header with the specified substring in its value\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Authorization\", \"token 1234567890\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "header_matches": "Sets the requirement that the HTTP request must contain a specific header whose key and value match the specified regular expressions.\nThis function ensures that the specified header is present and both its key and value match the given regular expressions.\nHeader names are case-insensitive, as per RFC 2616.\n\nThis function may be called multiple times to check multiple headers and patterns.\n\n# Parameters\n- `key_regex`: The regular expression that the header key must match.\n- `value_regex`: The regular expression that the header value must match.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the \"Authorization\" header's key to match the regex \"^Auth.*\"\n// and its value to match the regex \"token .*\"\nlet mock = server.mock(|when, then| {\n when.header_matches(\"^Auth.*\", \"token .*\");\n then.status(200); // Respond with a 200 status code if the header key and value match the patterns\n});\n\n// Make a request that includes the \"Authorization\" header with a value matching the regex\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Authorization\", \"token 1234567890\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "header_missing": "Sets the requirement that the HTTP request must not contain a specific header.\nThis function ensures that the specified header is absent in the request.\nHeader names are case-insensitive, as per RFC 2616.\n\nThis function may be called multiple times to add multiple excluded headers.\n\n# Parameters\n- `name`: The HTTP header name. Header names are case-insensitive.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the \"Authorization\" header to be absent in the request\nlet mock = server.mock(|when, then| {\n when.header_missing(\"Authorization\");\n then.status(200); // Respond with a 200 status code if the header is absent\n});\n\n// Make a request that does not include the \"Authorization\" header\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "header_not": "Sets the requirement that the HTTP request must not contain a specific header with the specified value.\nThis function ensures that the specified header with the given value is absent in the request.\nHeader names are case-insensitive, as per RFC 2616.\n\nThis function may be called multiple times to add multiple excluded headers.\n\n# Parameters\n- `name`: The HTTP header name. Header names are case-insensitive.\n- `value`: The value of the HTTP header that must not be present.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the \"Authorization\" header with a specific value to be absent\nlet mock = server.mock(|when, then| {\n when.header_not(\"Authorization\", \"token 1234567890\");\n then.status(200); // Respond with a 200 status code if the header and value are absent\n});\n\n// Make a request that includes the \"Authorization\" header with a different value\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Authorization\", \"token abcdefg\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "header_prefix": "Sets the requirement that the HTTP request must contain a specific header whose value starts with a specified prefix.\nThis function ensures that the specified header is present and its value starts with the given prefix.\nHeader names are case-insensitive, as per RFC 2616.\n\nThis function may be called multiple times to check multiple headers and prefixes.\n\n# Parameters\n- `name`: The HTTP header name. Header names are case-insensitive.\n- `prefix`: The prefix that the header value must start with.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the \"Authorization\" header's value to start with \"token\"\nlet mock = server.mock(|when, then| {\n when.header_prefix(\"Authorization\", \"token\");\n then.status(200); // Respond with a 200 status code if the header value starts with the prefix\n});\n\n// Make a request that includes the \"Authorization\" header with the specified prefix in its value\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Authorization\", \"token 1234567890\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "header_prefix_not": "Sets the requirement that the HTTP request must contain a specific header whose value does not start with a specified prefix.\nThis function ensures that the specified header is present and its value does not start with the given prefix.\nHeader names are case-insensitive, as per RFC 2616.\n\nThis function may be called multiple times to check multiple headers and prefixes.\n\n# Parameters\n- `name`: The HTTP header name. Header names are case-insensitive.\n- `prefix`: The prefix that the header value must not start with.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the \"Authorization\" header's value to not start with \"Bearer\"\nlet mock = server.mock(|when, then| {\n when.header_prefix_not(\"Authorization\", \"Bearer\");\n then.status(200); // Respond with a 200 status code if the header value does not start with the prefix\n});\n\n// Make a request that includes the \"Authorization\" header without the \"Bearer\" prefix in its value\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Authorization\", \"token 1234567890\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "header_suffix": "Sets the requirement that the HTTP request must contain a specific header whose value ends with a specified suffix.\nThis function ensures that the specified header is present and its value ends with the given suffix.\nHeader names are case-insensitive, as per RFC 2616.\n\nThis function may be called multiple times to check multiple headers and suffixes.\n\n# Parameters\n- `name`: The HTTP header name. Header names are case-insensitive.\n- `suffix`: The suffix that the header value must end with.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the \"Authorization\" header's value to end with \"7890\"\nlet mock = server.mock(|when, then| {\n when.header_suffix(\"Authorization\", \"7890\");\n then.status(200); // Respond with a 200 status code if the header value ends with the suffix\n});\n\n// Make a request that includes the \"Authorization\" header with the specified suffix in its value\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Authorization\", \"token 1234567890\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "header_suffix_not": "Sets the requirement that the HTTP request must contain a specific header whose value does not end with a specified suffix.\nThis function ensures that the specified header is present and its value does not end with the given suffix.\nHeader names are case-insensitive, as per RFC 2616.\n\nThis function may be called multiple times to check multiple headers and suffixes.\n\n# Parameters\n- `name`: The HTTP header name. Header names are case-insensitive.\n- `suffix`: The suffix that the header value must not end with.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the \"Authorization\" header's value to not end with \"abc\"\nlet mock = server.mock(|when, then| {\n when.header_suffix_not(\"Authorization\", \"abc\");\n then.status(200); // Respond with a 200 status code if the header value does not end with the suffix\n});\n\n// Make a request that includes the \"Authorization\" header without the \"abc\" suffix in its value\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Authorization\", \"token 1234567890\")\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "host": "Sets the expected host name. This constraint is especially useful when working with\nproxy or forwarding rules, but it can also be used to serve mocks (e.g., when using a mock\nserver as a proxy).\n\n**Note**: Host matching is case-insensitive, conforming to\n[RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2).\nThis standard dictates that all host names are treated equivalently, regardless of character case.\n\n**Note**: Both `localhost` and `127.0.0.1` are treated equally.\nIf the provided host is set to either `localhost` or `127.0.0.1`, it will match\nrequests containing either `localhost` or `127.0.0.1`.\n\n* `host` - The host name (should not include a port).\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\nlet server = MockServer::start();\n\nserver.mock(|when, then| {\n when.host(\"github.com\");\n then.body(\"This is a mock response\");\n});\n\nlet client = Client::builder()\n .proxy(reqwest::Proxy::all(&server.base_url()).unwrap())\n .build()\n .unwrap();\n\nlet response = client.get(\"http://github.com\").send().unwrap();\n\nassert_eq!(response.text().unwrap(), \"This is a mock response\");\n```\n\n# Returns\nThe updated `When` instance to enable method chaining.\n\n", + "host_excludes": "Adds a substring that must not be present within the request's host name for the mock server to respond.\n\nThis method ensures that the mock server does not respond to requests if the host name contains the specified substring.\n\nThis constraint is especially useful when working with proxy or forwarding rules, but it\ncan also be used to serve mocks (e.g., when using a mock server as a proxy).\n\nTo add multiple excluded substrings, invoke this function multiple times.\n\n**Note**: Host matching is case-insensitive, conforming to\n[RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2).\nThis standard dictates that all host names are treated equivalently, regardless of character case.\n\n**Note**: This function does not automatically compare with pseudo names, like \"localhost\".\n\n# Example\n\n```rust\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that excludes any request where the host name contains \"www.google.com\"\nlet mock = server.mock(|when, then| {\n when.host_excludes(\"www.google.com\"); // Exclude hosts containing \"www.google.com\"\n then.status(200); // Respond with status code 200 for other matched requests\n});\n\n// Make a request to a URL whose host name will be \"localhost\" and trigger the mock\nlet response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n// Ensure the request was successful\nassert_eq!(response.status(), 200);\n\n// Ensure that the mock was called at least once\nmock.assert();\n```\n\n# Parameters\n- `host`: A string or other type convertible to `String` that will be added as a substring to exclude from matching.\n\n# Returns\nThe updated `When` instance to enable method chaining.\n\n", + "host_includes": "Adds a substring to match within the request's host name.\n\nThis method ensures that the mock server only matches requests whose host name contains the specified substring.\n\nThis constraint is especially useful when working with proxy or forwarding rules, but it\ncan also be used to serve mocks (e.g., when using a mock server as a proxy).\n\nTo add multiple substrings, invoke this function multiple times.\n\n**Note**: Host matching is case-insensitive, conforming to\n[RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2).\nThis standard dictates that all host names are treated equivalently, regardless of character case.\n\n**Note**: This function does not automatically compare with pseudo names, like \"localhost\".\n\n# Attention\nThis function does not automatically treat 127.0.0.1 like localhost.\n\n# Example\n\n```rust\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that matches any request where the host name contains \"localhost\"\nlet mock = server.mock(|when, then| {\n when.host_includes(\"0.0\"); // Only match hosts containing \"0.0\" (e.g., 127.0.0.1)\n then.status(200); // Respond with status code 200 for all matched requests\n});\n\n// Make a request to a URL whose host name is \"localhost\" to trigger the mock\nlet response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n// Ensure the request was successful\nassert_eq!(response.status(), 200);\n\n// Ensure that the mock was called at least once\nmock.assert();\n```\n\n# Parameters\n- `host`: A string or other type convertible to `String` that will be added as a substring to match against the request's host name.\n\n# Returns\nThe updated `When` instance to enable method chaining.\n\n", + "host_matches": "Sets a regular expression pattern that the request's host name must match for the mock server to respond.\n\nThis constraint is especially useful when working with proxy or forwarding rules, but it\ncan also be used to serve mocks (e.g., when using a mock server as a proxy).\n\nTo add multiple patterns, invoke this function multiple times.\n\n**Note**: Host matching is case-insensitive, conforming to\n[RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2).\nThis standard dictates that all host names are treated equivalently, regardless of character case.\n\n**Note**: This function does not automatically compare with pseudo names, like \"localhost\".\n\n# Parameters\n- `regex`: A regular expression pattern to match against the host name. Should be a valid regex string.\n\n# Example\n```rust\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that matches requests where the host name is exactly \"localhost\"\nlet mock = server.mock(|when, then| {\n when.host_matches(r\"^127.0.0.1$\");\n then.status(200);\n});\n\n// Make a request with \"127.0.0.1\" as the host name to trigger the mock response.\nlet response = reqwest::blocking::get(server.url(\"/\")).unwrap();\n\n// Ensure the request was successful\nassert_eq!(response.status(), 200);\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to enable method chaining.\n\n", + "host_not": "Sets the host name that should **NOT** be responded for.\n\nThis constraint is especially useful when working with proxy or forwarding rules, but it\ncan also be used to serve mocks (e.g., when using a mock server as a proxy).\n\nTo add multiple suffixes, invoke this function multiple times.\n\n**Note**: Host matching is case-insensitive, conforming to\n[RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2).\nThis standard dictates that all host names are treated equivalently, regardless of character case.\n\n* `host` - The host name (should not include a port).\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\nlet server = MockServer::start();\n\nserver.mock(|when, then| {\n when.host(\"github.com\");\n then.body(\"This is a mock response\");\n});\n\nlet client = Client::builder()\n .proxy(reqwest::Proxy::all(&server.base_url()).unwrap())\n .build()\n .unwrap();\n\nlet response = client.get(\"http://github.com\").send().unwrap();\n\nassert_eq!(response.text().unwrap(), \"This is a mock response\");\n```\n\n# Returns\nThe updated `When` instance to enable method chaining.\n\n", + "host_prefix": "Adds a prefix that the request's host name must start with for the mock server to respond.\n\nThis constraint is especially useful when working with proxy or forwarding rules, but it\ncan also be used to serve mocks (e.g., when using a mock server as a proxy).\n\nTo add multiple prefixes, invoke this function multiple times.\n\n**Note**: Host matching is case-insensitive, conforming to\n[RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2).\nThis standard dictates that all host names are treated equivalently, regardless of character case.\n\n**Note**: This function does not automatically compare with pseudo names, like \"localhost\".\n\n# Example\n\n```rust\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that matches any request where the host name starts with \"local\"\nlet mock = server.mock(|when, then| {\n when.host_prefix(\"127.0\"); // Only match hosts starting with \"127.0\"\n then.status(200); // Respond with status code 200 for all matched requests\n});\n\n// Make a request to the mock server with a host name of \"127.0.0.1\" to trigger the mock response.\nlet response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n// Ensure the request was successful\nassert_eq!(response.status(), 200);\n\n// Ensure that the mock was called at least once\nmock.assert();\n```\n\n# Parameters\n- `prefix`: A string or other type convertible to `String` specifying the prefix that the host name should start with.\n\n# Returns\nThe updated `When` instance to enable method chaining.\n\n", + "host_prefix_not": "Adds a prefix that the request's host name must *not* start with for the mock server to respond.\n\nThis constraint is especially useful when working with proxy or forwarding rules, but it\ncan also be used to serve mocks (e.g., when using a mock server as a proxy).\n\nTo add multiple excluded prefixes, invoke this function multiple times.\n\n**Note**: Host matching is case-insensitive, conforming to\n[RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2).\nThis standard dictates that all host names are treated equivalently, regardless of character case.\n\n**Note**: This function does not automatically compare with pseudo names, like \"localhost\".\n\n# Example\n```rust\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that matches any request where the host name does not start with \"www.\"\nlet mock = server.mock(|when, then| {\n when.host_prefix_not(\"www.\"); // Exclude hosts starting with \"www\"\n then.status(200); // Respond with status code 200 for all other requests\n});\n\n// Make a request with host name \"localhost\" that does not start with \"www\" and therefore\n// triggers the mock response.\nlet response = reqwest::blocking::get(server.url(\"/example\")).unwrap();\n\n// Ensure the request was successful\nassert_eq!(response.status(), 200);\n\n// Ensure that the mock was called at least once\nmock.assert();\n```\n\n# Parameters\n- `prefix`: A string or other type convertible to `String` specifying the prefix that the host name should *not* start with.\n\n# Returns\nThe updated `When` instance to enable method chaining.\n\n", + "host_suffix": "Adds a suffix that the request's host name must end with for the mock server to respond.\n\nThis constraint is especially useful when working with proxy or forwarding rules, but it\ncan also be used to serve mocks (e.g., when using a mock server as a proxy).\n\nTo add multiple suffixes, invoke this function multiple times.\n\n**Note**: Host matching is case-insensitive, conforming to\n[RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2).\nThis standard dictates that all host names are treated equivalently, regardless of character case.\n\n**Note**: This function does not automatically compare with pseudo names, like \"localhost\".\n\n# Example\n\n```rust\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that matches any request where the host name ends with \"host\" (e.g., \"localhost\").\nlet mock = server.mock(|when, then| {\n when.host_suffix(\"0.1\"); // Only match hosts ending with \"0.1\"\n then.status(200); // Respond with status code 200 for all matched requests\n});\n\n// Make a request to the mock server with a host name of \"127.0.0.1\" to trigger the mock response.\nlet response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n// Ensure the request was successful\nassert_eq!(response.status(), 200);\n\n// Ensure that the mock was called at least once\nmock.assert();\n```\n\n# Parameters\n- `host`: A string or other type convertible to `String` specifying the suffix that the host name should end with.\n\n# Returns\nThe updated `When` instance to enable method chaining.\n\n", + "host_suffix_not": "Adds a suffix that the request's host name must *not* end with for the mock server to respond.\n\nThis constraint is especially useful when working with proxy or forwarding rules, but it\ncan also be used to serve mocks (e.g., when using a mock server as a proxy).\n\nTo add multiple excluded suffixes, invoke this function multiple times.\n\n**Note**: Host matching is case-insensitive, conforming to\n[RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2).\nThis standard dictates that all host names are treated equivalently, regardless of character case.\n\n**Note**: This function does not automatically compare with pseudo names, like \"localhost\".\n\n# Example\n```rust\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that matches any request where the host name does not end with \"host\".\nlet mock = server.mock(|when, then| {\n when.host_suffix_not(\"host\"); // Exclude hosts ending with \"host\"\n then.status(200); // Respond with status code 200 for all other requests\n});\n\n// Make a request with a host name that does not end with \"host\" to trigger the mock response.\nlet response = reqwest::blocking::get(server.url(\"/example\")).unwrap();\n\n// Ensure the request was successful\nassert_eq!(response.status(), 200);\n\n// Ensure that the mock was called at least once\nmock.assert();\n```\n\n# Parameters\n- `host`: A string or other type convertible to `String` specifying the suffix that the host name should *not* end with.\n\n# Returns\nThe updated `When` instance to enable method chaining.\n\n", + "is_false": "Adds a custom matcher for expected HTTP requests. If this function returns false, the request\nis considered a match, and the mock server will respond to the request\n(given all other criteria are also met).\n\nYou can use this function to create custom expectations for your mock server based on any aspect\nof the `HttpMockRequest` object.\n\n# Parameters\n- `matcher`: A function that takes a reference to an `HttpMockRequest` and returns a boolean indicating whether the request matches.\n\n## Example\n```rust\nuse httpmock::prelude::*;\n\n// Arrange\nlet server = MockServer::start();\n\nlet m = server.mock(|when, then| {\n when.is_false(|req: &HttpMockRequest| {\n req.uri().path().contains(\"es\")\n });\n then.status(404);\n});\n\n// Act: Send the HTTP request\nlet response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n// Assert\nm.assert();\nassert_eq!(response.status(), 404);\n```\n\n# Returns\n`When`: Returns the modified `When` object with the new custom matcher added to the expectations.\n", + "is_true": "Adds a custom matcher for expected HTTP requests. If this function returns true, the request\nis considered a match, and the mock server will respond to the request\n(given all other criteria are also met).\n\nYou can use this function to create custom expectations for your mock server based on any aspect\nof the `HttpMockRequest` object.\n\n# Parameters\n- `matcher`: A function that takes a reference to an `HttpMockRequest` and returns a boolean indicating whether the request matches.\n\n## Example\n```rust\nuse httpmock::prelude::*;\n\n// Arrange\nlet server = MockServer::start();\n\nlet m = server.mock(|when, then| {\n when.is_true(|req: &HttpMockRequest| {\n req.uri().path().contains(\"es\")\n });\n then.status(200);\n});\n\n// Act: Send the HTTP request\nlet response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n// Assert\nm.assert();\nassert_eq!(response.status(), 200);\n```\n\n# Returns\n`When`: Returns the modified `When` object with the new custom matcher added to the expectations.\n", + "json_body": "Sets the condition that the HTTP request body content must match the specified JSON structure.\nThis method ensures that the request body exactly matches the JSON value provided.\n\n**Note**: The body content is case-sensitive.\n\n**Note**: This method does not automatically verify the `Content-Type` header.\nIf specific content type verification is required (e.g., `application/json`),\nyou must add this expectation manually.\n\n# Parameters\n- `json_value`: The JSON structure that the HTTP request body must match. This parameter accepts any type that can be converted into a `serde_json::Value`.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\nuse serde_json::json;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the request body to match a specific JSON structure\nlet mock = server.mock(|when, then| {\n when.json_body(json!({\n \"title\": \"The Great Gatsby\",\n \"author\": \"F. Scott Fitzgerald\"\n }));\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Make a request with a JSON body that matches the expected structure\nClient::new()\n .post(&format!(\"http://{}/test\", server.address()))\n .header(\"Content-Type\", \"application/json\") // It's important to set the Content-Type header manually\n .body(r#\"{\"title\":\"The Great Gatsby\",\"author\":\"F. Scott Fitzgerald\"}\"#)\n .send()\n .unwrap();\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When’ instance to allow method chaining for additional configuration.\n", + "json_body_excludes": "Sets the expected partial JSON body to ensure that specific content is not present within a larger JSON structure.\n\n**Attention:** The partial JSON string must be a valid JSON string and should represent a substructure\nof the full JSON object. It can omit irrelevant attributes but must maintain any necessary object hierarchy.\n\n**Note:** This method does not automatically set the `Content-Type` header to `application/json`.\nYou must explicitly set this header in your requests.\n\n# Parameters\n- `partial_body`: The partial JSON content to check for exclusion. This must be a valid JSON string.\n\n# Example\nSuppose your application sends the following JSON request body:\n```json\n{\n \"parent_attribute\": \"Some parent data goes here\",\n \"child\": {\n \"target_attribute\": \"Example\",\n \"other_attribute\": \"Another value\"\n }\n}\n```\nTo verify the absence of `target_attribute` with the value `Example`:\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\nlet server = MockServer::start();\n\nlet mock = server.mock(|when, then| {\n when.json_body_excludes(r#\"\n {\n \"child\": {\n \"target_attribute\": \"Example\"\n }\n }\n \"#);\n then.status(200);\n});\n\n// Send a POST request with a JSON body\nlet response = Client::new()\n .post(&format!(\"http://{}/some/path\", server.address()))\n .header(\"content-type\", \"application/json\")\n .body(r#\"\n {\n \"parent_attribute\": \"Some parent data goes here\",\n \"child\": {\n \"other_attribute\": \"Another value\"\n }\n }\n \"#)\n .send()\n .unwrap();\n\n// Assert the mock was called and the response status is as expected\nmock.assert();\nassert_eq!(response.status(), 200);\n```\nIt's important that the partial JSON contains the full object hierarchy necessary to reach the target attribute.\nIrrelevant attributes such as `parent_attribute` and `child.other_attribute` in the example can be omitted.\n", + "json_body_includes": "Sets the expected partial JSON body to check for specific content within a larger JSON structure.\n\n**Attention:** The partial JSON string must be a valid JSON string and should represent a substructure\nof the full JSON object. It can omit irrelevant attributes but must maintain any necessary object hierarchy.\n\n**Note:** This method does not automatically set the `Content-Type` header to `application/json`.\nYou must explicitly set this header in your requests.\n\n# Parameters\n- `partial_body`: The partial JSON content to check for. This must be a valid JSON string.\n\n# Example\nSuppose your application sends the following JSON request body:\n```json\n{\n \"parent_attribute\": \"Some parent data goes here\",\n \"child\": {\n \"target_attribute\": \"Example\",\n \"other_attribute\": \"Another value\"\n }\n}\n```\nTo verify the presence of `target_attribute` with the value `Example` without needing the entire JSON object:\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\nlet server = MockServer::start();\n\nlet mock = server.mock(|when, then| {\n when.json_body_includes(r#\"\n {\n \"child\": {\n \"target_attribute\": \"Example\"\n }\n }\n \"#);\n then.status(200);\n});\n\n// Send a POST request with a JSON body\nlet response = Client::new()\n .post(&format!(\"http://{}/some/path\", server.address()))\n .header(\"content-type\", \"application/json\")\n .body(r#\"\n {\n \"parent_attribute\": \"Some parent data goes here\",\n \"child\": {\n \"target_attribute\": \"Example\",\n \"other_attribute\": \"Another value\"\n }\n }\n \"#)\n .send()\n .unwrap();\n\n// Assert the mock was called and the response status is as expected\nmock.assert();\nassert_eq!(response.status(), 200);\n```\nIt's important that the partial JSON contains the full object hierarchy necessary to reach the target attribute.\nIrrelevant attributes such as `parent_attribute` and `child.other_attribute` can be omitted.\n", + "json_body_obj": "Sets the expected JSON body using a serializable serde object.\nThis function automatically serializes the given object into a JSON string using serde.\n\n**Note**: This method does not automatically verify the `Content-Type` header.\nIf specific content type verification is required (e.g., `application/json`),\nyou must add this expectation manually.\n\n# Parameters\n- `body`: The HTTP body object to be serialized to JSON. This object should implement both `serde::Serialize` and `serde::Deserialize`.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\nuse serde_json::json;\nuse serde::{Serialize, Deserialize};\n\n#[derive(Serialize, Deserialize)]\nstruct TestUser {\n name: String,\n}\n\n// Initialize logging (optional, for debugging purposes)\nlet _ = env_logger::try_init();\n\n// Start the mock server\nlet server = MockServer::start();\n\n// Set up a mock endpoint\nlet m = server.mock(|when, then| {\n when.path(\"/user\")\n .header(\"content-type\", \"application/json\")\n .json_body_obj(&TestUser { name: String::from(\"Fred\") });\n then.status(200);\n});\n\n// Send a POST request with a JSON body\nlet response = Client::new()\n .post(&format!(\"http://{}/user\", server.address()))\n .header(\"content-type\", \"application/json\")\n .body(json!(&TestUser { name: \"Fred\".to_string() }).to_string())\n .send()\n .unwrap();\n\n// Assert the mock was called and the response status is as expected\nm.assert();\nassert_eq!(response.status(), 200);\n```\n\nThis method is particularly useful when you need to test server responses to structured JSON data. It helps\nensure that the JSON serialization and deserialization processes are correctly implemented in your API handling logic.\n", + "matches": "Adds a custom matcher for expected HTTP requests. If this function returns true, the request\nis considered a match, and the mock server will respond to the request\n(given all other criteria are also met).\n\nYou can use this function to create custom expectations for your mock server based on any aspect\nof the `HttpMockRequest` object.\n\n# Parameters\n- `matcher`: A function that takes a reference to an `HttpMockRequest` and returns a boolean indicating whether the request matches.\n\n## Example\n```rust\nuse httpmock::prelude::*;\n\n// Arrange\nlet server = MockServer::start();\n\nlet m = server.mock(|when, then| {\n when.matches(|req: &HttpMockRequest| {\n req.uri().path().contains(\"es\")\n });\n then.status(200);\n});\n\n// Act: Send the HTTP request\nlet response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n// Assert\nm.assert();\nassert_eq!(response.status(), 200);\n```\n\n# Returns\n`When`: Returns the modified `When` object with the new custom matcher added to the expectations.\n", + "method": "Sets the expected HTTP method for which the mock server should respond.\n\nThis method ensures that the mock server only matches requests that use the specified HTTP method,\nsuch as `GET`, `POST`, or any other valid method. This allows testing behavior that's specific\nto different types of HTTP requests.\n\n**Note**: Method matching is case-insensitive.\n\n# Example\n\n```rust\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that matches only `GET` requests\nlet mock = server.mock(|when, then| {\n when.method(GET); // Match only `GET` HTTP method\n then.status(200); // Respond with status code 200 for all matched requests\n});\n\n// Make a GET request to the server's URL to trigger the mock\nlet response = reqwest::blocking::get(server.url(\"/\")).unwrap();\n\n// Ensure the request was successful\nassert_eq!(response.status(), 200);\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Parameters\n- `method`: An HTTP method as either a `Method` enum or a `String` value, specifying the expected method type for matching.\n\n# Returns\nThe updated `When` instance to allow for method chaining.\n\n", + "method_not": "Excludes the specified HTTP method from the requests the mock server will respond to.\n\nThis method ensures that the mock server does not respond to requests using the given HTTP method,\nlike `GET`, `POST`, etc. This allows testing scenarios where a particular method should not\ntrigger a response, and thus testing behaviors like method-based security.\n\n**Note**: Method matching is case-insensitive.\n\n# Example\n\n```rust\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that matches any request except those using the `POST` method\nlet mock = server.mock(|when, then| {\n when.method_not(POST); // Exclude the `POST` HTTP method from matching\n then.status(200); // Respond with status code 200 for all other matched requests\n});\n\n// Make a GET request to the server's URL, which will trigger the mock\nlet response = reqwest::blocking::get(server.url(\"/\")).unwrap();\n\n// Ensure the request was successful\nassert_eq!(response.status(), 200);\n\n// Ensure that the mock was called at least once\nmock.assert();\n```\n\n# Parameters\n- `method`: An HTTP method as either a `Method` enum or a `String` value, specifying the method type to exclude from matching.\n\n# Returns\nThe updated `When` instance to allow for method chaining.\n\n", + "path": "Specifies the expected URL path that incoming requests must match for the mock server to respond.\nThis is useful for targeting specific endpoints, such as API routes, to ensure only relevant requests trigger the mock response.\n\n# Parameters\n- `path`: A string or other value convertible to `String` that represents the expected URL path.\n\n# Example\n```rust\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that matches requests to `/test`\nlet mock = server.mock(|when, then| {\n when.path(\"/test\");\n then.status(200); // Respond with a 200 status code\n});\n\n// Make a request to the mock server using the specified path\nlet response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n// Ensure the request was successful\nassert_eq!(response.status(), 200);\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance, allowing method chaining for additional configuration.\n\n", + "path_excludes": "Specifies a substring that the URL path must *not* contain for the mock server to respond.\nThis constraint is useful for excluding requests to paths containing particular segments or patterns.\n\n# Parameters\n- `substring`: A string or other value convertible to `String` representing the substring that should not appear in the URL path.\n\n# Example\n```rust\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that matches any path not containing the substring \"xyz\"\nlet mock = server.mock(|when, then| {\n when.path_excludes(\"xyz\");\n then.status(200); // Respond with status 200 for paths excluding \"xyz\"\n});\n\n// Make a request to a path that does not contain \"xyz\"\nlet response = reqwest::blocking::get(server.url(\"/testpath\")).unwrap();\n\n// Ensure the request was successful\nassert_eq!(response.status(), 200);\n\n// Ensure the mock server returned the expected response\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to enable method chaining for additional configuration.\n\n", + "path_includes": "Specifies a substring that the URL path must contain for the mock server to respond.\nThis constraint is useful for matching URLs based on partial segments, especially when exact path matching isn't required.\n\n# Parameters\n- `substring`: A string or any value convertible to `String` representing the substring that must be present in the URL path.\n\n# Example\n```rust\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that matches any path containing the substring \"es\"\nlet mock = server.mock(|when, then| {\n when.path_includes(\"es\");\n then.status(200); // Respond with a 200 status code for matched requests\n});\n\n// Make a request to a path containing \"es\" to trigger the mock response\nlet response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n// Ensure the request was successful\nassert_eq!(response.status(), 200);\n\n// Ensure that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for further configuration.\n\n", + "path_matches": "Specifies a regular expression that the URL path must match for the mock server to respond.\nThis method allows flexible matching using regex patterns, making it useful for various matching scenarios.\n\n# Parameters\n- `regex`: An expression that implements `Into`, representing the regex pattern to match against the URL path.\n\n# Example\n```rust\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that matches paths ending with the suffix \"le\"\nlet mock = server.mock(|when, then| {\n when.path_matches(r\"le$\");\n then.status(200); // Respond with a 200 status code for paths matching the pattern\n});\n\n// Make a request to a path ending with \"le\"\nlet response = reqwest::blocking::get(server.url(\"/example\")).unwrap();\n\n// Ensure the request was successful\nassert_eq!(response.status(), 200);\n\n// Verify that the mock server returned the expected response\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n# Errors\nThis function will panic if the provided regex pattern is invalid.\n\n", + "path_not": "Specifies the URL path that incoming requests must *not* match for the mock server to respond.\nThis is helpful when you need to exclude specific endpoints while allowing others through.\n\nTo add multiple excluded paths, invoke this function multiple times.\n\n# Parameters\n- `path`: A string or other value convertible to `String` that represents the URL path to exclude.\n\n# Example\n```rust\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that will not match requests to `/exclude`\nlet mock = server.mock(|when, then| {\n when.path_not(\"/exclude\");\n then.status(200); // Respond with status 200 for all other paths\n});\n\n// Make a request to a path that does not match the exclusion\nlet response = reqwest::blocking::get(server.url(\"/include\")).unwrap();\n\n// Ensure the request was successful\nassert_eq!(response.status(), 200);\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance, allowing method chaining for further configuration.\n\n", + "path_prefix": "Specifies a prefix that the URL path must start with for the mock server to respond.\nThis is useful when only the initial segments of a path need to be validated, such as checking specific API routes.\n\n# Parameters\n- `prefix`: A string or other value convertible to `String` representing the prefix that the URL path should start with.\n\n# Example\n```rust\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that matches any path starting with the prefix \"/api\"\nlet mock = server.mock(|when, then| {\n when.path_prefix(\"/api\");\n then.status(200); // Respond with a 200 status code for matched requests\n});\n\n// Make a request to a path starting with \"/api\"\nlet response = reqwest::blocking::get(server.url(\"/api/v1/resource\")).unwrap();\n\n// Ensure the request was successful\nassert_eq!(response.status(), 200);\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for further configuration.\n\n", + "path_prefix_not": "Specifies a prefix that the URL path must not start with for the mock server to respond.\nThis constraint is useful for excluding paths that begin with particular segments or patterns.\n\n# Parameters\n- `prefix`: A string or other value convertible to `String` representing the prefix that the URL path should not start with.\n\n# Example\n```rust\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that matches any path not starting with the prefix \"/admin\"\nlet mock = server.mock(|when, then| {\n when.path_prefix_not(\"/admin\");\n then.status(200); // Respond with status 200 for paths excluding \"/admin\"\n});\n\n// Make a request to a path that does not start with \"/admin\"\nlet response = reqwest::blocking::get(server.url(\"/public/home\")).unwrap();\n\n// Ensure the request was successful\nassert_eq!(response.status(), 200);\n\n// Verify that the mock server returned the expected response\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "path_suffix": "Specifies a suffix that the URL path must end with for the mock server to respond.\nThis is useful when the final segments of a path need to be validated, such as file extensions or specific patterns.\n\n# Parameters\n- `suffix`: A string or other value convertible to `String` representing the suffix that the URL path should end with.\n\n# Example\n```rust\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that matches any path ending with the suffix \".html\"\nlet mock = server.mock(|when, then| {\n when.path_suffix(\".html\");\n then.status(200); // Respond with a 200 status code for matched requests\n});\n\n// Make a request to a path ending with \".html\"\nlet response = reqwest::blocking::get(server.url(\"/about/index.html\")).unwrap();\n\n// Ensure the request was successful\nassert_eq!(response.status(), 200);\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for further configuration.\n\n", + "path_suffix_not": "Specifies a suffix that the URL path must not end with for the mock server to respond.\nThis constraint is useful for excluding paths with specific file extensions or patterns.\n\n# Parameters\n- `suffix`: A string or other value convertible to `String` representing the suffix that the URL path should not end with.\n\n# Example\n```rust\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that matches any path not ending with the suffix \".json\"\nlet mock = server.mock(|when, then| {\n when.path_suffix_not(\".json\");\n then.status(200); // Respond with a 200 status code for paths excluding \".json\"\n});\n\n// Make a request to a path that does not end with \".json\"\nlet response = reqwest::blocking::get(server.url(\"/about/index.html\")).unwrap();\n\n// Ensure the request was successful\nassert_eq!(response.status(), 200);\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for further configuration.\n\n", + "port": "Specifies the expected port number for incoming requests to match.\n\nThis constraint is especially useful when working with proxy or forwarding rules, but it\ncan also be used to serve mocks (e.g., when using a mock server as a proxy).\n\n# Parameters\n- `port`: A value convertible to `u16`, representing the expected port number.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Configure a mock to respond to requests made to `github.com`\n// with a specific port\nserver.mock(|when, then| {\n when.port(80); // Specify the expected port\n then.body(\"This is a mock response\");\n});\n\n// Set up an HTTP client to use the mock server as a proxy\nlet client = Client::builder()\n // Proxy all requests to the mock server\n .proxy(reqwest::Proxy::all(&server.base_url()).unwrap())\n .build()\n .unwrap();\n\n// Send a GET request to `github.com` on port 80.\n// The request will be sent to our mock server due to the HTTP client proxy settings.\nlet response = client.get(\"http://github.com:80\").send().unwrap();\n\n// Validate that the mock server returned the expected response\nassert_eq!(response.text().unwrap(), \"This is a mock response\");\n```\n\n# Errors\n- This function will panic if the port number cannot be converted to a valid `u16` value.\n\n# Returns\nThe updated `When` instance to allow method chaining.\n\n", + "port_not": "Specifies the port number that incoming requests must *not* match.\n\nThis constraint is especially useful when working with proxy or forwarding rules, but it\ncan also be used to serve mocks (e.g., when using a mock server as a proxy).\n\nTo add multiple excluded ports, invoke this function multiple times.\n\n# Parameters\n- `port`: A value convertible to `u16`, representing the port number to be excluded.\n\n# Example\n```rust\nuse httpmock::prelude::*;\nuse reqwest::blocking::Client;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Configure a mock to respond to requests not using port 81\nserver.mock(|when, then| {\n when.port_not(81); // Exclude requests on port 81\n then.body(\"This is a mock response\");\n});\n\n// Set up an HTTP client to use the mock server as a proxy\nlet client = Client::builder()\n .proxy(reqwest::Proxy::all(&server.base_url()).unwrap())\n .build()\n .unwrap();\n\n// Make a request to `github.com` on port 80, which will trigger\n// the mock response\nlet response = client.get(\"http://github.com:80\").send().unwrap();\n\n// Validate that the mock server returned the expected response\nassert_eq!(response.text().unwrap(), \"This is a mock response\");\n```\n\n# Errors\n- This function will panic if the port number cannot be converted to a valid `u16` value.\n\n# Returns\nThe updated `When` instance to enable method chaining.\n\n", + "query_param": "Specifies a required query parameter for the request.\nThis function ensures that the specified query parameter (key-value pair) must be included\nin the request URL for the mock server to respond.\n\n**Note**: The request query keys and values are implicitly *allowed but not required* to be URL-encoded.\nHowever, the value passed to this method should always be in plain text (i.e., not encoded).\n\n# Parameters\n- `name`: The name of the query parameter to match against.\n- `value`: The expected value of the query parameter.\n\n# Example\n```rust\n// Arrange\nuse reqwest::blocking::get;\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the query parameter `query` to have the value \"This is cool\"\nlet m = server.mock(|when, then| {\n when.query_param(\"query\", \"This is cool\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Act: Make a request that includes the specified query parameter and value\nget(&server.url(\"/search?query=This+is+cool\")).unwrap();\n\n// Assert: Verify that the mock was called at least once\nm.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n", + "query_param_count": "Specifies that the count of query parameters with keys and values matching specific regular\nexpression patterns must equal a specified number for the request to match.\nThis function ensures that the number of query parameters whose keys and values match the\ngiven regex patterns is equal to the specified count in the request URL for the mock\nserver to respond.\n\n# Parameters\n- `key_regex`: A regular expression pattern for the query parameter's key to match against.\n- `value_regex`: A regular expression pattern for the query parameter's value to match against.\n- `expected_count`: The expected number of query parameters whose keys and values match the regex patterns.\n\n# Example\n```rust\n// Arrange\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects exactly two query parameters with keys matching the regex \"user.*\"\n// and values matching the regex \"admin.*\"\nlet m = server.mock(|when, then| {\n when.query_param_count(r\"user.*\", r\"admin.*\", 2);\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Act: Make a request that matches the conditions\nreqwest::blocking::get(&server.url(\"/search?user1=admin1&user2=admin2\")).unwrap();\n\n// Assert: Verify that the mock was called at least once\nm.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "query_param_excludes": "Specifies that a query parameter's value (**not** the key) must not contain a specific substring for the request to match.\n\nThis function ensures that the specified query parameter (key) does exist in the request URL, and\nit does not have a value containing the given substring for the mock server to respond.\n\n**Note**: The request query key-value pairs are implicitly *allowed but not required* to be URL-encoded.\nHowever, provide the substring in plain text here (i.e., not encoded).\n\n# Parameters\n- `name`: The name of the query parameter to match against.\n- `substring`: The substring that must not appear within the value of the query parameter.\n\n# Example\n```rust\n// Arrange\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the query parameter `query`\n// to have a value that does not contain \"uncool\"\nlet m = server.mock(|when, then| {\n when.query_param_excludes(\"query\", \"uncool\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Act: Make a request that includes a value not containing the substring \"uncool\"\nreqwest::blocking::get(&server.url(\"/search?query=Something+cool\")).unwrap();\n\n// Assert: Verify that the mock was called at least once\nm.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "query_param_exists": "Specifies that a query parameter must be present in an HTTP request.\nThis function ensures that the specified query parameter key exists in the request URL\nfor the mock server to respond, regardless of the parameter's value.\n\n**Note**: The query key in the request is implicitly *allowed but not required* to be URL-encoded.\nHowever, provide the key in plain text here (i.e., not encoded).\n\n# Parameters\n- `name`: The name of the query parameter that must exist in the request.\n\n# Example\n```rust\n// Arrange\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the query parameter `query` to exist, regardless of its value\nlet m = server.mock(|when, then| {\n when.query_param_exists(\"query\");\n then.status(200); // Respond with a 200 status code if the parameter exists\n});\n\n// Act: Make a request with the specified query parameter\nreqwest::blocking::get(&server.url(\"/search?query=restaurants+near+me\")).unwrap();\n\n// Assert: Verify that the mock was called at least once\nm.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "query_param_includes": "Specifies that a query parameter's value (**not** the key) must contain a specific substring for the request to match.\nThis function ensures that the specified query parameter (key) does exist in the request URL, and\nit does have a value containing the given substring for the mock server to respond.\n\n**Note**: The request query key-value pairs are implicitly *allowed but not required* to be URL-encoded.\nHowever, provide the substring in plain text (i.e., not encoded).\n\n# Parameters\n- `name`: The name of the query parameter to match against.\n- `substring`: The substring that must appear within the value of the query parameter.\n\n# Example\n```rust\n// Arrange\nuse reqwest::blocking::get;\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the query parameter `query`\n// to have a value containing \"cool\"\nlet m = server.mock(|when, then| {\n when.query_param_includes(\"query\", \"cool\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Act: Make a request that includes a value containing the substring \"cool\"\nget(server.url(\"/search?query=Something+cool\")).unwrap();\n\n// Assert: Verify that the mock was called at least once\nm.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "query_param_matches": "Specifies that a query parameter must match a specific regular expression pattern for the key and another pattern for the value.\nThis function ensures that the specified query parameter key-value pair matches the given patterns\nin the request URL for the mock server to respond.\n\n# Parameters\n- `key_regex`: A regular expression pattern for the query parameter's key to match against.\n- `value_regex`: A regular expression pattern for the query parameter's value to match against.\n\n# Example\n```rust\n// Arrange\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the query parameter key to match the regex \"user.*\"\n// and the value to match the regex \"admin.*\"\nlet m = server.mock(|when, then| {\n when.query_param_matches(r\"user.*\", r\"admin.*\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Act: Make a request that matches the regex patterns for both key and value\nreqwest::blocking::get(&server.url(\"/search?user=admin_user\")).unwrap();\n\n// Assert: Verify that the mock was called at least once\nm.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "query_param_missing": "Specifies that a query parameter must *not* be present in an HTTP request.\nThis function ensures that the specified query parameter key is absent in the request URL\nfor the mock server to respond, regardless of the parameter's value.\n\n**Note**: The request query key is implicitly *allowed but not required* to be URL-encoded.\nHowever, provide the key in plain text (i.e., not encoded).\n\n# Parameters\n- `name`: The name of the query parameter that should be missing from the request.\n\n# Example\n```rust\n// Arrange\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the query parameter `query` to be missing\nlet m = server.mock(|when, then| {\n when.query_param_missing(\"query\");\n then.status(200); // Respond with a 200 status code if the parameter is absent\n});\n\n// Act: Make a request without the specified query parameter\nreqwest::blocking::get(&server.url(\"/search\")).unwrap();\n\n// Assert: Verify that the mock was called at least once\nm.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "query_param_not": "This function ensures that the specified query parameter (key) does exist in the request URL,\nand its value is not equal to the specified value.\n\n**Note**: Query keys and values are implicitly *allowed but not required* to be URL-encoded\nin the HTTP request. However, values passed to this method should always be in plain text\n(i.e., not encoded).\n\n# Parameters\n- `name`: The name of the query parameter to ensure is not present.\n- `value`: The value of the query parameter to ensure is not present.\n\n# Example\n```rust\n// Arrange\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the query parameter `query` to NOT have the value \"This is cool\"\nlet m = server.mock(|when, then| {\n when.query_param_not(\"query\", \"This is cool\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Act: Make a request that does not include the specified query parameter and value\nlet response = reqwest::blocking::get(&server.url(\"/search?query=awesome\")).unwrap();\n\n// Assert: Verify that the mock was called\nassert_eq!(response.status(), 200);\nm.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n", + "query_param_prefix": "Specifies that a query parameter's value (**not** the key) must start with a specific prefix for the request to match.\nThis function ensures that the specified query parameter (key) has a value starting with the given prefix\nin the request URL for the mock server to respond.\n\n**Note**: The request query key-value pairs are implicitly *allowed but not required* to be URL-encoded.\nProvide the prefix in plain text here (i.e., not encoded).\n\n# Parameters\n- `name`: The name of the query parameter to match against.\n- `prefix`: The prefix that the query parameter value should start with.\n\n# Example\n```rust\n// Arrange\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the query parameter `query`\n// to have a value starting with \"cool\"\nlet m = server.mock(|when, then| {\n when.query_param_prefix(\"query\", \"cool\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Act: Make a request that includes a value starting with the prefix \"cool\"\nreqwest::blocking::get(&server.url(\"/search?query=cool+stuff\")).unwrap();\n\n// Assert: Verify that the mock was called at least once\nm.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "query_param_prefix_not": "Specifies that a query parameter's value (**not** the key) must not start with a specific prefix for the request to match.\nThis function ensures that the specified query parameter (key) has a value not starting with the given prefix\nin the request URL for the mock server to respond.\n\n**Note**: The request query key-value pairs are implicitly *allowed but not required* to be URL-encoded.\nProvide the prefix in plain text here (i.e., not encoded).\n\n# Parameters\n- `name`: The name of the query parameter to match against.\n- `prefix`: The prefix that the query parameter value should not start with.\n\n# Example\n```rust\n// Arrange\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the query parameter `query`\n// to have a value not starting with \"cool\"\nlet m = server.mock(|when, then| {\n when.query_param_prefix_not(\"query\", \"cool\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Act: Make a request that does not start with the prefix \"cool\"\nreqwest::blocking::get(&server.url(\"/search?query=warm_stuff\")).unwrap();\n\n// Assert: Verify that the mock was called at least once\nm.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "query_param_suffix": "Specifies that a query parameter's value (**not** the key) must end with a specific suffix for the request to match.\nThis function ensures that the specified query parameter (key) has a value ending with the given suffix\nin the request URL for the mock server to respond.\n\n**Note**: The request query key-value pairs are implicitly *allowed but not required* to be URL-encoded.\nProvide the suffix in plain text here (i.e., not encoded).\n\n# Parameters\n- `name`: The name of the query parameter to match against.\n- `suffix`: The suffix that the query parameter value should end with.\n\n# Example\n```rust\n// Arrange\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the query parameter `query`\n// to have a value ending with \"cool\"\nlet m = server.mock(|when, then| {\n when.query_param_suffix(\"query\", \"cool\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Act: Make a request that includes a value ending with the suffix \"cool\"\nreqwest::blocking::get(&server.url(\"/search?query=really_cool\")).unwrap();\n\n// Assert: Verify that the mock was called at least once\nm.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "query_param_suffix_not": "Specifies that a query parameter's value (**not** the key) must not end with a specific suffix for the request to match.\nThis function ensures that the specified query parameter (key) has a value not ending with the given suffix\nin the request URL for the mock server to respond.\n\n**Note**: The request query key-value pairs are implicitly *allowed but not required* to be URL-encoded.\nProvide the suffix in plain text here (i.e., not encoded).\n\n# Parameters\n- `name`: The name of the query parameter to match against.\n- `suffix`: The suffix that the query parameter value should not end with.\n\n# Example\n```rust\n// Arrange\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that expects the query parameter `query`\n// to have a value not ending with \"cool\"\nlet m = server.mock(|when, then| {\n when.query_param_suffix_not(\"query\", \"cool\");\n then.status(200); // Respond with a 200 status code if the condition is met\n});\n\n// Act: Make a request that doesn't end with the suffix \"cool\"\nreqwest::blocking::get(&server.url(\"/search?query=uncool_stuff\")).unwrap();\n\n// Assert: Verify that the mock was called at least once\nm.assert();\n```\n\n# Returns\nThe updated `When` instance to allow method chaining for additional configuration.\n\n", + "scheme": "Specifies the scheme (e.g., \"http\" or \"https\") that requests must match for the mock server to respond.\n\nThis method sets the scheme to filter requests and ensures that the mock server only matches\nrequests with the specified scheme. This allows for more precise testing in environments where\nmultiple protocols are used.\n\n**Note**: Scheme matching is case-insensitive, conforming to\n[RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2).\n\n# Example\n\n```rust\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that only matches requests with the \"http\" scheme\nlet mock = server.mock(|when, then| {\n when.scheme(\"http\"); // Restrict to the \"http\" scheme\n then.status(200); // Respond with status code 200 for all matched requests\n});\n\n// Make an \"http\" request to the server's URL to trigger the mock\nlet response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n// Ensure the request was successful\nassert_eq!(response.status(), 200);\n\n// Verify that the mock was called at least once\nmock.assert();\n```\n\n# Parameters\n- `scheme`: A string specifying the scheme that requests should match. Common values include \"http\" and \"https\".\n\n# Returns\nThe modified `When` instance to allow for method chaining.\n\n", + "scheme_not": "Specifies a scheme (e.g., \"https\") that requests must not match for the mock server to respond.\n\nThis method allows you to exclude specific schemes from matching, ensuring that the mock server\nwon't respond to requests using those protocols. This is useful when you want to mock server\nbehavior based on protocol security requirements or other criteria.\n\n**Note**: Scheme matching is case-insensitive, conforming to\n[RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2).\n\n# Example\n\n```rust\nuse httpmock::prelude::*;\n\n// Start a new mock server\nlet server = MockServer::start();\n\n// Create a mock that will only match requests that do not use the \"https\" scheme\nlet mock = server.mock(|when, then| {\n when.scheme_not(\"https\"); // Exclude the \"https\" scheme from matching\n then.status(200); // Respond with status code 200 for all matched requests\n});\n\n// Make a request to the server's URL with the \"http\" scheme to trigger the mock\nlet response = reqwest::blocking::get(server.url(\"/test\")).unwrap();\n\n// Ensure the request was successful\nassert_eq!(response.status(), 200);\n\n// Ensure that the mock was called at least once\nmock.assert();\n```\n\n# Parameters\n- `scheme`: A string specifying the scheme that requests should not match. Common values include \"http\" and \"https\".\n\n# Returns\nThe modified `When` instance to allow for method chaining.\n\n" + } +} \ No newline at end of file diff --git a/docs/website/generated/example_tests.json b/docs/website/generated/example_tests.json new file mode 100644 index 00000000..9d7ce7fe --- /dev/null +++ b/docs/website/generated/example_tests.json @@ -0,0 +1,7 @@ +{ + "record-forwarding-github": "#[cfg(all(feature = \"proxy\", feature = \"record\"))]\n#[test]\nfn record_github_api_with_forwarding_test() {\n // Let's create our mock server for the test\n let server = MockServer::start();\n\n // We configure our server to forward the request to the target\n // host instead of answering with a mocked response. The 'when'\n // variable lets you configure rules under which forwarding\n // should take place.\n server.forward_to(\"https://api.github.com\", |rule| {\n rule.filter(|when| {\n when.any_request(); // Ensure all requests are forwarded.\n });\n });\n\n let recording = server.record(|rule| {\n rule\n // Specify which headers to record.\n // Only the headers listed here will be captured and stored\n // as part of the recorded mock. This selective recording is\n // necessary because some headers may vary between requests\n // and could cause issues when replaying the mock later.\n // For instance, headers like 'Authorization' or 'Date' may\n // change with each request.\n .record_request_header(\"User-Agent\")\n .filter(|when| {\n when.any_request(); // Ensure all requests are recorded.\n });\n });\n\n // Now let's send an HTTP request to the mock server. The request\n // will be forwarded to the GitHub API, as we configured before.\n let client = Client::new();\n\n let response = client\n .get(server.url(\"/repos/torvalds/linux\"))\n // GitHub requires us to send a user agent header\n .header(\"User-Agent\", \"httpmock-test\")\n .send()\n .unwrap();\n\n // Since the request was forwarded, we should see a GitHub API response.\n assert_eq!(response.status().as_u16(), 200);\n assert_eq!(true, response.text().unwrap().contains(\"\\\"private\\\":false\"));\n\n // Save the recording to\n // \"target/httpmock/recordings/github-torvalds-scenario_.yaml\".\n recording\n .save(\"github-torvalds-scenario\")\n .expect(\"cannot store scenario on disk\");\n}", + "record-proxy-github": "#[cfg(all(feature = \"proxy\", feature = \"record\", feature = \"experimental\"))]\n#[test]\nfn record_with_proxy_test() {\n // Start a mock server to act as a proxy for the HTTP client\n let server = MockServer::start();\n\n // Configure the mock server to proxy all incoming requests\n server.proxy(|rule| {\n rule.filter(|when| {\n when.any_request(); // Intercept all requests\n });\n });\n\n // Set up recording on the mock server to capture all proxied\n // requests and responses\n let recording = server.record(|rule| {\n rule.filter(|when| {\n when.any_request(); // Record all requests\n });\n });\n\n // Create an HTTP client configured to route requests\n // through the mock proxy server\n let github_client = Client::builder()\n // Set the proxy URL to the mock server's URL\n .proxy(reqwest::Proxy::all(server.base_url()).unwrap())\n .build()\n .unwrap();\n\n // Send a GET request using the client, which will be proxied by the mock server\n let response = github_client.get(server.base_url()).send().unwrap();\n\n // Verify that the response matches the expected mock response\n assert_eq!(response.text().unwrap(), \"This is a mock response\");\n\n // Save the recorded HTTP interactions to a file for future reference or testing\n recording\n .save(\"my_scenario_name\")\n .expect(\"could not save the recording\");\n}", + "forwarding-github": "#[cfg(feature = \"proxy\")]\n#[test]\nfn forward_to_github_test() {\n // Let's create our mock server for the test\n let server = MockServer::start();\n\n // We configure our server to forward the request to the target\n // host instead of answering with a mocked response. The 'when'\n // variable lets you configure rules under which forwarding\n // should take place.\n server.forward_to(\"https://api.github.com\", |rule| {\n rule.filter(|when| {\n when.any_request(); // Ensure all requests are forwarded.\n });\n });\n\n // Now let's send an HTTP request to the mock server. The request\n // will be forwarded to the GitHub API, as we configured before.\n let client = Client::new();\n\n let response = client\n .get(server.url(\"/repos/torvalds/linux\"))\n // GitHub requires us to send a user agent header\n .header(\"User-Agent\", \"httpmock-test\")\n .send()\n .unwrap();\n\n // Since the request was forwarded, we should see a GitHub API response.\n assert_eq!(response.status().as_u16(), 200);\n assert_eq!(true, response.text().unwrap().contains(\"\\\"private\\\":false\"));\n}", + "forwarding": "#[cfg(feature = \"proxy\")]\n#[test]\nfn forwarding_test() {\n // We will create this mock server to simulate a real service (e.g., GitHub, AWS, etc.).\n let target_server = MockServer::start();\n target_server.mock(|when, then| {\n when.any_request();\n then.status(200).body(\"Hi from fake GitHub!\");\n });\n\n // Let's create our mock server for the test\n let server = MockServer::start();\n\n // We configure our server to forward the request to the target host instead of\n // answering with a mocked response. The 'when' variable lets you configure\n // rules under which forwarding should take place.\n server.forward_to(target_server.base_url(), |rule| {\n rule.filter(|when| {\n when.any_request(); // We want all requests to be forwarded.\n });\n });\n\n // Now let's send an HTTP request to the mock server. The request will be forwarded\n // to the target host, as we configured before.\n let client = Client::new();\n\n // Since the request was forwarded, we should see the target host's response.\n let response = client.get(server.url(\"/get\")).send().unwrap();\n assert_eq!(response.status().as_u16(), 200);\n assert_eq!(response.text().unwrap(), \"Hi from fake GitHub!\");\n}", + "playback-forwarding-github": "#[cfg(all(feature = \"proxy\", feature = \"record\"))]\n#[test]\nfn playback_github_api() {\n // Start a mock server for the test\n let server = MockServer::start();\n\n // Configure the mock server to forward requests to the target\n // host (GitHub API) instead of responding with a mock. The 'rule'\n // parameter allows you to define conditions under which forwarding\n // should occur.\n server.forward_to(\"https://api.github.com\", |rule| {\n rule.filter(|when| {\n when.any_request(); // Forward all requests.\n });\n });\n\n // Set up recording to capture all forwarded requests and responses\n let recording = server.record(|rule| {\n rule.filter(|when| {\n when.any_request(); // Record all requests and responses.\n });\n });\n\n // Send an HTTP request to the mock server, which will be forwarded\n // to the GitHub API\n let client = Client::new();\n let response = client\n .get(server.url(\"/repos/torvalds/linux\"))\n // GitHub requires a User-Agent header\n .header(\"User-Agent\", \"httpmock-test\")\n .send()\n .unwrap();\n\n // Assert that the response from the forwarded request is as expected\n assert_eq!(response.status().as_u16(), 200);\n assert!(response.text().unwrap().contains(\"\\\"private\\\":false\"));\n\n // Save the recorded interactions to a file\n let target_path = recording\n .save(\"github-torvalds-scenario\")\n .expect(\"Failed to save the recording to disk\");\n\n // Start a new mock server instance for playback\n let playback_server = MockServer::start();\n\n // Load the recorded interactions into the new mock server\n playback_server.playback(target_path);\n\n // Send a request to the playback server and verify the response\n // matches the recorded data\n let response = client\n .get(playback_server.url(\"/repos/torvalds/linux\"))\n .send()\n .unwrap();\n assert_eq!(response.status().as_u16(), 200);\n assert!(response.text().unwrap().contains(\"\\\"private\\\":false\"));\n}" +} \ No newline at end of file diff --git a/docs/website/generated/groups.json b/docs/website/generated/groups.json new file mode 100644 index 00000000..cb78fd8c --- /dev/null +++ b/docs/website/generated/groups.json @@ -0,0 +1,398 @@ +{ + "then": [ + { + "group": "Status", + "method": "status" + }, + { + "group": "Body", + "method": "body" + }, + { + "group": "Body", + "method": "body_from_file" + }, + { + "group": "Body", + "method": "json_body" + }, + { + "group": "Body", + "method": "json_body_obj" + }, + { + "group": "Headers", + "method": "header" + }, + { + "group": "Network", + "method": "delay" + }, + { + "group": "Miscellaneous", + "method": "and" + } + ], + "when": [ + { + "group": "Miscellaneous", + "method": "any_request" + }, + { + "group": "Scheme", + "method": "scheme" + }, + { + "group": "Scheme", + "method": "scheme_not" + }, + { + "group": "Method", + "method": "method" + }, + { + "group": "Method", + "method": "method_not" + }, + { + "group": "Host", + "method": "host" + }, + { + "group": "Host", + "method": "host_not" + }, + { + "group": "Host", + "method": "host_includes" + }, + { + "group": "Host", + "method": "host_excludes" + }, + { + "group": "Host", + "method": "host_prefix" + }, + { + "group": "Host", + "method": "host_suffix" + }, + { + "group": "Host", + "method": "host_prefix_not" + }, + { + "group": "Host", + "method": "host_suffix_not" + }, + { + "group": "Host", + "method": "host_matches" + }, + { + "group": "Port", + "method": "port" + }, + { + "group": "Port", + "method": "port_not" + }, + { + "group": "Path", + "method": "path" + }, + { + "group": "Path", + "method": "path_not" + }, + { + "group": "Path", + "method": "path_includes" + }, + { + "group": "Path", + "method": "path_excludes" + }, + { + "group": "Path", + "method": "path_prefix" + }, + { + "group": "Path", + "method": "path_suffix" + }, + { + "group": "Path", + "method": "path_prefix_not" + }, + { + "group": "Path", + "method": "path_suffix_not" + }, + { + "group": "Path", + "method": "path_matches" + }, + { + "group": "Query Parameters", + "method": "query_param" + }, + { + "group": "Query Parameters", + "method": "query_param_not" + }, + { + "group": "Query Parameters", + "method": "query_param_exists" + }, + { + "group": "Query Parameters", + "method": "query_param_missing" + }, + { + "group": "Query Parameters", + "method": "query_param_includes" + }, + { + "group": "Query Parameters", + "method": "query_param_excludes" + }, + { + "group": "Query Parameters", + "method": "query_param_prefix" + }, + { + "group": "Query Parameters", + "method": "query_param_suffix" + }, + { + "group": "Query Parameters", + "method": "query_param_prefix_not" + }, + { + "group": "Query Parameters", + "method": "query_param_suffix_not" + }, + { + "group": "Query Parameters", + "method": "query_param_matches" + }, + { + "group": "Query Parameters", + "method": "query_param_count" + }, + { + "group": "Headers", + "method": "header" + }, + { + "group": "Headers", + "method": "header_not" + }, + { + "group": "Headers", + "method": "header_exists" + }, + { + "group": "Headers", + "method": "header_missing" + }, + { + "group": "Headers", + "method": "header_includes" + }, + { + "group": "Headers", + "method": "header_excludes" + }, + { + "group": "Headers", + "method": "header_prefix" + }, + { + "group": "Headers", + "method": "header_suffix" + }, + { + "group": "Headers", + "method": "header_prefix_not" + }, + { + "group": "Headers", + "method": "header_suffix_not" + }, + { + "group": "Headers", + "method": "header_matches" + }, + { + "group": "Headers", + "method": "header_count" + }, + { + "group": "Cookies", + "method": "cookie" + }, + { + "group": "Cookies", + "method": "cookie_not" + }, + { + "group": "Cookies", + "method": "cookie_exists" + }, + { + "group": "Cookies", + "method": "cookie_missing" + }, + { + "group": "Cookies", + "method": "cookie_includes" + }, + { + "group": "Cookies", + "method": "cookie_excludes" + }, + { + "group": "Cookies", + "method": "cookie_prefix" + }, + { + "group": "Cookies", + "method": "cookie_suffix" + }, + { + "group": "Cookies", + "method": "cookie_prefix_not" + }, + { + "group": "Cookies", + "method": "cookie_suffix_not" + }, + { + "group": "Cookies", + "method": "cookie_matches" + }, + { + "group": "Cookies", + "method": "cookie_count" + }, + { + "group": "Body", + "method": "body" + }, + { + "group": "Body", + "method": "body_not" + }, + { + "group": "Body", + "method": "body_includes" + }, + { + "group": "Body", + "method": "body_excludes" + }, + { + "group": "Body", + "method": "body_prefix" + }, + { + "group": "Body", + "method": "body_suffix" + }, + { + "group": "Body", + "method": "body_prefix_not" + }, + { + "group": "Body", + "method": "body_suffix_not" + }, + { + "group": "Body", + "method": "body_matches" + }, + { + "group": "Body", + "method": "json_body" + }, + { + "group": "Body", + "method": "json_body_obj" + }, + { + "group": "Body", + "method": "json_body_includes" + }, + { + "group": "Body", + "method": "json_body_excludes" + }, + { + "group": "Body", + "method": "form_urlencoded_tuple" + }, + { + "group": "Body", + "method": "form_urlencoded_tuple_not" + }, + { + "group": "Body", + "method": "form_urlencoded_tuple_exists" + }, + { + "group": "Body", + "method": "form_urlencoded_tuple_missing" + }, + { + "group": "Body", + "method": "form_urlencoded_tuple_includes" + }, + { + "group": "Body", + "method": "form_urlencoded_tuple_excludes" + }, + { + "group": "Body", + "method": "form_urlencoded_tuple_prefix" + }, + { + "group": "Body", + "method": "form_urlencoded_tuple_prefix_not" + }, + { + "group": "Body", + "method": "form_urlencoded_tuple_suffix" + }, + { + "group": "Body", + "method": "form_urlencoded_tuple_suffix_not" + }, + { + "group": "Body", + "method": "form_urlencoded_tuple_matches" + }, + { + "group": "Body", + "method": "form_urlencoded_tuple_count" + }, + { + "group": "Custom", + "method": "matches" + }, + { + "group": "Custom", + "method": "is_true" + }, + { + "group": "Custom", + "method": "is_false" + }, + { + "group": "Miscellaneous", + "method": "and" + } + ] +} \ No newline at end of file diff --git a/docs/website/package-lock.json b/docs/website/package-lock.json new file mode 100644 index 00000000..c8dd99b9 --- /dev/null +++ b/docs/website/package-lock.json @@ -0,0 +1,8807 @@ +{ + "name": "tutorial", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tutorial", + "version": "0.0.1", + "dependencies": { + "@astrojs/check": "^0.5.10", + "@astrojs/starlight": "^0.21.5", + "astro": "^4.3.5", + "sharp": "^0.32.5", + "typescript": "^5.4.4" + }, + "devDependencies": { + "handlebars": "^4.7.8" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@astrojs/check": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.5.10.tgz", + "integrity": "sha512-vliHXM9cu/viGeKiksUM4mXfO816ohWtawTl2ADPgTsd4nUMjFiyAl7xFZhF34yy4hq4qf7jvK1F2PlR3b5I5w==", + "dependencies": { + "@astrojs/language-server": "^2.8.4", + "chokidar": "^3.5.3", + "fast-glob": "^3.3.1", + "kleur": "^4.1.5", + "yargs": "^17.7.2" + }, + "bin": { + "astro-check": "dist/bin.js" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } + }, + "node_modules/@astrojs/compiler": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.7.1.tgz", + "integrity": "sha512-/POejAYuj8WEw7ZI0J8JBvevjfp9jQ9Wmu/Bg52RiNwGXkMV7JnYpsenVfHvvf1G7R5sXHGKlTcxlQWhoUTiGQ==" + }, + "node_modules/@astrojs/internal-helpers": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.4.0.tgz", + "integrity": "sha512-6B13lz5n6BrbTqCTwhXjJXuR1sqiX/H6rTxzlXx+lN1NnV4jgnq/KJldCQaUWJzPL5SiWahQyinxAbxQtwgPHA==" + }, + "node_modules/@astrojs/language-server": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-2.8.4.tgz", + "integrity": "sha512-sJH5vGTBkhgA8+hdhzX78UUp4cFz4Mt7xkEkevD188OS5bDMkaue6hK+dtXWM47mnrXFveXA2u38K7S+5+IRjA==", + "dependencies": { + "@astrojs/compiler": "^2.7.0", + "@jridgewell/sourcemap-codec": "^1.4.15", + "@volar/kit": "~2.1.5", + "@volar/language-core": "~2.1.5", + "@volar/language-server": "~2.1.5", + "@volar/language-service": "~2.1.5", + "@volar/typescript": "~2.1.5", + "fast-glob": "^3.2.12", + "volar-service-css": "0.0.34", + "volar-service-emmet": "0.0.34", + "volar-service-html": "0.0.34", + "volar-service-prettier": "0.0.34", + "volar-service-typescript": "0.0.34", + "volar-service-typescript-twoslash-queries": "0.0.34", + "vscode-html-languageservice": "^5.1.2", + "vscode-uri": "^3.0.8" + }, + "bin": { + "astro-ls": "bin/nodeServer.js" + }, + "peerDependencies": { + "prettier": "^3.0.0", + "prettier-plugin-astro": ">=0.11.0" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + } + } + }, + "node_modules/@astrojs/markdown-remark": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-5.0.0.tgz", + "integrity": "sha512-QBXbxXZamVRoqCNN2gjDXa7qYPUkJZq7KYFfg3DX7rze3QL6xiz4N+Wg202dNPRaIkQa16BV6D8+EHibQFubRg==", + "dependencies": { + "@astrojs/prism": "^3.0.0", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.0", + "hast-util-to-text": "^4.0.0", + "import-meta-resolve": "^4.0.0", + "mdast-util-definitions": "^6.0.0", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.0", + "remark-gfm": "^4.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "remark-smartypants": "^2.0.0", + "shiki": "^1.1.2", + "unified": "^11.0.4", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.0", + "vfile": "^6.0.1" + } + }, + "node_modules/@astrojs/mdx": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-2.2.4.tgz", + "integrity": "sha512-eXCmftMsWj4vTECrc4vgdaXrA8xIvrmJ9rM37BZNK5anQ2PunUm9N8wbnK2VVTE0CAmW5U8v9y3IGps2RYGUvQ==", + "dependencies": { + "@astrojs/markdown-remark": "5.0.0", + "@mdx-js/mdx": "^3.0.0", + "acorn": "^8.11.2", + "es-module-lexer": "^1.4.1", + "estree-util-visit": "^2.0.0", + "github-slugger": "^2.0.0", + "gray-matter": "^4.0.3", + "hast-util-to-html": "^9.0.0", + "kleur": "^4.1.4", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.0", + "remark-smartypants": "^2.0.0", + "source-map": "^0.7.4", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.1" + }, + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "astro": "^4.0.0" + } + }, + "node_modules/@astrojs/prism": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-3.0.0.tgz", + "integrity": "sha512-g61lZupWq1bYbcBnYZqdjndShr/J3l/oFobBKPA3+qMat146zce3nz2kdO4giGbhYDt4gYdhmoBz0vZJ4sIurQ==", + "dependencies": { + "prismjs": "^1.29.0" + }, + "engines": { + "node": ">=18.14.1" + } + }, + "node_modules/@astrojs/sitemap": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.1.2.tgz", + "integrity": "sha512-FxOJldIl5ltZ5CNjocQxHkAO9orwHBjqtaU28o4smobp9vowS0nbGp+I9CrPxkzWdl1crSDm9vjL9tnvG1DSug==", + "dependencies": { + "sitemap": "^7.1.1", + "zod": "^3.22.4" + } + }, + "node_modules/@astrojs/starlight": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.21.5.tgz", + "integrity": "sha512-cvftxu7DM4C25KGSxqyIk81DiQGX0zx9s5sfmprd1kKQK1h/MQXaRVDCpJrK4SjrgWtpG1UoKLJZBgD5w4k9kw==", + "dependencies": { + "@astrojs/mdx": "^2.1.1", + "@astrojs/sitemap": "^3.0.5", + "@pagefind/default-ui": "^1.0.3", + "@types/hast": "^3.0.3", + "@types/mdast": "^4.0.3", + "astro-expressive-code": "^0.33.4", + "bcp-47": "^2.1.0", + "hast-util-from-html": "^2.0.1", + "hast-util-select": "^6.0.2", + "hast-util-to-string": "^3.0.0", + "hastscript": "^8.0.0", + "mdast-util-directive": "^3.0.0", + "mdast-util-to-markdown": "^2.1.0", + "pagefind": "^1.0.3", + "rehype": "^13.0.1", + "remark-directive": "^3.0.0", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.1" + }, + "peerDependencies": { + "astro": "^4.2.7" + } + }, + "node_modules/@astrojs/telemetry": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.0.4.tgz", + "integrity": "sha512-A+0c7k/Xy293xx6odsYZuXiaHO0PL+bnDoXOc47sGDF5ffIKdKQGRPFl2NMlCF4L0NqN4Ynbgnaip+pPF0s7pQ==", + "dependencies": { + "ci-info": "^3.8.0", + "debug": "^4.3.4", + "dlv": "^1.1.3", + "dset": "^3.1.2", + "is-docker": "^3.0.0", + "is-wsl": "^3.0.0", + "which-pm-runs": "^1.1.0" + }, + "engines": { + "node": ">=18.14.1" + } + }, + "node_modules/@astrojs/telemetry/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "dependencies": { + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", + "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", + "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.4", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.24.4", + "@babel/parser": "^7.24.4", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.1", + "@babel/types": "^7.24.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz", + "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==", + "dependencies": { + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "dependencies": { + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.4.tgz", + "integrity": "sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==", + "dependencies": { + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.1", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", + "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz", + "integrity": "sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz", + "integrity": "sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.23.3", + "@babel/types": "^7.23.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", + "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", + "dependencies": { + "@babel/code-frame": "^7.24.1", + "@babel/generator": "^7.24.1", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.24.1", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@emmetio/abbreviation": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@emmetio/abbreviation/-/abbreviation-2.3.3.tgz", + "integrity": "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==", + "dependencies": { + "@emmetio/scanner": "^1.0.4" + } + }, + "node_modules/@emmetio/css-abbreviation": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@emmetio/css-abbreviation/-/css-abbreviation-2.1.8.tgz", + "integrity": "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==", + "dependencies": { + "@emmetio/scanner": "^1.0.4" + } + }, + "node_modules/@emmetio/scanner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emmetio/scanner/-/scanner-1.0.4.tgz", + "integrity": "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@expressive-code/core": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.33.5.tgz", + "integrity": "sha512-KL0EkKAvd7SSIQL3ZIP19xqe4xNjBaQYNvcJC6RmoBUnQpvxaJNFwRxCBEF/X0ftJEMaSG7WTrabZ9c/zFeqmA==", + "dependencies": { + "@ctrl/tinycolor": "^3.6.0", + "hast-util-to-html": "^8.0.4", + "hastscript": "^7.2.0", + "postcss": "^8.4.21", + "postcss-nested": "^6.0.1" + } + }, + "node_modules/@expressive-code/core/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@expressive-code/core/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" + }, + "node_modules/@expressive-code/core/node_modules/hast-util-from-parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz", + "integrity": "sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "hastscript": "^7.0.0", + "property-information": "^6.0.0", + "vfile": "^5.0.0", + "vfile-location": "^4.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@expressive-code/core/node_modules/hast-util-parse-selector": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz", + "integrity": "sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==", + "dependencies": { + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@expressive-code/core/node_modules/hast-util-raw": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-7.2.3.tgz", + "integrity": "sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "hast-util-from-parse5": "^7.0.0", + "hast-util-to-parse5": "^7.0.0", + "html-void-elements": "^2.0.0", + "parse5": "^6.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@expressive-code/core/node_modules/hast-util-to-html": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.4.tgz", + "integrity": "sha512-4tpQTUOr9BMjtYyNlt0P50mH7xj0Ks2xpo8M943Vykljf99HW6EzulIoJP1N3eKOSScEHzyzi9dm7/cn0RfGwA==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-raw": "^7.0.0", + "hast-util-whitespace": "^2.0.0", + "html-void-elements": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@expressive-code/core/node_modules/hast-util-to-parse5": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz", + "integrity": "sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@expressive-code/core/node_modules/hast-util-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", + "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@expressive-code/core/node_modules/hastscript": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz", + "integrity": "sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@expressive-code/core/node_modules/html-void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", + "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/@expressive-code/core/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + }, + "node_modules/@expressive-code/core/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@expressive-code/core/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@expressive-code/core/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@expressive-code/core/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@expressive-code/core/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@expressive-code/core/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@expressive-code/core/node_modules/vfile-location": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.1.0.tgz", + "integrity": "sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==", + "dependencies": { + "@types/unist": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@expressive-code/core/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@expressive-code/plugin-frames": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.33.5.tgz", + "integrity": "sha512-lFt/gbnZscBSxHovg4XiWohp5nrxk4McS6RGABdj6+0gJcX8/YrFTM23GKBIkaDePxdDidVY0jQYGYDL/RrQHw==", + "dependencies": { + "@expressive-code/core": "^0.33.5", + "hastscript": "^7.2.0" + } + }, + "node_modules/@expressive-code/plugin-frames/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@expressive-code/plugin-frames/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" + }, + "node_modules/@expressive-code/plugin-frames/node_modules/hast-util-parse-selector": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz", + "integrity": "sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==", + "dependencies": { + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@expressive-code/plugin-frames/node_modules/hastscript": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz", + "integrity": "sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@expressive-code/plugin-shiki": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.33.5.tgz", + "integrity": "sha512-LWgttQTUrIPE1X+Lya1qFWiX47tH2AS2hkbj/cZoWkdiSjn6zUvtTypK/2Xn6Rgn6z6ClzpgHvkXRqFn7nAB4A==", + "dependencies": { + "@expressive-code/core": "^0.33.5", + "shiki": "^1.1.7" + } + }, + "node_modules/@expressive-code/plugin-text-markers": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.33.5.tgz", + "integrity": "sha512-JxSHL1MGrJAPNaUMjFXex3K+9NJDbfew9H6PmX8LQ+fm9VNQdtBYTAz/x7nqOk7bkTrtAZK5RfDqUfb8U5M+2A==", + "dependencies": { + "@expressive-code/core": "^0.33.5", + "hastscript": "^7.2.0", + "unist-util-visit-parents": "^5.1.3" + } + }, + "node_modules/@expressive-code/plugin-text-markers/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@expressive-code/plugin-text-markers/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" + }, + "node_modules/@expressive-code/plugin-text-markers/node_modules/hast-util-parse-selector": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz", + "integrity": "sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==", + "dependencies": { + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@expressive-code/plugin-text-markers/node_modules/hastscript": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz", + "integrity": "sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@expressive-code/plugin-text-markers/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@expressive-code/plugin-text-markers/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mdx-js/mdx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.0.1.tgz", + "integrity": "sha512-eIQ4QTrOWyL3LWEe/bu6Taqzq2HQvHcyTMaOrI95P2/LmJE7AsfPfgJGuFLPVqBUE1BC1rik3VIhU+s9u72arA==", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-to-js": "^2.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-estree": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "periscopic": "^3.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pagefind/darwin-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.1.0.tgz", + "integrity": "sha512-SLsXNLtSilGZjvqis8sX42fBWsWAVkcDh1oerxwqbac84HbiwxpxOC2jm8hRwcR0Z55HPZPWO77XeRix/8GwTg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pagefind/darwin-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-x64/-/darwin-x64-1.1.0.tgz", + "integrity": "sha512-QjQSE/L5oS1C8N8GdljGaWtjCBMgMtfrPAoiCmINTu9Y9dp0ggAyXvF8K7Qg3VyIMYJ6v8vg2PN7Z3b+AaAqUA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pagefind/default-ui": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pagefind/default-ui/-/default-ui-1.1.0.tgz", + "integrity": "sha512-+XiAJAK++C64nQcD7s3Prdmd5S92lT05fwjOxm0L1jj80jbL+tmvcqkkFnPpoqhnicIPgcAX/Y5W0HRZnBt35w==" + }, + "node_modules/@pagefind/linux-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pagefind/linux-arm64/-/linux-arm64-1.1.0.tgz", + "integrity": "sha512-8zjYCa2BtNEL7KnXtysPtBELCyv5DSQ4yHeK/nsEq6w4ToAMTBl0K06khqxdSGgjMSwwrxvLzq3so0LC5Q14dA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pagefind/linux-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pagefind/linux-x64/-/linux-x64-1.1.0.tgz", + "integrity": "sha512-4lsg6VB7A6PWTwaP8oSmXV4O9H0IHX7AlwTDcfyT+YJo/sPXOVjqycD5cdBgqNLfUk8B9bkWcTDCRmJbHrKeCw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pagefind/windows-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pagefind/windows-x64/-/windows-x64-1.1.0.tgz", + "integrity": "sha512-OboCM76BcMKT9IoSfZuFhiqMRgTde8x4qDDvKulFmycgiJrlL5WnIqBHJLQxZq+o2KyZpoHF97iwsGAm8c32sQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.1.tgz", + "integrity": "sha512-fH8/o8nSUek8ceQnT7K4EQbSiV7jgkHq81m9lWZFIXjJ7lJzpWXbQFpT/Zh6OZYnpFykvzC3fbEvEAFZu03dPA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.1.tgz", + "integrity": "sha512-Y/9OHLjzkunF+KGEoJr3heiD5X9OLa8sbT1lm0NYeKyaM3oMhhQFvPB0bNZYJwlq93j8Z6wSxh9+cyKQaxS7PQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.1.tgz", + "integrity": "sha512-+kecg3FY84WadgcuSVm6llrABOdQAEbNdnpi5X3UwWiFVhZIZvKgGrF7kmLguvxHNQy+UuRV66cLVl3S+Rkt+Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.1.tgz", + "integrity": "sha512-2pYRzEjVqq2TB/UNv47BV/8vQiXkFGVmPFwJb+1E0IFFZbIX8/jo1olxqqMbo6xCXf8kabANhp5bzCij2tFLUA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.1.tgz", + "integrity": "sha512-mS6wQ6Do6/wmrF9aTFVpIJ3/IDXhg1EZcQFYHZLHqw6AzMBjTHWnCG35HxSqUNphh0EHqSM6wRTT8HsL1C0x5g==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.1.tgz", + "integrity": "sha512-p9rGKYkHdFMzhckOTFubfxgyIO1vw//7IIjBBRVzyZebWlzRLeNhqxuSaZ7kCEKVkm/kuC9fVRW9HkC/zNRG2w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.1.tgz", + "integrity": "sha512-nDY6Yz5xS/Y4M2i9JLQd3Rofh5OR8Bn8qe3Mv/qCVpHFlwtZSBYSPaU4mrGazWkXrdQ98GB//H0BirGR/SKFSw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.1.tgz", + "integrity": "sha512-im7HE4VBL+aDswvcmfx88Mp1soqL9OBsdDBU8NqDEYtkri0qV0THhQsvZtZeNNlLeCUQ16PZyv7cqutjDF35qw==", + "cpu": [ + "ppc64le" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.1.tgz", + "integrity": "sha512-RWdiHuAxWmzPJgaHJdpvUUlDz8sdQz4P2uv367T2JocdDa98iRw2UjIJ4QxSyt077mXZT2X6pKfT2iYtVEvOFw==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.1.tgz", + "integrity": "sha512-VMgaGQ5zRX6ZqV/fas65/sUGc9cPmsntq2FiGmayW9KMNfWVG/j0BAqImvU4KTeOOgYSf1F+k6at1UfNONuNjA==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.1.tgz", + "integrity": "sha512-9Q7DGjZN+hTdJomaQ3Iub4m6VPu1r94bmK2z3UeWP3dGUecRC54tmVu9vKHTm1bOt3ASoYtEz6JSRLFzrysKlA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.1.tgz", + "integrity": "sha512-JNEG/Ti55413SsreTguSx0LOVKX902OfXIKVg+TCXO6Gjans/k9O6ww9q3oLGjNDaTLxM+IHFMeXy/0RXL5R/g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.1.tgz", + "integrity": "sha512-ryS22I9y0mumlLNwDFYZRDFLwWh3aKaC72CWjFcFvxK0U6v/mOkM5Up1bTbCRAhv3kEIwW2ajROegCIQViUCeA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.1.tgz", + "integrity": "sha512-TdloItiGk+T0mTxKx7Hp279xy30LspMso+GzQvV2maYePMAWdmrzqSNZhUpPj3CGw12aGj57I026PgLCTu8CGg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.1.tgz", + "integrity": "sha512-wQGI+LY/Py20zdUPq+XCem7JcPOyzIJBm3dli+56DJsQOHbnXZFEwgmnC6el1TPAfC8lBT3m+z69RmLykNUbew==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.3.0.tgz", + "integrity": "sha512-7fedsBfuILDTBmrYZNFI8B6ATTxhQAasUHllHmjvSZPnoq4bULWoTpHwmuQvZ8Aq03/tAa2IGo6RXqWtHdWaCA==" + }, + "node_modules/@types/acorn": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", + "integrity": "sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz", + "integrity": "sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.12.tgz", + "integrity": "sha512-H9VZ9YqE+H28FQVchC83RCs5xQ2J7mAAv6qdDEaWmXEVl3OpdH+xfrSUzQ1lp7U7oSTRZ0RvW08ASPJsYBi7Cw==" + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" + }, + "node_modules/@types/nlcst": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-1.0.4.tgz", + "integrity": "sha512-ABoYdNQ/kBSsLvZAekMhIPMQ3YUZvavStpKYs7BjLLuKVmIMA0LUgZ7b54zzuWJRbHF80v1cNf4r90Vd6eMQDg==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@types/nlcst/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" + }, + "node_modules/@types/node": { + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/parse5": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", + "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==" + }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, + "node_modules/@volar/kit": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@volar/kit/-/kit-2.1.6.tgz", + "integrity": "sha512-dSuXChDGM0nSG/0fxqlNfadjpAeeo1P1SJPBQ+pDf8H1XrqeJq5gIhxRTEbiS+dyNIG69ATq1CArkbCif+oxJw==", + "dependencies": { + "@volar/language-service": "2.1.6", + "@volar/typescript": "2.1.6", + "typesafe-path": "^0.2.2", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/@volar/language-core": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.1.6.tgz", + "integrity": "sha512-pAlMCGX/HatBSiDFMdMyqUshkbwWbLxpN/RL7HCQDOo2gYBE+uS+nanosLc1qR6pTQ/U8q00xt8bdrrAFPSC0A==", + "dependencies": { + "@volar/source-map": "2.1.6" + } + }, + "node_modules/@volar/language-server": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@volar/language-server/-/language-server-2.1.6.tgz", + "integrity": "sha512-0w+FV8ro37hVb3qE4ONo3VbS5kEQXv4H/D2xCePyY5dRw6XnbJAPFNKvoxI9mxHTPonvIG1si5rN9MSGSKtgZQ==", + "dependencies": { + "@volar/language-core": "2.1.6", + "@volar/language-service": "2.1.6", + "@volar/snapshot-document": "2.1.6", + "@volar/typescript": "2.1.6", + "@vscode/l10n": "^0.0.16", + "path-browserify": "^1.0.1", + "request-light": "^0.7.0", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@volar/language-service": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@volar/language-service/-/language-service-2.1.6.tgz", + "integrity": "sha512-1OpbbPQ6wUIumwMP5r45y8utVEmvq1n6BC8JHqGKsuFr9RGFIldDBlvA/xuO3MDKhjmmPGPHKb54kg1/YN78ow==", + "dependencies": { + "@volar/language-core": "2.1.6", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@volar/snapshot-document": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@volar/snapshot-document/-/snapshot-document-2.1.6.tgz", + "integrity": "sha512-YNYk1sCOrGg7VHbZM+1It97q0GWhFxdqIwnxSNFoL0X1LuSRXoCT2DRb/aa1J6aBpPMbKqSFUWHGQEAFUnc4Zw==", + "dependencies": { + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11" + } + }, + "node_modules/@volar/source-map": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.1.6.tgz", + "integrity": "sha512-TeyH8pHHonRCHYI91J7fWUoxi0zWV8whZTVRlsWHSYfjm58Blalkf9LrZ+pj6OiverPTmrHRkBsG17ScQyWECw==", + "dependencies": { + "muggle-string": "^0.4.0" + } + }, + "node_modules/@volar/typescript": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.1.6.tgz", + "integrity": "sha512-JgPGhORHqXuyC3r6skPmPHIZj4LoMmGlYErFTuPNBq9Nhc9VTv7ctHY7A3jMN3ngKEfRrfnUcwXHztvdSQqNfw==", + "dependencies": { + "@volar/language-core": "2.1.6", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@vscode/emmet-helper": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@vscode/emmet-helper/-/emmet-helper-2.9.2.tgz", + "integrity": "sha512-MaGuyW+fa13q3aYsluKqclmh62Hgp0BpKIqS66fCxfOaBcVQ1OnMQxRRgQUYnCkxFISAQlkJ0qWWPyXjro1Qrg==", + "dependencies": { + "emmet": "^2.4.3", + "jsonc-parser": "^2.3.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.15.1", + "vscode-uri": "^2.1.2" + } + }, + "node_modules/@vscode/emmet-helper/node_modules/vscode-uri": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-2.1.2.tgz", + "integrity": "sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==" + }, + "node_modules/@vscode/l10n": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.16.tgz", + "integrity": "sha512-JT5CvrIYYCrmB+dCana8sUqJEcGB1ZDXNLMQ2+42bW995WmNoenijWMUdZfwmuQUTQcEVVIa2OecZzTYWUW9Cg==" + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-iterate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz", + "integrity": "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/astring": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.8.6.tgz", + "integrity": "sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/astro": { + "version": "4.5.18", + "resolved": "https://registry.npmjs.org/astro/-/astro-4.5.18.tgz", + "integrity": "sha512-iytLnUfyUneKMjIQdj79zzniByXtcmGNDobIV/gjGsatC9vAyPqeCT8TbMqfkRBMeYGs+S/wCzSoPqaaMJiQnw==", + "dependencies": { + "@astrojs/compiler": "^2.7.1", + "@astrojs/internal-helpers": "0.4.0", + "@astrojs/markdown-remark": "5.0.0", + "@astrojs/telemetry": "3.0.4", + "@babel/core": "^7.24.3", + "@babel/generator": "^7.23.3", + "@babel/parser": "^7.23.3", + "@babel/plugin-transform-react-jsx": "^7.22.5", + "@babel/traverse": "^7.23.3", + "@babel/types": "^7.23.3", + "@types/babel__core": "^7.20.4", + "acorn": "^8.11.2", + "aria-query": "^5.3.0", + "axobject-query": "^4.0.0", + "boxen": "^7.1.1", + "chokidar": "^3.5.3", + "ci-info": "^4.0.0", + "clsx": "^2.0.0", + "common-ancestor-path": "^1.0.1", + "cookie": "^0.6.0", + "cssesc": "^3.0.0", + "debug": "^4.3.4", + "deterministic-object-hash": "^2.0.1", + "devalue": "^4.3.2", + "diff": "^5.1.0", + "dlv": "^1.1.3", + "dset": "^3.1.3", + "es-module-lexer": "^1.4.1", + "esbuild": "^0.19.6", + "estree-walker": "^3.0.3", + "execa": "^8.0.1", + "fast-glob": "^3.3.2", + "flattie": "^1.1.0", + "github-slugger": "^2.0.0", + "gray-matter": "^4.0.3", + "html-escaper": "^3.0.3", + "http-cache-semantics": "^4.1.1", + "js-yaml": "^4.1.0", + "kleur": "^4.1.4", + "magic-string": "^0.30.3", + "mime": "^3.0.0", + "ora": "^7.0.1", + "p-limit": "^5.0.0", + "p-queue": "^8.0.1", + "path-to-regexp": "^6.2.1", + "preferred-pm": "^3.1.2", + "prompts": "^2.4.2", + "rehype": "^13.0.1", + "resolve": "^1.22.4", + "semver": "^7.5.4", + "shiki": "^1.1.2", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0", + "tsconfck": "^3.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.1", + "vite": "^5.1.4", + "vitefu": "^0.2.5", + "which-pm": "^2.1.1", + "yargs-parser": "^21.1.1", + "zod": "^3.22.4", + "zod-to-json-schema": "^3.22.4" + }, + "bin": { + "astro": "astro.js" + }, + "engines": { + "node": ">=18.14.1", + "npm": ">=6.14.0" + }, + "optionalDependencies": { + "sharp": "^0.32.6" + } + }, + "node_modules/astro-expressive-code": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.33.5.tgz", + "integrity": "sha512-9JAyllueMUN8JTl/h/yTdbKinNmfalEWcV11s3lSf/UJQbAZfWJuy+IlGcArZDI/CmD21GXhFHLqYthpdY33ug==", + "dependencies": { + "hast-util-to-html": "^8.0.4", + "remark-expressive-code": "^0.33.5" + }, + "peerDependencies": { + "astro": "^4.0.0-beta || ^3.3.0" + } + }, + "node_modules/astro-expressive-code/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/astro-expressive-code/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" + }, + "node_modules/astro-expressive-code/node_modules/hast-util-from-parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz", + "integrity": "sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "hastscript": "^7.0.0", + "property-information": "^6.0.0", + "vfile": "^5.0.0", + "vfile-location": "^4.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/astro-expressive-code/node_modules/hast-util-parse-selector": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz", + "integrity": "sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==", + "dependencies": { + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/astro-expressive-code/node_modules/hast-util-raw": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-7.2.3.tgz", + "integrity": "sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "hast-util-from-parse5": "^7.0.0", + "hast-util-to-parse5": "^7.0.0", + "html-void-elements": "^2.0.0", + "parse5": "^6.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/astro-expressive-code/node_modules/hast-util-to-html": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.4.tgz", + "integrity": "sha512-4tpQTUOr9BMjtYyNlt0P50mH7xj0Ks2xpo8M943Vykljf99HW6EzulIoJP1N3eKOSScEHzyzi9dm7/cn0RfGwA==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-raw": "^7.0.0", + "hast-util-whitespace": "^2.0.0", + "html-void-elements": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/astro-expressive-code/node_modules/hast-util-to-parse5": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz", + "integrity": "sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/astro-expressive-code/node_modules/hast-util-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", + "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/astro-expressive-code/node_modules/hastscript": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz", + "integrity": "sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/astro-expressive-code/node_modules/html-void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", + "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/astro-expressive-code/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + }, + "node_modules/astro-expressive-code/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/astro-expressive-code/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/astro-expressive-code/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/astro-expressive-code/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/astro-expressive-code/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/astro-expressive-code/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/astro-expressive-code/node_modules/vfile-location": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.1.0.tgz", + "integrity": "sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==", + "dependencies": { + "@types/unist": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/astro-expressive-code/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/axobject-query": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", + "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==" + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bare-events": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.2.tgz", + "integrity": "sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==", + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.2.3.tgz", + "integrity": "sha512-amG72llr9pstfXOBOHve1WjiuKKAMnebcmMbPWDZ7BCevAoJLpugjuAPRsDINEyjT0a6tbaVx3DctkXIRbLuJw==", + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "streamx": "^2.13.0" + } + }, + "node_modules/bare-os": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.2.1.tgz", + "integrity": "sha512-OwPyHgBBMkhC29Hl3O4/YfxW9n7mdTr2+SsO29XBWKKJsbgj3mnorDB80r5TiCQgQstgE5ga1qNYrpes6NvX2w==", + "optional": true + }, + "node_modules/bare-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.1.tgz", + "integrity": "sha512-OHM+iwRDRMDBsSW7kl3dO62JyHdBKO3B25FB9vNQBPcGHMo4+eA8Yj41Lfbk3pS/seDY+siNge0LdRTulAau/A==", + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, + "node_modules/base-64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bcp-47": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz", + "integrity": "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-match": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "node_modules/boxen": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz", + "integrity": "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.1", + "chalk": "^5.2.0", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/boxen/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/boxen/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001608", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001608.tgz", + "integrity": "sha512-cjUJTQkk9fQlJR2s4HMuPMvTiRggl0rAVMtthQuyOlDWuqHXqN8azLq+pi8B2TjwKJ32diHjUqRIKeFX4z1FoA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/ci-info": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", + "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/common-ancestor-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz", + "integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-selector-parser": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.0.5.tgz", + "integrity": "sha512-3itoDFbKUNx1eKmVpYMFyqKX04Ww9osZ+dLgrk6GEv6KMVeXUhUnp4I5X+evw+u3ZxVU6RFXSSRxlTeMh8bA+g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/deterministic-object-hash": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/deterministic-object-hash/-/deterministic-object-hash-2.0.2.tgz", + "integrity": "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==", + "dependencies": { + "base-64": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/devalue": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.2.tgz", + "integrity": "sha512-KqFl6pOgOW+Y6wJgu80rHpo2/3H07vr8ntR9rkkFIRETewbf5GaYYcakYfiKz89K+sLsuPkQIZaXDMjUObZwWg==" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/direction": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz", + "integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + }, + "node_modules/dset": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.3.tgz", + "integrity": "sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/electron-to-chromium": { + "version": "1.4.731", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.731.tgz", + "integrity": "sha512-+TqVfZjpRz2V/5SPpmJxq9qK620SC5SqCnxQIOi7i/U08ZDcTpKbT7Xjj9FU5CbXTMUb4fywbIr8C7cGv4hcjw==" + }, + "node_modules/emmet": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/emmet/-/emmet-2.4.7.tgz", + "integrity": "sha512-O5O5QNqtdlnQM2bmKHtJgyChcrFMgQuulI+WdiOw2NArzprUqqxUW6bgYtKvzKgrsYpuLWalOkdhNP+1jluhCA==", + "dependencies": { + "@emmetio/abbreviation": "^2.3.3", + "@emmetio/css-abbreviation": "^2.1.8" + } + }, + "node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz", + "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==" + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/expressive-code": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.33.5.tgz", + "integrity": "sha512-UPg2jSvZEfXPiCa4MKtMoMQ5Hwiv7In5/LSCa/ukhjzZqPO48iVsCcEBgXWEUmEAQ02P0z00/xFfBmVnUKH+Zw==", + "dependencies": { + "@expressive-code/core": "^0.33.5", + "@expressive-code/plugin-frames": "^0.33.5", + "@expressive-code/plugin-shiki": "^0.33.5", + "@expressive-code/plugin-text-markers": "^0.33.5" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-yarn-workspace-root2": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root2/-/find-yarn-workspace-root2-1.2.16.tgz", + "integrity": "sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==", + "dependencies": { + "micromatch": "^4.0.2", + "pkg-dir": "^4.2.0" + } + }, + "node_modules/flattie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", + "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", + "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==" + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.1.tgz", + "integrity": "sha512-RXQBLMl9kjKVNkJTIO6bZyb2n+cUH8LFaSSzo82jiLT6Tfc+Pt7VQCS+/h3YwG4jaNE2TA2sdJisGWR+aJrp0g==", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz", + "integrity": "sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^8.0.0", + "property-information": "^6.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-has-property": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.2.tgz", + "integrity": "sha512-PldBy71wO9Uq1kyaMch9AHIghtQvIwxBUkv823pKmkTM3oV1JxtsTNYdevMxvUHqcnOAuO65JKU2+0NOxc2ksA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-select": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-6.0.2.tgz", + "integrity": "sha512-hT/SD/d/Meu+iobvgkffo1QecV8WeKWxwsNMzcTJsKw1cKTQKSR/7ArJeURLNJF9HDjp9nVoORyNNJxrvBye8Q==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "bcp-47-match": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "css-selector-parser": "^3.0.0", + "devlop": "^1.0.0", + "direction": "^2.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "not": "^0.1.0", + "nth-check": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.0.tgz", + "integrity": "sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw==", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^0.4.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.1.tgz", + "integrity": "sha512-hZOofyZANbyWo+9RP75xIDV/gq+OUKx+T46IlwERnKmfpwp81XBFbT9mi26ws+SJchA4RVUQwIBJpqEOBhMzEQ==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-raw": "^9.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz", + "integrity": "sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ==", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/inline-style-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.3.tgz", + "integrity": "sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==" + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/style-to-object": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.6.tgz", + "integrity": "sha512-khxq+Qm3xEyZfKd/y9L3oIWQimxuc4STrQKtQn8aSDRHb8mFgpukgX1hdzfrMEW6JCjyJ8p89x+IUMVnCBI1PA==", + "dependencies": { + "inline-style-parser": "0.2.3" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.0.tgz", + "integrity": "sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.1.tgz", + "integrity": "sha512-RHL7Vo2n06ZocCFWqmbyhZ1pCYX/mSKdywt9YD5U6Hquu5syV+dImCXFKLFt02JoK5QxkQFS0PoVdFdPXuPffQ==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz", + "integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/import-meta-resolve": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz", + "integrity": "sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-reference": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", + "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz", + "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/load-yaml-file": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/load-yaml-file/-/load-yaml-file-0.2.0.tgz", + "integrity": "sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==", + "dependencies": { + "graceful-fs": "^4.1.5", + "js-yaml": "^3.13.0", + "pify": "^4.0.1", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/load-yaml-file/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/load-yaml-file/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", + "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", + "dependencies": { + "chalk": "^5.0.0", + "is-unicode-supported": "^1.1.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.9.tgz", + "integrity": "sha512-S1+hd+dIrC8EZqKyT9DstTH/0Z+f76kmmvZnkfQVmOpDEF9iVgdYif3Q/pIWHmCoo59bQVGW0kVL3e2nl+9+Sw==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", + "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-definitions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", + "integrity": "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-directive": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.0.0.tgz", + "integrity": "sha512-JUpYOqKI4mM3sZcNxmF/ox04XYFFkNwr0CFlrQIkCwbvH0xzMCqkMqAde9wRd80VAhaUrwFwKm2nxretdT1h7Q==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", + "integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz", + "integrity": "sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz", + "integrity": "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz", + "integrity": "sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz", + "integrity": "sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz", + "integrity": "sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.2.tgz", + "integrity": "sha512-eKMQDeywY2wlHc97k5eD8VC+9ASMjN8ItEZQNGwJ6E0XWKiW/Z0V5/H8pvoXUf+y+Mj0VIgeRRbujBmFn4FTyA==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-remove-position": "^5.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz", + "integrity": "sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", + "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.0.tgz", + "integrity": "sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.0.tgz", + "integrity": "sha512-61OI07qpQrERc+0wEysLHMvoiO3s2R56x5u7glHq2Yqq6EHbH4dW25G9GfDdGCDYqA21KE6DWgNSzxSwHc2hSg==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.0.0.tgz", + "integrity": "sha512-rTHfnpt/Q7dEAK1Y5ii0W8bhfJlVJFnJMHIPisfPK3gpVNuOP0VnRl96+YJ3RYWV/P4gFeQoGKNlT3RhuvpqAg==", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.0.0.tgz", + "integrity": "sha512-6Rzu0CYRKDv3BfLAUnZsSlzx3ak6HAoI85KTiijuKIz5UxZxbUI+pD6oHgw+6UtQuiRwnGRhzMmPRv4smcz0fg==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-c3BR1ClMp5fxxmwP6AoOY2fXO9U8uFMKs4ADD66ahLTNcwzSCyRVU4k7LPV5Nxo/VJiR4TdzxRQY2v3qIUceCw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.0.0.tgz", + "integrity": "sha512-PoHlhypg1ItIucOaHmKE8fbin3vTLpDOUg8KAr8gRCF1MOZI9Nquq2i/44wFvviM4WuxJzc3demT8Y3dkfvYrw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.0.1.tgz", + "integrity": "sha512-cY5PzGcnULaN5O7T+cOzfMoHjBW7j+T9D2sucA5d/KbsBTPcYdebm9zUd9zzdgJGCwahV+/W78Z3nbulBYVbTw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.0.tgz", + "integrity": "sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.0.tgz", + "integrity": "sha512-uvhhss8OGuzR4/N17L1JwvmJIpPhAd8oByMawEKx6NVdBCbesjH4t+vjEp3ZXft9DwvlKSD07fCeI44/N0Vf2w==", + "dependencies": { + "@types/acorn": "^4.0.0", + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", + "integrity": "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz", + "integrity": "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.1.tgz", + "integrity": "sha512-F0ccWIUHRLRrYp5TC9ZYXmZo+p2AM13ggbsW4T0b5CRKP8KHVRB8t4pwtBgTxtjRmwrK0Irwm7vs2JOZabHZfg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", + "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz", + "integrity": "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz", + "integrity": "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", + "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", + "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", + "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz", + "integrity": "sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz", + "integrity": "sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.2.tgz", + "integrity": "sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/acorn": "^4.0.0", + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz", + "integrity": "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz", + "integrity": "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", + "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.0.tgz", + "integrity": "sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", + "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/nlcst-to-string": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-3.1.1.tgz", + "integrity": "sha512-63mVyqaqt0cmn2VcI2aH6kxe1rLAmSROqHMA0i4qqg1tidkfExgpb0FGMikMCn86mw5dFtBtEANfmSSK7TjNHw==", + "dependencies": { + "@types/nlcst": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/node-abi": { + "version": "3.57.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.57.0.tgz", + "integrity": "sha512-Dp+A9JWxRaKuHP35H77I4kCKesDy5HUDEmScia2FyncMTOXASMyg251F5PhFoDA5uqBrDDffiLpbqnrZmNXW+g==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/not": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/not/-/not-0.1.0.tgz", + "integrity": "sha512-5PDmaAsVfnWUgTUbJ3ERwn7u79Z0dYxN9ErxCpVJJqe2RK0PJ3z+iFUxuqjwtlDDegXvtWoxD/3Fzxox7tFGWA==" + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-7.0.1.tgz", + "integrity": "sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^4.0.0", + "cli-spinners": "^2.9.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^1.3.0", + "log-symbols": "^5.1.0", + "stdin-discarder": "^0.1.0", + "string-width": "^6.1.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-6.1.0.tgz", + "integrity": "sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^10.2.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.0.1.tgz", + "integrity": "sha512-NXzu9aQJTAzbBqOt2hwsR63ea7yvxJc0PwN/zobNAudYfb1B7R08SzB4TsLeSbUCuG467NhnoT0oO6w1qRO+BA==", + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.2.tgz", + "integrity": "sha512-UbD77BuZ9Bc9aABo74gfXhNvzC9Tx7SxtHSh1fxvx3jTLLYvmVhiQZZrJzqqU0jKbN32kb5VOKiLEQI/3bIjgQ==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pagefind": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.1.0.tgz", + "integrity": "sha512-1nmj0/vfYcMxNEQj0YDRp6bTVv9hI7HLdPhK/vBBYlrnwjATndQvHyicj5Y7pUHrpCFZpFnLVQXIF829tpFmaw==", + "bin": { + "pagefind": "lib/runner/bin.cjs" + }, + "optionalDependencies": { + "@pagefind/darwin-arm64": "1.1.0", + "@pagefind/darwin-x64": "1.1.0", + "@pagefind/linux-arm64": "1.1.0", + "@pagefind/linux-x64": "1.1.0", + "@pagefind/windows-x64": "1.1.0" + } + }, + "node_modules/parse-entities": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", + "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" + }, + "node_modules/parse-latin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-5.0.1.tgz", + "integrity": "sha512-b/K8ExXaWC9t34kKeDV8kGXBkXZ1HCSAZRYE7HR14eA1GlXX5L8iWhs8USJNhQU9q5ci413jCKF0gOyovvyRBg==", + "dependencies": { + "nlcst-to-string": "^3.0.0", + "unist-util-modify-children": "^3.0.0", + "unist-util-visit-children": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==" + }, + "node_modules/periscopic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", + "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^3.0.0", + "is-reference": "^3.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/prebuild-install/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/prebuild-install/node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/prebuild-install/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/preferred-pm": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/preferred-pm/-/preferred-pm-3.1.3.tgz", + "integrity": "sha512-MkXsENfftWSRpzCzImcp4FRsCc3y1opwB73CfCNWyzMqArju2CrlMHlqB7VexKiPEOjGMbttv1r9fSCn5S610w==", + "dependencies": { + "find-up": "^5.0.0", + "find-yarn-workspace-root2": "1.2.16", + "path-exists": "^4.0.0", + "which-pm": "2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/preferred-pm/node_modules/which-pm": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-pm/-/which-pm-2.0.0.tgz", + "integrity": "sha512-Lhs9Pmyph0p5n5Z3mVnN0yWcbQYUAD7rbQUiMsQxOJ3T57k7RFe35SUwWMf7dsbDZks1uOmw4AecB/JMDj3v/w==", + "dependencies": { + "load-yaml-file": "^0.2.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8.15" + } + }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "engines": { + "node": ">=6" + } + }, + "node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rehype": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.1.tgz", + "integrity": "sha512-AcSLS2mItY+0fYu9xKxOu1LhUZeBZZBx8//5HKzF+0XP+eP8+6a5MXn2+DW2kfXR6Dtp1FEXMVrjyKAcvcU8vg==", + "dependencies": { + "@types/hast": "^3.0.0", + "rehype-parse": "^9.0.0", + "rehype-stringify": "^10.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.0.tgz", + "integrity": "sha512-WG7nfvmWWkCR++KEkZevZb/uw41E8TsH4DsY9UxsTbIXCVGbAs4S+r8FrQ+OtH5EEQAs+5UxKC42VinkmpA1Yw==", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.0.tgz", + "integrity": "sha512-1TX1i048LooI9QoecrXy7nGFFbFSufxVRAfc6Y9YMRAi56l+oB0zP51mLSV312uRuvVLPV1opSlJmslozR1XHQ==", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-directive": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.0.tgz", + "integrity": "sha512-l1UyWJ6Eg1VPU7Hm/9tt0zKtReJQNOA4+iDMAxTyZNWnJnFlbS/7zhiel/rogTLQ2vMYwDzSJa4BiVNqGlqIMA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-directive": "^3.0.0", + "micromark-extension-directive": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-expressive-code": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/remark-expressive-code/-/remark-expressive-code-0.33.5.tgz", + "integrity": "sha512-E4CZq3AuUXLu6or0AaDKkgsHYqmnm4ZL8/+1/8YgwtKcogHwTMRJfQtxkZpth90QQoNUpsapvm5x5n3Np2OC9w==", + "dependencies": { + "expressive-code": "^0.33.5", + "hast-util-to-html": "^8.0.4", + "unist-util-visit": "^4.1.2" + } + }, + "node_modules/remark-expressive-code/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-expressive-code/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" + }, + "node_modules/remark-expressive-code/node_modules/hast-util-from-parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz", + "integrity": "sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "hastscript": "^7.0.0", + "property-information": "^6.0.0", + "vfile": "^5.0.0", + "vfile-location": "^4.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-expressive-code/node_modules/hast-util-parse-selector": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz", + "integrity": "sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==", + "dependencies": { + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-expressive-code/node_modules/hast-util-raw": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-7.2.3.tgz", + "integrity": "sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "hast-util-from-parse5": "^7.0.0", + "hast-util-to-parse5": "^7.0.0", + "html-void-elements": "^2.0.0", + "parse5": "^6.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-expressive-code/node_modules/hast-util-to-html": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.4.tgz", + "integrity": "sha512-4tpQTUOr9BMjtYyNlt0P50mH7xj0Ks2xpo8M943Vykljf99HW6EzulIoJP1N3eKOSScEHzyzi9dm7/cn0RfGwA==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-raw": "^7.0.0", + "hast-util-whitespace": "^2.0.0", + "html-void-elements": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-expressive-code/node_modules/hast-util-to-parse5": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz", + "integrity": "sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-expressive-code/node_modules/hast-util-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", + "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-expressive-code/node_modules/hastscript": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz", + "integrity": "sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-expressive-code/node_modules/html-void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", + "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-expressive-code/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + }, + "node_modules/remark-expressive-code/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-expressive-code/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-expressive-code/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-expressive-code/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-expressive-code/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-expressive-code/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-expressive-code/node_modules/vfile-location": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.1.0.tgz", + "integrity": "sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==", + "dependencies": { + "@types/unist": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-expressive-code/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz", + "integrity": "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.0.1.tgz", + "integrity": "sha512-3Pz3yPQ5Rht2pM5R+0J2MrGoBSrzf+tJG94N+t/ilfdh8YLyyKYtidAYwTveB20BoHAcwIopOUqhcmh2F7hGYA==", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.0.tgz", + "integrity": "sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-smartypants": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-2.1.0.tgz", + "integrity": "sha512-qoF6Vz3BjU2tP6OfZqHOvCU0ACmu/6jhGaINSQRI9mM7wCxNQTKB3JUAN4SVoN2ybElEDTxBIABRep7e569iJw==", + "dependencies": { + "retext": "^8.1.0", + "retext-smartypants": "^5.2.0", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/request-light": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/request-light/-/request-light-0.7.0.tgz", + "integrity": "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/retext": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/retext/-/retext-8.1.0.tgz", + "integrity": "sha512-N9/Kq7YTn6ZpzfiGW45WfEGJqFf1IM1q8OsRa1CGzIebCJBNCANDRmOrholiDRGKo/We7ofKR4SEvcGAWEMD3Q==", + "dependencies": { + "@types/nlcst": "^1.0.0", + "retext-latin": "^3.0.0", + "retext-stringify": "^3.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-latin": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-3.1.0.tgz", + "integrity": "sha512-5MrD1tuebzO8ppsja5eEu+ZbBeUNCjoEarn70tkXOS7Bdsdf6tNahsv2bY0Z8VooFF6cw7/6S+d3yI/TMlMVVQ==", + "dependencies": { + "@types/nlcst": "^1.0.0", + "parse-latin": "^5.0.0", + "unherit": "^3.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-latin/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" + }, + "node_modules/retext-latin/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-latin/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-latin/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-latin/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-5.2.0.tgz", + "integrity": "sha512-Do8oM+SsjrbzT2UNIKgheP0hgUQTDDQYyZaIY3kfq0pdFzoPk+ZClYJ+OERNXveog4xf1pZL4PfRxNoVL7a/jw==", + "dependencies": { + "@types/nlcst": "^1.0.0", + "nlcst-to-string": "^3.0.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" + }, + "node_modules/retext-smartypants/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-stringify": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-3.1.0.tgz", + "integrity": "sha512-767TLOaoXFXyOnjx/EggXlb37ZD2u4P1n0GJqVdpipqACsQP+20W+BNpMYrlJkq7hxffnFk+jc6mAK9qrbuB8w==", + "dependencies": { + "@types/nlcst": "^1.0.0", + "nlcst-to-string": "^3.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-stringify/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" + }, + "node_modules/retext-stringify/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-stringify/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-stringify/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-stringify/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" + }, + "node_modules/retext/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.1.tgz", + "integrity": "sha512-4LnHSdd3QK2pa1J6dFbfm1HN0D7vSK/ZuZTsdyUAlA6Rr1yTouUTL13HaDOGJVgby461AhrNGBS7sCGXXtT+SA==", + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.14.1", + "@rollup/rollup-android-arm64": "4.14.1", + "@rollup/rollup-darwin-arm64": "4.14.1", + "@rollup/rollup-darwin-x64": "4.14.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.14.1", + "@rollup/rollup-linux-arm64-gnu": "4.14.1", + "@rollup/rollup-linux-arm64-musl": "4.14.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.14.1", + "@rollup/rollup-linux-riscv64-gnu": "4.14.1", + "@rollup/rollup-linux-s390x-gnu": "4.14.1", + "@rollup/rollup-linux-x64-gnu": "4.14.1", + "@rollup/rollup-linux-x64-musl": "4.14.1", + "@rollup/rollup-win32-arm64-msvc": "4.14.1", + "@rollup/rollup-win32-ia32-msvc": "4.14.1", + "@rollup/rollup-win32-x64-msvc": "4.14.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/sharp": { + "version": "0.32.6", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", + "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.2", + "node-addon-api": "^6.1.0", + "prebuild-install": "^7.1.1", + "semver": "^7.5.4", + "simple-get": "^4.0.1", + "tar-fs": "^3.0.4", + "tunnel-agent": "^0.6.0" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/shiki": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.3.0.tgz", + "integrity": "sha512-9aNdQy/etMXctnPzsje1h1XIGm9YfRcSksKOGqZWXA/qP9G18/8fpz5Bjpma8bOgz3tqIpjERAd6/lLjFyzoww==", + "dependencies": { + "@shikijs/core": "1.3.0" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + }, + "node_modules/sitemap": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-7.1.1.tgz", + "integrity": "sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg==", + "dependencies": { + "@types/node": "^17.0.5", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.2.4" + }, + "bin": { + "sitemap": "dist/cli.js" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=5.6.0" + } + }, + "node_modules/sitemap/node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==" + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/stdin-discarder": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", + "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==", + "dependencies": { + "bl": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/streamx": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", + "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", + "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/style-to-object": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz", + "integrity": "sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==", + "dependencies": { + "inline-style-parser": "0.1.1" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar-fs": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.5.tgz", + "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tsconfck": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.0.3.tgz", + "integrity": "sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typesafe-path": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/typesafe-path/-/typesafe-path-0.2.2.tgz", + "integrity": "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==" + }, + "node_modules/typescript": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz", + "integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-auto-import-cache": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.2.tgz", + "integrity": "sha512-+laqe5SFL1vN62FPOOJSUDTZxtgsoOXjneYOXIpx5rQ4UMiN89NAtJLpqLqyebv9fgQ/IMeeTX+mQyRnwvJzvg==", + "dependencies": { + "semver": "^7.3.8" + } + }, + "node_modules/uglify-js": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.18.0.tgz", + "integrity": "sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A==", + "dev": true, + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/unherit": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/unherit/-/unherit-3.0.1.tgz", + "integrity": "sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unified": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", + "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-modify-children": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-3.1.1.tgz", + "integrity": "sha512-yXi4Lm+TG5VG+qvokP6tpnk+r1EPwyYL04JWDxLvgvPV40jANh7nm3udk65OOWquvbMDe+PL9+LmkxDpTv/7BA==", + "dependencies": { + "@types/unist": "^2.0.0", + "array-iterate": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-modify-children/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-children": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-2.0.2.tgz", + "integrity": "sha512-+LWpMFqyUwLGpsQxpumsQ9o9DG2VGLFrpz+rpVXYIEdPy57GSy5HioC0g3bg/8WP9oCLlapQtklOzQ8uLS496Q==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-children/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/vfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", + "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.2.tgz", + "integrity": "sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz", + "integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==", + "dependencies": { + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/vitefu": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", + "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/volar-service-css": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/volar-service-css/-/volar-service-css-0.0.34.tgz", + "integrity": "sha512-C7ua0j80ZD7bsgALAz/cA1bykPehoIa5n+3+Ccr+YLpj0fypqw9iLUmGLX11CqzqNCO2XFGe/1eXB/c+SWrF/g==", + "dependencies": { + "vscode-css-languageservice": "^6.2.10", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.1.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-emmet": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/volar-service-emmet/-/volar-service-emmet-0.0.34.tgz", + "integrity": "sha512-ubQvMCmHPp8Ic82LMPkgrp9ot+u2p/RDd0RyT0EykRkZpWsagHUF5HWkVheLfiMyx2rFuWx/+7qZPOgypx6h6g==", + "dependencies": { + "@vscode/emmet-helper": "^2.9.2", + "vscode-html-languageservice": "^5.1.0" + }, + "peerDependencies": { + "@volar/language-service": "~2.1.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-html": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/volar-service-html/-/volar-service-html-0.0.34.tgz", + "integrity": "sha512-kMEneea1tQbiRcyKavqdrSVt8zV06t+0/3pGkjO3gV6sikXTNShIDkdtB4Tq9vE2cQdM50TuS7utVV7iysUxHw==", + "dependencies": { + "vscode-html-languageservice": "^5.1.0", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.1.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-prettier": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/volar-service-prettier/-/volar-service-prettier-0.0.34.tgz", + "integrity": "sha512-BNfJ8FwfPi1Wm/JkuzNjraOLdtKieGksNT/bDyquygVawv1QUzO2HB1hiMKfZGdcSFG5ZL9R0j7bBfRTfXA2gg==", + "dependencies": { + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.1.0", + "prettier": "^2.2 || ^3.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + }, + "prettier": { + "optional": true + } + } + }, + "node_modules/volar-service-typescript": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/volar-service-typescript/-/volar-service-typescript-0.0.34.tgz", + "integrity": "sha512-NbAry0w8ZXFgGsflvMwmPDCzgJGx3C+eYxFEbldaumkpTAJiywECWiUbPIOfmEHgpOllUKSnhwtLlWFK4YnfQg==", + "dependencies": { + "path-browserify": "^1.0.1", + "semver": "^7.5.4", + "typescript-auto-import-cache": "^0.3.1", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-nls": "^5.2.0" + }, + "peerDependencies": { + "@volar/language-service": "~2.1.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-typescript-twoslash-queries": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/volar-service-typescript-twoslash-queries/-/volar-service-typescript-twoslash-queries-0.0.34.tgz", + "integrity": "sha512-XAY2YtWKUp6ht89gxt3L5Dr46LU45d/VlBkj1KXUwNlinpoWiGN4Nm3B6DRF3VoBThAnQgm4c7WD0S+5yTzh+w==", + "peerDependencies": { + "@volar/language-service": "~2.1.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/vscode-css-languageservice": { + "version": "6.2.13", + "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.2.13.tgz", + "integrity": "sha512-2rKWXfH++Kxd9Z4QuEgd1IF7WmblWWU7DScuyf1YumoGLkY9DW6wF/OTlhOyO2rN63sWHX2dehIpKBbho4ZwvA==", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-languageserver-types": "3.17.5", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/vscode-css-languageservice/node_modules/@vscode/l10n": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz", + "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==" + }, + "node_modules/vscode-html-languageservice": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.2.0.tgz", + "integrity": "sha512-cdNMhyw57/SQzgUUGSIMQ66jikqEN6nBNyhx5YuOyj9310+eY9zw8Q0cXpiKzDX8aHYFewQEXRnigl06j/TVwQ==", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-languageserver-types": "^3.17.5", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/vscode-html-languageservice/node_modules/@vscode/l10n": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz", + "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==" + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz", + "integrity": "sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" + }, + "node_modules/vscode-nls": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.2.0.tgz", + "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==" + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-pm": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/which-pm/-/which-pm-2.1.1.tgz", + "integrity": "sha512-xzzxNw2wMaoCWXiGE8IJ9wuPMU+EYhFksjHxrRT8kMT5SnocBPRg69YAMtyV4D12fP582RA+k3P8H9J5EMdIxQ==", + "dependencies": { + "load-yaml-file": "^0.2.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8.15" + } + }, + "node_modules/which-pm-runs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", + "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/widest-line/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.22.5.tgz", + "integrity": "sha512-+akaPo6a0zpVCCseDed504KBJUQpEW5QZw7RMneNmKw+fGaML1Z9tUNLnHHAC8x6dzVRO1eB2oEMyZRnuBZg7Q==", + "peerDependencies": { + "zod": "^3.22.4" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/docs/website/package.json b/docs/website/package.json new file mode 100644 index 00000000..1faea42e --- /dev/null +++ b/docs/website/package.json @@ -0,0 +1,25 @@ +{ + "name": "tutorial", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "npm run generate-docs && astro check && astro build", + "preview": "astro preview", + "astro": "astro", + "generate-when-method-docs" : "rm -rf src/content/docs/matching_requests && mkdir -p src/content/docs/matching_requests && node tools/generate-docs.cjs generated/docs.json ./templates/matching_requests src/content/docs/matching_requests", + "generate-then-method-docs" : "rm -rf src/content/docs/mocking_responses && mkdir -p src/content/docs/mocking_responses && node tools/generate-docs.cjs generated/docs.json ./templates/mocking_responses src/content/docs/mocking_responses", + "generate-docs": "npm run generate-when-method-docs && npm run generate-then-method-docs" + }, + "dependencies": { + "@astrojs/check": "^0.5.10", + "@astrojs/starlight": "^0.21.5", + "astro": "^4.3.5", + "sharp": "^0.32.5", + "typescript": "^5.4.4" + }, + "devDependencies": { + "handlebars": "^4.7.8" + } +} diff --git a/docs/website/public/favicon.svg b/docs/website/public/favicon.svg new file mode 100644 index 00000000..cba5ac14 --- /dev/null +++ b/docs/website/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/website/src/assets/houston.webp b/docs/website/src/assets/houston.webp new file mode 100644 index 00000000..930c1649 Binary files /dev/null and b/docs/website/src/assets/houston.webp differ diff --git a/docs/website/src/assets/landing.css b/docs/website/src/assets/landing.css new file mode 100644 index 00000000..416f9392 --- /dev/null +++ b/docs/website/src/assets/landing.css @@ -0,0 +1,45 @@ +:root { + --purple-hsl: 255, 60%, 60%; + --overlay-blurple: hsla(var(--purple-hsl), 0.2); +} + +:root[data-theme='light'] { + --purple-hsl: 255, 85%, 65%; +} + +[data-has-hero] .page { + background: + linear-gradient(215deg, var(--overlay-blurple), transparent 40%), + radial-gradient(var(--overlay-blurple), transparent 40%) no-repeat -60vw -40vh / 105vw 200vh, + radial-gradient(var(--overlay-blurple), transparent 65%) no-repeat 50% calc(100% + 20rem) / + 60rem 30rem; +} + +[data-has-hero] header { + border-bottom: 1px solid transparent; + background-color: transparent; + -webkit-backdrop-filter: blur(16px); + backdrop-filter: blur(16px); +} + +[data-has-hero] .hero > img { + filter: drop-shadow(0 0 3rem var(--overlay-blurple)); +} + +[data-has-hero] .hero { + display: block; + text-align: center; +} + +[data-has-hero] .hero h1 { + display: none; +} + +[data-has-hero] .hero .stack { + align-items: center; + text-align: center; +} + +[data-has-hero] .hero .tagline { + font-size: 1.5rem; +} \ No newline at end of file diff --git a/docs/website/src/assets/logo-dark.svg b/docs/website/src/assets/logo-dark.svg new file mode 100644 index 00000000..e6a0490a --- /dev/null +++ b/docs/website/src/assets/logo-dark.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/website/src/assets/logo-light.svg b/docs/website/src/assets/logo-light.svg new file mode 100644 index 00000000..e68eec0e --- /dev/null +++ b/docs/website/src/assets/logo-light.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/website/src/assets/logo.png b/docs/website/src/assets/logo.png new file mode 100644 index 00000000..e7e76158 Binary files /dev/null and b/docs/website/src/assets/logo.png differ diff --git a/docs/website/src/assets/logo_color.png b/docs/website/src/assets/logo_color.png new file mode 100644 index 00000000..3cf1159b Binary files /dev/null and b/docs/website/src/assets/logo_color.png differ diff --git a/docs/website/src/assets/logo_color_round.png b/docs/website/src/assets/logo_color_round.png new file mode 100644 index 00000000..09a7c243 Binary files /dev/null and b/docs/website/src/assets/logo_color_round.png differ diff --git a/docs/website/src/assets/logo_color_round_sm.png b/docs/website/src/assets/logo_color_round_sm.png new file mode 100644 index 00000000..93c5b6f9 Binary files /dev/null and b/docs/website/src/assets/logo_color_round_sm.png differ diff --git a/docs/website/src/assets/logo_color_sqare.png b/docs/website/src/assets/logo_color_sqare.png new file mode 100644 index 00000000..044d3427 Binary files /dev/null and b/docs/website/src/assets/logo_color_sqare.png differ diff --git a/docs/website/src/assets/logo_round_sm.png b/docs/website/src/assets/logo_round_sm.png new file mode 100644 index 00000000..d3e501b5 Binary files /dev/null and b/docs/website/src/assets/logo_round_sm.png differ diff --git a/docs/website/src/assets/logo_square.png b/docs/website/src/assets/logo_square.png new file mode 100644 index 00000000..5f08fa6e Binary files /dev/null and b/docs/website/src/assets/logo_square.png differ diff --git a/docs/website/src/content/config.ts b/docs/website/src/content/config.ts new file mode 100644 index 00000000..45f60b01 --- /dev/null +++ b/docs/website/src/content/config.ts @@ -0,0 +1,6 @@ +import { defineCollection } from 'astro:content'; +import { docsSchema } from '@astrojs/starlight/schema'; + +export const collections = { + docs: defineCollection({ schema: docsSchema() }), +}; diff --git a/docs/website/src/content/docs/getting_started/contributing.md b/docs/website/src/content/docs/getting_started/contributing.md new file mode 100644 index 00000000..8800a03b --- /dev/null +++ b/docs/website/src/content/docs/getting_started/contributing.md @@ -0,0 +1,6 @@ +--- +title: Contributing +description: Explains how individuals and companies can contribute to this project. +--- + +Everyone is welcome to contribute to this project. diff --git a/docs/website/src/content/docs/getting_started/fundamentals.mdx b/docs/website/src/content/docs/getting_started/fundamentals.mdx new file mode 100644 index 00000000..769caf5f --- /dev/null +++ b/docs/website/src/content/docs/getting_started/fundamentals.mdx @@ -0,0 +1,121 @@ +--- +title: Fundamental Concepts +description: A description of the fundamental concepts how to use httpmock. +--- + +## Why Mocking HTTP Services? + +Many applications rely on HTTP-based endpoints like REST APIs. During testing, developers often mock HTTP communication to avoid external dependencies. This is typically done by replacing the real HTTP client with a test-specific stub, allowing tests to focus on internal logic without executing actual HTTP requests. + +However, this approach leaves a significant portion of the client code untested, including whether it sends correct HTTP requests and is able to handle real HTTP responses, network issues, or error responses. + +`httpmock` bridges this gap by letting your tests use real client code to send requests to a mock server instead of the actual service. The mock server is set up to respond according to the specific needs of each test scenario. + +## Mocking Basics + +`httpmock` allows you to configure mock servers for a test scenario by calling its [`mock`](https://docs.rs/httpmock/latest/httpmock/struct.MockServer.html#method.mock) method: + +```rust +let server = httpmock::MockServer::start(); + +let mock = server.mock(|when, then| { + when.path("/hello"); + then.status(200); +}); +``` + +The [`mock`](https://docs.rs/httpmock/latest/httpmock/struct.MockServer.html#method.mock) method creates a +rule on the mock server that responds to HTTP requests when the request marches all specified criteria. + +The [`when`](https://docs.rs/httpmock/latest/httpmock/struct.When.html) variable is a builder-like structure that specifies when the mock server should respond. +In the example, it is configured to respond to requests with the path `/hello` (e.g., `GET http://localhost:8080/hello`). +Since no additional criteria are provided, the mock server will respond to any request with the path `/hello`, +regardless of the HTTP method (e.g., `GET`, `POST`) or request body content. + +The [`then`](https://docs.rs/httpmock/latest/httpmock/struct.Then.html) variable specifies the response details when a request meets the defined criteria. In the example, +it sets the response to return a status code `200 (OK)` without body, headers, or any other value. + +If a request does not meet all specified criteria, the mock server will automatically respond with status code `404 (Not Found)`. + +There is no limit to the number of mocks you can create on a mock server. You can set up as many as needed for your test scenarios. + + +## Sync and Async API + +The internal implementation of `httpmock` is entirely based on asynchronous communication. However, it provides both, a synchronous (blocking) and an asynchronous +(non-blocking) API. Asynchronous operations can be identified by the API's method signatures, typically ending with `_async` (such as [`mock_async`](https://docs.rs/httpmock/latest/httpmock/struct.MockServer.html#method.mock_async) +as the asynchronous variant of the method [`mock`](https://docs.rs/httpmock/latest/httpmock/struct.MockServer.html#method.mock)). +This pattern can be found throughout the entire API of `httpmock`. + +### Supported Async Executors + +`httpmock` is designed to be executor-agnostic. Our CI/CD pipeline includes dedicated tests to verify compatibility with at least the following async executors: + +- [tokio](https://docs.rs/tokio/latest/tokio/) +- [async-std](https://docs.rs/async-std/latest/async_std) +- [actix-rt](https://docs.rs/actix-rt/latest/actix_rt) + +## Test Execution and Pooling + +When you initialize `MockServer` instances in your tests (e.g., using +[`MockServer::start()`](https://docs.rs/httpmock/latest/httpmock/struct.MockServer.html#method.start)), `httpmock` allocates a mock server from a global server pool for exclusive use by your test. +This ensures that no other test can use the same mock server instance during test execution. + +Once the test ends, either successfully or with a failure, the mock server instance is automatically returned +to the global pool and made available for other tests. The library automatically manages all cleanup of the mock server. + +The default pool limit is set at 25 mock servers to avoid overloading the host system. +Although this limit is conservative, many host machines can handle additional servers. +Increasing this limit may not be necessary, depending on the parallelism settings of your test execution. +You can modify the pool size by setting the `HTTPMOCK_MAX_SERVERS` environment variable. + +When the global mock server pool is empty and all servers are in use, any test attempting to +instantiate a mock server will automatically be **blocked** until a server becomes available again. + +### Parallelism With Remote Mock Servers + +`httpmock` allows mock servers to be executed in standalone mode, where each server runs in its own process, +typically launched as a separate binary. Tests can connect to these "remote" mock servers using +[`MockServer::connect`](https://docs.rs/httpmock/latest/httpmock/struct.MockServer.html#method.connect), +[`MockServer::connect_async`](https://docs.rs/httpmock/latest/httpmock/struct.MockServer.html#method.connect_async), +and other [MockServer](https://docs.rs/httpmock/latest/httpmock/struct.MockServer.html) functions prefixed +with `connect`. + +When using any connect function in tests, the parallelism is set to 1, allowing only one test to access the remote +server at a time. This ensures that there are no conflicts with other tests during execution. + +## Environment Variables + +**Rust Tests** +- `HTTPMOCK_MAX_SERVERS`: Sets the maximum number of mock servers in the global mock server pool. +- `HTTPMOCK_REQUEST_HISTORY_LIMIT`: Mock servers keep a history of all requests that have been received in a test. +The history works like a ring buffer, removing the oldest request from the history once it reaches the configured maximum. +The request history is used for call assertions to verify that a request has been received that matches certain criteria, +such as in [`Mock::assert`](https://docs.rs/httpmock/latest/httpmock/struct.Mock.html#method.assert). +By default, this number is set to 100. +- `HTTPMOCK_HOST` / `HTTPMOCK_PORT`: Sets the hostname/port that should be used connect to remote mock servers when using +[`MockServer::connect_from_env()`](https://docs.rs/httpmock/latest/httpmock/struct.MockServer.html#method.connect_from_env) +or [`MockServer::connect_from_env_async()`](https://docs.rs/httpmock/latest/httpmock/struct.MockServer.html#method.connect_from_env_async). + +**Standalone Binary**: +- `HTTPMOCK_EXPOSE`: If set to `1`, the mock server will be configured to accept external connections (binds to `0.0.0.0`). Otherwise, only connections from your local machine will be accepted. +- `HTTPMOCK_MOCK_FILES_DIR`: The location where the mock server should look for mock definition files (YAML format). +- `HTTPMOCK_DISABLE_ACCESS_LOG`: When set to `0`, the mock server will not log incoming requests. + +## Cargo Features + +The crate provides the following Cargo features: + +- `cookies`: Enables request matchers for parsing and matching values in cookies +- `proxy`: Enables the mock server to function as a proxy server +- `record`: Enables functionality to record requests and responses (most useful in combination with the `proxy` feature). Enables reading mock specifications from YAML files (e.g., recorded responses) +- `https`: Enables the mock server to provide a unified port for both, HTTP and HTTPS. Attention: This feature is experimental. Hence, there are no guarantees that this feature will work. +- `http2`: Enables mock server support for HTTP2 +- `standalone`: Enables standalone mode +- `remote`: Allows to connect to remote (standalone) mock servers +- `remote-https`: Enables communication to remote (standalone) mock servers via `HTTPS` +- `color`: enables colorful output in standalone mode + +For example, the command `cargo test --features=remote` enables the functionality in your tests to communicate with remote standalone mock servers. + + diff --git a/docs/website/src/content/docs/getting_started/quick_introduction.mdx b/docs/website/src/content/docs/getting_started/quick_introduction.mdx new file mode 100644 index 00000000..d0bc759a --- /dev/null +++ b/docs/website/src/content/docs/getting_started/quick_introduction.mdx @@ -0,0 +1,89 @@ +--- +title: Quick Introduction +description: A quick tutorial explaining how to get started using httpmock. +tableOfContents: false +--- + +`httpmock` allows you to mock HTTP services in your Rust tests, such as third-party APIs, authentication providers, or data sources. + +The following is a basic example of how you can use `httpmock` to mock HTTP endpoints in your tests. + +Let's start off by adding the `httpmock` crate to `Cargo.toml`: + +```toml +[dev-dependencies] +httpmock = "0.8" +``` + +You can then use it as follows: + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + + + + ```rust + use httpmock::prelude::*; + + // Start a lightweight mock server. + let server = MockServer::start(); + + // Create a mock on the server. + let hello_mock = server.mock(|when, then| { + when.method("GET") + .path("/translate") + .query_param("word", "hello"); + then.status(200) + .header("content-type", "text/html; charset=UTF-8") + .body("Привет"); + }); + + // Send an HTTP request to the mock server. This simulates your code. + let response = reqwest::blocking::get(server.url("/translate?word=hello")).unwrap(); + + // Ensure the specified mock was called. + hello_mock.assert(); + + // Ensure the mock server did respond as specified. + assert_eq!(response.status(), 200); + ``` + + + ```rust + use httpmock::prelude::*; + + // Start a lightweight mock server. + let server = MockServer::start_async().await; + + // Create a mock on the server. + let hello_mock = server.mock_async(|when, then| { + when.method("GET") + .path("/translate") + .query_param("word", "hello"); + then.status(200) + .header("content-type", "text/html; charset=UTF-8") + .body("Привет"); + }).await; + + // Send an HTTP request to the mock server. This simulates your code. + let client = reqwest::Client::new(); + let response = client.get(server.url("/translate?word=hello")).send().await.unwrap(); + + // Ensure the specified mock was called exactly one time (or fail with a + // detailed error description). + hello_mock.assert(); + + // Ensure the mock server did respond as specified. + assert_eq!(response.status(), 200); + ``` + + + + + +The above example will spin up a lightweight HTTP mock server and configure it to respond to all `GET` requests +to path `/translate` with query parameter `word=hello`. The corresponding HTTP response will contain the text body +`Привет`. + +If the mock server receives a request that does not match the specified criteria +(i.e., the request does not have path `/translate` with query parameter `word=hello`), +then the mock server will respond with status code `404 (Not Found)`. diff --git a/docs/website/src/content/docs/getting_started/resources.md b/docs/website/src/content/docs/getting_started/resources.md new file mode 100644 index 00000000..fe605748 --- /dev/null +++ b/docs/website/src/content/docs/getting_started/resources.md @@ -0,0 +1,30 @@ +--- +title: Resources +description: Describes what resources are available to developers. +--- + +To learn more about this project, please refer to the following resources: +- Issue Tracker: https://github.com/alexliesenfeld/httpmock/issues +- Community Forum: https://github.com/alexliesenfeld/httpmock/discussions +- API Reference: https://docs.rs/httpmock/latest/httpmock/ +- GitHub Repository: https://github.com/alexliesenfeld/httpmock +- Official Website: https://alexliesenfeld.github.io/httpmock +- Changelog: https://github.com/alexliesenfeld/httpmock/blob/master/CHANGELOG.md +- Discord Server: https://discord.gg/QrjhRh7A + +## Getting Help + +For assistance with `httpmock`, please join the conversation on +[GitHub Discussions](https://github.com/alexliesenfeld/httpmock/discussions). + +## Create an Issue + +If you think you found a bug or have a feature suggestion, please open a new +[issue on GitHub](https://github.com/alexliesenfeld/httpmock/issues). + +## Contributing + +We welcome contributions from everyone in any capacity. +Please review [our code of conduct](https://github.com/alexliesenfeld/httpmock/blob/master/CODE_OF_CONDUCT.md) +for guidelines on how to contribute. To start contributing, please refer to our +[issue tracker on GitHub](https://github.com/alexliesenfeld/httpmock/issues). diff --git a/docs/website/src/content/docs/getting_started/why_httpmock.md b/docs/website/src/content/docs/getting_started/why_httpmock.md new file mode 100644 index 00000000..ebd0f3bc --- /dev/null +++ b/docs/website/src/content/docs/getting_started/why_httpmock.md @@ -0,0 +1,11 @@ +--- +title: Example Guide +description: A guide in my new Starlight docs site. +--- + +Guides lead a user through a specific task they want to accomplish, often with a sequence of steps. +Writing a good guide requires thinking about what your users are trying to do. + +## Further reading + +- Read [about how-to guides](https://diataxis.fr/how-to-guides/) in the Diátaxis framework diff --git a/docs/website/src/content/docs/index.mdx b/docs/website/src/content/docs/index.mdx new file mode 100644 index 00000000..64975653 --- /dev/null +++ b/docs/website/src/content/docs/index.mdx @@ -0,0 +1,56 @@ +--- +title: httpmock +description: Get started building your docs site with Starlight. +template: splash +hero: + tagline: Simple yet powerful HTTP mocking library for Rust + image: + alt: The httpmock logo + dark: ../../assets/logo-dark.svg + light: ../../assets/logo-light.svg + actions: + - text: Get started + link: /httpmock/getting_started/quick_introduction/ + icon: right-arrow + variant: primary + - text: View on GitHub + link: https://github.com/alexliesenfeld/httpmock + icon: github + - text: Support this project + link: https://buymeacoffee.com/alexliesenfeld + icon: external + +head: + - tag: style + content: | + a{} +--- + +import { Card, CardGrid } from '@astrojs/starlight/components'; + + + + + If you call HTTP services or APIs in your application, `httpmock` makes it **easy** to mock their responses in your tests. + + + `httpmock` offers a range of advanced features, including **record and playback**, **proxy mode**, **standalone mode**, **HTTPS support**, **sharing mocks**, and many more. + + + **Record requests** and responses when interacting with a real service and **play them back** in your tests. + + + `httpmock` is a **Rust testing library**, but it also includes a **standalone server** that runs in a separate process, such as a Docker container. + + + One of the primary goals of `httpmock` is to be **actually helpful** by explaining the reasons when tests fail. It comes with useful logging, error messages, and an advanced request comparison algorithm. + + + `httpmock` allows you to **create and share mocked responses** using YAML-files. + + + Includes an **extensive set of built-in matchers** that let you define rules for matching requests based on method, path, headers, query parameters, body, and more, or use **custom matchers** for complex matching logic. + + + + diff --git a/docs/website/src/content/docs/miscellaneous/faq.mdx b/docs/website/src/content/docs/miscellaneous/faq.mdx new file mode 100644 index 00000000..a32cb4d2 --- /dev/null +++ b/docs/website/src/content/docs/miscellaneous/faq.mdx @@ -0,0 +1,13 @@ +--- +title: FAQ +description: Lists frequently asked questions. +--- + +### Can I start multiple mock servers in one Rust test function? +Yes, you can start multiple mock servers. However, `httpmock` uses a server pool with a default limit of 25 +servers. You can adjust this limit by setting the `HTTPMOCK_MAX_SERVERS` environment variable. + +### In which order are mocks evaluated? +When the `httpmock` server receives a request that matches multiple mocks, they are evaluated in +reverse order of their definition. This means the most recently defined mock takes precedence and will be used. + diff --git a/docs/website/src/content/docs/record-and-playback/playback.mdx b/docs/website/src/content/docs/record-and-playback/playback.mdx new file mode 100644 index 00000000..038968de --- /dev/null +++ b/docs/website/src/content/docs/record-and-playback/playback.mdx @@ -0,0 +1,39 @@ +--- +title: Playback +description: Explains how requests can be replayed. +--- +import codeExamples from "../../../../generated/example_tests.json" +import { Code } from '@astrojs/starlight/components'; + +After creating a recording, you can replay it by loading it into a mock server instance using the +`httpmock` Rust API as follows: + +```rust +// ... + +// Save the recording to +// "target/httpmock/recordings/github-torvalds-scenario_.yaml". +let target_path = recording + .save("github-torvalds-scenario") + .expect("cannot store scenario on disk"); + +let playback_server = MockServer::start(); + +// Play back the recorded interactions from the file. +playback_server.playback(target_path); +``` + +After calling [`MockServer::playback`](https://docs.rs/httpmock/latest/httpmock/struct.MockServer.html#method.playback), +the recording will be loaded into the mock server. This allows all previously recorded requests to act as matching +criteria, similar to how you configure normal mocks using +[`MockServer::mock`](https://docs.rs/httpmock/latest/httpmock/struct.MockServer.html#method.mock) with the +[`When`](https://docs.rs/httpmock/latest/httpmock/struct.When.html) structure. + +Hereafter, whenever the mock server receives a request that matches any of the recorded requests, it will +respond with the corresponding recorded response. + +## Full Example +The following example demonstrates how you can use the forwarding feature to record and playback +requests sent to the GitHub API and the responses it returns. + + \ No newline at end of file diff --git a/docs/website/src/content/docs/record-and-playback/recording.mdx b/docs/website/src/content/docs/record-and-playback/recording.mdx new file mode 100644 index 00000000..e8c1ba16 --- /dev/null +++ b/docs/website/src/content/docs/record-and-playback/recording.mdx @@ -0,0 +1,89 @@ +--- +title: Recording +description: Explains how requests can be recorded. +--- + +import codeExamples from "../../../../generated/example_tests.json" +import { Code } from '@astrojs/starlight/components'; +import { Aside } from '@astrojs/starlight/components'; + +`httpmock` provides functionality to record both requests to third-party services and their responses. +There are two strategies how you can achieve that: Forwarding and Proxy. + +## Forwarding Strategy + +The forwarding feature is the easier method for intercepting and recording responses from third-party services. +However, it requires you to change the client’s base URL to direct requests to the mock server’s address. + +When using the forwarding strategy, your client sends requests to an `httpmock` mock server. +The mock server forwards requests that match the criteria defined in the +[When](https://docs.rs/httpmock/latest/httpmock/struct.When.html) structure to a predefined target base URL. + +Let's have a look at a basic forwarding example: +```rust +// Initialize the mock server for testing +let server = MockServer::start(); + +// Configure the server to forward all requests to the GitHub API, +// instead of using mocked responses. The 'when' configuration allows +// setting conditions for when forwarding should occur, using the same +// structure familiar from creating mocks. +server.forward_to("https://github.com", |rule| { + rule.filter(|when| { + when.any_request(); // Ensure all requests are forwarded. + }); +}); +``` + +

+ +You can use the forwarding functionality to record requests sent to the remote service. + +### Full Example + +The following example demonstrates how you can use the forwarding feature to record requests sent to the GitHub API and +the responses it returns. + + + +## Proxy Strategy + + + +The proxy feature in `httpmock`, while functional on its own, is particularly useful for recording +in scenarios where modifying or injecting the base URL used by the client is not possible. + +Many SDKs, APIs, and HTTP clients support proxy server configuration. For example, +the reqwest crate allows you to set up a proxy server with the following configuration: + +```rust +// Create a client using the reqwest crate with a configured proxy +let client = Client::builder() + .proxy(reqwest::Proxy::all("my-proxy-server:8080").unwrap()) + .build() + .unwrap(); + +// Send a GET request and unwrap the result +let response = client.get("https://github.com").send().unwrap(); +``` + +In this example, each request is routed through the proxy server rather than directly to the requested domain host. +The proxy server then tunnels or forwards the request to the target host, which is `github.com` in this case. + +When configured as a proxy, `httpmock` can intercept, record, and forward both requests and responses. + + + +### Full Example + + + diff --git a/docs/website/src/content/docs/server/debugging.mdx b/docs/website/src/content/docs/server/debugging.mdx new file mode 100644 index 00000000..d2944fd1 --- /dev/null +++ b/docs/website/src/content/docs/server/debugging.mdx @@ -0,0 +1,86 @@ +--- +title: Debugging +description: Describes what features are available to make debugging easier. +--- +import { Aside } from '@astrojs/starlight/components'; + +## Test Failure Output + +When your tests don't send the expected data, `httpmock` tries to provide as much information as possible about what +exactly is missing or different. However, to see details about unmet expectations, you need to use one of the +following assertion methods: + +- [Mock::assert](https://docs.rs/httpmock/latest/httpmock/struct.Mock.html#method.assert) / [Mock::assert_async](https://docs.rs/httpmock/latest/httpmock/struct.Mock.html#method.assert_async) +- [Mock::assert_calls](https://docs.rs/httpmock/latest/httpmock/struct.Mock.html#method.assert_calls) / [Mock::assert_calls_async](https://docs.rs/httpmock/latest/httpmock/struct.Mock.html#method.assert_calls_async) + +Let's have a look at an example: +```rust +#[test] +fn getting_started_testxx() { + use httpmock::prelude::*; + + // Start a lightweight mock server. + let server = MockServer::start(); + + // Create a mock on the server. + let hello_mock = server.mock(|when, then| { + when.method("GET") + .path("/translate") + .query_param("word", "hello-rustaceans"); + then.status(200) + .header("content-type", "text/html; charset=UTF-8") + .body("Привет"); + }); + + // Send an HTTP request to the mock server. This simulates your code. + let response = reqwest::blocking::get(server.url("/translate?word=hello")) + .unwrap(); + + // Ensure the specified mock was called. This will fail and print output + // with an explanation of what was expected and provided. + hello_mock.assert(); +} +``` + +Notice how `mock.assert()` is used to verify that the mock you defined earlier has been called **exactly once**. +If you expect a different number of calls, use [Mock::assert_calls](https://docs.rs/httpmock/latest/httpmock/struct.Mock.html#method.assert_calls). + +Since the path of the request that was actually sent to the mock server differs from the expected one, +`hello_mock.assert()` will panic and cause the test to fail with the following message: + +```bash +0 of 1 expected requests matched the mock specification. +Here is a comparison with the most similar unmatched request (request number 1): + +------------------------------------------------------------ +1 : Query Parameter Mismatch +------------------------------------------------------------ +Expected: + key [equals] word + value [equals] hello-rustaceans + +Received (most similar query parameter): + word=hello + +All received query parameter values: + 1. word=hello + +Matcher: query_param +Docs: https://docs.rs/httpmock/0.8.0/httpmock/struct.When.html#method.query_param +``` + +## Logs + +`httpmock` logs through the log crate, so you can see detailed log output about its behavior. +This output is useful for investigating issues, like figuring out why a request doesn't match a mock definition. + +The debug log level is usually the most helpful, but you can use trace to get even more details. + + + + diff --git a/docs/website/src/content/docs/server/https.mdx b/docs/website/src/content/docs/server/https.mdx new file mode 100644 index 00000000..4b70e3be --- /dev/null +++ b/docs/website/src/content/docs/server/https.mdx @@ -0,0 +1,80 @@ +--- +title: HTTPS +description: Describes how the mock server can be configured to support HTTPS +--- +import { Aside } from '@astrojs/starlight/components'; + + + +By default, `httpmock` does not enable HTTPS support for testing. However, you can enable it on demand by using the +Cargo feature `https`. When this feature is enabled, `httpmock` automatically uses HTTPS for all internal communication +between the Rust API and the mock server, whether it’s a remote standalone server or a local +[`MockServer`](https://docs.rs/httpmock/latest/httpmock/struct.MockServer.html) instance. +It also allows your client to send HTTPS requests to the mock server. + +## Unified Port +`httpmock` uses a unified port approach for both HTTP and HTTPS communication. This means you don’t need to change +the port or modify anything in your Rust tests to switch to HTTPS. Your client can send requests using HTTPS as needed, +and `httpmock` will automatically detect HTTPS traffic on the port and transition from HTTP to HTTPS without +any additional configuration. + +## CA Certificate +Since HTTPS requires the use of certificates, you'll need to accept the `httpmock` CA certificate +in your client settings or, more conveniently, in your system preferences when using HTTPS. +You can find the `httpmock` CA certificate in the `httpmock` +[GitHub repository](https://github.com/alexliesenfeld/httpmock/blob/master/certs) (ca.pem file). + + +`httpmock` uses its CA certificate to generate domain-specific certificates for your tests. For instance, if you want +to mock requests from https://wikipedia.org (such as when using the `httpmock` proxy feature), the mock server +will generate and cache a certificate for that domain based on the `httpmock` CA certificate. Since your system +trusts the CA certificate, the self-signed, domain-specific certificate for Wikipedia will also be trusted +automatically. + +### Trusting the CA Certificate + +Here is how you can add the `httpmock` CA crtificate to your system. + +#### MacOS + +```bash +# Download the CA certificate +curl -o httpmock-ca.crt https://github.com/alexliesenfeld/httpmock/raw/master/certs/ca.pem + +# Open Keychain Access manually or use the open command +open /Applications/Utilities/Keychain\ Access.app + +# Import the certificate: +# - Drag the downloaded 'httpmock-ca.crt' file into the "System" keychain. +# - Set the certificate to "Always Trust" under "Get Info". +``` + +#### Windows +``` +# Download the CA certificate +Invoke-WebRequest -Uri "https://github.com/alexliesenfeld/httpmock/raw/master/certs/ca.pem" -OutFile "C:\Path\To\httpmock-ca.crt" + +# Open the Certificate Manager +# Press 'Win + R', type 'mmc', and press Enter. + +# Import the certificate: +# - In MMC, go to File > Add/Remove Snap-in, select "Certificates", and click "Add". +# - Choose "Computer account" and then "Local computer". +# - Under "Trusted Root Certification Authorities", right-click on "Certificates" and choose "All Tasks > Import". +# - Browse to the downloaded 'httpmock-ca.crt' file and complete the import wizard. +``` + +#### Ubuntu +```bash +# Clone the repository to get the CA certificate +git clone git@github.com:alexliesenfeld/httpmock.git + +# Copy the certificate to the system's trusted certificates directory +sudo cp httpmock/certs/ca.pem /usr/local/share/ca-certificates/httpmock.crt + +# Update the system's trusted certificates +sudo update-ca-certificates +``` diff --git a/docs/website/src/content/docs/server/standalone.mdx b/docs/website/src/content/docs/server/standalone.mdx new file mode 100644 index 00000000..de1cfdbf --- /dev/null +++ b/docs/website/src/content/docs/server/standalone.mdx @@ -0,0 +1,120 @@ +--- +title: Standalone Server +description: Describes how to set up and use a standalone mock server. +--- +import { Aside } from '@astrojs/starlight/components'; + +You can use `httpmock` to run a standalone mock server in a separate process, such as a Docker container. +This setup allows the mock server to be accessible to multiple applications, not just within your Rust tests. +It’s particularly useful for system or end-to-end tests that require mocked services. + +By deploying `httpmock` as an independent service, it becomes available outside of Rust tests, providing +fake responses for services that are unavailable for testing. + +Even when a mock server is running outside your Rust tests in a separate process, such as a Docker container, +you can still use it within your Rust tests just like a local MockServer instance +(see [Connecting to Standalone Mock Servers](#connecting-to-standalone-mock-servers)). This enables your Rust tests +to set up remote, standalone servers for larger, federated end-to-end test scenarios, with your Rust tests +acting as the test runner. + +With this feature, `httpmock` can be used as universal HTTP mocking tool that is useful in all stages +of the development lifecycle. + +## Running Standalone Mock Servers + +### Docker Image +Although you can build the mock server in standalone mode yourself, it is easiest to use the accompanying +[Docker image](https://hub.docker.com/r/alexliesenfeld/httpmock) hosted on Docker Hub. + +You can run it as follows: +```bash +docker run alexliesenfeld/httpmock +```` + +#### Build Docker Image + +If you want to build the Docker image yourself, you can clone the `httpmock` GitHub repository and +build it yourself using the Dockerfile that is contained in the project root directory: + +```bash +# Clone the repository +git clone git@github.com:alexliesenfeld/httpmock.git + +# Build the Docker image +docker build -t my-httpmock-image . + +# Start a Docker container +docker run my-httpmock-image +```` + +### Build Binary +Alternatively, you can clone the GitHub repository and build a binary from the projects root directory +and execute it as follows: + +```bash +# Clone the repository +git clone git@github.com:alexliesenfeld/httpmock.git + +# Build a standalone mock server binary +cargo build --release --all-features + +# Execute the binary +./target/release/httpmock +``` + +### Environment Variables + +Please refer to the [Environment Variables](/getting_started/fundamentals/#environment-variables) section for information what environment variables are available when +using a standalone mock server. + +## Connecting to Standalone Mock Servers + +To be able to use the standalone server from within your tests, you need to change how an instance of the MockServer +instance is created. Instead of using [`MockServer::start`](https://docs.rs/httpmock/latest/httpmock/struct.MockServer.html#method.start), +you need to connect to a remote server by using one of the `connect` methods (such as +[`MockServer::connect`](https://docs.rs/httpmock/latest/httpmock/struct.MockServer.html#method.connect) or +[`MockServer::connect_from_env`](https://docs.rs/httpmock/latest/httpmock/struct.MockServer.html#method.connect_from_env)). +Note: These are only available with the remote feature enabled. + +## Sequential Test Execution + +To prevent interference with other tests, only one test function can use the remote server at the same time. +This means that test functions may be blocked when connecting to the remote server until it becomes free again. +This is in contrast to tests that use a local mock server where parallel test execution is possible, because +each test uses its own mock server. + + + +## Usage Without Rust + +You can use a standalone mock server independently, without needing Rust to configure mock behavior. `httpmock` allows +you to define mocks using YAML files, which follow a similar when/then pattern as the Rust API. Here's an example +that defines two mocks (mock definitions are separated using the triple dash separator): + +```yaml +when: + method: GET + path: /static-mock/examples/simple +then: + status: 200 + json_body: '{ "response" : "hello" }' +--- +when: + method: POST + path: /static-mock/examples/submit +then: + status: 201 + json_body: '{ "status" : "created" }' +``` + + + +Please refer to [this example file](https://github.com/alexliesenfeld/httpmock/blob/master/tests/resources/static_yaml_mock.yaml), +which includes many of the usable fields. \ No newline at end of file diff --git a/docs/website/src/env.d.ts b/docs/website/src/env.d.ts new file mode 100644 index 00000000..acef35f1 --- /dev/null +++ b/docs/website/src/env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/docs/website/templates/matching_requests/body.md b/docs/website/templates/matching_requests/body.md new file mode 100644 index 00000000..fa1eee4a --- /dev/null +++ b/docs/website/templates/matching_requests/body.md @@ -0,0 +1,82 @@ +--- +title: Body +description: Using request matchers to specify which requests should respond. TODO +--- + +This section describes matcher functions designed to target and match HTTP request body content in incoming HTTP requests. + +### body +{{{docs.when.body}}} + +### body_not +{{{docs.when.body_not}}} + +### body_includes +{{{docs.when.body_includes}}} + +### body_excludes +{{{docs.when.body_excludes}}} + +### body_prefix +{{{docs.when.body_prefix}}} + +### body_prefix_not +{{{docs.when.body_prefix_not}}} + +### body_suffix +{{{docs.when.body_suffix}}} + +### body_suffix_not +{{{docs.when.body_suffix_not}}} + +### body_matches +{{{docs.when.body_matches}}} + +## JSON Body + +### json_body +{{{docs.when.json_body}}} + +### json_body_includes +{{{docs.when.json_body_includes}}} + +### json_body_excludes +{{{docs.when.json_body_excludes}}} + +## URL Encoded Body + +### form_urlencoded_tuple +{{{docs.when.form_urlencoded_tuple}}} + +### form_urlencoded_tuple_not +{{{docs.when.form_urlencoded_tuple_not}}} + +### form_urlencoded_tuple_exists +{{{docs.when.form_urlencoded_tuple_exists}}} + +### form_urlencoded_tuple_missing +{{{docs.when.form_urlencoded_tuple_missing}}} + +### form_urlencoded_tuple_includes +{{{docs.when.form_urlencoded_tuple_includes}}} + +### form_urlencoded_tuple_excludes +{{{docs.when.form_urlencoded_tuple_excludes}}} + +### form_urlencoded_tuple_prefix +{{{docs.when.form_urlencoded_tuple_prefix}}} + +### form_urlencoded_tuple_prefix_not +{{{docs.when.form_urlencoded_tuple_prefix_not}}} + +### form_urlencoded_tuple_suffix +{{{docs.when.form_urlencoded_tuple_suffix}}} + +### form_urlencoded_tuple_suffix_not +{{{docs.when.form_urlencoded_tuple_suffix_not}}} + +### form_urlencoded_tuple_matches +{{{docs.when.form_urlencoded_tuple_matches}}} + +### form_urlencoded_tuple_key_value_count +{{{docs.when.form_urlencoded_tuple_key_value_count}}} \ No newline at end of file diff --git a/docs/website/templates/matching_requests/cookies.md b/docs/website/templates/matching_requests/cookies.md new file mode 100644 index 00000000..5834def1 --- /dev/null +++ b/docs/website/templates/matching_requests/cookies.md @@ -0,0 +1,44 @@ +--- +title: Cookie +description: Using request matchers to specify which requests should respond. TODO +--- + +This section describes matcher functions designed to target and match cookies headers in incoming HTTP requests. + +> **Attention:** To use these matchers, enable the cookies feature by adding `--features=cookies` to your Cargo command. For example: `cargo test --features=cookies`. + +## cookie +{{{docs.when.cookie}}} + +## cookie_not +{{{docs.when.cookie_not}}} + +## cookie_exists +{{{docs.when.cookie_exists}}} + +## cookie_missing +{{{docs.when.cookie_missing}}} + +## cookie_includes +{{{docs.when.cookie_includes}}} + +## cookie_excludes +{{{docs.when.cookie_excludes}}} + +## cookie_prefix +{{{docs.when.cookie_prefix}}} + +## cookie_prefix_not +{{{docs.when.cookie_prefix_not}}} + +## cookie_suffix +{{{docs.when.cookie_suffix}}} + +## cookie_suffix_not +{{{docs.when.cookie_suffix_not}}} + +## cookie_matches +{{{docs.when.cookie_matches}}} + +## cookie_count +{{{docs.when.cookie_count}}} diff --git a/docs/website/templates/matching_requests/custom.mdx b/docs/website/templates/matching_requests/custom.mdx new file mode 100644 index 00000000..69dda4dc --- /dev/null +++ b/docs/website/templates/matching_requests/custom.mdx @@ -0,0 +1,21 @@ +--- +title: Request Matchers +description: Using request matchers to specify which requests should respond. TODO +--- + +import { Aside } from '@astrojs/starlight/components'; + +This section describes matcher functions that enable developers to implement custom matchers. +These matchers execute user-defined code to determine if a request meets specific criteria. + + + +## is_true +{{{docs.when.is_true}}} + +## is_false +{{{docs.when.is_false}}} diff --git a/docs/website/templates/matching_requests/headers.md b/docs/website/templates/matching_requests/headers.md new file mode 100644 index 00000000..c622cb0f --- /dev/null +++ b/docs/website/templates/matching_requests/headers.md @@ -0,0 +1,42 @@ +--- +title: Headers +description: Using request matchers to specify which requests should respond. TODO +--- + +This section describes matcher functions designed to target and match HTTP headers in incoming HTTP requests. + +## header +{{{docs.when.header}}} + +## header_not +{{{docs.when.header_not}}} + +## header_exists +{{{docs.when.header_exists}}} + +## header_missing +{{{docs.when.header_missing}}} + +## header_includes +{{{docs.when.header_includes}}} + +## header_excludes +{{{docs.when.header_excludes}}} + +## header_prefix +{{{docs.when.header_prefix}}} + +## header_prefix_not +{{{docs.when.header_prefix_not}}} + +## header_suffix +{{{docs.when.header_suffix}}} + +## header_suffix_not +{{{docs.when.header_suffix_not}}} + +## header_matches +{{{docs.when.header_matches}}} + +## header_count +{{{docs.when.header_count}}} diff --git a/docs/website/templates/matching_requests/host.md b/docs/website/templates/matching_requests/host.md new file mode 100644 index 00000000..f86624ea --- /dev/null +++ b/docs/website/templates/matching_requests/host.md @@ -0,0 +1,34 @@ +--- +title: Request Matchers +description: Using request matchers to specify which requests should respond. TODO +--- + +This section describes matcher functions designed to target and match the host in incoming HTTP requests. +These matchers are especially useful when using the proxy and record-and-playback features of `httpmock`. + +## host +{{{docs.when.host}}} + +## host_not +{{{docs.when.host_not}}} + +## host_includes +{{{docs.when.host_includes}}} + +## host_excludes +{{{docs.when.host_excludes}}} + +## host_prefix +{{{docs.when.host_prefix}}} + +## host_prefix_not +{{{docs.when.host_prefix_not}}} + +## host_suffix +{{{docs.when.host_suffix}}} + +## host_suffix_not +{{{docs.when.host_suffix_not}}} + +## host_matches +{{{docs.when.host_matches}}} diff --git a/docs/website/templates/matching_requests/method.md b/docs/website/templates/matching_requests/method.md new file mode 100644 index 00000000..ec3c279e --- /dev/null +++ b/docs/website/templates/matching_requests/method.md @@ -0,0 +1,13 @@ +--- +title: Method +description: Using request matchers to specify which requests should respond. TODO +--- + +This section describes matcher functions designed to target and match the method in incoming HTTP requests, +such as `GET`, `POST`, etc. + +## method +{{{docs.when.method}}} + +## method_not +{{{docs.when.method_not}}} diff --git a/docs/website/templates/matching_requests/path.md b/docs/website/templates/matching_requests/path.md new file mode 100644 index 00000000..a7fa19a5 --- /dev/null +++ b/docs/website/templates/matching_requests/path.md @@ -0,0 +1,33 @@ +--- +title: Path +description: Using request matchers to specify which requests should respond. TODO +--- + +This section describes matcher functions designed to target and match the request path, such as `http://localhost:8080/my-path`. + +## path +{{{docs.when.path}}} + +## path_not +{{{docs.when.path_not}}} + +## path_includes +{{{docs.when.path_includes}}} + +## path_excludes +{{{docs.when.path_excludes}}} + +## path_prefix +{{{docs.when.path_prefix}}} + +## path_prefix_not +{{{docs.when.path_prefix_not}}} + +## path_suffix +{{{docs.when.path_suffix}}} + +## path_suffix_not +{{{docs.when.path_suffix_not}}} + +## path_matches +{{{docs.when.path_matches}}} diff --git a/docs/website/templates/matching_requests/port.md b/docs/website/templates/matching_requests/port.md new file mode 100644 index 00000000..137b5e92 --- /dev/null +++ b/docs/website/templates/matching_requests/port.md @@ -0,0 +1,13 @@ +--- +title: Request Matchers +description: Using request matchers to specify which requests should respond. TODO +--- + +This section describes matcher functions designed to target and match the TCP port in incoming HTTP requests. +These matchers are especially useful when using the proxy and record-and-playback features of `httpmock`. + +## port +{{{docs.when.port}}} + +## port_not +{{{docs.when.port_not}}} diff --git a/docs/website/templates/matching_requests/query.md b/docs/website/templates/matching_requests/query.md new file mode 100644 index 00000000..542dd84b --- /dev/null +++ b/docs/website/templates/matching_requests/query.md @@ -0,0 +1,42 @@ +--- +title: Query Parameters +description: Using request matchers to specify which requests should respond. TODO +--- + +This section describes matcher functions designed to target and match query parameters in incoming HTTP requests. + +## query_param +{{{docs.when.query_param}}} + +## query_param_not +{{{docs.when.query_param_not}}} + +## query_param_exists +{{{docs.when.query_param_exists}}} + +## query_param_missing +{{{docs.when.query_param_missing}}} + +## query_param_includes +{{{docs.when.query_param_includes}}} + +## query_param_excludes +{{{docs.when.query_param_excludes}}} + +## query_param_prefix +{{{docs.when.query_param_prefix}}} + +## query_param_prefix_not +{{{docs.when.query_param_prefix_not}}} + +## query_param_suffix +{{{docs.when.query_param_suffix}}} + +## query_param_suffix_not +{{{docs.when.query_param_suffix_not}}} + +## query_param_matches +{{{docs.when.query_param_matches}}} + +## query_param_count +{{{docs.when.query_param_count}}} diff --git a/docs/website/templates/matching_requests/scheme.md b/docs/website/templates/matching_requests/scheme.md new file mode 100644 index 00000000..2023a2ec --- /dev/null +++ b/docs/website/templates/matching_requests/scheme.md @@ -0,0 +1,15 @@ +--- +title: Request Matchers +description: Using request matchers to specify which requests should respond. TODO +--- + +This section describes matcher functions designed to target and match the scheme in incoming HTTP requests +(such as `http` or `https` as in `https://localhost:8080`). + +These matchers are especially useful when using the proxy and record-and-playback features of `httpmock`. + +## scheme +{{{docs.when.scheme}}} + +## scheme_not +{{{docs.when.scheme_not}}} diff --git a/docs/website/templates/mocking_responses/all.md b/docs/website/templates/mocking_responses/all.md new file mode 100644 index 00000000..b9cf8827 --- /dev/null +++ b/docs/website/templates/mocking_responses/all.md @@ -0,0 +1,24 @@ +--- +title: Body +description: Using request matchers to specify which requests should respond. TODO +--- + +This section explains functions designed to set values in the response when a request meets all specified criteria. + +### status +{{{docs.then.status}}} + +### body +{{{docs.then.body}}} + +### body_from_file +{{{docs.then.body_from_file}}} + +### json_body +{{{docs.then.json_body}}} + +### json_body_obj +{{{docs.then.json_body_obj}}} + +### header +{{{docs.then.header}}} diff --git a/docs/website/templates/mocking_responses/delay.md b/docs/website/templates/mocking_responses/delay.md new file mode 100644 index 00000000..69dd6abe --- /dev/null +++ b/docs/website/templates/mocking_responses/delay.md @@ -0,0 +1,9 @@ +--- +title: Network Delay +description: Demonstrates how to simulate network delay. +--- + +This section describes functions designed to simulate network issues, such as latency (delay). + +### delay +{{{docs.then.delay}}} diff --git a/docs/website/tools/generate-docs.cjs b/docs/website/tools/generate-docs.cjs new file mode 100644 index 00000000..12292054 --- /dev/null +++ b/docs/website/tools/generate-docs.cjs @@ -0,0 +1,56 @@ +const fs = require('fs'); +const path = require('path'); + +const Handlebars = require('handlebars'); +Handlebars.registerHelper('eq', (a, b) => a === b); + +function readJsonFile(filename) { + const rawData = fs.readFileSync(filename); + return JSON.parse(rawData); +} + +function deepenMarkdownHeaders(markdownText) { + return markdownText.split('\n').map(line => { + // Check if the line starts with one or more '#' + if (line.startsWith('#')) { + return '###' + line; // Add two more '#' to deepen the header + } + return line; + }).join('\n'); +} + +function generateMarkdownDocs() { + const methodDocs = readJsonFile(process.argv[2]); + Object.keys(methodDocs.then).forEach(key => { + methodDocs.then[key] = deepenMarkdownHeaders(methodDocs.then[key]); + }); + Object.keys(methodDocs.when).forEach(key => { + methodDocs.when[key] = deepenMarkdownHeaders(methodDocs.when[key]); + }); + + const templatesDir = process.argv[3]; + + fs.readdir(templatesDir, (err, files) => { + if (err) return console.error(err); + + files.forEach(file => { + const filePath = path.join(templatesDir, file); + fs.readFile(filePath, 'utf8', (err, content) => { + if (err) return console.error(err); + + const template = Handlebars.compile(content); + const result = template({ docs: methodDocs }); + + const fileName = `${process.argv[4]}/${file}`; + if (fs.existsSync(fileName)) { + fs.unlinkSync(fileName); + } + + console.log("writing: " + fileName) + fs.writeFileSync(fileName, result); + }); + }); + }); +} + +generateMarkdownDocs(); \ No newline at end of file diff --git a/docs/website/tsconfig.json b/docs/website/tsconfig.json new file mode 100644 index 00000000..77da9dd0 --- /dev/null +++ b/docs/website/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "astro/tsconfigs/strict" +} \ No newline at end of file diff --git a/gitlab-ci.yml b/gitlab-ci.yml new file mode 100644 index 00000000..b79523b2 --- /dev/null +++ b/gitlab-ci.yml @@ -0,0 +1,10 @@ +stages: + - build + +rust-latest: + stage: build + image: rust:latest + script: + - make setup + - cargo build --verbose + - make test-powerset diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..4e727a08 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +reorder_imports = true \ No newline at end of file diff --git a/scripts/generate_tarpaulin_config.sh b/scripts/generate_tarpaulin_config.sh new file mode 100755 index 00000000..dc868fe3 --- /dev/null +++ b/scripts/generate_tarpaulin_config.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Populate the features variable +features=$(awk '/^\[features\]/ {flag=1; next} /^\[/ {flag=0} flag {print}' Cargo.toml | \ + sed 's/^[ \t]*//;s/[ \t]*$//' | \ + grep -v '^\s*#' | \ + grep -v '^\s*$' | \ + cut -d'=' -f1 | \ + grep -vE "(default|color)") + +# Convert the multiline string to an array +IFS=$'\n' read -r -d '' -a feature_array <<< "$features" + +generate_powerset() { + local items=("$@") + local len=${#items[@]} + local total=$((1 << len)) + + for ((i=0; i, + state: Arc, } impl LocalMockServerAdapter { - pub fn new(addr: SocketAddr, local_state: Arc) -> Self { - LocalMockServerAdapter { addr, local_state } + pub fn new(addr: SocketAddr, local_state: Arc) -> Self { + LocalMockServerAdapter { + addr, + state: local_state, + } } } #[async_trait] impl MockServerAdapter for LocalMockServerAdapter { + async fn ping(&self) -> Result<(), ServerAdapterError> { + let response = simple_http_get_request(&self.addr, "/__httpmock__/ping").await?; + + if !response.contains("200 OK") { + return Err(PingError(format!( + "Expected server response body to contain '200 OK' but it didn't. Body: '{}'", + response + ))); + } + + Ok(()) + } + fn host(&self) -> String { self.addr.ip().to_string() } @@ -43,72 +63,156 @@ impl MockServerAdapter for LocalMockServerAdapter { &self.addr } - async fn create_mock(&self, mock: &MockDefinition) -> Result { - let id = add_new_mock(&self.local_state, mock.clone(), false)?; - Ok(MockRef::new(id)) + async fn reset(&self) -> Result<(), ServerAdapterError> { + self.state.reset(); + Ok(()) } - async fn fetch_mock(&self, mock_id: usize) -> Result { - match read_one_mock(&self.local_state, mock_id)? { - Some(mock) => Ok(mock), - None => Err("Cannot find mock".to_string()), - } + async fn create_mock(&self, mock: &MockDefinition) -> Result { + let active_mock = self + .state + .add_mock(mock.clone(), false) + .map_err(|e| UpstreamError(e.to_string()))?; + Ok(active_mock) } - async fn delete_mock(&self, mock_id: usize) -> Result<(), String> { - let deleted = delete_one_mock(&self.local_state, mock_id)?; - if deleted { - Ok(()) - } else { - Err("Mock could not deleted".to_string()) - } + async fn fetch_mock(&self, mock_id: usize) -> Result { + let mock = self + .state + .read_mock(mock_id) + .map_err(|e| UpstreamError(e.to_string()))? + .ok_or_else(|| MockNotFound(mock_id))?; + Ok(mock) + } + + async fn delete_mock(&self, mock_id: usize) -> Result<(), ServerAdapterError> { + self.state + .delete_mock(mock_id) + .map_err(|e| UpstreamError(format!("Cannot delete mock: {:?}", e)))?; + Ok(()) } - async fn delete_all_mocks(&self) -> Result<(), String> { - delete_all_mocks(&self.local_state); + async fn delete_all_mocks(&self) -> Result<(), ServerAdapterError> { + self.state.delete_all_mocks(); Ok(()) } - async fn verify(&self, mock_rr: &RequestRequirements) -> Result, String> { - verify(&self.local_state, mock_rr) + async fn verify( + &self, + mock_rr: &RequestRequirements, + ) -> Result, ServerAdapterError> { + let closest_match = self + .state + .verify(mock_rr) + .map_err(|e| UpstreamError(format!("Cannot delete mock: {:?}", e)))?; + Ok(closest_match) } - async fn delete_history(&self) -> Result<(), String> { - delete_history(&self.local_state); + async fn delete_history(&self) -> Result<(), ServerAdapterError> { + self.state.delete_history(); Ok(()) } - async fn ping(&self) -> Result<(), String> { - let addr = self.addr.to_string(); + async fn create_forwarding_rule( + &self, + config: ForwardingRuleConfig, + ) -> Result { + Ok(self.state.create_forwarding_rule(config)) + } - let mut stream = TcpStream::connect(&addr) - .await - .map_err(|err| format!("Cannot connect to mock server: {}", err))?; + async fn delete_forwarding_rule(&self, id: usize) -> Result<(), ServerAdapterError> { + self.state.delete_forwarding_rule(id); + Ok(()) + } - let request = format!( - "GET /__httpmock__/ping HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n", - addr - ); + async fn delete_all_forwarding_rules(&self) -> Result<(), ServerAdapterError> { + self.state.delete_all_forwarding_rules(); + Ok(()) + } - stream - .write_all(request.as_bytes()) - .await - .map_err(|err| format!("Cannot send request to mock server: {}", err))?; + async fn create_proxy_rule( + &self, + config: ProxyRuleConfig, + ) -> Result { + Ok(self.state.create_proxy_rule(config)) + } - let mut buf = vec![0u8; 1024]; - stream - .read(&mut buf) - .await - .map_err(|err| format!("Cannot read response from mock server: {}", err))?; + async fn delete_proxy_rule(&self, id: usize) -> Result<(), ServerAdapterError> { + self.state.delete_proxy_rule(id); + Ok(()) + } - let response = String::from_utf8_lossy(&buf); - if !response.contains("200 OK") { - return Err(format!( - "Unexpected mock server response. Expected '{}' to contain '200 OK'", - response - )); - } + async fn delete_all_proxy_rules(&self) -> Result<(), ServerAdapterError> { + self.state.delete_all_proxy_rules(); + Ok(()) + } + async fn create_recording( + &self, + config: RecordingRuleConfig, + ) -> Result { + Ok(self.state.create_recording(config)) + } + + async fn delete_recording(&self, id: usize) -> Result<(), ServerAdapterError> { + self.state.delete_recording(id); + Ok(()) + } + + async fn delete_all_recordings(&self) -> Result<(), ServerAdapterError> { + self.state.delete_all_recordings(); Ok(()) } + + #[cfg(feature = "record")] + async fn export_recording(&self, id: usize) -> Result, ServerAdapterError> { + Ok(self + .state + .export_recording(id) + .map_err(|err| UpstreamError(err.to_string()))?) + } + + #[cfg(feature = "record")] + async fn create_mocks_from_recording<'a>( + &self, + recording_file_content: &'a str, + ) -> Result, ServerAdapterError> { + Ok(self + .state + .load_mocks_from_recording(recording_file_content) + .map_err(|err| UpstreamError(err.to_string()))?) + } +} + +pub async fn simple_http_get_request( + addr: &SocketAddr, + path: &str, +) -> Result { + let addr = addr.to_string(); + + let mut stream = TcpStream::connect(&addr) + .await + .map_err(|err| UpstreamError(err.to_string()))?; + + let request = format!( + "GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n", + path, addr + ); + + stream + .write_all(request.as_bytes()) + .await + .map_err(|err| UpstreamError(err.to_string()))?; + + let mut buf = vec![0u8; 1024]; + let bytes_read = stream + .read(&mut buf) + .await + .map_err(|err| UpstreamError(err.to_string()))?; + + buf.resize(bytes_read, 0); + + let response = String::from_utf8_lossy(&buf); + + Ok(response.to_string()) } diff --git a/src/api/adapter/mod.rs b/src/api/adapter/mod.rs index 7aca9396..f1ad3f1f 100644 --- a/src/api/adapter/mod.rs +++ b/src/api/adapter/mod.rs @@ -1,80 +1,88 @@ -use std::net::SocketAddr; -use std::str::FromStr; -use std::sync::Arc; -use std::time::Duration; +use std::{net::SocketAddr, str::FromStr}; use async_trait::async_trait; +use bytes::Bytes; use serde::{Deserialize, Serialize}; -use crate::common::data::{ActiveMock, ClosestMatch, MockDefinition, MockRef, RequestRequirements}; -use crate::server::web::handlers::{ - add_new_mock, delete_all_mocks, delete_history, delete_one_mock, read_one_mock, verify, -}; +use crate::common::data::{ActiveForwardingRule, ActiveMock, ActiveProxyRule}; + +use crate::common::data::{ActiveRecording, ClosestMatch, MockDefinition, RequestRequirements}; pub mod local; -#[cfg(feature = "remote")] -pub mod standalone; - -/// Type alias for [regex::Regex](../regex/struct.Regex.html). -pub type Regex = regex::Regex; - -/// Represents an HTTP method. -#[derive(Serialize, Deserialize, Debug)] -pub enum Method { - GET, - HEAD, - POST, - PUT, - DELETE, - CONNECT, - OPTIONS, - TRACE, - PATCH, -} +use crate::common::data::{ForwardingRuleConfig, ProxyRuleConfig, RecordingRuleConfig}; -impl FromStr for Method { - type Err = String; - - fn from_str(input: &str) -> Result { - match input { - "GET" => Ok(Method::GET), - "HEAD" => Ok(Method::HEAD), - "POST" => Ok(Method::POST), - "PUT" => Ok(Method::PUT), - "DELETE" => Ok(Method::DELETE), - "CONNECT" => Ok(Method::CONNECT), - "OPTIONS" => Ok(Method::OPTIONS), - "TRACE" => Ok(Method::TRACE), - "PATCH" => Ok(Method::PATCH), - _ => Err(format!("Invalid HTTP method {}", input)), - } - } -} +use thiserror::Error; -impl From<&str> for Method { - fn from(value: &str) -> Self { - value.parse().expect("Cannot parse HTTP method") - } +#[derive(Error, Debug)] +pub enum ServerAdapterError { + #[error("mock with ID {0} not found")] + MockNotFound(usize), + #[error("invalid mock definition: {0}")] + InvalidMockDefinitionError(String), + #[error("cannot serialize JSON: {0}")] + JsonSerializationError(serde_json::error::Error), + #[error("cannot deserialize JSON: {0}")] + JsonDeserializationError(serde_json::error::Error), + #[error("adapter error: {0}")] + UpstreamError(String), + #[error("cannot ping mock server: {0}")] + PingError(String), + #[error("unknown error")] + Unknown, } -impl std::fmt::Display for Method { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - std::fmt::Debug::fmt(self, f) - } -} +#[cfg(feature = "remote")] +pub mod remote; #[async_trait] pub trait MockServerAdapter { + async fn ping(&self) -> Result<(), ServerAdapterError>; fn host(&self) -> String; fn port(&self) -> u16; fn address(&self) -> &SocketAddr; - async fn create_mock(&self, mock: &MockDefinition) -> Result; - async fn fetch_mock(&self, mock_id: usize) -> Result; - async fn delete_mock(&self, mock_id: usize) -> Result<(), String>; - async fn delete_all_mocks(&self) -> Result<(), String>; - async fn verify(&self, rr: &RequestRequirements) -> Result, String>; - async fn delete_history(&self) -> Result<(), String>; - async fn ping(&self) -> Result<(), String>; + + async fn reset(&self) -> Result<(), ServerAdapterError>; + + async fn create_mock(&self, mock: &MockDefinition) -> Result; + async fn fetch_mock(&self, mock_id: usize) -> Result; + async fn delete_mock(&self, mock_id: usize) -> Result<(), ServerAdapterError>; + async fn delete_all_mocks(&self) -> Result<(), ServerAdapterError>; + + async fn verify( + &self, + rr: &RequestRequirements, + ) -> Result, ServerAdapterError>; + async fn delete_history(&self) -> Result<(), ServerAdapterError>; + + async fn create_forwarding_rule( + &self, + config: ForwardingRuleConfig, + ) -> Result; + async fn delete_forwarding_rule(&self, mock_id: usize) -> Result<(), ServerAdapterError>; + async fn delete_all_forwarding_rules(&self) -> Result<(), ServerAdapterError>; + + async fn create_proxy_rule( + &self, + config: ProxyRuleConfig, + ) -> Result; + async fn delete_proxy_rule(&self, mock_id: usize) -> Result<(), ServerAdapterError>; + async fn delete_all_proxy_rules(&self) -> Result<(), ServerAdapterError>; + + async fn create_recording( + &self, + mock: RecordingRuleConfig, + ) -> Result; + async fn delete_recording(&self, id: usize) -> Result<(), ServerAdapterError>; + async fn delete_all_recordings(&self) -> Result<(), ServerAdapterError>; + + #[cfg(feature = "record")] + async fn export_recording(&self, id: usize) -> Result, ServerAdapterError>; + + #[cfg(feature = "record")] + async fn create_mocks_from_recording<'a>( + &self, + recording_file_content: &'a str, + ) -> Result, ServerAdapterError>; } diff --git a/src/api/adapter/remote.rs b/src/api/adapter/remote.rs new file mode 100644 index 00000000..ad53b32d --- /dev/null +++ b/src/api/adapter/remote.rs @@ -0,0 +1,557 @@ +use crate::common::data::{ForwardingRuleConfig, ProxyRuleConfig, RecordingRuleConfig}; +use std::{borrow::Borrow, net::SocketAddr, sync::Arc}; + +use crate::api::{ + adapter::{ + ServerAdapterError, + ServerAdapterError::{ + InvalidMockDefinitionError, JsonDeserializationError, JsonSerializationError, + UpstreamError, + }, + }, + MockServerAdapter, +}; +use async_trait::async_trait; +use bytes::Bytes; +use http::{Request, StatusCode}; + +use crate::common::{ + data::{ + ActiveForwardingRule, ActiveMock, ActiveProxyRule, ActiveRecording, ClosestMatch, + MockDefinition, RequestRequirements, + }, + http::HttpClient, +}; + +pub struct RemoteMockServerAdapter { + addr: SocketAddr, + http_client: Arc, +} + +impl RemoteMockServerAdapter { + pub fn new(addr: SocketAddr, http_client: Arc) -> Self { + Self { addr, http_client } + } + + fn validate_request_requirements( + &self, + requirements: &RequestRequirements, + ) -> Result<(), ServerAdapterError> { + match requirements.is_true { + Some(_) => Err(InvalidMockDefinitionError( + "Anonymous function request matchers are not supported when using a remote mock server".to_string(), + )), + None => Ok(()), + } + } + + async fn do_request(&self, req: Request) -> Result<(u16, String), ServerAdapterError> { + let (code, body_bytes) = self.do_request_raw(req).await?; + + let body = + String::from_utf8(body_bytes.to_vec()).map_err(|e| UpstreamError(e.to_string()))?; + + Ok((code, body)) + } + + async fn do_request_raw( + &self, + req: Request, + ) -> Result<(u16, Bytes), ServerAdapterError> { + let mut response = self + .http_client + .send(req) + .await + .map_err(|e| UpstreamError(e.to_string()))?; + + Ok((response.status().as_u16(), response.body().clone())) + } +} + +#[async_trait] +impl MockServerAdapter for RemoteMockServerAdapter { + async fn ping(&self) -> Result<(), ServerAdapterError> { + let request = Request::builder() + .method("GET") + .uri(format!("http://{}/__httpmock__/ping", &self.addr)) + .body(Bytes::new()) + .map_err(|e| UpstreamError(e.to_string()))?; + + let (status, body) = self.do_request(request).await?; + + if status != StatusCode::OK { + return Err(UpstreamError(format!( + "Could not ping the mock server. Expected response status 202 but was {} (response body = '{}')", + status, body + ))); + } + + Ok(()) + } + + fn host(&self) -> String { + self.addr.ip().to_string() + } + + fn port(&self) -> u16 { + self.addr.port() + } + + fn address(&self) -> &SocketAddr { + &self.addr + } + + async fn reset(&self) -> Result<(), ServerAdapterError> { + let request = Request::builder() + .method("DELETE") + .uri(format!("http://{}/__httpmock__/state", &self.addr)) + .body(Bytes::new()) + .map_err(|e| UpstreamError(e.to_string()))?; + + let (status, body) = self.do_request(request).await?; + + if status != StatusCode::NO_CONTENT { + return Err(UpstreamError(format!( + "Could not reset the mock server. Expected response status 204 but was {} (response body = '{}')", + status, body + ))); + } + + Ok(()) + } + + async fn create_mock(&self, mock: &MockDefinition) -> Result { + self.validate_request_requirements(&mock.request)?; + + let json = serde_json::to_string(mock).map_err(|e| JsonSerializationError(e))?; + + let request = Request::builder() + .method("POST") + .uri(format!("http://{}/__httpmock__/mocks", &self.address())) + .header("content-type", "application/json") + .body(Bytes::from(json)) + .map_err(|e| UpstreamError(e.to_string()))?; + + let (status, body) = self.do_request(request).await?; + + if status != StatusCode::CREATED.as_u16() { + return Err(UpstreamError(format!( + "Could not create mock. Expected response status 201 but was {} (response body = '{}')", + status, body + ))); + } + + let response: ActiveMock = + serde_json::from_str(&body).map_err(|e| JsonDeserializationError(e))?; + + Ok(response) + } + + async fn fetch_mock(&self, mock_id: usize) -> Result { + let request = Request::builder() + .method("GET") + .uri(format!( + "http://{}/__httpmock__/mocks/{}", + &self.address(), + mock_id + )) + .body(Bytes::new()) + .map_err(|e| UpstreamError(e.to_string()))?; + + let (status, body) = self.do_request(request).await?; + + if status != StatusCode::OK { + return Err(UpstreamError(format!( + "Could not fetch mock from the mock server. Expected response status 200 but was {} (response body = '{}')", + status, body + ))); + } + + let response: ActiveMock = + serde_json::from_str(&body).map_err(|e| JsonDeserializationError(e))?; + + Ok(response) + } + + async fn delete_mock(&self, mock_id: usize) -> Result<(), ServerAdapterError> { + let request = Request::builder() + .method("DELETE") + .uri(format!( + "http://{}/__httpmock__/mocks/{}", + &self.address(), + mock_id + )) + .body(Bytes::new()) + .map_err(|e| UpstreamError(e.to_string()))?; + + let (status, body) = self.do_request(request).await?; + + if status != StatusCode::NO_CONTENT { + return Err(UpstreamError(format!( + "Could not delete mock from the mock server. Expected response status 204 but was {} (response body = '{}')", + status, body + ))); + } + + Ok(()) + } + + async fn delete_all_mocks(&self) -> Result<(), ServerAdapterError> { + let request = Request::builder() + .method("DELETE") + .uri(format!("http://{}/__httpmock__/mocks", &self.address())) + .body(Bytes::new()) + .map_err(|e| UpstreamError(e.to_string()))?; + + let (status, body) = self.do_request(request).await?; + + if status != StatusCode::NO_CONTENT { + return Err(UpstreamError(format!( + "Could not delete all mocks from the mock server. Expected response status 204 but was {} (response body = '{}')", + status, body + ))); + } + + Ok(()) + } + + async fn verify( + &self, + requirements: &RequestRequirements, + ) -> Result, ServerAdapterError> { + let json = serde_json::to_string(requirements).map_err(|e| JsonSerializationError(e))?; + + let request = Request::builder() + .method("POST") + .uri(format!("http://{}/__httpmock__/verify", &self.address())) + .header("content-type", "application/json") + .body(Bytes::from(json)) + .map_err(|e| UpstreamError(e.to_string()))?; + + let (status, body) = self.do_request(request).await?; + + if status == StatusCode::NOT_FOUND { + return Ok(None); + } + + if status != StatusCode::OK { + return Err(UpstreamError(format!( + "Could not verify mock. Expected response status 200 but was {} (response body = '{}')", + status, body + ))); + } + + let response: ClosestMatch = + serde_json::from_str(&body).map_err(|e| JsonDeserializationError(e))?; + + Ok(Some(response)) + } + + async fn delete_history(&self) -> Result<(), ServerAdapterError> { + let request = Request::builder() + .method("DELETE") + .uri(format!("http://{}/__httpmock__/history", &self.address())) + .body(Bytes::new()) + .map_err(|e| UpstreamError(e.to_string()))?; + + let (status, body) = self.do_request(request).await?; + + if status != StatusCode::NO_CONTENT { + return Err(UpstreamError(format!( + "Could not delete request history from the mock server. Expected response status 204 but was {} (response body = '{}')", + status, body + ))); + } + + Ok(()) + } + + async fn create_forwarding_rule( + &self, + config: ForwardingRuleConfig, + ) -> Result { + self.validate_request_requirements(&config.request_requirements)?; + + let json = serde_json::to_string(&config).map_err(|e| JsonSerializationError(e))?; + + let request = Request::builder() + .method("POST") + .uri(format!( + "http://{}/__httpmock__/forwarding_rules", + &self.address() + )) + .header("content-type", "application/json") + .body(Bytes::from(json)) + .map_err(|e| UpstreamError(e.to_string()))?; + + let (status, body) = self.do_request(request).await?; + + if status != StatusCode::CREATED.as_u16() { + return Err(UpstreamError(format!( + "Could not create forwarding rule. Expected response status 201 but was {} (response body = '{}')", + status, body + ))); + } + + let response: ActiveForwardingRule = + serde_json::from_str(&body).map_err(|e| JsonDeserializationError(e))?; + + Ok(response) + } + + async fn delete_forwarding_rule(&self, id: usize) -> Result<(), ServerAdapterError> { + let request = Request::builder() + .method("DELETE") + .uri(format!( + "http://{}/__httpmock__/forwarding_rules/{}", + &self.address(), + id + )) + .body(Bytes::new()) + .map_err(|e| UpstreamError(e.to_string()))?; + + let (status, body) = self.do_request(request).await?; + + if status != StatusCode::NO_CONTENT { + return Err(UpstreamError(format!( + "Could not delete forwarding rule from the mock server. Expected response status 204 but was {} (response body = '{}')", + status, body + ))); + } + + Ok(()) + } + + async fn delete_all_forwarding_rules(&self) -> Result<(), ServerAdapterError> { + let request = Request::builder() + .method("DELETE") + .uri(format!( + "http://{}/__httpmock__/forwarding_rules", + &self.address() + )) + .body(Bytes::new()) + .map_err(|e| UpstreamError(e.to_string()))?; + + let (status, body) = self.do_request(request).await?; + + if status != StatusCode::NO_CONTENT { + return Err(UpstreamError(format!( + "Could not delete all forwarding rules from the mock server. Expected response status 204 but was {} (response body = '{}')", + status, body + ))); + } + + Ok(()) + } + + async fn create_proxy_rule( + &self, + config: ProxyRuleConfig, + ) -> Result { + self.validate_request_requirements(&config.request_requirements)?; + + let json = serde_json::to_string(&config).map_err(|e| JsonSerializationError(e))?; + + let request = Request::builder() + .method("POST") + .uri(format!( + "http://{}/__httpmock__/proxy_rules", + &self.address() + )) + .header("content-type", "application/json") + .body(Bytes::from(json)) + .map_err(|e| UpstreamError(e.to_string()))?; + + let (status, body) = self.do_request(request).await?; + + if status != StatusCode::CREATED.as_u16() { + return Err(UpstreamError(format!( + "Could not create proxy rule. Expected response status 201 but was {} (response body = '{}')", + status, body + ))); + } + + let response: ActiveProxyRule = + serde_json::from_str(&body).map_err(|e| JsonDeserializationError(e))?; + + Ok(response) + } + + async fn delete_proxy_rule(&self, id: usize) -> Result<(), ServerAdapterError> { + let request = Request::builder() + .method("DELETE") + .uri(format!( + "http://{}/__httpmock__/proxy_rules/{}", + &self.address(), + id + )) + .body(Bytes::new()) + .map_err(|e| UpstreamError(e.to_string()))?; + + let (status, body) = self.do_request(request).await?; + + if status != StatusCode::NO_CONTENT { + return Err(UpstreamError(format!( + "Could not delete proxy rule from the mock server. Expected response status 204 but was {} (response body = '{}')", + status, body + ))); + } + + Ok(()) + } + + async fn delete_all_proxy_rules(&self) -> Result<(), ServerAdapterError> { + let request = Request::builder() + .method("DELETE") + .uri(format!( + "http://{}/__httpmock__/proxy_rules", + &self.address() + )) + .body(Bytes::new()) + .map_err(|e| UpstreamError(e.to_string()))?; + + let (status, body) = self.do_request(request).await?; + + if status != StatusCode::NO_CONTENT { + return Err(UpstreamError(format!( + "Could not delete all proxy rules from the mock server. Expected response status 204 but was {} (response body = '{}')", + status, body + ))); + } + + Ok(()) + } + + async fn create_recording( + &self, + config: RecordingRuleConfig, + ) -> Result { + self.validate_request_requirements(&config.request_requirements)?; + + let json = serde_json::to_string(&config).map_err(|e| JsonSerializationError(e))?; + + let request = Request::builder() + .method("POST") + .uri(format!( + "http://{}/__httpmock__/recordings", + &self.address() + )) + .header("content-type", "application/json") + .body(Bytes::from(json)) + .map_err(|e| UpstreamError(e.to_string()))?; + + let (status, body) = self.do_request(request).await?; + + if status != StatusCode::CREATED.as_u16() { + return Err(UpstreamError(format!( + "Could not create recording. Expected response status 201 but was {} (response body = '{}')", + status, body + ))); + } + + let response: ActiveRecording = + serde_json::from_str(&body).map_err(|e| JsonDeserializationError(e))?; + + Ok(response) + } + + async fn delete_recording(&self, id: usize) -> Result<(), ServerAdapterError> { + let request = Request::builder() + .method("DELETE") + .uri(format!( + "http://{}/__httpmock__/recordings/{}", + &self.address(), + id + )) + .body(Bytes::new()) + .map_err(|e| UpstreamError(e.to_string()))?; + + let (status, body) = self.do_request(request).await?; + + if status != StatusCode::NO_CONTENT { + return Err(UpstreamError(format!( + "Could not delete recording from the mock server. Expected response status 204 but was {} (response body = '{}')", + status, body + ))); + } + + Ok(()) + } + + async fn delete_all_recordings(&self) -> Result<(), ServerAdapterError> { + let request = Request::builder() + .method("DELETE") + .uri(format!( + "http://{}/__httpmock__/recordings", + &self.address() + )) + .body(Bytes::new()) + .map_err(|e| UpstreamError(e.to_string()))?; + + let (status, body) = self.do_request(request).await?; + + if status != StatusCode::NO_CONTENT { + return Err(UpstreamError(format!( + "Could not delete all recordings from the mock server. Expected response status 204 but was {} (response body = '{}')", + status, body + ))); + } + + Ok(()) + } + + #[cfg(feature = "record")] + async fn export_recording(&self, id: usize) -> Result, ServerAdapterError> { + let request = Request::builder() + .method("GET") + .uri(format!( + "http://{}/__httpmock__/recordings/{}", + &self.address(), + id + )) + .body(Bytes::new()) + .map_err(|e| UpstreamError(e.to_string()))?; + + let (status, body) = self.do_request_raw(request).await?; + + if status == StatusCode::NOT_FOUND { + return Ok(None); + } else if status != StatusCode::OK { + return Err(UpstreamError(format!( + "Could not fetch mock from the mock server. Expected response status 200 but was {}", + status + ))); + } + + Ok(Some(body)) + } + + #[cfg(feature = "record")] + async fn create_mocks_from_recording<'a>( + &self, + recording_file_content: &'a str, + ) -> Result, ServerAdapterError> { + let request = Request::builder() + .method("POST") + .uri(format!( + "http://{}/__httpmock__/recordings", + &self.address(), + )) + .body(Bytes::from(recording_file_content.to_owned())) + .map_err(|e| UpstreamError(e.to_string()))?; + + let (status, body) = self.do_request(request).await?; + + if status != StatusCode::OK { + return Err(UpstreamError(format!( + "Could not create mocks from recording. Expected response status 200 but was {}", + status + ))); + } + + let response: Vec = + serde_json::from_str(&body).map_err(|e| JsonDeserializationError(e))?; + + Ok(response) + } +} diff --git a/src/api/adapter/standalone.rs b/src/api/adapter/standalone.rs deleted file mode 100644 index fab076a0..00000000 --- a/src/api/adapter/standalone.rs +++ /dev/null @@ -1,298 +0,0 @@ -use std::borrow::Borrow; -use std::net::SocketAddr; -use std::sync::Arc; -use std::time::Duration; - -use crate::api::MockServerAdapter; -use async_trait::async_trait; -use isahc::config::Configurable; -use isahc::{AsyncReadResponseExt, Request, ResponseExt}; - -pub type InternalHttpClient = isahc::HttpClient; - -use crate::common::data::{ActiveMock, ClosestMatch, MockDefinition, MockRef, RequestRequirements}; - -#[derive(Debug)] -pub struct RemoteMockServerAdapter { - addr: SocketAddr, - http_client: Arc, -} - -impl RemoteMockServerAdapter { - pub fn new(addr: SocketAddr) -> Self { - Self { - addr, - http_client: build_http_client(), - } - } - - fn validate_mock(&self, mock: &MockDefinition) -> Result<(), String> { - if mock.request.matchers.is_some() { - return Err( - "Anonymous function request matchers are not supported when using a remote mock server".to_string(), - ); - } - Ok(()) - } -} - -#[async_trait] -impl MockServerAdapter for RemoteMockServerAdapter { - fn host(&self) -> String { - self.addr.ip().to_string() - } - - fn port(&self) -> u16 { - self.addr.port() - } - - fn address(&self) -> &SocketAddr { - &self.addr - } - - async fn create_mock(&self, mock: &MockDefinition) -> Result { - // Check if the request can be sent via HTTP - self.validate_mock(mock).expect("Cannot create mock"); - - // Serialize to JSON - let json = match serde_json::to_string(mock) { - Err(err) => return Err(format!("cannot serialize mock object to JSON: {}", err)), - Ok(json) => json, - }; - - // Send the request to the mock server - let request_url = format!("http://{}/__httpmock__/mocks", &self.address()); - let request = Request::builder() - .method("POST") - .uri(request_url) - .header("content-type", "application/json") - .body(json) - .unwrap(); - - let (status, body) = match execute_request(request, &self.http_client).await { - Err(err) => return Err(format!("cannot send request to mock server: {}", err)), - Ok(sb) => sb, - }; - - // Evaluate the response status - if status != 201 { - return Err(format!( - "Could not create mock. Mock server response: status = {}, message = {}", - status, body - )); - } - - // Create response object - let response: serde_json::Result = serde_json::from_str(&body); - if let Err(err) = response { - return Err(format!("Cannot deserialize mock server response: {}", err)); - } - - Ok(response.unwrap()) - } - - async fn fetch_mock(&self, mock_id: usize) -> Result { - // Send the request to the mock server - let request_url = format!("http://{}/__httpmock__/mocks/{}", &self.address(), mock_id); - let request = Request::builder() - .method("GET") - .uri(request_url) - .body("".to_string()) - .unwrap(); - - let (status, body) = match execute_request(request, &self.http_client).await { - Err(err) => return Err(format!("Cannot send request to mock server: {}", err)), - Ok(r) => r, - }; - - // Evaluate response status code - if status != 200 { - return Err(format!( - "Could not create mock. Mock server response: status = {}, message = {}", - status, body - )); - } - - // Create response object - let response: serde_json::Result = serde_json::from_str(&body); - if let Err(err) = response { - return Err(format!("Cannot deserialize mock server response: {}", err)); - } - - Ok(response.unwrap()) - } - - async fn delete_mock(&self, mock_id: usize) -> Result<(), String> { - // Send the request to the mock server - let request_url = format!("http://{}/__httpmock__/mocks/{}", &self.address(), mock_id); - let request = Request::builder() - .method("DELETE") - .uri(request_url) - .body("".to_string()) - .unwrap(); - - let (status, body) = match execute_request(request, &self.http_client).await { - Err(err) => return Err(format!("Cannot send request to mock server: {}", err)), - Ok(sb) => sb, - }; - - // Evaluate response status code - if status != 202 { - return Err(format!( - "Could not delete mocks from server (status = {}, message = {})", - status, body - )); - } - - Ok(()) - } - - async fn delete_all_mocks(&self) -> Result<(), String> { - // Send the request to the mock server - let request_url = format!("http://{}/__httpmock__/mocks", &self.address()); - let request = Request::builder() - .method("DELETE") - .uri(request_url) - .body("".to_string()) - .unwrap(); - - let (status, body) = match execute_request(request, &self.http_client).await { - Err(err) => return Err(format!("Cannot send request to mock server: {}", err)), - Ok(sb) => sb, - }; - - // Evaluate response status code - if status != 202 { - return Err(format!( - "Could not delete mocks from server (status = {}, message = {})", - status, body - )); - } - - Ok(()) - } - - async fn verify(&self, mock_rr: &RequestRequirements) -> Result, String> { - // Serialize to JSON - let json = match serde_json::to_string(mock_rr) { - Err(err) => return Err(format!("Cannot serialize mock object to JSON: {}", err)), - Ok(json) => json, - }; - - // Send the request to the mock server - let request_url = format!("http://{}/__httpmock__/verify", &self.address()); - let request = Request::builder() - .method("POST") - .uri(request_url) - .header("content-type", "application/json") - .body(json) - .unwrap(); - - let (status, body) = match execute_request(request, &self.http_client).await { - Err(err) => return Err(format!("Cannot send request to mock server: {}", err)), - Ok(sb) => sb, - }; - - // Evaluate the response status - if status == 404 { - return Ok(None); - } - - if status != 200 { - return Err(format!( - "Could not execute verification (status = {}, message = {})", - status, body - )); - } - - // Create response object - let response: serde_json::Result = serde_json::from_str(&body); - if let Err(err) = response { - return Err(format!("cannot deserialize mock server response: {}", err)); - } - - Ok(Some(response.unwrap())) - } - - async fn delete_history(&self) -> Result<(), String> { - // Send the request to the mock server - let request_url = format!("http://{}/__httpmock__/history", &self.address()); - let request = Request::builder() - .method("DELETE") - .uri(request_url) - .body("".to_string()) - .unwrap(); - - let (status, body) = match execute_request(request, &self.http_client).await { - Err(err) => return Err(format!("Cannot send request to mock server: {}", err)), - Ok(sb) => sb, - }; - - // Evaluate response status code - if status != 202 { - return Err(format!( - "Could not delete history from server (status = {}, message = {})", - status, body - )); - } - - Ok(()) - } - - async fn ping(&self) -> Result<(), String> { - http_ping(&self.addr, self.http_client.borrow()).await - } -} - -async fn http_ping( - server_addr: &SocketAddr, - http_client: &InternalHttpClient, -) -> Result<(), String> { - let request_url = format!("http://{}/__httpmock__/ping", server_addr); - let request = Request::builder() - .method("GET") - .uri(request_url) - .body("".to_string()) - .unwrap(); - - let (status, _body) = match execute_request(request, http_client).await { - Err(err) => return Err(format!("cannot send request to mock server: {}", err)), - Ok(sb) => sb, - }; - - if status != 200 { - return Err(format!( - "Could not create mock. Mock server response: status = {}", - status - )); - } - - Ok(()) -} - -async fn execute_request( - req: Request, - http_client: &InternalHttpClient, -) -> Result<(u16, String), String> { - let mut response = match http_client.send_async(req).await { - Err(err) => return Err(format!("cannot send request to mock server: {}", err)), - Ok(r) => r, - }; - - // Evaluate the response status - let body = match response.text().await { - Err(err) => return Err(format!("cannot send request to mock server: {}", err)), - Ok(b) => b, - }; - - Ok((response.status().as_u16(), body)) -} - -fn build_http_client() -> Arc { - Arc::new( - InternalHttpClient::builder() - .tcp_keepalive(Duration::from_secs(60 * 60 * 24)) - .build() - .expect("Cannot build HTTP client"), - ) -} diff --git a/src/api/mock.rs b/src/api/mock.rs index 727560da..86f0777d 100644 --- a/src/api/mock.rs +++ b/src/api/mock.rs @@ -1,52 +1,50 @@ -use std::net::SocketAddr; -use std::str::FromStr; -use std::time::Duration; -use std::{ - collections::BTreeMap, - path::{Path, PathBuf}, -}; +use std::{io::Write, net::SocketAddr}; +use tabwriter::TabWriter; +use crate::api::output; #[cfg(feature = "color")] use colored::*; use serde::{Deserialize, Serialize}; -use serde_json::Value; use crate::api::server::MockServer; -use crate::api::{Method, Regex}; -use crate::common::data::{ClosestMatch, Diff, DiffResult, Mismatch, Reason}; -use crate::common::util::{get_test_resource_file_path, read_file, Join}; +use crate::common::util::Join; -/// Represents a reference to the mock object on a [MockServer](struct.MockServer.html). -/// It can be used to spy on the mock and also perform some management operations, such as -/// deleting the mock from the [MockServer](struct.MockServer.html). +/// Provides a reference to a mock configuration stored on a [MockServer](struct.MockServer.html). +/// This structure is used for interacting with, monitoring, and managing a specific mock's lifecycle, +/// such as observing call counts or removing the mock from the server. +/// +/// This reference allows you to control and verify the behavior of the server in response to +/// incoming HTTP requests that match the mock criteria. /// /// # Example -/// ``` -/// // Arrange +/// Demonstrates how to create and manipulate a mock on the server. This includes monitoring its usage +/// and effectively managing its lifecycle by removing it when necessary. +/// +/// ```rust /// use httpmock::prelude::*; +/// use reqwest::blocking::get; /// +/// // Arrange /// let server = MockServer::start(); /// -/// let mut mock = server.mock(|when, then|{ +/// // Create and configure a mock +/// let mut mock = server.mock(|when, then| { /// when.path("/test"); /// then.status(202); /// }); /// -/// // Send a first request, then delete the mock from the mock and send another request. -/// let response1 = isahc::get(server.url("/test")).unwrap(); -/// -/// // Fetch how often this mock has been called from the server until now -/// assert_eq!(mock.hits(), 1); +/// // Act by sending a request and verifying the mock's hit count +/// let response1 = get(&server.url("/test")).unwrap(); +/// assert_eq!(mock.hits(), 1); // Verify the mock was triggered /// -/// // Delete the mock from the mock server +/// // Remove the mock and test the server's response to the same path again /// mock.delete(); -/// -/// let response2 = isahc::get(server.url("/test")).unwrap(); +/// let response2 = get(&server.url("/test")).unwrap(); /// /// // Assert /// assert_eq!(response1.status(), 202); -/// assert_eq!(response2.status(), 404); +/// assert_eq!(response2.status(), 404); // Expect a 404 status after the mock is deleted /// ``` pub struct Mock<'a> { // Please find the reason why id is public in @@ -59,137 +57,245 @@ impl<'a> Mock<'a> { pub fn new(id: usize, server: &'a MockServer) -> Self { Self { id, server } } - /// This method asserts that the mock server received **exactly one** HTTP request that matched - /// all the request requirements of this mock. + + /// Verifies that the mock server received exactly one HTTP request matching all specified + /// request conditions for this mock. This method is useful for confirming that a particular + /// operation interacts with the server as expected in test scenarios. /// - /// **Attention**: If you want to assert more than one request, consider using either - /// [Mock::assert_hits](struct.Mock.html#method.assert_hits) or - /// [Mock::hits](struct.Mock.html#method.hits). + /// **Attention**: To assert receipt of multiple requests, use [Mock::assert_hits](struct.Mock.html#method.assert_hits) + /// or [Mock::hits](struct.Mock.html#method.hits) methods instead. /// /// # Example - /// ``` - /// // Arrange: Create mock server and a mock + /// Demonstrates creating a mock to match a specific request path, sending a request to that path, + /// and then verifying that exactly one such request was received. + /// + /// ```rust /// use httpmock::prelude::*; + /// use reqwest::blocking::get; /// + /// // Arrange: Start a mock server and set up a mock /// let server = MockServer::start(); - /// /// let mut mock = server.mock(|when, then| { /// when.path("/hits"); /// then.status(200); /// }); /// - /// // Act: Send a request, then delete the mock from the mock and send another request. - /// isahc::get(server.url("/hits")).unwrap(); + /// // Act: Send a request to the specified path + /// get(&server.url("/hits")).unwrap(); /// - /// // Assert: Make sure the mock server received exactly one request that matched all - /// // the request requirements of the mock. + /// // Assert: Check that the server received exactly one request that matched the mock /// mock.assert(); /// ``` + /// /// # Panics - /// This method will panic if there is a problem with the (standalone) mock server. + /// This method will panic if the mock server did not receive exactly one matching request or if + /// there are issues with the mock server's availability. pub fn assert(&self) { self.assert_async().join() } - /// This method asserts that the mock server received **exactly one** HTTP request that matched - /// all the request requirements of this mock. + /// Asynchronously verifies that the mock server received exactly one HTTP request matching all + /// specified request conditions for this mock. This method is suited for asynchronous testing environments + /// where operations against the mock server occur non-blockingly. /// - /// **Attention**: If you want to assert more than one request, consider using either - /// [Mock::assert_hits](struct.Mock.html#method.assert_hits) or - /// [Mock::hits](struct.Mock.html#method.hits). + /// **Attention**: To assert the receipt of multiple requests asynchronously, consider using + /// [Mock::assert_hits_async](struct.Mock.html#method.assert_hits_async) or + /// [Mock::hits_async](struct.Mock.html#method.hits_async). /// /// # Example - /// ``` - /// // Arrange: Create mock server and a mock + /// Demonstrates setting up an asynchronous mock, sending a request, and verifying that exactly + /// one such request was received. + /// + /// ```rust /// use httpmock::prelude::*; + /// use reqwest::get; + /// use syn::token; /// - /// async_std::task::block_on(async { + /// let rt = tokio::runtime::Runtime::new().unwrap(); + /// rt.block_on(async { + /// // Arrange: Start a mock server asynchronously and set up a mock /// let server = MockServer::start_async().await; - /// /// let mut mock = server.mock_async(|when, then| { /// when.path("/hits"); /// then.status(200); /// }).await; /// - /// // Act: Send a request, then delete the mock from the mock and send another request. - /// isahc::get_async(server.url("/hits")).await.unwrap(); + /// // Act: Send a request to the specified path asynchronously + /// get(&server.url("/hits")).await.unwrap(); /// - /// // Assert: Make sure the mock server received exactly one request that matched all - /// // the request requirements of the mock. + /// // Assert: Check that the server received exactly one request that matched the mock /// mock.assert_async().await; /// }); /// ``` + /// /// # Panics - /// This method will panic if there is a problem with the (standalone) mock server. + /// This method will panic if the mock server did not receive exactly one matching request or if + /// there are issues with the mock server's availability. pub async fn assert_async(&self) { self.assert_hits_async(1).await } - /// This method asserts that the mock server received the provided number of HTTP requests which - /// matched all the request requirements of this mock. + /// Verifies that the mock server received the specified number of HTTP requests matching all + /// the request conditions defined for this mock. /// - /// **Attention**: Consider using the shorthand version - /// [Mock::assert](struct.Mock.html#method.assert) if you want to assert only one hit. + /// This method is useful for confirming that a series of operations interact with the server as expected + /// within test scenarios, especially when specific interaction counts are significant. /// + /// **Attention**: Use [Mock::assert](struct.Mock.html#method.assert) for the common case of asserting exactly one hit. /// /// # Example - /// ``` - /// // Arrange: Create mock server and a mock + /// Demonstrates creating a mock, sending multiple requests, and verifying the number of received requests + /// matches expectations. + /// + /// ```rust /// use httpmock::prelude::*; - /// use isahc::get; + /// use reqwest::blocking::get; /// + /// // Arrange: Start a mock server and configure a mock /// let server = MockServer::start(); - /// /// let mut mock = server.mock(|when, then| { /// when.path("/hits"); /// then.status(200); /// }); /// - /// // Act: Send a request, then delete the mock from the mock and send another request. - /// get(server.url("/hits")).unwrap(); - /// get(server.url("/hits")).unwrap(); + /// // Act: Send multiple requests to the configured path + /// get(&server.url("/hits")).unwrap(); + /// get(&server.url("/hits")).unwrap(); /// - /// // Assert: Make sure the mock server received exactly two requests that matched all - /// // the request requirements of the mock. + /// // Assert: Check that the server received exactly two requests that matched the mock /// mock.assert_hits(2); /// ``` + /// /// # Panics - /// This method will panic if there is a problem with the (standalone) mock server. + /// This method will panic if the actual number of hits differs from the specified `hits`, or if + /// there are issues with the mock server's availability. + #[deprecated(since = "0.8.0", note = "please use `assert_calls` instead")] pub fn assert_hits(&self, hits: usize) { - self.assert_hits_async(hits).join() + self.assert_calls(hits) } - /// This method asserts that the mock server received the provided number of HTTP requests which - /// matched all the request requirements of this mock. + /// Verifies that the mock server received the specified number of HTTP requests matching all + /// the request conditions defined for this mock. + /// + /// This method is useful for confirming that a series of operations interact with the server as expected + /// within test scenarios, especially when specific interaction counts are significant. /// - /// **Attention**: Consider using the shorthand version - /// [Mock::assert_async](struct.Mock.html#method.assert_async) if you want to assert only one hit. + /// **Attention**: Use [Mock::assert](struct.Mock.html#method.assert) for the common case of asserting exactly one hit. /// /// # Example + /// Demonstrates creating a mock, sending multiple requests, and verifying the number of received requests + /// matches expectations. + /// + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::get; + /// + /// // Arrange: Start a mock server and configure a mock + /// let server = MockServer::start(); + /// let mut mock = server.mock(|when, then| { + /// when.path("/hits"); + /// then.status(200); + /// }); + /// + /// // Act: Send multiple requests to the configured path + /// get(&server.url("/hits")).unwrap(); + /// get(&server.url("/hits")).unwrap(); + /// + /// // Assert: Check that the server received exactly two requests that matched the mock + /// mock.assert_calls(2); /// ``` - /// // Arrange: Create mock server and a mock + /// + /// # Panics + /// This method will panic if the actual number of hits differs from the specified `hits`, or if + /// there are issues with the mock server's availability. + pub fn assert_calls(&self, count: usize) { + self.assert_calls_async(count).join() + } + + /// Asynchronously verifies that the mock server received the specified number of HTTP requests + /// matching all defined request conditions for this mock. + /// + /// This method supports asynchronous testing environments, enabling non-blocking verification + /// of multiple interactions with the mock server. It's particularly useful when exact counts + /// of interactions are critical for test assertions. + /// + /// **Attention**: For asserting exactly one request asynchronously, use + /// [Mock::assert_async](struct.Mock.html#method.assert_async) for simpler syntax. + /// + /// # Example + /// Demonstrates setting up an asynchronous mock, sending multiple requests, and verifying the + /// number of requests received matches expectations. + /// + /// ```rust /// use httpmock::prelude::*; + /// use reqwest::get; /// - /// async_std::task::block_on(async { + /// let rt = tokio::runtime::Runtime::new().unwrap(); + /// rt.block_on(async { + /// // Arrange: Start a mock server asynchronously and set up a mock /// let server = MockServer::start_async().await; - /// /// let mut mock = server.mock_async(|when, then| { /// when.path("/hits"); /// then.status(200); /// }).await; /// - /// // Act: Send a request, then delete the mock from the mock and send another request. - /// isahc::get_async(server.url("/hits")).await.unwrap(); - /// isahc::get_async(server.url("/hits")).await.unwrap(); + /// // Act: Send multiple asynchronous requests to the configured path + /// get(&server.url("/hits")).await.unwrap(); + /// get(&server.url("/hits")).await.unwrap(); /// - /// // Assert: Make sure the mock server received exactly two requests that matched all - /// // the request requirements of the mock. + /// // Assert: Check that the server received exactly two requests that matched the mock /// mock.assert_hits_async(2).await; /// }); /// ``` + /// /// # Panics - /// This method will panic if there is a problem with the (standalone) mock server. + /// This method will panic if the actual number of hits differs from the specified `hits`, or if + /// there are issues with the mock server's availability. + #[deprecated(since = "0.8.0", note = "please use `assert_calls_async` instead")] pub async fn assert_hits_async(&self, hits: usize) { + self.assert_calls_async(hits).await + } + + /// Asynchronously verifies that the mock server received the specified number of HTTP requests + /// matching all defined request conditions for this mock. + /// + /// This method supports asynchronous testing environments, enabling non-blocking verification + /// of multiple interactions with the mock server. It's particularly useful when exact counts + /// of interactions are critical for test assertions. + /// + /// **Attention**: For asserting exactly one request asynchronously, use + /// [Mock::assert_async](struct.Mock.html#method.assert_async) for simpler syntax. + /// + /// # Example + /// Demonstrates setting up an asynchronous mock, sending multiple requests, and verifying the + /// number of requests received matches expectations. + /// + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::get; + /// + /// let rt = tokio::runtime::Runtime::new().unwrap(); + /// rt.block_on(async { + /// // Arrange: Start a mock server asynchronously and set up a mock + /// let server = MockServer::start_async().await; + /// let mut mock = server.mock_async(|when, then| { + /// when.path("/hits"); + /// then.status(200); + /// }).await; + /// + /// // Act: Send multiple asynchronous requests to the configured path + /// get(&server.url("/hits")).await.unwrap(); + /// get(&server.url("/hits")).await.unwrap(); + /// + /// // Assert: Check that the server received exactly two requests that matched the mock + /// mock.assert_calls_async(2).await; + /// }); + /// ``` + /// + /// # Panics + /// This method will panic if the actual number of hits differs from the specified `hits`, or if + /// there are issues with the mock server's availability. + pub async fn assert_calls_async(&self, hits: usize) { let active_mock = self .server .server_adapter @@ -220,45 +326,94 @@ impl<'a> Mock<'a> { .await .expect("Cannot contact mock server"); - fail_with(active_mock.call_counter, hits, closest_match) + output::fail_with(active_mock.call_counter, hits, closest_match) } - /// This method returns the number of times a mock has been called at the mock server. + /// Returns the number of times the specified mock has been triggered on the mock server. + /// + /// This method is useful for verifying that a mock has been invoked the expected number of times, + /// allowing for precise control and assertion of interactions within test scenarios. /// /// # Example - /// ``` - /// // Arrange: Create mock server and a mock + /// Demonstrates setting up a mock, sending a request, and then verifying that the mock + /// was triggered exactly once. + /// + /// ```rust /// use httpmock::prelude::*; + /// use reqwest::blocking::get; /// + /// // Arrange: Start a mock server and create a mock /// let server = MockServer::start(); - /// /// let mut mock = server.mock(|when, then| { /// when.path("/hits"); /// then.status(200); /// }); /// - /// // Act: Send a request, then delete the mock from the mock and send another request. - /// isahc::get(server.url("/hits")).unwrap(); + /// // Act: Send a request to the mock path + /// get(&server.url("/hits")).unwrap(); /// - /// // Assert: Make sure the mock has been called exactly one time + /// // Assert: Verify the mock was called once /// assert_eq!(1, mock.hits()); /// ``` + /// /// # Panics - /// This method will panic if there is a problem with the (standalone) mock server. + /// This method will panic if there are issues accessing the mock server or retrieving the hit count. + #[deprecated(since = "0.8.0", note = "please use `calls` instead")] pub fn hits(&self) -> usize { - self.hits_async().join() + self.calls() } - /// This method returns the number of times a mock has been called at the mock server. + /// Returns the number of times the specified mock has been triggered on the mock server. + /// + /// This method is useful for verifying that a mock has been invoked the expected number of times, + /// allowing for precise control and assertion of interactions within test scenarios. /// /// # Example + /// Demonstrates setting up a mock, sending a request, and then verifying that the mock + /// was triggered exactly once. + /// + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::get; + /// + /// // Arrange: Start a mock server and create a mock + /// let server = MockServer::start(); + /// let mut mock = server.mock(|when, then| { + /// when.path("/hits"); + /// then.status(200); + /// }); + /// + /// // Act: Send a request to the mock path + /// get(&server.url("/hits")).unwrap(); + /// + /// // Assert: Verify the mock was called once + /// assert_eq!(1, mock.calls()); /// ``` - /// async_std::task::block_on(async { - /// // Arrange: Create mock server and a mock - /// use httpmock::prelude::*; /// - /// let server = MockServer::start_async().await; + /// # Panics + /// This method will panic if there are issues accessing the mock server or retrieving the hit count. + pub fn calls(&self) -> usize { + self.calls_async().join() + } + + /// Asynchronously returns the number of times the specified mock has been triggered on the mock server. + /// + /// This method is particularly useful in asynchronous test setups where non-blocking verifications + /// are needed to confirm that a mock has been invoked the expected number of times. It ensures test + /// assertions align with asynchronous operations. + /// + /// # Example + /// Demonstrates setting up an asynchronous mock, sending a request, and then verifying the number + /// of times the mock was triggered. + /// + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::get; /// + /// let rt = tokio::runtime::Runtime::new().unwrap(); + /// rt.block_on(async { + /// // Arrange: Start an asynchronous mock server and create a mock + /// let server = MockServer::start_async().await; /// let mut mock = server /// .mock_async(|when, then| { /// when.path("/hits"); @@ -266,16 +421,57 @@ impl<'a> Mock<'a> { /// }) /// .await; /// - /// // Act: Send a request, then delete the mock from the mock and send another request. - /// isahc::get_async(server.url("/hits")).await.unwrap(); + /// // Act: Send an asynchronous request to the mock path + /// get(&server.url("/hits")).await.unwrap(); /// - /// // Assert: Make sure the mock was called with all required attributes exactly one time. + /// // Assert: Verify the mock was called once /// assert_eq!(1, mock.hits_async().await); /// }); /// ``` + /// /// # Panics - /// This method will panic if there is a problem with the (standalone) mock server. + /// This method will panic if there are issues accessing the mock server or retrieving the hit count asynchronously. + #[deprecated(since = "0.8.0", note = "please use `calls_async` instead")] pub async fn hits_async(&self) -> usize { + self.calls_async().await + } + + /// Asynchronously returns the number of times the specified mock has been triggered on the mock server. + /// + /// This method is particularly useful in asynchronous test setups where non-blocking verifications + /// are needed to confirm that a mock has been invoked the expected number of times. It ensures test + /// assertions align with asynchronous operations. + /// + /// # Example + /// Demonstrates setting up an asynchronous mock, sending a request, and then verifying the number + /// of times the mock was triggered. + /// + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::get; + /// + /// let rt = tokio::runtime::Runtime::new().unwrap(); + /// rt.block_on(async { + /// // Arrange: Start an asynchronous mock server and create a mock + /// let server = MockServer::start_async().await; + /// let mut mock = server + /// .mock_async(|when, then| { + /// when.path("/hits"); + /// then.status(200); + /// }) + /// .await; + /// + /// // Act: Send an asynchronous request to the mock path + /// get(&server.url("/hits")).await.unwrap(); + /// + /// // Assert: Verify the mock was called once + /// assert_eq!(1, mock.calls_async().await); + /// }); + /// ``` + /// + /// # Panics + /// This method will panic if there are issues accessing the mock server or retrieving the hit count asynchronously. + pub async fn calls_async(&self) -> usize { let response = self .server .server_adapter @@ -288,73 +484,89 @@ impl<'a> Mock<'a> { response.call_counter } - /// Deletes the associated mock object from the mock server. + /// Removes the specified mock from the mock server. This operation is useful for testing scenarios + /// where the mock should no longer intercept requests, effectively simulating an environment + /// where certain endpoints may go offline or change behavior dynamically during testing. /// /// # Example - /// ``` - /// // Arrange + /// Demonstrates creating a mock, verifying its behavior with a request, then deleting the mock and + /// verifying that subsequent requests are not intercepted. + /// + /// ```rust /// use httpmock::prelude::*; + /// use reqwest::blocking::get; /// + /// // Arrange: Start a mock server and set up a mock /// let server = MockServer::start(); - /// - /// let mut mock = server.mock(|when, then|{ + /// let mut mock = server.mock(|when, then| { /// when.path("/test"); /// then.status(202); /// }); /// - /// // Send a first request, then delete the mock from the mock and send another request. - /// let response1 = isahc::get(server.url("/test")).unwrap(); - /// - /// // Fetch how often this mock has been called from the server until now - /// assert_eq!(mock.hits(), 1); + /// // Act: Send a request to the mock and verify its behavior + /// let response1 = get(&server.url("/test")).unwrap(); + /// assert_eq!(mock.hits(), 1); // Verify the mock was called once /// - /// // Delete the mock from the mock server + /// // Delete the mock from the server /// mock.delete(); /// - /// let response2 = isahc::get(server.url("/test")).unwrap(); + /// // Send another request and verify the response now that the mock is deleted + /// let response2 = get(&server.url("/test")).unwrap(); /// - /// // Assert + /// // Assert: The first response should be 202 as the mock was active, the second should be 404 /// assert_eq!(response1.status(), 202); /// assert_eq!(response2.status(), 404); /// ``` + /// + /// This method ensures that the mock is completely removed, and any subsequent requests to the + /// same path will not be intercepted by this mock, typically resulting in a 404 Not Found response + /// unless another active mock matches the request. pub fn delete(&mut self) { self.delete_async().join(); } - /// Deletes this mock from the mock server. This method is the asynchronous equivalent of - /// [Mock::delete](struct.Mock.html#method.delete). + /// Asynchronously deletes this mock from the mock server. This method is the asynchronous equivalent of + /// [Mock::delete](struct.Mock.html#method.delete) and is suited for use in asynchronous testing environments + /// where non-blocking operations are preferred. /// /// # Example - /// ``` - /// async_std::task::block_on(async { - /// // Arrange - /// use httpmock::prelude::*; + /// Demonstrates creating an asynchronous mock, sending a request to verify its behavior, then deleting + /// the mock and verifying that subsequent requests are not intercepted. /// - /// let server = MockServer::start_async().await; + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::get; /// + /// let rt = tokio::runtime::Runtime::new().unwrap(); + /// rt.block_on(async { + /// // Arrange: Start an asynchronous mock server and create a mock + /// let server = MockServer::start_async().await; /// let mut mock = server - /// .mock_async(|when, then|{ - /// when.path("/test"); - /// then.status(202); - /// }) - /// .await; - /// - /// // Send a first request, then delete the mock from the mock and send another request. - /// let response1 = isahc::get_async(server.url("/test")).await.unwrap(); + /// .mock_async(|when, then| { + /// when.path("/test"); + /// then.status(202); + /// }) + /// .await; /// - /// // Fetch how often this mock has been called from the server until now - /// assert_eq!(mock.hits_async().await, 1); + /// // Act: Send a request to the mock path and verify the mock's hit count + /// let response1 = get(&server.url("/test")).await.unwrap(); + /// assert_eq!(mock.hits_async().await, 1); // Verify the mock was called once /// - /// // Delete the mock from the mock server + /// // Delete the mock asynchronously from the server /// mock.delete_async().await; /// - /// let response2 = isahc::get_async(server.url("/test")).await.unwrap(); + /// // Send another request and check the response now that the mock is deleted + /// let response2 = get(&server.url("/test")).await.unwrap(); /// - /// // Assert + /// // Assert: The first response should be 202 as the mock was active, the second should be 404 /// assert_eq!(response1.status(), 202); /// assert_eq!(response2.status(), 404); /// }); /// ``` + /// + /// This method ensures that the mock is completely removed asynchronously, and any subsequent requests to the + /// same path will not be intercepted by this mock, typically resulting in a 404 Not Found response + /// unless another active mock matches the request. pub async fn delete_async(&self) { self.server .server_adapter @@ -365,18 +577,25 @@ impl<'a> Mock<'a> { .expect("could not delete mock from server"); } - /// Returns the address of the mock server where the associated mock object is store on. + /// Returns the network address of the mock server where the associated mock object is stored. + /// + /// This method provides access to the IP address and port number of the mock server, useful for + /// connecting to it in tests or displaying its address in debugging output. /// /// # Example - /// ``` - /// // Arrange: Create mock server and a mock + /// Demonstrates how to retrieve and print the address of a mock server after it has been started. + /// + /// ```rust /// use httpmock::prelude::*; + /// use std::net::SocketAddr; /// + /// // Arrange: Start a mock server /// let server = MockServer::start(); /// - /// println!("{}", server.address()); - /// // Will print "127.0.0.1:12345", - /// // where 12345 is the port that the mock server is running on. + /// // Print the address of the server + /// let address: &SocketAddr = server.address(); + /// println!("{}", address); + /// // Output will be something like "127.0.0.1:12345", where 12345 is the port the server is running on. /// ``` pub fn server_address(&self) -> &SocketAddr { self.server.server_adapter.as_ref().unwrap().address() @@ -387,45 +606,79 @@ impl<'a> Mock<'a> { /// structure with some additional functionality, that is usually not required. pub trait MockExt<'a> { /// Creates a new [Mock](struct.Mock.html) instance that references an already existing - /// mock on a [MockServer](struct.MockServer.html). This functionality is usually not required. - /// You can use it if for you need to recreate [Mock](struct.Mock.html) instances - ///. - /// * `id` - The ID of the existing mock to the [MockServer](struct.MockServer.html). - /// * `mock_server` - The [MockServer](struct.MockServer.html) to which the - /// [Mock](struct.Mock.html) instance will reference. + /// mock on a [MockServer](struct.MockServer.html). This method is typically used in advanced scenarios + /// where you need to re-establish a reference to a mock after its original instance has been dropped + /// or lost. + /// + /// # Parameters + /// * `id` - The ID of the existing mock on the [MockServer](struct.MockServer.html). + /// * `mock_server` - A reference to the [MockServer](struct.MockServer.html) where the mock is hosted. /// /// # Example - /// ``` + /// Demonstrates how to recreate a [Mock](struct.Mock.html) instance from a mock ID to verify + /// assertions or perform further actions after the original [Mock](struct.Mock.html) reference + /// has been discarded. + /// + /// ```rust /// use httpmock::{MockServer, Mock, MockExt}; - /// use isahc::get; + /// use reqwest::blocking::get; /// /// // Arrange /// let server = MockServer::start(); - /// let mock_ref = server.mock(|when, then| { + /// let initial_mock = server.mock(|when, then| { /// when.path("/test"); /// then.status(202); /// }); /// - /// // Store away the mock ID for later usage and drop the Mock instance. - /// let mock_id = mock_ref.id(); - /// drop(mock_ref); + /// // Store away the mock ID and drop the initial Mock instance + /// let mock_id = initial_mock.id(); + /// drop(initial_mock); /// - /// // Act: Send the HTTP request - /// let response = get(server.url("/test")).unwrap(); + /// // Act: Send an HTTP request to the mock endpoint + /// let response = get(&server.url("/test")).unwrap(); /// - /// // Create a new Mock instance that references the earlier mock at the MockServer. - /// let mock_ref = Mock::new(mock_id, &server); + /// // Recreate the Mock instance using the stored ID + /// let recreated_mock = Mock::new(mock_id, &server); /// - /// // Use the recreated Mock as usual. - /// mock_ref.assert(); + /// // Assert: Use the recreated Mock to check assertions + /// recreated_mock.assert(); /// assert_eq!(response.status(), 202); /// ``` - /// Refer to [`Issue 26`][https://github.com/alexliesenfeld/httpmock/issues/26] for more - /// information. + /// For more detailed use cases, see [`Issue 26`](https://github.com/alexliesenfeld/httpmock/issues/26) on GitHub. fn new(id: usize, mock_server: &'a MockServer) -> Mock<'a>; - /// Returns the ID that the mock was assigned to on the - /// [MockServer](struct.MockServer.html). + /// Returns the unique identifier (ID) assigned to the mock on the [MockServer](struct.MockServer.html). + /// This ID is used internally by the mock server to track and manage the mock throughout its lifecycle. + /// + /// The ID can be particularly useful in advanced testing scenarios where mocks need to be referenced or manipulated + /// programmatically after their creation. + /// + /// # Returns + /// Returns the ID of the mock as a `usize`. + /// + /// # Example + /// Demonstrates how to retrieve the ID of a mock for later reference or manipulation. + /// + /// ```rust + /// use httpmock::MockExt; + /// use httpmock::prelude::*; + /// + /// // Arrange: Start a mock server and create a mock + /// let server = MockServer::start(); + /// let mock = server.mock(|when, then| { + /// when.path("/example"); + /// then.status(200); + /// }); + /// + /// // Act: Retrieve the ID of the mock + /// let mock_id = mock.id(); + /// + /// // The mock_id can now be used to reference or manipulate this specific mock in subsequent operations + /// println!("Mock ID: {}", mock_id); + /// ``` + /// + /// This method is particularly useful when dealing with multiple mocks and needing to assert or modify + /// specific mocks based on their identifiers. fn id(&self) -> usize; } @@ -442,162 +695,25 @@ impl<'a> MockExt<'a> for Mock<'a> { } } -fn create_reason_output(reason: &Reason) -> String { - let mut output = String::new(); - let offsets = match reason.best_match { - true => ("\t".repeat(5), "\t".repeat(2)), - false => ("\t".repeat(1), "\t".repeat(2)), - }; - let actual_text = match reason.best_match { - true => "Actual (closest match):", - false => "Actual:", - }; - output.push_str(&format!( - "Expected:{}[{}]\t\t{}\n", - offsets.0, reason.comparison, &reason.expected - )); - output.push_str(&format!( - "{}{}{}\t{}\n", - actual_text, - offsets.1, - " ".repeat(reason.comparison.len() + 7), - &reason.actual - )); - output +pub struct MockSet<'a> { + pub ids: Vec, + pub(crate) server: &'a MockServer, } -fn create_diff_result_output(dd: &DiffResult) -> String { - let mut output = String::new(); - output.push_str("Diff:"); - if dd.differences.is_empty() { - output.push_str(""); +impl<'a> MockSet<'a> { + pub fn delete(&mut self) { + self.delete_async().join(); } - output.push_str("\n"); - - dd.differences.iter().for_each(|d| { - match d { - Diff::Same(e) => { - output.push_str(&format!(" | {}", e)); - } - Diff::Add(e) => { - #[cfg(feature = "color")] - output.push_str(&format!("+++| {}", e).green().to_string()); - #[cfg(not(feature = "color"))] - output.push_str(&format!("+++| {}", e)); - } - Diff::Rem(e) => { - #[cfg(feature = "color")] - output.push_str(&format!("---| {}", e).red().to_string()); - #[cfg(not(feature = "color"))] - output.push_str(&format!("---| {}", e)); - } - } - output.push_str("\n") - }); - output.push_str("\n"); - output -} - -fn create_mismatch_output(idx: usize, mm: &Mismatch) -> String { - let mut output = String::new(); - - output.push_str(&format!("{} : {}", idx + 1, &mm.title)); - output.push_str("\n"); - output.push_str(&"-".repeat(90)); - output.push_str("\n"); - - mm.reason - .as_ref() - .map(|reason| output.push_str(&create_reason_output(reason))); - - mm.diff - .as_ref() - .map(|diff_result| output.push_str(&create_diff_result_output(diff_result))); - - output.push_str("\n"); - output -} -fn fail_with(actual_hits: usize, expected_hits: usize, closest_match: Option) { - match closest_match { - None => assert!(false, "No request has been received by the mock server."), - Some(closest_match) => { - let mut output = String::new(); - output.push_str(&format!( - "{} of {} expected requests matched the mock specification, .\n", - actual_hits, expected_hits - )); - output.push_str(&format!( - "Here is a comparison with the most similar non-matching request (request number {}): \n\n", - closest_match.request_index + 1 - )); - - for (idx, mm) in closest_match.mismatches.iter().enumerate() { - output.push_str(&create_mismatch_output(idx, &mm)); - } - - closest_match.mismatches.first().map(|mismatch| { - mismatch - .reason - .as_ref() - .map(|reason| assert_eq!(reason.expected, reason.actual, "{}", output)) - }); - - assert!(false, output) + pub async fn delete_async(&self) { + for id in &self.ids { + self.server + .server_adapter + .as_ref() + .unwrap() + .delete_mock(*id) + .await + .expect("could not delete mock from server"); } } } - -#[cfg(test)] -mod test { - use crate::api::mock::fail_with; - use crate::common::data::{ - ClosestMatch, Diff, DiffResult, HttpMockRequest, Mismatch, Reason, Tokenizer, - }; - - #[test] - #[cfg(not(feature = "color"))] - #[should_panic(expected = "1 : This is a title\n\ - ------------------------------------------------------------------------------------------\n\ - Expected: [equals] /toast\n\ - Actual: /test\n\ - Diff:\n | t\n---| e\n+++| oa\n | st")] - fn fail_with_message_test() { - // Arrange - let closest_match = ClosestMatch { - request: HttpMockRequest { - path: "/test".to_string(), - method: "GET".to_string(), - headers: None, - query_params: None, - body: None, - }, - request_index: 0, - mismatches: vec![Mismatch { - title: "This is a title".to_string(), - reason: Some(Reason { - expected: "/toast".to_string(), - actual: "/test".to_string(), - comparison: "equals".to_string(), - best_match: false, - }), - diff: Some(DiffResult { - differences: vec![ - Diff::Same(String::from("t")), - Diff::Rem(String::from("e")), - Diff::Add(String::from("oa")), - Diff::Same(String::from("st")), - ], - distance: 5.0, - tokenizer: Tokenizer::Line, - }), - }], - }; - - // Act - fail_with(1, 2, Some(closest_match)); - - // Assert - // see "should panic" annotation - } -} diff --git a/src/api/mod.rs b/src/api/mod.rs index 68c650ef..8c9b9ff6 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,16 +1,27 @@ // TODO: Remove this at some point #![allow(clippy::needless_lifetimes)] -pub use adapter::{local::LocalMockServerAdapter, Method, MockServerAdapter, Regex}; +pub use adapter::{local::LocalMockServerAdapter, MockServerAdapter}; + +use serde::{Deserialize, Serialize}; +use std::str::FromStr; #[cfg(feature = "remote")] -pub use adapter::standalone::RemoteMockServerAdapter; +pub use adapter::remote::RemoteMockServerAdapter; +use crate::common; pub use mock::{Mock, MockExt}; pub use server::MockServer; pub use spec::{Then, When}; mod adapter; mod mock; +mod output; +mod proxy; mod server; pub mod spec; + +/// Type alias for [regex::Regex](../regex/struct.Regex.html). +pub type Regex = common::data::HttpMockRegex; + +pub use crate::common::data::Method; diff --git a/src/api/output.rs b/src/api/output.rs new file mode 100644 index 00000000..fd8641d9 --- /dev/null +++ b/src/api/output.rs @@ -0,0 +1,382 @@ +use std::io::Write; + +use crate::common::{ + data::{ + ClosestMatch, Diff, DiffResult, FunctionComparison, KeyValueComparison, + KeyValueComparisonKeyValuePair, Mismatch, SingleValueComparison, + }, + util::title_case, +}; + +use tabwriter::TabWriter; + +use crate::server::matchers::generic::MatchingStrategy; +#[cfg(feature = "color")] +use colored::Colorize; + +const QUOTED_TEXT: &'static str = "quoted for better readability"; + +pub fn fail_with(actual_hits: usize, expected_hits: usize, closest_match: Option) { + match closest_match { + None => assert!(false, "No request has been received by the mock server."), + Some(closest_match) => { + let mut output = String::new(); + output.push_str(&format!( + "{} of {} expected requests matched the mock specification.\n", + actual_hits, expected_hits + )); + output.push_str(&format!( + "Here is a comparison with the most similar unmatched request (request number {}): \n\n", + closest_match.request_index + 1 + )); + + let mut fail_text = None; + + for (idx, mm) in closest_match.mismatches.iter().enumerate() { + let (mm_output, fail_text_pair) = create_mismatch_output(idx, &mm); + + if fail_text == None { + if let Some(text) = fail_text_pair { + fail_text = Some(text) + } + } + + output.push_str(&mm_output); + } + + if let Some((left, right)) = fail_text { + assert_eq!(left, right, "{}", output) + } + + assert!(false, "{}", output) + } + } +} + +pub fn create_mismatch_output( + idx: usize, + mismatch: &Mismatch, +) -> (String, Option<(String, String)>) { + let mut tw = TabWriter::new(vec![]); + let mut ide_diff_left = String::new(); + let mut ide_diff_right = String::new(); + + write_header(&mut tw, idx, mismatch); + + if let Some(comparison) = &mismatch.comparison { + let (left, right) = handle_single_value_comparison(&mut tw, mismatch, comparison); + + ide_diff_left.push_str(&left); + ide_diff_right.push_str(&right); + } else if let Some(comparison) = &mismatch.key_value_comparison { + let (left, right) = handle_key_value_comparison(&mut tw, mismatch, comparison); + + ide_diff_left.push_str(&left); + ide_diff_right.push_str(&right); + } else if let Some(comparison) = &mismatch.function_comparison { + handle_function_comparison(&mut tw, mismatch, comparison); + } + + write_footer(&mut tw, mismatch); + + tw.flush().unwrap(); + + let output = String::from_utf8(tw.into_inner().unwrap()).unwrap(); + + if ide_diff_left.len() > 0 && ide_diff_right.len() > 0 { + return (output, Some((ide_diff_left, ide_diff_right))); + } + + (output, None) +} + +fn write_header(tw: &mut TabWriter>, idx: usize, mismatch: &Mismatch) { + writeln!(tw, "{}", &"-".repeat(60)).unwrap(); + writeln!( + tw, + "{}", + &format!("{} : {} Mismatch ", idx + 1, title_case(&mismatch.entity),) + ) + .unwrap(); + writeln!(tw, "{}", &"-".repeat(60)).unwrap(); +} + +fn handle_single_value_comparison( + tw: &mut TabWriter>, + mismatch: &Mismatch, + comparison: &SingleValueComparison, +) -> (String, String) { + writeln!( + tw, + "Expected {} {}:\n{}", + mismatch.entity, comparison.operator, comparison.expected + ) + .unwrap(); + + writeln!(tw, "\nReceived:\n{}", comparison.actual).unwrap(); + + ( + comparison.expected.to_string(), + comparison.actual.to_string(), + ) +} + +fn handle_key_value_comparison( + tw: &mut TabWriter>, + mismatch: &Mismatch, + comparison: &KeyValueComparison, +) -> (String, String) { + let most_similar = match mismatch.best_match { + true => format!(" (most similar {})", mismatch.entity), + false => String::from(" "), + }; + + writeln!(tw, "Expected:").unwrap(); + + if let Some(key) = &comparison.key { + let expected = match quote_if_whitespace(&key.expected) { + (actual, true) => format!("{} ({})", actual, "ED_TEXT), + (actual, false) => format!("{}", actual), + }; + writeln!(tw, "\tkey\t[{}]\t{}", key.operator, expected).unwrap(); + } + + if let Some(value) = &comparison.value { + let expected = match quote_if_whitespace(&value.expected) { + (expected, true) => format!("{} ({})", expected, "ED_TEXT), + (expected, false) => format!("{}", expected), + }; + writeln!(tw, "\tvalue\t[{}]\t{}", value.operator, expected).unwrap(); + } + + if let (Some(expected_count), Some(actual_count)) = + (comparison.expected_count, comparison.actual_count) + { + if comparison.key.is_none() && comparison.value.is_none() { + writeln!( + tw, + "\n{} to appear {} {} but appeared {}", + mismatch.entity, + expected_count, + times_str(expected_count), + actual_count + ) + .unwrap(); + } else { + writeln!( + tw, + "\nto appear {} {} but appeared {}", + expected_count, + times_str(expected_count), + actual_count + ) + .unwrap(); + } + + print_all_request_values(tw, &mismatch.entity, &comparison.all); + + return (expected_count.to_string(), actual_count.to_string()); + } + + if let (Some(key_attr), Some(value_attr)) = (&comparison.key, &comparison.value) { + let result = match (&key_attr.actual, &value_attr.actual) { + (Some(key), Some(value)) => { + writeln!(tw, "\nReceived{}:\n\t{}={}", most_similar, key, value).unwrap(); + (format!("{}\n{}", key, value), format!("{}\n{}", key, value)) + } + (None, Some(value)) => { + writeln!( + tw, + "\nbut{}{} value was\n\t{}", + most_similar, mismatch.entity, value + ) + .unwrap(); + (format!("{}", value), format!("{}", value)) + } + (Some(key), None) => { + writeln!( + tw, + "\nbut{}{} key was\n\t{}", + most_similar, mismatch.entity, key + ) + .unwrap(); + (format!("{}", key), format!("{}", key)) + } + (None, None) => { + let msg = match &mismatch.matching_strategy { + None => "but none was provided", + Some(v) => match v { + MatchingStrategy::Presence => { + "to be in the request, but none was provided." + } + MatchingStrategy::Absence => { + "not to be present, but the request contained it." + } + }, + }; + + writeln!(tw, "\n{}", msg).unwrap(); + (String::new(), String::new()) + } + }; + + // print_value_not_in_request(tw, &mismatch.matching_strategy); + print_all_request_values(tw, &mismatch.entity, &comparison.all); + + return result; + } + + print_value_not_in_request(tw, &mismatch.matching_strategy); + print_all_request_values(tw, &mismatch.entity, &comparison.all); + + (String::new(), String::new()) +} + +fn print_all_request_values( + tw: &mut TabWriter>, + entity: &str, + all: &Vec, +) { + if all.is_empty() { + return; + } + + writeln!(tw, "\nAll received {} values:", entity).unwrap(); + + for (index, pair) in all.iter().enumerate() { + let value = if pair.value.is_some() { + format!("={}", pair.value.clone().unwrap()) + } else { + String::new() + }; + + let text = format!("{}{}", pair.key, value); + writeln!(tw, "\t{}. {}", index + 1, text).unwrap(); + } +} + +fn print_value_not_in_request( + tw: &mut TabWriter>, + matching_strategy: &Option, +) { + writeln!( + tw, + "\n{}", + match matching_strategy { + None => "but none was provided", + Some(v) => match v { + MatchingStrategy::Presence => "to be in the request, but none was provided.", + MatchingStrategy::Absence => "not to be present, but the request contained it.", + }, + } + ) + .unwrap(); +} + +fn handle_function_comparison( + tw: &mut TabWriter>, + mismatch: &Mismatch, + comparison: &FunctionComparison, +) { + writeln!( + tw, + "Custom matcher function {} with index {} did not match the request", + mismatch.matcher_method, comparison.index + ) + .unwrap(); +} + +fn write_footer(tw: &mut TabWriter>, mismatch: &Mismatch) { + let mut version = env!("CARGO_PKG_VERSION"); + if version.trim().is_empty() { + version = "latest"; + } + + let link = format!( + "https://docs.rs/httpmock/{}/httpmock/struct.When.html#method.{}", + version, mismatch.matcher_method + ); + + writeln!(tw).unwrap(); + + if let Some(diff_result) = &mismatch.diff { + writeln!(tw, "{}", &create_diff_result_output(diff_result)).unwrap(); + writeln!(tw).unwrap(); + } + + writeln!(tw, "Matcher:\t{}", mismatch.matcher_method).unwrap(); + writeln!(tw, "Docs:\t{}", link).unwrap(); + writeln!(tw, " ").unwrap(); +} + +fn create_diff_result_output(dd: &DiffResult) -> String { + let mut output = String::new(); + output.push_str("Diff:"); + if dd.differences.is_empty() { + output.push_str(""); + } + output.push_str("\n"); + + dd.differences.iter().enumerate().for_each(|(idx, d)| { + if idx > 0 { + output.push_str("\n") + } + + match d { + Diff::Same(edit) => { + for line in remove_trailing_linebreak(edit).split("\n") { + output.push_str(&format!(" | {}", line)); + } + } + Diff::Add(edit) => { + for line in remove_trailing_linebreak(edit).split("\n") { + #[cfg(feature = "color")] + output.push_str(&format!("+++| {}", line).green().to_string()); + #[cfg(not(feature = "color"))] + output.push_str(&format!("+++| {}", line)); + } + } + Diff::Rem(edit) => { + for line in remove_trailing_linebreak(edit).split("\n") { + #[cfg(feature = "color")] + output.push_str(&format!("---| {}", line).red().to_string()); + #[cfg(not(feature = "color"))] + output.push_str(&format!("---| {}", line)); + } + } + } + }); + output +} + +#[inline] +fn times_str<'a>(v: usize) -> &'a str { + if v == 1 { + return "time"; + } + + return "times"; +} + +fn quote_if_whitespace(s: &str) -> (String, bool) { + if s.is_empty() || s.starts_with(char::is_whitespace) || s.ends_with(char::is_whitespace) { + (format!("\"{}\"", s), true) + } else { + (s.to_string(), false) + } +} + +fn remove_linebreaks(s: &str) -> String { + s.replace("\r\n", "").replace('\n', "").replace('\r', "") +} + +fn remove_trailing_linebreak(s: &str) -> String { + let mut result = s.to_string(); + if result.ends_with('\n') { + result.pop(); + if result.ends_with('\r') { + result.pop(); + } + } + result +} diff --git a/src/api/proxy.rs b/src/api/proxy.rs new file mode 100644 index 00000000..1d1e0814 --- /dev/null +++ b/src/api/proxy.rs @@ -0,0 +1,356 @@ +use crate::{ + api::server::MockServer, + common::{ + data::RecordingRuleConfig, + data::RequestRequirements, + util::{write_file, Join}, + }, + When, +}; +use std::{ + cell::Cell, + path::{Path, PathBuf}, + rc::Rc, +}; + +/// Represents a forwarding rule on a [MockServer](struct.MockServer.html), allowing HTTP requests +/// that meet specific criteria to be redirected to a designated destination. Each rule is +/// uniquely identified by an ID within the server context. +pub struct ForwardingRule<'a> { + pub id: usize, + pub(crate) server: &'a MockServer, +} + +impl<'a> ForwardingRule<'a> { + pub fn new(id: usize, server: &'a MockServer) -> Self { + Self { id, server } + } + + /// Synchronously deletes the forwarding rule from the mock server. + /// This method blocks the current thread until the deletion has been completed, ensuring that the rule is no longer active and will not affect any further requests. + /// + /// # Panics + /// Panics if the deletion fails, typically due to issues such as the rule not existing or server connectivity problems. + pub fn delete(&mut self) { + self.delete_async().join(); + } + + /// Asynchronously deletes the forwarding rule from the mock server. + /// This method performs the deletion without blocking the current thread, + /// making it suitable for use in asynchronous applications where maintaining responsiveness + /// or concurrent execution is necessary. + /// + /// # Panics + /// Panics if the deletion fails, typically due to issues such as the rule not existing or server connectivity problems. + /// This method will raise an immediate panic on such failures, signaling that the operation could not be completed as expected. + pub async fn delete_async(&self) { + self.server + .server_adapter + .as_ref() + .unwrap() + .delete_forwarding_rule(self.id) + .await + .expect("could not delete mock from server"); + } +} +/// Provides methods for managing a proxy rule from the server. +pub struct ProxyRule<'a> { + pub id: usize, + pub(crate) server: &'a MockServer, +} + +impl<'a> ProxyRule<'a> { + pub fn new(id: usize, server: &'a MockServer) -> Self { + Self { id, server } + } + + /// Synchronously deletes the proxy rule from the server. + /// This method blocks the current thread until the deletion is complete, ensuring that + /// the rule is removed and will no longer redirect any requests. + /// + /// # Usage + /// This method is typically used in synchronous environments where immediate removal of the + /// rule is necessary and can afford a blocking operation. + /// + /// # Panics + /// Panics if the deletion fails due to server-related issues such as connectivity problems, + /// or if the rule does not exist on the server. + pub fn delete(&mut self) { + self.delete_async().join(); + } + + /// Asynchronously deletes the proxy rule from the server. + /// This method allows for non-blocking operations, suitable for asynchronous environments + /// where tasks are performed concurrently without interrupting the main workflow. + /// + /// # Usage + /// Ideal for use in modern async/await patterns in Rust, providing a way to handle resource + /// cleanup without stalling other operations. + /// + /// # Panics + /// Panics if the deletion fails due to server-related issues such as connectivity problems, + /// or if the rule does not exist on the server. This method raises an immediate panic to + /// indicate that the operation could not be completed as expected. + pub async fn delete_async(&self) { + self.server + .server_adapter + .as_ref() + .unwrap() + .delete_proxy_rule(self.id) + .await + .expect("could not delete mock from server"); + } +} + +/// Represents a recording of interactions (requests and responses) on a mock server. +/// This structure is used to capture and store detailed information about the HTTP +/// requests received by the server and the corresponding responses sent back. +/// +/// The `Recording` structure can be especially useful in testing scenarios where +/// monitoring and verifying the exact behavior of HTTP interactions is necessary, +/// such as ensuring that a server is responding with the correct headers, body content, +/// and status codes in response to various requests. +pub struct Recording<'a> { + pub id: usize, + pub(crate) server: &'a MockServer, +} + +/// Represents a reference to a recording of HTTP interactions on a mock server. +/// This struct allows for management and retrieval of recorded data, such as viewing, +/// exporting, and deleting the recording. +impl<'a> Recording<'a> { + pub fn new(id: usize, server: &'a MockServer) -> Self { + Self { id, server } + } + + /// Synchronously deletes the recording from the mock server. + /// This method blocks the current thread until the deletion is completed, + /// ensuring that the recording is fully removed before proceeding. + /// + /// # Panics + /// Panics if the deletion fails, which can occur if the recording does not exist, + /// or there are server connectivity issues. + pub fn delete(&mut self) { + self.delete_async().join(); + } + + /// Asynchronously deletes the recording from the mock server. + /// This method allows for non-blocking operations, suitable for asynchronous environments + /// where tasks are performed concurrently without waiting for the deletion to complete. + /// + /// # Panics + /// Panics if the deletion fails, typically due to the recording not existing on the server + /// or connectivity issues with the server. This method provides immediate feedback by + /// raising a panic on such failures. + pub async fn delete_async(&self) { + self.server + .server_adapter + .as_ref() + .unwrap() + .delete_recording(self.id) + .await + .expect("could not delete mock from server"); + } + + /// Synchronously saves the recording to a specified directory with a timestamped filename. + /// The file is named using a combination of the provided scenario name and a UNIX timestamp, formatted as YAML. + /// + /// # Parameters + /// - `dir`: The directory path where the file will be saved. + /// - `scenario_name`: A descriptive name for the scenario, used as part of the filename. + /// + /// # Returns + /// Returns a `Result` containing the `PathBuf` of the created file, or an error if the save operation fails. + /// + /// # Errors + /// Errors if the file cannot be written due to issues like directory permissions, unavailable disk space, or other I/O errors. + #[cfg(feature = "record")] + pub fn save_to, IntoString: Into>( + &self, + dir: PathRef, + scenario_name: IntoString, + ) -> Result> { + self.save_to_async(dir, scenario_name).join() + } + + /// Asynchronously saves the recording to the specified directory with a scenario-specific and timestamped filename. + /// + /// # Parameters + /// - `dir`: The directory path where the file will be saved. + /// - `scenario`: A string representing the scenario name, used as part of the filename. + /// + /// # Returns + /// Returns an `async` `Result` with the `PathBuf` of the saved file or an error if unable to save. + #[cfg(feature = "record")] + pub async fn save_to_async, IntoString: Into>( + &self, + dir: PathRef, + scenario: IntoString, + ) -> Result> { + let rec = self + .server + .server_adapter + .as_ref() + .unwrap() + .export_recording(self.id) + .await?; + + let scenario = scenario.into(); + let dir = dir.as_ref(); + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_secs(); + let filename = format!("{}_{}.yaml", scenario, timestamp); + let filepath = dir.join(filename); + + if let Some(bytes) = rec { + return Ok(write_file(&filepath, &bytes, true).await?); + } + + Err("No recording data available".into()) + } + + /// Synchronously saves the recording to the default directory (`target/httpmock/recordings`) with the scenario name. + /// + /// # Parameters + /// - `scenario_name`: A descriptive name for the scenario, which helps identify the recording file. + /// + /// # Returns + /// Returns a `Result` with the `PathBuf` to the saved file or an error. + #[cfg(feature = "record")] + pub fn save>( + &self, + scenario_name: IntoString, + ) -> Result> { + self.save_async(scenario_name).join() + } + + /// Asynchronously saves the recording to the default directory structured under `target/httpmock/recordings`. + /// + /// # Parameters + /// - `scenario`: A descriptive name for the test scenario, used in naming the saved file. + /// + /// # Returns + /// Returns an `async` `Result` with the `PathBuf` of the saved file or an error. + #[cfg(feature = "record")] + pub async fn save_async>( + &self, + scenario: IntoString, + ) -> Result> { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("target") + .join("httpmock") + .join("recordings"); + self.save_to_async(path, scenario).await + } +} + +pub struct ForwardingRuleBuilder { + pub(crate) request_requirements: Rc>, + pub(crate) headers: Rc>>, +} + +impl ForwardingRuleBuilder { + pub fn add_request_header, Value: Into>( + mut self, + key: Key, + value: Value, + ) -> Self { + let mut headers = self.headers.take(); + headers.push((key.into(), value.into())); + self.headers.set(headers); + self + } + + pub fn filter(mut self, when: WhenSpecFn) -> Self + where + WhenSpecFn: FnOnce(When), + { + when(When { + expectations: self.request_requirements.clone(), + }); + self + } +} + +pub struct ProxyRuleBuilder { + // TODO: These fields are visible to the user, make them not public + pub(crate) request_requirements: Rc>, + pub(crate) headers: Rc>>, +} + +impl ProxyRuleBuilder { + pub fn add_request_header, Value: Into>( + mut self, + key: Key, + value: Value, + ) -> Self { + let mut headers = self.headers.take(); + headers.push((key.into(), value.into())); + self.headers.set(headers); + self + } + + pub fn filter(mut self, when: WhenSpecFn) -> Self + where + WhenSpecFn: FnOnce(When), + { + when(When { + expectations: self.request_requirements.clone(), + }); + + self + } +} + +pub struct RecordingRuleBuilder { + pub config: Rc>, +} + +impl RecordingRuleBuilder { + pub fn record_request_header>(mut self, header: IntoString) -> Self { + let mut config = self.config.take(); + config.record_headers.push(header.into()); + self.config.set(config); + self + } + + pub fn record_request_headers>( + mut self, + headers: Vec, + ) -> Self { + let mut config = self.config.take(); + config + .record_headers + .extend(headers.into_iter().map(Into::into)); + self.config.set(config); + self + } + + pub fn filter(mut self, when: WhenSpecFn) -> Self + where + WhenSpecFn: FnOnce(When), + { + let mut config = self.config.take(); + + let mut request_requirements = Rc::new(Cell::new(config.request_requirements)); + + when(When { + expectations: request_requirements.clone(), + }); + + config.request_requirements = request_requirements.take(); + + self.config.set(config); + + self + } + + pub fn record_response_delays(mut self, record: bool) -> Self { + let mut config = self.config.take(); + config.record_response_delays = record; + self.config.set(config); + + self + } +} diff --git a/src/api/server.rs b/src/api/server.rs index 7f9dad9b..18e4bb74 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -1,21 +1,62 @@ use crate::api::spec::{Then, When}; #[cfg(feature = "remote")] use crate::api::RemoteMockServerAdapter; -use crate::api::{LocalMockServerAdapter, MockServerAdapter}; -use crate::common::data::{MockDefinition, MockServerHttpResponse, RequestRequirements}; -use crate::common::util::{read_env, with_retry, Join}; -use crate::server::{start_server, MockServerState}; +#[cfg(feature = "remote")] +use crate::common::http::HttpMockHttpClient; + +use crate::{ + api::{LocalMockServerAdapter, MockServerAdapter}, + common::{ + data::{MockDefinition, MockServerHttpResponse, RequestRequirements}, + runtime, + util::{read_env, with_retry, Join}, + }, +}; + +#[cfg(feature = "proxy")] +use crate::{ + api::proxy::{ForwardingRule, ForwardingRuleBuilder, ProxyRule, ProxyRuleBuilder}, + common::{ + data::{ForwardingRuleConfig, ProxyRuleConfig}, + util::read_file_async, + }, +}; + +#[cfg(feature = "record")] +use crate::api::{ + common::data::RecordingRuleConfig, + mock::MockSet, + proxy::{Recording, RecordingRuleBuilder}, +}; + +#[cfg(feature = "record")] +use std::path::PathBuf; + +use crate::server::{state::HttpMockStateManager, HttpMockServerBuilder}; + use crate::Mock; use async_object_pool::Pool; -use std::cell::Cell; -use std::future::pending; -use std::net::{SocketAddr, ToSocketAddrs}; -use std::rc::Rc; -use std::sync::Arc; -use std::thread; -use tokio::task::LocalSet; - -/// A mock server that is able to receive and respond to HTTP requests. +use lazy_static::lazy_static; +use std::{ + cell::Cell, + future::pending, + net::{SocketAddr, ToSocketAddrs}, + rc::Rc, + sync::Arc, + thread, +}; +use tokio::sync::oneshot::channel; + +/// Represents a mock server designed to simulate HTTP server behaviors for testing purposes. +/// This server intercepts HTTP requests and can be configured to return predetermined responses. +/// It is used extensively in automated tests to validate client behavior without the need for a live server, +/// ensuring that applications behave as expected in controlled environments. +/// +/// The mock server allows developers to: +/// - Specify expected HTTP requests using a variety of matching criteria such as path, method, headers, and body content. +/// - Define corresponding HTTP responses including status codes, headers, and body data. +/// - Monitor and verify that the expected requests are made by the client under test. +/// - Simulate various network conditions and server responses, including errors and latencies. pub struct MockServer { pub(crate) server_adapter: Option>, pool: Arc>>, @@ -36,10 +77,19 @@ impl MockServer { return server; } - /// Asynchronously connects to a remote mock server that is running in standalone mode using - /// the provided address of the form : (e.g. "127.0.0.1:8080") to establish - /// the connection. - /// **Note**: This method requires the feature `remote` to be enabled. + /// Asynchronously connects to a remote mock server running in standalone mode. + /// + /// # Arguments + /// * `address` - A string slice representing the address in the format ":", e.g., "127.0.0.1:8080". + /// + /// # Returns + /// An instance of `Self` representing the connected mock server. + /// + /// # Panics + /// This method will panic if the address cannot be parsed, resolved to an IPv4 address, or if the mock server is unreachable. + /// + /// # Note + /// This method requires the `remote` feature to be enabled. #[cfg(feature = "remote")] pub async fn connect_async(address: &str) -> Self { let addr = address @@ -49,35 +99,73 @@ impl MockServer { .expect("Not able to resolve the provided host name to an IPv4 address"); let adapter = REMOTE_SERVER_POOL_REF - .take_or_create(|| Arc::new(RemoteMockServerAdapter::new(addr))) + .take_or_create(|| { + Arc::new(RemoteMockServerAdapter::new( + addr, + REMOTE_SERVER_CLIENT.clone(), + )) + }) .await; Self::from(adapter, REMOTE_SERVER_POOL_REF.clone()).await } - /// Synchronously connects to a remote mock server that is running in standalone mode using - /// the provided address of the form : (e.g. "127.0.0.1:8080") to establish - /// the connection. - /// **Note**: This method requires the feature `remote` to be enabled. + /// Synchronously connects to a remote mock server running in standalone mode. + /// + /// # Arguments + /// * `address` - A string slice representing the address in the format ":", e.g., "127.0.0.1:8080". + /// + /// # Returns + /// An instance of `Self` representing the connected mock server. + /// + /// # Panics + /// This method will panic if the address cannot be parsed, resolved to an IPv4 address, or if the mock server is unreachable. + /// + /// # Note + /// This method requires the `remote` feature to be enabled. #[cfg(feature = "remote")] pub fn connect(address: &str) -> Self { Self::connect_async(address).join() } - /// Asynchronously connects to a remote mock server that is running in standalone mode using - /// connection parameters stored in `HTTPMOCK_HOST` and `HTTPMOCK_PORT` environment variables. - /// **Note**: This method requires the feature `remote` to be enabled. + /// Asynchronously connects to a remote mock server running in standalone mode + /// using connection parameters stored in the `HTTPMOCK_HOST` and `HTTPMOCK_PORT` + /// environment variables. + /// + /// # Returns + /// An instance of `Self` representing the connected mock server. + /// + /// # Panics + /// This method will panic if the `HTTPMOCK_PORT` environment variable cannot be + /// parsed to an integer or if the connection fails. + /// + /// # Note + /// This method requires the `remote` feature to be enabled. + /// + /// # Environment Variables + /// * `HTTPMOCK_HOST` - The hostname or IP address of the mock server (default: "127.0.0.1"). + /// * `HTTPMOCK_PORT` - The port number of the mock server (default: "5050"). #[cfg(feature = "remote")] pub async fn connect_from_env_async() -> Self { let host = read_env("HTTPMOCK_HOST", "127.0.0.1"); - let port = read_env("HTTPMOCK_PORT", "5000") + let port = read_env("HTTPMOCK_PORT", "5050") .parse::() .expect("Cannot parse environment variable HTTPMOCK_PORT to an integer"); Self::connect_async(&format!("{}:{}", host, port)).await } - /// Synchronously connects to a remote mock server that is running in standalone mode using - /// connection parameters stored in `HTTPMOCK_HOST` and `HTTPMOCK_PORT` environment variables. - /// **Note**: This method requires the feature `remote` to be enabled. + /// Synchronously connects to a remote mock server running in standalone mode + /// using connection parameters stored in the `HTTPMOCK_HOST` and `HTTPMOCK_PORT` + /// environment variables. + /// + /// # Returns + /// An instance of `Self` representing the connected mock server. + /// + /// # Panics + /// This method will panic if the `HTTPMOCK_PORT` environment variable cannot be + /// parsed to an integer or if the connection fails. + /// + /// # Note + /// This method requires the `remote` feature to be enabled. #[cfg(feature = "remote")] pub fn connect_from_env() -> Self { Self::connect_from_env_async().join() @@ -85,18 +173,23 @@ impl MockServer { /// Starts a new `MockServer` asynchronously. /// - /// Attention: This library manages a pool of `MockServer` instances in the background. - /// Instead of always starting a new mock server, a `MockServer` instance is only created - /// on demand if there is no free `MockServer` instance in the pool and the pool has not - /// reached a maximum size yet. Otherwise, *THIS METHOD WILL BLOCK* the executing function - /// until a free mock server is available. + /// # Attention + /// This library manages a pool of `MockServer` instances in the background. + /// Instead of always starting a new mock server, a `MockServer` instance is + /// only created on demand if there is no free `MockServer` instance in the pool + /// and the pool has not reached its maximum size yet. Otherwise, **THIS METHOD WILL BLOCK** + /// the executing function until a free mock server is available. /// - /// This allows to run many tests in parallel, but will prevent exhaust the executing - /// machine by creating too many mock servers. + /// This approach allows running many tests in parallel without exhausting + /// the executing machine by creating too many mock servers. /// /// A `MockServer` instance is automatically taken from the pool whenever this method is called. /// The instance is put back into the pool automatically when the corresponding - /// 'MockServer' variable gets out of scope. + /// `MockServer` variable goes out of scope. + /// + /// # Returns + /// An instance of `Self` representing the started mock server. + /// ``` pub async fn start_async() -> Self { let adapter = LOCAL_SERVER_POOL_REF .take_or_create(LOCAL_SERVER_ADAPTER_GENERATOR) @@ -122,22 +215,52 @@ impl MockServer { Self::start_async().join() } - /// The hostname of the `MockServer`. By default, this is `127.0.0.1`. - /// In standalone mode, the hostname will be the host where the standalone mock server is - /// running. + /// Returns the hostname of the `MockServer`. + /// + /// By default, this is `127.0.0.1`. In standalone mode, the hostname will be + /// the host where the standalone mock server is running. + /// + /// # Returns + /// A `String` representing the hostname of the `MockServer`. + /// + /// # Example + /// ```rust + /// use httpmock::MockServer; + /// + /// let server = MockServer::start(); + /// let host = server.host(); + /// + /// assert_eq!(host, "127.0.0.1"); + /// ``` pub fn host(&self) -> String { self.server_adapter.as_ref().unwrap().host() } - /// The TCP port that the mock server is listening on. + /// Returns the TCP port that the mock server is listening on. + /// + /// # Returns + /// A `u16` representing the port number of the `MockServer`. + /// + /// # Example + /// ```rust + /// use httpmock::MockServer; + /// + /// let server = MockServer::start(); + /// let port = server.port(); + /// + /// assert!(port > 0); + /// ``` pub fn port(&self) -> u16 { self.server_adapter.as_ref().unwrap().port() } /// Builds the address for a specific path on the mock server. /// - /// **Example**: - /// ``` + /// # Returns + /// A reference to the `SocketAddr` representing the address of the `MockServer`. + /// + /// # Example + /// ```rust /// // Start a local mock server for exclusive use by this test function. /// let server = httpmock::MockServer::start(); /// @@ -146,7 +269,7 @@ impl MockServer { /// // Get the address of the MockServer. /// let addr = server.address(); /// - /// // Ensure the returned URL is as expected + /// // Ensure the returned URL is as expected. /// assert_eq!(expected_addr_str, addr.to_string()); /// ``` pub fn address(&self) -> &SocketAddr { @@ -155,8 +278,42 @@ impl MockServer { /// Builds the URL for a specific path on the mock server. /// - /// **Example**: + /// # Arguments + /// * `path` - A string slice representing the specific path on the mock server. + /// + /// # Returns + /// A `String` representing the full URL for the given path on the `MockServer`. + /// + /// # Example + /// ```rust + /// // Start a local mock server for exclusive use by this test function. + /// let server = httpmock::MockServer::start(); + /// + /// let expected_url = format!("https://127.0.0.1:{}/hello", server.port()); + /// + /// // Get the URL for path "/hello". + /// let url = server.url("/hello"); + /// + /// // Ensure the returned URL is as expected. + /// assert_eq!(expected_url, url); /// ``` + #[cfg(feature = "https")] + pub fn url>(&self, path: S) -> String { + return format!("https://{}{}", self.address(), path.into()); + } + + /// Builds the URL for a specific path on the mock server. + /// + /// # Arguments + /// * `path` - A string slice representing the specific path on the mock server. + /// + /// # Returns + /// A `String` representing the full URL for the given path on the `MockServer`. + /// + /// # Example + /// ```rust + /// use httpmock::MockServer; + /// /// // Start a local mock server for exclusive use by this test function. /// let server = httpmock::MockServer::start(); /// @@ -165,27 +322,33 @@ impl MockServer { /// // Get the URL for path "/hello". /// let url = server.url("/hello"); /// - /// // Ensure the returned URL is as expected + /// // Ensure the returned URL is as expected. /// assert_eq!(expected_url, url); /// ``` + #[cfg(not(feature = "https"))] pub fn url>(&self, path: S) -> String { - format!("http://{}{}", self.address(), path.into()) + return format!("http://{}{}", self.address(), path.into()); } /// Builds the base URL for the mock server. /// - /// **Example**: - /// ``` + /// # Returns + /// A `String` representing the base URL of the `MockServer`. + /// + /// # Example + /// ```rust + /// use httpmock::MockServer; + /// /// // Start a local mock server for exclusive use by this test function. /// let server = httpmock::MockServer::start(); /// /// let expected_url = format!("http://127.0.0.1:{}", server.port()); /// - /// // Get the URL for path "/hello". - /// let url = server.base_url(); + /// // Get the base URL of the MockServer. + /// let base_url = server.base_url(); /// - /// // Ensure the returned URL is as expected - /// assert_eq!(expected_url, url); + /// // Ensure the returned URL is as expected. + /// assert_eq!(expected_url, base_url); /// ``` pub fn base_url(&self) -> String { self.url("") @@ -193,19 +356,30 @@ impl MockServer { /// Creates a [Mock](struct.Mock.html) object on the mock server. /// - /// **Example**: - /// ``` - /// use isahc::get; + /// # Arguments + /// * `config_fn` - A closure that takes a `When` and `Then` to configure the mock. /// - /// let server = httpmock::MockServer::start(); + /// # Returns + /// A `Mock` object representing the created mock on the server. + /// + /// # Example + /// ```rust + /// use reqwest::blocking::get; + /// use httpmock::MockServer; /// + /// // Start a local mock server for exclusive use by this test function. + /// let server = MockServer::start(); + /// + /// // Create a mock on the server. /// let mock = server.mock(|when, then| { /// when.path("/hello"); /// then.status(200); /// }); /// - /// get(server.url("/hello")).unwrap(); + /// // Send an HTTP request to the mock server. This simulates your code. + /// get(&server.url("/hello")).unwrap(); /// + /// // Ensure the mock was called as expected. /// mock.assert(); /// ``` pub fn mock(&self, config_fn: F) -> Mock @@ -215,13 +389,22 @@ impl MockServer { self.mock_async(config_fn).join() } - /// Creates a [Mock](struct.Mock.html) object on the mock server. + /// Creates a [Mock](struct.Mock.html) object on the mock server asynchronously. /// - /// **Example**: - /// ``` - /// use isahc::{get_async}; - /// async_std::task::block_on(async { - /// let server = httpmock::MockServer::start(); + /// # Arguments + /// * `spec_fn` - A closure that takes a `When` and `Then` to configure the mock. + /// + /// # Returns + /// A `Mock` object representing the created mock on the server. + /// + /// # Example + /// ```rust + /// use reqwest::get; + /// use httpmock::MockServer; + /// + /// let rt = tokio::runtime::Runtime::new().unwrap(); + /// rt.block_on(async { + /// let server = MockServer::start(); /// /// let mock = server /// .mock_async(|when, then| { @@ -230,14 +413,14 @@ impl MockServer { /// }) /// .await; /// - /// get_async(server.url("/hello")).await.unwrap(); + /// get(&server.url("/hello")).await.unwrap(); /// /// mock.assert_async().await; /// }); /// ``` - pub async fn mock_async<'a, F>(&'a self, spec_fn: F) -> Mock<'a> + pub async fn mock_async<'a, SpecFn>(&'a self, spec_fn: SpecFn) -> Mock<'a> where - F: FnOnce(When, Then), + SpecFn: FnOnce(When, Then), { let mut req = Rc::new(Cell::new(RequestRequirements::new())); let mut res = Rc::new(Cell::new(MockServerHttpResponse::new())); @@ -263,7 +446,7 @@ impl MockServer { .expect("Cannot deserialize mock server response"); Mock { - id: response.mock_id, + id: response.id, server: self, } } @@ -271,23 +454,25 @@ impl MockServer { /// Resets the mock server. More specifically, it deletes all [Mock](struct.Mock.html) objects /// from the mock server and clears its request history. /// - /// **Example**: - /// ``` - /// use isahc::get; - /// let server = httpmock::MockServer::start(); + /// # Example + /// ```rust + /// use reqwest::blocking::get; + /// use httpmock::MockServer; + /// + /// let server = MockServer::start(); /// - /// let mock = server.mock(|when, then| { + /// let mock = server.mock(|when, then| { /// when.path("/hello"); /// then.status(200); - /// }); + /// }); /// - /// let mut response = get(server.url("/hello")).unwrap(); - /// assert_eq!(response.status(), 200); + /// let response = get(&server.url("/hello")).unwrap(); + /// assert_eq!(response.status(), 200); /// - /// server.reset(); + /// server.reset(); /// - /// let mut response = get(server.url("/hello")).unwrap(); - /// assert_eq!(response.status(), 404); + /// let response = get(&server.url("/hello")).unwrap(); + /// assert_eq!(response.status(), 404); /// ``` pub fn reset(&self) { self.reset_async().join() @@ -296,39 +481,860 @@ impl MockServer { /// Resets the mock server. More specifically, it deletes all [Mock](struct.Mock.html) objects /// from the mock server and clears its request history. /// - /// **Example**: - /// ``` - /// use isahc::get; - /// async_std::task::block_on(async { - /// let server = httpmock::MockServer::start_async().await; + /// # Example + /// ```rust + /// use reqwest::get; + /// use httpmock::MockServer; + /// + /// let rt = tokio::runtime::Runtime::new().unwrap(); + /// rt.block_on(async { + /// let server = MockServer::start_async().await; /// /// let mock = server.mock_async(|when, then| { - /// when.path("/hello"); - /// then.status(200); + /// when.path("/hello"); + /// then.status(200); /// }).await; /// - /// let mut response = get(server.url("/hello")).unwrap(); + /// let response = get(&server.url("/hello")).await.unwrap(); /// assert_eq!(response.status(), 200); /// /// server.reset_async().await; /// - /// let mut response = get(server.url("/hello")).unwrap(); + /// let response = get(&server.url("/hello")).await.unwrap(); /// assert_eq!(response.status(), 404); /// }); /// ``` pub async fn reset_async(&self) { if let Some(server_adapter) = &self.server_adapter { - with_retry(5, || server_adapter.delete_all_mocks()) + with_retry(3, || server_adapter.reset()) .await .expect("Cannot reset mock server (task: delete mocks)."); - with_retry(5, || server_adapter.delete_history()) - .await - .expect("Cannot reset mock server (task: delete request history)."); + } + } + + /// Configures the mock server to forward the request to the target host by replacing the host name, + /// but only if the request expectations are met. If the request is recorded, the recording will + /// **NOT** contain the host name as an expectation to allow the recording to be reused. + /// + /// # Arguments + /// * `to_base_url` - A string that represents the base URL to which the request should be forwarded. + /// * `rule` - A closure that takes a `ForwardingRuleBuilder` to configure the forwarding rule. + /// + /// # Returns + /// A `ForwardingRule` object representing the configured forwarding rule. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // We will create this mock server to simulate a real service (e.g., GitHub, AWS, etc.). + /// let target_server = MockServer::start(); + /// target_server.mock(|when, then| { + /// when.any_request(); + /// then.status(200).body("Hi from fake GitHub!"); + /// }); + /// + /// // Let's create our mock server for the test + /// let server = MockServer::start(); + /// + /// // We configure our server to forward the request to the target host instead of + /// // answering with a mocked response. The 'rule' variable lets you configure + /// // rules under which forwarding should take place. + /// server.forward_to(target_server.base_url(), |rule| { + /// rule.filter(|when| { + /// when.any_request(); // We want all requests to be forwarded. + /// }); + /// }); + /// + /// // Now let's send an HTTP request to the mock server. The request will be forwarded + /// // to the target host, as we configured before. + /// let client = Client::new(); + /// + /// // Since the request was forwarded, we should see the target host's response. + /// let response = client.get(&server.url("/get")).send().unwrap(); + /// let status = response.status(); + /// + /// assert_eq!("Hi from fake GitHub!", response.text().unwrap()); + /// assert_eq!(status, 200); + /// ``` + /// + /// # Feature + /// This method is only available when the `proxy` feature is enabled. + #[cfg(feature = "proxy")] + pub fn forward_to( + &self, + to_base_url: IntoString, + rule: ForwardingRuleBuilderFn, + ) -> ForwardingRule + where + ForwardingRuleBuilderFn: FnOnce(ForwardingRuleBuilder), + IntoString: Into, + { + self.forward_to_async(to_base_url, rule).join() + } + + /// Asynchronously configures the mock server to forward the request to the target host by replacing the host name, + /// but only if the request expectations are met. If the request is recorded, the recording will + /// contain the host name as an expectation to allow the recording to be reused. + /// + /// # Arguments + /// * `target_base_url` - A string that represents the base URL to which the request should be forwarded. + /// * `rule` - A closure that takes a `ForwardingRuleBuilder` to configure the forwarding rule. + /// + /// # Returns + /// A `ForwardingRule` object representing the configured forwarding rule. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::Client; + /// + /// let rt = tokio::runtime::Runtime::new().unwrap(); + /// rt.block_on(async { + /// // We will create this mock server to simulate a real service (e.g., GitHub, AWS, etc.). + /// let target_server = MockServer::start_async().await; + /// target_server.mock_async(|when, then| { + /// when.any_request(); + /// then.status(200).body("Hi from fake GitHub!"); + /// }).await; + /// + /// // Let's create our mock server for the test + /// let server = MockServer::start_async().await; + /// + /// // We configure our server to forward the request to the target host instead of + /// // answering with a mocked response. The 'rule' variable lets you configure + /// // rules under which forwarding should take place. + /// server.forward_to_async(target_server.base_url(), |rule| { + /// rule.filter(|when| { + /// when.any_request(); // We want all requests to be forwarded. + /// }); + /// }).await; + /// + /// // Now let's send an HTTP request to the mock server. The request will be forwarded + /// // to the target host, as we configured before. + /// let client = Client::new(); + /// + /// // Since the request was forwarded, we should see the target host's response. + /// let response = client.get(&server.url("/get")).send().await.unwrap(); + /// let status = response.status(); + /// assert_eq!(status, 200); + /// assert_eq!("Hi from fake GitHub!", response.text().await.unwrap()); + /// }); + /// ``` + /// + /// # Feature + /// This method is only available when the `proxy` feature is enabled. + #[cfg(feature = "proxy")] + pub async fn forward_to_async<'a, IntoString, ForwardingRuleBuilderFn>( + &'a self, + target_base_url: IntoString, + rule: ForwardingRuleBuilderFn, + ) -> ForwardingRule<'a> + where + ForwardingRuleBuilderFn: FnOnce(ForwardingRuleBuilder), + IntoString: Into, + { + let mut headers = Rc::new(Cell::new(Vec::new())); + let mut req = Rc::new(Cell::new(RequestRequirements::new())); + + rule(ForwardingRuleBuilder { + headers: headers.clone(), + request_requirements: req.clone(), + }); + + let response = self + .server_adapter + .as_ref() + .unwrap() + .create_forwarding_rule(ForwardingRuleConfig { + target_base_url: target_base_url.into(), + request_requirements: req.take(), + request_header: headers.take(), + }) + .await + .expect("Cannot deserialize mock server response"); + + ForwardingRule { + id: response.id, + server: self, + } + } + + /// Configures the mock server to proxy HTTP requests based on specified criteria. + /// + /// This method configures the mock server to forward incoming requests to the target host + /// when the requests meet the defined criteria. If a request matches the criteria, it will be + /// proxied to the target host. + /// + /// When a recording is active (which records requests and responses), the host name of the request + /// will be stored with the recording as a request expectation. + /// + /// # Arguments + /// * `rule` - A closure that takes a `ProxyRuleBuilder` to configure the proxy rule. + /// + /// # Returns + /// A `ProxyRule` object representing the configured proxy rule that is stored on the mock server. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Create a mock server to simulate a real service (e.g., GitHub, AWS, etc.). + /// let target_server = MockServer::start(); + /// target_server.mock(|when, then| { + /// when.any_request(); + /// then.status(200).body("Hi from fake GitHub!"); + /// }); + /// + /// // Create a proxy mock server for the test. + /// let proxy_server = MockServer::start(); + /// + /// // Configure the proxy server to forward requests to the target server. + /// // The `rule` closure allows specifying criteria for requests that should be proxied. + /// proxy_server.proxy(|rule| { + /// rule.filter(|when| { + /// // Only allow requests to the target server to be proxied. + /// when.host(target_server.host()).port(target_server.port()); + /// }); + /// }); + /// + /// // Create an HTTP client configured to use the proxy server. + /// let client = Client::builder() + /// .proxy(reqwest::Proxy::all(proxy_server.base_url()).unwrap()) // Set the proxy server + /// .build() + /// .unwrap(); + /// + /// // Send a request to the target server through the proxy server. + /// // The request will be forwarded to the target server as configured. + /// let response = client.get(&target_server.url("/get")).send().unwrap(); + /// let status = response.status(); + /// + /// // Verify that the response comes from the target server. + /// assert_eq!(status, 200); + /// assert_eq!("Hi from fake GitHub!", response.text().unwrap()); + /// ``` + /// + /// # Feature + /// This method is only available when the `proxy` feature is enabled. + #[cfg(feature = "proxy")] + pub fn proxy(&self, rule: ProxyRuleBuilderFn) -> ProxyRule + where + ProxyRuleBuilderFn: FnOnce(ProxyRuleBuilder), + { + self.proxy_async(rule).join() + } + + /// Asynchronously configures the mock server to proxy HTTP requests based on specified criteria. + /// + /// This method configures the mock server to forward incoming requests to the target host + /// when the requests meet the defined criteria. If a request matches the criteria, it will be + /// proxied to the target host. + /// + /// When a recording is active (which records requests and responses), the host name of the request + /// will be stored with the recording to allow the recording to be reused. + /// + /// # Arguments + /// * `rule` - A closure that takes a `ProxyRuleBuilder` to configure the proxy rule. + /// + /// # Returns + /// A `ProxyRule` object representing the configured proxy rule. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::Client; + /// + /// let rt = tokio::runtime::Runtime::new().unwrap(); + /// rt.block_on(async { + /// // We will create this mock server to simulate a real service (e.g., GitHub, AWS, etc.). + /// let target_server = MockServer::start_async().await; + /// target_server.mock_async(|when, then| { + /// when.any_request(); + /// then.status(200).body("Hi from fake GitHub!"); + /// }).await; + /// + /// // Let's create our proxy mock server for the test + /// let proxy_server = MockServer::start_async().await; + /// + /// // We configure our proxy server to forward requests to the target server + /// // The 'rule' closure allows specifying criteria for requests that should be proxied + /// proxy_server.proxy_async(|rule| { + /// rule.filter(|when| { + /// // Only allow requests to the target server to be proxied + /// when.host(target_server.host()).port(target_server.port()); + /// }); + /// }).await; + /// + /// // Create an HTTP client configured to use the proxy server + /// let client = Client::builder() + /// .proxy(reqwest::Proxy::all(proxy_server.base_url()).unwrap()) + /// .build() + /// .unwrap(); + /// + /// // Send a request to the target server through the proxy server + /// // The request will be forwarded to the target server as configured + /// let response = client.get(&target_server.url("/get")).send().await.unwrap(); + /// let status = response.status(); + /// + /// // Verify that the response comes from the target server + /// assert_eq!(status, 200); + /// assert_eq!("Hi from fake GitHub!", response.text().await.unwrap()); + /// }); + /// ``` + /// + /// # Feature + /// This method is only available when the `proxy` feature is enabled. + #[cfg(feature = "proxy")] + pub async fn proxy_async<'a, ProxyRuleBuilderFn>( + &'a self, + rule: ProxyRuleBuilderFn, + ) -> ProxyRule<'a> + where + ProxyRuleBuilderFn: FnOnce(ProxyRuleBuilder), + { + let mut headers = Rc::new(Cell::new(Vec::new())); + let mut req = Rc::new(Cell::new(RequestRequirements::new())); + + rule(ProxyRuleBuilder { + headers: headers.clone(), + request_requirements: req.clone(), + }); + + let response = self + .server_adapter + .as_ref() + .unwrap() + .create_proxy_rule(ProxyRuleConfig { + request_requirements: req.take(), + request_header: headers.take(), + }) + .await + .expect("Cannot deserialize mock server response"); + + ProxyRule { + id: response.id, + server: self, + } + } + + /// Records all requests matching a given rule and the corresponding responses + /// sent back by the mock server. If requests are forwarded or proxied to another + /// host, the original responses from those target hosts will also be recorded. + /// + /// # Parameters + /// + /// * `rule`: A closure that takes a `RecordingRuleBuilder` as an argument, + /// which defines the conditions under which HTTP requests and + /// their corresponding responses will be recorded. + /// + /// # Returns + /// + /// * `Recording`: A reference to the recording object stored on the mock server, + /// which can be used to manage the recording, such as downloading + /// or deleting it. The `Recording` object provides functionality + /// to download the recording and store it under a file. Users can + /// use these files for later playback by calling the `playback` + /// method of the mock server. + /// + /// # Example + /// + /// ```rust + /// // Create a mock server to simulate a real service (e.g., GitHub, AWS, etc.). + /// use reqwest::blocking::Client; + /// use httpmock::MockServer; + /// + /// let target_server = MockServer::start(); + /// target_server.mock(|when, then| { + /// when.any_request(); + /// then.status(200).body("Hi from fake GitHub!"); + /// }); + /// + /// // Create the recording server for the test. + /// let recording_server = MockServer::start(); + /// + /// // Configure the recording server to forward requests to the target host. + /// recording_server.forward_to(target_server.base_url(), |rule| { + /// rule.filter(|when| { + /// when.path("/hello"); // Forward all requests with path "/hello". + /// }); + /// }); + /// + /// // Record the target server's response. + /// let recording = recording_server.record(|rule| { + /// rule.record_response_delays(true) + /// .record_request_headers(vec!["Accept", "Content-Type"]) // Record specific headers. + /// .filter(|when| { + /// when.path("/hello"); // Only record requests with path "/hello". + /// }); + /// }); + /// + /// // Use httpmock as a proxy server. + /// let github_client = Client::new(); + /// + /// let response = github_client + /// .get(&format!("{}/hello", recording_server.base_url())) + /// .send() + /// .unwrap(); + /// assert_eq!(response.text().unwrap(), "Hi from fake GitHub!"); + /// + /// // Store the recording to a file and create a new mock server to playback the recording. + /// let target_path = recording.save("my_test_scenario").unwrap(); + /// + /// let playback_server = MockServer::start(); + /// + /// playback_server.playback(target_path); + /// + /// let response = github_client + /// .get(&format!("{}/hello", playback_server.base_url())) + /// .send() + /// .unwrap(); + /// assert_eq!(response.text().unwrap(), "Hi from fake GitHub!"); + /// ``` + /// + /// # Feature + /// + /// This method is only available when the `record` feature is enabled. + #[cfg(feature = "record")] + pub fn record(&self, rule: RecordingRuleBuilderFn) -> Recording + where + RecordingRuleBuilderFn: FnOnce(RecordingRuleBuilder), + { + self.record_async(rule).join() + } + + /// Asynchronously records all requests matching a given rule and the corresponding responses + /// sent back by the mock server. If requests are forwarded or proxied to another + /// host, the original responses from those target hosts will also be recorded. + /// + /// # Parameters + /// + /// * `rule`: A closure that takes a `RecordingRuleBuilder` as an argument, + /// which defines the conditions under which requests will be recorded. + /// + /// # Returns + /// + /// * `Recording`: A reference to the recording object stored on the mock server, + /// which can be used to manage the recording, such as downloading + /// or deleting it. The `Recording` object provides functionality + /// to download the recording and store it under a file. Users can + /// use these files for later playback by calling the `playback` + /// method of the mock server. + /// + /// # Example + /// + /// ```rust + /// use httpmock::MockServer; + /// use reqwest::Client; + /// + /// let rt = tokio::runtime::Runtime::new().unwrap(); + /// rt.block_on(async { + /// // Create a mock server to simulate a real service (e.g., GitHub, AWS, etc.). + /// let target_server = MockServer::start_async().await; + /// target_server.mock_async(|when, then| { + /// when.any_request(); + /// then.status(200).body("Hi from fake GitHub!"); + /// }).await; + /// + /// // Create the recording server for the test. + /// let recording_server = MockServer::start_async().await; + /// + /// // Configure the recording server to forward requests to the target host. + /// recording_server.forward_to_async(target_server.base_url(), |rule| { + /// rule.filter(|when| { + /// when.path("/hello"); // Forward all requests with path "/hello". + /// }); + /// }).await; + /// + /// // Record the target server's response. + /// let recording = recording_server.record_async(|rule| { + /// rule.record_response_delays(true) + /// .record_request_headers(vec!["Accept", "Content-Type"]) // Record specific headers. + /// .filter(|when| { + /// when.path("/hello"); // Only record requests with path "/hello". + /// }); + /// }).await; + /// + /// // Use httpmock as a proxy server. + /// let client = Client::new(); + /// + /// let response = client + /// .get(&format!("{}/hello", recording_server.base_url())) + /// .send() + /// .await + /// .unwrap(); + /// assert_eq!(response.text().await.unwrap(), "Hi from fake GitHub!"); + /// + /// // Store the recording to a file and create a new mock server to playback the recording. + /// let target_path = recording.save_async("my_test_scenario").await.unwrap(); + /// + /// let playback_server = MockServer::start_async().await; + /// + /// playback_server.playback_async(target_path).await; + /// + /// let response = client + /// .get(&format!("{}/hello", playback_server.base_url())) + /// .send() + /// .await + /// .unwrap(); + /// assert_eq!(response.text().await.unwrap(), "Hi from fake GitHub!"); + /// }); + /// ``` + /// + /// # Feature + /// + /// This method is only available when the `record` feature is enabled. + #[cfg(feature = "record")] + pub async fn record_async<'a, RecordingRuleBuilderFn>( + &'a self, + rule: RecordingRuleBuilderFn, + ) -> Recording<'a> + where + RecordingRuleBuilderFn: FnOnce(RecordingRuleBuilder), + { + let mut config = Rc::new(Cell::new(RecordingRuleConfig { + request_requirements: RequestRequirements::new(), + record_headers: Vec::new(), + record_response_delays: false, + })); + + rule(RecordingRuleBuilder { + config: config.clone(), + }); + + let response = self + .server_adapter + .as_ref() + .unwrap() + .create_recording(config.take()) + .await + .expect("Cannot deserialize mock server response"); + + Recording { + id: response.id, + server: self, + } + } + + /// Reads a recording file and configures the mock server to respond with the + /// recorded responses when an incoming request matches the corresponding recorded HTTP request. + /// This allows users to record responses from a real service and use these recordings for testing later, + /// without needing to be online or having access to the real service during subsequent tests. + /// + /// # Parameters + /// + /// * `path`: A path to the file containing the recording. This can be any type + /// that implements `Into`, such as a `&str` or `String`. + /// + /// # Returns + /// + /// * `MockSet`: An object representing the set of mocks that were loaded from the recording file. + /// + /// # Example + /// + /// ```rust + /// // Create a mock server to simulate a real service (e.g., GitHub, AWS, etc.). + /// use reqwest::blocking::Client; + /// use httpmock::MockServer; + /// + /// let target_server = MockServer::start(); + /// target_server.mock(|when, then| { + /// when.any_request(); + /// then.status(200).body("Hi from fake GitHub!"); + /// }); + /// + /// // Create the recording server for the test. + /// let recording_server = MockServer::start(); + /// + /// // Configure the recording server to forward requests to the target host. + /// recording_server.forward_to(target_server.base_url(), |rule| { + /// rule.filter(|when| { + /// when.path("/hello"); // Forward all requests with path "/hello". + /// }); + /// }); + /// + /// // Record the target server's response. + /// let recording = recording_server.record(|rule| { + /// rule.record_response_delays(true) + /// .record_request_headers(vec!["Accept", "Content-Type"]) // Record specific headers. + /// .filter(|when| { + /// when.path("/hello"); // Only record requests with path "/hello". + /// }); + /// }); + /// + /// // Use httpmock as a proxy server. + /// let client = Client::new(); + /// + /// let response = client + /// .get(&format!("{}/hello", recording_server.base_url())) + /// .send() + /// .unwrap(); + /// assert_eq!(response.text().unwrap(), "Hi from fake GitHub!"); + /// + /// // Store the recording to a file and create a new mock server to play back the recording. + /// let target_path = recording.save("my_test_scenario").unwrap(); + /// + /// let playback_server = MockServer::start(); + /// + /// // Play back the recorded interactions from the file. + /// playback_server.playback(target_path); + /// + /// let response = client + /// .get(&format!("{}/hello", playback_server.base_url())) + /// .send() + /// .unwrap(); + /// assert_eq!(response.text().unwrap(), "Hi from fake GitHub!"); + /// ``` + /// + /// # Feature + /// + /// This method is only available when the `record` feature is enabled. + #[cfg(feature = "record")] + pub fn playback>(&self, path: IntoPathBuf) -> MockSet { + self.playback_async(path).join() + } + + /// Asynchronously reads a recording file and configures the mock server to respond with the + /// recorded responses when an incoming request matches the corresponding recorded HTTP request. + /// This allows users to record responses from a real service and use these recordings for testing later, + /// without needing to be online or having access to the real service during subsequent tests. + /// + /// # Parameters + /// + /// * `path`: A path to the file containing the recorded interactions. This can be any type + /// that implements `Into`, such as a `&str` or `String`. + /// + /// # Returns + /// + /// * `MockSet`: An object representing the set of mocks that were loaded from the recording file. + /// + /// # Example + /// + /// ```rust + /// use httpmock::MockServer; + /// use reqwest::Client; + /// + /// let rt = tokio::runtime::Runtime::new().unwrap(); + /// rt.block_on(async { + /// // Create a mock server to simulate a real service (e.g., GitHub, AWS, etc.). + /// let target_server = MockServer::start_async().await; + /// target_server.mock_async(|when, then| { + /// when.any_request(); + /// then.status(200).body("Hi from fake GitHub!"); + /// }).await; + /// + /// // Create the recording server for the test. + /// let recording_server = MockServer::start_async().await; + /// + /// // Configure the recording server to forward requests to the target host. + /// recording_server.forward_to_async(target_server.base_url(), |rule| { + /// rule.filter(|when| { + /// when.path("/hello"); // Forward all requests with path "/hello". + /// }); + /// }).await; + /// + /// // Record the target server's response. + /// let recording = recording_server.record_async(|rule| { + /// rule.record_response_delays(true) + /// .record_request_headers(vec!["Accept", "Content-Type"]) // Record specific headers. + /// .filter(|when| { + /// when.path("/hello"); // Only record requests with path "/hello". + /// }); + /// }).await; + /// + /// // Use httpmock as a proxy server. + /// let client = Client::new(); + /// + /// let response = client + /// .get(&format!("{}/hello", recording_server.base_url())) + /// .send() + /// .await + /// .unwrap(); + /// assert_eq!(response.text().await.unwrap(), "Hi from fake GitHub!"); + /// + /// // Store the recording to a file and create a new mock server to play back the recording. + /// let target_path = recording.save("my_test_scenario").unwrap(); + /// + /// let playback_server = MockServer::start_async().await; + /// + /// playback_server.playback_async(target_path).await; + /// + /// let response = client + /// .get(&format!("{}/hello", playback_server.base_url())) + /// .send() + /// .await + /// .unwrap(); + /// assert_eq!(response.text().await.unwrap(), "Hi from fake GitHub!"); + /// }); + /// ``` + /// + /// # Feature + /// + /// This method is only available when the `record` feature is enabled. + #[cfg(feature = "record")] + pub async fn playback_async>(&self, path: IntoPathBuf) -> MockSet { + let path = path.into(); + let content = read_file_async(&path).await.expect(&format!( + "could not read from file {}", + path.as_os_str() + .to_str() + .map_or(String::new(), |p| p.to_string()) + )); + + return self + .playback_from_yaml_async( + String::from_utf8(content).expect("cannot convert file content to UTF-8"), + ) + .await; + } + + /// Configures the mock server to respond with the recorded responses based on a provided recording + /// in the form of a YAML string. This allows users to directly use a YAML string representing + /// the recorded interactions, which can be useful for testing and debugging without needing a physical file. + /// + /// # Parameters + /// + /// * `content`: A YAML string that represents the contents of the recording file. + /// This can be any type that implements `AsRef`, such as a `&str` or `String`. + /// + /// # Returns + /// + /// * `MockSet`: An object representing the set of mocks that were loaded from the YAML string. + /// + /// # Example + /// + /// ```rust + /// use httpmock::MockServer; + /// use reqwest::blocking::Client; + /// + /// // Example YAML content representing recorded interactions. + /// let yaml_content = r#" + /// when: + /// method: GET + /// path: /recorded-mock + /// then: + /// status: 200 + /// header: + /// - name: Content-Type + /// value: application/json + /// body: '{ "response" : "hello" }' + /// "#; + /// + /// // Create the mock server. + /// let mock_server = MockServer::start(); + /// + /// // Play back the recorded interactions from the YAML string. + /// mock_server.playback_from_yaml(yaml_content); + /// + /// // Use the reqwest HTTP client to send a request to the mock server. + /// let client = Client::new(); + /// + /// let response = client + /// .get(&format!("{}/recorded-mock", mock_server.base_url())) // Build the full URL using the mock server's base URL + /// .send() // Send the GET request + /// .unwrap(); // Unwrap the result, assuming the request is successful + /// + /// assert_eq!(response.headers().get("Content-Type").unwrap(), "application/json"); + /// assert_eq!(response.text().unwrap(), r#"{ "response" : "hello" }"#); + /// ``` + /// + /// # Feature + /// + /// This method is only available when the `record` feature is enabled. + #[cfg(feature = "record")] + pub fn playback_from_yaml>(&self, content: AsStrRef) -> MockSet { + self.playback_from_yaml_async(content).join() + } + + /// Asynchronously configures the mock server to respond with the recorded responses based on a provided recording + /// in the form of a YAML string. This allows users to directly use a YAML string representing + /// the recorded interactions, which can be useful for testing and debugging without needing a physical file. + /// + /// # Parameters + /// + /// * `content`: A YAML string that represents the contents of the recording file. + /// This can be any type that implements `AsRef`, such as a `&str` or `String`. + /// + /// # Returns + /// + /// * `MockSet`: An object representing the set of mocks that were loaded from the YAML string. + /// + /// # Example + /// + /// ```rust + /// use tokio::runtime::Runtime; // Import tokio for asynchronous runtime + /// use httpmock::MockServer; + /// use reqwest::Client; + /// + /// // Example YAML content representing a recording. + /// let yaml_content = r#" + /// when: + /// method: GET + /// path: /recorded-mock + /// then: + /// status: 200 + /// body: '{ "response" : "hello" }' + /// "#; + /// + /// let rt = Runtime::new().unwrap(); + /// rt.block_on(async { + /// // Create the mock server. + /// let mock_server = MockServer::start_async().await; + /// + /// // Play back the recorded interactions from the YAML string. + /// mock_server.playback_from_yaml_async(yaml_content).await; + /// + /// // Use reqwest to send an asynchronous request to the mock server. + /// let client = Client::new(); + /// + /// let response = client + /// .get(&format!("{}/recorded-mock", mock_server.base_url())) + /// .send() + /// .await + /// .unwrap(); + /// + /// assert_eq!(response.text().await.unwrap(), r#"{ "response" : "hello" }"#); + /// }); + /// ``` + /// + /// # Feature + /// + /// This method is only available when the `record` feature is enabled. + #[cfg(feature = "record")] + pub async fn playback_from_yaml_async>( + &self, + content: AsStrRef, + ) -> MockSet { + let response = self + .server_adapter + .as_ref() + .unwrap() + .create_mocks_from_recording(content.as_ref()) + .await + .expect("Cannot deserialize mock server response"); + + MockSet { + ids: response, + server: self, } } } +/// Implements the `Drop` trait for `MockServer`. +/// When a `MockServer` instance goes out of scope, this method is called automatically to manage resources. impl Drop for MockServer { + /// This method will returns the mock server to the pool of mock servers. The mock server is not cleaned immediately. + /// Instead, it will be reset and cleaned when `MockServer::start()` is called again, preparing it for reuse by another test. + /// + /// # Important Considerations + /// + /// Users should be aware that when a `MockServer` instance is dropped, the server is not immediately cleaned. + /// The actual reset and cleaning of the server happen when `MockServer::start()` is called again, making it ready for reuse. + /// + /// # Feature + /// + /// This behavior is part of the `MockServer` struct and does not require any additional features to be enabled. fn drop(&mut self) { let adapter = self.server_adapter.take().unwrap(); self.pool.put(adapter).join(); @@ -336,33 +1342,48 @@ impl Drop for MockServer { } const LOCAL_SERVER_ADAPTER_GENERATOR: fn() -> Arc = || { - let (addr_sender, addr_receiver) = tokio::sync::oneshot::channel::(); - let state = Arc::new(MockServerState::default()); - let server_state = state.clone(); + let (addr_sender, addr_receiver) = channel::(); + let state_manager = Arc::new(HttpMockStateManager::default()); + let srv = HttpMockServerBuilder::new() + .build_with_state(state_manager.clone()) + .expect("cannot build mock server"); + // TODO: Check how we can improve here to not create a Tokio runtime on the current thread per MockServer. + // Can we create one runtime and use it for all servers? thread::spawn(move || { - let server_state = server_state.clone(); - let srv = start_server(0, false, &server_state, Some(addr_sender), false, pending()); - - let mut runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("Cannot build local tokio runtime"); - - LocalSet::new().block_on(&mut runtime, srv) + let server_fn = srv.start_with_signals(Some(addr_sender), pending()); + runtime::block_on_current_thread(server_fn).expect("Server execution failed"); }); let addr = addr_receiver.join().expect("Cannot get server address"); - Arc::new(LocalMockServerAdapter::new(addr, state)) + Arc::new(LocalMockServerAdapter::new(addr, state_manager)) }; lazy_static! { static ref LOCAL_SERVER_POOL_REF: Arc>> = { let max_servers = read_env("HTTPMOCK_MAX_SERVERS", "25") .parse::() - .expect("Cannot parse environment variable HTTPMOCK_MAX_SERVERS to an integer"); + .expect("Cannot parse environment variable HTTPMOCK_MAX_SERVERS as an integer"); Arc::new(Pool::new(max_servers)) }; static ref REMOTE_SERVER_POOL_REF: Arc>> = Arc::new(Pool::new(1)); } + +#[cfg(feature = "remote")] +lazy_static! { + // TODO: REFACTOR to use a runtime agnostic HTTP client for remote access. + // This solution does not require OpenSSL and less dependencies compared to + // other HTTP clients (tested: isahc, surf). Curl seems to use OpenSSL by default, + // so this is not an option. Optimally, the HTTP client uses rustls to avoid the + // dependency on OpenSSL installed on the OS. + static ref REMOTE_SERVER_CLIENT: Arc = { + let max_workers = read_env("HTTPMOCK_HTTP_CLIENT_WORKER_THREADS", "1") + .parse::() + .expect("Cannot parse environment variable HTTPMOCK_HTTP_CLIENT_WORKER_THREADS as an integer"); + let max_blocking_threads = read_env("HTTPMOCK_HTTP_CLIENT_MAX_BLOCKING_THREADS", "10") + .parse::() + .expect("Cannot parse environment variable HTTPMOCK_HTTP_CLIENT_MAX_BLOCKING_THREADS to an integer"); + Arc::new(HttpMockHttpClient::new(Some(Arc::new(runtime::new(max_workers,max_blocking_threads).unwrap())))) + }; +} diff --git a/src/api/spec.rs b/src/api/spec.rs index 2071f6b2..c8afe754 100644 --- a/src/api/spec.rs +++ b/src/api/spec.rs @@ -1,15 +1,17 @@ -use crate::common::data::{ - MockMatcherFunction, MockServerHttpResponse, Pattern, RequestRequirements, +use crate::{ + common::{ + data::{MockServerHttpResponse, RequestRequirements}, + util::{get_test_resource_file_path, read_file, update_cell, HttpMockBytes}, + }, + prelude::HttpMockRequest, + Method, Regex, }; -use crate::common::util::{get_test_resource_file_path, read_file, update_cell}; -use crate::{Method, Regex}; +use bytes::Bytes; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::cell::Cell; -use std::path::Path; -use std::rc::Rc; -use std::str::FromStr; -use std::time::Duration; +use std::{ + cell::Cell, convert::TryInto, path::Path, rc::Rc, str::FromStr, sync::Arc, time::Duration, +}; /// A function that encapsulates one or more /// [`When`](When) method calls as an abstraction @@ -21,910 +23,5036 @@ pub type AndWhenFunction = fn(When) -> When; /// or convenience pub type AndThenFunction = fn(Then) -> Then; -/// A type that allows the specification of HTTP request values. +/// Represents the conditions that an incoming HTTP request must satisfy to be handled by the mock server. +/// +/// The `When` structure is used exclusively to define the expectations for HTTP requests. It allows +/// the configuration of various aspects of the request such as paths, headers, methods, and more. +/// These specifications determine whether a request matches the mock setup and should be handled accordingly. +/// This structure is part of the setup process in creating a mock server, typically used before defining the response +/// behavior with a `Then` structure. pub struct When { pub(crate) expectations: Rc>, } impl When { - /// Sets the mock server to respond to any incoming request. + /// Configures the mock server to respond to any incoming request, regardless of the URL path, + /// query parameters, headers, or method. + /// + /// This method doesn't directly alter the behavior of the mock server, as it already responds to any + /// request by default. However, it serves as an explicit indication in your code that the + /// server will respond to any request. /// /// # Example - /// ``` + /// + /// ```rust /// use httpmock::prelude::*; /// + /// // Start a new mock server /// let server = MockServer::start(); /// - /// let mock = server.mock(|when, then|{ - /// when.any_request(); - /// then.status(200); + /// // Configure the mock server to respond to any request + /// let mock = server.mock(|when, then| { + /// when.any_request(); // Explicitly specify that any request should match + /// then.status(200); // Respond with status code 200 for all matched requests /// }); /// - /// isahc::get(server.url("/anyPath")).unwrap(); + /// // Make a request to the server's URL and ensure the mock is triggered + /// let response = reqwest::blocking::get(server.url("/anyPath")).unwrap(); + /// + /// // Ensure the request was successful + /// assert_eq!(response.status(), 200); /// + /// // Assert that the mock was called at least once /// mock.assert(); /// ``` + /// + /// # Note + /// This is the default behavior as of now, but it may change in future versions. + /// + /// # Returns + /// The updated `When` instance to enable method chaining. + /// pub fn any_request(self) -> Self { // This method does nothing. It only exists to make it very explicit that // the mock server will respond to any request. This is the default at this time, but // may change in the future. self } + // @docs-group: Miscellaneous - /// Sets the expected HTTP method. + /// Specifies the scheme (e.g., "http" or "https") that requests must match for the mock server to respond. + /// + /// This method sets the scheme to filter requests and ensures that the mock server only matches + /// requests with the specified scheme. This allows for more precise testing in environments where + /// multiple protocols are used. /// - /// * `method` - The HTTP method (a [Method](enum.Method.html) or a `String`). + /// **Note**: Scheme matching is case-insensitive, conforming to + /// [RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2). /// /// # Example - /// ``` + /// + /// ```rust /// use httpmock::prelude::*; /// + /// // Start a new mock server /// let server = MockServer::start(); /// - /// let mock = server.mock(|when, then|{ - /// when.method(GET); - /// then.status(200); + /// // Create a mock that only matches requests with the "http" scheme + /// let mock = server.mock(|when, then| { + /// when.scheme("http"); // Restrict to the "http" scheme + /// then.status(200); // Respond with status code 200 for all matched requests /// }); /// - /// isahc::get(server.url("/")).unwrap(); + /// // Make an "http" request to the server's URL to trigger the mock + /// let response = reqwest::blocking::get(server.url("/test")).unwrap(); /// + /// // Ensure the request was successful + /// assert_eq!(response.status(), 200); + /// + /// // Verify that the mock was called at least once /// mock.assert(); /// ``` - pub fn method>(mut self, method: M) -> Self { + /// + /// # Parameters + /// - `scheme`: A string specifying the scheme that requests should match. Common values include "http" and "https". + /// + /// # Returns + /// The modified `When` instance to allow for method chaining. + /// + pub fn scheme>(mut self, scheme: TryIntoString) -> Self + where + >::Error: std::fmt::Debug, + { + let scheme = scheme + .try_into() + .expect("cannot convert scheme into a string"); update_cell(&self.expectations, |e| { - e.method = Some(method.into().to_string()) + e.scheme = Some(scheme); }); self } + // @docs-group: Scheme - /// Sets the expected URL path. - /// * `path` - The URL path. + /// Specifies a scheme (e.g., "https") that requests must not match for the mock server to respond. + /// + /// This method allows you to exclude specific schemes from matching, ensuring that the mock server + /// won't respond to requests using those protocols. This is useful when you want to mock server + /// behavior based on protocol security requirements or other criteria. + /// + /// **Note**: Scheme matching is case-insensitive, conforming to + /// [RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2). /// /// # Example - /// ``` + /// + /// ```rust /// use httpmock::prelude::*; /// + /// // Start a new mock server /// let server = MockServer::start(); /// - /// let mock = server.mock(|when, then|{ - /// when.path("/test"); - /// then.status(200); + /// // Create a mock that will only match requests that do not use the "https" scheme + /// let mock = server.mock(|when, then| { + /// when.scheme_not("https"); // Exclude the "https" scheme from matching + /// then.status(200); // Respond with status code 200 for all matched requests /// }); /// - /// isahc::get(server.url("/test")).unwrap(); + /// // Make a request to the server's URL with the "http" scheme to trigger the mock + /// let response = reqwest::blocking::get(server.url("/test")).unwrap(); + /// + /// // Ensure the request was successful + /// assert_eq!(response.status(), 200); /// + /// // Ensure that the mock was called at least once /// mock.assert(); /// ``` - pub fn path>(mut self, path: S) -> Self { + /// + /// # Parameters + /// - `scheme`: A string specifying the scheme that requests should not match. Common values include "http" and "https". + /// + /// # Returns + /// The modified `When` instance to allow for method chaining. + /// + pub fn scheme_not>(mut self, scheme: TryIntoString) -> Self + where + >::Error: std::fmt::Debug, + { + let scheme = scheme + .try_into() + .expect("cannot convert scheme into a string"); update_cell(&self.expectations, |e| { - e.path = Some(path.into()); + e.scheme_not = Some(scheme); }); self } + // @docs-group: Scheme - /// Sets an substring that the URL path needs to contain. - /// * `substring` - The substring to match against. + /// Sets the expected HTTP method for which the mock server should respond. + /// + /// This method ensures that the mock server only matches requests that use the specified HTTP method, + /// such as `GET`, `POST`, or any other valid method. This allows testing behavior that's specific + /// to different types of HTTP requests. + /// + /// **Note**: Method matching is case-insensitive. /// /// # Example - /// ``` + /// + /// ```rust /// use httpmock::prelude::*; /// + /// // Start a new mock server /// let server = MockServer::start(); /// - /// let mock = server.mock(|when, then|{ - /// when.path_contains("es"); - /// then.status(200); + /// // Create a mock that matches only `GET` requests + /// let mock = server.mock(|when, then| { + /// when.method(GET); // Match only `GET` HTTP method + /// then.status(200); // Respond with status code 200 for all matched requests /// }); /// - /// isahc::get(server.url("/test")).unwrap(); + /// // Make a GET request to the server's URL to trigger the mock + /// let response = reqwest::blocking::get(server.url("/")).unwrap(); + /// + /// // Ensure the request was successful + /// assert_eq!(response.status(), 200); /// + /// // Verify that the mock was called at least once /// mock.assert(); /// ``` - pub fn path_contains>(mut self, substring: S) -> Self { - update_cell(&self.expectations, |e| { - if e.path_contains.is_none() { - e.path_contains = Some(Vec::new()); - } - e.path_contains.as_mut().unwrap().push(substring.into()); - }); + /// + /// # Parameters + /// - `method`: An HTTP method as either a `Method` enum or a `String` value, specifying the expected method type for matching. + /// + /// # Returns + /// The updated `When` instance to allow for method chaining. + /// + pub fn method>(mut self, method: TryIntoMethod) -> Self + where + >::Error: std::fmt::Debug, + { + let method = method + .try_into() + .expect("cannot convert method into httpmock::Method"); + + update_cell(&self.expectations, |e| e.method = Some(method.to_string())); self } + // @docs-group: Method - /// Sets a regex that the URL path needs to match. - /// * `regex` - The regex to match against. + /// Excludes the specified HTTP method from the requests the mock server will respond to. + /// + /// This method ensures that the mock server does not respond to requests using the given HTTP method, + /// like `GET`, `POST`, etc. This allows testing scenarios where a particular method should not + /// trigger a response, and thus testing behaviors like method-based security. + /// + /// **Note**: Method matching is case-insensitive. /// /// # Example - /// ``` + /// + /// ```rust /// use httpmock::prelude::*; /// + /// // Start a new mock server /// let server = MockServer::start(); /// - /// let mock = server.mock(|when, then|{ - /// when.path_matches(Regex::new("le$").unwrap()); - /// then.status(200); + /// // Create a mock that matches any request except those using the `POST` method + /// let mock = server.mock(|when, then| { + /// when.method_not(POST); // Exclude the `POST` HTTP method from matching + /// then.status(200); // Respond with status code 200 for all other matched requests /// }); /// - /// isahc::get(server.url("/example")).unwrap(); + /// // Make a GET request to the server's URL, which will trigger the mock + /// let response = reqwest::blocking::get(server.url("/")).unwrap(); /// + /// // Ensure the request was successful + /// assert_eq!(response.status(), 200); + /// + /// // Ensure that the mock was called at least once /// mock.assert(); /// ``` - pub fn path_matches>(mut self, regex: R) -> Self { + /// + /// # Parameters + /// - `method`: An HTTP method as either a `Method` enum or a `String` value, specifying the method type to exclude from matching. + /// + /// # Returns + /// The updated `When` instance to allow for method chaining. + /// + pub fn method_not>(mut self, method: IntoMethod) -> Self { update_cell(&self.expectations, |e| { - if e.path_matches.is_none() { - e.path_matches = Some(Vec::new()); + if e.method_not.is_none() { + e.method_not = Some(Vec::new()); } - e.path_matches + e.method_not .as_mut() .unwrap() - .push(Pattern::from_regex(regex.into())); + .push(method.into().to_string()); }); + self } + // @docs-group: Method - /// Sets a query parameter that needs to be provided. + /// Sets the expected host name. This constraint is especially useful when working with + /// proxy or forwarding rules, but it can also be used to serve mocks (e.g., when using a mock + /// server as a proxy). /// - /// Attention!: The request query keys and values are implicitly *allowed, but is not required* - /// to be urlencoded! The value you pass here, however, must be in plain text (i.e. not encoded)! + /// **Note**: Host matching is case-insensitive, conforming to + /// [RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2). + /// This standard dictates that all host names are treated equivalently, regardless of character case. /// - /// * `name` - The query parameter name that will matched against. - /// * `value` - The value parameter name that will matched against. + /// **Note**: Both `localhost` and `127.0.0.1` are treated equally. + /// If the provided host is set to either `localhost` or `127.0.0.1`, it will match + /// requests containing either `localhost` or `127.0.0.1`. /// - /// ``` - /// // Arrange - /// use isahc::get; + /// * `host` - The host name (should not include a port). + /// + /// # Example + /// ```rust /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; /// - /// let _ = env_logger::try_init(); /// let server = MockServer::start(); /// - /// let m = server.mock(|when, then|{ - /// when.query_param("query", "Metallica is cool"); - /// then.status(200); + /// server.mock(|when, then| { + /// when.host("github.com"); + /// then.body("This is a mock response"); /// }); /// - /// // Act - /// get(server.url("/search?query=Metallica+is+cool")).unwrap(); + /// let client = Client::builder() + /// .proxy(reqwest::Proxy::all(&server.base_url()).unwrap()) + /// .build() + /// .unwrap(); /// - /// // Assert - /// m.assert(); + /// let response = client.get("http://github.com").send().unwrap(); + /// + /// assert_eq!(response.text().unwrap(), "This is a mock response"); /// ``` - pub fn query_param, SV: Into>(mut self, name: SK, value: SV) -> Self { - update_cell(&self.expectations, |e| { - if e.query_param.is_none() { - e.query_param = Some(Vec::new()); - } - e.query_param - .as_mut() - .unwrap() - .push((name.into(), value.into())); - }); + /// + /// # Returns + /// The updated `When` instance to enable method chaining. + /// + pub fn host>(mut self, host: IntoString) -> Self { + update_cell(&self.expectations, |e| e.host = Some(host.into())); self } + // @docs-group: Host - /// Sets a query parameter that needs to exist in an HTTP request. + /// Sets the host name that should **NOT** be responded for. /// - /// Attention!: The request query key is implicitly *allowed, but is not required* to be - /// urlencoded! The value you pass here, however, must be in plain text (i.e. not encoded)! + /// This constraint is especially useful when working with proxy or forwarding rules, but it + /// can also be used to serve mocks (e.g., when using a mock server as a proxy). /// - /// * `name` - The query parameter name that will matched against. + /// To add multiple suffixes, invoke this function multiple times. /// - /// ``` - /// // Arrange - /// use isahc::get; + /// **Note**: Host matching is case-insensitive, conforming to + /// [RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2). + /// This standard dictates that all host names are treated equivalently, regardless of character case. + /// + /// * `host` - The host name (should not include a port). + /// + /// # Example + /// ```rust /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; /// - /// let _ = env_logger::try_init(); /// let server = MockServer::start(); /// - /// let m = server.mock(|when, then| { - /// when.query_param_exists("query"); - /// then.status(200); + /// server.mock(|when, then| { + /// when.host("github.com"); + /// then.body("This is a mock response"); /// }); /// - /// // Act - /// get(server.url("/search?query=Metallica")).unwrap(); + /// let client = Client::builder() + /// .proxy(reqwest::Proxy::all(&server.base_url()).unwrap()) + /// .build() + /// .unwrap(); /// - /// // Assert - /// m.assert(); + /// let response = client.get("http://github.com").send().unwrap(); + /// + /// assert_eq!(response.text().unwrap(), "This is a mock response"); /// ``` - pub fn query_param_exists>(mut self, name: S) -> Self { + /// + /// # Returns + /// The updated `When` instance to enable method chaining. + /// + pub fn host_not>(mut self, host: IntoString) -> Self { update_cell(&self.expectations, |e| { - if e.query_param_exists.is_none() { - e.query_param_exists = Some(Vec::new()); + if e.host_not.is_none() { + e.host_not = Some(Vec::new()); } - e.query_param_exists.as_mut().unwrap().push(name.into()); + e.host_not.as_mut().unwrap().push(host.into()); }); self } + // @docs-group: Host - /// Sets a requirement for a tuple in an x-www-form-urlencoded request body. - /// Please refer to https://url.spec.whatwg.org/#application/x-www-form-urlencoded for more - /// information. - /// ``` + /// Adds a substring to match within the request's host name. + /// + /// This method ensures that the mock server only matches requests whose host name contains the specified substring. + /// + /// This constraint is especially useful when working with proxy or forwarding rules, but it + /// can also be used to serve mocks (e.g., when using a mock server as a proxy). + /// + /// To add multiple substrings, invoke this function multiple times. + /// + /// **Note**: Host matching is case-insensitive, conforming to + /// [RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2). + /// This standard dictates that all host names are treated equivalently, regardless of character case. + /// + /// **Note**: This function does not automatically compare with pseudo names, like "localhost". + /// + /// # Attention + /// This function does not automatically treat 127.0.0.1 like localhost. + /// + /// # Example + /// + /// ```rust /// use httpmock::prelude::*; - /// use isahc::{prelude::*, Request}; /// - /// // Arrange + /// // Start a new mock server /// let server = MockServer::start(); /// - /// let m = server.mock(|when, then| { - /// when.method(POST) - /// .path("/example") - /// .header("content-type", "application/x-www-form-urlencoded") - /// .x_www_form_urlencoded_tuple("name", "Peter Griffin") - /// .x_www_form_urlencoded_tuple("town", "Quahog"); - /// then.status(202); + /// // Create a mock that matches any request where the host name contains "localhost" + /// let mock = server.mock(|when, then| { + /// when.host_includes("0.0"); // Only match hosts containing "0.0" (e.g., 127.0.0.1) + /// then.status(200); // Respond with status code 200 for all matched requests /// }); /// - /// let response = Request::post(server.url("/example")) - /// .header("content-type", "application/x-www-form-urlencoded") - /// .body("name=Peter%20Griffin&town=Quahog") - /// .unwrap() - /// .send() - /// .unwrap(); + /// // Make a request to a URL whose host name is "localhost" to trigger the mock + /// let response = reqwest::blocking::get(server.url("/test")).unwrap(); /// - /// // Assert - /// m.assert(); - /// assert_eq!(response.status(), 202); + /// // Ensure the request was successful + /// assert_eq!(response.status(), 200); + /// + /// // Ensure that the mock was called at least once + /// mock.assert(); /// ``` - pub fn x_www_form_urlencoded_tuple, SV: Into>( - mut self, - key: SK, - value: SV, - ) -> Self { + /// + /// # Parameters + /// - `host`: A string or other type convertible to `String` that will be added as a substring to match against the request's host name. + /// + /// # Returns + /// The updated `When` instance to enable method chaining. + /// + pub fn host_includes>(mut self, host: IntoString) -> Self { update_cell(&self.expectations, |e| { - if e.x_www_form_urlencoded.is_none() { - e.x_www_form_urlencoded = Some(Vec::new()); + if e.host_contains.is_none() { + e.host_contains = Some(Vec::new()); } - e.x_www_form_urlencoded - .as_mut() - .unwrap() - .push((key.into(), value.into())); + e.host_contains.as_mut().unwrap().push(host.into()); }); self } + // @docs-group: Host - /// Sets a requirement for a tuple key in an x-www-form-urlencoded request body. - /// Please refer to https://url.spec.whatwg.org/#application/x-www-form-urlencoded for more - /// information. - /// ``` + /// Adds a substring that must not be present within the request's host name for the mock server to respond. + /// + /// This method ensures that the mock server does not respond to requests if the host name contains the specified substring. + /// + /// This constraint is especially useful when working with proxy or forwarding rules, but it + /// can also be used to serve mocks (e.g., when using a mock server as a proxy). + /// + /// To add multiple excluded substrings, invoke this function multiple times. + /// + /// **Note**: Host matching is case-insensitive, conforming to + /// [RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2). + /// This standard dictates that all host names are treated equivalently, regardless of character case. + /// + /// **Note**: This function does not automatically compare with pseudo names, like "localhost". + /// + /// # Example + /// + /// ```rust /// use httpmock::prelude::*; - /// use isahc::{prelude::*, Request}; /// - /// // Arrange + /// // Start a new mock server /// let server = MockServer::start(); /// - /// let m = server.mock(|when, then| { - /// when.method(POST) - /// .path("/example") - /// .header("content-type", "application/x-www-form-urlencoded") - /// .x_www_form_urlencoded_key_exists("name") - /// .x_www_form_urlencoded_key_exists("town"); - /// then.status(202); + /// // Create a mock that excludes any request where the host name contains "www.google.com" + /// let mock = server.mock(|when, then| { + /// when.host_excludes("www.google.com"); // Exclude hosts containing "www.google.com" + /// then.status(200); // Respond with status code 200 for other matched requests /// }); /// - /// let response = Request::post(server.url("/example")) - /// .header("content-type", "application/x-www-form-urlencoded") - /// .body("name=Peter%20Griffin&town=Quahog") - /// .unwrap() - /// .send() - /// .unwrap(); + /// // Make a request to a URL whose host name will be "localhost" and trigger the mock + /// let response = reqwest::blocking::get(server.url("/test")).unwrap(); /// - /// // Assert - /// m.assert(); - /// assert_eq!(response.status(), 202); + /// // Ensure the request was successful + /// assert_eq!(response.status(), 200); + /// + /// // Ensure that the mock was called at least once + /// mock.assert(); /// ``` - pub fn x_www_form_urlencoded_key_exists>(mut self, key: S) -> Self { + /// + /// # Parameters + /// - `host`: A string or other type convertible to `String` that will be added as a substring to exclude from matching. + /// + /// # Returns + /// The updated `When` instance to enable method chaining. + /// + pub fn host_excludes>(mut self, host: IntoString) -> Self { update_cell(&self.expectations, |e| { - if e.x_www_form_urlencoded_key_exists.is_none() { - e.x_www_form_urlencoded_key_exists = Some(Vec::new()); + if e.host_excludes.is_none() { + e.host_excludes = Some(Vec::new()); } - e.x_www_form_urlencoded_key_exists - .as_mut() - .unwrap() - .push(key.into()); + e.host_excludes.as_mut().unwrap().push(host.into()); }); self } + // @docs-group: Host - /// Sets the required HTTP request body content. + /// Adds a prefix that the request's host name must start with for the mock server to respond. + /// + /// This constraint is especially useful when working with proxy or forwarding rules, but it + /// can also be used to serve mocks (e.g., when using a mock server as a proxy). + /// + /// To add multiple prefixes, invoke this function multiple times. + /// + /// **Note**: Host matching is case-insensitive, conforming to + /// [RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2). + /// This standard dictates that all host names are treated equivalently, regardless of character case. /// - /// * `body` - The required HTTP request body. + /// **Note**: This function does not automatically compare with pseudo names, like "localhost". /// /// # Example - /// ``` + /// + /// ```rust /// use httpmock::prelude::*; - /// use isahc::{prelude::*, Request}; /// + /// // Start a new mock server /// let server = MockServer::start(); /// - /// let mock = server.mock(|when, then|{ - /// when.body("The Great Gatsby"); - /// then.status(200); + /// // Create a mock that matches any request where the host name starts with "local" + /// let mock = server.mock(|when, then| { + /// when.host_prefix("127.0"); // Only match hosts starting with "127.0" + /// then.status(200); // Respond with status code 200 for all matched requests /// }); /// - /// Request::post(&format!("http://{}/test", server.address())) - /// .body("The Great Gatsby") - /// .unwrap() - /// .send() - /// .unwrap(); + /// // Make a request to the mock server with a host name of "127.0.0.1" to trigger the mock response. + /// let response = reqwest::blocking::get(server.url("/test")).unwrap(); /// + /// // Ensure the request was successful + /// assert_eq!(response.status(), 200); + /// + /// // Ensure that the mock was called at least once /// mock.assert(); /// ``` - pub fn body>(mut self, body: S) -> Self { + /// + /// # Parameters + /// - `prefix`: A string or other type convertible to `String` specifying the prefix that the host name should start with. + /// + /// # Returns + /// The updated `When` instance to enable method chaining. + /// + pub fn host_prefix>(mut self, host: IntoString) -> Self { update_cell(&self.expectations, |e| { - e.body = Some(body.into()); + if e.host_prefix.is_none() { + e.host_prefix = Some(Vec::new()); + } + e.host_prefix.as_mut().unwrap().push(host.into()); }); self } + // @docs-group: Host - /// Sets a [Regex](type.Regex.html) for the expected HTTP body. + /// Adds a suffix that the request's host name must end with for the mock server to respond. /// - /// * `regex` - The regex that the HTTP request body will matched against. + /// This constraint is especially useful when working with proxy or forwarding rules, but it + /// can also be used to serve mocks (e.g., when using a mock server as a proxy). /// - /// ``` - /// use isahc::{prelude::*, Request}; - /// use httpmock::prelude::*; + /// To add multiple suffixes, invoke this function multiple times. /// - /// // Arrange - /// let _ = env_logger::try_init(); + /// **Note**: Host matching is case-insensitive, conforming to + /// [RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2). + /// This standard dictates that all host names are treated equivalently, regardless of character case. + /// + /// **Note**: This function does not automatically compare with pseudo names, like "localhost". + /// + /// # Example + /// + /// ```rust + /// use httpmock::prelude::*; /// + /// // Start a new mock server /// let server = MockServer::start(); /// - /// let m = server.mock(|when, then|{ - /// when.method(POST) - /// .path("/books") - /// .body_matches(Regex::new("Fellowship").unwrap()); - /// then.status(201); + /// // Create a mock that matches any request where the host name ends with "host" (e.g., "localhost"). + /// let mock = server.mock(|when, then| { + /// when.host_suffix("0.1"); // Only match hosts ending with "0.1" + /// then.status(200); // Respond with status code 200 for all matched requests /// }); /// - /// // Act: Send the request - /// let response = Request::post(server.url("/books")) - /// .body("The Fellowship of the Ring") - /// .unwrap() - /// .send() - /// .unwrap(); + /// // Make a request to the mock server with a host name of "127.0.0.1" to trigger the mock response. + /// let response = reqwest::blocking::get(server.url("/test")).unwrap(); /// - /// // Assert - /// m.assert(); - /// assert_eq!(response.status(), 201); + /// // Ensure the request was successful + /// assert_eq!(response.status(), 200); + /// + /// // Ensure that the mock was called at least once + /// mock.assert(); /// ``` - pub fn body_matches>(mut self, regex: R) -> Self { + /// + /// # Parameters + /// - `host`: A string or other type convertible to `String` specifying the suffix that the host name should end with. + /// + /// # Returns + /// The updated `When` instance to enable method chaining. + /// + pub fn host_suffix>(mut self, host: IntoString) -> Self { update_cell(&self.expectations, |e| { - if e.body_matches.is_none() { - e.body_matches = Some(Vec::new()); + if e.host_suffix.is_none() { + e.host_suffix = Some(Vec::new()); } - e.body_matches - .as_mut() - .unwrap() - .push(Pattern::from_regex(regex.into())); + e.host_suffix.as_mut().unwrap().push(host.into()); }); self } + // @docs-group: Host - /// Sets the expected HTTP body substring. + /// Adds a prefix that the request's host name must *not* start with for the mock server to respond. /// - /// * `substring` - The substring that will matched against. + /// This constraint is especially useful when working with proxy or forwarding rules, but it + /// can also be used to serve mocks (e.g., when using a mock server as a proxy). /// - /// ``` - /// use httpmock::prelude::*; - /// use isahc::{prelude::*, Request}; + /// To add multiple excluded prefixes, invoke this function multiple times. /// - /// // Arrange - /// let _ = env_logger::try_init(); + /// **Note**: Host matching is case-insensitive, conforming to + /// [RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2). + /// This standard dictates that all host names are treated equivalently, regardless of character case. + /// + /// **Note**: This function does not automatically compare with pseudo names, like "localhost". /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// + /// // Start a new mock server /// let server = MockServer::start(); /// - /// let m = server.mock(|when, then|{ - /// when.path("/books") - /// .body_contains("Ring"); - /// then.status(201); + /// // Create a mock that matches any request where the host name does not start with "www." + /// let mock = server.mock(|when, then| { + /// when.host_prefix_not("www."); // Exclude hosts starting with "www" + /// then.status(200); // Respond with status code 200 for all other requests /// }); /// - /// // Act: Send the request - /// let response = Request::post(server.url("/books")) - /// .body("The Fellowship of the Ring") - /// .unwrap() - /// .send() - /// .unwrap(); + /// // Make a request with host name "localhost" that does not start with "www" and therefore + /// // triggers the mock response. + /// let response = reqwest::blocking::get(server.url("/example")).unwrap(); /// - /// // Assert - /// m.assert(); - /// assert_eq!(response.status(), 201); + /// // Ensure the request was successful + /// assert_eq!(response.status(), 200); + /// + /// // Ensure that the mock was called at least once + /// mock.assert(); /// ``` - pub fn body_contains>(mut self, substring: S) -> Self { + /// + /// # Parameters + /// - `prefix`: A string or other type convertible to `String` specifying the prefix that the host name should *not* start with. + /// + /// # Returns + /// The updated `When` instance to enable method chaining. + /// + pub fn host_prefix_not>(mut self, prefix: IntoString) -> Self { update_cell(&self.expectations, |e| { - if e.body_contains.is_none() { - e.body_contains = Some(Vec::new()); + if e.host_prefix_not.is_none() { + e.host_prefix_not = Some(Vec::new()); } - e.body_contains.as_mut().unwrap().push(substring.into()); + e.host_prefix_not.as_mut().unwrap().push(prefix.into()); }); self } + // @docs-group: Host - /// Sets the expected JSON body. This method expects a [serde_json::Value](../serde_json/enum.Value.html) - /// that will be serialized/deserialized to/from a JSON string. + /// Adds a suffix that the request's host name must *not* end with for the mock server to respond. /// - /// Note that this method does not set the `content-type` header automatically, so you - /// need to provide one yourself! + /// This constraint is especially useful when working with proxy or forwarding rules, but it + /// can also be used to serve mocks (e.g., when using a mock server as a proxy). /// - /// * `body` - The HTTP body object that will be serialized to JSON using serde. + /// To add multiple excluded suffixes, invoke this function multiple times. /// - /// ``` + /// **Note**: Host matching is case-insensitive, conforming to + /// [RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2). + /// This standard dictates that all host names are treated equivalently, regardless of character case. + /// + /// **Note**: This function does not automatically compare with pseudo names, like "localhost". + /// + /// # Example + /// ```rust /// use httpmock::prelude::*; - /// use serde_json::json; - /// use isahc::{prelude::*, Request}; /// - /// // Arrange - /// let _ = env_logger::try_init(); + /// // Start a new mock server /// let server = MockServer::start(); /// - /// let m = server.mock(|when, then|{ - /// when.path("/user") - /// .header("content-type", "application/json") - /// .json_body(json!({ "name": "Hans" })); - /// then.status(201); + /// // Create a mock that matches any request where the host name does not end with "host". + /// let mock = server.mock(|when, then| { + /// when.host_suffix_not("host"); // Exclude hosts ending with "host" + /// then.status(200); // Respond with status code 200 for all other requests /// }); /// - /// // Act: Send the request and deserialize the response to JSON - /// let mut response = Request::post(&format!("http://{}/user", server.address())) - /// .header("content-type", "application/json") - /// .body(json!({ "name": "Hans" }).to_string()) - /// .unwrap() - /// .send() - /// .unwrap(); + /// // Make a request with a host name that does not end with "host" to trigger the mock response. + /// let response = reqwest::blocking::get(server.url("/example")).unwrap(); /// - /// // Assert - /// m.assert(); - /// assert_eq!(response.status(), 201); - /// ``` - pub fn json_body>(mut self, value: V) -> Self { - update_cell(&self.expectations, |e| { - e.json_body = Some(value.into()); + /// // Ensure the request was successful + /// assert_eq!(response.status(), 200); + /// + /// // Ensure that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Parameters + /// - `host`: A string or other type convertible to `String` specifying the suffix that the host name should *not* end with. + /// + /// # Returns + /// The updated `When` instance to enable method chaining. + /// + pub fn host_suffix_not>(mut self, host: IntoString) -> Self { + update_cell(&self.expectations, |e| { + if e.host_suffix_not.is_none() { + e.host_suffix_not = Some(Vec::new()); + } + e.host_suffix_not.as_mut().unwrap().push(host.into()); }); self } + // @docs-group: Host - /// Sets the expected JSON body. This method expects a serializable serde object - /// that will be serialized/deserialized to/from a JSON string. + /// Sets a regular expression pattern that the request's host name must match for the mock server to respond. + /// + /// This constraint is especially useful when working with proxy or forwarding rules, but it + /// can also be used to serve mocks (e.g., when using a mock server as a proxy). + /// + /// To add multiple patterns, invoke this function multiple times. + /// + /// **Note**: Host matching is case-insensitive, conforming to + /// [RFC 3986, Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2). + /// This standard dictates that all host names are treated equivalently, regardless of character case. + /// + /// **Note**: This function does not automatically compare with pseudo names, like "localhost". + /// + /// # Parameters + /// - `regex`: A regular expression pattern to match against the host name. Should be a valid regex string. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that matches requests where the host name is exactly "localhost" + /// let mock = server.mock(|when, then| { + /// when.host_matches(r"^127.0.0.1$"); + /// then.status(200); + /// }); /// - /// Note that this method does not set the "content-type" header automatically, so you - /// need to provide one yourself! + /// // Make a request with "127.0.0.1" as the host name to trigger the mock response. + /// let response = reqwest::blocking::get(server.url("/")).unwrap(); /// - /// * `body` - The HTTP body object that will be serialized to JSON using serde. + /// // Ensure the request was successful + /// assert_eq!(response.status(), 200); /// + /// // Verify that the mock was called at least once + /// mock.assert(); /// ``` + /// + /// # Returns + /// The updated `When` instance to enable method chaining. + /// + pub fn host_matches>(mut self, regex: IntoRegex) -> Self { + update_cell(&self.expectations, |e| { + if e.host_matches.is_none() { + e.host_matches = Some(Vec::new()); + } + e.host_matches.as_mut().unwrap().push(regex.into()); + }); + self + } + // @docs-group: Host + + /// Specifies the expected port number for incoming requests to match. + /// + /// This constraint is especially useful when working with proxy or forwarding rules, but it + /// can also be used to serve mocks (e.g., when using a mock server as a proxy). + /// + /// # Parameters + /// - `port`: A value convertible to `u16`, representing the expected port number. + /// + /// # Example + /// ```rust /// use httpmock::prelude::*; - /// use serde_json::json; - /// use isahc::{prelude::*, Request}; + /// use reqwest::blocking::Client; /// - /// // This is a temporary type that we will use for this test - /// #[derive(serde::Serialize, serde::Deserialize)] - /// struct TestUser { - /// name: String, - /// } + /// // Start a new mock server + /// let server = MockServer::start(); /// - /// // Arrange - /// let _ = env_logger::try_init(); + /// // Configure a mock to respond to requests made to `github.com` + /// // with a specific port + /// server.mock(|when, then| { + /// when.port(80); // Specify the expected port + /// then.body("This is a mock response"); + /// }); + /// + /// // Set up an HTTP client to use the mock server as a proxy + /// let client = Client::builder() + /// // Proxy all requests to the mock server + /// .proxy(reqwest::Proxy::all(&server.base_url()).unwrap()) + /// .build() + /// .unwrap(); + /// + /// // Send a GET request to `github.com` on port 80. + /// // The request will be sent to our mock server due to the HTTP client proxy settings. + /// let response = client.get("http://github.com:80").send().unwrap(); + /// + /// // Validate that the mock server returned the expected response + /// assert_eq!(response.text().unwrap(), "This is a mock response"); + /// ``` + /// + /// # Errors + /// - This function will panic if the port number cannot be converted to a valid `u16` value. + /// + /// # Returns + /// The updated `When` instance to allow method chaining. + /// + pub fn port>(mut self, port: U16) -> Self + where + >::Error: std::fmt::Debug, + { + let port: u16 = port.try_into().expect("Port value is out of range for u16"); + + update_cell(&self.expectations, |e| e.port = Some(port)); + self + } + // @docs-group: Port + + /// Specifies the port number that incoming requests must *not* match. + /// + /// This constraint is especially useful when working with proxy or forwarding rules, but it + /// can also be used to serve mocks (e.g., when using a mock server as a proxy). + /// + /// To add multiple excluded ports, invoke this function multiple times. + /// + /// # Parameters + /// - `port`: A value convertible to `u16`, representing the port number to be excluded. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server /// let server = MockServer::start(); /// - /// let m = server.mock(|when, then|{ - /// when.path("/user") - /// .header("content-type", "application/json") - /// .json_body_obj(&TestUser { - /// name: String::from("Fred"), - /// }); - /// then.status(200); + /// // Configure a mock to respond to requests not using port 81 + /// server.mock(|when, then| { + /// when.port_not(81); // Exclude requests on port 81 + /// then.body("This is a mock response"); /// }); /// - /// // Act: Send the request and deserialize the response to JSON - /// let mut response = Request::post(&format!("http://{}/user", server.address())) - /// .header("content-type", "application/json") - /// .body(json!(&TestUser { - /// name: "Fred".to_string() - /// }).to_string()) - /// .unwrap() - /// .send() + /// // Set up an HTTP client to use the mock server as a proxy + /// let client = Client::builder() + /// .proxy(reqwest::Proxy::all(&server.base_url()).unwrap()) + /// .build() /// .unwrap(); /// - /// // Assert - /// m.assert(); + /// // Make a request to `github.com` on port 80, which will trigger + /// // the mock response + /// let response = client.get("http://github.com:80").send().unwrap(); + /// + /// // Validate that the mock server returned the expected response + /// assert_eq!(response.text().unwrap(), "This is a mock response"); + /// ``` + /// + /// # Errors + /// - This function will panic if the port number cannot be converted to a valid `u16` value. + /// + /// # Returns + /// The updated `When` instance to enable method chaining. + /// + pub fn port_not>(mut self, port: U16) -> Self + where + >::Error: std::fmt::Debug, + { + let port: u16 = port.try_into().expect("Port value is out of range for u16"); + + update_cell(&self.expectations, |e| { + if e.port_not.is_none() { + e.port_not = Some(Vec::new()); + } + e.port_not.as_mut().unwrap().push(port); + }); + self + } + // @docs-group: Port + + /// Specifies the expected URL path that incoming requests must match for the mock server to respond. + /// This is useful for targeting specific endpoints, such as API routes, to ensure only relevant requests trigger the mock response. + /// + /// # Parameters + /// - `path`: A string or other value convertible to `String` that represents the expected URL path. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that matches requests to `/test` + /// let mock = server.mock(|when, then| { + /// when.path("/test"); + /// then.status(200); // Respond with a 200 status code + /// }); + /// + /// // Make a request to the mock server using the specified path + /// let response = reqwest::blocking::get(server.url("/test")).unwrap(); + /// + /// // Ensure the request was successful /// assert_eq!(response.status(), 200); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); /// ``` - pub fn json_body_obj<'a, T>(self, body: &T) -> Self + /// + /// # Returns + /// The updated `When` instance, allowing method chaining for additional configuration. + /// + pub fn path>(mut self, path: TryIntoString) -> Self where - T: Serialize + Deserialize<'a>, + >::Error: std::fmt::Debug, { - let json_value = serde_json::to_value(body).expect("Cannot serialize json body to JSON"); - self.json_body(json_value) + let path = path.try_into().expect("cannot convert path into a string"); + update_cell(&self.expectations, |e| { + e.path = Some(path); + }); + self } + // @docs-group: Path - /// Sets the expected partial JSON body. + /// Specifies the URL path that incoming requests must *not* match for the mock server to respond. + /// This is helpful when you need to exclude specific endpoints while allowing others through. /// - /// **Attention: The partial string needs to be a valid JSON string. It must contain - /// the full object hierarchy from the original JSON object but can leave out irrelevant - /// attributes (see example).** + /// To add multiple excluded paths, invoke this function multiple times. /// - /// Note that this method does not set the `content-type` header automatically, so you - /// need to provide one yourself! + /// # Parameters + /// - `path`: A string or other value convertible to `String` that represents the URL path to exclude. /// - /// String format and attribute order are irrelevant. + /// # Example + /// ```rust + /// use httpmock::prelude::*; /// - /// * `partial_body` - The HTTP body object that will be serialized to JSON using serde. + /// // Start a new mock server + /// let server = MockServer::start(); /// - /// ## Example - /// Suppose your application sends the following JSON request body: - /// ```json - /// { - /// "parent_attribute" : "Some parent data goes here", - /// "child" : { - /// "target_attribute" : "Example", - /// "other_attribute" : "Another value" - /// } - /// } + /// // Create a mock that will not match requests to `/exclude` + /// let mock = server.mock(|when, then| { + /// when.path_not("/exclude"); + /// then.status(200); // Respond with status 200 for all other paths + /// }); + /// + /// // Make a request to a path that does not match the exclusion + /// let response = reqwest::blocking::get(server.url("/include")).unwrap(); + /// + /// // Ensure the request was successful + /// assert_eq!(response.status(), 200); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); /// ``` - /// If we only want to verify that `target_attribute` has value `Example` without the need - /// to provide a full JSON object, we can use this method as follows: + /// + /// # Returns + /// The updated `When` instance, allowing method chaining for further configuration. + /// + pub fn path_not>(mut self, path: TryIntoString) -> Self + where + >::Error: std::fmt::Debug, + { + let path = path.try_into().expect("cannot convert path into string"); + update_cell(&self.expectations, |e| { + if e.path_not.is_none() { + e.path_not = Some(Vec::new()); + } + e.path_not.as_mut().unwrap().push(path); + }); + self + } + // @docs-group: Path + + /// Specifies a substring that the URL path must contain for the mock server to respond. + /// This constraint is useful for matching URLs based on partial segments, especially when exact path matching isn't required. + /// + /// # Parameters + /// - `substring`: A string or any value convertible to `String` representing the substring that must be present in the URL path. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that matches any path containing the substring "es" + /// let mock = server.mock(|when, then| { + /// when.path_includes("es"); + /// then.status(200); // Respond with a 200 status code for matched requests + /// }); + /// + /// // Make a request to a path containing "es" to trigger the mock response + /// let response = reqwest::blocking::get(server.url("/test")).unwrap(); + /// + /// // Ensure the request was successful + /// assert_eq!(response.status(), 200); + /// + /// // Ensure that the mock was called at least once + /// mock.assert(); /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for further configuration. + /// + pub fn path_includes>(mut self, substring: TryIntoString) -> Self + where + >::Error: std::fmt::Debug, + { + let substring = substring + .try_into() + .expect("cannot convert substring into string"); + update_cell(&self.expectations, |e| { + if e.path_includes.is_none() { + e.path_includes = Some(Vec::new()); + } + e.path_includes.as_mut().unwrap().push(substring); + }); + self + } + // @docs-group: Path + + /// Specifies a substring that the URL path must *not* contain for the mock server to respond. + /// This constraint is useful for excluding requests to paths containing particular segments or patterns. + /// + /// # Parameters + /// - `substring`: A string or other value convertible to `String` representing the substring that should not appear in the URL path. + /// + /// # Example + /// ```rust /// use httpmock::prelude::*; /// + /// // Start a new mock server /// let server = MockServer::start(); /// - /// let mut mock = server.mock(|when, then|{ - /// when.json_body_partial(r#" - /// { - /// "child" : { - /// "target_attribute" : "Example" - /// } - /// } - /// "#); - /// then.status(200); + /// // Create a mock that matches any path not containing the substring "xyz" + /// let mock = server.mock(|when, then| { + /// when.path_excludes("xyz"); + /// then.status(200); // Respond with status 200 for paths excluding "xyz" /// }); + /// + /// // Make a request to a path that does not contain "xyz" + /// let response = reqwest::blocking::get(server.url("/testpath")).unwrap(); + /// + /// // Ensure the request was successful + /// assert_eq!(response.status(), 200); + /// + /// // Ensure the mock server returned the expected response + /// mock.assert(); /// ``` - /// Please note that the JSON partial contains the full object hierarchy, i.e. it needs to start - /// from the root! It leaves out irrelevant attributes, however (`parent_attribute` - /// and `child.other_attribute`). - pub fn json_body_partial>(mut self, partial: S) -> Self { + /// + /// # Returns + /// The updated `When` instance to enable method chaining for additional configuration. + /// + pub fn path_excludes>(mut self, substring: TryIntoString) -> Self + where + >::Error: std::fmt::Debug, + { + let substring = substring + .try_into() + .expect("cannot convert substring into string"); update_cell(&self.expectations, |e| { - if e.json_body_includes.is_none() { - e.json_body_includes = Some(Vec::new()); + if e.path_excludes.is_none() { + e.path_excludes = Some(Vec::new()); } - let value = Value::from_str(&partial.into()) - .expect("cannot convert JSON string to serde value"); - e.json_body_includes.as_mut().unwrap().push(value); + e.path_excludes.as_mut().unwrap().push(substring); + }); + self + } + // @docs-group: Path + + /// Specifies a prefix that the URL path must start with for the mock server to respond. + /// This is useful when only the initial segments of a path need to be validated, such as checking specific API routes. + /// + /// # Parameters + /// - `prefix`: A string or other value convertible to `String` representing the prefix that the URL path should start with. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that matches any path starting with the prefix "/api" + /// let mock = server.mock(|when, then| { + /// when.path_prefix("/api"); + /// then.status(200); // Respond with a 200 status code for matched requests + /// }); + /// + /// // Make a request to a path starting with "/api" + /// let response = reqwest::blocking::get(server.url("/api/v1/resource")).unwrap(); + /// + /// // Ensure the request was successful + /// assert_eq!(response.status(), 200); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for further configuration. + /// + pub fn path_prefix>(mut self, prefix: TryIntoString) -> Self + where + >::Error: std::fmt::Debug, + { + let prefix = prefix + .try_into() + .expect("cannot convert prefix into string"); + update_cell(&self.expectations, |e| { + if e.path_prefix.is_none() { + e.path_prefix = Some(Vec::new()); + } + e.path_prefix.as_mut().unwrap().push(prefix); + }); + self + } + // @docs-group: Path + + /// Specifies a suffix that the URL path must end with for the mock server to respond. + /// This is useful when the final segments of a path need to be validated, such as file extensions or specific patterns. + /// + /// # Parameters + /// - `suffix`: A string or other value convertible to `String` representing the suffix that the URL path should end with. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that matches any path ending with the suffix ".html" + /// let mock = server.mock(|when, then| { + /// when.path_suffix(".html"); + /// then.status(200); // Respond with a 200 status code for matched requests + /// }); + /// + /// // Make a request to a path ending with ".html" + /// let response = reqwest::blocking::get(server.url("/about/index.html")).unwrap(); + /// + /// // Ensure the request was successful + /// assert_eq!(response.status(), 200); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for further configuration. + /// + pub fn path_suffix>(mut self, suffix: TryIntoString) -> Self + where + >::Error: std::fmt::Debug, + { + let suffix = suffix + .try_into() + .expect("cannot convert suffix into string"); + update_cell(&self.expectations, |e| { + if e.path_suffix.is_none() { + e.path_suffix = Some(Vec::new()); + } + e.path_suffix.as_mut().unwrap().push(suffix); + }); + self + } + // @docs-group: Path + + /// Specifies a prefix that the URL path must not start with for the mock server to respond. + /// This constraint is useful for excluding paths that begin with particular segments or patterns. + /// + /// # Parameters + /// - `prefix`: A string or other value convertible to `String` representing the prefix that the URL path should not start with. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that matches any path not starting with the prefix "/admin" + /// let mock = server.mock(|when, then| { + /// when.path_prefix_not("/admin"); + /// then.status(200); // Respond with status 200 for paths excluding "/admin" + /// }); + /// + /// // Make a request to a path that does not start with "/admin" + /// let response = reqwest::blocking::get(server.url("/public/home")).unwrap(); + /// + /// // Ensure the request was successful + /// assert_eq!(response.status(), 200); + /// + /// // Verify that the mock server returned the expected response + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn path_prefix_not>(mut self, prefix: TryIntoString) -> Self + where + >::Error: std::fmt::Debug, + { + let prefix = prefix + .try_into() + .expect("cannot convert prefix into string"); + update_cell(&self.expectations, |e| { + if e.path_prefix_not.is_none() { + e.path_prefix_not = Some(Vec::new()); + } + e.path_prefix_not.as_mut().unwrap().push(prefix); + }); + self + } + // @docs-group: Path + + /// Specifies a suffix that the URL path must not end with for the mock server to respond. + /// This constraint is useful for excluding paths with specific file extensions or patterns. + /// + /// # Parameters + /// - `suffix`: A string or other value convertible to `String` representing the suffix that the URL path should not end with. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that matches any path not ending with the suffix ".json" + /// let mock = server.mock(|when, then| { + /// when.path_suffix_not(".json"); + /// then.status(200); // Respond with a 200 status code for paths excluding ".json" + /// }); + /// + /// // Make a request to a path that does not end with ".json" + /// let response = reqwest::blocking::get(server.url("/about/index.html")).unwrap(); + /// + /// // Ensure the request was successful + /// assert_eq!(response.status(), 200); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for further configuration. + /// + pub fn path_suffix_not>(mut self, suffix: TryIntoString) -> Self + where + >::Error: std::fmt::Debug, + { + let suffix = suffix + .try_into() + .expect("cannot convert suffix into string"); + update_cell(&self.expectations, |e| { + if e.path_suffix_not.is_none() { + e.path_suffix_not = Some(Vec::new()); + } + e.path_suffix_not.as_mut().unwrap().push(suffix); + }); + self + } + // @docs-group: Path + + /// Specifies a regular expression that the URL path must match for the mock server to respond. + /// This method allows flexible matching using regex patterns, making it useful for various matching scenarios. + /// + /// # Parameters + /// - `regex`: An expression that implements `Into`, representing the regex pattern to match against the URL path. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that matches paths ending with the suffix "le" + /// let mock = server.mock(|when, then| { + /// when.path_matches(r"le$"); + /// then.status(200); // Respond with a 200 status code for paths matching the pattern + /// }); + /// + /// // Make a request to a path ending with "le" + /// let response = reqwest::blocking::get(server.url("/example")).unwrap(); + /// + /// // Ensure the request was successful + /// assert_eq!(response.status(), 200); + /// + /// // Verify that the mock server returned the expected response + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + /// # Errors + /// This function will panic if the provided regex pattern is invalid. + /// + pub fn path_matches>(mut self, regex: TryIntoRegex) -> Self + where + >::Error: std::fmt::Debug, + { + let regex = regex + .try_into() + .expect("cannot convert provided value into regex"); + update_cell(&self.expectations, |e| { + if e.path_matches.is_none() { + e.path_matches = Some(Vec::new()); + } + e.path_matches.as_mut().unwrap().push(regex) + }); + self + } + // @docs-group: Path + + /// Specifies a required query parameter for the request. + /// This function ensures that the specified query parameter (key-value pair) must be included + /// in the request URL for the mock server to respond. + /// + /// **Note**: The request query keys and values are implicitly *allowed but not required* to be URL-encoded. + /// However, the value passed to this method should always be in plain text (i.e., not encoded). + /// + /// # Parameters + /// - `name`: The name of the query parameter to match against. + /// - `value`: The expected value of the query parameter. + /// + /// # Example + /// ```rust + /// // Arrange + /// use reqwest::blocking::get; + /// use httpmock::prelude::*; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the query parameter `query` to have the value "This is cool" + /// let m = server.mock(|when, then| { + /// when.query_param("query", "This is cool"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Act: Make a request that includes the specified query parameter and value + /// get(&server.url("/search?query=This+is+cool")).unwrap(); + /// + /// // Assert: Verify that the mock was called at least once + /// m.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + pub fn query_param, ValueString: Into>( + mut self, + name: KeyString, + value: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.query_param.is_none() { + e.query_param = Some(Vec::new()); + } + e.query_param + .as_mut() + .unwrap() + .push((name.into(), value.into())); + }); + self + } + // @docs-group: Query Parameters + + /// This function ensures that the specified query parameter (key) does exist in the request URL, + /// and its value is not equal to the specified value. + /// + /// **Note**: Query keys and values are implicitly *allowed but not required* to be URL-encoded + /// in the HTTP request. However, values passed to this method should always be in plain text + /// (i.e., not encoded). + /// + /// # Parameters + /// - `name`: The name of the query parameter to ensure is not present. + /// - `value`: The value of the query parameter to ensure is not present. + /// + /// # Example + /// ```rust + /// // Arrange + /// use httpmock::prelude::*; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the query parameter `query` to NOT have the value "This is cool" + /// let m = server.mock(|when, then| { + /// when.query_param_not("query", "This is cool"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Act: Make a request that does not include the specified query parameter and value + /// let response = reqwest::blocking::get(&server.url("/search?query=awesome")).unwrap(); + /// + /// // Assert: Verify that the mock was called + /// assert_eq!(response.status(), 200); + /// m.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + pub fn query_param_not, ValueString: Into>( + mut self, + name: KeyString, + value: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.query_param_not.is_none() { + e.query_param_not = Some(Vec::new()); + } + e.query_param_not + .as_mut() + .unwrap() + .push((name.into(), value.into())); + }); + self + } + // @docs-group: Query Parameters + + /// Specifies that a query parameter must be present in an HTTP request. + /// This function ensures that the specified query parameter key exists in the request URL + /// for the mock server to respond, regardless of the parameter's value. + /// + /// **Note**: The query key in the request is implicitly *allowed but not required* to be URL-encoded. + /// However, provide the key in plain text here (i.e., not encoded). + /// + /// # Parameters + /// - `name`: The name of the query parameter that must exist in the request. + /// + /// # Example + /// ```rust + /// // Arrange + /// use httpmock::prelude::*; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the query parameter `query` to exist, regardless of its value + /// let m = server.mock(|when, then| { + /// when.query_param_exists("query"); + /// then.status(200); // Respond with a 200 status code if the parameter exists + /// }); + /// + /// // Act: Make a request with the specified query parameter + /// reqwest::blocking::get(&server.url("/search?query=restaurants+near+me")).unwrap(); + /// + /// // Assert: Verify that the mock was called at least once + /// m.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn query_param_exists>(mut self, name: IntoString) -> Self { + update_cell(&self.expectations, |e| { + if e.query_param_exists.is_none() { + e.query_param_exists = Some(Vec::new()); + } + e.query_param_exists.as_mut().unwrap().push(name.into()); + }); + self + } + // @docs-group: Query Parameters + + /// Specifies that a query parameter must *not* be present in an HTTP request. + /// This function ensures that the specified query parameter key is absent in the request URL + /// for the mock server to respond, regardless of the parameter's value. + /// + /// **Note**: The request query key is implicitly *allowed but not required* to be URL-encoded. + /// However, provide the key in plain text (i.e., not encoded). + /// + /// # Parameters + /// - `name`: The name of the query parameter that should be missing from the request. + /// + /// # Example + /// ```rust + /// // Arrange + /// use httpmock::prelude::*; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the query parameter `query` to be missing + /// let m = server.mock(|when, then| { + /// when.query_param_missing("query"); + /// then.status(200); // Respond with a 200 status code if the parameter is absent + /// }); + /// + /// // Act: Make a request without the specified query parameter + /// reqwest::blocking::get(&server.url("/search")).unwrap(); + /// + /// // Assert: Verify that the mock was called at least once + /// m.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn query_param_missing>(mut self, name: IntoString) -> Self { + update_cell(&self.expectations, |e| { + if e.query_param_missing.is_none() { + e.query_param_missing = Some(Vec::new()); + } + e.query_param_missing.as_mut().unwrap().push(name.into()); + }); + self + } + // @docs-group: Query Parameters + + /// Specifies that a query parameter's value (**not** the key) must contain a specific substring for the request to match. + /// This function ensures that the specified query parameter (key) does exist in the request URL, and + /// it does have a value containing the given substring for the mock server to respond. + /// + /// **Note**: The request query key-value pairs are implicitly *allowed but not required* to be URL-encoded. + /// However, provide the substring in plain text (i.e., not encoded). + /// + /// # Parameters + /// - `name`: The name of the query parameter to match against. + /// - `substring`: The substring that must appear within the value of the query parameter. + /// + /// # Example + /// ```rust + /// // Arrange + /// use reqwest::blocking::get; + /// use httpmock::prelude::*; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the query parameter `query` + /// // to have a value containing "cool" + /// let m = server.mock(|when, then| { + /// when.query_param_includes("query", "cool"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Act: Make a request that includes a value containing the substring "cool" + /// get(server.url("/search?query=Something+cool")).unwrap(); + /// + /// // Assert: Verify that the mock was called at least once + /// m.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn query_param_includes, ValueString: Into>( + mut self, + name: KeyString, + substring: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.query_param_includes.is_none() { + e.query_param_includes = Some(Vec::new()); + } + e.query_param_includes + .as_mut() + .unwrap() + .push((name.into(), substring.into())); + }); + self + } + // @docs-group: Query Parameters + + /// Specifies that a query parameter's value (**not** the key) must not contain a specific substring for the request to match. + /// + /// This function ensures that the specified query parameter (key) does exist in the request URL, and + /// it does not have a value containing the given substring for the mock server to respond. + /// + /// **Note**: The request query key-value pairs are implicitly *allowed but not required* to be URL-encoded. + /// However, provide the substring in plain text here (i.e., not encoded). + /// + /// # Parameters + /// - `name`: The name of the query parameter to match against. + /// - `substring`: The substring that must not appear within the value of the query parameter. + /// + /// # Example + /// ```rust + /// // Arrange + /// use httpmock::prelude::*; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the query parameter `query` + /// // to have a value that does not contain "uncool" + /// let m = server.mock(|when, then| { + /// when.query_param_excludes("query", "uncool"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Act: Make a request that includes a value not containing the substring "uncool" + /// reqwest::blocking::get(&server.url("/search?query=Something+cool")).unwrap(); + /// + /// // Assert: Verify that the mock was called at least once + /// m.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn query_param_excludes, ValueString: Into>( + mut self, + name: KeyString, + substring: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.query_param_excludes.is_none() { + e.query_param_excludes = Some(Vec::new()); + } + e.query_param_excludes + .as_mut() + .unwrap() + .push((name.into(), substring.into())); + }); + + self + } + // @docs-group: Query Parameters + + /// Specifies that a query parameter's value (**not** the key) must start with a specific prefix for the request to match. + /// This function ensures that the specified query parameter (key) has a value starting with the given prefix + /// in the request URL for the mock server to respond. + /// + /// **Note**: The request query key-value pairs are implicitly *allowed but not required* to be URL-encoded. + /// Provide the prefix in plain text here (i.e., not encoded). + /// + /// # Parameters + /// - `name`: The name of the query parameter to match against. + /// - `prefix`: The prefix that the query parameter value should start with. + /// + /// # Example + /// ```rust + /// // Arrange + /// use httpmock::prelude::*; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the query parameter `query` + /// // to have a value starting with "cool" + /// let m = server.mock(|when, then| { + /// when.query_param_prefix("query", "cool"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Act: Make a request that includes a value starting with the prefix "cool" + /// reqwest::blocking::get(&server.url("/search?query=cool+stuff")).unwrap(); + /// + /// // Assert: Verify that the mock was called at least once + /// m.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn query_param_prefix, ValueString: Into>( + mut self, + name: KeyString, + prefix: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.query_param_prefix.is_none() { + e.query_param_prefix = Some(Vec::new()); + } + e.query_param_prefix + .as_mut() + .unwrap() + .push((name.into(), prefix.into())); + }); + self + } + // @docs-group: Query Parameters + + /// Specifies that a query parameter's value (**not** the key) must end with a specific suffix for the request to match. + /// This function ensures that the specified query parameter (key) has a value ending with the given suffix + /// in the request URL for the mock server to respond. + /// + /// **Note**: The request query key-value pairs are implicitly *allowed but not required* to be URL-encoded. + /// Provide the suffix in plain text here (i.e., not encoded). + /// + /// # Parameters + /// - `name`: The name of the query parameter to match against. + /// - `suffix`: The suffix that the query parameter value should end with. + /// + /// # Example + /// ```rust + /// // Arrange + /// use httpmock::prelude::*; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the query parameter `query` + /// // to have a value ending with "cool" + /// let m = server.mock(|when, then| { + /// when.query_param_suffix("query", "cool"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Act: Make a request that includes a value ending with the suffix "cool" + /// reqwest::blocking::get(&server.url("/search?query=really_cool")).unwrap(); + /// + /// // Assert: Verify that the mock was called at least once + /// m.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn query_param_suffix, ValueString: Into>( + mut self, + name: KeyString, + suffix: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.query_param_suffix.is_none() { + e.query_param_suffix = Some(Vec::new()); + } + e.query_param_suffix + .as_mut() + .unwrap() + .push((name.into(), suffix.into())); + }); + self + } + // @docs-group: Query Parameters + + /// Specifies that a query parameter's value (**not** the key) must not start with a specific prefix for the request to match. + /// This function ensures that the specified query parameter (key) has a value not starting with the given prefix + /// in the request URL for the mock server to respond. + /// + /// **Note**: The request query key-value pairs are implicitly *allowed but not required* to be URL-encoded. + /// Provide the prefix in plain text here (i.e., not encoded). + /// + /// # Parameters + /// - `name`: The name of the query parameter to match against. + /// - `prefix`: The prefix that the query parameter value should not start with. + /// + /// # Example + /// ```rust + /// // Arrange + /// use httpmock::prelude::*; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the query parameter `query` + /// // to have a value not starting with "cool" + /// let m = server.mock(|when, then| { + /// when.query_param_prefix_not("query", "cool"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Act: Make a request that does not start with the prefix "cool" + /// reqwest::blocking::get(&server.url("/search?query=warm_stuff")).unwrap(); + /// + /// // Assert: Verify that the mock was called at least once + /// m.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn query_param_prefix_not, ValueString: Into>( + mut self, + name: KeyString, + prefix: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.query_param_prefix_not.is_none() { + e.query_param_prefix_not = Some(Vec::new()); + } + e.query_param_prefix_not + .as_mut() + .unwrap() + .push((name.into(), prefix.into())); + }); + self + } + // @docs-group: Query Parameters + + /// Specifies that a query parameter's value (**not** the key) must not end with a specific suffix for the request to match. + /// This function ensures that the specified query parameter (key) has a value not ending with the given suffix + /// in the request URL for the mock server to respond. + /// + /// **Note**: The request query key-value pairs are implicitly *allowed but not required* to be URL-encoded. + /// Provide the suffix in plain text here (i.e., not encoded). + /// + /// # Parameters + /// - `name`: The name of the query parameter to match against. + /// - `suffix`: The suffix that the query parameter value should not end with. + /// + /// # Example + /// ```rust + /// // Arrange + /// use httpmock::prelude::*; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the query parameter `query` + /// // to have a value not ending with "cool" + /// let m = server.mock(|when, then| { + /// when.query_param_suffix_not("query", "cool"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Act: Make a request that doesn't end with the suffix "cool" + /// reqwest::blocking::get(&server.url("/search?query=uncool_stuff")).unwrap(); + /// + /// // Assert: Verify that the mock was called at least once + /// m.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn query_param_suffix_not, ValueString: Into>( + mut self, + name: KeyString, + suffix: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.query_param_suffix_not.is_none() { + e.query_param_suffix_not = Some(Vec::new()); + } + e.query_param_suffix_not + .as_mut() + .unwrap() + .push((name.into(), suffix.into())); + }); + self + } + // @docs-group: Query Parameters + + /// Specifies that a query parameter must match a specific regular expression pattern for the key and another pattern for the value. + /// This function ensures that the specified query parameter key-value pair matches the given patterns + /// in the request URL for the mock server to respond. + /// + /// # Parameters + /// - `key_regex`: A regular expression pattern for the query parameter's key to match against. + /// - `value_regex`: A regular expression pattern for the query parameter's value to match against. + /// + /// # Example + /// ```rust + /// // Arrange + /// use httpmock::prelude::*; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the query parameter key to match the regex "user.*" + /// // and the value to match the regex "admin.*" + /// let m = server.mock(|when, then| { + /// when.query_param_matches(r"user.*", r"admin.*"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Act: Make a request that matches the regex patterns for both key and value + /// reqwest::blocking::get(&server.url("/search?user=admin_user")).unwrap(); + /// + /// // Assert: Verify that the mock was called at least once + /// m.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn query_param_matches, ValueRegex: Into>( + mut self, + key_regex: KeyRegex, + value_regex: ValueRegex, + ) -> Self { + let key_regex = key_regex.into(); + let value_regex = value_regex.into(); + + update_cell(&self.expectations, |e| { + if e.query_param_matches.is_none() { + e.query_param_matches = Some(Vec::new()); + } + e.query_param_matches + .as_mut() + .unwrap() + .push((key_regex, value_regex)); + }); + self + } + // @docs-group: Query Parameters + + /// Specifies that the count of query parameters with keys and values matching specific regular + /// expression patterns must equal a specified number for the request to match. + /// This function ensures that the number of query parameters whose keys and values match the + /// given regex patterns is equal to the specified count in the request URL for the mock + /// server to respond. + /// + /// # Parameters + /// - `key_regex`: A regular expression pattern for the query parameter's key to match against. + /// - `value_regex`: A regular expression pattern for the query parameter's value to match against. + /// - `expected_count`: The expected number of query parameters whose keys and values match the regex patterns. + /// + /// # Example + /// ```rust + /// // Arrange + /// use httpmock::prelude::*; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects exactly two query parameters with keys matching the regex "user.*" + /// // and values matching the regex "admin.*" + /// let m = server.mock(|when, then| { + /// when.query_param_count(r"user.*", r"admin.*", 2); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Act: Make a request that matches the conditions + /// reqwest::blocking::get(&server.url("/search?user1=admin1&user2=admin2")).unwrap(); + /// + /// // Assert: Verify that the mock was called at least once + /// m.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn query_param_count, ValueRegex: Into>( + mut self, + key_regex: KeyRegex, + value_regex: ValueRegex, + expected_count: usize, + ) -> Self { + let key_regex = key_regex.into(); + let value_regex = value_regex.into(); + + update_cell(&self.expectations, |e| { + if e.query_param_count.is_none() { + e.query_param_count = Some(Vec::new()); + } + e.query_param_count + .as_mut() + .unwrap() + .push((key_regex, value_regex, expected_count)); + }); + self + } + // @docs-group: Query Parameters + + /// Sets the expected HTTP header and its value for the request to match. + /// This function ensures that the specified header with the given value is present in the request. + /// Header names are case-insensitive, as per RFC 2616. + /// + /// # Parameters + /// - `name`: The HTTP header name. Header names are case-insensitive. + /// - `value`: The expected value of the HTTP header. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the "Authorization" header with a specific value + /// let mock = server.mock(|when, then| { + /// when.header("Authorization", "token 1234567890"); + /// then.status(200); // Respond with a 200 status code if the header and value are present + /// }); + /// + /// // Make a request that includes the "Authorization" header with the specified value + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Authorization", "token 1234567890") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn header, ValueString: Into>( + mut self, + name: KeyString, + value: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.header.is_none() { + e.header = Some(Vec::new()); + } + e.header.as_mut().unwrap().push((name.into(), value.into())); + }); + self + } + // @docs-group: Headers + + /// Sets the requirement that the HTTP request must not contain a specific header with the specified value. + /// This function ensures that the specified header with the given value is absent in the request. + /// Header names are case-insensitive, as per RFC 2616. + /// + /// This function may be called multiple times to add multiple excluded headers. + /// + /// # Parameters + /// - `name`: The HTTP header name. Header names are case-insensitive. + /// - `value`: The value of the HTTP header that must not be present. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the "Authorization" header with a specific value to be absent + /// let mock = server.mock(|when, then| { + /// when.header_not("Authorization", "token 1234567890"); + /// then.status(200); // Respond with a 200 status code if the header and value are absent + /// }); + /// + /// // Make a request that includes the "Authorization" header with a different value + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Authorization", "token abcdefg") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn header_not, ValueString: Into>( + mut self, + name: KeyString, + value: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.header_not.is_none() { + e.header_not = Some(Vec::new()); + } + e.header_not + .as_mut() + .unwrap() + .push((name.into(), value.into())); + }); + self + } + // @docs-group: Headers + + /// Sets the requirement that the HTTP request must contain a specific header. + /// The presence of the header is checked, but its value is not validated. + /// For value validation, refer to [Mock::expect_header](struct.Mock.html#method.expect_header). + /// + /// # Parameters + /// - `name`: The HTTP header name. Header names are case-insensitive, as per RFC 2616. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the "Authorization" header to be present in the request + /// let mock = server.mock(|when, then| { + /// when.header_exists("Authorization"); + /// then.status(200); // Respond with a 200 status code if the header is present + /// }); + /// + /// // Make a request that includes the "Authorization" header + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Authorization", "token 1234567890") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn header_exists>(mut self, name: IntoString) -> Self { + update_cell(&self.expectations, |e| { + if e.header_exists.is_none() { + e.header_exists = Some(Vec::new()); + } + e.header_exists.as_mut().unwrap().push(name.into()); + }); + self + } + // @docs-group: Headers + + /// Sets the requirement that the HTTP request must not contain a specific header. + /// This function ensures that the specified header is absent in the request. + /// Header names are case-insensitive, as per RFC 2616. + /// + /// This function may be called multiple times to add multiple excluded headers. + /// + /// # Parameters + /// - `name`: The HTTP header name. Header names are case-insensitive. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the "Authorization" header to be absent in the request + /// let mock = server.mock(|when, then| { + /// when.header_missing("Authorization"); + /// then.status(200); // Respond with a 200 status code if the header is absent + /// }); + /// + /// // Make a request that does not include the "Authorization" header + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn header_missing>(mut self, name: IntoString) -> Self { + update_cell(&self.expectations, |e| { + if e.header_missing.is_none() { + e.header_missing = Some(Vec::new()); + } + e.header_missing.as_mut().unwrap().push(name.into()); + }); + self + } + // @docs-group: Headers + + /// Sets the requirement that the HTTP request must contain a specific header whose value contains a specified substring. + /// This function ensures that the specified header is present and its value contains the given substring. + /// Header names are case-insensitive, as per RFC 2616. + /// + /// This function may be called multiple times to check multiple headers and substrings. + /// + /// # Parameters + /// - `name`: The HTTP header name. Header names are case-insensitive. + /// - `substring`: The substring that the header value must contain. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the "Authorization" header's value to contain "token" + /// let mock = server.mock(|when, then| { + /// when.header_includes("Authorization", "token"); + /// then.status(200); // Respond with a 200 status code if the header value contains the substring + /// }); + /// + /// // Make a request that includes the "Authorization" header with the specified substring in its value + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Authorization", "token 1234567890") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn header_includes, ValueString: Into>( + mut self, + name: KeyString, + substring: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.header_includes.is_none() { + e.header_includes = Some(Vec::new()); + } + e.header_includes + .as_mut() + .unwrap() + .push((name.into(), substring.into())); + }); + self + } + // @docs-group: Headers + + /// Sets the requirement that the HTTP request must contain a specific header whose value does not contain a specified substring. + /// This function ensures that the specified header is present and its value does not contain the given substring. + /// Header names are case-insensitive, as per RFC 2616. + /// + /// This function may be called multiple times to check multiple headers and substrings. + /// + /// # Parameters + /// - `name`: The HTTP header name. Header names are case-insensitive. + /// - `substring`: The substring that the header value must not contain. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the "Authorization" header's value to not contain "Bearer" + /// let mock = server.mock(|when, then| { + /// when.header_excludes("Authorization", "Bearer"); + /// then.status(200); // Respond with a 200 status code if the header value does not contain the substring + /// }); + /// + /// // Make a request that includes the "Authorization" header without the forbidden substring in its value + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Authorization", "token 1234567890") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn header_excludes, ValueString: Into>( + mut self, + name: KeyString, + substring: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.header_excludes.is_none() { + e.header_excludes = Some(Vec::new()); + } + e.header_excludes + .as_mut() + .unwrap() + .push((name.into(), substring.into())); + }); + self + } + // @docs-group: Headers + + /// Sets the requirement that the HTTP request must contain a specific header whose value starts with a specified prefix. + /// This function ensures that the specified header is present and its value starts with the given prefix. + /// Header names are case-insensitive, as per RFC 2616. + /// + /// This function may be called multiple times to check multiple headers and prefixes. + /// + /// # Parameters + /// - `name`: The HTTP header name. Header names are case-insensitive. + /// - `prefix`: The prefix that the header value must start with. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the "Authorization" header's value to start with "token" + /// let mock = server.mock(|when, then| { + /// when.header_prefix("Authorization", "token"); + /// then.status(200); // Respond with a 200 status code if the header value starts with the prefix + /// }); + /// + /// // Make a request that includes the "Authorization" header with the specified prefix in its value + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Authorization", "token 1234567890") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn header_prefix, ValueString: Into>( + mut self, + name: KeyString, + prefix: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.header_prefix.is_none() { + e.header_prefix = Some(Vec::new()); + } + e.header_prefix + .as_mut() + .unwrap() + .push((name.into(), prefix.into())); + }); + self + } + // @docs-group: Headers + + /// Sets the requirement that the HTTP request must contain a specific header whose value ends with a specified suffix. + /// This function ensures that the specified header is present and its value ends with the given suffix. + /// Header names are case-insensitive, as per RFC 2616. + /// + /// This function may be called multiple times to check multiple headers and suffixes. + /// + /// # Parameters + /// - `name`: The HTTP header name. Header names are case-insensitive. + /// - `suffix`: The suffix that the header value must end with. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the "Authorization" header's value to end with "7890" + /// let mock = server.mock(|when, then| { + /// when.header_suffix("Authorization", "7890"); + /// then.status(200); // Respond with a 200 status code if the header value ends with the suffix + /// }); + /// + /// // Make a request that includes the "Authorization" header with the specified suffix in its value + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Authorization", "token 1234567890") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn header_suffix, ValueString: Into>( + mut self, + name: KeyString, + suffix: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.header_suffix.is_none() { + e.header_suffix = Some(Vec::new()); + } + e.header_suffix + .as_mut() + .unwrap() + .push((name.into(), suffix.into())); + }); + self + } + // @docs-group: Headers + + /// Sets the requirement that the HTTP request must contain a specific header whose value does not start with a specified prefix. + /// This function ensures that the specified header is present and its value does not start with the given prefix. + /// Header names are case-insensitive, as per RFC 2616. + /// + /// This function may be called multiple times to check multiple headers and prefixes. + /// + /// # Parameters + /// - `name`: The HTTP header name. Header names are case-insensitive. + /// - `prefix`: The prefix that the header value must not start with. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the "Authorization" header's value to not start with "Bearer" + /// let mock = server.mock(|when, then| { + /// when.header_prefix_not("Authorization", "Bearer"); + /// then.status(200); // Respond with a 200 status code if the header value does not start with the prefix + /// }); + /// + /// // Make a request that includes the "Authorization" header without the "Bearer" prefix in its value + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Authorization", "token 1234567890") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn header_prefix_not, ValueString: Into>( + mut self, + name: KeyString, + prefix: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.header_prefix_not.is_none() { + e.header_prefix_not = Some(Vec::new()); + } + e.header_prefix_not + .as_mut() + .unwrap() + .push((name.into(), prefix.into())); + }); + self + } + // @docs-group: Headers + + /// Sets the requirement that the HTTP request must contain a specific header whose value does not end with a specified suffix. + /// This function ensures that the specified header is present and its value does not end with the given suffix. + /// Header names are case-insensitive, as per RFC 2616. + /// + /// This function may be called multiple times to check multiple headers and suffixes. + /// + /// # Parameters + /// - `name`: The HTTP header name. Header names are case-insensitive. + /// - `suffix`: The suffix that the header value must not end with. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the "Authorization" header's value to not end with "abc" + /// let mock = server.mock(|when, then| { + /// when.header_suffix_not("Authorization", "abc"); + /// then.status(200); // Respond with a 200 status code if the header value does not end with the suffix + /// }); + /// + /// // Make a request that includes the "Authorization" header without the "abc" suffix in its value + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Authorization", "token 1234567890") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn header_suffix_not, ValueString: Into>( + mut self, + name: KeyString, + suffix: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.header_suffix_not.is_none() { + e.header_suffix_not = Some(Vec::new()); + } + e.header_suffix_not + .as_mut() + .unwrap() + .push((name.into(), suffix.into())); + }); + self + } + // @docs-group: Headers + + /// Sets the requirement that the HTTP request must contain a specific header whose key and value match the specified regular expressions. + /// This function ensures that the specified header is present and both its key and value match the given regular expressions. + /// Header names are case-insensitive, as per RFC 2616. + /// + /// This function may be called multiple times to check multiple headers and patterns. + /// + /// # Parameters + /// - `key_regex`: The regular expression that the header key must match. + /// - `value_regex`: The regular expression that the header value must match. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the "Authorization" header's key to match the regex "^Auth.*" + /// // and its value to match the regex "token .*" + /// let mock = server.mock(|when, then| { + /// when.header_matches("^Auth.*", "token .*"); + /// then.status(200); // Respond with a 200 status code if the header key and value match the patterns + /// }); + /// + /// // Make a request that includes the "Authorization" header with a value matching the regex + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Authorization", "token 1234567890") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn header_matches, ValueString: Into>( + mut self, + key_regex: KeyString, + value_regex: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.header_matches.is_none() { + e.header_matches = Some(Vec::new()); + } + e.header_matches + .as_mut() + .unwrap() + .push((key_regex.into(), value_regex.into())); + }); + self + } + // @docs-group: Headers + + /// Sets the requirement that the HTTP request must contain a specific number of headers whose keys and values match specified patterns. + /// This function ensures that the specified number of headers with keys and values matching the given patterns are present in the request. + /// Header names are case-insensitive, as per RFC 2616. + /// + /// This function may be called multiple times to check multiple patterns and counts. + /// + /// # Parameters + /// - `key_pattern`: The pattern that the header keys must match. + /// - `value_pattern`: The pattern that the header values must match. + /// - `count`: The number of headers with keys and values matching the patterns that must be present. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects at least 2 headers whose keys match the regex "^X-Custom-Header.*" + /// // and values match the regex "value.*" + /// let mock = server.mock(|when, then| { + /// when.header_count("^X-Custom-Header.*", "value.*", 2); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request that includes the required headers + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("x-custom-header-1", "value1") + /// .header("X-Custom-Header-2", "value2") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + /// + pub fn header_count< + KeyRegex: TryInto, + ValueRegex: TryInto, + IntoUsize: TryInto, + >( + mut self, + key_pattern: KeyRegex, + value_pattern: ValueRegex, + count: IntoUsize, + ) -> Self + where + >::Error: std::fmt::Debug, + >::Error: std::fmt::Debug, + >::Error: std::fmt::Debug, + { + let count = match count.try_into() { + Ok(c) => c, + Err(_) => panic!("parameter count must be a positive integer that fits into a usize"), + }; + + let key_pattern = key_pattern.try_into().expect("cannot convert key to regex"); + let value_pattern = value_pattern + .try_into() + .expect("cannot convert key to regex"); + + update_cell(&self.expectations, |e| { + if e.header_count.is_none() { + e.header_count = Some(Vec::new()); + } + e.header_count.as_mut().unwrap().push(( + key_pattern.into(), + value_pattern.into(), + count, + )); + }); + self + } + // @docs-group: Headers + + /// Sets the cookie that needs to exist in the HTTP request. + /// Cookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html). + /// **Attention**: Cookie names are **case-sensitive**. + /// + /// # Parameters + /// - `name`: The name of the cookie. Must be a case-sensitive match. + /// - `value`: The expected value of the cookie. + /// + /// > Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects a cookie named "SESSIONID" with the value "1234567890" + /// let mock = server.mock(|when, then| { + /// when.cookie("SESSIONID", "1234567890"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request that includes the required cookie + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Cookie", "TRACK=12345; SESSIONID=1234567890; CONSENT=1") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + pub fn cookie, ValueString: Into>( + mut self, + name: KeyString, + value: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.cookie.is_none() { + e.cookie = Some(Vec::new()); + } + e.cookie.as_mut().unwrap().push((name.into(), value.into())); + }); + self + } + // @docs-group: Cookies + + /// Sets the cookie that should not exist or should not have a specific value in the HTTP request. + /// Cookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html). + /// **Attention**: Cookie names are **case-sensitive**. + /// + /// # Parameters + /// - `name`: The name of the cookie. Must be a case-sensitive match. + /// - `value`: The value that the cookie should not have. + /// + /// > Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects a cookie named "SESSIONID" to not have the value "1234567890" + /// let mock = server.mock(|when, then| { + /// when.cookie_not("SESSIONID", "1234567890"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request that includes the required cookie + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Cookie", "TRACK=12345; SESSIONID=0987654321; CONSENT=1") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + pub fn cookie_not, ValueString: Into>( + mut self, + name: KeyString, + value: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.cookie_not.is_none() { + e.cookie_not = Some(Vec::new()); + } + e.cookie_not + .as_mut() + .unwrap() + .push((name.into(), value.into())); + }); + self + } + // @docs-group: Cookies + + /// Sets the requirement that a cookie with the specified name must exist in the HTTP request. + /// Cookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html). + /// **Attention**: Cookie names are **case-sensitive**. + /// + /// # Parameters + /// - `name`: The name of the cookie that must exist. + /// + /// > Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects a cookie named "SESSIONID" + /// let mock = server.mock(|when, then| { + /// when.cookie_exists("SESSIONID"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request that includes the required cookie + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Cookie", "TRACK=12345; SESSIONID=1234567890; CONSENT=1") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + pub fn cookie_exists>(mut self, name: IntoString) -> Self { + update_cell(&self.expectations, |e| { + if e.cookie_exists.is_none() { + e.cookie_exists = Some(Vec::new()); + } + e.cookie_exists.as_mut().unwrap().push(name.into()); + }); + self + } + // @docs-group: Cookies + + /// Sets the requirement that a cookie with the specified name must not exist in the HTTP request. + /// Cookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html). + /// **Attention**: Cookie names are **case-sensitive**. + /// + /// # Parameters + /// - `name`: The name of the cookie that must not exist. + /// + /// > Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects a cookie named "SESSIONID" not to exist + /// let mock = server.mock(|when, then| { + /// when.cookie_missing("SESSIONID"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request that does not include the excluded cookie + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Cookie", "TRACK=12345; CONSENT=1") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + pub fn cookie_missing>(mut self, name: IntoString) -> Self { + update_cell(&self.expectations, |e| { + if e.cookie_missing.is_none() { + e.cookie_missing = Some(Vec::new()); + } + e.cookie_missing.as_mut().unwrap().push(name.into()); + }); + self + } + // @docs-group: Cookies + + /// Sets the requirement that a cookie with the specified name must exist and its value must contain the specified substring. + /// Cookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html). + /// **Attention**: Cookie names are **case-sensitive**. + /// + /// # Parameters + /// - `name`: The name of the cookie that must exist. + /// - `value_substring`: The substring that must be present in the cookie value. + /// + /// > Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects a cookie named "SESSIONID" with a value containing "1234" + /// let mock = server.mock(|when, then| { + /// when.cookie_includes("SESSIONID", "1234"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request that includes the required cookie + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Cookie", "TRACK=12345; SESSIONID=abc1234def; CONSENT=1") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + pub fn cookie_includes, ValueString: Into>( + mut self, + name: KeyString, + value_substring: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.cookie_includes.is_none() { + e.cookie_includes = Some(Vec::new()); + } + e.cookie_includes + .as_mut() + .unwrap() + .push((name.into(), value_substring.into())); + }); + self + } + // @docs-group: Cookies + + /// Sets the requirement that a cookie with the specified name must exist and its value must not contain the specified substring. + /// Cookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html). + /// **Attention**: Cookie names are **case-sensitive**. + /// + /// # Parameters + /// - `name`: The name of the cookie that must exist. + /// - `value_substring`: The substring that must not be present in the cookie value. + /// + /// > Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects a cookie named "SESSIONID" with a value not containing "1234" + /// let mock = server.mock(|when, then| { + /// when.cookie_excludes("SESSIONID", "1234"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request that includes the required cookie + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Cookie", "TRACK=12345; SESSIONID=abcdef; CONSENT=1") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + pub fn cookie_excludes, ValueString: Into>( + mut self, + name: KeyString, + value_substring: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.cookie_excludes.is_none() { + e.cookie_excludes = Some(Vec::new()); + } + e.cookie_excludes + .as_mut() + .unwrap() + .push((name.into(), value_substring.into())); + }); + self + } + // @docs-group: Cookies + + /// Sets the requirement that a cookie with the specified name must exist and its value must start with the specified substring. + /// Cookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html). + /// **Attention**: Cookie names are **case-sensitive**. + /// + /// # Parameters + /// - `name`: The name of the cookie that must exist. + /// - `value_prefix`: The substring that must be at the start of the cookie value. + /// + /// > Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects a cookie named "SESSIONID" with a value starting with "1234" + /// let mock = server.mock(|when, then| { + /// when.cookie_prefix("SESSIONID", "1234"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request that includes the required cookie + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Cookie", "TRACK=12345; SESSIONID=1234abcdef; CONSENT=1") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + pub fn cookie_prefix, ValueString: Into>( + mut self, + name: KeyString, + value_prefix: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.cookie_prefix.is_none() { + e.cookie_prefix = Some(Vec::new()); + } + e.cookie_prefix + .as_mut() + .unwrap() + .push((name.into(), value_prefix.into())); + }); + self + } + // @docs-group: Cookies + + /// Sets the requirement that a cookie with the specified name must exist and its value must end with the specified substring. + /// Cookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html). + /// **Attention**: Cookie names are **case-sensitive**. + /// + /// # Parameters + /// - `name`: The name of the cookie that must exist. + /// - `value_suffix`: The substring that must be at the end of the cookie value. + /// + /// > Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects a cookie named "SESSIONID" with a value ending with "7890" + /// let mock = server.mock(|when, then| { + /// when.cookie_suffix("SESSIONID", "7890"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request that includes the required cookie + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Cookie", "TRACK=12345; SESSIONID=abcdef7890; CONSENT=1") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + pub fn cookie_suffix, ValueString: Into>( + mut self, + name: KeyString, + value_suffix: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.cookie_suffix.is_none() { + e.cookie_suffix = Some(Vec::new()); + } + e.cookie_suffix + .as_mut() + .unwrap() + .push((name.into(), value_suffix.into())); + }); + self + } + // @docs-group: Cookies + + /// Sets the requirement that a cookie with the specified name must exist and its value must not start with the specified substring. + /// Cookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html). + /// **Attention**: Cookie names are **case-sensitive**. + /// + /// # Parameters + /// - `name`: The name of the cookie that must exist. + /// - `value_prefix`: The substring that must not be at the start of the cookie value. + /// + /// > Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects a cookie named "SESSIONID" with a value not starting with "1234" + /// let mock = server.mock(|when, then| { + /// when.cookie_prefix_not("SESSIONID", "1234"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request that includes the required cookie + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Cookie", "TRACK=12345; SESSIONID=abcd1234; CONSENT=1") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + pub fn cookie_prefix_not, ValueString: Into>( + mut self, + name: KeyString, + value_prefix: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.cookie_prefix_not.is_none() { + e.cookie_prefix_not = Some(Vec::new()); + } + e.cookie_prefix_not + .as_mut() + .unwrap() + .push((name.into(), value_prefix.into())); + }); + self + } + // @docs-group: Cookies + + /// Sets the requirement that a cookie with the specified name must exist and its value must not end with the specified substring. + /// Cookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html). + /// **Attention**: Cookie names are **case-sensitive**. + /// + /// # Parameters + /// - `name`: The name of the cookie that must exist. + /// - `value_suffix`: The substring that must not be at the end of the cookie value. + /// + /// > Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects a cookie named "SESSIONID" with a value not ending with "7890" + /// let mock = server.mock(|when, then| { + /// when.cookie_suffix_not("SESSIONID", "7890"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request that includes the required cookie + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Cookie", "TRACK=12345; SESSIONID=abcdef1234; CONSENT=1") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + pub fn cookie_suffix_not, ValueString: Into>( + mut self, + name: KeyString, + value_suffix: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.cookie_suffix_not.is_none() { + e.cookie_suffix_not = Some(Vec::new()); + } + e.cookie_suffix_not + .as_mut() + .unwrap() + .push((name.into(), value_suffix.into())); + }); + self + } + // @docs-group: Cookies + + /// Sets the requirement that a cookie with a name matching the specified regex must exist and its value must match the specified regex. + /// Cookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html). + /// **Attention**: Cookie names are **case-sensitive**. + /// + /// # Parameters + /// - `key_regex`: The regex pattern that the cookie name must match. + /// - `value_regex`: The regex pattern that the cookie value must match. + /// + /// > Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects a cookie with a name matching the regex "^SESSION" + /// // and a value matching the regex "^[0-9]{10}$" + /// let mock = server.mock(|when, then| { + /// when.cookie_matches(r"^SESSION", r"^[0-9]{10}$"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request that includes the required cookie + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Cookie", "TRACK=12345; SESSIONID=1234567890; CONSENT=1") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + pub fn cookie_matches, ValueRegex: Into>( + mut self, + key_regex: KeyRegex, + value_regex: ValueRegex, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.cookie_matches.is_none() { + e.cookie_matches = Some(Vec::new()); + } + e.cookie_matches + .as_mut() + .unwrap() + .push((key_regex.into(), value_regex.into())); + }); + self + } + // @docs-group: Cookies + + /// Sets the requirement that a cookie with a name and value matching the specified regexes must appear a specified number of times in the HTTP request. + /// Cookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html). + /// **Attention**: Cookie names are **case-sensitive**. + /// + /// # Parameters + /// - `key_regex`: The regex pattern that the cookie name must match. + /// - `value_regex`: The regex pattern that the cookie value must match. + /// - `count`: The number of times a cookie with a matching name and value must appear. + /// + /// > Note: This function is only available when the `cookies` feature is enabled. This feature is enabled by default. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects a cookie with a name matching the regex "^SESSION" + /// // and a value matching the regex "^[0-9]{10}$" to appear exactly twice + /// let mock = server.mock(|when, then| { + /// when.cookie_count(r"^SESSION", r"^[0-9]{10}$", 2); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request that includes the required cookies + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Cookie", "SESSIONID=1234567890; TRACK=12345; SESSIONTOKEN=0987654321; CONSENT=1") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + pub fn cookie_count, ValueRegex: Into>( + mut self, + key_regex: KeyRegex, + value_regex: ValueRegex, + count: usize, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.cookie_count.is_none() { + e.cookie_count = Some(Vec::new()); + } + e.cookie_count + .as_mut() + .unwrap() + .push((key_regex.into(), value_regex.into(), count)); + }); + self + } + // @docs-group: Cookies + + /// Sets the required HTTP request body content. + /// This method specifies that the HTTP request body must match the provided content exactly. + /// + /// **Note**: The body content is case-sensitive and must be an exact match. + /// + /// # Parameters + /// - `body`: The required HTTP request body content. This parameter accepts any type that can be converted into a `String`. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the request body to be "The Great Gatsby" + /// let mock = server.mock(|when, then| { + /// when.body("The Great Gatsby"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request with the required body content + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .body("The Great Gatsby") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + pub fn body>(mut self, body: IntoString) -> Self { + update_cell(&self.expectations, |e| { + e.body = Some(HttpMockBytes::from(Bytes::from(body.into()))); + }); + self + } + // @docs-group: Body + + /// Sets the condition that the HTTP request body content must not match the specified value. + /// This method ensures that the request body does not contain the provided content exactly. + /// + /// **Note**: The body content is case-sensitive and must be an exact mismatch. + /// + /// # Parameters + /// - `body`: The body content that the HTTP request must not contain. This parameter accepts any type that can be converted into a `String`. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the request body to not be "The Great Gatsby" + /// let mock = server.mock(|when, then| { + /// when.body_not("The Great Gatsby"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request with a different body content + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .body("A Tale of Two Cities") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + pub fn body_not>(mut self, body: IntoString) -> Self { + update_cell(&self.expectations, |e| { + if e.body_not.is_none() { + e.body_not = Some(Vec::new()); + } + e.body_not + .as_mut() + .unwrap() + .push(HttpMockBytes::from(Bytes::from(body.into()))); + }); + self + } + // @docs-group: Body + + /// Sets the condition that the HTTP request body content must contain the specified substring. + /// This method ensures that the request body includes the provided content as a substring. + /// + /// **Note**: The body content is case-sensitive. + /// + /// # Parameters + /// - `substring`: The substring that the HTTP request body must contain. This parameter accepts any type that can be converted into a `String`. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the request body to contain the substring "Gatsby" + /// let mock = server.mock(|when, then| { + /// when.body_includes("Gatsby"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request with the required substring in the body content + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .body("The Great Gatsby is a novel.") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + pub fn body_includes>(mut self, substring: IntoString) -> Self { + update_cell(&self.expectations, |e| { + if e.body_includes.is_none() { + e.body_includes = Some(Vec::new()); + } + e.body_includes + .as_mut() + .unwrap() + .push(HttpMockBytes::from(Bytes::from(substring.into()))); + }); + self + } + // @docs-group: Body + + /// Sets the condition that the HTTP request body content must not contain the specified substring. + /// This method ensures that the request body does not include the provided content as a substring. + /// + /// **Note**: The body content is case-sensitive. + /// + /// # Parameters + /// - `substring`: The substring that the HTTP request body must not contain. This parameter accepts any type that can be converted into a `String`. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the request body to not contain the substring "Gatsby" + /// let mock = server.mock(|when, then| { + /// when.body_excludes("Gatsby"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request with a different body content + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .body("A Tale of Two Cities is a novel.") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + pub fn body_excludes>(mut self, substring: IntoString) -> Self { + update_cell(&self.expectations, |e| { + if e.body_excludes.is_none() { + e.body_excludes = Some(Vec::new()); + } + e.body_excludes + .as_mut() + .unwrap() + .push(HttpMockBytes::from(Bytes::from(substring.into()))); + }); + self + } + // @docs-group: Body + + /// Sets the condition that the HTTP request body content must begin with the specified substring. + /// This method ensures that the request body starts with the provided content as a substring. + /// + /// **Note**: The body content is case-sensitive. + /// + /// # Parameters + /// - `prefix`: The substring that the HTTP request body must begin with. This parameter accepts any type that can be converted into a `String`. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the request body to begin with the substring "The Great" + /// let mock = server.mock(|when, then| { + /// when.body_prefix("The Great"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request with the required prefix in the body content + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .body("The Great Gatsby is a novel.") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When` instance to allow method chaining for additional configuration. + pub fn body_prefix>(mut self, prefix: IntoString) -> Self { + update_cell(&self.expectations, |e| { + if e.body_prefix.is_none() { + e.body_prefix = Some(Vec::new()); + } + e.body_prefix + .as_mut() + .unwrap() + .push(HttpMockBytes::from(Bytes::from(prefix.into()))); + }); + self + } + // @docs-group: Body + + /// Sets the condition that the HTTP request body content must end with the specified substring. + /// This method ensures that the request body concludes with the provided content as a substring. + /// + /// **Note**: The body content is case-sensitive. + /// + /// # Parameters + /// - `suffix`: The substring that the HTTP request body must end with. This parameter accepts any type that can be converted into a `String`. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the request body to end with the substring "a novel." + /// let mock = server.mock(|when, then| { + /// when.body_suffix("a novel."); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request with the required suffix in the body content + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .body("The Great Gatsby is a novel.") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When’ instance to allow method chaining for additional configuration. + pub fn body_suffix>(mut self, suffix: IntoString) -> Self { + update_cell(&self.expectations, |e| { + if e.body_suffix.is_none() { + e.body_suffix = Some(Vec::new()); + } + e.body_suffix + .as_mut() + .unwrap() + .push(HttpMockBytes::from(Bytes::from(suffix.into()))); + }); + self + } + // @docs-group: Body + + /// Sets the condition that the HTTP request body content must not begin with the specified substring. + /// This method ensures that the request body does not start with the provided content as a substring. + /// + /// **Note**: The body content is case-sensitive. + /// + /// # Parameters + /// - `prefix`: The substring that the HTTP request body must not begin with. This parameter accepts any type that can be converted into a `String`. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the request body to not begin with the substring "Error:" + /// let mock = server.mock(|when, then| { + /// when.body_prefix_not("Error:"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request with a different body content + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .body("Success: Operation completed.") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When’ instance to allow method chaining for additional configuration. + pub fn body_prefix_not>(mut self, prefix: IntoString) -> Self { + update_cell(&self.expectations, |e| { + if e.body_prefix_not.is_none() { + e.body_prefix_not = Some(Vec::new()); + } + e.body_prefix_not + .as_mut() + .unwrap() + .push(HttpMockBytes::from(Bytes::from(prefix.into()))); + }); + self + } + // @docs-group: Body + + /// Sets the condition that the HTTP request body content must not end with the specified substring. + /// This method ensures that the request body does not conclude with the provided content as a substring. + /// + /// **Note**: The body content is case-sensitive. + /// + /// # Parameters + /// - `suffix`: The substring that the HTTP request body must not end with. This parameter accepts any type that can be converted into a `String`. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the request body to not end with the substring "a novel." + /// let mock = server.mock(|when, then| { + /// when.body_suffix_not("a novel."); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request with a different body content + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .body("The Great Gatsby is a story.") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When’ instance to allow method chaining for additional configuration. + pub fn body_suffix_not>(mut self, suffix: IntoString) -> Self { + update_cell(&self.expectations, |e| { + if e.body_suffix_not.is_none() { + e.body_suffix_not = Some(Vec::new()); + } + e.body_suffix_not + .as_mut() + .unwrap() + .push(HttpMockBytes::from(Bytes::from(suffix.into()))); + }); + self + } + // @docs-group: Body + + /// Sets the condition that the HTTP request body content must match the specified regular expression. + /// This method ensures that the request body fully conforms to the provided regex pattern. + /// + /// **Note**: The regex matching is case-sensitive unless the regex is explicitly defined to be case-insensitive. + /// + /// # Parameters + /// - `pattern`: The regular expression pattern that the HTTP request body must match. This parameter accepts any type that can be converted into a `Regex`. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the request body to match the regex pattern "^The Great Gatsby.*" + /// let mock = server.mock(|when, then| { + /// when.body_matches("^The Great Gatsby.*"); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request with a body that matches the regex pattern + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .body("The Great Gatsby is a novel by F. Scott Fitzgerald.") + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When’ instance to allow method chaining for additional configuration. + pub fn body_matches>(mut self, pattern: IntoRegex) -> Self { + update_cell(&self.expectations, |e| { + if e.body_matches.is_none() { + e.body_matches = Some(Vec::new()); + } + e.body_matches.as_mut().unwrap().push(pattern.into()); + }); + self + } + // @docs-group: Body + + /// Sets the condition that the HTTP request body content must match the specified JSON structure. + /// This method ensures that the request body exactly matches the JSON value provided. + /// + /// **Note**: The body content is case-sensitive. + /// + /// **Note**: This method does not automatically verify the `Content-Type` header. + /// If specific content type verification is required (e.g., `application/json`), + /// you must add this expectation manually. + /// + /// # Parameters + /// - `json_value`: The JSON structure that the HTTP request body must match. This parameter accepts any type that can be converted into a `serde_json::Value`. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// use serde_json::json; + /// + /// // Start a new mock server + /// let server = MockServer::start(); + /// + /// // Create a mock that expects the request body to match a specific JSON structure + /// let mock = server.mock(|when, then| { + /// when.json_body(json!({ + /// "title": "The Great Gatsby", + /// "author": "F. Scott Fitzgerald" + /// })); + /// then.status(200); // Respond with a 200 status code if the condition is met + /// }); + /// + /// // Make a request with a JSON body that matches the expected structure + /// Client::new() + /// .post(&format!("http://{}/test", server.address())) + /// .header("Content-Type", "application/json") // It's important to set the Content-Type header manually + /// .body(r#"{"title":"The Great Gatsby","author":"F. Scott Fitzgerald"}"#) + /// .send() + /// .unwrap(); + /// + /// // Verify that the mock was called at least once + /// mock.assert(); + /// ``` + /// + /// # Returns + /// The updated `When’ instance to allow method chaining for additional configuration. + pub fn json_body>(mut self, json_value: JsonValue) -> Self { + update_cell(&self.expectations, |e| { + e.json_body = Some(json_value.into()); + }); + self + } + // @docs-group: Body + + /// Sets the expected JSON body using a serializable serde object. + /// This function automatically serializes the given object into a JSON string using serde. + /// + /// **Note**: This method does not automatically verify the `Content-Type` header. + /// If specific content type verification is required (e.g., `application/json`), + /// you must add this expectation manually. + /// + /// # Parameters + /// - `body`: The HTTP body object to be serialized to JSON. This object should implement both `serde::Serialize` and `serde::Deserialize`. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// use serde_json::json; + /// use serde::{Serialize, Deserialize}; + /// + /// #[derive(Serialize, Deserialize)] + /// struct TestUser { + /// name: String, + /// } + /// + /// // Initialize logging (optional, for debugging purposes) + /// let _ = env_logger::try_init(); + /// + /// // Start the mock server + /// let server = MockServer::start(); + /// + /// // Set up a mock endpoint + /// let m = server.mock(|when, then| { + /// when.path("/user") + /// .header("content-type", "application/json") + /// .json_body_obj(&TestUser { name: String::from("Fred") }); + /// then.status(200); + /// }); + /// + /// // Send a POST request with a JSON body + /// let response = Client::new() + /// .post(&format!("http://{}/user", server.address())) + /// .header("content-type", "application/json") + /// .body(json!(&TestUser { name: "Fred".to_string() }).to_string()) + /// .send() + /// .unwrap(); + /// + /// // Assert the mock was called and the response status is as expected + /// m.assert(); + /// assert_eq!(response.status(), 200); + /// ``` + /// + /// This method is particularly useful when you need to test server responses to structured JSON data. It helps + /// ensure that the JSON serialization and deserialization processes are correctly implemented in your API handling logic. + pub fn json_body_obj<'a, T>(self, body: &T) -> Self + where + T: Serialize + Deserialize<'a>, + { + let json_value = serde_json::to_value(body).expect("Cannot serialize json body to JSON"); + self.json_body(json_value) + } + // @docs-group: Body + + /// Sets the expected partial JSON body to check for specific content within a larger JSON structure. + /// + /// **Attention:** The partial JSON string must be a valid JSON string and should represent a substructure + /// of the full JSON object. It can omit irrelevant attributes but must maintain any necessary object hierarchy. + /// + /// **Note:** This method does not automatically set the `Content-Type` header to `application/json`. + /// You must explicitly set this header in your requests. + /// + /// # Parameters + /// - `partial_body`: The partial JSON content to check for. This must be a valid JSON string. + /// + /// # Example + /// Suppose your application sends the following JSON request body: + /// ```json + /// { + /// "parent_attribute": "Some parent data goes here", + /// "child": { + /// "target_attribute": "Example", + /// "other_attribute": "Another value" + /// } + /// } + /// ``` + /// To verify the presence of `target_attribute` with the value `Example` without needing the entire JSON object: + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// let server = MockServer::start(); + /// + /// let mock = server.mock(|when, then| { + /// when.json_body_includes(r#" + /// { + /// "child": { + /// "target_attribute": "Example" + /// } + /// } + /// "#); + /// then.status(200); + /// }); + /// + /// // Send a POST request with a JSON body + /// let response = Client::new() + /// .post(&format!("http://{}/some/path", server.address())) + /// .header("content-type", "application/json") + /// .body(r#" + /// { + /// "parent_attribute": "Some parent data goes here", + /// "child": { + /// "target_attribute": "Example", + /// "other_attribute": "Another value" + /// } + /// } + /// "#) + /// .send() + /// .unwrap(); + /// + /// // Assert the mock was called and the response status is as expected + /// mock.assert(); + /// assert_eq!(response.status(), 200); + /// ``` + /// It's important that the partial JSON contains the full object hierarchy necessary to reach the target attribute. + /// Irrelevant attributes such as `parent_attribute` and `child.other_attribute` can be omitted. + pub fn json_body_includes>(mut self, partial: IntoString) -> Self { + update_cell(&self.expectations, |e| { + if e.json_body_includes.is_none() { + e.json_body_includes = Some(Vec::new()); + } + let value = Value::from_str(&partial.into()) + .expect("cannot convert JSON string to serde value"); + e.json_body_includes.as_mut().unwrap().push(value); + }); + self + } + // @docs-group: Body + + /// Sets the expected partial JSON body to ensure that specific content is not present within a larger JSON structure. + /// + /// **Attention:** The partial JSON string must be a valid JSON string and should represent a substructure + /// of the full JSON object. It can omit irrelevant attributes but must maintain any necessary object hierarchy. + /// + /// **Note:** This method does not automatically set the `Content-Type` header to `application/json`. + /// You must explicitly set this header in your requests. + /// + /// # Parameters + /// - `partial_body`: The partial JSON content to check for exclusion. This must be a valid JSON string. + /// + /// # Example + /// Suppose your application sends the following JSON request body: + /// ```json + /// { + /// "parent_attribute": "Some parent data goes here", + /// "child": { + /// "target_attribute": "Example", + /// "other_attribute": "Another value" + /// } + /// } + /// ``` + /// To verify the absence of `target_attribute` with the value `Example`: + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// let server = MockServer::start(); + /// + /// let mock = server.mock(|when, then| { + /// when.json_body_excludes(r#" + /// { + /// "child": { + /// "target_attribute": "Example" + /// } + /// } + /// "#); + /// then.status(200); + /// }); + /// + /// // Send a POST request with a JSON body + /// let response = Client::new() + /// .post(&format!("http://{}/some/path", server.address())) + /// .header("content-type", "application/json") + /// .body(r#" + /// { + /// "parent_attribute": "Some parent data goes here", + /// "child": { + /// "other_attribute": "Another value" + /// } + /// } + /// "#) + /// .send() + /// .unwrap(); + /// + /// // Assert the mock was called and the response status is as expected + /// mock.assert(); + /// assert_eq!(response.status(), 200); + /// ``` + /// It's important that the partial JSON contains the full object hierarchy necessary to reach the target attribute. + /// Irrelevant attributes such as `parent_attribute` and `child.other_attribute` in the example can be omitted. + pub fn json_body_excludes>(mut self, partial: IntoString) -> Self { + update_cell(&self.expectations, |e| { + if e.json_body_excludes.is_none() { + e.json_body_excludes = Some(Vec::new()); + } + let value = Value::from_str(&partial.into()) + .expect("cannot convert JSON string to serde value"); + e.json_body_excludes.as_mut().unwrap().push(value); + }); + self + } + // @docs-group: Body + + /// Adds a key-value pair to the requirements for an `application/x-www-form-urlencoded` request body. + /// + /// This method sets an expectation for a specific key-value pair to be included in the request body + /// of an `application/x-www-form-urlencoded` POST request. Each key and value are URL-encoded as specified + /// by the [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded). + /// + /// **Note**: The mock server does not automatically verify that the HTTP method is POST as per spec. + /// If you want to verify that the request method is POST, you must explicitly set it in your mock configuration. + /// + /// # Parameters + /// - `key`: The key of the key-value pair to set as a requirement. + /// - `value`: The value of the key-value pair to set as a requirement. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Arrange + /// let server = MockServer::start(); + /// + /// let m = server.mock(|when, then| { + /// when.method(POST) + /// .path("/example") + /// .header("content-type", "application/x-www-form-urlencoded") + /// .form_urlencoded_tuple("name", "Peter Griffin") + /// .form_urlencoded_tuple("town", "Quahog"); + /// then.status(202); + /// }); + /// + /// let response = Client::new() + /// .post(server.url("/example")) + /// .header("content-type", "application/x-www-form-urlencoded") + /// .body("name=Peter%20Griffin&town=Quahog") + /// .send() + /// .unwrap(); + /// + /// // Assert + /// m.assert(); + /// assert_eq!(response.status(), 202); + /// ``` + /// + /// # Returns + /// `When`: Returns the modified `When` object with the new key-value pair added to the `application/x-www-form-urlencoded` expectations. + pub fn form_urlencoded_tuple, ValueString: Into>( + mut self, + key: KeyString, + value: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.form_urlencoded_tuple.is_none() { + e.form_urlencoded_tuple = Some(Vec::new()); + } + e.form_urlencoded_tuple + .as_mut() + .unwrap() + .push((key.into(), value.into())); + }); + self + } + // @docs-group: Body + + /// Adds a key-value pair to the negative requirements for an `application/x-www-form-urlencoded` request body. + /// + /// This method sets an expectation for a specific key-value pair to be excluded from the request body + /// of an `application/x-www-form-urlencoded` POST request. Each key and value are URL-encoded as specified + /// by the [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded). + /// + /// **Note**: The mock server does not automatically verify that the HTTP method is POST as per spec. + /// If you want to verify that the request method is POST, you must explicitly set it in your mock configuration. + /// + /// # Parameters + /// - `key`: The key of the key-value pair to set as a requirement. + /// - `value`: The value of the key-value pair to set as a requirement. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Arrange + /// let server = MockServer::start(); + /// + /// let m = server.mock(|when, then| { + /// when.method(POST) + /// .path("/example") + /// .header("content-type", "application/x-www-form-urlencoded") + /// .form_urlencoded_tuple_not("name", "Peter Griffin"); + /// then.status(202); + /// }); + /// + /// let response = Client::new() + /// .post(server.url("/example")) + /// .header("content-type", "application/x-www-form-urlencoded") + /// .body("name=Lois%20Griffin&town=Quahog") + /// .send() + /// .unwrap(); + /// + /// // Assert + /// m.assert(); + /// assert_eq!(response.status(), 202); + /// ``` + /// + /// # Returns + /// `When`: Returns the modified `When` object with the new key-value pair added to the negative `application/x-www-form-urlencoded` expectations. + pub fn form_urlencoded_tuple_not, ValueString: Into>( + mut self, + key: KeyString, + value: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.form_urlencoded_tuple_not.is_none() { + e.form_urlencoded_tuple_not = Some(Vec::new()); + } + e.form_urlencoded_tuple_not + .as_mut() + .unwrap() + .push((key.into(), value.into())); + }); + self + } + // @docs-group: Body + + /// Sets a requirement for the existence of a key in an `application/x-www-form-urlencoded` request body. + /// + /// This method sets an expectation that a specific key must be present in the request body of an + /// `application/x-www-form-urlencoded` POST request, regardless of its value. The key is URL-encoded + /// as specified by the [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded). + /// + /// **Note**: The mock server does not automatically verify that the HTTP method is POST as per spec. + /// If you want to verify that the request method is POST, you must explicitly set it in your mock configuration. + /// + /// # Parameters + /// - `key`: The key that must exist in the `application/x-www-form-urlencoded` request body. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Arrange + /// let server = MockServer::start(); + /// + /// let m = server.mock(|when, then| { + /// when.method(POST) + /// .path("/example") + /// .header("content-type", "application/x-www-form-urlencoded") + /// .form_urlencoded_tuple_exists("name") + /// .form_urlencoded_tuple_exists("town"); + /// then.status(202); + /// }); + /// + /// let response = Client::new() + /// .post(server.url("/example")) + /// .header("content-type", "application/x-www-form-urlencoded") + /// .body("name=Peter%20Griffin&town=Quahog") + /// .send() + /// .unwrap(); + /// + /// // Assert + /// m.assert(); + /// assert_eq!(response.status(), 202); + /// ``` + /// + /// # Returns + /// `When`: Returns the modified `When` object with the new key existence requirement added to the + /// `application/x-www-form-urlencoded` expectations. + pub fn form_urlencoded_tuple_exists>( + mut self, + key: IntoString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.form_urlencoded_tuple_exists.is_none() { + e.form_urlencoded_tuple_exists = Some(Vec::new()); + } + e.form_urlencoded_tuple_exists + .as_mut() + .unwrap() + .push(key.into()); + }); + self + } + // @docs-group: Body + + /// Sets a requirement that a key must be absent in an `application/x-www-form-urlencoded` request body. + /// + /// This method sets an expectation that a specific key must not be present in the request body of an + /// `application/x-www-form-urlencoded` POST request. The key is URL-encoded as specified by the + /// [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded). + /// + /// **Note**: The mock server does not automatically verify that the HTTP method is POST as per spec. + /// If you want to verify that the request method is POST, you must explicitly set it in your mock configuration. + /// + /// # Parameters + /// - `key`: The key that must be absent in the `application/x-www-form-urlencoded` request body. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Arrange + /// let server = MockServer::start(); + /// + /// let m = server.mock(|when, then| { + /// when.method(POST) + /// .path("/example") + /// .header("content-type", "application/x-www-form-urlencoded") + /// .form_urlencoded_tuple_missing("name") + /// .form_urlencoded_tuple_missing("town"); + /// then.status(202); + /// }); + /// + /// let response = Client::new() + /// .post(server.url("/example")) + /// .header("content-type", "application/x-www-form-urlencoded") + /// .body("city=Quahog&occupation=Cartoonist") + /// .send() + /// .unwrap(); + /// + /// // Assert + /// m.assert(); + /// assert_eq!(response.status(), 202); + /// ``` + /// + /// # Returns + /// `When`: Returns the modified `When` object with the new key absence requirement added to the + /// `application/x-www-form-urlencoded` expectations. + pub fn form_urlencoded_tuple_missing>( + mut self, + key: IntoString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.form_urlencoded_tuple_missing.is_none() { + e.form_urlencoded_tuple_missing = Some(Vec::new()); + } + e.form_urlencoded_tuple_missing + .as_mut() + .unwrap() + .push(key.into()); + }); + self + } + // @docs-group: Body + + /// Sets a requirement that a key's value in an `application/x-www-form-urlencoded` request body must contain a specific substring. + /// + /// This method sets an expectation that the value associated with a specific key must contain a specified substring + /// in the request body of an `application/x-www-form-urlencoded` POST request. The key and the substring are URL-encoded + /// as specified by the [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded). + /// + /// **Note**: The mock server does not automatically verify that the HTTP method is POST as per spec. + /// If you want to verify that the request method is POST, you must explicitly set it in your mock configuration. + /// + /// # Parameters + /// - `key`: The key in the `application/x-www-form-urlencoded` request body. + /// - `substring`: The substring that must be present in the value associated with the key. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Arrange + /// let server = MockServer::start(); + /// + /// let m = server.mock(|when, then| { + /// when.method(POST) + /// .path("/example") + /// .header("content-type", "application/x-www-form-urlencoded") + /// .form_urlencoded_tuple_includes("name", "Griffin") + /// .form_urlencoded_tuple_includes("town", "Quahog"); + /// then.status(202); + /// }); + /// + /// let response = Client::new() + /// .post(server.url("/example")) + /// .header("content-type", "application/x-www-form-urlencoded") + /// .body("name=Peter%20Griffin&town=Quahog") + /// .send() + /// .unwrap(); + /// + /// // Assert + /// m.assert(); + /// assert_eq!(response.status(), 202); + /// ``` + /// + /// # Returns + /// `When`: Returns the modified `When` object with the new key-value substring requirement added to the + /// `application/x-www-form-urlencoded` expectations. + pub fn form_urlencoded_tuple_includes, ValueString: Into>( + mut self, + key: KeyString, + substring: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.form_urlencoded_tuple_includes.is_none() { + e.form_urlencoded_tuple_includes = Some(Vec::new()); + } + e.form_urlencoded_tuple_includes + .as_mut() + .unwrap() + .push((key.into(), substring.into())); + }); + self + } + // @docs-group: Body + + /// Sets a requirement that a key's value in an `application/x-www-form-urlencoded` request body must not contain a specific substring. + /// + /// This method sets an expectation that the value associated with a specific key must not contain a specified substring + /// in the request body of an `application/x-www-form-urlencoded` POST request. The key and the substring are URL-encoded + /// as specified by the [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded). + /// + /// **Note**: The mock server does not automatically verify that the HTTP method is POST as per spec. + /// If you want to verify that the request method is POST, you must explicitly set it in your mock configuration. + /// + /// # Parameters + /// - `key`: The key in the `application/x-www-form-urlencoded` request body. + /// - `substring`: The substring that must not be present in the value associated with the key. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Arrange + /// let server = MockServer::start(); + /// + /// let m = server.mock(|when, then| { + /// when.method(POST) + /// .path("/example") + /// .header("content-type", "application/x-www-form-urlencoded") + /// .form_urlencoded_tuple_excludes("name", "Griffin"); + /// then.status(202); + /// }); + /// + /// let response = Client::new() + /// .post(server.url("/example")) + /// .header("content-type", "application/x-www-form-urlencoded") + /// .body("name=Lois%20Smith&city=Quahog") + /// .send() + /// .unwrap(); + /// + /// // Assert + /// m.assert(); + /// assert_eq!(response.status(), 202); + /// ``` + /// + /// # Returns + /// `When`: Returns the modified `When` object with the new key-value substring exclusion requirement added to the + /// `application/x-www-form-urlencoded` expectations. + pub fn form_urlencoded_tuple_excludes, ValueString: Into>( + mut self, + key: KeyString, + substring: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.form_urlencoded_tuple_excludes.is_none() { + e.form_urlencoded_tuple_excludes = Some(Vec::new()); + } + e.form_urlencoded_tuple_excludes + .as_mut() + .unwrap() + .push((key.into(), substring.into())); + }); + self + } + // @docs-group: Body + + /// Sets a requirement that a key's value in an `application/x-www-form-urlencoded` request body must start with a specific prefix. + /// + /// This method sets an expectation that the value associated with a specific key must start with a specified prefix + /// in the request body of an `application/x-www-form-urlencoded` POST request. The key and the prefix are URL-encoded + /// as specified by the [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded). + /// + /// **Note**: The mock server does not automatically verify that the HTTP method is POST as per spec. + /// If you want to verify that the request method is POST, you must explicitly set it in your mock configuration. + /// + /// # Parameters + /// - `key`: The key in the `application/x-www-form-urlencoded` request body. + /// - `prefix`: The prefix that must appear at the start of the value associated with the key. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Arrange + /// let server = MockServer::start(); + /// + /// let m = server.mock(|when, then| { + /// when.method(POST) + /// .path("/example") + /// .header("content-type", "application/x-www-form-urlencoded") + /// .form_urlencoded_tuple_prefix("name", "Pete") + /// .form_urlencoded_tuple_prefix("town", "Qua"); + /// then.status(202); + /// }); + /// + /// let response = Client::new() + /// .post(server.url("/example")) + /// .header("content-type", "application/x-www-form-urlencoded") + /// .body("name=Peter%20Griffin&town=Quahog") + /// .send() + /// .unwrap(); + /// + /// // Assert + /// m.assert(); + /// assert_eq!(response.status(), 202); + /// ``` + /// + /// # Returns + /// `When`: Returns the modified `When` object with the new key-value prefix requirement added to the + /// `application/x-www-form-urlencoded` expectations. + pub fn form_urlencoded_tuple_prefix, ValueString: Into>( + mut self, + key: KeyString, + prefix: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.form_urlencoded_tuple_prefix.is_none() { + e.form_urlencoded_tuple_prefix = Some(Vec::new()); + } + e.form_urlencoded_tuple_prefix + .as_mut() + .unwrap() + .push((key.into(), prefix.into())); + }); + self + } + // @docs-group: Body + + /// Sets a requirement that a key's value in an `application/x-www-form-urlencoded` request body must not start with a specific prefix. + /// + /// This method sets an expectation that the value associated with a specific key must not start with a specified prefix + /// in the request body of an `application/x-www-form-urlencoded` POST request. The key and the prefix are URL-encoded + /// as specified by the [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded). + /// + /// **Note**: The mock server does not automatically verify that the HTTP method is POST as per spec. + /// If you want to verify that the request method is POST, you must explicitly set it in your mock configuration. + /// + /// # Parameters + /// - `key`: The key in the `application/x-www-form-urlencoded` request body. + /// - `prefix`: The prefix that must not appear at the start of the value associated with the key. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Arrange + /// let server = MockServer::start(); + /// + /// let m = server.mock(|when, then| { + /// when.method(POST) + /// .path("/example") + /// .header("content-type", "application/x-www-form-urlencoded") + /// .form_urlencoded_tuple_prefix_not("name", "Lois") + /// .form_urlencoded_tuple_prefix_not("town", "Hog"); + /// then.status(202); + /// }); + /// + /// let response = Client::new() + /// .post(server.url("/example")) + /// .header("content-type", "application/x-www-form-urlencoded") + /// .body("name=Peter%20Griffin&town=Quahog") + /// .send() + /// .unwrap(); + /// + /// // Assert + /// m.assert(); + /// assert_eq!(response.status(), 202); + /// ``` + /// + /// # Returns + /// `When`: Returns the modified `When` object with the new key-value prefix exclusion requirement added to the + /// `application/x-www-form-urlencoded` expectations. + pub fn form_urlencoded_tuple_prefix_not, ValueString: Into>( + mut self, + key: KeyString, + prefix: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.form_urlencoded_tuple_prefix_not.is_none() { + e.form_urlencoded_tuple_prefix_not = Some(Vec::new()); + } + e.form_urlencoded_tuple_prefix_not + .as_mut() + .unwrap() + .push((key.into(), prefix.into())); + }); + self + } + // @docs-group: Body + + /// Sets a requirement that a key's value in an `application/x-www-form-urlencoded` request body must end with a specific suffix. + /// + /// This method sets an expectation that the value associated with a specific key must end with a specified suffix + /// in the request body of an `application/x-www-form-urlencoded` POST request. The key and the suffix are URL-encoded + /// as specified by the [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded). + /// + /// **Note**: The mock server does not automatically verify that the HTTP method is POST as per spec. + /// If you want to verify that the request method is POST, you must explicitly set it in your mock configuration. + /// + /// # Parameters + /// - `key`: The key in the `application/x-www-form-urlencoded` request body. + /// - `suffix`: The suffix that must appear at the end of the value associated with the key. + /// + /// # Example + /// ```rust + /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; + /// + /// // Arrange + /// let server = MockServer::start(); + /// + /// let m = server.mock(|when, then| { + /// when.method(POST) + /// .path("/example") + /// .header("content-type", "application/x-www-form-urlencoded") + /// .form_urlencoded_tuple_suffix("name", "Griffin") + /// .form_urlencoded_tuple_suffix("town", "hog"); + /// then.status(202); + /// }); + /// + /// let response = Client::new() + /// .post(server.url("/example")) + /// .header("content-type", "application/x-www-form-urlencoded") + /// .body("name=Peter%20Griffin&town=Quahog") + /// .send() + /// .unwrap(); + /// + /// // Assert + /// m.assert(); + /// assert_eq!(response.status(), 202); + /// ``` + /// + /// # Returns + /// `When`: Returns the modified `When` object with the new key-value suffix requirement added to the + /// `application/x-www-form-urlencoded` expectations. + pub fn form_urlencoded_tuple_suffix, ValueString: Into>( + mut self, + key: KeyString, + suffix: ValueString, + ) -> Self { + update_cell(&self.expectations, |e| { + if e.form_urlencoded_tuple_suffix.is_none() { + e.form_urlencoded_tuple_suffix = Some(Vec::new()); + } + e.form_urlencoded_tuple_suffix + .as_mut() + .unwrap() + .push((key.into(), suffix.into())); }); self } + // @docs-group: Body - /// Sets the expected HTTP header. - /// * `name` - The HTTP header name (header names are case-insensitive by RFC 2616). - /// * `value` - The header value. + /// Sets a requirement that a key's value in an `application/x-www-form-urlencoded` request body must not end with a specific suffix. + /// + /// This method sets an expectation that the value associated with a specific key must not end with a specified suffix + /// in the request body of an `application/x-www-form-urlencoded` POST request. The key and the suffix are URL-encoded + /// as specified by the [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded). + /// + /// **Note**: The mock server does not automatically verify that the HTTP method is POST as per spec. + /// If you want to verify that the request method is POST, you must explicitly set it in your mock configuration. + /// + /// # Parameters + /// - `key`: The key in the `application/x-www-form-urlencoded` request body. + /// - `suffix`: The suffix that must not appear at the end of the value associated with the key. /// /// # Example - /// ``` + /// ```rust /// use httpmock::prelude::*; - /// use isahc::{prelude::*, Request}; + /// use reqwest::blocking::Client; /// + /// // Arrange /// let server = MockServer::start(); /// - /// let mock = server.mock(|when, then|{ - /// when.header("Authorization", "token 1234567890"); - /// then.status(200); + /// let m = server.mock(|when, then| { + /// when.method(POST) + /// .path("/example") + /// .header("content-type", "application/x-www-form-urlencoded") + /// .form_urlencoded_tuple_suffix_not("name", "Smith") + /// .form_urlencoded_tuple_suffix_not("town", "ville"); + /// then.status(202); /// }); /// - /// Request::post(&format!("http://{}/test", server.address())) - /// .header("Authorization", "token 1234567890") - /// .body(()) - /// .unwrap() - /// .send() - /// .unwrap(); + /// let response = Client::new() + /// .post(server.url("/example")) + /// .header("content-type", "application/x-www-form-urlencoded") + /// .body("name=Peter%20Griffin&town=Quahog") + /// .send() + /// .unwrap(); /// - /// mock.assert(); + /// // Assert + /// m.assert(); + /// assert_eq!(response.status(), 202); /// ``` - pub fn header, SV: Into>(mut self, name: SK, value: SV) -> Self { + /// + /// # Returns + /// `When`: Returns the modified `When` object with the new key-value suffix exclusion requirement added to the + /// `application/x-www-form-urlencoded` expectations. + pub fn form_urlencoded_tuple_suffix_not, ValueString: Into>( + mut self, + key: KeyString, + suffix: ValueString, + ) -> Self { update_cell(&self.expectations, |e| { - if e.headers.is_none() { - e.headers = Some(Vec::new()); + if e.form_urlencoded_tuple_suffix_not.is_none() { + e.form_urlencoded_tuple_suffix_not = Some(Vec::new()); } - e.headers + e.form_urlencoded_tuple_suffix_not .as_mut() .unwrap() - .push((name.into(), value.into())); + .push((key.into(), suffix.into())); }); self } + // @docs-group: Body - /// Sets the requirement that the HTTP request needs to contain a specific header - /// (value is unchecked, refer to [Mock::expect_header](struct.Mock.html#method.expect_header)). + /// Sets a requirement that a key-value pair in an `application/x-www-form-urlencoded` request body must match specific regular expressions. + /// + /// This method sets an expectation that the key and the value in a key-value pair must match the specified regular expressions + /// in the request body of an `application/x-www-form-urlencoded` POST request. The key and value regular expressions are URL-encoded + /// as specified by the [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded). + /// + /// **Note**: The mock server does not automatically verify that the HTTP method is POST as per spec. + /// If you want to verify that the request method is POST, you must explicitly set it in your mock configuration. /// - /// * `name` - The HTTP header name (header names are case-insensitive by RFC 2616). + /// # Parameters + /// - `key_regex`: The regular expression that the key must match in the `application/x-www-form-urlencoded` request body. + /// - `value_regex`: The regular expression that the value must match in the `application/x-www-form-urlencoded` request body. /// /// # Example - /// ``` + /// ```rust /// use httpmock::prelude::*; - /// use isahc::{prelude::*, Request}; + /// use reqwest::blocking::Client; + /// use regex::Regex; /// + /// // Arrange /// let server = MockServer::start(); /// - /// let mock = server.mock(|when, then|{ - /// when.header_exists("Authorization"); - /// then.status(200); + /// let key_regex = Regex::new(r"^name$").unwrap(); + /// let value_regex = Regex::new(r"^Peter\sGriffin$").unwrap(); + /// + /// let m = server.mock(|when, then| { + /// when.method(POST) + /// .path("/example") + /// .header("content-type", "application/x-www-form-urlencoded") + /// .form_urlencoded_tuple_matches(key_regex, value_regex); + /// then.status(202); /// }); /// - /// Request::post(&format!("http://{}/test", server.address())) - /// .header("Authorization", "token 1234567890") - /// .body(()) - /// .unwrap() - /// .send() - /// .unwrap(); + /// let response = Client::new() + /// .post(server.url("/example")) + /// .header("content-type", "application/x-www-form-urlencoded") + /// .body("name=Peter%20Griffin&town=Quahog") + /// .send() + /// .unwrap(); /// - /// mock.assert(); + /// // Assert + /// m.assert(); + /// assert_eq!(response.status(), 202); /// ``` - pub fn header_exists>(mut self, name: S) -> Self { + /// + /// # Returns + /// `When`: Returns the modified `When` object with the new key-value regex matching requirement added to the + /// `application/x-www-form-urlencoded` expectations. + pub fn form_urlencoded_tuple_matches, ValueRegex: Into>( + mut self, + key_regex: KeyRegex, + value_regex: ValueRegex, + ) -> Self { update_cell(&self.expectations, |e| { - if e.header_exists.is_none() { - e.header_exists = Some(Vec::new()); + if e.form_urlencoded_tuple_matches.is_none() { + e.form_urlencoded_tuple_matches = Some(Vec::new()); } - e.header_exists.as_mut().unwrap().push(name.into()); + e.form_urlencoded_tuple_matches + .as_mut() + .unwrap() + .push((key_regex.into(), value_regex.into())); }); self } + // @docs-group: Body - /// Sets the cookie that needs to exist in the HTTP request. - /// Cookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html). - /// **Attention**: Cookie names are **case-sensitive**. + /// Sets a requirement for the number of times a key-value pair matching specific regular expressions appears in an `application/x-www-form-urlencoded` request body. /// - /// * `name` - The cookie name. - /// * `value` - The expected cookie value. + /// This method sets an expectation that the key-value pair must appear a specific number of times in the request body of an + /// `application/x-www-form-urlencoded` POST request. The key and value regular expressions are URL-encoded as specified by the + /// [URL Standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded). /// - /// > Note: This function is only available when the `cookies` feature is enabled. - /// > It is enabled by default. + /// **Note**: The mock server does not automatically verify that the HTTP method is POST as per spec. + /// If you want to verify that the request method is POST, you must explicitly set it in your mock configuration. + /// + /// # Parameters + /// - `key_regex`: The regular expression that the key must match in the `application/x-www-form-urlencoded` request body. + /// - `value_regex`: The regular expression that the value must match in the `application/x-www-form-urlencoded` request body. + /// - `count`: The number of times the key-value pair matching the regular expressions must appear. /// /// # Example - /// ``` + /// ```rust /// use httpmock::prelude::*; - /// use isahc::{prelude::*, Request}; + /// use reqwest::blocking::Client; + /// use regex::Regex; /// + /// // Arrange /// let server = MockServer::start(); /// - /// let mock = server.mock(|when, then|{ - /// when.cookie("SESSIONID", "1234567890"); - /// then.status(200); + /// let m = server.mock(|when, then| { + /// when.method(POST) + /// .path("/example") + /// .header("content-type", "application/x-www-form-urlencoded") + /// .form_urlencoded_tuple_count( + /// Regex::new(r"^name$").unwrap(), + /// Regex::new(r".*Griffin$").unwrap(), + /// 2 + /// ); + /// then.status(202); /// }); /// - /// Request::post(&format!("http://{}/test", server.address())) - /// .header("Cookie", "TRACK=12345; SESSIONID=1234567890; CONSENT=1") - /// .body(()) - /// .unwrap() - /// .send() - /// .unwrap(); + /// // Act + /// let response = Client::new() + /// .post(server.url("/example")) + /// .header("content-type", "application/x-www-form-urlencoded") + /// .body("name=Peter%20Griffin&name=Lois%20Griffin&town=Quahog") + /// .send() + /// .unwrap(); /// - /// mock.assert(); + /// // Assert + /// m.assert(); + /// assert_eq!(response.status(), 202); /// ``` - pub fn cookie, SV: Into>(mut self, name: SK, value: SV) -> Self { + /// + /// # Returns + /// `When`: Returns the modified `When` object with the new key-value count requirement added to the + /// `application/x-www-form-urlencoded` expectations. + pub fn form_urlencoded_tuple_count, ValueRegex: Into>( + mut self, + key_regex: KeyRegex, + value_regex: ValueRegex, + count: usize, + ) -> Self { update_cell(&self.expectations, |e| { - if e.cookies.is_none() { - e.cookies = Some(Vec::new()); + if e.form_urlencoded_tuple_count.is_none() { + e.form_urlencoded_tuple_count = Some(Vec::new()); } - e.cookies - .as_mut() - .unwrap() - .push((name.into(), value.into())); + e.form_urlencoded_tuple_count.as_mut().unwrap().push(( + key_regex.into(), + value_regex.into(), + count, + )); }); self } + // @docs-group: Body - /// Sets the cookie that needs to exist in the HTTP request. - /// Cookie parsing follows [RFC-6265](https://tools.ietf.org/html/rfc6265.html). - /// **Attention**: Cookie names are **case-sensitive**. + /// Adds a custom matcher for expected HTTP requests. If this function returns true, the request + /// is considered a match, and the mock server will respond to the request + /// (given all other criteria are also met). /// - /// * `name` - The cookie name + /// You can use this function to create custom expectations for your mock server based on any aspect + /// of the `HttpMockRequest` object. /// - /// > Note: This function is only available when the `cookies` feature is enabled. - /// > It is enabled by default. + /// # Parameters + /// - `matcher`: A function that takes a reference to an `HttpMockRequest` and returns a boolean indicating whether the request matches. /// - /// # Example + /// ## Example + /// ```rust + /// use httpmock::prelude::*; + /// + /// // Arrange + /// let server = MockServer::start(); + /// + /// let m = server.mock(|when, then| { + /// when.matches(|req: &HttpMockRequest| { + /// req.uri().path().contains("es") + /// }); + /// then.status(200); + /// }); + /// + /// // Act: Send the HTTP request + /// let response = reqwest::blocking::get(server.url("/test")).unwrap(); + /// + /// // Assert + /// m.assert(); + /// assert_eq!(response.status(), 200); /// ``` + /// + /// # Returns + /// `When`: Returns the modified `When` object with the new custom matcher added to the expectations. + #[deprecated( + since = "0.8.0", + note = "Please use the `is_true` and `is_false` function instead" + )] + pub fn matches( + mut self, + matcher: impl Fn(&HttpMockRequest) -> bool + Sync + Send + 'static, + ) -> Self { + return self.is_true(matcher); + } + // @docs-group: Custom + + /// Adds a custom matcher for expected HTTP requests. If this function returns true, the request + /// is considered a match, and the mock server will respond to the request + /// (given all other criteria are also met). + /// + /// You can use this function to create custom expectations for your mock server based on any aspect + /// of the `HttpMockRequest` object. + /// + /// # Parameters + /// - `matcher`: A function that takes a reference to an `HttpMockRequest` and returns a boolean indicating whether the request matches. + /// + /// ## Example + /// ```rust /// use httpmock::prelude::*; - /// use isahc::{prelude::*, Request}; /// + /// // Arrange /// let server = MockServer::start(); /// - /// let mock = server.mock(|when, then|{ - /// when.cookie_exists("SESSIONID"); - /// then.status(200); + /// let m = server.mock(|when, then| { + /// when.is_true(|req: &HttpMockRequest| { + /// req.uri().path().contains("es") + /// }); + /// then.status(200); /// }); /// - /// Request::post(&format!("http://{}/test", server.address())) - /// .header("Cookie", "TRACK=12345; SESSIONID=1234567890; CONSENT=1") - /// .body(()) - /// .unwrap() - /// .send() - /// .unwrap(); + /// // Act: Send the HTTP request + /// let response = reqwest::blocking::get(server.url("/test")).unwrap(); /// - /// mock.assert(); + /// // Assert + /// m.assert(); + /// assert_eq!(response.status(), 200); /// ``` - pub fn cookie_exists>(mut self, name: S) -> Self { + /// + /// # Returns + /// `When`: Returns the modified `When` object with the new custom matcher added to the expectations. + pub fn is_true( + mut self, + matcher: impl Fn(&HttpMockRequest) -> bool + Sync + Send + 'static, + ) -> Self { update_cell(&self.expectations, |e| { - if e.cookie_exists.is_none() { - e.cookie_exists = Some(Vec::new()); + if e.is_true.is_none() { + e.is_true = Some(Vec::new()); } - e.cookie_exists.as_mut().unwrap().push(name.into()); + e.is_true.as_mut().unwrap().push(Arc::new(matcher)); }); self } - /// Sets a custom matcher for expected HTTP request. If this function returns true, the request - /// is considered a match and the mock server will respond to the request + // @docs-group: Custom + + /// Adds a custom matcher for expected HTTP requests. If this function returns false, the request + /// is considered a match, and the mock server will respond to the request /// (given all other criteria are also met). - /// * `matcher` - The matcher function. /// - /// ## Example: - /// ``` + /// You can use this function to create custom expectations for your mock server based on any aspect + /// of the `HttpMockRequest` object. + /// + /// # Parameters + /// - `matcher`: A function that takes a reference to an `HttpMockRequest` and returns a boolean indicating whether the request matches. + /// + /// ## Example + /// ```rust /// use httpmock::prelude::*; /// /// // Arrange /// let server = MockServer::start(); /// - /// let m = server.mock(|when, then|{ - /// when.matches(|req: &HttpMockRequest| { - /// req.path.contains("es") + /// let m = server.mock(|when, then| { + /// when.is_false(|req: &HttpMockRequest| { + /// req.uri().path().contains("es") /// }); - /// then.status(200); + /// then.status(404); /// }); /// /// // Act: Send the HTTP request - /// let response = isahc::get(server.url("/test")).unwrap(); + /// let response = reqwest::blocking::get(server.url("/test")).unwrap(); /// /// // Assert /// m.assert(); - /// assert_eq!(response.status(), 200); + /// assert_eq!(response.status(), 404); /// ``` - pub fn matches(mut self, matcher: MockMatcherFunction) -> Self { + /// + /// # Returns + /// `When`: Returns the modified `When` object with the new custom matcher added to the expectations. + pub fn is_false( + mut self, + matcher: impl Fn(&HttpMockRequest) -> bool + Sync + Send + 'static, + ) -> Self { update_cell(&self.expectations, |e| { - if e.matchers.is_none() { - e.matchers = Some(Vec::new()); + if e.is_false.is_none() { + e.is_false = Some(Vec::new()); } - e.matchers.as_mut().unwrap().push(matcher); + e.is_false.as_mut().unwrap().push(Arc::new(matcher)); }); self } - /// A semantic convenience for applying multiple operations - /// on a given `When` instance via an discrete encapsulating - /// function. + // @docs-group: Custom + + /// Applies a specified function to enhance or modify the `When` instance. This method allows for the + /// encapsulation of multiple matching conditions into a single function, maintaining a clear and fluent + /// interface for setting up HTTP request expectations. /// - /// ## Example: + /// This method is particularly useful for reusing common setup patterns across multiple test scenarios, + /// promoting cleaner and more maintainable test code. /// - /// ``` - /// # use httpmock::When; - /// // Assuming an encapsulating function like: + /// # Parameters + /// - `func`: A function that takes a `When` instance and returns it after applying some conditions. + /// + /// ## Example + /// ```rust + /// use httpmock::{prelude::*, When}; + /// use httpmock::Method::POST; /// + /// // Function to apply a standard authorization and content type setup for JSON POST requests /// fn is_authorized_json_post_request(when: When) -> When { - /// when.method(httpmock::Method::POST) - /// .header("authorization", "SOME API KEY") - /// .header("content-type", "application/json") + /// when.method(POST) + /// .header("Authorization", "SOME API KEY") + /// .header("Content-Type", "application/json") /// } /// - /// // It can be applied without breaking the usual `When` - /// // semantic style. Meaning instead of: - /// # - /// # fn counter_example(when: When) -> When { - /// is_authorized_json_post_request(when.json_body_partial(r#"{"key": "value"}"#)) - /// # } - /// - /// // the `and` method can be used to preserve the - /// // legibility of the method chain: - /// # fn semantic_example(when: When) -> When { - /// when.query_param("some-param", "some-value") - /// .and(is_authorized_json_post_request) - /// .json_body_partial(r#"{"key": "value"}"#) - /// # } - /// - /// // is still intuitively legible as "when some query - /// // parameter equals "some-value", the request is an - /// // authorized POST request, and the request body - /// // is the literal JSON object `{"key": "value"}`. - /// - pub fn and(mut self, func: AndWhenFunction) -> Self { + /// // Usage example demonstrating how to maintain fluent interface style with complex setups. + /// // This approach keeps the chain of conditions clear and readable, enhancing test legibility + /// let server = MockServer::start(); + /// let m = server.mock(|when, then| { + /// when.query_param("user_id", "12345") + /// .and(is_authorized_json_post_request) // apply the function to include common setup + /// .json_body_includes(r#"{"key": "value"}"#); // additional specific condition + /// then.status(200); + /// }); + /// ``` + /// + /// # Returns + /// `When`: The modified `When` instance with additional conditions applied, suitable for further chaining. + pub fn and(mut self, func: impl FnOnce(When) -> When) -> Self { func(self) } + // @docs-group: Miscellaneous } -/// A type that allows the specification of HTTP response values. +/// Represents the configuration of HTTP responses in a mock server environment. +/// +/// The `Then` structure is used to define the details of the HTTP response that will be sent if +/// an incoming request meets the conditions specified by a corresponding `When` structure. It +/// allows for detailed customization of response aspects such as status codes, headers, body +/// content, and delays. This structure is integral to defining how the mock server behaves when +/// it receives a request that matches the defined expectations. pub struct Then { pub(crate) response_template: Rc>, } impl Then { - /// Sets the HTTP response code that will be returned by the mock server. + /// Configures the HTTP response status code that the mock server will return. /// - /// * `status` - The status code. + /// # Parameters + /// - `status`: A `u16` HTTP status code that the mock server should return for the configured request. /// - /// ## Example: - /// ``` + /// # Returns + /// Returns `self` to allow chaining of method calls on the `Mock` object. + /// + /// # Example + /// Demonstrates setting a 200 OK status for a request to the path `/hello`. + /// + /// ```rust /// use httpmock::prelude::*; /// - /// // Arrange + /// // Initialize the mock server /// let server = MockServer::start(); /// - /// let m = server.mock(|when, then|{ + /// // Configure the mock + /// let m = server.mock(|when, then| { /// when.path("/hello"); /// then.status(200); /// }); /// - /// // Act - /// let response = isahc::get(server.url("/hello")).unwrap(); + /// // Send a request and verify the response + /// let response = reqwest::blocking::get(server.url("/hello")).unwrap(); /// - /// // Assert + /// // Check that the mock was called as expected and the response status is as configured /// m.assert(); /// assert_eq!(response.status(), 200); /// ``` - pub fn status(mut self, status: u16) -> Self { + pub fn status>(mut self, status: U16) -> Self + where + >::Error: std::fmt::Debug, + { update_cell(&self.response_template, |r| { - r.status = Some(status); + r.status = Some( + status + .try_into() + .expect("cannot parse status code to usize"), + ); }); self } + // @docs-group: Status - /// Sets the HTTP response body that will be returned by the mock server. + /// Configures the HTTP response body that the mock server will return. /// - /// * `body` - The response body content. + /// # Parameters + /// - `body`: The content of the response body, provided as a type that can be referenced as a byte slice. /// - /// ## Example: - /// ``` + /// # Returns + /// Returns `self` to allow chaining of method calls on the `Mock` object. + /// + /// # Example + /// Demonstrates setting a response body for a request to the path `/hello` with a 200 OK status. + /// + /// ```rust /// use httpmock::prelude::*; - /// use isahc::{prelude::*, ResponseExt}; + /// use reqwest::blocking::Client; /// - /// // Arrange + /// // Initialize the mock server /// let server = MockServer::start(); /// + /// // Configure the mock /// let m = server.mock(|when, then| { /// when.path("/hello"); /// then.status(200) /// .body("ohi!"); /// }); /// - /// // Act - /// let mut response = isahc::get(server.url("/hello")).unwrap(); + /// // Send a request and verify the response + /// let response = Client::new() + /// .get(server.url("/hello")) + /// .send() + /// .unwrap(); /// - /// // Assert + /// // Check that the mock was called as expected and the response body is as configured /// m.assert(); /// assert_eq!(response.status(), 200); /// assert_eq!(response.text().unwrap(), "ohi!"); /// ``` - pub fn body(mut self, body: impl AsRef<[u8]>) -> Self { + pub fn body>(mut self, body: SliceRef) -> Self { update_cell(&self.response_template, |r| { - r.body = Some(body.as_ref().to_vec()); + r.body = Some(HttpMockBytes::from(Bytes::copy_from_slice(body.as_ref()))); }); self } + // @docs-group: Body - /// Sets the HTTP response body that will be returned by the mock server. + /// Configures the HTTP response body with content loaded from a specified file on the mock server. /// - /// * `body` - The response body content. + /// # Parameters + /// - `resource_file_path`: A string representing the path to the file whose contents will be used as the response body. The path can be absolute or relative to the server's running directory. /// - /// ## Example: - /// ``` + /// # Returns + /// Returns `self` to allow chaining of method calls on the `Mock` object. + /// + /// # Panics + /// Panics if the specified file cannot be read, or if the path provided cannot be resolved to an absolute path. + /// + /// # Example + /// Demonstrates setting the response body from a file for a request to the path `/hello` with a 200 OK status. + /// + /// ```rust /// use httpmock::prelude::*; - /// use isahc::{prelude::*, ResponseExt}; + /// use reqwest::blocking::Client; /// - /// // Arrange + /// // Initialize the mock server /// let server = MockServer::start(); /// - /// let m = server.mock(|when, then|{ + /// // Configure the mock + /// let m = server.mock(|when, then| { /// when.path("/hello"); /// then.status(200) /// .body_from_file("tests/resources/simple_body.txt"); /// }); /// - /// // Act - /// let mut response = isahc::get(server.url("/hello")).unwrap(); + /// // Send a request and verify the response + /// let response = Client::new() + /// .get(server.url("/hello")) + /// .send() + /// .unwrap(); /// - /// // Assert + /// // Check that the mock was called as expected and the response body matches the file contents /// m.assert(); /// assert_eq!(response.status(), 200); /// assert_eq!(response.text().unwrap(), "ohi!"); /// ``` - pub fn body_from_file>(mut self, resource_file_path: S) -> Self { + pub fn body_from_file>( + mut self, + resource_file_path: IntoString, + ) -> Self { let resource_file_path = resource_file_path.into(); let path = Path::new(&resource_file_path); let absolute_path = match path.is_absolute() { @@ -940,30 +5068,34 @@ impl Then { )); self.body(content) } + // @docs-group: Body /// Sets the JSON body for the HTTP response that will be returned by the mock server. /// - /// The provided JSON object needs to be both, a deserializable and serializable serde object. + /// This function accepts a JSON object that must be serializable and deserializable by serde. + /// Note that this method does not automatically set the "Content-Type" header to "application/json". + /// You will need to set this header manually if required. /// - /// Note that this method does not set the "content-type" header automatically, so you need - /// to provide one yourself! + /// # Parameters + /// - `body`: The HTTP response body in the form of a `serde_json::Value` object. /// - /// * `body` - The HTTP response body the mock server will return in the form of a - /// serde_json::Value object. + /// # Returns + /// Returns `self` to allow chaining of method calls on the `Mock` object. /// - /// ## Example - /// You can use this method conveniently as follows: - /// ``` + /// # Example + /// Demonstrates how to set a JSON body and a matching "Content-Type" header for a mock response. + /// + /// ```rust /// use httpmock::prelude::*; /// use serde_json::{Value, json}; - /// use isahc::ResponseExt; - /// use isahc::prelude::*; + /// use reqwest::blocking::Client; /// /// // Arrange /// let _ = env_logger::try_init(); /// let server = MockServer::start(); /// - /// let m = server.mock(|when, then|{ + /// // Configure the mock + /// let m = server.mock(|when, then| { /// when.path("/user"); /// then.status(200) /// .header("content-type", "application/json") @@ -971,38 +5103,58 @@ impl Then { /// }); /// /// // Act - /// let mut response = isahc::get(server.url("/user")).unwrap(); + /// let response = Client::new() + /// .get(server.url("/user")) + /// .send() + /// .unwrap(); + /// + /// // Get the status code first + /// let status = response.status(); + /// + /// // Extract the text from the response + /// let response_text = response.text().unwrap(); /// + /// // Deserialize the JSON response /// let user: Value = - /// serde_json::from_str(&response.text().unwrap()).expect("cannot deserialize JSON"); + /// serde_json::from_str(&response_text).expect("cannot deserialize JSON"); /// /// // Assert /// m.assert(); - /// assert_eq!(response.status(), 200); - /// assert_eq!(user.as_object().unwrap().get("name").unwrap(), "Hans"); + /// assert_eq!(status, 200); + /// assert_eq!(user["name"], "Hans"); /// ``` pub fn json_body>(mut self, body: V) -> Self { update_cell(&self.response_template, |r| { - r.body = Some(body.into().to_string().into_bytes()); + r.body = Some(HttpMockBytes::from(Bytes::from(body.into().to_string()))); }); self } + // @docs-group: Body - /// Sets the JSON body that will be returned by the mock server. - /// This method expects a serializable serde object that will be serialized/deserialized - /// to/from a JSON string. + /// Sets the JSON body that will be returned by the mock server using a serializable serde object. /// - /// Note that this method does not set the "content-type" header automatically, so you - /// need to provide one yourself! + /// This method converts the provided object into a JSON string. It does not automatically set + /// the "Content-Type" header to "application/json", so you must set this header manually if it's + /// needed. /// - /// * `body` - The HTTP body object that will be serialized to JSON using serde. + /// # Parameters + /// - `body`: A reference to an object that implements the `serde::Serialize` trait. /// - /// ``` + /// # Returns + /// Returns `self` to allow chaining of method calls on the `Mock` object. + /// + /// # Panics + /// Panics if the object cannot be serialized into a JSON string. + /// + /// # Example + /// Demonstrates setting a JSON body and the corresponding "Content-Type" header for a user object. + /// + /// ```rust /// use httpmock::prelude::*; - /// use isahc::{prelude::*, ResponseExt}; + /// use reqwest::blocking::Client; + /// use serde::{Serialize, Deserialize}; /// - /// // This is a temporary type that we will use for this example - /// #[derive(serde::Serialize, serde::Deserialize)] + /// #[derive(Serialize, Deserialize)] /// struct TestUser { /// name: String, /// } @@ -1011,6 +5163,7 @@ impl Then { /// let _ = env_logger::try_init(); /// let server = MockServer::start(); /// + /// // Configure the mock /// let m = server.mock(|when, then| { /// when.path("/user"); /// then.status(200) @@ -1021,55 +5174,81 @@ impl Then { /// }); /// /// // Act - /// let mut response = isahc::get(server.url("/user")).unwrap(); + /// let response = Client::new() + /// .get(server.url("/user")) + /// .send() + /// .unwrap(); /// + /// // Get the status code first + /// let status = response.status(); + /// + /// // Extract the text from the response + /// let response_text = response.text().unwrap(); + /// + /// // Deserialize the JSON response into a TestUser object /// let user: TestUser = - /// serde_json::from_str(&response.text().unwrap()).unwrap(); + /// serde_json::from_str(&response_text).unwrap(); /// /// // Assert /// m.assert(); - /// assert_eq!(response.status(), 200); + /// assert_eq!(status, 200); /// assert_eq!(user.name, "Hans"); /// ``` - pub fn json_body_obj(self, body: &T) -> Self - where - T: Serialize, - { + pub fn json_body_obj(self, body: &T) -> Self { let json_body = - serde_json::to_value(body).expect("cannot serialize json body to JSON string "); + serde_json::to_value(body).expect("Failed to serialize object to JSON string"); self.json_body(json_body) } + // @docs-group: Body - /// Sets an HTTP header that the mock server will return. + /// Sets an HTTP header that the mock server will return in the response. /// - /// * `name` - The name of the header. - /// * `value` - The value of the header. + /// This method configures a response header to be included when the mock server handles a request. /// - /// ## Example - /// You can use this method conveniently as follows: - /// ``` - /// // Arrange + /// # Parameters + /// - `name`: The name of the header to set. + /// - `value`: The value of the header. + /// + /// # Returns + /// Returns `self` to allow chaining of method calls on the `Mock` object. + /// + /// # Example + /// Demonstrates setting the "Expires" header for a response to a request to the root path. + /// + /// ```rust /// use httpmock::prelude::*; - /// use serde_json::Value; - /// use isahc::ResponseExt; + /// use reqwest::blocking::Client; /// + /// // Arrange /// let _ = env_logger::try_init(); /// let server = MockServer::start(); /// - /// let m = server.mock(|when, then|{ + /// // Configure the mock + /// let m = server.mock(|when, then| { /// when.path("/"); /// then.status(200) /// .header("Expires", "Wed, 21 Oct 2050 07:28:00 GMT"); /// }); /// /// // Act - /// let mut response = isahc::get(server.url("/")).unwrap(); + /// let response = Client::new() + /// .get(server.url("/")) + /// .send() + /// .unwrap(); /// /// // Assert /// m.assert(); /// assert_eq!(response.status(), 200); + /// assert_eq!( + /// response.headers().get("Expires").unwrap().to_str().unwrap(), + /// "Wed, 21 Oct 2050 07:28:00 GMT" + /// ); /// ``` - pub fn header, SV: Into>(mut self, name: SK, value: SV) -> Self { + pub fn header, ValueString: Into>( + mut self, + name: KeyString, + value: ValueString, + ) -> Self { update_cell(&self.response_template, |r| { if r.headers.is_none() { r.headers = Some(Vec::new()); @@ -1081,21 +5260,38 @@ impl Then { }); self } + // @docs-group: Headers - /// Sets a duration that will delay the mock server response. + /// Sets a delay for the mock server response. /// - /// * `duration` - The delay. + /// This method configures the server to wait for a specified duration before sending a response, + /// which can be useful for testing timeout scenarios or asynchronous operations. /// - /// ``` - /// // Arrange + /// # Parameters + /// - `duration`: The length of the delay as a `std::time::Duration`. + /// + /// # Returns + /// Returns `self` to allow chaining of method calls on the `Mock` object. + /// + /// # Panics + /// Panics if the specified duration results in a delay that cannot be represented as a 64-bit + /// unsigned integer of milliseconds (more than approximately 584 million years). + /// + /// # Example + /// Demonstrates setting a 3-second delay for a request to the path `/delay`. + /// + /// ```rust /// use std::time::{SystemTime, Duration}; /// use httpmock::prelude::*; + /// use reqwest::blocking::Client; /// + /// // Arrange /// let _ = env_logger::try_init(); /// let start_time = SystemTime::now(); /// let three_seconds = Duration::from_secs(3); /// let server = MockServer::start(); /// + /// // Configure the mock /// let mock = server.mock(|when, then| { /// when.path("/delay"); /// then.status(200) @@ -1103,55 +5299,72 @@ impl Then { /// }); /// /// // Act - /// let response = isahc::get(server.url("/delay")).unwrap(); + /// let response = Client::new() + /// .get(server.url("/delay")) + /// .send() + /// .unwrap(); /// /// // Assert /// mock.assert(); - /// assert_eq!(start_time.elapsed().unwrap() > three_seconds, true); + /// assert!(start_time.elapsed().unwrap() >= three_seconds); /// ``` pub fn delay>(mut self, duration: D) -> Self { + let duration = duration.into(); + + // Ensure the delay duration does not exceed the maximum u64 milliseconds limit + let millis = duration.as_millis(); + let max = u64::MAX as u128; + if millis >= max { + panic!("A delay higher than {} milliseconds is not supported.", max) + } + update_cell(&self.response_template, |r| { - r.delay = Some(duration.into()); + r.delay = Some(duration.as_millis() as u64); }); self } - /// A semantic convenience for applying multiple operations - /// on a given `Then` instance via an discrete encapsulating - /// function. + // @docs-group: Network + + /// Applies a custom function to modify a `Then` instance, enhancing flexibility and readability + /// in setting up mock server responses. /// - /// ## Example: + /// This method allows you to encapsulate complex configurations into reusable functions, + /// and apply them without breaking the chain of method calls on a `Then` object. /// - /// ``` - /// # use std::time::Duration; - /// use hyper::http;use httpmock::Then; - /// // Assuming an encapsulating function like: + /// # Parameters + /// - `func`: A function that takes a `Then` instance and returns it after applying some modifications. /// + /// # Returns + /// Returns `self` to allow chaining of method calls on the `Then` object. + /// + /// # Example + /// Demonstrates how to use the `and` method to maintain readability while applying multiple + /// modifications from an external function. + /// + /// ```rust + /// use std::time::Duration; + /// use http::{StatusCode, header::HeaderValue}; + /// use httpmock::{Then, MockServer}; + /// + /// // Function that configures a response with JSON content and a delay /// fn ok_json_with_delay(then: Then) -> Then { - /// then.status(http::StatusCode::OK.as_u16()) + /// then.status(StatusCode::OK.as_u16()) /// .header("content-type", "application/json") /// .delay(Duration::from_secs_f32(0.5)) /// } /// - /// // It can be applied without breaking the usual `Then` - /// // semantic style. Meaning instead of: - /// # - /// # fn counter_example(then: Then) -> Then { - /// ok_json_with_delay(then.header("general-vibe", "not great my guy")) - /// # } - /// - /// // the `and` method can be used to preserve the - /// // legibility of the method chain: - /// # fn semantic_example(then: Then) -> Then { - /// then.header("general-vibe", "much better") - /// .and(ok_json_with_delay) - /// # } - /// - /// // is still intuitively legible as "{when some criteria}, - /// // then set the 'general-vibe' header to 'much better' - /// // *and* the status code to 200 (ok), the 'content-type' - /// // header to 'application/json' and return it with a delay - /// // of 0.50 seconds". - pub fn and(mut self, func: AndThenFunction) -> Self { + /// // Usage within a method chain + /// let server = MockServer::start(); + /// let then = server.mock(|when, then| { + /// when.path("/example"); + /// then.header("general-vibe", "much better") + /// .and(ok_json_with_delay); + /// }); + /// + /// // The `and` method keeps the setup intuitively readable as a continuous chain + /// ``` + pub fn and(mut self, func: impl FnOnce(Then) -> Then) -> Self { func(self) } + // @docs-group: Miscellaneous } diff --git a/src/common/data.rs b/src/common/data.rs index 37b7469e..64b5ccd6 100644 --- a/src/common/data.rs +++ b/src/common/data.rs @@ -1,52 +1,379 @@ extern crate serde_regex; -use std::cmp::Ordering; -use std::collections::BTreeMap; -use std::fmt; -use std::fmt::Debug; -use std::sync::atomic::AtomicUsize; -use std::sync::atomic::Ordering::Relaxed; -use std::sync::{Arc, RwLock}; -use std::time::Duration; - -use regex::Regex; +use crate::{ + common::{ + data::Error::{ + HeaderDeserializationError, RequestConversionError, StaticMockConversionError, + }, + util::HttpMockBytes, + }, + server::matchers::generic::MatchingStrategy, +}; +use bytes::Bytes; use serde::{Deserialize, Serialize}; use serde_json::Value; +use std::{ + cmp::Ordering, + collections::HashMap, + convert::{TryFrom, TryInto}, + fmt, + fmt::Debug, + str::FromStr, + sync::Arc, +}; +use url::Url; + +use crate::server::RequestMetadata; +#[cfg(feature = "cookies")] +use headers::{Cookie, HeaderMapExt}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Cannot deserialize header: {0}")] + HeaderDeserializationError(String), + #[error("Cookie parser error: {0}")] + CookieParserError(String), + #[error("cannot convert to/from static mock: {0}")] + StaticMockConversionError(String), + #[error("JSONConversionError: {0}")] + JSONConversionError(#[from] serde_json::Error), + #[error("Invalid request data: {0}")] + InvalidRequestData(String), + #[error("Cannot convert request to/from internal structure: {0}")] + RequestConversionError(String), +} /// A general abstraction of an HTTP request of `httpmock`. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct HttpMockRequest { - pub path: String, - pub method: String, - pub headers: Option>, - pub query_params: Option>, - pub body: Option>, + scheme: String, + uri: String, + method: String, + headers: Vec<(String, String)>, + version: String, + body: HttpMockBytes, } impl HttpMockRequest { - pub fn new(method: String, path: String) -> Self { + pub(crate) fn new( + scheme: String, + uri: String, + method: String, + headers: Vec<(String, String)>, + version: String, + body: HttpMockBytes, + ) -> Self { + // TODO: Many fields from the struct are exposed as structures from http package to the user. + // These values here are also converted to these http crate structures every call. + // ==> Convert these values here into http crate structures and allow returning an error + // here instead of "unwrap" all the time later (see functions below). + // Convert into http crate structures once here and store the converted + // values in the struct instance here rather than only String values everywhere. + // This will require to make the HttpMockRequest serde compatible + // (http types are not serializable by default). Self { - path, + scheme, + uri, method, - headers: None, - query_params: None, - body: None, + headers, + version, + body, + } + } + + /// Parses and returns the URI of the request. + /// + /// # Attention + /// + /// - This method returns the full URI of the request as an `http::Uri` object. + /// - The URI returned by this method does not include the `Host` part. In HTTP/1.1, + /// the request line typically contains only the path and query, not the full URL with the host. + /// - To retrieve the host, you should use the `HttpMockRequest::host` method which extracts the `Host` + /// header (for HTTP/1.1) or the `:authority` pseudo-header (for HTTP/2 and HTTP/3). + /// + /// # Returns + /// + /// An `http::Uri` object representing the full URI of the request. + pub fn uri(&self) -> http::Uri { + self.uri.parse().unwrap() + } + + /// Parses the scheme from the request. + /// + /// This function extracts the scheme (protocol) used in the request. If the request contains a relative path, + /// the scheme will be inferred based on how the server received the request. For instance, if the request was + /// sent to the server using HTTPS, the scheme will be set to "https"; otherwise, it will be set to "http". + /// + /// # Returns + /// + /// A `String` representing the scheme of the request, either "https" or "http". + pub fn scheme(&self) -> String { + let uri = self.uri(); + if let Some(scheme) = uri.scheme() { + return scheme.to_string(); + } + + self.scheme.clone() + } + + /// Returns the URI of the request as a string slice. + /// + /// # Attention + /// + /// - This method returns the full URI as a string slice. + /// - The URI string returned by this method does not include the `Host` part. In HTTP/1.1, + /// the request line typically contains only the path and query, not the full URL with the host. + /// - To retrieve the host, you should use the `host` method which extracts the `Host` + /// header (for HTTP/1.1) or the `:authority` pseudo-header (for HTTP/2 and HTTP/3). + /// + /// # Returns + /// + /// A string slice representing the full URI of the request. + pub fn uri_str(&self) -> &str { + self.uri.as_ref() + } + + /// Returns the host that the request was sent to, based on the `Host` header or `:authority` pseudo-header. + /// + /// # Attention + /// + /// - This method retrieves the host from the `Host` header of the HTTP request for HTTP/1.1 requests. + /// For HTTP/2 and HTTP/3 requests, it retrieves the host from the `:authority` pseudo-header. + /// - If you use the `HttpMockRequest::uri` method to get the full URI, note that + /// the URI might not include the host part. In HTTP/1.1, the request line + /// typically contains only the path and query, not the full URL. + /// + /// # Returns + /// + /// An `Option` containing the host if the `Host` header or `:authority` pseudo-header is present, or + /// `None` if neither is found. + pub fn host(&self) -> Option { + // Check the Host header first (HTTP 1.1) + if let Some((_, host)) = self + .headers + .iter() + .find(|&&(ref k, _)| k.eq_ignore_ascii_case("host")) + { + return Some(host.split(':').next().unwrap().to_string()); + } + + // If Host header is not found, check the URI authority (HTTP/2 and HTTP/3) + let uri = self.uri(); + if let Some(authority) = uri.authority() { + return Some(authority.as_str().split(':').next().unwrap().to_string()); + } + + None + } + + /// Returns the port that the request was sent to, based on the `Host` header or `:authority` pseudo-header. + /// + /// # Attention + /// + /// 1. This method retrieves the port from the `Host` header of the HTTP request for HTTP/1.1 requests. + /// For HTTP/2 and HTTP/3 requests, it retrieves the port from the `:authority` pseudo-header. + /// This method attempts to parse the port as a `u16`. If the port cannot be parsed as a `u16`, this method will continue as if the port was not specified (see point 2). + /// 2. If the port is not specified in the `Host` header or `:authority` pseudo-header, this method will return 443 (https) or 80 (http) based on the used scheme. + /// + /// # Returns + /// + /// An `u16` containing the port if the `Host` header or `:authority` pseudo-header is present and includes a valid port, + /// or 443 (https) or 80 (http) based on the used scheme otherwise. + pub fn port(&self) -> u16 { + // Check the Host header first (HTTP 1.1) + if let Some((_, host)) = self + .headers + .iter() + .find(|&&(ref k, _)| k.eq_ignore_ascii_case("host")) + { + if let Some(port_str) = host.split(':').nth(1) { + if let Ok(port) = port_str.parse::() { + return port; + } + } + } + + // If Host header is not found, check the URI authority (HTTP/2 and HTTP/3) + let uri = self.uri(); + if let Some(authority) = uri.authority() { + if let Some(port_str) = authority.as_str().split(':').nth(1) { + if let Ok(port) = port_str.parse::() { + return port; + } + } + } + + if self.scheme().eq("https") { + return 443; + } + + return 80; + } + + pub fn method(&self) -> http::Method { + http::Method::from_bytes(self.method.as_bytes()).unwrap() + } + + pub fn method_str(&self) -> &str { + self.method.as_ref() + } + + pub fn headers(&self) -> http::HeaderMap { + let mut header_map: http::HeaderMap = http::HeaderMap::new(); + for (key, value) in &self.headers { + let header_name = http::HeaderName::from_bytes(key.as_bytes()).unwrap(); + let header_value = http::HeaderValue::from_str(&value).unwrap(); + + header_map.insert(header_name, header_value); + } + + header_map + } + + pub fn headers_vec(&self) -> &Vec<(String, String)> { + self.headers.as_ref() + } + + pub fn query_params(&self) -> HashMap { + self.query_params_vec().into_iter().collect() + } + + pub fn query_params_vec(&self) -> Vec<(String, String)> { + // There doesn't seem to be a way to just parse Query string with `url` crate, so we're + // prefixing a dummy URL for parsing. + let url = format!("http://dummy?{}", self.uri().query().unwrap_or("")); + let url = Url::parse(&url).unwrap(); + + url.query_pairs() + .map(|(k, v)| (k.into_owned(), v.into_owned())) + .collect() + } + + pub fn body(&self) -> &HttpMockBytes { + &self.body + } + + pub fn body_string(&self) -> String { + self.body.to_string() + } + + pub fn body_ref<'a>(&'a self) -> &'a [u8] { + self.body.as_ref() + } + + // Move all body functions to HttpMockBytes + pub fn body_vec(&self) -> Vec { + self.body.to_vec() + } + + pub fn body_bytes(&self) -> bytes::Bytes { + self.body.to_bytes() + } + + pub fn version(&self) -> http::Version { + match self.version.as_ref() { + "HTTP/0.9" => http::Version::HTTP_09, + "HTTP/1.0" => http::Version::HTTP_10, + "HTTP/1.1" => http::Version::HTTP_11, + "HTTP/2.0" => http::Version::HTTP_2, + "HTTP/3.0" => http::Version::HTTP_3, + // Attention: This scenario is highly unlikely, so we panic here for the users + // convenience (user does not need to deal with errors for this reason alone). + _ => panic!("unknown HTTP version: {:?}", self.version), } } - pub fn with_headers(mut self, arg: Vec<(String, String)>) -> Self { - self.headers = Some(arg); - self + pub fn version_ref(&self) -> &str { + self.version.as_ref() } - pub fn with_query_params(mut self, arg: Vec<(String, String)>) -> Self { - self.query_params = Some(arg); - self + #[cfg(feature = "cookies")] + pub(crate) fn cookies(&self) -> Result, Error> { + let mut result = Vec::new(); + + if let Some(cookie) = self.headers().typed_get::() { + for (key, value) in cookie.iter() { + result.push((key.to_string(), value.to_string())); + } + } + + Ok(result) + } + + pub fn to_http_request(&self) -> http::Request { + self.try_into().unwrap() + } +} + +fn headers_to_vec(parts: &http::request::Parts) -> Vec<(String, String)> { + parts + .headers + .iter() + .map(|(name, value)| { + ( + name.as_str().to_string(), + value.to_str().unwrap().to_string(), + ) + }) + .collect() +} + +fn http_headers_to_vec(req: &http::Request) -> Result, Error> { + req.headers() + .iter() + .map(|(name, value)| { + // Attempt to convert the HeaderValue to a &str, returning an error if it fails. + let value_str = value + .to_str() + .map_err(|e| RequestConversionError(e.to_string()))?; + Ok((name.as_str().to_string(), value_str.to_string())) + }) + .collect() +} + +impl TryInto> for &HttpMockRequest { + type Error = Error; + + fn try_into(self) -> Result, Self::Error> { + let mut builder = http::Request::builder() + .method(self.method()) + .uri(self.uri()) + .version(self.version()); + + for (k, v) in self.headers() { + builder = builder.header(k.map_or(String::new(), |v| v.to_string()), v) + } + + let req = builder + .body(self.body().to_bytes()) + .map_err(|err| RequestConversionError(err.to_string()))?; + + Ok(req) } +} + +impl TryFrom<&http::Request> for HttpMockRequest { + type Error = Error; - pub fn with_body(mut self, arg: Vec) -> Self { - self.body = Some(arg); - self + fn try_from(value: &http::Request) -> Result { + let metadata = value + .extensions() + .get::() + .unwrap_or_else(|| panic!("request metadata was not added to the request")); + + let headers = http_headers_to_vec(&value)?; + + // Since Bytes shares data, clone does not copy the body. + let body = HttpMockBytes::from(value.body().clone()); + + Ok(HttpMockRequest::new( + metadata.scheme.to_string(), + value.uri().to_string(), + value.method().to_string(), + headers, + format!("{:?}", value.version()), + body, + )) } } @@ -56,8 +383,8 @@ pub struct MockServerHttpResponse { pub status: Option, pub headers: Option>, #[serde(default, with = "opt_vector_serde_base64")] - pub body: Option>, - pub delay: Option, + pub body: Option, + pub delay: Option, } impl MockServerHttpResponse { @@ -77,8 +404,41 @@ impl Default for MockServerHttpResponse { } } +impl TryFrom<&http::Response> for MockServerHttpResponse { + type Error = Error; + + fn try_from(value: &http::Response) -> Result { + let mut headers = Vec::with_capacity(value.headers().len()); + + for (key, value) in value.headers() { + let value = value + .to_str() + .map_err(|err| HeaderDeserializationError(err.to_string()))?; + + headers.push((key.as_str().to_string(), value.to_string())) + } + + Ok(Self { + status: Some(value.status().as_u16()), + headers: if !headers.is_empty() { + Some(headers) + } else { + None + }, + body: if !value.body().is_empty() { + Some(HttpMockBytes::from(value.body().clone())) + } else { + None + }, + delay: None, + }) + } +} + /// Serializes and deserializes the response body to/from a Base64 string. mod opt_vector_serde_base64 { + use crate::common::util::HttpMockBytes; + use bytes::Bytes; use serde::{Deserialize, Deserializer, Serializer}; // See the following references: @@ -97,23 +457,24 @@ mod opt_vector_serde_base64 { // See the following references: // https://github.com/serde-rs/serde/issues/1444 - pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { #[derive(Deserialize)] - struct Wrapper(#[serde(deserialize_with = "from_base64")] Vec); + struct Wrapper(#[serde(deserialize_with = "from_base64")] HttpMockBytes); let v = Option::deserialize(deserializer)?; Ok(v.map(|Wrapper(a)| a)) } - fn from_base64<'de, D>(deserializer: D) -> Result, D::Error> + fn from_base64<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { - let vec = Vec::deserialize(deserializer)?; - base64::decode(vec).map_err(serde::de::Error::custom) + let value = Vec::deserialize(deserializer)?; + let decoded = base64::decode(value).map_err(serde::de::Error::custom)?; + Ok(HttpMockBytes::from(Bytes::from(decoded))) } } @@ -137,62 +498,145 @@ impl fmt::Debug for MockServerHttpResponse { /// A general abstraction of an HTTP request for all handlers. #[derive(Serialize, Deserialize, Clone, Debug)] -pub struct Pattern { - #[serde(with = "serde_regex")] - pub regex: Regex, -} - -impl Pattern { - pub fn from_regex(regex: Regex) -> Pattern { - Pattern { regex } - } -} +pub struct HttpMockRegex(#[serde(with = "serde_regex")] pub regex::Regex); -impl Ord for Pattern { +impl Ord for HttpMockRegex { fn cmp(&self, other: &Self) -> Ordering { - self.regex.as_str().cmp(other.regex.as_str()) + self.0.as_str().cmp(other.0.as_str()) } } -impl PartialOrd for Pattern { +impl PartialOrd for HttpMockRegex { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl PartialEq for Pattern { +impl PartialEq for HttpMockRegex { fn eq(&self, other: &Self) -> bool { - self.regex.as_str() == other.regex.as_str() + self.0.as_str() == other.0.as_str() + } +} + +impl Eq for HttpMockRegex {} + +impl From for HttpMockRegex { + fn from(value: regex::Regex) -> Self { + HttpMockRegex(value) + } +} + +impl From<&str> for HttpMockRegex { + fn from(value: &str) -> Self { + let re = regex::Regex::from_str(value).expect("cannot parse value as regex"); + HttpMockRegex::from(re) } } -impl Eq for Pattern {} +impl From for HttpMockRegex { + fn from(value: String) -> Self { + HttpMockRegex::from(value.as_str()) + } +} -pub type MockMatcherFunction = fn(&HttpMockRequest) -> bool; +impl fmt::Display for HttpMockRegex { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} /// A general abstraction of an HTTP request for all handlers. #[derive(Serialize, Deserialize, Clone)] pub struct RequestRequirements { - pub path: Option, - pub path_contains: Option>, - pub path_matches: Option>, + pub scheme: Option, + pub scheme_not: Option, // NEW + pub host: Option, + pub host_not: Option>, // NEW + pub host_contains: Option>, // NEW + pub host_excludes: Option>, // NEW + pub host_prefix: Option>, // NEW + pub host_suffix: Option>, // NEW + pub host_prefix_not: Option>, // NEW + pub host_suffix_not: Option>, // NEW + pub host_matches: Option>, + pub port: Option, + pub port_not: Option>, // NEW pub method: Option, - pub headers: Option>, + pub method_not: Option>, // NEW + pub path: Option, + pub path_not: Option>, // NEW + pub path_includes: Option>, // NEW + pub path_excludes: Option>, // NEW + pub path_prefix: Option>, // NEW + pub path_suffix: Option>, // NEW + pub path_prefix_not: Option>, // NEW + pub path_suffix_not: Option>, // NEW + pub path_matches: Option>, + pub query_param: Option>, + pub query_param_not: Option>, // NEW + pub query_param_exists: Option>, + pub query_param_missing: Option>, // NEW + pub query_param_includes: Option>, // NEW + pub query_param_excludes: Option>, // NEW + pub query_param_prefix: Option>, // NEW + pub query_param_suffix: Option>, // NEW + pub query_param_prefix_not: Option>, // NEW + pub query_param_suffix_not: Option>, // NEW + pub query_param_matches: Option>, // NEW + pub query_param_count: Option>, // NEW + pub header: Option>, // CHANGED from headers to header + pub header_not: Option>, // NEW pub header_exists: Option>, - pub cookies: Option>, + pub header_missing: Option>, // NEW + pub header_includes: Option>, // NEW + pub header_excludes: Option>, // NEW + pub header_prefix: Option>, // NEW + pub header_suffix: Option>, // NEW + pub header_prefix_not: Option>, // NEW + pub header_suffix_not: Option>, // NEW + pub header_matches: Option>, // NEW + pub header_count: Option>, // NEW + pub cookie: Option>, // CHANGED from cookies to cookie + pub cookie_not: Option>, // NEW pub cookie_exists: Option>, - pub body: Option, + pub cookie_missing: Option>, // NEW + pub cookie_includes: Option>, // NEW + pub cookie_excludes: Option>, // NEW + pub cookie_prefix: Option>, // NEW + pub cookie_suffix: Option>, // NEW + pub cookie_prefix_not: Option>, // NEW + pub cookie_suffix_not: Option>, // NEW + pub cookie_matches: Option>, // NEW + pub cookie_count: Option>, // NEW // NEW + pub body: Option, + pub body_not: Option>, // NEW + pub body_includes: Option>, // CHANG + pub body_excludes: Option>, // NEW + pub body_prefix: Option>, // NEW + pub body_suffix: Option>, // NEW + pub body_prefix_not: Option>, // + pub body_suffix_not: Option>, // + pub body_matches: Option>, // NEW pub json_body: Option, + pub json_body_not: Option, // NEW pub json_body_includes: Option>, - pub body_contains: Option>, - pub body_matches: Option>, - pub query_param_exists: Option>, - pub query_param: Option>, - pub x_www_form_urlencoded_key_exists: Option>, - pub x_www_form_urlencoded: Option>, - - #[serde(skip_serializing, skip_deserializing)] - pub matchers: Option>, + pub json_body_excludes: Option>, // NEW + pub form_urlencoded_tuple: Option>, + pub form_urlencoded_tuple_not: Option>, // NEW + pub form_urlencoded_tuple_exists: Option>, + pub form_urlencoded_tuple_missing: Option>, // NEW + pub form_urlencoded_tuple_includes: Option>, // NEW + pub form_urlencoded_tuple_excludes: Option>, // NEW + pub form_urlencoded_tuple_prefix: Option>, // NEW + pub form_urlencoded_tuple_suffix: Option>, // NEW + pub form_urlencoded_tuple_prefix_not: Option>, // NEW + pub form_urlencoded_tuple_suffix_not: Option>, // NEW + pub form_urlencoded_tuple_matches: Option>, // NEW + pub form_urlencoded_tuple_count: Option>, // NEW + #[serde(skip)] + pub is_true: Option bool + Sync + Send>>>, // NEW + DEPRECATE matches() -> point to using "is_true" instead + #[serde(skip)] + pub is_false: Option bool + Sync + Send>>>, // NEW } impl Default for RequestRequirements { @@ -204,101 +648,95 @@ impl Default for RequestRequirements { impl RequestRequirements { pub fn new() -> Self { Self { + scheme: None, + scheme_not: None, + host: None, + host_not: None, + host_contains: None, + host_excludes: None, + host_prefix: None, + host_suffix: None, + host_prefix_not: None, + host_suffix_not: None, + host_matches: None, + port: None, path: None, - path_contains: None, + path_not: None, + path_includes: None, + path_excludes: None, + path_prefix: None, + path_suffix: None, + path_prefix_not: None, + path_suffix_not: None, path_matches: None, method: None, - headers: None, + header: None, + header_not: None, header_exists: None, - cookies: None, + header_missing: None, + header_includes: None, + header_excludes: None, + header_prefix: None, + header_suffix: None, + header_prefix_not: None, + header_suffix_not: None, + header_matches: None, + header_count: None, + cookie: None, + cookie_not: None, cookie_exists: None, + cookie_missing: None, + cookie_includes: None, + cookie_excludes: None, + cookie_prefix: None, + cookie_suffix: None, + cookie_prefix_not: None, + cookie_suffix_not: None, + cookie_matches: None, + cookie_count: None, body: None, json_body: None, + json_body_not: None, json_body_includes: None, - body_contains: None, + body_includes: None, + body_excludes: None, + body_prefix: None, + body_suffix: None, + body_prefix_not: None, + body_suffix_not: None, body_matches: None, query_param_exists: None, + query_param_missing: None, + query_param_includes: None, + query_param_excludes: None, + query_param_prefix: None, + query_param_suffix: None, + query_param_prefix_not: None, + query_param_suffix_not: None, + query_param_matches: None, + query_param_count: None, query_param: None, - x_www_form_urlencoded: None, - x_www_form_urlencoded_key_exists: None, - matchers: None, + form_urlencoded_tuple: None, + form_urlencoded_tuple_not: None, + form_urlencoded_tuple_exists: None, + form_urlencoded_tuple_missing: None, + form_urlencoded_tuple_includes: None, + form_urlencoded_tuple_excludes: None, + form_urlencoded_tuple_prefix: None, + form_urlencoded_tuple_suffix: None, + form_urlencoded_tuple_prefix_not: None, + form_urlencoded_tuple_suffix_not: None, + form_urlencoded_tuple_matches: None, + form_urlencoded_tuple_count: None, + is_true: None, + port_not: None, + method_not: None, + query_param_not: None, + body_not: None, + json_body_excludes: None, + is_false: None, } } - - pub fn with_path(mut self, arg: String) -> Self { - self.path = Some(arg); - self - } - - pub fn with_method(mut self, arg: String) -> Self { - self.method = Some(arg); - self - } - - pub fn with_body(mut self, arg: String) -> Self { - self.body = Some(arg); - self - } - - pub fn with_json_body(mut self, arg: Value) -> Self { - self.json_body = Some(arg); - self - } - - pub fn with_path_contains(mut self, arg: Vec) -> Self { - self.path_contains = Some(arg); - self - } - - pub fn with_path_matches(mut self, arg: Vec) -> Self { - self.path_matches = Some(arg); - self - } - - pub fn with_headers(mut self, arg: Vec<(String, String)>) -> Self { - self.headers = Some(arg); - self - } - - pub fn with_header_exists(mut self, arg: Vec) -> Self { - self.header_exists = Some(arg); - self - } - - pub fn with_cookies(mut self, arg: Vec<(String, String)>) -> Self { - self.cookies = Some(arg); - self - } - - pub fn with_cookie_exists(mut self, arg: Vec) -> Self { - self.cookie_exists = Some(arg); - self - } - - pub fn with_json_body_includes(mut self, arg: Vec) -> Self { - self.json_body_includes = Some(arg); - self - } - - pub fn with_body_contains(mut self, arg: Vec) -> Self { - self.body_contains = Some(arg); - self - } - - pub fn with_body_matches(mut self, arg: Vec) -> Self { - self.body_matches = Some(arg); - self - } - - pub fn with_query_param_exists(mut self, arg: Vec) -> Self { - self.query_param_exists = Some(arg); - self - } - - pub fn with_query_param(mut self, arg: Vec<(String, String)>) -> Self { - self.query_param = Some(arg); - self - } } /// A Request that is made to set a new mock. @@ -317,17 +755,6 @@ impl MockDefinition { } } -#[derive(Serialize, Deserialize)] -pub struct MockRef { - pub mock_id: usize, -} - -impl MockRef { - pub fn new(mock_id: usize) -> Self { - Self { mock_id } - } -} - #[derive(Serialize, Deserialize, Clone)] pub struct ActiveMock { pub id: usize, @@ -337,16 +764,62 @@ pub struct ActiveMock { } impl ActiveMock { - pub fn new(id: usize, mock_definition: MockDefinition, is_static: bool) -> Self { + pub fn new( + id: usize, + definition: MockDefinition, + call_counter: usize, + is_static: bool, + ) -> Self { ActiveMock { id, - definition: mock_definition, - call_counter: 0, + definition, + call_counter, is_static, } } } +#[derive(Serialize, Deserialize, Clone)] +pub struct ActiveForwardingRule { + pub id: usize, + pub config: ForwardingRuleConfig, +} + +impl ActiveForwardingRule { + pub fn new(id: usize, config: ForwardingRuleConfig) -> Self { + ActiveForwardingRule { id, config } + } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct ActiveProxyRule { + pub id: usize, + pub config: ProxyRuleConfig, +} + +impl ActiveProxyRule { + pub fn new(id: usize, config: ProxyRuleConfig) -> Self { + ActiveProxyRule { id, config } + } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct ActiveRecording { + pub id: usize, + pub config: RecordingRuleConfig, + pub mocks: Vec, +} + +impl ActiveRecording { + pub fn new(id: usize, config: RecordingRuleConfig) -> Self { + ActiveRecording { + id, + config, + mocks: vec![], + } + } +} + #[derive(Serialize, Deserialize)] pub struct ClosestMatch { pub request: HttpMockRequest, @@ -396,102 +869,970 @@ pub enum Tokenizer { } #[derive(Debug, Serialize, Deserialize)] -pub struct Reason { +pub struct KeyValueComparisonKeyValuePair { + pub key: String, + pub value: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KeyValueComparisonAttribute { + pub operator: String, + pub expected: String, + pub actual: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KeyValueComparison { + pub key: Option, + pub value: Option, + pub expected_count: Option, + pub actual_count: Option, + pub all: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct FunctionComparison { + pub index: usize, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SingleValueComparison { + pub operator: String, pub expected: String, pub actual: String, - pub comparison: String, - pub best_match: bool, } #[derive(Debug, Serialize, Deserialize)] pub struct Mismatch { - pub title: String, - pub reason: Option, + pub entity: String, + pub matcher_method: String, + pub comparison: Option, + pub key_value_comparison: Option, + pub function_comparison: Option, + pub matching_strategy: Option, + pub best_match: bool, pub diff: Option, } -#[cfg(test)] -mod test { - use std::collections::BTreeMap; - - use regex::Regex; - use serde_json::json; - - use crate::common::data::{Pattern, RequestRequirements}; - - /// This test makes sure that adding the matching rules to a mock fills the struct as expected. - #[test] - fn fill_mock_requirements() { - // Arrange - let with_path = "with_path"; - let with_path_contains = vec!["with_path_contains".into()]; - let with_path_matches = vec![Pattern::from_regex( - Regex::new(r#"with_path_matches"#).unwrap(), - )]; - let mut with_headers = Vec::new(); - with_headers.push(("test".into(), "value".into())); - let with_method = "GET"; - let with_body = "with_body"; - let with_body_contains = vec!["body_contains".into()]; - let with_body_matches = vec![Pattern::from_regex( - Regex::new(r#"with_body_matches"#).unwrap(), - )]; - let with_json_body = json!(12.5); - let with_json_body_includes = vec![json!(12.5)]; - let with_query_param_exists = vec!["with_query_param_exists".into()]; - let mut with_query_param = Vec::new(); - with_query_param.push(("with_query_param".into(), "value".into())); - let with_header_exists = vec!["with_header_exists".into()]; - - // Act - let rr = RequestRequirements::new() - .with_path(with_path.clone().into()) - .with_path_contains(with_path_contains.clone()) - .with_path_matches(with_path_matches.clone()) - .with_headers(with_headers.clone()) - .with_method(with_method.clone().into()) - .with_body(with_body.clone().into()) - .with_body_contains(with_body_contains.clone()) - .with_body_matches(with_body_matches.clone()) - .with_json_body(with_json_body.clone()) - .with_json_body_includes(with_json_body_includes.clone()) - .with_query_param_exists(with_query_param_exists.clone()) - .with_query_param(with_query_param.clone()) - .with_header_exists(with_header_exists.clone()); - - // Assert - assert_eq!(rr.path.as_ref().unwrap(), with_path.clone()); - assert_eq!( - rr.path_contains.as_ref().unwrap(), - &with_path_contains.clone() - ); - assert_eq!( - rr.path_matches.as_ref().unwrap(), - &with_path_matches.clone() - ); - assert_eq!(rr.headers.as_ref().unwrap(), &with_headers.clone()); - assert_eq!(rr.body.as_ref().unwrap(), with_body.clone()); - assert_eq!( - rr.body_contains.as_ref().unwrap(), - &with_body_contains.clone() - ); - assert_eq!( - rr.body_matches.as_ref().unwrap(), - &with_body_matches.clone() - ); - assert_eq!(rr.json_body.as_ref().unwrap(), &with_json_body.clone()); - assert_eq!( - rr.json_body_includes.as_ref().unwrap(), - &with_json_body_includes.clone() - ); - assert_eq!( - rr.query_param_exists.as_ref().unwrap(), - &with_query_param_exists.clone() - ); - assert_eq!(rr.query_param.as_ref().unwrap(), &with_query_param.clone()); - assert_eq!( - rr.header_exists.as_ref().unwrap(), - &with_header_exists.clone() +// ************************************************************************************************* +// Configs and Builders +// ************************************************************************************************* + +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct RecordingRuleConfig { + pub request_requirements: RequestRequirements, + pub record_headers: Vec, + pub record_response_delays: bool, +} + +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct ProxyRuleConfig { + pub request_requirements: RequestRequirements, + pub request_header: Vec<(String, String)>, +} + +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct ForwardingRuleConfig { + pub target_base_url: String, + pub request_requirements: RequestRequirements, + pub request_header: Vec<(String, String)>, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct NameValueStringPair { + name: String, + value: String, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct NameValuePatternPair { + name: HttpMockRegex, + value: HttpMockRegex, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct KeyPatternCountPair { + key: HttpMockRegex, + count: usize, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct ValuePatternCountPair { + value: HttpMockRegex, + count: usize, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct KeyValuePatternCountTriple { + name: HttpMockRegex, + value: HttpMockRegex, + count: usize, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct StaticRequestRequirements { + // Scheme-related fields + #[serde(skip_serializing_if = "Option::is_none")] + pub scheme: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub scheme_not: Option, + + // Host-related fields + #[serde(skip_serializing_if = "Option::is_none")] + pub host: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub host_not: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub host_contains: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub host_excludes: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub host_prefix: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub host_suffix: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub host_prefix_not: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub host_suffix_not: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub host_matches: Option>, + + // Port-related fields + #[serde(skip_serializing_if = "Option::is_none")] + pub port: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub port_not: Option>, + + // Path-related fields + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub path_not: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub path_contains: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub path_excludes: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub path_prefix: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub path_suffix: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub path_prefix_not: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub path_suffix_not: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub path_matches: Option>, + + // Method-related fields + #[serde(skip_serializing_if = "Option::is_none")] + pub method: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub method_not: Option>, + + // Query Parameter-related fields + #[serde(skip_serializing_if = "Option::is_none")] + pub query_param: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub query_param_not: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub query_param_exists: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub query_param_missing: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub query_param_contains: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub query_param_excludes: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub query_param_prefix: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub query_param_suffix: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub query_param_prefix_not: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub query_param_suffix_not: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub query_param_matches: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub query_param_count: Option>, + + // Header-related fields + #[serde(skip_serializing_if = "Option::is_none")] + pub header: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub header_not: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub header_exists: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub header_missing: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub header_contains: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub header_excludes: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub header_prefix: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub header_suffix: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub header_prefix_not: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub header_suffix_not: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub header_matches: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub header_count: Option>, + + // Cookie-related fields + #[serde(skip_serializing_if = "Option::is_none")] + pub cookie: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub cookie_not: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub cookie_exists: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub cookie_missing: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub cookie_contains: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub cookie_excludes: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub cookie_prefix: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub cookie_suffix: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub cookie_prefix_not: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub cookie_suffix_not: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub cookie_matches: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub cookie_count: Option>, + + // Body-related fields + #[serde(skip_serializing_if = "Option::is_none")] + pub body: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_base64: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_not: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_not_base64: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_contains: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_contains_base64: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_excludes: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_excludes_base64: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_prefix: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_prefix_base64: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_suffix: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_suffix_base64: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_prefix_not: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_prefix_not_base64: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_suffix_not: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_suffix_not_base64: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_matches: Option>, + + // JSON Body-related fields + #[serde(skip_serializing_if = "Option::is_none")] + pub json_body: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub json_body_not: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub json_body_includes: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub json_body_excludes: Option>, + + // x-www-form-urlencoded fields + #[serde(skip_serializing_if = "Option::is_none")] + pub form_urlencoded_tuple: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub form_urlencoded_tuple_not: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub form_urlencoded_key_exists: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub form_urlencoded_key_missing: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub form_urlencoded_contains: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub form_urlencoded_excludes: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub form_urlencoded_prefix: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub form_urlencoded_suffix: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub form_urlencoded_prefix_not: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub form_urlencoded_suffix_not: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub form_urlencoded_matches: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub form_urlencoded_count: Option>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct StaticHTTPResponse { + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub header: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub body: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_base64: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub delay: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct StaticMockDefinition { + when: StaticRequestRequirements, + then: StaticHTTPResponse, +} + +impl TryInto for StaticMockDefinition { + type Error = Error; + + fn try_into(self) -> Result { + Ok(MockDefinition { + request: RequestRequirements { + // Scheme-related fields + scheme: self.when.scheme, + scheme_not: self.when.scheme_not, + + // Host-related fields + host: self.when.host, + host_not: self.when.host_not, + host_contains: self.when.host_contains, + host_excludes: self.when.host_excludes, + host_prefix: self.when.host_prefix, + host_suffix: self.when.host_suffix, + host_prefix_not: self.when.host_prefix_not, + host_suffix_not: self.when.host_suffix_not, + host_matches: self.when.host_matches, + + // Port-related fields + port: self.when.port, + port_not: self.when.port_not, + + // Path-related fields + path: self.when.path, + path_not: self.when.path_not, + path_includes: self.when.path_contains, + path_excludes: self.when.path_excludes, + path_prefix: self.when.path_prefix, + path_suffix: self.when.path_suffix, + path_prefix_not: self.when.path_prefix_not, + path_suffix_not: self.when.path_suffix_not, + path_matches: self.when.path_matches, + + // Method-related fields + method: self.when.method.map(|m| m.to_string()), + method_not: from_method_vec(self.when.method_not), + // Query Parameter-related fields + query_param: from_name_value_string_pair_vec(self.when.query_param), + query_param_not: from_name_value_string_pair_vec(self.when.query_param_not), + query_param_exists: self.when.query_param_exists, + query_param_missing: self.when.query_param_missing, + query_param_includes: from_name_value_string_pair_vec( + self.when.query_param_contains, + ), + query_param_excludes: from_name_value_string_pair_vec( + self.when.query_param_excludes, + ), + query_param_prefix: from_name_value_string_pair_vec(self.when.query_param_prefix), + query_param_suffix: from_name_value_string_pair_vec(self.when.query_param_suffix), + query_param_prefix_not: from_name_value_string_pair_vec( + self.when.query_param_prefix_not, + ), + query_param_suffix_not: from_name_value_string_pair_vec( + self.when.query_param_suffix_not, + ), + query_param_matches: from_name_value_pattern_pair_vec( + self.when.query_param_matches, + ), + query_param_count: from_key_value_pattern_count_triple_vec( + self.when.query_param_count, + ), + + // Header-related fields + header: from_name_value_string_pair_vec(self.when.header), + header_not: from_name_value_string_pair_vec(self.when.header_not), + header_exists: self.when.header_exists, + header_missing: self.when.header_missing, + header_includes: from_name_value_string_pair_vec(self.when.header_contains), + header_excludes: from_name_value_string_pair_vec(self.when.header_excludes), + header_prefix: from_name_value_string_pair_vec(self.when.header_prefix), + header_suffix: from_name_value_string_pair_vec(self.when.header_suffix), + header_prefix_not: from_name_value_string_pair_vec(self.when.header_prefix_not), + header_suffix_not: from_name_value_string_pair_vec(self.when.header_suffix_not), + header_matches: from_name_value_pattern_pair_vec(self.when.header_matches), + header_count: from_key_value_pattern_count_triple_vec(self.when.header_count), + // Cookie-related fields + cookie: from_name_value_string_pair_vec(self.when.cookie), + cookie_not: from_name_value_string_pair_vec(self.when.cookie_not), + cookie_exists: self.when.cookie_exists, + cookie_missing: self.when.cookie_missing, + cookie_includes: from_name_value_string_pair_vec(self.when.cookie_contains), + cookie_excludes: from_name_value_string_pair_vec(self.when.cookie_excludes), + cookie_prefix: from_name_value_string_pair_vec(self.when.cookie_prefix), + cookie_suffix: from_name_value_string_pair_vec(self.when.cookie_suffix), + cookie_prefix_not: from_name_value_string_pair_vec(self.when.cookie_prefix_not), + cookie_suffix_not: from_name_value_string_pair_vec(self.when.cookie_suffix_not), + cookie_matches: from_name_value_pattern_pair_vec(self.when.cookie_matches), + cookie_count: from_key_value_pattern_count_triple_vec(self.when.cookie_count), + + // Body-related fields + body: from_string_to_bytes_choose(self.when.body, self.when.body_base64), + body_not: to_bytes_vec(self.when.body_not, self.when.body_not_base64), + body_includes: to_bytes_vec( + self.when.body_contains, + self.when.body_contains_base64, + ), + body_excludes: to_bytes_vec( + self.when.body_excludes, + self.when.body_excludes_base64, + ), + body_prefix: to_bytes_vec(self.when.body_prefix, self.when.body_prefix_base64), + body_suffix: to_bytes_vec(self.when.body_suffix, self.when.body_suffix_base64), + body_prefix_not: to_bytes_vec( + self.when.body_prefix_not, + self.when.body_prefix_not_base64, + ), + body_suffix_not: to_bytes_vec( + self.when.body_suffix_not, + self.when.body_suffix_not_base64, + ), + body_matches: from_pattern_vec(self.when.body_matches), + + // JSON Body-related fields + json_body: self.when.json_body, + json_body_not: self.when.json_body_not, + json_body_includes: self.when.json_body_includes, + json_body_excludes: self.when.json_body_excludes, + + // x-www-form-urlencoded fields + form_urlencoded_tuple: from_name_value_string_pair_vec( + self.when.form_urlencoded_tuple, + ), + form_urlencoded_tuple_not: from_name_value_string_pair_vec( + self.when.form_urlencoded_tuple_not, + ), + form_urlencoded_tuple_exists: self.when.form_urlencoded_key_exists, + form_urlencoded_tuple_missing: self.when.form_urlencoded_key_missing, + form_urlencoded_tuple_includes: from_name_value_string_pair_vec( + self.when.form_urlencoded_contains, + ), + form_urlencoded_tuple_excludes: from_name_value_string_pair_vec( + self.when.form_urlencoded_excludes, + ), + form_urlencoded_tuple_prefix: from_name_value_string_pair_vec( + self.when.form_urlencoded_prefix, + ), + form_urlencoded_tuple_suffix: from_name_value_string_pair_vec( + self.when.form_urlencoded_suffix, + ), + form_urlencoded_tuple_prefix_not: from_name_value_string_pair_vec( + self.when.form_urlencoded_prefix_not, + ), + form_urlencoded_tuple_suffix_not: from_name_value_string_pair_vec( + self.when.form_urlencoded_suffix_not, + ), + form_urlencoded_tuple_matches: from_name_value_pattern_pair_vec( + self.when.form_urlencoded_matches, + ), + + form_urlencoded_tuple_count: from_key_value_pattern_count_triple_vec( + self.when.form_urlencoded_count, + ), + + // Boolean dynamic checks + is_true: None, + is_false: None, + }, + response: MockServerHttpResponse { + status: self.then.status, + headers: from_name_value_string_pair_vec(self.then.header), + body: from_string_to_bytes_choose(self.then.body, self.then.body_base64), + delay: self.then.delay, + }, + }) + } +} + +fn to_method_vec(vec: Option>) -> Option> { + vec.map(|vec| vec.iter().map(|val| Method::from(val.as_str())).collect()) +} + +fn from_method_vec(value: Option>) -> Option> { + value.map(|vec| vec.iter().map(|m| m.to_string()).collect()) +} + +fn to_pattern_vec(vec: Option>) -> Option> { + vec.map(|vec| { + vec.iter() + .map(|val| HttpMockRegex(regex::Regex::from_str(val).expect("cannot parse regex"))) + .collect() + }) +} + +fn from_pattern_vec(patterns: Option>) -> Option> { + patterns.map(|vec| vec.iter().cloned().collect()) +} + +fn from_name_value_string_pair_vec( + kvp: Option>, +) -> Option> { + kvp.map(|vec| vec.into_iter().map(|nvp| (nvp.name, nvp.value)).collect()) +} + +fn from_name_value_pattern_pair_vec( + kvp: Option>, +) -> Option> { + kvp.map(|vec| { + vec.into_iter() + .map(|pair| (pair.name, pair.value)) + .collect() + }) +} + +fn from_string_pair_vec(vec: Option>) -> Option> { + vec.map(|vec| { + vec.into_iter() + .map(|(name, value)| NameValueStringPair { name, value }) + .collect() + }) +} + +fn from_key_pattern_count_pair_vec( + input: Option>, +) -> Option> { + input.map(|vec| vec.into_iter().map(|pair| (pair.key, pair.count)).collect()) +} + +fn from_value_pattern_count_pair_vec( + input: Option>, +) -> Option> { + input.map(|vec| { + vec.into_iter() + .map(|pair| (pair.value, pair.count)) + .collect() + }) +} + +fn from_key_value_pattern_count_triple_vec( + input: Option>, +) -> Option> { + input.map(|vec| { + vec.into_iter() + .map(|triple| (triple.name, triple.value, triple.count)) + .collect() + }) +} + +fn to_name_value_string_pair_vec( + vec: Option>, +) -> Option> { + vec.map(|vec| { + vec.into_iter() + .map(|(name, value)| NameValueStringPair { name, value }) + .collect() + }) +} + +fn to_name_value_pattern_pair_vec( + vec: Option>, +) -> Option> { + vec.map(|vec| { + vec.into_iter() + .map(|(name, value)| NameValuePatternPair { name, value }) + .collect() + }) +} + +fn to_key_pattern_count_pair_vec( + vec: Option>, +) -> Option> { + vec.map(|vec| { + vec.into_iter() + .map(|(key, count)| KeyPatternCountPair { key, count }) + .collect() + }) +} + +fn to_value_pattern_count_pair_vec( + vec: Option>, +) -> Option> { + vec.map(|vec| { + vec.into_iter() + .map(|(value, count)| ValuePatternCountPair { value, count }) + .collect() + }) +} + +fn to_key_value_pattern_count_triple_vec( + vec: Option>, +) -> Option> { + vec.map(|vec| { + vec.into_iter() + .map(|(name, value, count)| KeyValuePatternCountTriple { name, value, count }) + .collect() + }) +} + +fn from_bytes_to_string(data: Option) -> (Option, Option) { + let mut text_representation = None; + let mut base64_representation = None; + + if let Some(bytes_container) = data { + if let Ok(text_str) = std::str::from_utf8(&bytes_container.to_bytes()) { + text_representation = Some(text_str.to_string()); + } else { + base64_representation = Some(base64::encode(&bytes_container.to_bytes())); + } + } + + (text_representation, base64_representation) +} + +fn bytes_to_string_vec( + data: Option>, +) -> (Option>, Option>) { + let mut text_representations = Vec::new(); + let mut base64_representations = Vec::new(); + + if let Some(bytes_vec) = data { + for bytes_container in bytes_vec { + let bytes = bytes_container.to_bytes(); + if let Ok(text) = std::str::from_utf8(&bytes) { + text_representations.push(text.to_owned()); + } else { + base64_representations.push(base64::encode(&bytes)); + } + } + } + + let text_opt_vec = if !text_representations.is_empty() { + Some(text_representations) + } else { + None + }; + + let base64_opt_vec = if !base64_representations.is_empty() { + Some(base64_representations) + } else { + None + }; + + (text_opt_vec, base64_opt_vec) +} + +fn to_bytes_vec( + option_string: Option>, + option_base64: Option>, +) -> Option> { + let mut result = Vec::new(); + + if let Some(strings) = option_string { + result.extend( + strings + .into_iter() + .map(|s| HttpMockBytes::from(Bytes::from(s))), ); } + + if let Some(base64_strings) = option_base64 { + result.extend(base64_strings.into_iter().filter_map(|s| { + base64::decode(&s) + .ok() + .map(|decoded_bytes| HttpMockBytes::from(Bytes::from(decoded_bytes))) + })); + } + + if result.is_empty() { + None + } else { + Some(result) + } +} + +fn to_bytes(option_string: Option, option_base64: Option) -> Option { + if option_string.is_some() { + return option_string; + } + + return option_base64; +} + +fn from_string_to_bytes_choose( + option_string: Option, + option_base64: Option, +) -> Option { + let request_body = match (option_string, option_base64) { + (Some(body), None) => Some(body.into_bytes()), + (None, Some(base64_body)) => base64::decode(base64_body).ok(), + _ => None, // Handle unexpected combinations or both None + }; + + return request_body.map(|s| HttpMockBytes::from(Bytes::from(s))); +} + +impl TryFrom<&MockDefinition> for StaticMockDefinition { + type Error = Error; + + fn try_from(value: &MockDefinition) -> Result { + let value = value.clone(); + + let (response_body, response_body_base64) = from_bytes_to_string(value.response.body); + + let (request_body, request_body_base64) = from_bytes_to_string(value.request.body); + let (request_body_not, request_body_not_base64) = + bytes_to_string_vec(value.request.body_not); + let (request_body_includes, request_body_includes_base64) = + bytes_to_string_vec(value.request.body_includes); + let (request_body_excludes, request_body_excludes_base64) = + bytes_to_string_vec(value.request.body_excludes); + let (request_body_prefix, request_body_prefix_base64) = + bytes_to_string_vec(value.request.body_prefix); + let (request_body_suffix, request_body_suffix_base64) = + bytes_to_string_vec(value.request.body_suffix); + let (request_body_prefix_not, request_body_prefix_not_base64) = + bytes_to_string_vec(value.request.body_prefix_not); + let (request_body_suffix_not, request_body_suffix_not_base64) = + bytes_to_string_vec(value.request.body_suffix_not); + + let mut method = None; + if let Some(method_str) = value.request.method { + method = Some( + Method::from_str(&method_str) + .map_err(|err| StaticMockConversionError(err.to_string()))?, + ); + } + + Ok(StaticMockDefinition { + when: StaticRequestRequirements { + // Scheme-related fields + scheme: value.request.scheme, + scheme_not: value.request.scheme_not, + + // Method-related fields + method, + method_not: to_method_vec(value.request.method_not), + // Host-related fields + host: value.request.host, + host_not: value.request.host_not, + host_contains: value.request.host_contains, + host_excludes: value.request.host_excludes, + host_prefix: value.request.host_prefix, + host_suffix: value.request.host_suffix, + host_prefix_not: value.request.host_prefix_not, + host_suffix_not: value.request.host_suffix_not, + host_matches: value.request.host_matches, + + // Port-related fields + port: value.request.port, + port_not: value.request.port_not, + + // Path-related fields + path: value.request.path, + path_not: value.request.path_not, + path_contains: value.request.path_includes, + path_excludes: value.request.path_excludes, + path_prefix: value.request.path_prefix, + path_suffix: value.request.path_suffix, + path_prefix_not: value.request.path_prefix_not, + path_suffix_not: value.request.path_suffix_not, + path_matches: from_pattern_vec(value.request.path_matches), + + // Header-related fields + header: from_string_pair_vec(value.request.header), + header_not: from_string_pair_vec(value.request.header_not), + header_exists: value.request.header_exists, + header_missing: value.request.header_missing, + header_contains: to_name_value_string_pair_vec(value.request.header_includes), + header_excludes: to_name_value_string_pair_vec(value.request.header_excludes), + header_prefix: to_name_value_string_pair_vec(value.request.header_prefix), + header_suffix: to_name_value_string_pair_vec(value.request.header_suffix), + header_prefix_not: to_name_value_string_pair_vec(value.request.header_prefix_not), + header_suffix_not: to_name_value_string_pair_vec(value.request.header_suffix_not), + header_matches: to_name_value_pattern_pair_vec(value.request.header_matches), + header_count: to_key_value_pattern_count_triple_vec(value.request.header_count), + + // Cookie-related fields + cookie: from_string_pair_vec(value.request.cookie), + cookie_not: from_string_pair_vec(value.request.cookie_not), + cookie_exists: value.request.cookie_exists, + cookie_missing: value.request.cookie_missing, + cookie_contains: to_name_value_string_pair_vec(value.request.cookie_includes), + cookie_excludes: to_name_value_string_pair_vec(value.request.cookie_excludes), + cookie_prefix: to_name_value_string_pair_vec(value.request.cookie_prefix), + cookie_suffix: to_name_value_string_pair_vec(value.request.cookie_suffix), + cookie_prefix_not: to_name_value_string_pair_vec(value.request.cookie_prefix_not), + cookie_suffix_not: to_name_value_string_pair_vec(value.request.cookie_suffix_not), + cookie_matches: to_name_value_pattern_pair_vec(value.request.cookie_matches), + + cookie_count: to_key_value_pattern_count_triple_vec(value.request.cookie_count), + + // Query Parameter-related fields + query_param: from_string_pair_vec(value.request.query_param), + query_param_not: from_string_pair_vec(value.request.query_param_not), + query_param_exists: value.request.query_param_exists, + query_param_missing: value.request.query_param_missing, + query_param_contains: to_name_value_string_pair_vec( + value.request.query_param_includes, + ), + query_param_excludes: to_name_value_string_pair_vec( + value.request.query_param_excludes, + ), + query_param_prefix: to_name_value_string_pair_vec(value.request.query_param_prefix), + query_param_suffix: to_name_value_string_pair_vec(value.request.query_param_suffix), + query_param_prefix_not: to_name_value_string_pair_vec( + value.request.query_param_prefix_not, + ), + query_param_suffix_not: to_name_value_string_pair_vec( + value.request.query_param_suffix_not, + ), + query_param_matches: to_name_value_pattern_pair_vec( + value.request.query_param_matches, + ), + query_param_count: to_key_value_pattern_count_triple_vec( + value.request.query_param_count, + ), + + // Body-related fields + body: request_body, + body_base64: request_body_base64, + body_not: request_body_not, + body_not_base64: request_body_not_base64, + body_contains: request_body_includes, + body_contains_base64: request_body_includes_base64, + body_excludes: request_body_excludes, + body_excludes_base64: request_body_excludes_base64, + body_prefix: request_body_prefix, + body_prefix_base64: request_body_prefix_base64, + body_suffix: request_body_suffix, + body_suffix_base64: request_body_suffix_base64, + body_prefix_not: request_body_prefix_not, + body_prefix_not_base64: request_body_prefix_not_base64, + body_suffix_not: request_body_suffix_not, + body_suffix_not_base64: request_body_suffix_not_base64, + body_matches: from_pattern_vec(value.request.body_matches), + + // JSON Body-related fields + json_body: value.request.json_body, + json_body_not: value.request.json_body_not, + json_body_includes: value.request.json_body_includes, + json_body_excludes: value.request.json_body_excludes, + + // Form URL-encoded fields + form_urlencoded_tuple: from_string_pair_vec(value.request.form_urlencoded_tuple), + form_urlencoded_tuple_not: from_string_pair_vec( + value.request.form_urlencoded_tuple_not, + ), + form_urlencoded_key_exists: value.request.form_urlencoded_tuple_exists, + form_urlencoded_key_missing: value.request.form_urlencoded_tuple_missing, + form_urlencoded_contains: to_name_value_string_pair_vec( + value.request.form_urlencoded_tuple_includes, + ), + form_urlencoded_excludes: to_name_value_string_pair_vec( + value.request.form_urlencoded_tuple_excludes, + ), + form_urlencoded_prefix: to_name_value_string_pair_vec( + value.request.form_urlencoded_tuple_prefix, + ), + form_urlencoded_suffix: to_name_value_string_pair_vec( + value.request.form_urlencoded_tuple_suffix, + ), + form_urlencoded_prefix_not: to_name_value_string_pair_vec( + value.request.form_urlencoded_tuple_prefix_not, + ), + form_urlencoded_suffix_not: to_name_value_string_pair_vec( + value.request.form_urlencoded_tuple_suffix_not, + ), + form_urlencoded_matches: to_name_value_pattern_pair_vec( + value.request.form_urlencoded_tuple_matches, + ), + + form_urlencoded_count: to_key_value_pattern_count_triple_vec( + value.request.form_urlencoded_tuple_count, + ), + }, + then: StaticHTTPResponse { + status: value.response.status, + header: from_string_pair_vec(value.response.headers), + body: response_body, + body_base64: response_body_base64, + // Reason for the cast to u64: The Duration::as_millis method returns the total + // number of milliseconds contained within the Duration as a u128. This is + // because Duration::as_millis needs to handle larger values that + // can result from multiplying the seconds (stored internally as a u64) + // by 1000 and adding the milliseconds (also a u64), potentially + // exceeding the u64 limit. + delay: value.response.delay, + }, + }) + } +} + +/// Represents an HTTP method. +#[derive(Serialize, Deserialize, Debug)] +pub enum Method { + GET, + HEAD, + POST, + PUT, + DELETE, + CONNECT, + OPTIONS, + TRACE, + PATCH, +} + +impl PartialEq for http::method::Method { + fn eq(&self, other: &Method) -> bool { + self.to_string().to_uppercase() == other.to_string().to_uppercase() + } +} + +impl FromStr for Method { + type Err = String; + + fn from_str(input: &str) -> Result { + match input.to_uppercase().as_str() { + "GET" => Ok(Method::GET), + "HEAD" => Ok(Method::HEAD), + "POST" => Ok(Method::POST), + "PUT" => Ok(Method::PUT), + "DELETE" => Ok(Method::DELETE), + "CONNECT" => Ok(Method::CONNECT), + "OPTIONS" => Ok(Method::OPTIONS), + "TRACE" => Ok(Method::TRACE), + "PATCH" => Ok(Method::PATCH), + _ => Err(format!("Invalid HTTP method {}", input)), + } + } +} + +impl From<&str> for Method { + fn from(value: &str) -> Self { + value + .parse() + .expect(&format!("Cannot parse HTTP method from string {:?}", value)) + } +} + +impl std::fmt::Display for Method { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + std::fmt::Debug::fmt(self, f) + } } diff --git a/src/common/http.rs b/src/common/http.rs new file mode 100644 index 00000000..6501af92 --- /dev/null +++ b/src/common/http.rs @@ -0,0 +1,91 @@ +use async_trait::async_trait; +use bytes::Bytes; +use http::{Request, Response}; +use http_body_util::{BodyExt, Full}; +#[cfg(any(feature = "remote-https", feature = "https"))] +use hyper_rustls::HttpsConnector; +use hyper_util::{ + client::legacy::{connect::HttpConnector, Client}, + rt::TokioExecutor, +}; +use std::{convert::TryInto, sync::Arc}; +use thiserror::Error; +use tokio::runtime::Runtime; + +#[derive(Error, Debug)] +pub enum Error { + #[error("cannot send request: {0}")] + HyperError(#[from] hyper::Error), + #[error("cannot send request: {0}")] + HyperUtilError(#[from] hyper_util::client::legacy::Error), + #[error("runtime error: {0}")] + RuntimeError(#[from] tokio::task::JoinError), + #[error("unknown error")] + Unknown, +} + +#[async_trait] +pub trait HttpClient { + async fn send(&self, req: Request) -> Result, Error>; +} + +pub struct HttpMockHttpClient { + runtime: Option>, + #[cfg(any(feature = "remote-https", feature = "https"))] + client: Arc, Full>>, + #[cfg(not(any(feature = "remote-https", feature = "https")))] + client: Arc>>, +} + +impl<'a> HttpMockHttpClient { + #[cfg(any(feature = "remote-https", feature = "https"))] + pub fn new(runtime: Option>) -> Self { + // see https://github.com/rustls/rustls/issues/1938 + if rustls::crypto::CryptoProvider::get_default().is_none() { + rustls::crypto::ring::default_provider() + .install_default() + .expect("cannot install rustls crypto provider"); + } + + let https_connector = hyper_rustls::HttpsConnectorBuilder::new() + .with_native_roots() + .expect("cannot set up using native root certificates") + .https_or_http() + .enable_all_versions() + .build(); + + Self { + runtime, + client: Arc::new(Client::builder(TokioExecutor::new()).build(https_connector)), + } + } + + #[cfg(not(any(feature = "remote-https", feature = "https")))] + pub fn new(runtime: Option>) -> Self { + Self { + runtime, + client: Arc::new(Client::builder(TokioExecutor::new()).build(HttpConnector::new())), + } + } +} + +#[async_trait] +impl HttpClient for HttpMockHttpClient { + async fn send(&self, req: Request) -> Result, Error> { + let (req_parts, req_body) = req.into_parts(); + let hyper_req = Request::from_parts(req_parts, Full::new(req_body)); + + let res = if let Some(rt) = self.runtime.clone() { + let client = self.client.clone(); + rt.spawn(async move { client.request(hyper_req).await }) + .await?? + } else { + self.client.request(hyper_req).await? + }; + + let (res_parts, res_body) = res.into_parts(); + let body = res_body.collect().await?.to_bytes(); + + return Ok(Response::from_parts(res_parts, body)); + } +} diff --git a/src/common/mod.rs b/src/common/mod.rs index 5b853857..7870dd87 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,2 +1,6 @@ -pub mod data; +pub(crate) mod data; +pub(crate) mod runtime; pub mod util; + +#[cfg(any(feature = "remote", feature = "proxy"))] +pub mod http; diff --git a/src/common/runtime.rs b/src/common/runtime.rs new file mode 100644 index 00000000..7254561c --- /dev/null +++ b/src/common/runtime.rs @@ -0,0 +1,35 @@ +use std::{future::Future, time::Duration}; +use tokio::{runtime::Runtime, task::LocalSet}; + +pub(crate) async fn sleep(duration: Duration) { + tokio::time::sleep(duration).await +} + +pub(crate) fn block_on_current_thread(f: F) -> O +where + F: Future, +{ + let mut runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Cannot build local tokio runtime"); + + LocalSet::new().block_on(&mut runtime, f) +} + +pub(crate) fn new(worker_threads: usize, blocking_threads: usize) -> std::io::Result { + assert!( + worker_threads > 0, + "Parameter worker_threads must be larger than 0" + ); + assert!( + blocking_threads > 0, + "Parameter blocking_threads must be larger than 0" + ); + + return tokio::runtime::Builder::new_multi_thread() + .worker_threads(worker_threads) + .max_blocking_threads(blocking_threads) // This is a maximum + .enable_all() + .build(); +} diff --git a/src/common/util.rs b/src/common/util.rs index ebeede94..da6cacb6 100644 --- a/src/common/util.rs +++ b/src/common/util.rs @@ -1,17 +1,23 @@ -use std::fs::File; -use std::io::Read; -use std::path::{Path, PathBuf}; -use std::sync::Arc; +use async_std::fs::{create_dir_all as create_dir_all_async, File as AsyncFile}; use std::{ + borrow::Cow, env, + fs::File, future::Future, + io::Read, + path::{Path, PathBuf}, + sync::Arc, task::{Context, Poll}, }; +use async_std::io::{ReadExt, WriteExt}; +use bytes::Bytes; /// Extension trait for efficiently blocking on a future. use crossbeam_utils::sync::{Parker, Unparker}; +use futures_timer::Delay; use futures_util::{pin_mut, task::ArcWake}; -use std::cell::Cell; +use serde::{Deserialize, Serialize, Serializer}; +use std::{cell::Cell, time::Duration}; // =============================================================================================== // Misc @@ -32,9 +38,11 @@ where Fut: Future>, { let mut result = (f)().await; - for _ in 1..=retries { + for i in 1..=retries { if result.is_ok() { return result; + } else { + Delay::new(Duration::from_secs(1 * i as u64)).await; } result = (f)().await; } @@ -89,14 +97,14 @@ impl Join for F { // =============================================================================================== // Files // =============================================================================================== -pub(crate) fn get_test_resource_file_path(relative_resource_path: &str) -> Result { +pub fn get_test_resource_file_path(relative_resource_path: &str) -> Result { match env::var("CARGO_MANIFEST_DIR") { Ok(manifest_path) => Ok(Path::new(&manifest_path).join(relative_resource_path)), Err(e) => Err(e.to_string()), } } -pub(crate) fn read_file>(absolute_resource_path: P) -> Result, String> { +pub fn read_file>(absolute_resource_path: P) -> Result, String> { let mut f = match File::open(&absolute_resource_path) { Ok(mut opened_file) => opened_file, Err(e) => return Err(e.to_string()), @@ -118,6 +126,48 @@ pub(crate) fn read_file>(absolute_resource_path: P) -> Result>( + resource_path: P, + content: &Bytes, + create_dir: bool, +) -> Result> { + let mut path = resource_path.as_ref().to_path_buf(); + + if path.is_relative() { + let current_dir = env::current_dir()?; + path = current_dir.join(path); + } + + if create_dir { + if let Some(parent) = path.parent() { + create_dir_all_async(parent).await?; + } + } + + let mut file = AsyncFile::create(&path).await?; + file.write_all(content).await?; + file.flush().await?; + + Ok(path) +} + +pub async fn read_file_async>( + path: IntoPathBuf, +) -> std::io::Result> { + let mut file = async_std::fs::File::open(path.into()).await?; + let mut content = Vec::new(); + file.read_to_end(&mut content).await?; + Ok(content) +} + +// Checks if the executing thread is running in a Tokio runtime. +fn is_tokio_runtime_running() -> bool { + match tokio::runtime::Handle::try_current() { + Ok(_) => true, + Err(_) => false, + } +} + #[cfg(test)] mod test { use crate::common::util::{with_retry, Join}; @@ -133,3 +183,161 @@ mod test { assert_eq!(result.err().unwrap(), "test error") } } + +/// A wrapper around `bytes::Bytes` providing utility methods for common operations. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct HttpMockBytes(pub Bytes); + +impl HttpMockBytes { + /// Converts the bytes to a `Vec`. + /// + /// # Returns + /// A `Vec` containing the bytes. + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } + + /// Cheaply clones the bytes into a new `Bytes` instance. + /// See + /// + /// # Returns + /// A `Bytes` instance containing the same data. + pub fn to_bytes(&self) -> Bytes { + self.0.clone() + } + + /// Checks if the byte slice is empty. + /// + /// # Returns + /// `true` if the byte slice is empty, otherwise `false`. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Checks if the byte slice is blank (empty or only contains ASCII whitespace). + /// + /// # Returns + /// `true` if the byte slice is blank, otherwise `false`. + pub fn is_blank(&self) -> bool { + self.is_empty() || self.0.iter().all(|&b| b.is_ascii_whitespace()) + } + + /// Checks if the byte slice contains the specified substring. + /// + /// # Arguments + /// * `substring` - The substring to search for. + /// + /// # Returns + /// `true` if the substring is found, otherwise `false`. + pub fn contains_str(&self, substring: &str) -> bool { + if substring.is_empty() { + return true; + } + + self.0 + .as_ref() + .windows(substring.as_bytes().len()) + .any(|window| window == substring.as_bytes()) + } + + /// Checks if the byte slice contains the specified byte slice. + /// + /// # Arguments + /// * `slice` - The byte slice to search for. + /// + /// # Returns + /// `true` if the byte slice is found, otherwise `false`. + pub fn contains_slice(&self, slice: &[u8]) -> bool { + self.0 + .as_ref() + .windows(slice.len()) + .any(|window| window == slice) + } + + /// Checks if the byte slice contains the specified `Vec`. + /// + /// # Arguments + /// * `vec` - The vector to search for. + /// + /// # Returns + /// `true` if the vector is found, otherwise `false`. + pub fn contains_vec(&self, vec: &Vec) -> bool { + self.0 + .as_ref() + .windows(vec.len()) + .any(|window| window == vec.as_slice()) + } + + /// Converts the bytes to a UTF-8 string, potentially lossy. + /// Tries to parse input as a UTF-8 string first to avoid copying and creating an owned instance. + /// If the bytes are not valid UTF-8, it creates a lossy string by replacing invalid characters + /// with the Unicode replacement character. + /// + /// # Returns + /// A `Cow` which is either borrowed if the bytes are valid UTF-8 or owned if conversion was required. + pub fn to_maybe_lossy_str(&self) -> Cow { + return match std::str::from_utf8(&self.0) { + Ok(valid_str) => Cow::Borrowed(valid_str), + Err(_) => Cow::Owned(String::from_utf8_lossy(&self.0).to_string()), + }; + } +} + +impl Into for HttpMockBytes { + fn into(self) -> Bytes { + self.0.clone() + } +} + +impl From for HttpMockBytes { + fn from(value: Bytes) -> Self { + HttpMockBytes(value) + } +} + +impl PartialEq for HttpMockBytes { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl AsRef<[u8]> for HttpMockBytes { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl std::fmt::Display for HttpMockBytes { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match std::str::from_utf8(&self.0) { + Ok(result) => write!(f, "{}", result), + Err(_) => write!(f, "{}", base64::encode(&self.0)), + } + } +} + +pub fn title_case(s: &str) -> String { + let mut result = String::new(); + let mut capitalize_next = true; + + for c in s.chars() { + if c.is_whitespace() { + capitalize_next = true; + result.push(c); + } else if capitalize_next { + result.push(c.to_uppercase().next().unwrap()); + capitalize_next = false; + } else { + result.push(c.to_lowercase().next().unwrap()); + } + } + + result +} + +pub fn is_none_or_empty(option: &Option>) -> bool { + match option { + None => true, + Some(vec) => vec.is_empty(), + } +} diff --git a/src/lib.rs b/src/lib.rs index c239b67a..c029fca3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,27 +3,31 @@ //! //! # Features //! * Simple, expressive, fluent API. -//! * Many built-in helpers for easy request matching. +//! * Many built-in helpers for easy request matching ([Regex](https://docs.rs/regex/), JSON, [serde](https://crates.io/crates/serde), cookies, and more). +//! * Record and Playback +//! * Forward and Proxy Mode +//! * HTTPS support +//! * Fault and network delay simulation. +//! * Custom request matchers. +//! * Standalone mode with an accompanying [Docker image](https://hub.docker.com/r/alexliesenfeld/httpmock). +//! * Helpful error messages +//! * [Advanced verification and debugging support](https://alexliesenfeld.github.io/posts/mocking-http--services-in-rust/#creating-mocks) (including diff generation between actual and expected HTTP request values) //! * Parallel test execution. -//! * Extensible request matching. //! * Fully asynchronous core with synchronous and asynchronous APIs. -//! * [Advanced verification and debugging support](https://web.archive.org/web/20201202160613/https://dev.to/alexliesenfeld/rust-http-testing-with-httpmock-2mi0#verification) -//! * [Network delay simulation](https://github.com/alexliesenfeld/httpmock/blob/master/tests/examples/delay_tests.rs). -//! * Support for [Regex](https://docs.rs/regex/) matching, JSON, [serde](https://crates.io/crates/serde), cookies, and more. -//! * Standalone mode with an accompanying [Docker image](https://hub.docker.com/r/alexliesenfeld/httpmock). -//! * Support for [mock specification based on YAML files](https://github.com/alexliesenfeld/httpmock/blob/master/src/lib.rs#L185-L201). +//! * Support for [mock configuration using YAML files](https://github.com/alexliesenfeld/httpmock/tree/master#file-based-mock-specification). //! //! # Getting Started //! Add `httpmock` to `Cargo.toml`: //! //! ```toml //! [dev-dependencies] -//! httpmock = "0.7.0" +//! httpmock = "0.8.0-alpha.1" //! ``` //! //! You can then use `httpmock` as follows: //! ``` //! use httpmock::prelude::*; +//! use reqwest::blocking::get; //! //! // Start a lightweight mock server. //! let server = MockServer::start(); @@ -39,7 +43,7 @@ //! }); //! //! // Send an HTTP request to the mock server. This simulates your code. -//! let response = isahc::get(server.url("/translate?word=hello")).unwrap(); +//! let response = get(&server.url("/translate?word=hello")).unwrap(); //! //! // Ensure the specified mock was called exactly one time (or fail with a detailed error description). //! hello_mock.assert(); @@ -47,179 +51,51 @@ //! assert_eq!(response.status(), 200); //! ``` //! -//! In case the request fails, `httpmock` would show you a detailed error description including a diff between the -//! expected and the actual HTTP request: +//! When the specified expectations do not match the received request, `httpmock` provides a detailed error description, +//! including a diff that shows the differences between the expected and actual HTTP requests. Example: //! -//! ![colored-diff.png](https://raw.githubusercontent.com/alexliesenfeld/httpmock/master/docs/diff.png) +//! ```bash +//! 0 of 1 expected requests matched the mock specification. +//! Here is a comparison with the most similar unmatched request (request number 1): //! -//! # Usage -//! To be able to configure mocks, you first need to start a mock server by calling -//! [MockServer::start](struct.MockServer.html#method.start). -//! This will spin up a lightweight HTTP -//! mock server in the background and wait until the server is ready to accept requests. -//! -//! You can then create a [Mock](struct.Mock.html) object on the server by using the -//! [MockServer::mock](struct.MockServer.html#method.mock) method. This method expects a closure -//! with two parameters, that we will refer to as the `when` and `then` parameter: -//! - The `when` parameter is of type [When](struct.When.html) and holds all request characteristics. -//! The mock server will only respond to HTTP requests that meet all the criteria. Otherwise it -//! will respond with HTTP status code `404` and an error message. -//! - The `then` parameter is of type [Then](struct.Then.html) and holds all values that the mock -//! server will respond with. -//! -//! # Sync / Async -//! The internal implementation of `httpmock` is completely asynchronous. It provides you a -//! synchronous and an asynchronous API though. If you want to schedule awaiting operations manually, then -//! you can use the `async` variants that exist for every potentially blocking operation. For -//! example, there is [MockServer::start_async](struct.MockServer.html#method.start_async) as an -//! asynchronous counterpart to [MockServer::start](struct.MockServer.html#method.start). You can -//! find similar methods throughout the entire library. -//! -//! # Parallelism -//! To balance execution speed and resource consumption, [MockServer](struct.MockServer.html)s -//! are kept in a server pool internally. This allows to run tests in parallel without overwhelming -//! the executing machine by creating too many HTTP servers. A test will be blocked if it tries to -//! use a [MockServer](struct.MockServer.html) (e.g. by calling -//! [MockServer::start](struct.MockServer.html#method.start)) while the server pool is empty -//! (i.e. all servers are occupied by other tests). -//! -//! [MockServer](struct.MockServer.html)s are never recreated but recycled/reset. -//! The pool is filled on demand up to a maximum number of 25 servers. -//! You can override this number by using the environment variable `HTTPMOCK_MAX_SERVERS`. -//! -//! # Debugging -//! `httpmock` logs against the [log](https://crates.io/crates/log) crate. This allows you to -//! see detailed log output that contains information about `httpmock`s behaviour. -//! You can use this log output to investigate -//! issues, such as to find out why a request does not match a mock definition. -//! -//! The most useful log level is `debug`, but you can also go down to `trace` to see even more -//! information. -//! -//! **Attention**: To be able to see the log output, you need to add the `--nocapture` argument -//! when starting test execution! -//! -//! *Hint*: If you use the `env_logger` backend, you need to set the `RUST_LOG` environment variable to -//! `httpmock=debug`. -//! -//! # API Alternatives -//! This library provides two functionally interchangeable DSL APIs that allow you to create -//! mocks on the server. You can choose the one you like best or use both side-by-side. For a -//! consistent look, it is recommended to stick to one of them, though. -//! -//! ## When/Then API -//! This is the default API of `httpmock`. It is concise and easy to read. The main goal -//! is to reduce overhead emposed by this library to a bare minimum. It works well with -//! formatting tools, such as [rustfmt](https://crates.io/crates/rustfmt) (i.e. `cargo fmt`), -//! and can fully benefit from IDE support. -//! -//! ### Example -//! ``` -//! let server = httpmock::MockServer::start(); +//! ------------------------------------------------------------ +//! 1 : Query Parameter Mismatch +//! ------------------------------------------------------------ +//! Expected: +//! key [equals] word +//! value [equals] hello-rustaceans //! -//! let greeting_mock = server.mock(|when, then| { -//! when.path("/hi"); -//! then.status(200); -//! }); +//! Received (most similar query parameter): +//! word=hello //! -//! let response = isahc::get(server.url("/hi")).unwrap(); +//! All received query parameter values: +//! 1. word=hello //! -//! greeting_mock.assert(); +//! Matcher: query_param +//! Docs: https://docs.rs/httpmock/0.8.0/httpmock/struct.When.html#method.query_param //! ``` -//! Note that `when` and `then` are variables. This allows you to rename them to something you -//! like better (such as `expect`/`respond_with`). -//! -//! Relevant elements for this API are [MockServer::mock](struct.MockServer.html#method.mock), [When](struct.When.html) and [Then](struct.Then.html). -//! -//! # Examples -//! You can find examples in the test directory in this crates Git repository: -//! [this crates test directory](https://github.com/alexliesenfeld/httpmock/blob/master/tests ). -//! -//! # Standalone Mode -//! You can use `httpmock` to run a standalone mock server that runs in a separate process. -//! This allows it to be available to multiple applications, not only inside your unit and integration -//! tests. This is useful if you want to use `httpmock` in system (or even end-to-end) tests, that -//! require mocked services. With this feature, `httpmock` is a universal HTTP mocking tool that is -//! useful in all stages of the development lifecycle. -//! -//! ## Using a Standalone Mock Server -//! Although you can build the mock server in standalone mode yourself, it is easiest to use the -//! accompanying [Docker image](https://hub.docker.com/r/alexliesenfeld/httpmock). -//! -//! To be able to use the standalone server from within your tests, you need to change how an -//! instance of the [MockServer](struct.MockServer.html) instance is created. -//! Instead of using [MockServer::start](struct.MockServer.html#method.start), -//! you need to connect to a remote server by using one of the `connect` methods (such as -//! [MockServer::connect](struct.MockServer.html#method.connect) or -//! [MockServer::connect_from_env](struct.MockServer.html#method.connect_from_env)). **Note**: -//! These are only available with the `remote` feature **enabled**. //! -//! ``` -//! use httpmock::prelude::*; -//! use isahc::get; +//! # Usage //! -//! #[test] -//! fn simple_test() { -//! // Arrange -//! let server = MockServer::connect("some-host:5000"); +//! See the [official documentation](http://alexliesenfeld.github.io/httpmock) a for detailed +//! API documentation. //! -//! let hello_mock = server.mock(|when, then|{ -//! when.path("/hello/standalone"); -//! then.status(200); -//! }); +//! ## Examples //! -//! // Act -//! let response = get(server.url("/hello/standalone")).unwrap(); +//! You can find examples in the +//! [`httpmock` test directory](https://github.com/alexliesenfeld/httpmock/blob/master/tests/). +//! The [official documentation](http://alexliesenfeld.github.io/httpmock) and [reference docs](https://docs.rs/httpmock/) +//! also contain _**a lot**_ of examples. //! -//! // Assert -//! hello_mock.assert(); -//! assert_eq!(response.status(), 200); -//! } -//! ``` +//! ## License //! -//! ## Standalone Parallelism -//! To prevent interference with other tests, test functions are forced to use the standalone -//! mock server sequentially. -//! This means that test functions may be blocked when connecting to the remote server until -//! it becomes free again. -//! This is in contrast to tests that use a local mock server. -//! -//! ## Limitations of the Standalone Mode -//! At this time, it is not possible to use custom request matchers in combination with standalone -//! mock servers (see [When::matches](struct.When.html#method.matches) or -//! [Mock::expect_match](struct.Mock.html#method.expect_match)). -//! -//! ## Standalone Mode with YAML Mock Definition Files -//! The standalone server can also be used to read mock definitions from YAML files on startup once -//! and serve the mocked endpoints until the server is shut down again. These `static` mocks -//! cannot be deleted at runtime (even by Rust-based tests that use the mock server) and exist -//! for the entire uptime of the mock server. -//! -//! The definition files follow the standard `httpmock` API that you would also use in regular -//! Rust tests. Please find an example mock definition file in the `httpmock` Github repository -//! [here in this crates test directory](https://github.com/alexliesenfeld/httpmock/blob/master/tests/resources/static_yaml_mock.yaml). -//! -//! You can start the mock server with static mock support as follows: -//! * If you use the [Docker image from this creates repository](https://github.com/alexliesenfeld/httpmock/blob/master/Dockerfile) -//! or from [Dockerhub](https://hub.docker.com/r/alexliesenfeld/httpmock), you just need to mount a -//! volume with all your mock specification files to the `/mocks` directory within the container. -//! * If you build `httpmock` from source and use the binary, then you can pass the path to -//! the directory containing all your mock specification files using the `--static-mock-dir` -//! parameter. Example: `httpmock --expose --static-mock-dir=/mocks`. -//! -//! # License -//! `httpmock` is free software: you can redistribute it and/or modify it under the terms -//! of the MIT Public License. -//! -//! This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -//! without even the implied -//! warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MIT Public -//! License for more details. -#[macro_use] +//! `httpmock` is free software: you can redistribute it and/or modify it under the terms of the MIT Public License. +//! +//! This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +//! warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MIT Public License for more details. extern crate lazy_static; -use std::borrow::BorrowMut; -use std::net::ToSocketAddrs; +use std::{borrow::BorrowMut, net::ToSocketAddrs}; use std::str::FromStr; @@ -231,14 +107,19 @@ use common::util::Join; pub use api::{Method, Mock, MockExt, MockServer, Regex, Then, When}; mod api; -mod common; -mod server; -pub mod standalone; +pub mod common; +pub mod server; + +#[cfg(feature = "record")] +pub use common::data::RecordingRuleConfig; + +#[cfg(feature = "proxy")] +pub use common::data::{ForwardingRuleConfig, ProxyRuleConfig}; pub mod prelude { #[doc(no_inline)] pub use crate::{ - api::MockServer, common::data::HttpMockRequest, Method::DELETE, Method::GET, - Method::OPTIONS, Method::POST, Method::PUT, Regex, + api::MockServer, common::data::HttpMockRequest, Method, Method::DELETE, Method::GET, + Method::OPTIONS, Method::PATCH, Method::POST, Method::PUT, Regex, }; } diff --git a/src/main.rs b/src/main.rs index 69fbfbc4..b1d49e0a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ +use std::{env, path::PathBuf}; + use clap::Parser; -use httpmock::standalone::start_standalone_server; -use std::env; -use std::path::PathBuf; + +use httpmock::server::HttpMockServerBuilder; /// Holds command line parameters provided by the user. #[derive(Parser, Debug)] @@ -10,7 +11,7 @@ use std::path::PathBuf; author = "Alexander Liesenfeld " )] struct ExecutionParameters { - #[clap(short, long, env = "HTTPMOCK_PORT", default_value = "5000")] + #[clap(short, long, env = "HTTPMOCK_PORT", default_value = "5050")] pub port: u16, #[clap(short, long, env = "HTTPMOCK_EXPOSE")] pub expose: bool, @@ -48,16 +49,19 @@ async fn main() { log::info!("{:?}", params); - start_standalone_server( - params.port, - params.expose, - params.mock_files_dir, - !params.disable_access_log, - params.request_history_limit, - shutdown_signal(), - ) - .await - .expect("an error occurred during mock server execution"); + let server = HttpMockServerBuilder::new() + .port(params.port) + .expose(params.expose) + .print_access_log(!params.disable_access_log) + .history_limit(params.request_history_limit) + .static_mock_dir_option(params.mock_files_dir) + .build() + .unwrap(); + + server + .start_with_signals(None, shutdown_signal()) + .await + .expect("an error occurred during mock server execution"); } #[cfg(not(target_os = "windows"))] diff --git a/src/server/builder.rs b/src/server/builder.rs new file mode 100644 index 00000000..5c1479e2 --- /dev/null +++ b/src/server/builder.rs @@ -0,0 +1,508 @@ +#[cfg(feature = "proxy")] +use crate::common::http::{HttpClient, HttpMockHttpClient}; +#[cfg(any(feature = "record", feature = "record"))] +use crate::server::persistence::read_static_mock_definitions; +#[cfg(feature = "https")] +use crate::server::server::MockServerHttpsConfig; +#[cfg(feature = "https")] +use crate::server::tls::{CertificateResolverFactory, GeneratingCertificateResolverFactory}; + +use crate::server::{ + handler::HttpMockHandler, + server::{MockServer, MockServerConfig}, + state::{HttpMockStateManager, StateManager}, + HttpMockServer, +}; +use std::{error::Error, path::PathBuf, sync::Arc}; + +const DEFAULT_CA_PRIVATE_KEY: &'static str = include_str!("../../certs/ca.key"); +const DEFAULT_CA_CERTIFICATE: &'static str = include_str!("../../certs/ca.pem"); + +/// The Builder streamlines the configuration process, automatically setting up defaults and +/// handling dependency injection for the mock server. It consolidates configuration parameters, +/// fallback mechanisms, and default settings into a single point of management. +#[cfg(feature = "https")] +pub struct HttpsConfigBuilder { + ca_cert: Option, + ca_key: Option, + ca_cert_path: Option, + ca_key_path: Option, + enable_https: Option, + cert_resolver_factory: Option>, +} + +#[cfg(feature = "https")] +impl HttpsConfigBuilder { + fn new() -> Self { + Self { + ca_cert: None, + ca_key: None, + ca_cert_path: None, + ca_key_path: None, + cert_resolver_factory: None, + enable_https: None, + } + } + + /// Validates the HTTPS configuration to ensure no conflicting settings are present. + fn validate(&self) -> Result<(), Box> { + if self.enable_https.unwrap_or(true) { + let has_ca_cert = self.ca_cert.is_some() || self.ca_key.is_some(); + let has_ca_cert_path = self.ca_cert_path.is_some() || self.ca_key_path.is_some(); + let has_cert_generator = self.cert_resolver_factory.is_some(); + + if has_ca_cert && has_ca_cert_path { + return Err("A CA certificate and a CA certificate path have both been configured. Please choose only one method.".into()); + } + + if (has_ca_cert || has_ca_cert_path) && has_cert_generator { + return Err("Both a CA certificate and a certificate generator were configured. Please use only one of them.".into()); + } + } + + Ok(()) + } + + /// Sets the CA certificate for HTTPS. + /// + /// # Parameters + /// - `ca_cert`: An optional CA certificate as a string in PEM format. + /// + /// # Returns + /// A modified `HttpsConfigBuilder` instance for method chaining. + pub fn ca_cert(mut self, ca_cert: Option) -> Self + where + IntoString: Into, + { + self.ca_cert = ca_cert.map(|b| b.into()); + self + } + + /// Sets the CA private key for HTTPS. + /// + /// # Parameters + /// - `ca_key`: An optional CA private key as a string in PEM format. + /// + /// # Returns + /// A modified `HttpsConfigBuilder` instance for method chaining. + pub fn ca_key(mut self, ca_key: Option) -> Self + where + IntoString: Into, + { + self.ca_key = ca_key.map(|b| b.into()); + self + } + + /// Sets the path to the CA certificate for HTTPS. + /// + /// # Parameters + /// - `ca_cert_path`: An optional path to the CA certificate in PEM format. + /// + /// # Returns + /// A modified `HttpsConfigBuilder` instance for method chaining. + pub fn ca_cert_path(mut self, ca_cert_path: Option) -> Self { + self.ca_cert_path = ca_cert_path; + self + } + + /// Sets the path to the CA private key for HTTPS. + /// + /// # Parameters + /// - `ca_key_path`: An optional path to the CA private key. + /// + /// # Returns + /// A modified `HttpsConfigBuilder` instance for method chaining. + pub fn ca_key_path(mut self, ca_key_path: Option) -> Self { + self.ca_key_path = ca_key_path; + self + } + + /// Sets the certificate resolver factory for generating certificates. + /// + /// # Parameters + /// - `generator`: An optional certificate resolver factory. + /// + /// # Returns + /// A modified `HttpsConfigBuilder` instance for method chaining. + pub(crate) fn cert_resolver( + mut self, + generator: Option>, + ) -> Self { + self.cert_resolver_factory = generator; + self + } + + /// Enables or disables HTTPS. + /// + /// # Parameters + /// - `enable`: An optional boolean to enable or disable HTTPS. + /// + /// # Returns + /// A modified `HttpsConfigBuilder` instance for method chaining. + pub fn enable_https(mut self, enable: Option) -> Self { + self.enable_https = enable; + self + } + + /// Builds the `MockServerHttpsConfig` with the current settings. + /// + /// # Returns + /// A `MockServerHttpsConfig` instance or an error if validation fails. + pub fn build(mut self) -> Result> { + self.validate()?; + + let cert_resolver_factory = match ( + self.cert_resolver_factory, + self.ca_cert_path, + self.ca_key_path, + self.ca_cert, + self.ca_key, + ) { + // If a direct resolver was provided, use it. + (Some(cert_resolver), _, _, _, _) => cert_resolver, + // If paths are provided, read the certificates and create a default resolver + // with these certs. + (_, Some(ca_cert_path), Some(ca_key_path), _, _) => { + let ca_cert = std::fs::read_to_string(ca_cert_path)?; + let ca_key = std::fs::read_to_string(ca_key_path)?; + Arc::new(GeneratingCertificateResolverFactory::new(ca_cert, ca_key)?) + } + // If certificate data is directly provided, use it to create the resolver. + (_, _, _, Some(ca_cert), Some(ca_key)) => Arc::new( + GeneratingCertificateResolverFactory::new(ca_cert.clone(), ca_key.clone())?, + ), + // If no CA certificate information was configured, use the default. + _ => Arc::new(GeneratingCertificateResolverFactory::new( + DEFAULT_CA_CERTIFICATE, + DEFAULT_CA_PRIVATE_KEY, + )?), + }; + + Ok(MockServerHttpsConfig { + cert_resolver_factory, + }) + } +} + +/// The `HttpMockServerBuilder` struct is used to configure the HTTP mock server. +/// It provides methods to set various configuration options such as port, logging, history limit, and HTTPS settings. +pub struct HttpMockServerBuilder { + port: Option, + expose: Option, + print_access_log: Option, + history_limit: Option, + #[cfg(feature = "record")] + static_mock_dir: Option, + #[cfg(feature = "https")] + https_config_builder: HttpsConfigBuilder, + #[cfg(feature = "proxy")] + http_client: Option>, +} + +impl HttpMockServerBuilder { + /// Creates a new instance of `HttpMockServerBuilder` with default settings. + /// + /// # Returns + /// A new `HttpMockServerBuilder` instance. + pub fn new() -> Self { + HttpMockServerBuilder { + print_access_log: None, + port: None, + expose: None, + history_limit: None, + #[cfg(feature = "record")] + static_mock_dir: None, + #[cfg(feature = "proxy")] + http_client: None, + #[cfg(feature = "https")] + https_config_builder: HttpsConfigBuilder::new(), + } + } + + /// Sets the port for the HTTP mock server. + /// + /// # Parameters + /// - `port`: The port number. + /// + /// # Returns + /// A modified `HttpMockServerBuilder` instance for method chaining. + pub fn port(mut self, port: u16) -> Self { + self.port = Some(port); + self + } + + /// Sets the port for the HTTP mock server as an optional value. + /// + /// # Parameters + /// - `port`: An optional port number. + /// + /// # Returns + /// A modified `HttpMockServerBuilder` instance for method chaining. + pub fn port_option(mut self, port: Option) -> Self { + self.port = port; + self + } + + /// Sets whether the server should be exposed to external access. + /// + /// # Parameters + /// - `expose`: A boolean indicating whether to expose the server. + /// + /// # Returns + /// A modified `HttpMockServerBuilder` instance for method chaining. + pub fn expose(mut self, expose: bool) -> Self { + self.expose = Some(expose); + self + } + + /// Sets whether the server should be exposed to external access as an optional value. + /// + /// # Parameters + /// - `expose`: An optional boolean indicating whether to expose the server. + /// + /// # Returns + /// A modified `HttpMockServerBuilder` instance for method chaining. + pub fn expose_option(mut self, expose: Option) -> Self { + self.expose = expose; + self + } + + /// Sets whether to print access logs. + /// + /// # Parameters + /// - `enabled`: A boolean indicating whether to print access logs. + /// + /// # Returns + /// A modified `HttpMockServerBuilder` instance for method chaining. + pub fn print_access_log(mut self, enabled: bool) -> Self { + self.print_access_log = Some(enabled); + self + } + + /// Sets whether to print access logs as an optional value. + /// + /// # Parameters + /// - `enabled`: An optional boolean indicating whether to print access logs. + /// + /// # Returns + /// A modified `HttpMockServerBuilder` instance for method chaining. + pub fn print_access_log_option(mut self, enabled: Option) -> Self { + self.print_access_log = enabled; + self + } + + /// Sets the history limit for the server. + /// + /// # Parameters + /// - `limit`: The maximum number of history entries to keep. + /// + /// # Returns + /// A modified `HttpMockServerBuilder` instance for method chaining. + pub fn history_limit(mut self, limit: usize) -> Self { + self.history_limit = Some(limit); + self + } + + /// Sets the history limit for the server as an optional value. + /// + /// # Parameters + /// - `limit`: An optional maximum number of history entries to keep. + /// + /// # Returns + /// A modified `HttpMockServerBuilder` instance for method chaining. + pub fn history_limit_option(mut self, limit: Option) -> Self { + self.history_limit = limit; + self + } + + /// Sets the directory for static mock files. + /// + /// # Parameters + /// - `path`: The path to the static mock directory. + /// + /// # Returns + /// A modified `HttpMockServerBuilder` instance for method chaining. + #[cfg(feature = "record")] + pub fn static_mock_dir(mut self, path: PathBuf) -> Self { + self.static_mock_dir = Some(path); + self + } + + /// Sets the directory for static mock files as an optional value. + /// + /// # Parameters + /// - `path`: An optional path to the static mock directory. + /// + /// # Returns + /// A modified `HttpMockServerBuilder` instance for method chaining. + #[cfg(feature = "record")] + pub fn static_mock_dir_option(mut self, path: Option) -> Self { + self.static_mock_dir = path; + self + } + + /// Sets the certificate resolver factory for generating certificates. + /// + /// # Parameters + /// - `factory`: A certificate resolver factory. + /// + /// # Returns + /// A modified `HttpMockServerBuilder` instance for method chaining. + #[cfg(feature = "https")] + pub fn server_config_factory( + mut self, + factory: Arc, + ) -> Self { + self.https_config_builder = self.https_config_builder.cert_resolver(Some(factory)); + self + } + + /// Sets the certificate resolver factory as an optional value. + /// + /// # Parameters + /// - `factory`: An optional certificate resolver factory. + /// + /// # Returns + /// A modified `HttpMockServerBuilder` instance for method chaining. + #[cfg(feature = "https")] + pub fn cert_resolver_option( + mut self, + factory: Option>, + ) -> Self { + self.https_config_builder = self.https_config_builder.cert_resolver(factory); + self + } + + /// Sets the CA certificate and private key for HTTPS. + /// + /// # Parameters + /// - `cert`: The CA certificate. + /// - `private_key`: The CA private key. + /// + /// # Returns + /// A modified `HttpMockServerBuilder` instance for method chaining. + #[cfg(feature = "https")] + pub fn https_ca_key_pair>( + mut self, + cert: IntoString, + private_key: IntoString, + ) -> Self { + self.https_config_builder = self.https_config_builder.ca_cert(Some(cert)); + self.https_config_builder = self.https_config_builder.ca_key(Some(private_key)); + self + } + + /// Sets the CA certificate and private key for HTTPS as optional values. + /// + /// # Parameters + /// - `cert`: An optional CA certificate. + /// - `private_key`: An optional CA private key. + /// + /// # Returns + /// A modified `HttpMockServerBuilder` instance for method chaining. + #[cfg(feature = "https")] + pub fn https_ca_key_pair_option>( + mut self, + cert: Option, + private_key: Option, + ) -> Self { + self.https_config_builder = self.https_config_builder.ca_cert(cert); + self.https_config_builder = self.https_config_builder.ca_key(private_key); + self + } + + /// Sets the paths to the CA certificate and private key files for HTTPS. + /// + /// # Parameters + /// - `cert_path`: The path to the CA certificate file. + /// - `private_key_path`: The path to the CA private key file. + /// + /// # Returns + /// A modified `HttpMockServerBuilder` instance for method chaining. + #[cfg(feature = "https")] + pub fn https_ca_key_pair_files>( + mut self, + cert_path: Path, + private_key_path: Path, + ) -> Self { + self.https_config_builder = self + .https_config_builder + .ca_cert_path(Some(cert_path.into())); + self.https_config_builder = self + .https_config_builder + .ca_key_path(Some(private_key_path.into())); + self + } + + /// Sets the paths to the CA certificate and private key files for HTTPS as optional values. + /// + /// # Parameters + /// - `cert_path`: An optional path to the CA certificate file. + /// - `private_key_path`: An optional path to the CA private key file. + /// + /// # Returns + /// A modified `HttpMockServerBuilder` instance for method chaining. + #[cfg(feature = "https")] + pub fn https_ca_key_pair_files_option>( + mut self, + cert_path: Option, + private_key_path: Option, + ) -> Self { + let cert_path = cert_path.map(|b| b.into()); + let private_key_path = private_key_path.map(|b| b.into()); + + self.https_config_builder = self.https_config_builder.ca_cert_path(cert_path); + self.https_config_builder = self.https_config_builder.ca_key_path(private_key_path); + self + } + + /// Builds the `HttpMockServer` with the current settings. + /// + /// # Returns + /// A `HttpMockServer` instance or an error if the build process fails. + pub fn build(self) -> Result> { + self.build_with_state(Arc::new(HttpMockStateManager::default())) + } + + /// Builds the `MockServer` with the current settings and provided state manager. + /// + /// # Parameters + /// - `state`: The state manager to use. + /// + /// # Returns + /// A `MockServer` instance or an error if the build process fails. + pub(crate) fn build_with_state( + mut self, + state: Arc, + ) -> Result>, Box> + where + S: StateManager + Send + Sync + 'static, + { + #[cfg(feature = "proxy")] + let http_client = self + .http_client + .unwrap_or_else(|| Arc::new(HttpMockHttpClient::new(None))); + + #[cfg(feature = "record")] + if let Some(dir) = self.static_mock_dir { + read_static_mock_definitions(dir, state.as_ref())?; + } + + let handler = HttpMockHandler::new( + state, + #[cfg(feature = "proxy")] + http_client, + ); + + Ok(MockServer::new( + Box::new(handler), + MockServerConfig { + static_port: self.port, + expose: self.expose.unwrap_or(false), + print_access_log: self.print_access_log.unwrap_or(false), + #[cfg(feature = "https")] + https: self.https_config_builder.build()?, + }, + )?) + } +} diff --git a/src/server/handler.rs b/src/server/handler.rs new file mode 100644 index 00000000..a8e63d2c --- /dev/null +++ b/src/server/handler.rs @@ -0,0 +1,565 @@ +use crate::common::data::{ + ActiveForwardingRule, ActiveProxyRule, Error as DataError, ErrorResponse, MockDefinition, + RequestRequirements, +}; + +use crate::{ + common::runtime, + server::{ + handler::Error::{ + InvalidHeader, ParamError, ParamFormatError, RequestBodyDeserializeError, + RequestConversionError, ResponseBodyConversionError, ResponseBodySerializeError, + }, + state, + state::StateManager, + }, +}; +use std::convert::TryInto; + +#[cfg(any(feature = "remote", feature = "proxy"))] +use crate::common::http::{Error as HttpClientError, HttpClient}; + +use crate::common::data::{ForwardingRuleConfig, ProxyRuleConfig, RecordingRuleConfig}; + +use crate::prelude::HttpMockRequest; +use async_std::{sync::Mutex, task}; +use async_trait::async_trait; +use http::{HeaderMap, HeaderName, HeaderValue, StatusCode, Uri}; +use http_body_util::BodyExt; +use hyper::{body::Bytes, Method, Request, Response}; +use path_tree::{Path, PathTree}; +use serde::{de::DeserializeOwned, Serialize}; +use std::{ + fmt::{Debug, Display}, + str::FromStr, + sync::Arc, + thread, + time::Duration, +}; +use thiserror::Error; +use tokio::time::Instant; + +#[derive(Error, Debug)] +pub enum Error { + #[error("cannot parse regex: {0}")] + RegexError(#[from] regex::Error), + #[error("cannot deserialize request body: {0}")] + RequestBodyDeserializeError(serde_json::Error), + #[error("cannot process request body: {0}")] + RequestBodyError(String), + #[error("cannot serialize response body: {0}")] + ResponseBodySerializeError(serde_json::Error), + #[error("cannot convert response body: {0}")] + ResponseBodyConversionError(http::Error), + #[error("expected URL parameters not found")] + ParamError, + #[error("URL parameter format is invalid: {0}")] + ParamFormatError(String), + #[error("cannot modify state: {0}")] + StateManagerError(#[from] state::Error), + #[error("invalid status code: {0}")] + InvalidStatusCode(#[from] http::status::InvalidStatusCode), + #[error("cannot convert request to internal data structure: {0}")] + RequestConversionError(String), + #[cfg(any(feature = "remote", feature = "proxy"))] + #[error("failed to send HTTP request: {0}")] + HttpClientError(#[from] HttpClientError), + #[error("invalid header: {0}")] + InvalidHeader(String), + #[error("unknown error")] + Unknown, +} + +enum RoutePath { + Ping, + Reset, + MockCollection, + SingleMock, + History, + Verify, + SingleForwardingRule, + ForwardingRuleCollection, + ProxyRuleCollection, + SingleProxyRule, + #[cfg(feature = "record")] + RecordingCollection, + #[cfg(feature = "record")] + SingleRecording, +} + +#[async_trait] +pub(crate) trait Handler { + async fn handle(&self, req: Request) -> Result, Error>; +} + +pub struct HttpMockHandler +where + S: StateManager + Send + Sync + 'static, +{ + path_tree: PathTree, + state: Arc, + #[cfg(feature = "proxy")] + http_client: Arc, +} + +#[async_trait] +impl Handler for HttpMockHandler +where + H: StateManager + Send + Sync + 'static, +{ + async fn handle(&self, req: Request) -> Result, Error> { + log::trace!("Routing incoming request: {:?}", req); + + let method = req.method().clone(); + let path = req.uri().path().to_string(); + + if let Some((matched_path, params)) = self.path_tree.find(&path) { + match matched_path { + RoutePath::Ping => match method { + Method::GET => return self.handle_ping(), + _ => {} + }, + RoutePath::Reset => match method { + Method::DELETE => return self.handle_reset(), + _ => {} + }, + RoutePath::SingleMock => match method { + Method::GET => return self.handle_read_mock(params), + Method::DELETE => return self.handle_delete_mock(params), + _ => {} + }, + RoutePath::MockCollection => match method { + Method::POST => return self.handle_add_mock(req), + Method::DELETE => return self.handle_delete_all_mocks(), + _ => {} + }, + RoutePath::History => match method { + Method::DELETE => return self.handle_delete_history(), + _ => {} + }, + RoutePath::Verify => match method { + Method::POST => return self.handle_verify(req), + _ => {} + }, + RoutePath::ForwardingRuleCollection => match method { + Method::POST => return self.handle_add_forwarding_rule(req), + Method::DELETE => return self.handle_delete_all_forwarding_rules(), + _ => {} + }, + RoutePath::SingleForwardingRule => match method { + Method::DELETE => return self.handle_delete_forwarding_rule(params), + _ => {} + }, + RoutePath::ProxyRuleCollection => match method { + Method::POST => return self.handle_add_proxy_rule(req), + Method::DELETE => return self.handle_delete_all_proxy_rules(), + _ => {} + }, + RoutePath::SingleProxyRule => match method { + Method::DELETE => return self.handle_delete_proxy_rule(params), + _ => {} + }, + #[cfg(feature = "record")] + RoutePath::RecordingCollection => match method { + Method::POST => return self.handle_add_recording_matcher(req), + Method::DELETE => return self.handle_delete_all_recording_matchers(), + _ => {} + }, + #[cfg(feature = "record")] + RoutePath::SingleRecording => match method { + Method::GET => return self.handle_read_recording(params), + Method::DELETE => return self.handle_delete_recording(params), + Method::POST => return self.handle_load_recording(req), + _ => {} + }, + } + } + + return self.catch_all(req).await; + } +} + +impl HttpMockHandler +where + H: StateManager + Send + Sync + 'static, +{ + pub fn new( + state: Arc, + #[cfg(feature = "proxy")] http_client: Arc, + ) -> Self { + let mut path_tree: PathTree = PathTree::new(); + #[allow(unused_must_use)] + { + path_tree.insert("/__httpmock__/ping", RoutePath::Ping); + path_tree.insert("/__httpmock__/state", RoutePath::Reset); + path_tree.insert("/__httpmock__/mocks", RoutePath::MockCollection); + path_tree.insert("/__httpmock__/mocks/:id", RoutePath::SingleMock); + path_tree.insert("/__httpmock__/verify", RoutePath::Verify); + path_tree.insert("/__httpmock__/history", RoutePath::History); + path_tree.insert( + "/__httpmock__/forwarding_rules", + RoutePath::ForwardingRuleCollection, + ); + + #[cfg(feature = "record")] + path_tree.insert("/__httpmock__/proxy_rules", RoutePath::ProxyRuleCollection); + #[cfg(feature = "record")] + path_tree.insert("/__httpmock__/recordings", RoutePath::RecordingCollection); + } + + Self { + path_tree, + state, + #[cfg(feature = "proxy")] + http_client, + } + } + + fn ping(&self) -> Result<(), Error> { + Ok(()) + } + + fn handle_ping(&self) -> Result, Error> { + return response::<()>(StatusCode::OK, None); + } + + fn handle_reset(&self) -> Result, Error> { + self.state.reset(); + return response::<()>(StatusCode::NO_CONTENT, None); + } + + fn handle_add_mock(&self, req: Request) -> Result, Error> { + let definition: MockDefinition = parse_json_body(req)?; + let active_mock = self.state.add_mock(definition, false)?; + return response(StatusCode::CREATED, Some(active_mock)); + } + + fn handle_read_mock(&self, params: Path) -> Result, Error> { + let active_mock = self.state.read_mock(param("id", params)?)?; + let status_code = active_mock + .as_ref() + .map_or(StatusCode::NOT_FOUND, |_| StatusCode::OK); + return response(status_code, active_mock); + } + + fn handle_delete_mock(&self, params: Path) -> Result, Error> { + let deleted = self.state.delete_mock(param("id", params)?)?; + let status_code = if deleted { + StatusCode::NO_CONTENT + } else { + StatusCode::NOT_FOUND + }; + return response::<()>(status_code, None); + } + + fn handle_delete_all_mocks(&self) -> Result, Error> { + self.state.delete_all_mocks(); + return response::<()>(StatusCode::NO_CONTENT, None); + } + + fn handle_delete_history(&self) -> Result, Error> { + self.state.delete_history(); + return response::<()>(StatusCode::NO_CONTENT, None); + } + + fn handle_verify(&self, req: Request) -> Result, Error> { + let requirements: RequestRequirements = parse_json_body(req)?; + let closest_match = self.state.verify(&requirements)?; + let status_code = closest_match + .as_ref() + .map_or(StatusCode::NOT_FOUND, |_| StatusCode::OK); + return response(status_code, closest_match); + } + + fn handle_add_forwarding_rule(&self, req: Request) -> Result, Error> { + let config: ForwardingRuleConfig = parse_json_body(req)?; + let active_forwarding_rule = self.state.create_forwarding_rule(config); + return response(StatusCode::CREATED, Some(active_forwarding_rule)); + } + + fn handle_delete_forwarding_rule(&self, params: Path) -> Result, Error> { + let deleted = self.state.delete_forwarding_rule(param("id", params)?); + let status_code = if deleted.is_some() { + StatusCode::NO_CONTENT + } else { + StatusCode::NOT_FOUND + }; + return response::<()>(status_code, None); + } + + fn handle_delete_all_forwarding_rules(&self) -> Result, Error> { + self.state.delete_all_forwarding_rules(); + return response::<()>(StatusCode::NO_CONTENT, None); + } + + fn handle_add_proxy_rule(&self, req: Request) -> Result, Error> { + let config: ProxyRuleConfig = parse_json_body(req)?; + let active_proxy_rule = self.state.create_proxy_rule(config); + return response(StatusCode::CREATED, Some(active_proxy_rule)); + } + + fn handle_delete_proxy_rule(&self, params: Path) -> Result, Error> { + let deleted = self.state.delete_proxy_rule(param("id", params)?); + let status_code = if deleted.is_some() { + StatusCode::NO_CONTENT + } else { + StatusCode::NOT_FOUND + }; + return response::<()>(status_code, None); + } + + fn handle_delete_all_proxy_rules(&self) -> Result, Error> { + self.state.delete_all_proxy_rules(); + return response::<()>(StatusCode::NO_CONTENT, None); + } + + #[cfg(feature = "record")] + fn handle_add_recording_matcher(&self, req: Request) -> Result, Error> { + let req_req: RecordingRuleConfig = parse_json_body(req)?; + let active_recording = self.state.create_recording(req_req); + return response(StatusCode::CREATED, Some(active_recording)); + } + + #[cfg(feature = "record")] + fn handle_delete_recording(&self, params: Path) -> Result, Error> { + let deleted = self.state.delete_proxy_rule(param("id", params)?); + let status_code = if deleted.is_some() { + StatusCode::NO_CONTENT + } else { + StatusCode::NOT_FOUND + }; + return response::<()>(status_code, None); + } + + #[cfg(feature = "record")] + fn handle_delete_all_recording_matchers(&self) -> Result, Error> { + self.state.delete_all_recordings(); + return response::<()>(StatusCode::NO_CONTENT, None); + } + + #[cfg(feature = "record")] + fn handle_read_recording(&self, params: Path) -> Result, Error> { + let rec = self.state.export_recording(param("id", params)?)?; + let status_code = rec + .as_ref() + .map_or(StatusCode::NOT_FOUND, |_| StatusCode::OK); + return response(status_code, rec); + } + + #[cfg(feature = "record")] + fn handle_load_recording(&self, req: Request) -> Result, Error> { + let recording_file_content = std::str::from_utf8(&req.body()) + .map_err(|err| RequestConversionError(err.to_string()))?; + + let rec = self + .state + .load_mocks_from_recording(recording_file_content)?; + return response(StatusCode::OK, Some(rec)); + } + + async fn catch_all(&self, req: Request) -> Result, Error> { + let internal_request: HttpMockRequest = (&req) + .try_into() + .map_err(|err: DataError| RequestConversionError(err.to_string()))?; + + let mut is_proxied = false; + + let start = Instant::now(); + + #[cfg(feature = "proxy")] + let res = if let Some(rule) = self.state.find_forward_rule(&internal_request)? { + self.forward(rule, req).await? + } else if let Some(rule) = self.state.find_proxy_rule(&internal_request)? { + is_proxied = true; + self.proxy(rule, req).await? + } else { + self.serve_mock(&internal_request).await? + }; + + #[cfg(not(feature = "proxy"))] + let res = self.serve_mock(&internal_request).await?; + + #[cfg(feature = "record")] + self.state + .record(is_proxied, start.elapsed(), internal_request, &res)?; + + Ok(res) + } + + #[cfg(feature = "proxy")] + async fn forward( + &self, + rule: ActiveForwardingRule, + req: Request, + ) -> Result, Error> { + let to_base_uri: Uri = rule.config.target_base_url.parse().unwrap(); + + let (mut req_parts, body) = req.into_parts(); + + // We need to remove the host header, because it contains the host of this mock server. + req_parts.headers.remove(http::header::HOST); + + let mut uri_parts = req_parts.uri.into_parts(); + uri_parts.authority = Some(to_base_uri.authority().unwrap().clone()); + uri_parts.scheme = to_base_uri.scheme().map(|s| s.clone()).or(uri_parts.scheme); + req_parts.uri = Uri::from_parts(uri_parts).unwrap(); + + if !rule.config.request_header.is_empty() { + for (key, value) in &rule.config.request_header { + let key = HeaderName::from_str(key).map_err(|err| { + InvalidHeader(format!("invalid header key: {}", err.to_string())) + })?; + + let value = HeaderValue::from_str(value).map_err(|err| { + InvalidHeader(format!("invalid header value: {}", err.to_string())) + })?; + + req_parts.headers.insert(key, value); + } + } + + let req = Request::from_parts(req_parts, body); + Ok(self.http_client.send(req).await?) + } + + #[cfg(feature = "proxy")] + async fn proxy( + &self, + rule: ActiveProxyRule, + mut req: Request, + ) -> Result, Error> { + if !rule.config.request_header.is_empty() { + let headers = req.headers_mut(); + + for (key, value) in &rule.config.request_header { + let key = HeaderName::from_str(key).map_err(|err| { + InvalidHeader(format!("invalid header key: {}", err.to_string())) + })?; + + let value = HeaderValue::from_str(value).map_err(|err| { + InvalidHeader(format!("invalid header value: {}", err.to_string())) + })?; + + headers.insert(key, value); + } + } + + Ok(self.http_client.send(req).await?) + } + + async fn serve_mock(&self, req: &HttpMockRequest) -> Result, Error> { + let mock_response = self.state.serve_mock(req)?; + + if let Some(mock_response) = mock_response { + let status_code = match mock_response.status.as_ref() { + None => StatusCode::OK, + Some(c) => StatusCode::from_u16(c.clone())?, + }; + + let mut builder = Response::builder().status(status_code); + + if let Some(headers) = mock_response.headers { + for (name, value) in headers { + builder = builder.header(name, value); + } + } + + let response = builder + .body( + mock_response + .body + .map_or(Bytes::new(), |bytes| bytes.to_bytes()), + ) + .map_err(|e| ResponseBodyConversionError(e))?; + + if let Some(duration) = mock_response.delay { + runtime::sleep(Duration::from_millis(duration)).await; + } + + return Ok(response); + } + + let status_code = mock_response.map_or(StatusCode::NOT_FOUND, |_| StatusCode::OK); + + return response( + status_code, + Some(ErrorResponse::new( + &"Request did not match any route or mock", + )), + ); + } +} + +fn param(name: &str, tree_path: Path) -> Result +where + T: FromStr, + T::Err: Debug + Display, +{ + for (n, v) in tree_path.params() { + if n.eq(name) { + let parse_result: Result = v.parse::(); + let parsed_value = parse_result.map_err(|e| ParamFormatError(format!("{:?}", e)))?; + return Ok(parsed_value); + } + } + + Err(ParamError) +} + +fn response(status: StatusCode, body: Option) -> Result, Error> +where + T: Serialize, +{ + let mut builder = Response::builder().status(status); + + if let Some(body_obj) = body { + builder = builder.header("content-type", "application/json"); + + let body_bytes = + serde_json::to_vec(&body_obj).map_err(|e| ResponseBodySerializeError(e))?; + + return Ok(builder + .body(Bytes::from(body_bytes)) + .map_err(|e| ResponseBodyConversionError(e))?); + } + + return Ok(builder + .body(Bytes::new()) + .map_err(|e| ResponseBodyConversionError(e))?); +} + +fn parse_json_body(req: Request) -> Result +where + T: DeserializeOwned, +{ + let body: T = + serde_json::from_slice(req.body().as_ref()).map_err(|e| RequestBodyDeserializeError(e))?; + Ok(body) +} + +fn extract_query_params(req: &Request) -> Result, Error> { + // There doesn't seem to be a way to just parse Query string with the `url` crate, so we're + // prefixing a dummy URL for parsing. + let url = format!("http://dummy?{}", req.uri().query().unwrap_or("")); + let url = url::Url::parse(&url).map_err(|e| RequestConversionError(e.to_string()))?; + + let query_params = url + .query_pairs() + .map(|(k, v)| (k.into(), v.into())) + .collect(); + + Ok(query_params) +} + +fn headers_to_vec(req: &Request) -> Result, Error> { + req.headers() + .iter() + .map(|(name, value)| { + // Attempt to convert the HeaderValue to a &str, returning an error if it fails. + let value_str = value + .to_str() + .map_err(|e| RequestConversionError(e.to_string()))?; + Ok((name.as_str().to_string(), value_str.to_string())) + }) + .collect() +} diff --git a/src/server/matchers/comparators.rs b/src/server/matchers/comparators.rs index 8b783492..89e0a79e 100644 --- a/src/server/matchers/comparators.rs +++ b/src/server/matchers/comparators.rs @@ -1,12 +1,24 @@ use assert_json_diff::{assert_json_matches_no_panic, CompareMode, Config}; +use bytes::Bytes; use serde_json::Value; - -use crate::common::data::{HttpMockRequest, MockMatcherFunction}; -use crate::server::matchers::distance_for; -use crate::Regex; - -pub trait ValueComparator { - fn matches(&self, mock_value: &S, req_value: &T) -> bool; +use std::{borrow::Cow, convert::TryInto, ops::Deref, sync::Arc}; + +use crate::{ + common::{ + data::{HttpMockRegex, HttpMockRequest}, + util::HttpMockBytes, + }, + server::matchers::comparison::{ + distance_for, distance_for_prefix, distance_for_substring, distance_for_suffix, + equal_weight_distance_for, hostname_equals, regex_unmatched_length, string_contains, + string_distance, string_equals, string_has_prefix, string_has_suffix, + }, +}; + +use crate::server::matchers::comparison; + +pub trait ValueComparator { + fn matches(&self, mock_value: &Option<&S>, req_value: &Option<&T>) -> bool; fn name(&self) -> &str; fn distance(&self, mock_value: &Option<&S>, req_value: &Option<&T>) -> usize; } @@ -23,9 +35,15 @@ impl JSONExactMatchComparator { } impl ValueComparator for JSONExactMatchComparator { - fn matches(&self, mock_value: &Value, req_value: &Value) -> bool { - let config = Config::new(CompareMode::Strict); - assert_json_matches_no_panic(req_value, mock_value, config).is_ok() + fn matches(&self, mock_value: &Option<&Value>, req_value: &Option<&Value>) -> bool { + match (mock_value, req_value) { + (None, _) => true, + (Some(_), None) => false, + (Some(mv), Some(rv)) => { + let config = Config::new(CompareMode::Strict); + assert_json_matches_no_panic(rv, mv, config).is_ok() + } + } } fn name(&self) -> &str { @@ -33,91 +51,368 @@ impl ValueComparator for JSONExactMatchComparator { } fn distance(&self, mock_value: &Option<&Value>, req_value: &Option<&Value>) -> usize { - distance_for(mock_value, req_value) + let mv_bytes = mock_value.map_or(Vec::new(), |v| v.to_string().into_bytes()); + let rv_bytes = req_value.map_or(Vec::new(), |v| v.to_string().into_bytes()); + distance_for(&mv_bytes, &rv_bytes) } } // ************************************************************************************************ -// JSONExactMatchComparator +// JSONContainsMatchComparator // ************************************************************************************************ -pub struct JSONContainsMatchComparator {} +pub struct JSONContainsMatchComparator { + pub negated: bool, +} impl JSONContainsMatchComparator { - pub fn new() -> Self { - Self {} + pub fn new(negated: bool) -> Self { + Self { negated } } } impl ValueComparator for JSONContainsMatchComparator { - fn matches(&self, mock_value: &Value, req_value: &Value) -> bool { - let config = Config::new(CompareMode::Inclusive); - assert_json_matches_no_panic(req_value, mock_value, config).is_ok() + fn matches(&self, mock_value: &Option<&Value>, req_value: &Option<&Value>) -> bool { + match (mock_value, req_value) { + (None, _) => true, + (Some(_), None) => self.negated, + (Some(mv), Some(rv)) => { + let config = Config::new(CompareMode::Inclusive); + let matches = assert_json_matches_no_panic(rv, mv, config).is_ok(); + if self.negated { + !matches + } else { + matches + } + } + } } fn name(&self) -> &str { - "contains" + if self.negated { + return "excludes"; + } + + return "includes"; } fn distance(&self, mock_value: &Option<&Value>, req_value: &Option<&Value>) -> usize { - distance_for(mock_value, req_value) + let mv_bytes = mock_value.map_or(Vec::new(), |v| v.to_string().into_bytes()); + let rv_bytes = req_value.map_or(Vec::new(), |v| v.to_string().into_bytes()); + let distance = equal_weight_distance_for(&mv_bytes, &rv_bytes); + + if self.negated { + std::cmp::max(mv_bytes.len(), rv_bytes.len()) - distance + } else { + distance + } + } +} + +// ************************************************************************************************ +// StringExactMatchComparator +// ************************************************************************************************ +pub struct HostEqualsComparator { + negated: bool, +} + +impl HostEqualsComparator { + pub fn new(negated: bool) -> Self { + Self { negated } + } +} + +impl ValueComparator for HostEqualsComparator { + fn matches(&self, mock_value: &Option<&String>, req_value: &Option<&String>) -> bool { + hostname_equals(self.negated, &mock_value, &req_value) + } + + fn name(&self) -> &str { + if self.negated { + return "not equal to"; + } + + return "equals"; + } + + fn distance(&self, mock_value: &Option<&String>, req_value: &Option<&String>) -> usize { + // negation is taken care of in matches! + if self.matches(mock_value, req_value) { + return 0; + } + + string_distance(false, self.negated, mock_value, req_value) } } // ************************************************************************************************ // StringExactMatchComparator // ************************************************************************************************ -pub struct StringExactMatchComparator { +pub struct StringEqualsComparator { case_sensitive: bool, + negated: bool, +} + +impl StringEqualsComparator { + pub fn new(case_sensitive: bool, negated: bool) -> Self { + Self { + case_sensitive, + negated, + } + } } -impl StringExactMatchComparator { - pub fn new(case_sensitive: bool) -> Self { - Self { case_sensitive } +impl ValueComparator for StringEqualsComparator { + fn matches(&self, mock_value: &Option<&String>, req_value: &Option<&String>) -> bool { + string_equals(self.case_sensitive, self.negated, &mock_value, &req_value) } + + fn name(&self) -> &str { + if self.negated { + return "not equal to"; + } + + return "equals"; + } + + fn distance(&self, mock_value: &Option<&String>, req_value: &Option<&String>) -> usize { + string_distance(self.case_sensitive, self.negated, mock_value, req_value) + } +} + +// ************************************************************************************************ +// StringIncludesMatchComparator +// ************************************************************************************************ +pub struct StringContainsComparator { + case_sensitive: bool, + negated: bool, } -impl ValueComparator for StringExactMatchComparator { - fn matches(&self, mock_value: &String, req_value: &String) -> bool { - match self.case_sensitive { - true => mock_value.eq(req_value), - false => mock_value.to_lowercase().eq(&req_value.to_lowercase()), +impl StringContainsComparator { + pub fn new(case_sensitive: bool, negated: bool) -> Self { + Self { + case_sensitive, + negated, } } +} + +impl ValueComparator for StringContainsComparator { + fn matches(&self, mock_value: &Option<&String>, req_value: &Option<&String>) -> bool { + string_contains(self.case_sensitive, self.negated, &mock_value, &req_value) + } + fn name(&self) -> &str { - "equals" + if self.negated { + return "excludes"; + } + + return "includes"; } + fn distance(&self, mock_value: &Option<&String>, req_value: &Option<&String>) -> usize { - distance_for(mock_value, req_value) + let mock_slice = mock_value.as_ref().map(|s| s.as_str()); + let req_slice = req_value.as_ref().map(|s| s.as_str()); + + distance_for_substring(self.case_sensitive, self.negated, &mock_slice, &req_slice) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_distance_mock_shorter_than_req() { + let binding1 = "hello".to_string(); + let mock_value = Some(&binding1); + let binding2 = "hello world".to_string(); + let req_value = Some(&binding2); + + let comparator = StringContainsComparator { + case_sensitive: true, + negated: false, + }; + assert_eq!(comparator.distance(&mock_value, &req_value), 6); // Assuming "hello" vs "hello world" + } + + #[test] + fn test_distance_mock_equal_size_different() { + let binding1 = "hello".to_string(); + let mock_value = Some(&binding1); + let binding2 = "world".to_string(); + let req_value = Some(&binding2); + + let comparator = StringContainsComparator { + case_sensitive: true, + negated: false, + }; + assert_eq!(comparator.distance(&mock_value, &req_value), 4); // Assuming "hello" vs "world" + } + + #[test] + fn test_distance_exact_match() { + let binding1 = "hello".to_string(); + let mock_value = Some(&binding1); + let binding2 = "hello".to_string(); + let req_value = Some(&binding2); + + let comparator = StringContainsComparator { + case_sensitive: true, + negated: false, + }; + assert_eq!(comparator.distance(&mock_value, &req_value), 0); // Exact match } } // ************************************************************************************************ -// StringExactMatchComparator +// StringContainsMatchComparator // ************************************************************************************************ -pub struct StringContainsMatchComparator { +pub struct StringPrefixMatchComparator { case_sensitive: bool, + negated: bool, } -impl StringContainsMatchComparator { - pub fn new(case_sensitive: bool) -> Self { - Self { case_sensitive } +impl StringPrefixMatchComparator { + pub fn new(case_sensitive: bool, negated: bool) -> Self { + Self { + case_sensitive, + negated, + } + } +} + +impl ValueComparator for StringPrefixMatchComparator { + fn matches(&self, mock_value: &Option<&String>, req_value: &Option<&String>) -> bool { + string_has_prefix(self.case_sensitive, self.negated, &mock_value, &req_value) + } + + fn name(&self) -> &str { + if self.negated { + return "prefix not"; + } + + return "has prefix"; + } + + fn distance(&self, mock_value: &Option<&String>, req_value: &Option<&String>) -> usize { + distance_for_prefix(self.case_sensitive, self.negated, mock_value, req_value) } } -impl ValueComparator for StringContainsMatchComparator { - fn matches(&self, mock_value: &String, req_value: &String) -> bool { - match self.case_sensitive { - true => req_value.contains(mock_value), - false => req_value - .to_lowercase() - .contains(&mock_value.to_lowercase()), +// ************************************************************************************************ +// StringContainsMatchComparator +// ************************************************************************************************ +pub struct StringSuffixMatchComparator { + case_sensitive: bool, + negated: bool, +} + +impl StringSuffixMatchComparator { + pub fn new(case_sensitive: bool, negated: bool) -> Self { + Self { + case_sensitive, + negated, } } +} + +impl ValueComparator for StringSuffixMatchComparator { + fn matches(&self, mock_value: &Option<&String>, req_value: &Option<&String>) -> bool { + string_has_suffix(self.case_sensitive, self.negated, &mock_value, &req_value) + } + fn name(&self) -> &str { - "contains" + if self.negated { + return "suffix not"; + } + + return "has suffix"; } + fn distance(&self, mock_value: &Option<&String>, req_value: &Option<&String>) -> usize { - distance_for(mock_value, req_value) + distance_for_suffix(self.case_sensitive, self.negated, mock_value, req_value) + } +} + +// ************************************************************************************************ +// StringPatternMatchComparator +// ************************************************************************************************ +pub struct StringPatternMatchComparator { + case_sensitive: bool, + negated: bool, +} + +impl StringPatternMatchComparator { + pub fn new(negated: bool, case_sensitive: bool) -> Self { + Self { + negated, + case_sensitive, + } + } +} + +impl ValueComparator for StringPatternMatchComparator { + fn matches(&self, mock_value: &Option<&HttpMockRegex>, req_value: &Option<&String>) -> bool { + comparison::string_matches_regex(self.negated, self.case_sensitive, &mock_value, &req_value) + } + + fn name(&self) -> &str { + if self.negated { + return "does not match regex"; + } + + return "matches regex"; + } + + fn distance(&self, mock_value: &Option<&HttpMockRegex>, req_value: &Option<&String>) -> usize { + comparison::regex_string_distance(self.negated, self.case_sensitive, mock_value, req_value) + } +} + +// ************************************************************************************************ +// StringExactMatchComparator +// ************************************************************************************************ +pub struct HttpMockBytesPatternComparator {} + +impl HttpMockBytesPatternComparator { + pub fn new() -> Self { + Self {} + } +} + +impl ValueComparator for HttpMockBytesPatternComparator { + fn matches( + &self, + mock_value: &Option<&HttpMockRegex>, + req_value: &Option<&HttpMockBytes>, + ) -> bool { + match (mock_value, req_value) { + (None, Some(_)) => true, + (Some(_), None) => false, + (Some(mv), Some(rv)) => mv.0.is_match(&rv.to_maybe_lossy_str()), + (None, None) => true, + } + } + + fn name(&self) -> &str { + "matches regex" + } + + fn distance( + &self, + mock_value: &Option<&HttpMockRegex>, + req_value: &Option<&HttpMockBytes>, + ) -> usize { + let rv = match req_value { + Some(s) => s.to_maybe_lossy_str(), + None => Cow::Borrowed(""), + }; + + let default_pattern = HttpMockRegex(regex::Regex::new(".*").unwrap()); + + let mv = mock_value.unwrap_or(&default_pattern); + + regex_unmatched_length(&rv, &mv) } } @@ -132,17 +427,302 @@ impl StringRegexMatchComparator { } } -impl ValueComparator for StringRegexMatchComparator { - fn matches(&self, mock_value: &Regex, req_value: &String) -> bool { - mock_value.is_match(req_value) +impl ValueComparator for StringRegexMatchComparator { + fn matches(&self, mock_value: &Option<&HttpMockRegex>, req_value: &Option<&String>) -> bool { + return match (mock_value, req_value) { + (None, Some(_)) => true, + (Some(_), None) => false, + (Some(mv), Some(rv)) => mv.0.is_match(&rv), + (None, None) => true, + }; } fn name(&self) -> &str { "matches regex" } - fn distance(&self, mock_value: &Option<&Regex>, req_value: &Option<&String>) -> usize { - distance_for(mock_value, req_value) + fn distance(&self, mock_value: &Option<&HttpMockRegex>, req_value: &Option<&String>) -> usize { + let rv = req_value.map_or("", |s| s.as_str()); + let mut mv = &HttpMockRegex(regex::Regex::new(".*").unwrap()); + if mock_value.is_some() { + mv = mock_value.unwrap() + }; + regex_unmatched_length(rv, &mv) + } +} + +// ************************************************************************************************ +// IntegerExactMatchComparator +// ************************************************************************************************ +pub struct U16ExactMatchComparator { + negated: bool, +} + +impl U16ExactMatchComparator { + pub fn new(negated: bool) -> Self { + Self { negated } + } +} + +impl ValueComparator for U16ExactMatchComparator { + fn matches(&self, mock_value: &Option<&u16>, req_value: &Option<&u16>) -> bool { + comparison::integer_equals(self.negated, &mock_value, &req_value) + } + + fn name(&self) -> &str { + if self.negated { + return "not equal to"; + } + + return "equals"; + } + + fn distance(&self, mock_value: &Option<&u16>, req_value: &Option<&u16>) -> usize { + comparison::distance_for_usize(mock_value, req_value) + } +} + +// ************************************************************************************************ +// BytesExactMatchComparator +// ************************************************************************************************ +pub struct BytesExactMatchComparator { + negated: bool, +} + +impl BytesExactMatchComparator { + pub fn new(negated: bool) -> Self { + Self { negated } + } +} + +impl ValueComparator for BytesExactMatchComparator { + fn matches( + &self, + mock_value: &Option<&HttpMockBytes>, + req_value: &Option<&HttpMockBytes>, + ) -> bool { + return comparison::bytes_equal(self.negated, &mock_value, &req_value); + } + + fn name(&self) -> &str { + if self.negated { + return "not equal to"; + } + + return "equals"; + } + + fn distance( + &self, + mock_value: &Option<&HttpMockBytes>, + req_value: &Option<&HttpMockBytes>, + ) -> usize { + let mock_slice = mock_value + .as_ref() + .map(|mv| mv.to_bytes().clone()) + .unwrap_or_else(|| Bytes::new()); + + let req_slice = req_value + .as_ref() + .map(|rv| rv.to_bytes().clone()) + .unwrap_or_else(|| Bytes::new()); + + distance_for(mock_slice.as_ref(), req_slice.as_ref()) + } +} + +// ************************************************************************************************ +// BytesExactMatchComparator +// ************************************************************************************************ +pub struct BytesIncludesComparator { + negated: bool, +} + +impl BytesIncludesComparator { + pub fn new(negated: bool) -> Self { + Self { negated } + } +} + +impl ValueComparator for BytesIncludesComparator { + fn matches( + &self, + mock_value: &Option<&HttpMockBytes>, + req_value: &Option<&HttpMockBytes>, + ) -> bool { + comparison::bytes_includes(self.negated, &mock_value, &req_value) + } + + fn name(&self) -> &str { + if self.negated { + return "excludes"; + } + + return "includes"; + } + + fn distance( + &self, + mock_value: &Option<&HttpMockBytes>, + req_value: &Option<&HttpMockBytes>, + ) -> usize { + let mock_slice = mock_value + .as_ref() + .map(|mv| mv.to_bytes().clone()) + .unwrap_or_else(|| Bytes::new()); + + let req_slice = req_value + .as_ref() + .map(|rv| rv.to_bytes().clone()) + .unwrap_or_else(|| Bytes::new()); + + distance_for(mock_slice.as_ref(), req_slice.as_ref()) + } +} + +// ************************************************************************************************ +// BytesPrefixComparator +// ************************************************************************************************ +pub struct BytesPrefixComparator { + negated: bool, +} + +impl BytesPrefixComparator { + pub fn new(negated: bool) -> Self { + Self { negated } + } +} + +impl ValueComparator for BytesPrefixComparator { + fn matches( + &self, + mock_value: &Option<&HttpMockBytes>, + req_value: &Option<&HttpMockBytes>, + ) -> bool { + comparison::bytes_prefix(self.negated, &mock_value, &req_value) + } + + fn name(&self) -> &str { + if self.negated { + return "prefix not"; + } + + "has prefix" + } + + fn distance( + &self, + mock_value: &Option<&HttpMockBytes>, + req_value: &Option<&HttpMockBytes>, + ) -> usize { + let mock_slice = mock_value + .as_ref() + .map(|mv| mv.to_bytes().clone()) + .unwrap_or_else(|| Bytes::new()); + + let req_slice = req_value + .as_ref() + .map(|rv| rv.to_bytes().clone()) + .unwrap_or_else(|| Bytes::new()); + + // If mock has no requirement, distance is always 0 + if mock_value.is_none() || mock_slice.is_empty() { + return 0; + } + + // If request does not contain any data + if req_value.is_none() || req_slice.is_empty() { + return mock_slice.len(); + } + + // Compare only up to the length of the mock_slice + let compared_window = std::cmp::min(mock_slice.len(), req_slice.len()); + let distance = equal_weight_distance_for( + &mock_slice[..compared_window], + &req_slice[..compared_window], + ); + + // if negated, we want to find out how many + if self.negated { + // This is why we need the equal_weight_distance_for function: + // to calculate the distance as the number of differing characters. + return compared_window - distance; + } + + return distance; + } +} + +// ************************************************************************************************ +// BytesSuffixComparator +// ************************************************************************************************ +pub struct BytesSuffixComparator { + negated: bool, +} + +impl BytesSuffixComparator { + pub fn new(negated: bool) -> Self { + Self { negated } + } +} + +impl ValueComparator for BytesSuffixComparator { + fn matches( + &self, + mock_value: &Option<&HttpMockBytes>, + req_value: &Option<&HttpMockBytes>, + ) -> bool { + comparison::bytes_suffix(self.negated, &mock_value, &req_value) + } + + fn name(&self) -> &str { + if self.negated { + return "suffix not"; + } + + return "has suffix"; + } + + fn distance( + &self, + mock_value: &Option<&HttpMockBytes>, + req_value: &Option<&HttpMockBytes>, + ) -> usize { + let mock_slice = mock_value + .as_ref() + .map(|mv| mv.to_bytes().clone()) + .unwrap_or_else(|| Bytes::new()); + + let req_slice = req_value + .as_ref() + .map(|rv| rv.to_bytes().clone()) + .unwrap_or_else(|| Bytes::new()); + + // If mock has no requirement, distance is always 0 + if mock_value.is_none() || mock_slice.is_empty() { + return 0; + } + + // If request does not contain any data + if req_value.is_none() || req_slice.is_empty() { + return mock_slice.len(); + } + + // Compare only up to the length of the mock_slice + let compared_window = std::cmp::min(mock_slice.len(), req_slice.len()); + let distance = equal_weight_distance_for( + &mock_slice[..compared_window], + &req_slice[req_slice.len() - compared_window..], + ); + + // if negated, we want to find out how many + if self.negated { + // This is why we need the equal_weight_distance_for function: + // to calculate the distance as the number of differing characters. + return compared_window - distance; + } + + return distance; } } @@ -158,12 +738,13 @@ impl AnyValueComparator { } impl ValueComparator for AnyValueComparator { - fn matches(&self, _: &T, _: &U) -> bool { + fn matches(&self, _: &Option<&T>, _: &Option<&U>) -> bool { true } fn name(&self) -> &str { "any" } + fn distance(&self, _: &Option<&T>, _: &Option<&U>) -> usize { 0 } @@ -172,17 +753,35 @@ impl ValueComparator for AnyValueComparator { // ************************************************************************************************ // FunctionMatchComparator // ************************************************************************************************ -pub struct FunctionMatchesRequestComparator {} +pub struct FunctionMatchesRequestComparator { + negated: bool, +} impl FunctionMatchesRequestComparator { - pub fn new() -> Self { - Self {} + pub fn new(negated: bool) -> Self { + Self { negated } } } -impl ValueComparator for FunctionMatchesRequestComparator { - fn matches(&self, mock_value: &MockMatcherFunction, req_value: &HttpMockRequest) -> bool { - (*mock_value)(req_value) +impl ValueComparator bool + 'static + Sync + Send>, HttpMockRequest> + for FunctionMatchesRequestComparator +{ + fn matches( + &self, + mock_value: &Option<&Arc bool + 'static + Sync + Send>>, + req_value: &Option<&HttpMockRequest>, + ) -> bool { + let result = match (mock_value, req_value) { + (None, _) => true, + (Some(_), None) => self.negated, + (Some(mv), Some(rv)) => mv(rv), + }; + + if self.negated { + !result + } else { + result + } } fn name(&self) -> &str { @@ -191,34 +790,37 @@ impl ValueComparator for FunctionMatchesRe fn distance( &self, - mock_value: &Option<&MockMatcherFunction>, + mock_value: &Option<&Arc bool + 'static + Sync + Send>>, req_value: &Option<&HttpMockRequest>, ) -> usize { - let mock_value = match mock_value { - None => return 0, - Some(v) => v, - }; - let req_value = match req_value { - None => return 1, - Some(v) => v, - }; - match self.matches(mock_value, req_value) { + let result = match self.matches(mock_value, req_value) { true => 0, false => 1, + }; + + if self.negated { + return match result { + 0 => 1, + _ => 0, + }; } + + return result; } } #[cfg(test)] mod test { - use serde_json::json; - - use crate::server::matchers::comparators::{ - AnyValueComparator, JSONContainsMatchComparator, JSONExactMatchComparator, - StringContainsMatchComparator, StringExactMatchComparator, StringRegexMatchComparator, - ValueComparator, + use crate::{ + common::data::HttpMockRegex, + server::matchers::comparators::{ + AnyValueComparator, JSONContainsMatchComparator, JSONExactMatchComparator, + StringContainsComparator, StringEqualsComparator, StringRegexMatchComparator, + ValueComparator, + }, }; - use crate::Regex; + use regex::Regex; + use serde_json::json; fn run_test( comparator: &dyn ValueComparator, @@ -229,7 +831,7 @@ mod test { expected_name: &str, ) { // Act - let match_result = comparator.matches(&v1, &v2); + let match_result = comparator.matches(&Some(&v1), &Some(&v2)); let distance_result = comparator.distance(&Some(&v1), &Some(&v2)); let name_result = comparator.name(); @@ -266,31 +868,31 @@ mod test { #[test] fn json_contains_comparator_match() { run_test( - &JSONContainsMatchComparator::new(), + &JSONContainsMatchComparator::new(false), &json!({ "other" : { "human" : { "surname" : "Griffin" }}}), &json!({ "name" : "Peter", "other" : { "human" : { "surname" : "Griffin" }}}), true, 15, // compute distance even if values match! - "contains", + "includes", ); } #[test] fn json_contains_comparator_no_match() { run_test( - &JSONContainsMatchComparator::new(), + &JSONContainsMatchComparator::new(false), &json!({ "surname" : "Griffin" }), &json!({ "name" : "Peter", "other" : { "human" : { "surname" : "Griffin" }}}), false, 35, // compute distance even if values match! - "contains", + "includes", ); } #[test] fn string_exact_comparator_match() { run_test( - &StringExactMatchComparator::new(true), + &StringEqualsComparator::new(true, false), &"test string".to_string(), &"test string".to_string(), true, @@ -302,7 +904,7 @@ mod test { #[test] fn string_exact_comparator_no_match() { run_test( - &StringExactMatchComparator::new(true), + &StringEqualsComparator::new(true, false), &"test string".to_string(), &"not a test string".to_string(), false, @@ -314,11 +916,11 @@ mod test { #[test] fn string_exact_comparator_case_sensitive_match() { run_test( - &StringExactMatchComparator::new(false), + &StringEqualsComparator::new(false, false), &"TEST string".to_string(), &"test STRING".to_string(), true, - 10, // compute distance even if values match! + 0, "equals", ); } @@ -326,36 +928,36 @@ mod test { #[test] fn string_contains_comparator_match() { run_test( - &StringContainsMatchComparator::new(true), + &StringContainsComparator::new(true, false), &"st st".to_string(), &"test string".to_string(), true, 6, // compute distance even if values match! - "contains", + "includes", ); } #[test] fn string_contains_comparator_no_match() { run_test( - &StringContainsMatchComparator::new(true), + &StringContainsComparator::new(true, false), &"xxx".to_string(), &"yyy".to_string(), false, 3, // compute distance even if values match! - "contains", + "includes", ); } #[test] fn string_contains_comparator_case_sensitive_match() { run_test( - &StringContainsMatchComparator::new(false), + &StringContainsComparator::new(false, false), &"ST st".to_string(), &"test STRING".to_string(), true, - 9, // compute distance even if values match! - "contains", + 6, + "includes", ); } @@ -363,10 +965,10 @@ mod test { fn regex_comparator_match() { run_test( &StringRegexMatchComparator::new(), - &Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap(), + &HttpMockRegex(Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap()), &"2014-01-01".to_string(), true, - 16, // compute distance even if values match! + 0, // compute distance even if values match! "matches regex", ); } @@ -375,10 +977,10 @@ mod test { fn regex_comparator_no_match() { run_test( &StringRegexMatchComparator::new(), - &Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap(), + &HttpMockRegex(Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap()), &"xxx".to_string(), false, - 19, // compute distance even if values match! + 3, "matches regex", ); } diff --git a/src/server/matchers/comparison.rs b/src/server/matchers/comparison.rs new file mode 100644 index 00000000..c0e77a00 --- /dev/null +++ b/src/server/matchers/comparison.rs @@ -0,0 +1,1773 @@ +use crate::common::{data::HttpMockRegex, util::HttpMockBytes}; +use regex::Regex; +use std::{convert::TryInto, ops::Deref}; +use stringmetrics::LevWeights; + +pub fn string_has_prefix( + case_sensitive: bool, + negated: bool, + mock_value: &Option<&String>, + req_value: &Option<&String>, +) -> bool { + let result = match (mock_value, req_value) { + (None, _) => return true, + (Some(_), None) => return negated, + (Some(mv), Some(rv)) => { + if rv.len() < mv.len() { + return false; + } + + match case_sensitive { + true => rv.starts_with(mv.as_str()), + false => rv.to_lowercase().starts_with(&mv.to_lowercase()), + } + } + }; + + if negated { + !result + } else { + result + } +} + +#[cfg(test)] +mod string_has_prefix_tests { + use super::*; + + #[test] + fn test_case_sensitive_prefix_match() { + let mock_value = "Hello".to_string(); + let req_value = "Hello World".to_string(); + assert!(string_has_prefix( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + + let mock_value = "hello".to_string(); + let req_value = "Hello World".to_string(); + assert!(!string_has_prefix( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_case_insensitive_prefix_match() { + let mock_value = "hello".to_string(); + let req_value = "Hello World".to_string(); + assert!(string_has_prefix( + false, + false, + &Some(&mock_value), + &Some(&req_value), + )); + + let mock_value = "HELLO".to_string(); + let req_value = "Hello World".to_string(); + assert!(string_has_prefix( + false, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_negated_prefix_match() { + let mock_value = "Hello".to_string(); + let req_value = "Hello World".to_string(); + assert!(!string_has_prefix( + true, + true, + &Some(&mock_value), + &Some(&req_value), + )); + + let mock_value = "hello".to_string(); + let req_value = "Hello World".to_string(); + assert!(string_has_prefix( + true, + true, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_prefix_too_short() { + let mock_value = "Hello World".to_string(); + let req_value = "Hello".to_string(); + assert!(!string_has_prefix( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_exact_match() { + let mock_value = "Hello".to_string(); + let req_value = "Hello".to_string(); + assert!(string_has_prefix( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_empty_mock_value() { + let mock_value = "".to_string(); + let req_value = "Hello World".to_string(); + assert!(string_has_prefix( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_empty_req_value() { + let mock_value = "Hello".to_string(); + let req_value = "".to_string(); + assert!(!string_has_prefix( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_both_empty() { + let mock_value = "".to_string(); + let req_value = "".to_string(); + assert!(string_has_prefix( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } +} + +pub fn distance_for_prefix( + case_sensitive: bool, + negated: bool, + mock_value: &Option<&String>, + req_value: &Option<&String>, +) -> usize { + if mock_value.map_or(0, |v| v.len()) == 0 { + return 0; + } + + let mock_slice = mock_value.as_deref(); + let mock_slice_len = mock_slice.map_or(0, |v| v.len()); + + let req_slice = req_value + .as_deref() + .map(|s| &s[..mock_slice_len.min(s.len())]); + + return distance_for_substring( + case_sensitive, + negated, + &mock_slice.map(|v| v.as_str()), + &req_slice, + ); +} + +#[cfg(test)] +mod distance_for_prefix_tests { + use super::*; + + #[test] + fn test_distance_for_prefix_case_sensitive() { + let mock_value_str = "hello".to_string(); + let req_value_str = "hello world".to_string(); + let mock_value = Some(&mock_value_str); + let req_value = Some(&req_value_str); + assert_eq!(distance_for_prefix(true, false, &mock_value, &req_value), 0); + } + + #[test] + fn test_distance_for_prefix_case_insensitive() { + let mock_value_str = "Hello".to_string(); + let req_value_str = "hello world".to_string(); + let mock_value = Some(&mock_value_str); + let req_value = Some(&req_value_str); + assert_eq!( + distance_for_prefix(false, false, &mock_value, &req_value), + 0 + ); + } + + #[test] + fn test_distance_for_prefix_negated() { + let mock_value_str = "hello".to_string(); + let req_value_str = "hello world".to_string(); + let mock_value = Some(&mock_value_str); + let req_value = Some(&req_value_str); + assert_eq!(distance_for_prefix(true, true, &mock_value, &req_value), 5); + } + + #[test] + fn test_distance_for_prefix_no_match() { + let mock_value_str = "world".to_string(); + let req_value_str = "hello world".to_string(); + let mock_value = Some(&mock_value_str); + let req_value = Some(&req_value_str); + assert!(distance_for_prefix(true, false, &mock_value, &req_value) > 0); + } + + #[test] + fn test_distance_for_prefix_partial_match() { + let mock_value_str = "hell".to_string(); + let req_value_str = "hello world".to_string(); + let mock_value = Some(&mock_value_str); + let req_value = Some(&req_value_str); + assert_eq!(distance_for_prefix(true, false, &mock_value, &req_value), 0); + } + + #[test] + fn test_distance_for_prefix_empty_mock_value() { + let mock_value_str = "".to_string(); + let req_value_str = "hello world".to_string(); + let mock_value = Some(&mock_value_str); + let req_value = Some(&req_value_str); + assert_eq!(distance_for_prefix(true, false, &mock_value, &req_value), 0); + } + + #[test] + fn test_distance_for_prefix_empty_req_value() { + let mock_value_str = "hello".to_string(); + let req_value_str = "".to_string(); + let mock_value = Some(&mock_value_str); + let req_value = Some(&req_value_str); + assert_eq!(distance_for_prefix(true, false, &mock_value, &req_value), 5); + } + + #[test] + fn test_distance_for_prefix_both_empty() { + let mock_value_str = "".to_string(); + let req_value_str = "".to_string(); + let mock_value = Some(&mock_value_str); + let req_value = Some(&req_value_str); + assert_eq!(distance_for_prefix(true, false, &mock_value, &req_value), 0); + } + + #[test] + fn test_distance_for_prefix_none_mock_value() { + let req_value_str = "hello world".to_string(); + let mock_value: Option<&String> = None; + let req_value = Some(&req_value_str); + assert_eq!(distance_for_prefix(true, false, &mock_value, &req_value), 0); + } + + #[test] + fn test_distance_for_prefix_none_req_value() { + let mock_value_str = "hello".to_string(); + let mock_value = Some(&mock_value_str); + let req_value: Option<&String> = None; + assert_eq!(distance_for_prefix(true, false, &mock_value, &req_value), 5); + } + + #[test] + fn test_distance_for_prefix_none_both() { + let mock_value: Option<&String> = None; + let req_value: Option<&String> = None; + assert_eq!(distance_for_prefix(true, false, &mock_value, &req_value), 0); + } +} + +pub fn string_has_suffix( + case_sensitive: bool, + negated: bool, + mock_value: &Option<&String>, + req_value: &Option<&String>, +) -> bool { + let result = match (mock_value, req_value) { + (None, _) => return true, + (Some(_), None) => return negated, + (Some(mv), Some(rv)) => { + if rv.len() < mv.len() { + return false; + } + + match case_sensitive { + true => rv.ends_with(mv.as_str()), + false => rv.to_lowercase().ends_with(&mv.to_lowercase()), + } + } + }; + + if negated { + !result + } else { + result + } +} + +#[cfg(test)] +mod string_has_suffix_tests { + use super::*; + + #[test] + fn test_case_sensitive_suffix_match() { + let mock_value = "world".to_string(); + let req_value = "hello world".to_string(); + assert!(string_has_suffix( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + let mock_value = "World".to_string(); + assert!(!string_has_suffix( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_case_insensitive_suffix_match() { + let mock_value = "world".to_string(); + let req_value = "hello world".to_string(); + assert!(string_has_suffix( + false, + false, + &Some(&mock_value), + &Some(&req_value), + )); + let mock_value = "World".to_string(); + assert!(string_has_suffix( + false, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_negated_suffix_match() { + let mock_value = "world".to_string(); + let req_value = "hello world".to_string(); + assert!(!string_has_suffix( + true, + true, + &Some(&mock_value), + &Some(&req_value), + )); + let mock_value = "World".to_string(); + assert!(string_has_suffix( + true, + true, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_suffix_too_short() { + let mock_value = "hello world".to_string(); + let req_value = "world".to_string(); + assert!(!string_has_suffix( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_exact_match_case_sensitive() { + let mock_value = "hello".to_string(); + let req_value = "hello".to_string(); + assert!(string_has_suffix( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_exact_match_case_insensitive() { + let mock_value = "Hello".to_string(); + let req_value = "hello".to_string(); + assert!(string_has_suffix( + false, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_no_match_case_sensitive() { + let mock_value = "world".to_string(); + let req_value = "hello".to_string(); + assert!(!string_has_suffix( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_no_match_case_insensitive() { + let mock_value = "World".to_string(); + let req_value = "hello".to_string(); + assert!(!string_has_suffix( + false, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_empty_mock_value() { + let mock_value = "".to_string(); + let req_value = "hello world".to_string(); + assert!(string_has_suffix( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_empty_req_value() { + let mock_value = "hello".to_string(); + let req_value = "".to_string(); + assert!(!string_has_suffix( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_both_empty() { + let mock_value = "".to_string(); + let req_value = "".to_string(); + assert!(string_has_suffix( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } +} + +/// Calculates the distance between the suffix of a given string (`req_value`) and the expected suffix (`mock_value`). +/// +/// # Arguments +/// +/// * `case_sensitive` - A boolean indicating if the comparison should be case-sensitive. +/// * `negated` - A boolean indicating if the result should be negated. +/// * `mock_value` - An optional reference to the expected suffix. +/// * `req_value` - An optional reference to the string to be checked. +/// +/// # Returns +/// +/// A usize representing the distance between the suffix of `req_value` and `mock_value`. +/// If `negated` is true, the result will be the length of `mock_value` minus the calculated distance. +pub fn distance_for_suffix( + case_sensitive: bool, + negated: bool, + mock_value: &Option<&String>, + req_value: &Option<&String>, +) -> usize { + if mock_value.map_or(0, |v| v.len()) == 0 { + return 0; + } + + let mock_slice = mock_value.as_deref(); + let mock_slice_len = mock_slice.map_or(0, |v| v.len()); + + let req_slice = req_value + .as_deref() + .map(|s| &s[..mock_slice_len.min(s.len())]); + + return distance_for_substring( + case_sensitive, + negated, + &mock_slice.map(|v| v.as_str()), + &req_slice, + ); +} + +pub fn string_contains( + case_sensitive: bool, + negated: bool, + mock_value: &Option<&String>, + req_value: &Option<&String>, +) -> bool { + let result = match (mock_value, req_value) { + (None, _) => return true, + (Some(_), None) => return negated, + (Some(mv), Some(rv)) => match case_sensitive { + true => rv.contains(mv.as_str()), + false => rv.to_lowercase().contains(&mv.to_lowercase()), + }, + }; + + if negated { + !result + } else { + result + } +} + +#[cfg(test)] +mod string_contains_tests { + use super::*; + + #[test] + fn test_case_sensitive_contains() { + let mock_value = "world".to_string(); + let req_value = "hello world".to_string(); + assert!(string_contains( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + + let mock_value = "World".to_string(); + assert!(!string_contains( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_case_insensitive_contains() { + let mock_value = "world".to_string(); + let req_value = "hello world".to_string(); + assert!(string_contains( + false, + false, + &Some(&mock_value), + &Some(&req_value), + )); + + let mock_value = "World".to_string(); + assert!(string_contains( + false, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_negated_contains() { + let mock_value = "world".to_string(); + let req_value = "hello world".to_string(); + assert!(!string_contains( + true, + true, + &Some(&mock_value), + &Some(&req_value), + )); + + let mock_value = "World".to_string(); + assert!(string_contains( + true, + true, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_contains_substring() { + let mock_value = "lo wo".to_string(); + let req_value = "hello world".to_string(); + assert!(string_contains( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_no_match_contains() { + let mock_value = "test".to_string(); + let req_value = "hello world".to_string(); + assert!(!string_contains( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_empty_mock_value() { + let mock_value = "".to_string(); + let req_value = "hello world".to_string(); + assert!(string_contains( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_empty_req_value() { + let mock_value = "hello".to_string(); + let req_value = "".to_string(); + assert!(!string_contains( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } + + #[test] + fn test_both_empty() { + let mock_value = "".to_string(); + let req_value = "".to_string(); + assert!(string_contains( + true, + false, + &Some(&mock_value), + &Some(&req_value), + )); + } +} + +pub fn distance_for_substring( + case_sensitive: bool, + negated: bool, + mock_value: &Option, + req_value: &Option, +) -> usize +where + T: Deref + AsRef, +{ + if mock_value.is_none() { + return 0; + } + + let mock_slice = mock_value.as_deref().unwrap_or(""); + let req_slice = req_value.as_deref().unwrap_or(""); + + let lcs_length = longest_common_substring(case_sensitive, mock_slice, req_slice); + + if negated { + lcs_length + } else { + std::cmp::max(mock_slice.len(), req_slice.len()) - lcs_length + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_distance_for_substring_case_sensitive_match() { + let mock_value = Some("Hello"); + let req_value = Some("Hello World"); + assert_eq!( + distance_for_substring(true, false, &mock_value, &req_value), + 6 + ); + } + + #[test] + fn test_distance_for_substring_case_sensitive_no_match() { + let mock_value = Some("Hello"); + let req_value = Some("World"); + assert_eq!( + distance_for_substring(true, false, &mock_value, &req_value), + 4 + ); + } + + #[test] + fn test_distance_for_substring_case_insensitive_match() { + let mock_value = Some("hello"); + let req_value = Some("Hello World"); + assert_eq!( + distance_for_substring(false, false, &mock_value, &req_value), + 6 + ); + } + + #[test] + fn test_distance_for_substring_case_insensitive_no_match() { + let mock_value = Some("hello"); + let req_value = Some("WORLD"); + assert_eq!( + distance_for_substring(false, false, &mock_value, &req_value), + 4 + ); + } + + #[test] + fn test_distance_for_substring_negated_match() { + let mock_value = Some("Hello"); + let req_value = Some("Hello World"); + assert_eq!( + distance_for_substring(true, true, &mock_value, &req_value), + 5 + ); + } + + #[test] + fn test_distance_for_substring_negated_no_match() { + let mock_value = Some("Hello"); + let req_value = Some("World"); + assert_eq!( + distance_for_substring(true, true, &mock_value, &req_value), + 1 + ); + } + + #[test] + fn test_distance_for_substring_empty_mock_value() { + let mock_value = Some(""); + let req_value = Some("Hello"); + assert_eq!( + distance_for_substring(true, false, &mock_value, &req_value), + 5 + ); + } + + #[test] + fn test_distance_for_substring_empty_req_value() { + let mock_value = Some("Hello"); + let req_value = Some(""); + assert_eq!( + distance_for_substring(true, false, &mock_value, &req_value), + 5 + ); + } + + #[test] + fn test_distance_for_substring_both_empty() { + let mock_value = Some(""); + let req_value = Some(""); + assert_eq!( + distance_for_substring(true, false, &mock_value, &req_value), + 0 + ); + } + + #[test] + fn test_distance_for_substring_none_mock_value() { + let req_value = Some("hello"); + let mock_value: Option<&str> = None; + assert_eq!( + distance_for_substring(true, false, &mock_value, &req_value), + 0 + ); + } + + #[test] + fn test_distance_for_substring_none_req_value() { + let mock_value = Some("hello"); + let req_value: Option<&str> = None; + assert_eq!( + distance_for_substring(true, false, &mock_value, &req_value), + 5 + ); + } + + #[test] + fn test_distance_for_substring_none_both() { + let mock_value: Option<&str> = None; + let req_value: Option<&str> = None; + assert_eq!( + distance_for_substring(true, false, &mock_value, &req_value), + 0 + ); + } +} + +pub fn longest_common_substring(case_sensitive: bool, s1: &str, s2: &str) -> usize { + let (long_s, short_s) = if s1.len() < s2.len() { + (s2, s1) + } else { + (s1, s2) + }; + + let mut previous = vec![0; short_s.chars().count() + 1]; + let mut current = vec![0; short_s.chars().count() + 1]; + let mut longest = 0; + + for (i, long_char) in long_s.chars().enumerate() { + for (j, short_char) in short_s.chars().enumerate() { + let long_char = if case_sensitive { + long_char + } else { + long_char.to_lowercase().next().unwrap() + }; + + let short_char = if case_sensitive { + short_char + } else { + short_char.to_lowercase().next().unwrap() + }; + + if long_char == short_char { + current[j + 1] = previous[j] + 1; + if current[j + 1] > longest { + longest = current[j + 1]; + } + } else { + current[j + 1] = 0; + } + } + std::mem::swap(&mut previous, &mut current); + } + longest +} + +#[cfg(test)] +mod longest_common_substring_tests { + use super::*; + + #[test] + fn test_case_sensitive_match() { + let s1 = "abcdef"; + let s2 = "zabcy"; + assert_eq!(longest_common_substring(true, s1, s2), 3); + + let s1 = "abcdef"; + let s2 = "zabCY"; + assert_eq!(longest_common_substring(true, s1, s2), 2); + + let s1 = "abcDEF"; + let s2 = "zabcy"; + assert_eq!(longest_common_substring(true, s1, s2), 3); + } + + #[test] + fn test_case_insensitive_match() { + let s1 = "abcdef"; + let s2 = "zabcy"; + assert_eq!(longest_common_substring(false, s1, s2), 3); + + let s1 = "abcDEF"; + let s2 = "zabcY"; + assert_eq!(longest_common_substring(false, s1, s2), 3); + + let s1 = "ABCdef"; + let s2 = "ZABcy"; + assert_eq!(longest_common_substring(false, s1, s2), 3); + } + + #[test] + fn test_no_common_substring() { + let s1 = "abc"; + let s2 = "xyz"; + assert_eq!(longest_common_substring(true, s1, s2), 0); + assert_eq!(longest_common_substring(false, s1, s2), 0); + } + + #[test] + fn test_empty_strings() { + let s1 = ""; + let s2 = "abcdef"; + assert_eq!(longest_common_substring(true, s1, s2), 0); + assert_eq!(longest_common_substring(false, s1, s2), 0); + + let s1 = "abcdef"; + let s2 = ""; + assert_eq!(longest_common_substring(true, s1, s2), 0); + assert_eq!(longest_common_substring(false, s1, s2), 0); + + let s1 = ""; + let s2 = ""; + assert_eq!(longest_common_substring(true, s1, s2), 0); + assert_eq!(longest_common_substring(false, s1, s2), 0); + } + + #[test] + fn test_full_string_match() { + let s1 = "abcdef"; + let s2 = "abcdef"; + assert_eq!(longest_common_substring(true, s1, s2), 6); + assert_eq!(longest_common_substring(false, s1, s2), 6); + } + + #[test] + fn test_utf8_support() { + let s1 = "你好世界"; + let s2 = "世界你好"; + assert_eq!(longest_common_substring(true, s1, s2), 2); + + let s1 = "你好世界"; + let s2 = "世界HELLO"; + assert_eq!(longest_common_substring(false, s1, s2), 2); + } +} + +pub fn string_equals( + case_sensitive: bool, + negated: bool, + mock_value: &Option<&String>, + req_value: &Option<&String>, +) -> bool { + let result = match (mock_value, req_value) { + (None, _) => return true, + (Some(_), None) => return negated, + (Some(mv), Some(rv)) => match case_sensitive { + true => mv.eq(rv), + false => mv.to_lowercase().eq(&rv.to_lowercase()), + }, + }; + + if negated { + !result + } else { + result + } +} + +pub fn hostname_equals( + negated: bool, + mock_value: &Option<&String>, + req_value: &Option<&String>, +) -> bool { + if let (Some(mv), Some(rv)) = (mock_value, req_value) { + let mv_is = mv.eq_ignore_ascii_case("localhost") || mv.eq_ignore_ascii_case("127.0.0.1"); + let rv_is = rv.eq_ignore_ascii_case("localhost") || rv.eq_ignore_ascii_case("127.0.0.1"); + + if mv_is == rv_is { + return !negated; + } + } + + return string_equals(false, negated, mock_value, req_value); +} + +/// Computes the distance between two optional strings (`mock_value` and `req_value`), +/// with optional case sensitivity and negation. +/// +/// # Arguments +/// +/// * `case_sensitive` - A boolean indicating if the comparison should be case-sensitive. +/// * `negated` - A boolean indicating if the result should be negated. +/// * `mock_value` - An optional reference to the first string to compare. +/// * `req_value` - An optional reference to the second string to compare. +/// +/// # Returns +/// +/// A `usize` representing the distance between `mock_value` and `req_value`, +/// taking into account case sensitivity and negation. +pub fn string_distance( + case_sensitive: bool, + negated: bool, + mock_value: &Option<&String>, + req_value: &Option<&String>, +) -> usize { + if mock_value.is_none() { + return 0; + } + + let mock_slice = mock_value.as_deref().map_or("", |s| s.as_str()); + let req_slice = req_value.as_deref().map_or("", |s| s.as_str()); + + let (mock_slice, req_slice) = if case_sensitive { + (mock_slice.to_string(), req_slice.to_string()) + } else { + (mock_slice.to_lowercase(), req_slice.to_lowercase()) + }; + + let distance = equal_weight_distance_for(mock_slice.as_bytes(), req_slice.as_bytes()); + + if negated { + std::cmp::max(mock_slice.len(), req_slice.len()) - distance + } else { + distance + } +} + +#[cfg(test)] +mod string_distance_tests { + use super::*; + + #[test] + fn test_string_distance_case_sensitive() { + let mock_value_str = "Hello".to_string(); + let req_value_str = "Hello".to_string(); + let mock_value = Some(&mock_value_str); + let req_value = Some(&req_value_str); + assert_eq!(string_distance(true, false, &mock_value, &req_value), 0); + + let mock_value_str = "Hello".to_string(); + let req_value_str = "hello".to_string(); + let mock_value = Some(&mock_value_str); + let req_value = Some(&req_value_str); + assert_eq!(string_distance(true, false, &mock_value, &req_value), 1); + } + + #[test] + fn test_string_distance_case_insensitive() { + let mock_value_str = "Hello".to_string(); + let req_value_str = "hello".to_string(); + let mock_value = Some(&mock_value_str); + let req_value = Some(&req_value_str); + assert_eq!(string_distance(false, false, &mock_value, &req_value), 0); + + let mock_value_str = "HELLO".to_string(); + let req_value_str = "hello".to_string(); + let mock_value = Some(&mock_value_str); + let req_value = Some(&req_value_str); + assert_eq!(string_distance(false, false, &mock_value, &req_value), 0); + } + + #[test] + fn test_string_distance_negated() { + let mock_value_str = "Hello".to_string(); + let req_value_str = "Hello".to_string(); + let mock_value = Some(&mock_value_str); + let req_value = Some(&req_value_str); + assert_eq!(string_distance(true, true, &mock_value, &req_value), 5); + + let mock_value_str = "Hello".to_string(); + let req_value_str = "hello".to_string(); + let mock_value = Some(&mock_value_str); + let req_value = Some(&req_value_str); + assert_eq!(string_distance(true, true, &mock_value, &req_value), 4); + } + + #[test] + fn test_string_distance_no_match() { + let mock_value_str = "Hello".to_string(); + let req_value_str = "World".to_string(); + let mock_value = Some(&mock_value_str); + let req_value = Some(&req_value_str); + assert_eq!(string_distance(true, false, &mock_value, &req_value), 4); + } + + #[test] + fn test_string_distance_empty_mock_value() { + let mock_value_str = "".to_string(); + let req_value_str = "hello".to_string(); + let mock_value = Some(&mock_value_str); + let req_value = Some(&req_value_str); + assert_eq!(string_distance(true, false, &mock_value, &req_value), 5); + } + + #[test] + fn test_string_distance_empty_req_value() { + let mock_value_str = "hello".to_string(); + let req_value_str = "".to_string(); + let mock_value = Some(&mock_value_str); + let req_value = Some(&req_value_str); + assert_eq!(string_distance(true, false, &mock_value, &req_value), 5); + } + + #[test] + fn test_string_distance_both_empty() { + let mock_value_str = "".to_string(); + let req_value_str = "".to_string(); + let mock_value = Some(&mock_value_str); + let req_value = Some(&req_value_str); + assert_eq!(string_distance(true, false, &mock_value, &req_value), 0); + } + + #[test] + fn test_string_distance_none_mock_value() { + let req_value_str = "hello".to_string(); + let mock_value: Option<&String> = None; + let req_value = Some(&req_value_str); + assert_eq!(string_distance(true, false, &mock_value, &req_value), 0); + } +} + +pub fn is_lowercase(s: &str) -> bool { + s.chars().all(|c| c.is_lowercase() || !c.is_alphabetic()) +} + +// ************************************************************************************************* +// Helper functions +// ************************************************************************************************* +pub fn distance_for(expected: &[T], actual: &[T]) -> usize +where + T: PartialEq + Sized, +{ + stringmetrics::levenshtein_limit_iter(expected.iter(), actual.iter(), u32::MAX) as usize +} + +pub fn equal_weight_distance_for(expected: &[T], actual: &[T]) -> usize +where + T: PartialEq + Sized, +{ + stringmetrics::try_levenshtein_weight_iter( + expected.iter(), + actual.iter(), + u32::MAX, + &LevWeights { + insertion: 1, + deletion: 1, + substitution: 1, + }, + ) + // Option=None is only returned in case limit is maxed out here it realistically can't + .expect("character limit exceeded") as usize +} + +pub fn regex_unmatched_length(text: &str, re: &HttpMockRegex) -> usize { + let mut last_end = 0; + let mut total_unmatched_length = 0; + + // Iterate through all matches and sum the lengths of unmatched parts + for mat in re.0.find_iter(text) { + if last_end != mat.start() { + total_unmatched_length += mat.start() - last_end; + } + last_end = mat.end(); + } + + // Add any characters after the last match to the total + if last_end < text.len() { + total_unmatched_length += text.len() - last_end; + } + + total_unmatched_length +} + +pub fn integer_equals( + negated: bool, + mock_value: &Option<&T>, + req_value: &Option<&T>, +) -> bool { + let result = match (mock_value, req_value) { + (None, _) => return true, + (Some(_), None) => return negated, + (Some(mv), Some(rv)) => mv == rv, + }; + + if negated { + !result + } else { + result + } +} +#[cfg(test)] +mod usize_equals_tests { + use super::*; + + #[test] + fn test_usize_equals_equal_values_not_negated() { + assert_eq!(true, integer_equals(false, &Some(&10), &Some(&10))); + } + + #[test] + fn test_usize_equals_unequal_values_not_negated() { + assert_eq!(false, integer_equals(false, &Some(&10), &Some(&20))); + } + + #[test] + fn test_usize_equals_equal_values_negated() { + assert_eq!(false, integer_equals(true, &Some(&10), &Some(&10))); + } + + #[test] + fn test_usize_equals_unequal_values_negated() { + assert_eq!(true, integer_equals(true, &Some(&10), &Some(&20))); + } + + #[test] + fn test_usize_equals_mock_value_none_not_negated() { + assert_eq!(true, integer_equals(false, &None, &Some(&10))); + } + + #[test] + fn test_usize_equals_req_value_none_not_negated() { + assert_eq!(false, integer_equals(false, &Some(&10), &None)); + } + + #[test] + fn test_usize_equals_both_none_not_negated() { + assert_eq!(true, integer_equals::(false, &None, &None)); + } + + #[test] + fn test_usize_equals_mock_value_none_negated() { + assert_eq!(true, integer_equals(true, &None, &Some(&10))); + } + + #[test] + fn test_usize_equals_req_value_none_negated() { + assert_eq!(true, integer_equals(true, &Some(&10), &None)); + } + + #[test] + fn test_usize_equals_both_none_negated() { + assert_eq!(true, integer_equals::(true, &None, &None)); + } +} + +pub fn bytes_equal( + negated: bool, + mock_value: &Option<&HttpMockBytes>, + req_value: &Option<&HttpMockBytes>, +) -> bool { + let result = match (mock_value, req_value) { + (None, _) => return true, + (Some(_), None) => return negated, + (Some(mv), Some(rv)) => mv == rv, + }; + + if negated { + !result + } else { + result + } +} + +pub fn bytes_includes( + negated: bool, + mock_value: &Option<&HttpMockBytes>, + req_value: &Option<&HttpMockBytes>, +) -> bool { + let result = match (mock_value, req_value) { + (None, _) => return true, + (Some(_), None) => return negated, + (Some(mv), Some(rv)) => { + // Convert both Bytes into slices for comparison + let mock_slice = mv.to_bytes(); + let req_slice = rv.to_bytes(); + + if mock_slice.is_empty() { + return true; + } + + // Check if the request slice contains the mock slice + req_slice + .windows(mock_slice.len()) + .any(|window| window == mock_slice) + } + }; + + if negated { + !result + } else { + result + } +} + +#[cfg(test)] +mod bytes_includes_test { + use crate::{common::util::HttpMockBytes, server::matchers::comparison::bytes_includes}; + + #[test] + fn test_bytes_includes() { + assert_eq!( + bytes_includes( + false, + &Some(&HttpMockBytes::from(bytes::Bytes::from(" b\n c"))), + &Some(&HttpMockBytes::from(bytes::Bytes::from( + "a b\n c \ncd ef" + ))), + ), + true + ); + } +} + +pub fn bytes_prefix( + negated: bool, + mock_value: &Option<&HttpMockBytes>, + req_value: &Option<&HttpMockBytes>, +) -> bool { + let result = match (mock_value, req_value) { + (None, _) => return true, + (Some(_), None) => return negated, + (Some(mv), Some(rv)) => { + // Convert both Bytes into slices for comparison + let mock_slice = mv.to_bytes(); + let req_slice = rv.to_bytes(); + + // Check if the request slice starts with the mock slice + req_slice.starts_with(&mock_slice) + } + }; + + if negated { + !result + } else { + result + } +} + +pub fn bytes_suffix( + negated: bool, + mock_value: &Option<&HttpMockBytes>, + req_value: &Option<&HttpMockBytes>, +) -> bool { + let result = match (mock_value, req_value) { + (None, _) => return true, + (Some(_), None) => return negated, + (Some(mv), Some(rv)) => { + // Convert both Bytes into slices for comparison + let mock_slice = mv.to_bytes(); + let req_slice = rv.to_bytes(); + + // Check if the request slice ends with the mock slice + req_slice.ends_with(&mock_slice) + } + }; + + if negated { + !result + } else { + result + } +} + +/// Calculates the "distance" between two optional numeric values. +/// +/// The distance is defined as the absolute difference between the two values +/// if both are present. If one is present and the other is not, the distance +/// is the present value, unless it is zero, in which case the distance is 1. +/// If both values are `None`, the distance is 0. +/// +/// # Arguments +/// +/// * `mock_value` - An optional reference to a u16 that may or may not be present. +/// * `req_value` - An optional reference to a u16 that may or may not be present. +/// +/// # Returns +/// +/// Returns a usize representing the distance as defined above. +/// +pub fn distance_for_usize(expected: &Option<&T>, actual: &Option<&T>) -> usize +where + T: TryInto + Copy, +{ + let mock_size = expected.map_or(0, |&v| v.try_into().unwrap_or(0)); + let req_size = actual.map_or(0, |&v| v.try_into().unwrap_or(0)); + + match (expected, actual) { + (Some(&mv), Some(&rv)) => { + let diff = if mock_size > req_size { + mock_size - req_size + } else { + req_size - mock_size + }; + diff + } + (Some(&mv), None) | (None, Some(&mv)) => { + if mock_size == 0 { + 1 + } else { + mock_size + } + } + (None, None) => 0, + // Redundant pattern, logically unnecessary but included for completeness + } +} + +#[cfg(test)] +mod distance_for_usize_test { + use crate::server::matchers::comparison::distance_for_usize; + + #[test] + fn tree_map_fully_contains_other() { + assert_eq!(distance_for_usize::(&Some(&4), &None), 4); + assert_eq!(distance_for_usize::(&Some(&0), &None), 1); + assert_eq!(distance_for_usize::(&Some(&5), &Some(&3)), 2); + assert_eq!(distance_for_usize::(&None, &None), 0); + } +} + +pub fn string_matches_regex( + negated: bool, + case_sensitive: bool, + mock_value: &Option<&HttpMockRegex>, + req_value: &Option<&String>, +) -> bool { + let result = match (mock_value, req_value) { + (None, _) => return true, + (Some(_), None) => return negated, + (Some(mv), Some(rv)) => { + if case_sensitive { + mv.0.is_match(rv) + } else { + let case_insensitive_str = mv.0.as_str().to_lowercase(); + let case_insensitive_regex = Regex::new(&case_insensitive_str).unwrap(); + case_insensitive_regex.is_match(&rv.to_lowercase()) + } + } + }; + + match negated { + true => !result, + false => result, + } +} + +#[cfg(test)] +mod string_matches_regex_tests { + use super::*; + use crate::common::data::HttpMockRegex; + + #[test] + fn test_string_matches_regex() { + let pattern = HttpMockRegex(Regex::new(r"^Hello.*").unwrap()); + + let req_value = "Hello, world!".to_string(); + assert_eq!( + string_matches_regex(false, true, &Some(&pattern), &Some(&req_value)), + true + ); + + let req_value = "Goodbye, world!".to_string(); + assert_eq!( + string_matches_regex(false, true, &Some(&pattern), &Some(&req_value)), + false + ); + } + + #[test] + fn test_string_matches_regex_negated() { + let pattern = HttpMockRegex(Regex::new(r"^Hello.*").unwrap()); + + let req_value = "Hello, world!".to_string(); + assert_eq!( + string_matches_regex(true, true, &Some(&pattern), &Some(&req_value)), + false + ); + + let req_value = "Goodbye, world!".to_string(); + assert_eq!( + string_matches_regex(true, true, &Some(&pattern), &Some(&req_value)), + true + ); + } + + #[test] + fn test_string_matches_regex_empty_pattern() { + let pattern = HttpMockRegex(Regex::new(r"").unwrap()); + + let req_value = "Anything".to_string(); + assert_eq!( + string_matches_regex(false, true, &Some(&pattern), &Some(&req_value)), + true + ); + } + + #[test] + fn test_string_matches_regex_empty_request_value() { + let pattern = HttpMockRegex(Regex::new(r"^Hello.*").unwrap()); + + let req_value = "".to_string(); + assert_eq!( + string_matches_regex(false, true, &Some(&pattern), &Some(&req_value)), + false + ); + } + + #[test] + fn test_string_matches_regex_empty_pattern_and_request_value() { + let pattern = HttpMockRegex(Regex::new(r"").unwrap()); + + let req_value = "".to_string(); + assert_eq!( + string_matches_regex(false, true, &Some(&pattern), &Some(&req_value)), + true + ); + } + + #[test] + fn test_string_matches_regex_none_pattern() { + let req_value = "Hello, world!".to_string(); + assert_eq!( + string_matches_regex(false, true, &None, &Some(&req_value)), + true + ); + } + + #[test] + fn test_string_matches_regex_none_request_value() { + let pattern = HttpMockRegex(Regex::new(r"^Hello.*").unwrap()); + + assert_eq!( + string_matches_regex(false, true, &Some(&pattern), &None), + false + ); + } + + #[test] + fn test_string_matches_regex_none_pattern_and_request_value() { + assert_eq!(string_matches_regex(false, true, &None, &None), true); + } + + #[test] + fn test_string_matches_regex_none_pattern_negated() { + let req_value = "Hello, world!".to_string(); + assert_eq!( + string_matches_regex(true, true, &None, &Some(&req_value)), + true + ); + } + + #[test] + fn test_string_matches_regex_none_request_value_negated() { + let pattern = HttpMockRegex(Regex::new(r"^Hello.*").unwrap()); + + assert_eq!( + string_matches_regex(true, true, &Some(&pattern), &None), + true + ); + } + + #[test] + fn test_string_matches_regex_none_pattern_and_request_value_negated() { + assert_eq!(string_matches_regex(true, true, &None, &None), true); + } + + #[test] + fn test_string_matches_regex_pattern_with_special_chars() { + let pattern = HttpMockRegex(Regex::new(r"^\d{3}-\d{2}-\d{4}$").unwrap()); + + let req_value = "123-45-6789".to_string(); + assert_eq!( + string_matches_regex(false, true, &Some(&pattern), &Some(&req_value)), + true + ); + + let req_value = "123-45-678".to_string(); + assert_eq!( + string_matches_regex(false, true, &Some(&pattern), &Some(&req_value)), + false + ); + } + + #[test] + fn test_string_matches_regex_case_insensitive() { + let pattern = HttpMockRegex(Regex::new(r"(?i)^hello.*").unwrap()); + + let req_value = "Hello, world!".to_string(); + assert_eq!( + string_matches_regex(false, false, &Some(&pattern), &Some(&req_value)), + true + ); + + let req_value = "hello, world!".to_string(); + assert_eq!( + string_matches_regex(false, false, &Some(&pattern), &Some(&req_value)), + true + ); + + let req_value = "HELLO, WORLD!".to_string(); + assert_eq!( + string_matches_regex(false, false, &Some(&pattern), &Some(&req_value)), + true + ); + } +} +/// Computes the distance between a given string and a regex pattern based on whether the match is negated or not. +/// +/// # Arguments +/// * `negated` - A boolean indicating whether the match should be negated. +/// * `mock_value` - An optional reference to a `Pattern` containing the regex to match against. +/// * `req_value` - An optional reference to a `String` representing the input string to be matched. +/// +/// # Returns +/// * `usize` - The computed distance. In the negated case, it returns the number of characters that did match the regex. +/// In the non-negated case, it returns the number of characters that did not match the regex. +pub fn regex_string_distance( + negated: bool, + case_sensitive: bool, + mock_value: &Option<&HttpMockRegex>, + req_value: &Option<&String>, +) -> usize { + if mock_value.is_none() { + return 0; + } + + if req_value.is_none() || req_value.unwrap().is_empty() { + let matches = string_matches_regex(negated, case_sensitive, mock_value, req_value); + return match matches { + true => 0, + false => mock_value.unwrap().0.as_str().len(), + }; + } + + let rv = req_value.map_or("", |s| s.as_str()); + let unmatched_len = regex_unmatched_length(rv, &mock_value.unwrap()); + + return match negated { + true => rv.len() - unmatched_len, + false => unmatched_len, + }; +} + +#[cfg(test)] +mod regex_string_distance_tests { + use super::*; + use regex::Regex; + + #[test] + fn test_non_negated_full_match() { + let pattern = HttpMockRegex(Regex::new("a+").unwrap()); + let mock_value = Some(&pattern); + let req_value_str = String::from("aaaa"); + let req_value = Some(&req_value_str); + assert_eq!( + regex_string_distance(false, true, &mock_value, &req_value), + 0 + ); + } + + #[test] + fn test_negated_full_match() { + let pattern = HttpMockRegex(Regex::new("a+").unwrap()); + let mock_value = Some(&pattern); + let req_value_str = String::from("aaaa"); + let req_value = Some(&req_value_str); + assert_eq!( + regex_string_distance(true, true, &mock_value, &req_value), + 4 + ); + } + + #[test] + fn test_non_negated_partial_match() { + let pattern = HttpMockRegex(Regex::new("a+").unwrap()); + let mock_value = Some(&pattern); + let req_value_str = String::from("aaabbb"); + let req_value = Some(&req_value_str); + assert_eq!( + regex_string_distance(false, true, &mock_value, &req_value), + 3 + ); + } + + #[test] + fn test_negated_partial_match() { + let pattern = HttpMockRegex(Regex::new("a+").unwrap()); + let mock_value = Some(&pattern); + let req_value_str = String::from("aaaabbb"); + let req_value = Some(&req_value_str); + assert_eq!( + regex_string_distance(true, true, &mock_value, &req_value), + 4 + ); + } + + #[test] + fn test_non_negated_no_match() { + let pattern = HttpMockRegex(Regex::new("c+").unwrap()); + let mock_value = Some(&pattern); + let req_value_str = String::from("aaabbb"); + let req_value = Some(&req_value_str); + assert_eq!( + regex_string_distance(false, true, &mock_value, &req_value), + 6 + ); + } + + #[test] + fn test_negated_no_match() { + let pattern = HttpMockRegex(Regex::new("c+").unwrap()); + let mock_value = Some(&pattern); + let req_value_str = String::from("aaabbb"); + let req_value = Some(&req_value_str); + assert_eq!( + regex_string_distance(true, true, &mock_value, &req_value), + 0 + ); + } + + #[test] + fn test_no_req_value() { + let pattern = HttpMockRegex(Regex::new("a+").unwrap()); + let mock_value = Some(&pattern); + let req_value: Option<&String> = None; + assert_eq!( + regex_string_distance(false, true, &mock_value, &req_value), + 2 + ); + } + + #[test] + fn test_no_mock_value() { + let mock_value: Option<&HttpMockRegex> = None; + let req_value_str = String::from("aaabbb"); + let req_value = Some(&req_value_str); + assert_eq!( + regex_string_distance(false, true, &mock_value, &req_value), + 0 + ); + } + + #[test] + fn test_empty_rv_non_negated_match() { + let pattern = HttpMockRegex(Regex::new(".*").unwrap()); + let mock_value = Some(&pattern); + let req_value: Option<&String> = None; + assert_eq!( + regex_string_distance(false, true, &mock_value, &req_value), + 2 + ); + } + + #[test] + fn test_empty_rv_non_negated_no_match() { + let pattern = HttpMockRegex(Regex::new(".+").unwrap()); + let mock_value = Some(&pattern); + let req_value: Option<&String> = None; // This will make rv empty + assert_eq!( + regex_string_distance(false, true, &mock_value, &req_value), + pattern.0.as_str().len() + ); + } + + #[test] + fn test_empty_rv_negated_match() { + let pattern = HttpMockRegex(Regex::new(".*").unwrap()); + let mock_value = Some(&pattern); + let req_value: Option<&String> = None; + + assert_eq!( + // When request does not have any value, it is never a match. Since we have "negate=true", + // it is a match. Hence, distance is 0. + regex_string_distance(true, true, &mock_value, &req_value), + 0 + ); + } + + #[test] + fn test_empty_rv_negated_no_match() { + let pattern = HttpMockRegex(Regex::new(".+").unwrap()); + let mock_value = Some(&pattern); + let req_value: Option<&String> = None; + // Body does not match, but negated = true, so its a match, hence distance is a 0. + assert_eq!( + regex_string_distance(true, true, &mock_value, &req_value), + 0 + ); + } +} diff --git a/src/server/matchers/generic.rs b/src/server/matchers/generic.rs index c6bded0b..b3c5bc8b 100644 --- a/src/server/matchers/generic.rs +++ b/src/server/matchers/generic.rs @@ -1,15 +1,18 @@ -use std::collections::BTreeMap; -use std::fmt::Display; -use std::net::ToSocketAddrs; +use serde::{Deserialize, Serialize}; +use similar::{ChangeTag, TextDiff}; +use std::{collections::HashSet, fmt::Display}; -use serde_json::Value; - -use crate::common::data::{HttpMockRequest, Mismatch, Reason, RequestRequirements, Tokenizer}; -use crate::server::matchers::comparators::ValueComparator; -use crate::server::matchers::sources::{MultiValueSource, ValueRefSource}; -use crate::server::matchers::targets::{MultiValueTarget, ValueRefTarget, ValueTarget}; -use crate::server::matchers::transformers::Transformer; -use crate::server::matchers::{diff_str, Matcher}; +use crate::{ + common::{ + data::{ + Diff, DiffResult, FunctionComparison, HttpMockRequest, KeyValueComparison, + KeyValueComparisonAttribute, KeyValueComparisonKeyValuePair, Mismatch, + RequestRequirements, SingleValueComparison, Tokenizer, + }, + util::is_none_or_empty, + }, + server::matchers::{comparators::ValueComparator, Matcher}, +}; // ************************************************************************************************ // SingleValueMatcher @@ -20,10 +23,11 @@ where T: Display, { pub entity_name: &'static str, - pub source: Box + Send + Sync>, - pub target: Box + Send + Sync>, + pub matcher_method: &'static str, + pub matching_strategy: MatchingStrategy, + pub expectation: for<'a> fn(&'a RequestRequirements) -> Option>, + pub request_value: fn(&HttpMockRequest) -> Option, pub comparator: Box + Send + Sync>, - pub transformer: Option + Send + Sync>>, pub with_reason: bool, pub diff_with: Option, pub weight: usize, @@ -43,13 +47,10 @@ where None => return Vec::new(), Some(mv) => mv.to_vec(), }; - let req_value = match req_value { - None => return mock_values, - Some(rv) => rv, - }; + mock_values .into_iter() - .filter(|e| !self.comparator.matches(e, req_value)) + .filter(|e| !self.comparator.matches(&Some(e), &req_value.as_ref())) .collect() } } @@ -60,14 +61,22 @@ where T: Display, { fn matches(&self, req: &HttpMockRequest, mock: &RequestRequirements) -> bool { - let req_value = self.target.parse_from_request(req); - let mock_value = self.source.parse_from_mock(mock); + let mock_value = (self.expectation)(mock); + if is_none_or_empty(&mock_value) { + return true; + } + + let req_value = (self.request_value)(req); self.find_unmatched(&req_value, &mock_value).is_empty() } fn distance(&self, req: &HttpMockRequest, mock: &RequestRequirements) -> usize { - let req_value = self.target.parse_from_request(req); - let mock_values = self.source.parse_from_mock(mock); + let mock_values = (self.expectation)(mock); + if is_none_or_empty(&mock_values) { + return 0; + } + + let req_value = (self.request_value)(req); self.find_unmatched(&req_value, &mock_values) .into_iter() .map(|s| self.comparator.distance(&Some(s), &req_value.as_ref())) @@ -76,133 +85,184 @@ where } fn mismatches(&self, req: &HttpMockRequest, mock: &RequestRequirements) -> Vec { - let req_value = self.target.parse_from_request(req); - let mock_value = self.source.parse_from_mock(mock); + let mock_value = (self.expectation)(mock); + if is_none_or_empty(&mock_value) { + return Vec::new(); + } + + let req_value = (self.request_value)(req); self.find_unmatched(&req_value, &mock_value) .into_iter() .map(|mock_value| { let mock_value = mock_value.to_string(); - let req_value = req_value.as_ref().unwrap().to_string(); + let req_value = req_value.as_ref().map_or(String::new(), |v| v.to_string()); Mismatch { - title: format!("The {} does not match", self.entity_name), - reason: match self.with_reason { - true => Some(Reason { - expected: mock_value.to_owned(), - actual: req_value.to_owned(), - comparison: self.comparator.name().into(), - best_match: false, - }), - false => None, - }, + matcher_method: self.matcher_method.to_string(), + comparison: Some(SingleValueComparison { + operator: self.comparator.name().to_string(), + expected: mock_value.to_owned(), + actual: req_value.to_owned(), + }), + key_value_comparison: None, + function_comparison: None, + entity: self.entity_name.to_string(), diff: self.diff_with.map(|t| diff_str(&mock_value, &req_value, t)), + best_match: false, + matching_strategy: Some(self.matching_strategy.clone()), } }) .collect() } } +pub enum KeyValueOperator { + AND, + NAND, + NOR, + OR, + IMPLICATION, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum MatchingStrategy { + Presence, + Absence, +} + // ************************************************************************************************ // MultiValueMatcher // ************************************************************************************************ -pub(crate) struct MultiValueMatcher +pub(crate) struct MultiValueMatcher where - SK: Display, - SV: Display, - TK: Display, - TV: Display, + EK: Display, + EV: Display, + RK: Display, + RV: Display, { pub entity_name: &'static str, - pub source: Box + Send + Sync>, - pub target: Box + Send + Sync>, - pub key_comparator: Box + Send + Sync>, - pub value_comparator: Box + Send + Sync>, - pub key_transformer: Option + Send + Sync>>, - pub value_transformer: Option + Send + Sync>>, + pub matcher_method: &'static str, + pub operator: KeyValueOperator, + pub expectation: for<'a> fn(&'a RequestRequirements) -> Option)>>, + pub request_value: fn(&HttpMockRequest) -> Option)>>, + pub matching_strategy: MatchingStrategy, + pub key_required: bool, + pub key_comparator: Box + Send + Sync>, + pub value_comparator: Box + Send + Sync>, pub with_reason: bool, pub diff_with: Option, pub weight: usize, } -impl MultiValueMatcher +impl MultiValueMatcher where - SK: Display, - SV: Display, - TK: Display, - TV: Display, + EK: Display, + EV: Display, + RK: Display, + RV: Display, { fn find_unmatched<'a>( &self, - req_values: &Vec<(TK, Option)>, - mock_values: &'a Vec<(&'a SK, Option<&'a SV>)>, - ) -> Vec<&'a (&'a SK, Option<&'a SV>)> { - mock_values - .into_iter() - .filter(|(sk, sv)| { - req_values - .iter() - .find(|(tk, tv)| { - let key_matches = self.key_comparator.matches(sk, &tk); - let value_matches = match (sv, tv) { - (Some(_), None) => false, // Mock required a value but none was present - (Some(sv), Some(tv)) => self.value_comparator.matches(sv, &tv), - _ => true, - }; - key_matches && value_matches - }) - .is_none() + req_values: &Vec<(RK, Option)>, + mock_values: &'a Vec<(&'a EK, Option<&'a EV>)>, + ) -> Vec<&'a (&'a EK, Option<&'a EV>)> { + return mock_values + .iter() + .filter(|(ek, ev)| { + if self.key_required { + let key_present = req_values.iter().any(|(rk, _): &(RK, Option)| { + self.key_comparator.matches(&Some(ek), &Some(rk)) + }); + + if !key_present { + // We negate here, since we are filtering for "unmatched" expectations -> true = unmatched + return true; + } + } + + let request_value_matches = |(rk, rv): &(RK, Option)| { + let key_matches = self.key_comparator.matches(&Some(ek), &Some(rk)); + let value_matches = match (ev, rv) { + (Some(_), None) => false, // Mock required a value but none was present + (Some(ev), Some(rv)) => self.value_comparator.matches(&Some(ev), &Some(rv)), + _ => true, + }; + + return match self.operator { + KeyValueOperator::NAND => !(key_matches && value_matches), + KeyValueOperator::AND => key_matches && value_matches, + KeyValueOperator::NOR => !(key_matches || value_matches), + KeyValueOperator::OR => key_matches || value_matches, + KeyValueOperator::IMPLICATION => !key_matches || value_matches, + }; + }; + + let is_match = match self.matching_strategy { + MatchingStrategy::Absence => req_values.iter().all(request_value_matches), + MatchingStrategy::Presence => req_values.iter().any(request_value_matches), + }; + + // We negate here, since we are filtering for "unmatched" expectations -> true = unmatched + return !is_match; }) - .collect() + .collect(); } fn find_best_match<'a>( &self, - sk: &SK, - sv: &Option<&SV>, - req_values: &'a Vec<(TK, Option)>, - ) -> Option<(&'a TK, &'a Option)> { + sk: &EK, + sv: &Option<&EV>, + req_values: &'a [(RK, Option)], + ) -> Option<(&'a RK, &'a Option)> { if req_values.is_empty() { return None; } - let found = req_values - .into_iter() - .find(|(k, v)| k.to_string().eq(&sk.to_string())); - if let Some((fk, fv)) = found { + if let Some((fk, fv)) = req_values + .iter() + .find(|(k, _)| k.to_string() == sk.to_string()) + { return Some((fk, fv)); } req_values - .into_iter() + .iter() .map(|(tk, tv)| { let key_distance = self.key_comparator.distance(&Some(sk), &Some(tk)); - let value_distance = self.value_comparator.distance(&sv, &tv.as_ref()); + let value_distance = self.value_comparator.distance(sv, &tv.as_ref()); (tk, tv, key_distance + value_distance) }) .min_by(|(_, _, d1), (_, _, d2)| d1.cmp(d2)) - .map(|(k, v, _)| (k.to_owned(), v.to_owned())) + .map(|(k, v, _)| (k, v)) } } -impl Matcher for MultiValueMatcher +impl Matcher for MultiValueMatcher where - SK: Display, - SV: Display, - TK: Display, - TV: Display, + EK: Display, + EV: Display, + RK: Display, + RV: Display, { fn matches(&self, req: &HttpMockRequest, mock: &RequestRequirements) -> bool { - let req_values = self.target.parse_from_request(req).unwrap_or(Vec::new()); - let mock_values = self.source.parse_from_mock(mock).unwrap_or(Vec::new()); + let mock_values = (self.expectation)(mock).unwrap_or(Vec::new()); + if mock_values.is_empty() { + return true; + } + + let req_values = (self.request_value)(req).unwrap_or(Vec::new()); self.find_unmatched(&req_values, &mock_values).is_empty() } fn distance(&self, req: &HttpMockRequest, mock: &RequestRequirements) -> usize { - let req_values = self.target.parse_from_request(req).unwrap_or(Vec::new()); - let mock_values = self.source.parse_from_mock(mock).unwrap_or(Vec::new()); + let mock_values = (self.expectation)(mock).unwrap_or_default(); + if mock_values.is_empty() { + return 0; + } + + let req_values = (self.request_value)(req).unwrap_or_default(); self.find_unmatched(&req_values, &mock_values) - .into_iter() - .map(|(k, v)| (k, v, self.find_best_match(&k, v, &req_values))) - .map(|(k, v, best_match)| match best_match { + .iter() + .map(|(k, v)| match self.find_best_match(k, v, &req_values) { None => { self.key_comparator.distance(&Some(k), &None) + self.value_comparator.distance(v, &None) @@ -217,31 +277,222 @@ where } fn mismatches(&self, req: &HttpMockRequest, mock: &RequestRequirements) -> Vec { - let req_values = self.target.parse_from_request(req).unwrap_or(Vec::new()); - let mock_values = self.source.parse_from_mock(mock).unwrap_or(Vec::new()); + let mock_values = (self.expectation)(mock); + if is_none_or_empty(&mock_values) { + return Vec::new(); + } + + let mock_values = mock_values.unwrap_or_default(); + let req_values = (self.request_value)(req).unwrap_or_default(); self.find_unmatched(&req_values, &mock_values) - .into_iter() - .map(|(k, v)| (k, v, self.find_best_match(&k, v, &req_values))) - .map(|(k, v, best_match)| Mismatch { - title: match v { - None => format!("Expected {} with name '{}' to be present in the request but it wasn't.", self.entity_name, &k), - Some(v) => format!("Expected {} with name '{}' and value '{}' to be present in the request but it wasn't.", self.entity_name, &k, v), - }, - reason: best_match.as_ref().map(|(bmk, bmv)| { - Reason { - expected: match v { - None => format!("{}", k), - Some(v) => format!("{}={}", k, v), - }, - actual: match bmv { - None => format!("{}", bmk), - Some(bmv) => format!("{}={}", bmk, bmv), - }, - comparison: format!("key={}, value={}", self.key_comparator.name(), self.value_comparator.name()), - best_match: true, - } - }), - diff: None, + .iter() // Use iter to avoid ownership issues and unnecessary data moving. + .map(|(k, v)| { + let best_match = self.find_best_match(k, v, &req_values); + Mismatch { + entity: self.entity_name.to_string(), + matcher_method: self.matcher_method.to_string(), + comparison: None, + function_comparison: None, + key_value_comparison: Some(KeyValueComparison { + key: Some(KeyValueComparisonAttribute { + operator: self.key_comparator.name().to_string(), + expected: k.to_string(), + actual: best_match.map(|(bmk, _)| bmk.to_string()), + }), + value: v.map(|v| KeyValueComparisonAttribute { + operator: self.value_comparator.name().to_string(), + expected: v.to_string(), + actual: best_match + .and_then(|(_, bmv)| bmv.as_ref().map(|bmv| bmv.to_string())), + }), + expected_count: None, + actual_count: None, + all: (&req_values) + .into_iter() + .map(|(key, value)| KeyValueComparisonKeyValuePair { + key: key.to_string(), + value: value.as_ref().map(|v| v.to_string()), + }) + .collect(), + }), + matching_strategy: Some(self.matching_strategy.clone()), + diff: None, + best_match: best_match.is_some(), + } + }) + .collect() + } +} + +// ************************************************************************************************ +// MultiValueCountMatcher +// ************************************************************************************************ +pub(crate) struct MultiValueCountMatcher +where + EK: Display, + EV: Display, + RK: Display, + RV: Display, +{ + pub entity_name: &'static str, + pub matcher_method: &'static str, + pub expectation: + for<'a> fn(&'a RequestRequirements) -> Option, Option<&'a EV>, usize)>>, + pub request_value: fn(&HttpMockRequest) -> Option)>>, + pub key_comparator: Box + Send + Sync>, + pub value_comparator: Box + Send + Sync>, + pub with_reason: bool, + pub diff_with: Option, + pub weight: usize, +} + +impl MultiValueCountMatcher +where + EK: Display, + EV: Display, + RK: Display, + RV: Display, +{ + fn find_unmatched<'a>( + &self, + req_values: &[(RK, Option)], + mock_values: &'a [(Option<&'a EK>, Option<&'a EV>, usize)], + ) -> Vec<&'a (Option<&'a EK>, Option<&'a EV>, usize)> { + let matched_idx = self.find_matched_indices(req_values, mock_values); + self.filter_unmatched_indices(mock_values, &matched_idx) + } + + fn find_matched_indices<'a>( + &self, + req_values: &[(RK, Option)], + mock_values: &'a [(Option<&'a EK>, Option<&'a EV>, usize)], + ) -> HashSet { + let mut matched_idx = HashSet::new(); + + for (idx, (ek, ev, count)) in mock_values.iter().enumerate() { + let matches = self.count_matching_req_values(req_values, ek, ev); + if matches == *count { + matched_idx.insert(idx); + } + } + + matched_idx + } + + fn count_matching_req_values( + &self, + req_values: &[(RK, Option)], + ek: &Option<&EK>, + ev: &Option<&EV>, + ) -> usize { + req_values + .iter() + .filter(|(rk, rv)| { + let key_matches = match ek { + Some(ek) => self.key_comparator.matches(&Some(ek), &Some(rk)), + None => true, // No expectation => true + }; + + let value_matches = match (ev, rv) { + (Some(ev), Some(rv)) => self.value_comparator.matches(&Some(ev), &Some(rv)), + (Some(_), None) => false, // Expectation but no request value => false + (None, _) => true, // No expectation => true + }; + + key_matches && value_matches + }) + .count() + } + + fn filter_unmatched_indices<'a>( + &self, + mock_values: &'a [(Option<&'a EK>, Option<&'a EV>, usize)], + matched_idx: &HashSet, + ) -> Vec<&'a (Option<&'a EK>, Option<&'a EV>, usize)> { + mock_values + .iter() + .enumerate() + .filter(|(i, _)| !matched_idx.contains(i)) + .map(|(_, item)| item) + .collect() + } +} + +impl Matcher for MultiValueCountMatcher +where + EK: Display, + EV: Display, + RK: Display, + RV: Display, +{ + fn matches(&self, req: &HttpMockRequest, mock: &RequestRequirements) -> bool { + let mock_values = (self.expectation)(mock).unwrap_or_default(); + if mock_values.is_empty() { + return true; + } + + let req_values = (self.request_value)(req).unwrap_or(Vec::new()); + self.find_unmatched(&req_values, &mock_values).is_empty() + } + + fn distance(&self, req: &HttpMockRequest, mock: &RequestRequirements) -> usize { + let mock_values = (self.expectation)(mock).unwrap_or_default(); + if mock_values.is_empty() { + return 0; + } + + let req_values = (self.request_value)(req).unwrap_or_default(); + self.find_unmatched(&req_values, &mock_values) + .iter() + .map(|(k, v, c)| { + let num_matching = self.count_matching_req_values(&req_values, k, v); + num_matching.abs_diff(*c) + }) + .map(|d| d * self.weight) + .sum() + } + + fn mismatches(&self, req: &HttpMockRequest, mock: &RequestRequirements) -> Vec { + let mock_values = (self.expectation)(mock).unwrap_or_default(); + if mock_values.is_empty() { + return Vec::new(); + } + + let req_values = (self.request_value)(req).unwrap_or_default(); + self.find_unmatched(&req_values, &mock_values) + .iter() // Use iter to avoid ownership issues and unnecessary data moving. + .map(|(k, v, expected_count)| { + let actual_count = self.count_matching_req_values(&req_values, k, v); + Mismatch { + entity: self.entity_name.to_string(), + matcher_method: self.matcher_method.to_string(), + comparison: None, + key_value_comparison: Some(KeyValueComparison { + key: k.map(|k| KeyValueComparisonAttribute { + operator: self.key_comparator.name().to_string(), + expected: k.to_string(), + actual: None, + }), + value: v.map(|v| KeyValueComparisonAttribute { + operator: self.value_comparator.name().to_string(), + expected: v.to_string(), + actual: None, + }), + expected_count: Some(*expected_count), + actual_count: Some(actual_count), + all: (&req_values) + .into_iter() + .map(|(key, value)| KeyValueComparisonKeyValuePair { + key: key.to_string(), + value: value.as_ref().map(|v| v.to_string()), + }) + .collect(), + }), + matching_strategy: None, + function_comparison: None, + diff: None, + best_match: false, + } }) .collect() } @@ -252,10 +503,10 @@ where // ************************************************************************************************ pub(crate) struct FunctionValueMatcher { pub entity_name: &'static str, - pub source: Box + Send + Sync>, - pub target: Box + Send + Sync>, + pub matcher_function: &'static str, + pub expectation: for<'a> fn(&'a RequestRequirements) -> Option>, + pub request_value: for<'a> fn(&'a HttpMockRequest) -> Option<&'a T>, pub comparator: Box + Send + Sync>, - pub transformer: Option + Send + Sync>>, pub weight: usize, } @@ -282,7 +533,7 @@ impl FunctionValueMatcher { mock_values .into_iter() .enumerate() - .filter(|(idx, e)| !self.comparator.matches(e, req_value)) + .filter(|(idx, e)| !self.comparator.matches(&Some(e), &Some(req_value))) .map(|(idx, e)| (idx)) .collect() } @@ -290,37 +541,84 @@ impl FunctionValueMatcher { impl Matcher for FunctionValueMatcher { fn matches(&self, req: &HttpMockRequest, mock: &RequestRequirements) -> bool { - let req_value = self.target.parse_from_request(req); - let mock_values = self.source.parse_from_mock(mock); + let mock_values = (self.expectation)(mock); + if is_none_or_empty(&mock_values) { + return true; + } + + let req_value = (self.request_value)(req); self.get_unmatched(&req_value, &mock_values).is_empty() } fn distance(&self, req: &HttpMockRequest, mock: &RequestRequirements) -> usize { - let req_value = self.target.parse_from_request(req); - let mock_values = self.source.parse_from_mock(mock); + let mock_values = (self.expectation)(mock); + if is_none_or_empty(&mock_values) { + return 0; + } + + let req_value = (self.request_value)(req); self.get_unmatched(&req_value, &mock_values).len() * self.weight } fn mismatches(&self, req: &HttpMockRequest, mock: &RequestRequirements) -> Vec { - let req_value = self.target.parse_from_request(req); - let mock_value = self.source.parse_from_mock(mock); - self.get_unmatched(&req_value, &mock_value) + let mock_values = (self.expectation)(mock); + if is_none_or_empty(&mock_values) { + return Vec::new(); + } + + let req_value = (self.request_value)(req); + self.get_unmatched(&req_value, &mock_values) .into_iter() .map(|idx| Mismatch { - title: format!( - "The {} at position {} does not match", - self.entity_name, - idx + 1 - ), - reason: None, + entity: self.entity_name.to_string(), + matcher_method: self.matcher_function.to_string(), + function_comparison: Some(FunctionComparison { index: idx }), + comparison: None, + key_value_comparison: None, diff: None, + best_match: false, + matching_strategy: None, }) .collect() } } -#[cfg(test)] -mod test { - #[test] - fn todo() {} +#[inline] +fn times_str<'a>(v: usize) -> &'a str { + if v == 1 { + return "time"; + } + + return "times"; +} + +#[inline] +fn get_plural<'a>(v: usize, singular: &'a str, plural: &'a str) -> &'a str { + if v == 1 { + return singular; + } + + return plural; +} + +#[inline] +pub fn diff_str(base: &str, edit: &str, tokenizer: Tokenizer) -> DiffResult { + let changes = match tokenizer { + Tokenizer::Line => TextDiff::from_lines(base, edit), + Tokenizer::Word => TextDiff::from_words(base, edit), + Tokenizer::Character => TextDiff::from_chars(base, edit), + }; + + DiffResult { + tokenizer, + distance: changes.ratio(), + differences: changes + .iter_all_changes() + .map(|change| match change.tag() { + ChangeTag::Equal => Diff::Same(change.to_string_lossy().to_string()), + ChangeTag::Insert => Diff::Add(change.to_string_lossy().to_string()), + ChangeTag::Delete => Diff::Rem(change.to_string_lossy().to_string()), + }) + .collect(), + } } diff --git a/src/server/matchers/mod.rs b/src/server/matchers/mod.rs index 4bf05f44..b735bc88 100644 --- a/src/server/matchers/mod.rs +++ b/src/server/matchers/mod.rs @@ -1,40 +1,1168 @@ -use std::collections::BTreeMap; -use std::fmt::Display; +use std::{convert::TryInto, fmt::Display, ops::Deref}; -#[cfg(feature = "cookies")] -use basic_cookies::Cookie; use serde::{Deserialize, Serialize}; -use similar::{ChangeTag, TextDiff}; -use crate::common::data::{ - Diff, DiffResult, HttpMockRequest, Mismatch, RequestRequirements, Tokenizer, +use crate::common::data::{HttpMockRequest, Mismatch, RequestRequirements, Tokenizer}; + +use crate::server::matchers::comparators::{ + AnyValueComparator, BytesExactMatchComparator, BytesIncludesComparator, BytesPrefixComparator, + BytesSuffixComparator, FunctionMatchesRequestComparator, HostEqualsComparator, + HttpMockBytesPatternComparator, JSONContainsMatchComparator, JSONExactMatchComparator, + StringContainsComparator, StringEqualsComparator, StringPatternMatchComparator, + StringPrefixMatchComparator, StringRegexMatchComparator, StringSuffixMatchComparator, + U16ExactMatchComparator, }; -pub(crate) mod comparators; -pub(crate) mod generic; -pub(crate) mod sources; -pub(crate) mod targets; -pub(crate) mod transformers; +use crate::server::matchers::generic::{ + FunctionValueMatcher, KeyValueOperator, MatchingStrategy, MultiValueCountMatcher, + MultiValueMatcher, SingleValueMatcher, +}; -pub(crate) fn diff_str(base: &str, edit: &str, tokenizer: Tokenizer) -> DiffResult { - let changes = match tokenizer { - Tokenizer::Line => TextDiff::from_lines(base, edit), - Tokenizer::Word => TextDiff::from_words(base, edit), - Tokenizer::Character => TextDiff::from_chars(base, edit), - }; +pub mod comparators; +mod comparison; +pub mod generic; +pub mod readers; - DiffResult { - tokenizer, - distance: changes.ratio(), - differences: changes - .iter_all_changes() - .map(|change| match change.tag() { - ChangeTag::Equal => Diff::Same(change.to_string_lossy().to_string()), - ChangeTag::Insert => Diff::Add(change.to_string_lossy().to_string()), - ChangeTag::Delete => Diff::Rem(change.to_string_lossy().to_string()), - }) - .collect(), - } +pub fn all() -> Vec> { + vec![ + //************************************************************************************* + // Scheme matchers + //************************************************************************************* + Box::new(SingleValueMatcher { + entity_name: "scheme", + matcher_method: "scheme", + matching_strategy: MatchingStrategy::Presence, + comparator: Box::new(StringEqualsComparator::new(false, false)), + expectation: readers::expectations::scheme_equal_to, + request_value: readers::request_value::scheme, + with_reason: true, + diff_with: None, + weight: 3, + }), + Box::new(SingleValueMatcher { + entity_name: "scheme", + matcher_method: "scheme_not", + matching_strategy: MatchingStrategy::Absence, + comparator: Box::new(StringEqualsComparator::new(false, true)), + expectation: readers::expectations::scheme_not_equal_to, + request_value: readers::request_value::scheme, + with_reason: true, + diff_with: None, + weight: 3, + }), + //************************************************************************************* + // Method matchers + //************************************************************************************* + Box::new(SingleValueMatcher { + entity_name: "method", + matcher_method: "method", + matching_strategy: MatchingStrategy::Presence, + comparator: Box::new(StringEqualsComparator::new(false, false)), + expectation: readers::expectations::method_equal_to, + request_value: readers::request_value::method, + with_reason: true, + diff_with: None, + weight: 3, + }), + Box::new(SingleValueMatcher { + entity_name: "method", + matcher_method: "method_not", + matching_strategy: MatchingStrategy::Absence, + comparator: Box::new(StringEqualsComparator::new(false, true)), + expectation: readers::expectations::method_not_equal_to, + request_value: readers::request_value::method, + with_reason: true, + diff_with: None, + weight: 3, + }), + //************************************************************************************* + // Host matchers + //************************************************************************************* + Box::new(SingleValueMatcher { + entity_name: "host", + matcher_method: "host", + matching_strategy: MatchingStrategy::Presence, + comparator: Box::new(HostEqualsComparator::new(false)), + expectation: readers::expectations::host_equal_to, + request_value: readers::request_value::host, + with_reason: true, + diff_with: None, + weight: 3, + }), + Box::new(SingleValueMatcher { + entity_name: "host", + matcher_method: "host_not", + matching_strategy: MatchingStrategy::Absence, + comparator: Box::new(HostEqualsComparator::new(true)), + expectation: readers::expectations::host_not_equal_to, + request_value: readers::request_value::host, + with_reason: true, + diff_with: None, + weight: 3, + }), + Box::new(SingleValueMatcher { + entity_name: "host", + matcher_method: "host_includes", + matching_strategy: MatchingStrategy::Presence, + comparator: Box::new(StringContainsComparator::new(false, false)), + expectation: readers::expectations::host_includes, + request_value: readers::request_value::host, + with_reason: true, + diff_with: None, + weight: 3, + }), + Box::new(SingleValueMatcher { + entity_name: "host", + matcher_method: "host_excludes", + matching_strategy: MatchingStrategy::Absence, + comparator: Box::new(StringContainsComparator::new(false, true)), + expectation: readers::expectations::host_excludes, + request_value: readers::request_value::host, + with_reason: true, + diff_with: None, + weight: 3, + }), + Box::new(SingleValueMatcher { + entity_name: "host", + matcher_method: "host_prefix", + matching_strategy: MatchingStrategy::Presence, + comparator: Box::new(StringPrefixMatchComparator::new(false, false)), + expectation: readers::expectations::host_prefix, + request_value: readers::request_value::host, + with_reason: true, + diff_with: None, + weight: 3, + }), + Box::new(SingleValueMatcher { + entity_name: "host", + matcher_method: "host_prefix_not", + matching_strategy: MatchingStrategy::Absence, + comparator: Box::new(StringPrefixMatchComparator::new(false, true)), + expectation: readers::expectations::host_prefix_not, + request_value: readers::request_value::host, + with_reason: true, + diff_with: None, + weight: 3, + }), + Box::new(SingleValueMatcher { + entity_name: "host", + matcher_method: "host_suffix", + matching_strategy: MatchingStrategy::Presence, + comparator: Box::new(StringSuffixMatchComparator::new(false, false)), + expectation: readers::expectations::host_has_suffix, + request_value: readers::request_value::host, + with_reason: true, + diff_with: None, + weight: 3, + }), + Box::new(SingleValueMatcher { + entity_name: "host", + matcher_method: "host_suffix_not", + matching_strategy: MatchingStrategy::Absence, + comparator: Box::new(StringSuffixMatchComparator::new(false, true)), + expectation: readers::expectations::host_has_no_suffix, + request_value: readers::request_value::host, + with_reason: true, + diff_with: None, + weight: 3, + }), + Box::new(SingleValueMatcher { + entity_name: "host", + matcher_method: "host_matches", + matching_strategy: MatchingStrategy::Presence, + comparator: Box::new(StringPatternMatchComparator::new(false, true)), + expectation: readers::expectations::host_matches_regex, + request_value: readers::request_value::host, + with_reason: true, + diff_with: None, + weight: 3, + }), + //************************************************************************************* + // Port matchers + //************************************************************************************* + Box::new(SingleValueMatcher { + entity_name: "port", + matcher_method: "port", + matching_strategy: MatchingStrategy::Presence, + comparator: Box::new(U16ExactMatchComparator::new(false)), + expectation: readers::expectations::port_equal_to, + request_value: readers::request_value::port, + with_reason: true, + diff_with: None, + weight: 2, + }), + Box::new(SingleValueMatcher { + entity_name: "port", + matcher_method: "port_not", + matching_strategy: MatchingStrategy::Absence, + comparator: Box::new(U16ExactMatchComparator::new(true)), + expectation: readers::expectations::port_not_equal_to, + request_value: readers::request_value::port, + with_reason: true, + diff_with: None, + weight: 2, + }), + //************************************************************************************* + // Path matchers + //************************************************************************************* + Box::new(SingleValueMatcher { + entity_name: "path", + matcher_method: "path", + matching_strategy: MatchingStrategy::Presence, + comparator: Box::new(StringEqualsComparator::new(true, false)), + expectation: readers::expectations::path_equal_to, + request_value: readers::request_value::path, + with_reason: true, + diff_with: None, + weight: 10, + }), + Box::new(SingleValueMatcher { + entity_name: "path", + matcher_method: "path_not", + matching_strategy: MatchingStrategy::Absence, + comparator: Box::new(StringEqualsComparator::new(true, true)), + expectation: readers::expectations::path_not_equal_to, + request_value: readers::request_value::path, + with_reason: true, + diff_with: None, + weight: 10, + }), + Box::new(SingleValueMatcher { + entity_name: "path", + matcher_method: "path_includes", + matching_strategy: MatchingStrategy::Presence, + comparator: Box::new(StringContainsComparator::new(true, false)), + expectation: readers::expectations::path_includes, + request_value: readers::request_value::path, + with_reason: true, + diff_with: None, + weight: 10, + }), + // path excludes + Box::new(SingleValueMatcher { + entity_name: "path", + matcher_method: "path_excludes", + matching_strategy: MatchingStrategy::Absence, + comparator: Box::new(StringContainsComparator::new(true, true)), + expectation: readers::expectations::path_excludes, + request_value: readers::request_value::path, + with_reason: true, + diff_with: None, + weight: 10, + }), + Box::new(SingleValueMatcher { + entity_name: "path", + matcher_method: "path_prefix", + matching_strategy: MatchingStrategy::Presence, + comparator: Box::new(StringPrefixMatchComparator::new(true, false)), + expectation: readers::expectations::path_prefix, + request_value: readers::request_value::path, + with_reason: true, + diff_with: None, + weight: 10, + }), + Box::new(SingleValueMatcher { + entity_name: "path", + matcher_method: "path_prefix_not", + matching_strategy: MatchingStrategy::Absence, + comparator: Box::new(StringPrefixMatchComparator::new(true, true)), + expectation: readers::expectations::path_prefix_not, + request_value: readers::request_value::path, + with_reason: true, + diff_with: None, + weight: 10, + }), + Box::new(SingleValueMatcher { + entity_name: "path", + matcher_method: "path_suffix", + matching_strategy: MatchingStrategy::Presence, + comparator: Box::new(StringSuffixMatchComparator::new(true, false)), + expectation: readers::expectations::path_suffix, + request_value: readers::request_value::path, + with_reason: true, + diff_with: None, + weight: 10, + }), + Box::new(SingleValueMatcher { + entity_name: "path", + matcher_method: "path_suffix_not", + matching_strategy: MatchingStrategy::Absence, + comparator: Box::new(StringSuffixMatchComparator::new(true, true)), + expectation: readers::expectations::path_suffix_not, + request_value: readers::request_value::path, + with_reason: true, + diff_with: None, + weight: 10, + }), + Box::new(SingleValueMatcher { + entity_name: "path", + matcher_method: "path_matches", + matching_strategy: MatchingStrategy::Presence, + comparator: Box::new(StringRegexMatchComparator::new()), + expectation: readers::expectations::path_matches, + request_value: readers::request_value::path, + with_reason: true, + diff_with: None, + weight: 10, + }), + //************************************************************************************* + // Query param matchers + //************************************************************************************* + Box::new(MultiValueMatcher { + entity_name: "query parameter", + matcher_method: "query_param", + matching_strategy: MatchingStrategy::Presence, + operator: KeyValueOperator::AND, + expectation: readers::expectations::query_param, + request_value: readers::request_value::query_params, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(StringEqualsComparator::new(true, false)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "query parameter", + matcher_method: "query_param_not", + matching_strategy: MatchingStrategy::Absence, + operator: KeyValueOperator::IMPLICATION, + expectation: readers::expectations::query_param_not, + request_value: readers::request_value::query_params, + key_required: true, + // Key is not negated, since we expect a query parameter to be present with the expected key. + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(StringEqualsComparator::new(true, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "query parameter", + matcher_method: "query_param_exists", + matching_strategy: MatchingStrategy::Presence, + operator: KeyValueOperator::AND, + expectation: readers::expectations::query_param_exists, + request_value: readers::request_value::query_params, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(AnyValueComparator::new()), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "query parameter", + matcher_method: "query_param_missing", + matching_strategy: MatchingStrategy::Absence, + operator: KeyValueOperator::AND, + expectation: readers::expectations::query_param_missing, + request_value: readers::request_value::query_params, + key_required: false, + key_comparator: Box::new(StringEqualsComparator::new(true, true)), + value_comparator: Box::new(AnyValueComparator::new()), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "query parameter", + matcher_method: "query_param_includes", + matching_strategy: MatchingStrategy::Presence, + operator: KeyValueOperator::AND, + expectation: readers::expectations::query_param_includes, + request_value: readers::request_value::query_params, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(StringContainsComparator::new(true, false)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "query parameter", + matcher_method: "query_param_excludes", + matching_strategy: MatchingStrategy::Absence, + operator: KeyValueOperator::IMPLICATION, + expectation: readers::expectations::query_param_excludes, + request_value: readers::request_value::query_params, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(StringContainsComparator::new(true, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "query parameter", + matcher_method: "query_param_prefix", + matching_strategy: MatchingStrategy::Presence, + operator: KeyValueOperator::AND, + expectation: readers::expectations::query_param_prefix, + request_value: readers::request_value::query_params, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(StringPrefixMatchComparator::new(true, false)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "query parameter", + matcher_method: "query_param_prefix_not", + matching_strategy: MatchingStrategy::Absence, + operator: KeyValueOperator::IMPLICATION, + expectation: readers::expectations::query_param_prefix_not, + request_value: readers::request_value::query_params, + key_required: true, + // TODO: ATTENTION: still false, because it is expected that the key appears in the request! + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(StringPrefixMatchComparator::new(true, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "query parameter", + matcher_method: "query_param_suffix", + matching_strategy: MatchingStrategy::Presence, + operator: KeyValueOperator::AND, + expectation: readers::expectations::query_param_suffix, + request_value: readers::request_value::query_params, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(StringSuffixMatchComparator::new(true, false)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "query parameter", + matcher_method: "query_param_suffix_not", + matching_strategy: MatchingStrategy::Absence, + operator: KeyValueOperator::IMPLICATION, + expectation: readers::expectations::query_param_suffix_not, + request_value: readers::request_value::query_params, + key_required: true, + // TODO: ATTENTION: still false, because it is expected that the key appears in the request! + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(StringSuffixMatchComparator::new(true, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "query parameter", + matcher_method: "query_param_matches", + matching_strategy: MatchingStrategy::Presence, + operator: KeyValueOperator::AND, + expectation: readers::expectations::query_param_matches, + request_value: readers::request_value::query_params, + key_required: true, + // TODO: ATTENTION: still false, because it is expected that the key appears in the request! + key_comparator: Box::new(StringPatternMatchComparator::new(false, true)), + value_comparator: Box::new(StringPatternMatchComparator::new(false, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueCountMatcher { + entity_name: "query parameter", + matcher_method: "query_param_count", + expectation: readers::expectations::query_param_count, + request_value: readers::request_value::query_params, + // TODO: ATTENTION: still false, because it is expected that the key appears in the request! + key_comparator: Box::new(StringPatternMatchComparator::new(false, true)), + value_comparator: Box::new(StringPatternMatchComparator::new(false, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + //************************************************************************************ + // Header matchers + //************************************************************************************ + Box::new(MultiValueMatcher { + entity_name: "header", + matcher_method: "header", + matching_strategy: MatchingStrategy::Presence, + operator: KeyValueOperator::AND, + expectation: readers::expectations::header, + request_value: readers::request_value::headers, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(false, false)), + value_comparator: Box::new(StringEqualsComparator::new(true, false)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "header", + matcher_method: "header_not", + operator: KeyValueOperator::IMPLICATION, + expectation: readers::expectations::header_not, + request_value: readers::request_value::headers, + matching_strategy: MatchingStrategy::Absence, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(false, false)), + value_comparator: Box::new(StringEqualsComparator::new(true, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "header", + matcher_method: "header_exists", + operator: KeyValueOperator::AND, + expectation: readers::expectations::header_exists, + request_value: readers::request_value::headers, + matching_strategy: MatchingStrategy::Presence, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(false, false)), + value_comparator: Box::new(AnyValueComparator::new()), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "header", + matcher_method: "header_missing", + operator: KeyValueOperator::AND, + expectation: readers::expectations::header_missing, + request_value: readers::request_value::headers, + matching_strategy: MatchingStrategy::Absence, + key_required: false, + key_comparator: Box::new(StringEqualsComparator::new(false, true)), + value_comparator: Box::new(AnyValueComparator::new()), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "header", + matcher_method: "header_includes", + operator: KeyValueOperator::AND, + expectation: readers::expectations::header_includes, + request_value: readers::request_value::headers, + matching_strategy: MatchingStrategy::Presence, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(false, false)), + value_comparator: Box::new(StringContainsComparator::new(true, false)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "header", + matcher_method: "header_excludes", + operator: KeyValueOperator::IMPLICATION, + expectation: readers::expectations::header_excludes, + request_value: readers::request_value::headers, + matching_strategy: MatchingStrategy::Absence, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(false, false)), + value_comparator: Box::new(StringContainsComparator::new(true, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "header", + matcher_method: "header_prefix", + operator: KeyValueOperator::AND, + expectation: readers::expectations::header_prefix, + request_value: readers::request_value::headers, + matching_strategy: MatchingStrategy::Presence, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(false, false)), + value_comparator: Box::new(StringPrefixMatchComparator::new(true, false)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "header", + matcher_method: "header_prefix_not", + operator: KeyValueOperator::IMPLICATION, + expectation: readers::expectations::header_prefix_not, + request_value: readers::request_value::headers, + matching_strategy: MatchingStrategy::Absence, + key_required: true, + // TODO: ATTENTION: still false, because it is expected that the key appears in the request! + key_comparator: Box::new(StringEqualsComparator::new(false, false)), + value_comparator: Box::new(StringPrefixMatchComparator::new(true, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "header", + matcher_method: "header_suffix", + operator: KeyValueOperator::AND, + expectation: readers::expectations::header_suffix, + request_value: readers::request_value::headers, + matching_strategy: MatchingStrategy::Presence, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(false, false)), + value_comparator: Box::new(StringSuffixMatchComparator::new(true, false)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "header", + matcher_method: "header_suffix_not", + operator: KeyValueOperator::IMPLICATION, + expectation: readers::expectations::header_suffix_not, + request_value: readers::request_value::headers, + matching_strategy: MatchingStrategy::Absence, + key_required: true, + // TODO: ATTENTION: still false, because it is expected that the key appears in the request! + key_comparator: Box::new(StringEqualsComparator::new(false, false)), + value_comparator: Box::new(StringSuffixMatchComparator::new(true, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "header", + matcher_method: "header_matches", + operator: KeyValueOperator::AND, + expectation: readers::expectations::header_matches, + request_value: readers::request_value::headers, + matching_strategy: MatchingStrategy::Presence, + key_required: true, + // TODO: ATTENTION: still false, because it is expected that the key appears in the request! + key_comparator: Box::new(StringPatternMatchComparator::new(false, false)), + value_comparator: Box::new(StringPatternMatchComparator::new(false, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueCountMatcher { + entity_name: "header", + matcher_method: "header_count", + expectation: readers::expectations::header_count, + request_value: readers::request_value::headers, + // TODO: ATTENTION: still false, because it is expected that the key appears in the request! + key_comparator: Box::new(StringPatternMatchComparator::new(false, false)), + value_comparator: Box::new(StringPatternMatchComparator::new(false, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + // *********************************************************************************** + // Cookie matchers + // *********************************************************************************** + #[cfg(feature = "cookies")] + Box::new(MultiValueMatcher { + entity_name: "cookie", + matcher_method: "cookie", + operator: KeyValueOperator::AND, + expectation: readers::expectations::cookie, + request_value: readers::request_value::cookies, + matching_strategy: MatchingStrategy::Presence, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(StringEqualsComparator::new(true, false)), + with_reason: true, + diff_with: None, + weight: 1, + }), + #[cfg(feature = "cookies")] + Box::new(MultiValueMatcher { + entity_name: "cookie", + matcher_method: "cookie_not", + matching_strategy: MatchingStrategy::Absence, + operator: KeyValueOperator::IMPLICATION, + expectation: readers::expectations::cookie_not, + request_value: readers::request_value::cookies, + key_required: true, + // Key is not negated, since we expect a query parameter to be present with the expected key. + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(StringEqualsComparator::new(true, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + #[cfg(feature = "cookies")] + Box::new(MultiValueMatcher { + entity_name: "cookie", + matcher_method: "cookie_exists", + operator: KeyValueOperator::AND, + expectation: readers::expectations::cookie_exists, + request_value: readers::request_value::cookies, + matching_strategy: MatchingStrategy::Presence, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(AnyValueComparator::new()), + with_reason: true, + diff_with: None, + weight: 1, + }), + #[cfg(feature = "cookies")] + Box::new(MultiValueMatcher { + entity_name: "cookie", + matcher_method: "cookie_missing", + operator: KeyValueOperator::AND, + expectation: readers::expectations::cookie_missing, + request_value: readers::request_value::cookies, + matching_strategy: MatchingStrategy::Absence, + key_required: false, + key_comparator: Box::new(StringEqualsComparator::new(true, true)), + value_comparator: Box::new(AnyValueComparator::new()), + with_reason: true, + diff_with: None, + weight: 1, + }), + #[cfg(feature = "cookies")] + Box::new(MultiValueMatcher { + entity_name: "cookie", + matcher_method: "cookie_includes", + operator: KeyValueOperator::AND, + expectation: readers::expectations::cookie_includes, + request_value: readers::request_value::cookies, + matching_strategy: MatchingStrategy::Presence, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(StringContainsComparator::new(true, false)), + with_reason: true, + diff_with: None, + weight: 1, + }), + #[cfg(feature = "cookies")] + Box::new(MultiValueMatcher { + entity_name: "cookie", + matcher_method: "cookie_excludes", + operator: KeyValueOperator::IMPLICATION, + expectation: readers::expectations::cookie_excludes, + request_value: readers::request_value::cookies, + matching_strategy: MatchingStrategy::Absence, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(StringContainsComparator::new(true, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + #[cfg(feature = "cookies")] + Box::new(MultiValueMatcher { + entity_name: "cookie", + matcher_method: "cookie_prefix", + operator: KeyValueOperator::AND, + expectation: readers::expectations::cookie_prefix, + request_value: readers::request_value::cookies, + matching_strategy: MatchingStrategy::Presence, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(StringPrefixMatchComparator::new(true, false)), + with_reason: true, + diff_with: None, + weight: 1, + }), + #[cfg(feature = "cookies")] + Box::new(MultiValueMatcher { + entity_name: "cookie", + matcher_method: "cookie_prefix_not", + operator: KeyValueOperator::IMPLICATION, + expectation: readers::expectations::cookie_prefix_not, + request_value: readers::request_value::cookies, + matching_strategy: MatchingStrategy::Absence, + key_required: true, + // TODO: ATTENTION: still false, because it is expected that the key appears in the request! + key_comparator: Box::new(StringEqualsComparator::new(false, false)), + value_comparator: Box::new(StringPrefixMatchComparator::new(true, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + #[cfg(feature = "cookies")] + Box::new(MultiValueMatcher { + entity_name: "cookie", + matcher_method: "cookie_suffix", + operator: KeyValueOperator::AND, + expectation: readers::expectations::cookie_suffix, + request_value: readers::request_value::cookies, + matching_strategy: MatchingStrategy::Presence, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(StringSuffixMatchComparator::new(true, false)), + with_reason: true, + diff_with: None, + weight: 1, + }), + #[cfg(feature = "cookies")] + Box::new(MultiValueMatcher { + entity_name: "cookie", + matcher_method: "cookie_suffix_not", + operator: KeyValueOperator::IMPLICATION, + expectation: readers::expectations::cookie_suffix_not, + request_value: readers::request_value::cookies, + matching_strategy: MatchingStrategy::Absence, + key_required: true, + // TODO: ATTENTION: still false, because it is expected that the key appears in the request! + key_comparator: Box::new(StringEqualsComparator::new(false, false)), + value_comparator: Box::new(StringSuffixMatchComparator::new(true, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + #[cfg(feature = "cookies")] + Box::new(MultiValueMatcher { + entity_name: "cookie", + matcher_method: "cookie_matches", + operator: KeyValueOperator::AND, + expectation: readers::expectations::cookie_matches, + request_value: readers::request_value::cookies, + matching_strategy: MatchingStrategy::Presence, + key_required: true, + // TODO: ATTENTION: still false, because it is expected that the key appears in the request! + key_comparator: Box::new(StringPatternMatchComparator::new(false, true)), + value_comparator: Box::new(StringPatternMatchComparator::new(false, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + #[cfg(feature = "cookies")] + Box::new(MultiValueCountMatcher { + entity_name: "cookie", + matcher_method: "cookie_count", + expectation: readers::expectations::cookie_count, + request_value: readers::request_value::cookies, + // TODO: ATTENTION: still false, because it is expected that the key appears in the request! + key_comparator: Box::new(StringPatternMatchComparator::new(false, true)), + value_comparator: Box::new(StringPatternMatchComparator::new(false, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + // ************************************************************************************ + // Body matchers + // ************************************************************************************ + Box::new(SingleValueMatcher { + entity_name: "body", + matcher_method: "body", + matching_strategy: MatchingStrategy::Presence, + comparator: Box::new(BytesExactMatchComparator::new(false)), + expectation: readers::expectations::body, + request_value: readers::request_value::body, + with_reason: true, + diff_with: Some(Tokenizer::Line), + weight: 1, + }), + Box::new(SingleValueMatcher { + entity_name: "body", + matcher_method: "body_not", + matching_strategy: MatchingStrategy::Absence, + comparator: Box::new(BytesExactMatchComparator::new(true)), + expectation: readers::expectations::body_not, + request_value: readers::request_value::body, + with_reason: true, + diff_with: Some(Tokenizer::Line), + weight: 1, + }), + Box::new(SingleValueMatcher { + entity_name: "body", + matcher_method: "body_includes", + matching_strategy: MatchingStrategy::Presence, + comparator: Box::new(BytesIncludesComparator::new(false)), + expectation: readers::expectations::body_includes, + request_value: readers::request_value::body, + with_reason: true, + diff_with: Some(Tokenizer::Line), + weight: 1, + }), + Box::new(SingleValueMatcher { + entity_name: "body", + matcher_method: "body_excludes", + matching_strategy: MatchingStrategy::Absence, + comparator: Box::new(BytesIncludesComparator::new(true)), + expectation: readers::expectations::body_excludes, + request_value: readers::request_value::body, + with_reason: true, + diff_with: Some(Tokenizer::Line), + weight: 1, + }), + Box::new(SingleValueMatcher { + entity_name: "body", + matcher_method: "body_prefix", + matching_strategy: MatchingStrategy::Presence, + comparator: Box::new(BytesPrefixComparator::new(false)), + expectation: readers::expectations::body_prefix, + request_value: readers::request_value::body, + with_reason: true, + diff_with: Some(Tokenizer::Line), + weight: 1, + }), + Box::new(SingleValueMatcher { + entity_name: "body", + matcher_method: "body_prefix_not", + matching_strategy: MatchingStrategy::Absence, + comparator: Box::new(BytesPrefixComparator::new(true)), + expectation: readers::expectations::body_prefix_not, + request_value: readers::request_value::body, + with_reason: true, + diff_with: Some(Tokenizer::Line), + weight: 1, + }), + Box::new(SingleValueMatcher { + entity_name: "body", + matcher_method: "body_suffix", + matching_strategy: MatchingStrategy::Presence, + comparator: Box::new(BytesSuffixComparator::new(false)), + expectation: readers::expectations::body_suffix, + request_value: readers::request_value::body, + with_reason: true, + diff_with: Some(Tokenizer::Line), + weight: 1, + }), + Box::new(SingleValueMatcher { + entity_name: "body", + matcher_method: "body_suffix_not", + matching_strategy: MatchingStrategy::Absence, + comparator: Box::new(BytesSuffixComparator::new(true)), + expectation: readers::expectations::body_suffix_not, + request_value: readers::request_value::body, + with_reason: true, + diff_with: Some(Tokenizer::Line), + weight: 1, + }), + Box::new(SingleValueMatcher { + entity_name: "body", + matcher_method: "body_matches", + matching_strategy: MatchingStrategy::Presence, + comparator: Box::new(HttpMockBytesPatternComparator::new()), + expectation: readers::expectations::body_matches, + request_value: readers::request_value::body, + with_reason: true, + diff_with: Some(Tokenizer::Line), + weight: 1, + }), + //************************************************************************************ + // JSON body matchers + //************************************************************************************ + Box::new(SingleValueMatcher { + entity_name: "body", + matcher_method: "json_body", + matching_strategy: MatchingStrategy::Presence, + comparator: Box::new(JSONExactMatchComparator::new()), + expectation: readers::expectations::json_body, + request_value: readers::request_value::json_body, + with_reason: true, + diff_with: Some(Tokenizer::Line), + weight: 1, + }), + Box::new(SingleValueMatcher { + entity_name: "JSON body", + matcher_method: "json_body_includes", + matching_strategy: MatchingStrategy::Presence, + comparator: Box::new(JSONContainsMatchComparator::new(false)), + expectation: readers::expectations::json_body_includes, + request_value: readers::request_value::json_body, + with_reason: true, + diff_with: Some(Tokenizer::Line), + weight: 1, + }), + Box::new(SingleValueMatcher { + entity_name: "JSON body", + matcher_method: "json_body_excludes", + matching_strategy: MatchingStrategy::Absence, + comparator: Box::new(JSONContainsMatchComparator::new(true)), + expectation: readers::expectations::json_body_excludes, + request_value: readers::request_value::json_body, + with_reason: true, + diff_with: Some(Tokenizer::Line), + weight: 1, + }), + Box::new(FunctionValueMatcher { + entity_name: "custom matcher function", + matcher_function: "is_true", + comparator: Box::new(FunctionMatchesRequestComparator::new(false)), + expectation: readers::expectations::is_true, + request_value: readers::request_value::full_request, + weight: 1, + }), + Box::new(FunctionValueMatcher { + entity_name: "custom matcher function", + matcher_function: "is_false", + comparator: Box::new(FunctionMatchesRequestComparator::new(false)), + expectation: readers::expectations::is_true, + request_value: readers::request_value::full_request, + weight: 1, + }), + //************************************************************************************* + // x-www-form-urlencoded body + //************************************************************************************* + Box::new(MultiValueMatcher { + entity_name: "form-urlencoded body", + matcher_method: "form_urlencoded_tuple", + expectation: readers::expectations::form_urlencoded_tuple, + request_value: readers::request_value::form_urlencoded_body, + operator: KeyValueOperator::AND, + matching_strategy: MatchingStrategy::Presence, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(StringEqualsComparator::new(true, false)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "form-urlencoded body", + matcher_method: "form_urlencoded_tuple_not", + matching_strategy: MatchingStrategy::Absence, + operator: KeyValueOperator::IMPLICATION, + expectation: readers::expectations::form_urlencoded_tuple_not, + request_value: readers::request_value::form_urlencoded_body, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(StringEqualsComparator::new(true, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "form-urlencoded body", + matcher_method: "form_urlencoded_tuple_exists", + expectation: readers::expectations::form_urlencoded_key_exists, + request_value: readers::request_value::form_urlencoded_body, + operator: KeyValueOperator::AND, + matching_strategy: MatchingStrategy::Presence, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(AnyValueComparator::new()), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "form-urlencoded body", + matcher_method: "form_urlencoded_tuple_missing", + expectation: readers::expectations::form_urlencoded_key_missing, + request_value: readers::request_value::form_urlencoded_body, + operator: KeyValueOperator::AND, + matching_strategy: MatchingStrategy::Absence, + key_required: false, + key_comparator: Box::new(StringEqualsComparator::new(true, true)), + value_comparator: Box::new(AnyValueComparator::new()), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "form-urlencoded body", + matcher_method: "form_urlencoded_tuple_includes", + expectation: readers::expectations::form_urlencoded_includes, + request_value: readers::request_value::form_urlencoded_body, + operator: KeyValueOperator::AND, + matching_strategy: MatchingStrategy::Presence, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(StringContainsComparator::new(true, false)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "form-urlencoded body", + // TODO: Make text more understandable for the user (what excludes ? value? key?) + matcher_method: "form_urlencoded_tuple_excludes", + expectation: readers::expectations::form_urlencoded_excludes, + request_value: readers::request_value::form_urlencoded_body, + operator: KeyValueOperator::IMPLICATION, + matching_strategy: MatchingStrategy::Absence, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(StringContainsComparator::new(true, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "form-urlencoded body", + matcher_method: "form_urlencoded_tuple_prefix", + expectation: readers::expectations::form_urlencoded_prefix, + request_value: readers::request_value::form_urlencoded_body, + operator: KeyValueOperator::AND, + matching_strategy: MatchingStrategy::Presence, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(StringPrefixMatchComparator::new(true, false)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "form-urlencoded body", + matcher_method: "form_urlencoded_tuple_prefix_not", + expectation: readers::expectations::form_urlencoded_prefix_not, + request_value: readers::request_value::form_urlencoded_body, + operator: KeyValueOperator::IMPLICATION, + matching_strategy: MatchingStrategy::Absence, + key_required: true, + // TODO: ATTENTION: still false, because it is expected that the key appears in the request! + key_comparator: Box::new(StringEqualsComparator::new(false, false)), + value_comparator: Box::new(StringPrefixMatchComparator::new(true, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "form-urlencoded body", + matcher_method: "form_urlencoded_tuple_suffix", + operator: KeyValueOperator::AND, + expectation: readers::expectations::form_urlencoded_suffix, + request_value: readers::request_value::form_urlencoded_body, + matching_strategy: MatchingStrategy::Presence, + key_required: true, + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(StringSuffixMatchComparator::new(true, false)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "form-urlencoded body", + matcher_method: "form_urlencoded_tuple_suffix_not", + expectation: readers::expectations::form_urlencoded_suffix_not, + request_value: readers::request_value::form_urlencoded_body, + operator: KeyValueOperator::IMPLICATION, + matching_strategy: MatchingStrategy::Absence, + key_required: true, + // TODO: ATTENTION: still false, because it is expected that the key appears in the request! + key_comparator: Box::new(StringEqualsComparator::new(true, false)), + value_comparator: Box::new(StringSuffixMatchComparator::new(true, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueMatcher { + entity_name: "form-urlencoded body", + matcher_method: "form_urlencoded_tuple_matches", + operator: KeyValueOperator::AND, + expectation: readers::expectations::form_urlencoded_matches, + request_value: readers::request_value::form_urlencoded_body, + matching_strategy: MatchingStrategy::Presence, + key_required: true, + // TODO: ATTENTION: still false, because it is expected that the key appears in the request! + key_comparator: Box::new(StringPatternMatchComparator::new(false, true)), + value_comparator: Box::new(StringPatternMatchComparator::new(false, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + Box::new(MultiValueCountMatcher { + entity_name: "form-urlencoded body", + matcher_method: "form_urlencoded_tuple_count", + expectation: readers::expectations::form_urlencoded_key_value_count, + request_value: readers::request_value::form_urlencoded_body, + // TODO: ATTENTION: still false, because it is expected that the key appears in the request! + key_comparator: Box::new(StringPatternMatchComparator::new(false, true)), + value_comparator: Box::new(StringPatternMatchComparator::new(false, true)), + with_reason: true, + diff_with: None, + weight: 1, + }), + ] } pub trait Matcher { @@ -42,37 +1170,3 @@ pub trait Matcher { fn distance(&self, req: &HttpMockRequest, mock: &RequestRequirements) -> usize; fn mismatches(&self, req: &HttpMockRequest, mock: &RequestRequirements) -> Vec; } - -// ************************************************************************************************* -// Helper functions -// ************************************************************************************************* -#[cfg(feature = "cookies")] -pub(crate) fn parse_cookies(req: &HttpMockRequest) -> Result, String> { - let parsing_result = req.headers.as_ref().map_or(None, |request_headers| { - request_headers - .iter() - .find(|(k, _)| k.to_lowercase().eq("cookie")) - .map(|(k, v)| Cookie::parse(v)) - }); - - match parsing_result { - None => Ok(Vec::new()), - Some(res) => match res { - Err(err) => Err(err.to_string()), - Ok(vec) => Ok(vec - .into_iter() - .map(|c| (c.get_name().to_owned(), c.get_value().to_owned())) - .collect()), - }, - } -} - -pub(crate) fn distance_for(expected: &Option<&T>, actual: &Option<&U>) -> usize -where - T: Display, - U: Display, -{ - let expected = expected.map_or(String::new(), |x| x.to_string()); - let actual = actual.map_or(String::new(), |x| x.to_string()); - levenshtein::levenshtein(&expected, &actual) -} diff --git a/src/server/matchers/readers.rs b/src/server/matchers/readers.rs new file mode 100644 index 00000000..faec118e --- /dev/null +++ b/src/server/matchers/readers.rs @@ -0,0 +1,710 @@ +pub mod expectations { + use crate::{ + common::{ + data::{HttpMockRegex, RequestRequirements}, + util::HttpMockBytes, + }, + prelude::HttpMockRequest, + }; + use serde_json::Value; + use std::sync::Arc; + + #[inline] + pub fn scheme_equal_to(mock: &RequestRequirements) -> Option> { + mock.scheme.as_ref().map(|b| vec![b]) + } + + #[inline] + pub fn scheme_not_equal_to(mock: &RequestRequirements) -> Option> { + mock.scheme_not.as_ref().map(|b| vec![b]) + } + + #[inline] + pub fn method_equal_to(mock: &RequestRequirements) -> Option> { + mock.method.as_ref().map(|b| vec![b]) + } + + #[inline] + pub fn method_not_equal_to(mock: &RequestRequirements) -> Option> { + mock.method_not + .as_ref() + .map(|b| b.into_iter().map(|v| v).collect()) + } + + #[inline] + pub fn host_equal_to(mock: &RequestRequirements) -> Option> { + mock.host.as_ref().map(|b| vec![b]) + } + + #[inline] + pub fn host_not_equal_to(mock: &RequestRequirements) -> Option> { + mock.host_not.as_ref().map(|v| v.iter().collect()) + } + + #[inline] + pub fn host_includes(mock: &RequestRequirements) -> Option> { + mock.host_contains + .as_ref() + .map(|b| b.into_iter().map(|v| v).collect()) + } + + #[inline] + pub fn host_excludes(mock: &RequestRequirements) -> Option> { + mock.host_excludes + .as_ref() + .map(|b| b.into_iter().map(|v| v).collect()) + } + + #[inline] + pub fn host_prefix(mock: &RequestRequirements) -> Option> { + mock.host_prefix + .as_ref() + .map(|b| b.into_iter().map(|v| v).collect()) + } + + #[inline] + pub fn host_prefix_not(mock: &RequestRequirements) -> Option> { + mock.host_prefix_not + .as_ref() + .map(|b| b.into_iter().map(|v| v).collect()) + } + + #[inline] + pub fn host_has_suffix(mock: &RequestRequirements) -> Option> { + mock.host_suffix + .as_ref() + .map(|b| b.into_iter().map(|v| v).collect()) + } + + #[inline] + pub fn host_has_no_suffix(mock: &RequestRequirements) -> Option> { + mock.host_suffix_not + .as_ref() + .map(|b| b.into_iter().map(|v| v).collect()) + } + + #[inline] + pub fn host_matches_regex(mock: &RequestRequirements) -> Option> { + mock.host_matches + .as_ref() + .map(|b| b.into_iter().map(|v| v).collect()) + } + + #[inline] + pub fn port_equal_to(mock: &RequestRequirements) -> Option> { + mock.port.as_ref().map(|b| vec![b]) + } + + #[inline] + pub fn port_not_equal_to(mock: &RequestRequirements) -> Option> { + mock.port_not.as_ref().map(|v| v.iter().collect()) + } + + #[inline] + pub fn path_equal_to(mock: &RequestRequirements) -> Option> { + mock.path.as_ref().map(|b| vec![b]) + } + + #[inline] + pub fn path_not_equal_to(mock: &RequestRequirements) -> Option> { + mock.path_not.as_ref().map(|v| v.iter().collect()) + } + + #[inline] + pub fn path_includes(mock: &RequestRequirements) -> Option> { + mock.path_includes.as_ref().map(|v| v.iter().collect()) + } + + #[inline] + pub fn path_excludes(mock: &RequestRequirements) -> Option> { + mock.path_excludes.as_ref().map(|v| v.iter().collect()) + } + + #[inline] + pub fn path_prefix(mock: &RequestRequirements) -> Option> { + mock.path_prefix.as_ref().map(|v| v.iter().collect()) + } + + #[inline] + pub fn path_prefix_not(mock: &RequestRequirements) -> Option> { + mock.path_prefix_not.as_ref().map(|v| v.iter().collect()) + } + + #[inline] + pub fn path_suffix(mock: &RequestRequirements) -> Option> { + mock.path_suffix.as_ref().map(|v| v.iter().collect()) + } + + #[inline] + pub fn path_suffix_not(mock: &RequestRequirements) -> Option> { + mock.path_suffix_not.as_ref().map(|v| v.iter().collect()) + } + + #[inline] + pub fn path_matches(mock: &RequestRequirements) -> Option> { + mock.path_matches + .as_ref() + .map(|b| b.into_iter().map(|v| v).collect()) + } + + #[inline] + pub fn query_param(mock: &RequestRequirements) -> Option)>> { + mock.query_param + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn query_param_not(mock: &RequestRequirements) -> Option)>> { + mock.query_param_not + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn query_param_exists( + mock: &RequestRequirements, + ) -> Option)>> { + mock.query_param_exists + .as_ref() + .map(|v| v.into_iter().map(|v| (v, None)).collect()) + } + + #[inline] + pub fn query_param_missing( + mock: &RequestRequirements, + ) -> Option)>> { + mock.query_param_missing + .as_ref() + .map(|v| v.into_iter().map(|v| (v, None)).collect()) + } + + #[inline] + pub fn query_param_includes( + mock: &RequestRequirements, + ) -> Option)>> { + mock.query_param_includes + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn query_param_excludes( + mock: &RequestRequirements, + ) -> Option)>> { + mock.query_param_excludes + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn query_param_prefix( + mock: &RequestRequirements, + ) -> Option)>> { + mock.query_param_prefix + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn query_param_prefix_not( + mock: &RequestRequirements, + ) -> Option)>> { + mock.query_param_prefix_not + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn query_param_suffix( + mock: &RequestRequirements, + ) -> Option)>> { + mock.query_param_suffix + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn query_param_suffix_not( + mock: &RequestRequirements, + ) -> Option)>> { + mock.query_param_suffix_not + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn query_param_matches( + mock: &RequestRequirements, + ) -> Option)>> { + mock.query_param_matches + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn query_param_count( + mock: &RequestRequirements, + ) -> Option, Option<&HttpMockRegex>, usize)>> { + mock.query_param_count + .as_ref() + .map(|v| v.iter().map(|(k, v, c)| (Some(k), Some(v), *c)).collect()) + } + + #[inline] + pub fn header(mock: &RequestRequirements) -> Option)>> { + mock.header + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn header_not(mock: &RequestRequirements) -> Option)>> { + mock.header_not + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn header_exists(mock: &RequestRequirements) -> Option)>> { + mock.header_exists + .as_ref() + .map(|v| v.into_iter().map(|v| (v, None)).collect()) + } + + #[inline] + pub fn header_missing(mock: &RequestRequirements) -> Option)>> { + mock.header_missing + .as_ref() + .map(|v| v.into_iter().map(|v| (v, None)).collect()) + } + + #[inline] + pub fn header_includes(mock: &RequestRequirements) -> Option)>> { + mock.header_includes + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn header_excludes(mock: &RequestRequirements) -> Option)>> { + mock.header_excludes + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn header_prefix(mock: &RequestRequirements) -> Option)>> { + mock.header_prefix + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn header_prefix_not( + mock: &RequestRequirements, + ) -> Option)>> { + mock.header_prefix_not + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn header_suffix(mock: &RequestRequirements) -> Option)>> { + mock.header_suffix + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn header_suffix_not( + mock: &RequestRequirements, + ) -> Option)>> { + mock.header_suffix_not + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn header_matches( + mock: &RequestRequirements, + ) -> Option)>> { + mock.header_matches + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn header_count( + mock: &RequestRequirements, + ) -> Option, Option<&HttpMockRegex>, usize)>> { + mock.header_count + .as_ref() + .map(|v| v.iter().map(|(k, v, c)| (Some(k), Some(v), *c)).collect()) + } + + #[inline] + pub fn cookie(mock: &RequestRequirements) -> Option)>> { + mock.cookie + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn cookie_not(mock: &RequestRequirements) -> Option)>> { + mock.cookie_not + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn cookie_exists(mock: &RequestRequirements) -> Option)>> { + mock.cookie_exists + .as_ref() + .map(|v| v.into_iter().map(|v| (v, None)).collect()) + } + + #[inline] + pub fn cookie_missing(mock: &RequestRequirements) -> Option)>> { + mock.cookie_missing + .as_ref() + .map(|v| v.into_iter().map(|v| (v, None)).collect()) + } + + #[inline] + pub fn cookie_includes(mock: &RequestRequirements) -> Option)>> { + mock.cookie_includes + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn cookie_excludes(mock: &RequestRequirements) -> Option)>> { + mock.cookie_excludes + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn cookie_prefix(mock: &RequestRequirements) -> Option)>> { + mock.cookie_prefix + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn cookie_prefix_not( + mock: &RequestRequirements, + ) -> Option)>> { + mock.cookie_prefix_not + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn cookie_suffix(mock: &RequestRequirements) -> Option)>> { + mock.cookie_suffix + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn cookie_suffix_not( + mock: &RequestRequirements, + ) -> Option)>> { + mock.cookie_suffix_not + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn cookie_matches( + mock: &RequestRequirements, + ) -> Option)>> { + mock.cookie_matches + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn cookie_count( + mock: &RequestRequirements, + ) -> Option, Option<&HttpMockRegex>, usize)>> { + mock.cookie_count + .as_ref() + .map(|v| v.iter().map(|(k, v, c)| (Some(k), Some(v), *c)).collect()) + } + + #[inline] + pub fn body(mock: &RequestRequirements) -> Option> { + mock.body.as_ref().map(|b| vec![b]) + } + + #[inline] + pub fn body_not(mock: &RequestRequirements) -> Option> { + mock.body_not.as_ref().map(|v| v.iter().collect()) + } + + #[inline] + pub fn body_includes(mock: &RequestRequirements) -> Option> { + mock.body_includes.as_ref().map(|v| v.iter().collect()) + } + + #[inline] + pub fn body_excludes(mock: &RequestRequirements) -> Option> { + mock.body_excludes.as_ref().map(|v| v.iter().collect()) + } + + #[inline] + pub fn body_prefix(mock: &RequestRequirements) -> Option> { + mock.body_prefix.as_ref().map(|v| v.iter().collect()) + } + + #[inline] + pub fn body_prefix_not(mock: &RequestRequirements) -> Option> { + mock.body_prefix_not.as_ref().map(|v| v.iter().collect()) + } + + #[inline] + pub fn body_suffix(mock: &RequestRequirements) -> Option> { + mock.body_suffix.as_ref().map(|v| v.iter().collect()) + } + + #[inline] + pub fn body_suffix_not(mock: &RequestRequirements) -> Option> { + mock.body_suffix_not.as_ref().map(|v| v.iter().collect()) + } + + #[inline] + pub fn body_matches(mock: &RequestRequirements) -> Option> { + mock.body_matches + .as_ref() + .map(|b| b.into_iter().map(|v| v).collect()) + } + + #[inline] + pub fn json_body(mock: &RequestRequirements) -> Option> { + mock.json_body.as_ref().map(|b| vec![b]) + } + + #[inline] + pub fn json_body_includes(mock: &RequestRequirements) -> Option> { + mock.json_body_includes + .as_ref() + .map(|b| b.into_iter().collect()) + } + + #[inline] + pub fn json_body_excludes(mock: &RequestRequirements) -> Option> { + mock.json_body_excludes + .as_ref() + .map(|b| b.into_iter().collect()) + } + + #[inline] + pub fn is_true( + mock: &RequestRequirements, + ) -> Option bool + 'static + Sync + Send>>> { + mock.is_true.as_ref().map(|b| b.iter().map(|f| f).collect()) + } + + pub fn form_urlencoded_tuple( + mock: &RequestRequirements, + ) -> Option)>> { + mock.form_urlencoded_tuple + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + pub fn form_urlencoded_tuple_not( + mock: &RequestRequirements, + ) -> Option)>> { + mock.form_urlencoded_tuple_not + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + pub fn form_urlencoded_key_exists( + mock: &RequestRequirements, + ) -> Option)>> { + mock.form_urlencoded_tuple_exists + .as_ref() + .map(|v| v.into_iter().map(|v| (v, None)).collect()) + } + + pub fn form_urlencoded_key_missing( + mock: &RequestRequirements, + ) -> Option)>> { + mock.form_urlencoded_tuple_missing + .as_ref() + .map(|v| v.into_iter().map(|v| (v, None)).collect()) + } + + #[inline] + pub fn form_urlencoded_includes( + mock: &RequestRequirements, + ) -> Option)>> { + mock.form_urlencoded_tuple_includes + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn form_urlencoded_excludes( + mock: &RequestRequirements, + ) -> Option)>> { + mock.form_urlencoded_tuple_excludes + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn form_urlencoded_prefix( + mock: &RequestRequirements, + ) -> Option)>> { + mock.form_urlencoded_tuple_prefix + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn form_urlencoded_prefix_not( + mock: &RequestRequirements, + ) -> Option)>> { + mock.form_urlencoded_tuple_prefix_not + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn form_urlencoded_suffix( + mock: &RequestRequirements, + ) -> Option)>> { + mock.form_urlencoded_tuple_suffix + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn form_urlencoded_suffix_not( + mock: &RequestRequirements, + ) -> Option)>> { + mock.form_urlencoded_tuple_suffix_not + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn form_urlencoded_matches( + mock: &RequestRequirements, + ) -> Option)>> { + mock.form_urlencoded_tuple_matches + .as_ref() + .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) + } + + #[inline] + pub fn form_urlencoded_key_value_count( + mock: &RequestRequirements, + ) -> Option, Option<&HttpMockRegex>, usize)>> { + mock.form_urlencoded_tuple_count + .as_ref() + .map(|v| v.iter().map(|(k, v, c)| (Some(k), Some(v), *c)).collect()) + } +} + +pub mod request_value { + use crate::{common::util::HttpMockBytes, prelude::HttpMockRequest}; + use serde_json::Value; + + #[inline] + pub fn scheme(req: &HttpMockRequest) -> Option { + Some(req.scheme()) + } + + #[inline] + pub fn method(req: &HttpMockRequest) -> Option { + Some(req.method().to_string()) + } + + #[inline] + pub fn host(req: &HttpMockRequest) -> Option { + req.host().map(|h| h.to_string()) + } + + #[inline] + pub fn port(req: &HttpMockRequest) -> Option { + Some(req.port()) + } + + #[inline] + pub fn path(req: &HttpMockRequest) -> Option { + Some(req.uri().path().to_string()) + } + + #[inline] + pub fn query_params(req: &HttpMockRequest) -> Option)>> { + Some( + req.query_params_vec() + .iter() + .map(|(k, v)| (k.into(), Some(v.into()))) + .collect(), + ) + } + + #[inline] + pub fn headers(req: &HttpMockRequest) -> Option)>> { + Some( + req.headers_vec() + .iter() + .map(|(k, v)| (k.into(), Some(v.into()))) + .collect(), + ) + } + + #[inline] + pub fn cookies(req: &HttpMockRequest) -> Option)>> { + Some( + req.cookies() + .expect("cannot parse cookies") + .iter() + .map(|(k, v)| (k.into(), Some(v.into()))) + .collect(), + ) + } + + #[inline] + pub fn body(req: &HttpMockRequest) -> Option { + Some(req.body().clone()) + } + + #[inline] + pub fn json_body(req: &HttpMockRequest) -> Option { + let body = req.body_ref(); + if body.len() == 0 { + () + } + + match serde_json::from_slice(body) { + Err(e) => { + log::trace!("Cannot parse json value: {}", e); + None + } + Ok(v) => Some(v), + } + } + + pub fn form_urlencoded_body(req: &HttpMockRequest) -> Option)>> { + Some( + form_urlencoded::parse(req.body_ref()) + .into_owned() + .map(|(k, v)| (k, Some(v))) + .collect(), + ) + } + + #[inline] + pub fn full_request(req: &HttpMockRequest) -> Option<&HttpMockRequest> { + Some(req) + } +} diff --git a/src/server/matchers/sources.rs b/src/server/matchers/sources.rs deleted file mode 100644 index 23d7f11f..00000000 --- a/src/server/matchers/sources.rs +++ /dev/null @@ -1,378 +0,0 @@ -use std::collections::BTreeMap; - -use serde_json::Value; - -use crate::common::data::{MockMatcherFunction, RequestRequirements}; -use crate::Regex; - -pub(crate) trait ValueRefSource { - fn parse_from_mock<'a>(&self, mock: &'a RequestRequirements) -> Option>; -} - -pub(crate) trait MultiValueSource { - fn parse_from_mock<'a>( - &self, - mock: &'a RequestRequirements, - ) -> Option)>>; -} - -// ************************************************************************************************ -// StringBodySource -// ************************************************************************************************ -pub(crate) struct StringBodySource {} - -impl StringBodySource { - pub fn new() -> Self { - Self {} - } -} - -impl ValueRefSource for StringBodySource { - fn parse_from_mock<'a>(&self, mock: &'a RequestRequirements) -> Option> { - mock.body.as_ref().map(|b| vec![b]) - } -} - -// ************************************************************************************************ -// StringBodySource -// ************************************************************************************************ -pub(crate) struct StringBodyContainsSource {} - -impl StringBodyContainsSource { - pub fn new() -> Self { - Self {} - } -} - -impl ValueRefSource for StringBodyContainsSource { - fn parse_from_mock<'a>(&self, mock: &'a RequestRequirements) -> Option> { - mock.body_contains - .as_ref() - .map(|v| v.into_iter().map(|bc| bc).collect()) - } -} - -// ************************************************************************************************ -// BodyRegexSource -// ************************************************************************************************ -pub(crate) struct JSONBodySource {} - -impl JSONBodySource { - pub fn new() -> Self { - Self {} - } -} - -impl ValueRefSource for JSONBodySource { - fn parse_from_mock<'a>(&self, mock: &'a RequestRequirements) -> Option> { - mock.json_body.as_ref().map(|b| vec![b]) - } -} - -// ************************************************************************************************ -// PartialJSONBodySource -// ************************************************************************************************ -pub(crate) struct PartialJSONBodySource {} - -impl PartialJSONBodySource { - pub fn new() -> Self { - Self {} - } -} - -impl ValueRefSource for PartialJSONBodySource { - fn parse_from_mock<'a>(&self, mock: &'a RequestRequirements) -> Option> { - mock.json_body_includes - .as_ref() - .map(|b| b.into_iter().collect()) - } -} - -// ************************************************************************************************ -// BodyRegexSource -// ************************************************************************************************ -pub(crate) struct BodyRegexSource {} - -impl BodyRegexSource { - pub fn new() -> Self { - Self {} - } -} - -impl ValueRefSource for BodyRegexSource { - fn parse_from_mock<'a>(&self, mock: &'a RequestRequirements) -> Option> { - mock.body_matches - .as_ref() - .map(|b| b.iter().map(|p| &p.regex).collect()) - } -} - -// ************************************************************************************************ -// MethodSource -// ************************************************************************************************ -pub(crate) struct MethodSource {} - -impl MethodSource { - pub fn new() -> Self { - Self {} - } -} - -impl ValueRefSource for MethodSource { - fn parse_from_mock<'a>(&self, mock: &'a RequestRequirements) -> Option> { - mock.method.as_ref().map(|b| vec![b]) - } -} - -// ************************************************************************************************ -// StringPathSource -// ************************************************************************************************ -pub(crate) struct StringPathSource {} - -impl StringPathSource { - pub fn new() -> Self { - Self {} - } -} - -impl ValueRefSource for StringPathSource { - fn parse_from_mock<'a>(&self, mock: &'a RequestRequirements) -> Option> { - mock.path.as_ref().map(|b| vec![b]) - } -} - -// ************************************************************************************************ -// StringPathContainsSource -// ************************************************************************************************ -pub(crate) struct PathContainsSubstringSource {} - -impl PathContainsSubstringSource { - pub fn new() -> Self { - Self {} - } -} - -impl ValueRefSource for PathContainsSubstringSource { - fn parse_from_mock<'a>(&self, mock: &'a RequestRequirements) -> Option> { - mock.path_contains - .as_ref() - .map(|b| b.into_iter().map(|v| v).collect()) - } -} - -// ************************************************************************************************ -// PathRegexSource -// ************************************************************************************************ -pub(crate) struct PathRegexSource {} - -impl PathRegexSource { - pub fn new() -> Self { - Self {} - } -} - -impl ValueRefSource for PathRegexSource { - fn parse_from_mock<'a>(&self, mock: &'a RequestRequirements) -> Option> { - mock.path_matches - .as_ref() - .map(|b| b.into_iter().map(|v| &v.regex).collect()) - } -} - -// ************************************************************************************************ -// CookieSource -// ************************************************************************************************ -pub(crate) struct CookieSource {} - -impl CookieSource { - pub fn new() -> Self { - Self {} - } -} - -impl MultiValueSource for CookieSource { - fn parse_from_mock<'a>( - &self, - mock: &'a RequestRequirements, - ) -> Option)>> { - mock.cookies - .as_ref() - .map(|c| c.iter().map(|(k, v)| (k, Some(v))).collect()) - } -} - -// ************************************************************************************************ -// ContainsCookieSource -// ************************************************************************************************ -pub(crate) struct ContainsCookieSource {} - -impl ContainsCookieSource { - pub fn new() -> Self { - Self {} - } -} - -impl MultiValueSource for ContainsCookieSource { - fn parse_from_mock<'a>( - &self, - mock: &'a RequestRequirements, - ) -> Option)>> { - mock.cookie_exists - .as_ref() - .map(|c| c.iter().map(|v| (v, None)).collect()) - } -} - -// ************************************************************************************************ -// HeaderSource -// ************************************************************************************************ -pub(crate) struct HeaderSource {} - -impl HeaderSource { - pub fn new() -> Self { - Self {} - } -} - -impl MultiValueSource for HeaderSource { - fn parse_from_mock<'a>( - &self, - mock: &'a RequestRequirements, - ) -> Option)>> { - mock.headers - .as_ref() - .map(|c| c.iter().map(|(k, v)| (k, Some(v))).collect()) - } -} - -// ************************************************************************************************ -// ContainsCookieSource -// ************************************************************************************************ -pub(crate) struct ContainsHeaderSource {} - -impl ContainsHeaderSource { - pub fn new() -> Self { - Self {} - } -} - -impl MultiValueSource for ContainsHeaderSource { - fn parse_from_mock<'a>( - &self, - mock: &'a RequestRequirements, - ) -> Option)>> { - mock.header_exists - .as_ref() - .map(|c| c.iter().map(|v| (v, None)).collect()) - } -} - -// ************************************************************************************************ -// QueryParameterSource -// ************************************************************************************************ -pub(crate) struct QueryParameterSource {} - -impl QueryParameterSource { - pub fn new() -> Self { - Self {} - } -} - -impl MultiValueSource for QueryParameterSource { - fn parse_from_mock<'a>( - &self, - mock: &'a RequestRequirements, - ) -> Option)>> { - mock.query_param - .as_ref() - .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) - } -} - -// ************************************************************************************************ -// ContainsQueryParameterSource -// ************************************************************************************************ -pub(crate) struct ContainsQueryParameterSource {} - -impl ContainsQueryParameterSource { - pub fn new() -> Self { - Self {} - } -} - -impl MultiValueSource for ContainsQueryParameterSource { - fn parse_from_mock<'a>( - &self, - mock: &'a RequestRequirements, - ) -> Option)>> { - mock.query_param_exists - .as_ref() - .map(|v| v.into_iter().map(|v| (v, None)).collect()) - } -} - -// ************************************************************************************************ -// QueryParameterSource -// ************************************************************************************************ -pub(crate) struct XWWWFormUrlencodedSource {} - -impl XWWWFormUrlencodedSource { - pub fn new() -> Self { - Self {} - } -} - -impl MultiValueSource for XWWWFormUrlencodedSource { - fn parse_from_mock<'a>( - &self, - mock: &'a RequestRequirements, - ) -> Option)>> { - mock.x_www_form_urlencoded - .as_ref() - .map(|v| v.into_iter().map(|(k, v)| (k, Some(v))).collect()) - } -} - -// ************************************************************************************************ -// ContainsQueryParameterSource -// ************************************************************************************************ -pub(crate) struct ContainsXWWWFormUrlencodedKeySource {} - -impl ContainsXWWWFormUrlencodedKeySource { - pub fn new() -> Self { - Self {} - } -} - -impl MultiValueSource for ContainsXWWWFormUrlencodedKeySource { - fn parse_from_mock<'a>( - &self, - mock: &'a RequestRequirements, - ) -> Option)>> { - mock.x_www_form_urlencoded_key_exists - .as_ref() - .map(|v| v.into_iter().map(|v| (v, None)).collect()) - } -} - -// ************************************************************************************************ -// FunctionSource -// ************************************************************************************************ -pub(crate) struct FunctionSource {} - -impl FunctionSource { - pub fn new() -> Self { - Self {} - } -} - -impl ValueRefSource for FunctionSource { - fn parse_from_mock<'a>( - &self, - mock: &'a RequestRequirements, - ) -> Option> { - mock.matchers - .as_ref() - .map(|b| b.iter().map(|f| f).collect()) - } -} diff --git a/src/server/matchers/targets.rs b/src/server/matchers/targets.rs deleted file mode 100644 index 1fafc6fc..00000000 --- a/src/server/matchers/targets.rs +++ /dev/null @@ -1,214 +0,0 @@ -use std::cell::RefCell; -use std::collections::BTreeMap; - -use serde_json::Value; - -use crate::common::data::HttpMockRequest; -use crate::server::matchers; - -pub(crate) trait ValueTarget { - fn parse_from_request(&self, req: &HttpMockRequest) -> Option; -} - -pub(crate) trait ValueRefTarget { - fn parse_from_request<'a>(&self, req: &'a HttpMockRequest) -> Option<&'a T>; -} - -pub(crate) trait MultiValueTarget { - fn parse_from_request(&self, req: &HttpMockRequest) -> Option)>>; -} - -// ************************************************************************************* -// StringBodyTarget -// ************************************************************************************* -pub(crate) struct StringBodyTarget {} - -impl StringBodyTarget { - pub fn new() -> Self { - Self {} - } -} - -impl ValueTarget for StringBodyTarget { - fn parse_from_request(&self, req: &HttpMockRequest) -> Option { - req.body - .as_ref() - .map(|b| String::from_utf8_lossy(b).to_string()) // FIXME: Avoid copying here. Create a "ValueRefTarget". - } -} - -// ************************************************************************************* -// JSONBodyTarget -// ************************************************************************************* -pub(crate) struct JSONBodyTarget {} - -impl JSONBodyTarget { - pub fn new() -> Self { - Self {} - } -} - -impl ValueTarget for JSONBodyTarget { - fn parse_from_request(&self, req: &HttpMockRequest) -> Option { - let body = req.body.as_ref(); - if body.is_none() { - return None; - } - - match serde_json::from_slice(body.unwrap()) { - Err(e) => { - log::trace!("Cannot parse json value: {}", e); - None - } - Ok(v) => Some(v), - } - } -} - -// ************************************************************************************* -// CookieTarget -// ************************************************************************************* -#[cfg(feature = "cookies")] -pub(crate) struct CookieTarget {} - -#[cfg(feature = "cookies")] -impl CookieTarget { - pub fn new() -> Self { - Self {} - } -} - -#[cfg(feature = "cookies")] -impl MultiValueTarget for CookieTarget { - fn parse_from_request(&self, req: &HttpMockRequest) -> Option)>> { - let req_cookies = match matchers::parse_cookies(req) { - Ok(v) => v, - Err(err) => { - log::info!( - "Cannot parse cookies. Cookie matching will not work for this request. Error: {}", - err - ); - return None; - } - }; - - Some(req_cookies.into_iter().map(|(k, v)| (k, Some(v))).collect()) - } -} - -// ************************************************************************************* -// HeaderTarget -// ************************************************************************************* -pub(crate) struct HeaderTarget {} - -impl HeaderTarget { - pub fn new() -> Self { - Self {} - } -} - -impl MultiValueTarget for HeaderTarget { - fn parse_from_request(&self, req: &HttpMockRequest) -> Option)>> { - req.headers.as_ref().map(|headers| { - headers - .into_iter() - .map(|(k, v)| (k.to_string(), Some(v.to_string()))) - .collect() - }) - } -} - -// ************************************************************************************* -// HeaderTarget -// ************************************************************************************* -pub(crate) struct QueryParameterTarget {} - -impl QueryParameterTarget { - pub fn new() -> Self { - Self {} - } -} - -impl MultiValueTarget for QueryParameterTarget { - fn parse_from_request(&self, req: &HttpMockRequest) -> Option)>> { - req.query_params.as_ref().map(|headers| { - headers - .into_iter() - .map(|(k, v)| (k.to_string(), Some(v.to_string()))) - .collect() - }) - } -} - -// ************************************************************************************* -// PathTarget -// ************************************************************************************* -pub(crate) struct PathTarget {} - -impl PathTarget { - pub fn new() -> Self { - Self {} - } -} - -impl ValueTarget for PathTarget { - fn parse_from_request(&self, req: &HttpMockRequest) -> Option { - Some(req.path.to_string()) // FIXME: Avoid copying here. Create a "ValueRefTarget". - } -} - -// ************************************************************************************* -// MethodTarget -// ************************************************************************************* -pub(crate) struct MethodTarget {} - -impl MethodTarget { - pub fn new() -> Self { - Self {} - } -} - -impl ValueTarget for MethodTarget { - fn parse_from_request(&self, req: &HttpMockRequest) -> Option { - Some(req.method.to_string()) // FIXME: Avoid copying here. Create a "ValueRefTarget". - } -} - -// ************************************************************************************* -// FullRequestTarget -// ************************************************************************************* -pub(crate) struct FullRequestTarget {} - -impl FullRequestTarget { - pub fn new() -> Self { - Self {} - } -} - -impl ValueRefTarget for FullRequestTarget { - fn parse_from_request<'a>(&self, req: &'a HttpMockRequest) -> Option<&'a HttpMockRequest> { - Some(req) - } -} - -// ************************************************************************************* -// XWWWFormUrlEncodedBodyTarget -// ************************************************************************************* -pub(crate) struct XWWWFormUrlEncodedBodyTarget {} - -impl XWWWFormUrlEncodedBodyTarget { - pub fn new() -> Self { - Self {} - } -} - -impl MultiValueTarget for XWWWFormUrlEncodedBodyTarget { - fn parse_from_request(&self, req: &HttpMockRequest) -> Option)>> { - req.body.as_ref().map(|body| { - form_urlencoded::parse(body) - .into_owned() - .map(|(k, v)| (k, Some(v))) - .collect() - }) - } -} diff --git a/src/server/matchers/transformers.rs b/src/server/matchers/transformers.rs deleted file mode 100644 index a835b701..00000000 --- a/src/server/matchers/transformers.rs +++ /dev/null @@ -1,84 +0,0 @@ -pub(crate) trait Transformer { - fn transform(&self, v: &I) -> Result; -} - -// ************************************************************************************************ -// Base64ValueTransformer -// ************************************************************************************************ -pub(crate) struct DecodeBase64ValueTransformer {} - -impl DecodeBase64ValueTransformer { - pub fn new() -> Self { - Self {} - } -} - -impl Transformer for DecodeBase64ValueTransformer { - fn transform(&self, v: &String) -> Result { - base64::decode(v) - .map(|t| String::from_utf8_lossy(&t.as_slice()).into()) - .map_err(|err| err.to_string()) - } -} - -// ************************************************************************************************ -// Base64ValueTransformer -// ************************************************************************************************ -pub(crate) struct ToLowercaseTransformer {} - -impl ToLowercaseTransformer { - pub fn new() -> Self { - Self {} - } -} - -impl Transformer for ToLowercaseTransformer { - fn transform(&self, v: &String) -> Result { - Ok(v.to_lowercase()) - } -} - -#[cfg(test)] -mod test { - use crate::server::matchers::transformers::{ - DecodeBase64ValueTransformer, ToLowercaseTransformer, Transformer, - }; - - #[test] - fn base64_decode_transformer() { - // Arrange - let transformer = DecodeBase64ValueTransformer::new(); - - // Act - let result = transformer.transform(&"dGVzdA==".to_string()); - - // Assert - assert_eq!(result.is_ok(), true); - assert_eq!(result.unwrap(), "test".to_string()); - } - - #[test] - fn base64_decode_transformer_error() { - // Arrange - let transformer = DecodeBase64ValueTransformer::new(); - - // Act - let result = transformer.transform(&"xÿ".to_string()); - - // Assert - assert_eq!(result.is_err(), true); - } - - #[test] - fn to_lowercase_transformer() { - // Arrange - let transformer = ToLowercaseTransformer::new(); - - // Act - let result = transformer.transform(&"HeLlO".to_string()); - - // Assert - assert_eq!(result.is_ok(), true); - assert_eq!(result.unwrap(), "hello".to_string()); - } -} diff --git a/src/server/mod.rs b/src/server/mod.rs index ef8ef149..862d8c1a 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,776 +1,43 @@ #![allow(clippy::trivial_regex)] +use std::{borrow::Borrow, str::FromStr}; -use std::borrow::Borrow; -use std::collections::BTreeMap; -use std::net::SocketAddr; -use std::str::FromStr; -use std::sync::atomic::AtomicUsize; -use std::sync::atomic::Ordering::Relaxed; -use std::sync::{Arc, Mutex}; - -use hyper::body::Buf; -use hyper::header::HeaderValue; -use hyper::http::header::HeaderName; -use hyper::service::{make_service_fn, service_fn}; -use hyper::{ - Body, HeaderMap, Request as HyperRequest, Response as HyperResponse, Result as HyperResult, - Server, StatusCode, -}; -use regex::Regex; - -use matchers::generic::SingleValueMatcher; -use matchers::targets::{JSONBodyTarget, StringBodyTarget}; - -use crate::common::data::{ActiveMock, HttpMockRequest, Tokenizer}; -use crate::server::matchers::comparators::{ - AnyValueComparator, FunctionMatchesRequestComparator, JSONContainsMatchComparator, - JSONExactMatchComparator, StringContainsMatchComparator, StringExactMatchComparator, - StringRegexMatchComparator, -}; -use crate::server::matchers::generic::{FunctionValueMatcher, MultiValueMatcher}; -use crate::server::matchers::sources::{ - BodyRegexSource, ContainsCookieSource, ContainsHeaderSource, ContainsQueryParameterSource, - ContainsXWWWFormUrlencodedKeySource, CookieSource, FunctionSource, HeaderSource, - JSONBodySource, MethodSource, PartialJSONBodySource, PathContainsSubstringSource, - PathRegexSource, QueryParameterSource, StringBodyContainsSource, StringBodySource, - StringPathSource, XWWWFormUrlencodedSource, -}; -#[cfg(feature = "cookies")] -use crate::server::matchers::targets::CookieTarget; -use crate::server::matchers::targets::{ - FullRequestTarget, HeaderTarget, MethodTarget, PathTarget, QueryParameterTarget, - XWWWFormUrlEncodedBodyTarget, -}; use crate::server::matchers::Matcher; -use crate::server::web::routes; +use bytes::Bytes; use futures_util::task::Spawn; -use std::future::Future; -use std::iter::Map; -use std::time::Instant; +use hyper::body::{Body, Buf}; +use std::{future::Future, net::SocketAddr}; -mod matchers; +use futures_util::{FutureExt, TryStreamExt}; +use http_body_util::BodyExt; +mod builder; +mod handler; +pub mod matchers; +mod server; +pub mod state; mod util; -pub(crate) mod web; - -/// The shared state accessible to all handlers -pub struct MockServerState { - id_counter: AtomicUsize, - history_limit: usize, - pub mocks: Mutex>, - pub history: Mutex>>, - pub matchers: Vec>, -} - -impl MockServerState { - pub fn create_new_id(&self) -> usize { - self.id_counter.fetch_add(1, Relaxed) - } - - pub fn new(history_limit: usize) -> Self { - MockServerState { - mocks: Mutex::new(BTreeMap::new()), - history_limit, - history: Mutex::new(Vec::new()), - id_counter: AtomicUsize::new(0), - matchers: vec![ - // path exact - Box::new(SingleValueMatcher { - entity_name: "path", - comparator: Box::new(StringExactMatchComparator::new(false)), - source: Box::new(StringPathSource::new()), - target: Box::new(PathTarget::new()), - transformer: None, - with_reason: true, - diff_with: None, - weight: 10, - }), - // path contains - Box::new(SingleValueMatcher { - entity_name: "path", - comparator: Box::new(StringContainsMatchComparator::new(true)), - source: Box::new(PathContainsSubstringSource::new()), - target: Box::new(PathTarget::new()), - transformer: None, - with_reason: true, - diff_with: None, - weight: 10, - }), - // path matches regex - Box::new(SingleValueMatcher { - entity_name: "path", - comparator: Box::new(StringRegexMatchComparator::new()), - source: Box::new(PathRegexSource::new()), - target: Box::new(PathTarget::new()), - transformer: None, - with_reason: true, - diff_with: None, - weight: 10, - }), - // method exact - Box::new(SingleValueMatcher { - entity_name: "method", - comparator: Box::new(StringExactMatchComparator::new(false)), - source: Box::new(MethodSource::new()), - target: Box::new(MethodTarget::new()), - transformer: None, - with_reason: true, - diff_with: None, - weight: 3, - }), - // Query Param exact - Box::new(MultiValueMatcher { - entity_name: "query parameter", - key_comparator: Box::new(StringExactMatchComparator::new(true)), - value_comparator: Box::new(StringExactMatchComparator::new(true)), - key_transformer: None, - value_transformer: None, - source: Box::new(QueryParameterSource::new()), - target: Box::new(QueryParameterTarget::new()), - with_reason: true, - diff_with: None, - weight: 1, - }), - // Query Param exists - Box::new(MultiValueMatcher { - entity_name: "query parameter", - key_comparator: Box::new(StringExactMatchComparator::new(true)), - value_comparator: Box::new(AnyValueComparator::new()), - key_transformer: None, - value_transformer: None, - source: Box::new(ContainsQueryParameterSource::new()), - target: Box::new(QueryParameterTarget::new()), - with_reason: true, - diff_with: None, - weight: 1, - }), - // Cookie exact - #[cfg(feature = "cookies")] - Box::new(MultiValueMatcher { - entity_name: "cookie", - key_comparator: Box::new(StringExactMatchComparator::new(true)), - value_comparator: Box::new(StringExactMatchComparator::new(true)), - key_transformer: None, - value_transformer: None, - source: Box::new(CookieSource::new()), - target: Box::new(CookieTarget::new()), - with_reason: true, - diff_with: None, - weight: 1, - }), - // Cookie exists - #[cfg(feature = "cookies")] - Box::new(MultiValueMatcher { - entity_name: "cookie", - key_comparator: Box::new(StringExactMatchComparator::new(true)), - value_comparator: Box::new(AnyValueComparator::new()), - key_transformer: None, - value_transformer: None, - source: Box::new(ContainsCookieSource::new()), - target: Box::new(CookieTarget::new()), - with_reason: true, - diff_with: None, - weight: 1, - }), - // Header exact - Box::new(MultiValueMatcher { - entity_name: "header", - key_comparator: Box::new(StringExactMatchComparator::new(false)), - value_comparator: Box::new(StringExactMatchComparator::new(true)), - key_transformer: None, - value_transformer: None, - source: Box::new(HeaderSource::new()), - target: Box::new(HeaderTarget::new()), - with_reason: true, - diff_with: None, - weight: 1, - }), - // Header exists - Box::new(MultiValueMatcher { - entity_name: "header", - key_comparator: Box::new(StringExactMatchComparator::new(false)), - value_comparator: Box::new(AnyValueComparator::new()), - key_transformer: None, - value_transformer: None, - source: Box::new(ContainsHeaderSource::new()), - target: Box::new(HeaderTarget::new()), - with_reason: true, - diff_with: None, - weight: 1, - }), - // Box::new(CustomFunctionMatcher::new(1.0)), - // string body exact - Box::new(SingleValueMatcher { - entity_name: "body", - comparator: Box::new(StringExactMatchComparator::new(false)), - source: Box::new(StringBodySource::new()), - target: Box::new(StringBodyTarget::new()), - transformer: None, - with_reason: false, - diff_with: Some(Tokenizer::Line), - weight: 1, - }), - // string body contains - Box::new(SingleValueMatcher { - entity_name: "body", - comparator: Box::new(StringContainsMatchComparator::new(true)), - source: Box::new(StringBodyContainsSource::new()), - target: Box::new(StringBodyTarget::new()), - transformer: None, - with_reason: false, - diff_with: Some(Tokenizer::Line), - weight: 1, - }), - // string body regex - Box::new(SingleValueMatcher { - entity_name: "body", - comparator: Box::new(StringRegexMatchComparator::new()), - source: Box::new(BodyRegexSource::new()), - target: Box::new(StringBodyTarget::new()), - transformer: None, - with_reason: false, - diff_with: Some(Tokenizer::Line), - weight: 1, - }), - // JSON body contains - Box::new(SingleValueMatcher { - entity_name: "body", - comparator: Box::new(JSONContainsMatchComparator::new()), - source: Box::new(PartialJSONBodySource::new()), - target: Box::new(JSONBodyTarget::new()), - transformer: None, - with_reason: false, - diff_with: Some(Tokenizer::Line), - weight: 1, - }), - // JSON body exact - Box::new(SingleValueMatcher { - entity_name: "body", - comparator: Box::new(JSONExactMatchComparator::new()), - source: Box::new(JSONBodySource::new()), - target: Box::new(JSONBodyTarget::new()), - transformer: None, - with_reason: true, - diff_with: Some(Tokenizer::Line), - weight: 1, - }), - // Query Param exact - Box::new(MultiValueMatcher { - entity_name: "x-www-form-urlencoded body tuple", - key_comparator: Box::new(StringExactMatchComparator::new(true)), - value_comparator: Box::new(StringExactMatchComparator::new(true)), - key_transformer: None, - value_transformer: None, - source: Box::new(XWWWFormUrlencodedSource::new()), - target: Box::new(XWWWFormUrlEncodedBodyTarget::new()), - with_reason: true, - diff_with: None, - weight: 1, - }), - // Query Param exists - Box::new(MultiValueMatcher { - entity_name: "x-www-form-urlencoded body tuple", - key_comparator: Box::new(StringExactMatchComparator::new(true)), - value_comparator: Box::new(AnyValueComparator::new()), - key_transformer: None, - value_transformer: None, - source: Box::new(ContainsXWWWFormUrlencodedKeySource::new()), - target: Box::new(XWWWFormUrlEncodedBodyTarget::new()), - with_reason: true, - diff_with: None, - weight: 1, - }), - // User provided matcher function - Box::new(FunctionValueMatcher { - entity_name: "user provided matcher function", - comparator: Box::new(FunctionMatchesRequestComparator::new()), - source: Box::new(FunctionSource::new()), - target: Box::new(FullRequestTarget::new()), - transformer: None, - weight: 1, - }), - ], - } - } -} - -impl Default for MockServerState { - fn default() -> Self { - MockServerState::new(usize::MAX) - } -} - -type GenericError = Box; - -#[derive(Default, Debug)] -pub(crate) struct ServerRequestHeader { - pub method: String, - pub path: String, - pub query: String, - pub headers: Vec<(String, String)>, -} - -impl ServerRequestHeader { - pub fn from(req: &HyperRequest) -> Result { - let headers = extract_headers(req.headers()); - if let Err(e) = headers { - return Err(format!("error parsing headers: {}", e)); - } - - let method = req.method().as_str().to_string(); - let path = req.uri().path().to_string(); - let query = req.uri().query().unwrap_or("").to_string(); - let headers = headers.unwrap(); - - let server_request = ServerRequestHeader::new(method, path, query, headers); - - Ok(server_request) - } - - pub fn new( - method: String, - path: String, - query: String, - headers: Vec<(String, String)>, - ) -> Self { - Self { - method, - path, - query, - headers, - } - } -} - -#[derive(Default, Debug)] -pub(crate) struct ServerResponse { - pub status: u16, - pub headers: Vec<(String, String)>, - pub body: Vec, -} - -impl ServerResponse { - pub fn new(status: u16, headers: Vec<(String, String)>, body: Vec) -> Self { - Self { - status, - headers, - body, - } - } -} - -/// Extracts all headers from the URI of the given request. -fn extract_headers(header_map: &HeaderMap) -> Result, String> { - let mut headers = Vec::new(); - for (hn, hv) in header_map { - let hn = hn.as_str().to_string(); - let hv = hv.to_str(); - if let Err(e) = hv { - return Err(format!("error parsing headers: {}", e)); - } - headers.push((hn, hv.unwrap().to_string())); - } - Ok(headers) -} - -async fn access_log_middleware( - req: HyperRequest, - state: Arc, - print_access_log: bool, - next: fn(req: HyperRequest, state: Arc) -> T, -) -> HyperResult> -where - T: Future>>, -{ - let time_request_received = Instant::now(); - - let request_method = req.method().to_string(); - let request_uri = req.uri().to_string(); - let request_http_version = format!("{:?}", &req.version()); - - let result = next(req, state).await; - - if print_access_log && !request_uri.starts_with(&format!("{}/", BASE_PATH)) { - if let Ok(response) = &result { - log::info!( - "\"{} {} {:?}\" {} {}", - request_method, - request_uri, - request_http_version, - response.status().as_u16(), - time_request_received.elapsed().as_millis() - ); - } - }; - - return result; -} - -async fn handle_server_request( - req: HyperRequest, - state: Arc, -) -> HyperResult> { - let request_header = ServerRequestHeader::from(&req); - - if let Err(e) = request_header { - return Ok(error_response(format!("Cannot parse request: {}", e))); - } - - let body = hyper::body::to_bytes(req.into_body()).await; - if let Err(e) = body { - return Ok(error_response(format!("Cannot read request body: {}", e))); - } - - let routing_result = route_request( - state.borrow(), - &request_header.unwrap(), - body.unwrap().to_vec(), - ) - .await; - if let Err(e) = routing_result { - return Ok(error_response(format!("Request handler error: {}", e))); - } - - let response = map_response(routing_result.unwrap()); - if let Err(e) = response { - return Ok(error_response(format!("Cannot build response: {}", e))); - } - - Ok(response.unwrap()) -} - -/// Starts a new instance of an HTTP mock server. You should never need to use this function -/// directly. Use it if you absolutely need to manage the low-level details of how the mock -/// server operates. -pub(crate) async fn start_server( - port: u16, - expose: bool, - state: &Arc, - socket_addr_sender: Option>, - print_access_log: bool, - shutdown: F, -) -> Result<(), String> -where - F: Future, -{ - let host = if expose { "0.0.0.0" } else { "127.0.0.1" }; - - let state = state.clone(); - let new_service = make_service_fn(move |_| { - let state = state.clone(); - async move { - Ok::<_, GenericError>(service_fn(move |req: HyperRequest| { - let state = state.clone(); - access_log_middleware(req, state, print_access_log, handle_server_request) - })) - } - }); - - let server = Server::bind(&format!("{}:{}", host, port).parse().unwrap()).serve(new_service); - let addr = server.local_addr(); - - if let Some(socket_addr_sender) = socket_addr_sender { - if let Err(e) = socket_addr_sender.send(addr) { - return Err(format!( - "Cannot send socket information to the test thread: {:?}", - e - )); - } - } - - // And now add a graceful shutdown signal... - let graceful = server.with_graceful_shutdown(shutdown); - - log::info!("Listening on {}", addr); - if let Err(e) = graceful.await { - return Err(format!("Err: {}", e)); - } - - Ok(()) -} -/// Maps a server response to a hyper response. -fn map_response(route_response: ServerResponse) -> Result, String> { - let mut builder = HyperResponse::builder(); - builder = builder.status(route_response.status); +#[cfg(feature = "record")] +mod persistence; - for (key, value) in route_response.headers { - let name = HeaderName::from_str(&key); - if let Err(e) = name { - return Err(format!("Cannot create header from name: {}", e)); - } +#[cfg(feature = "https")] +mod tls; - let value = HeaderValue::from_str(&value); - if let Err(e) = value { - return Err(format!("Cannot create header from value: {}", e)); - } +use crate::server::{handler::HttpMockHandler, server::MockServer, state::HttpMockStateManager}; - let value = value.unwrap(); - let value = value.to_str(); - if let Err(e) = value { - return Err(format!("Cannot create header from value string: {}", e)); - } +pub use builder::HttpMockServerBuilder; +pub use server::Error; - builder = builder.header(name.unwrap(), value.unwrap()); - } - - let result = builder.body(Body::from(route_response.body)); - if let Err(e) = result { - return Err(format!("Cannot create HTTP response: {}", e)); - } - - Ok(result.unwrap()) -} - -/// Routes a request to the appropriate route handler. -async fn route_request( - state: &MockServerState, - request_header: &ServerRequestHeader, - body: Vec, -) -> Result { - log::trace!("Routing incoming request: {:?}", request_header); - - if PING_PATH.is_match(&request_header.path) { - if let "GET" = request_header.method.as_str() { - return routes::ping(); - } - } - - if MOCKS_PATH.is_match(&request_header.path) { - match request_header.method.as_str() { - "POST" => return routes::add(state, body), - "DELETE" => return routes::delete_all_mocks(state), - _ => {} - } - } - - if MOCK_PATH.is_match(&request_header.path) { - let id = get_path_param(&MOCK_PATH, 1, &request_header.path); - if let Err(e) = id { - return Err(format!("Cannot parse id from path: {}", e)); - } - let id = id.unwrap(); - - match request_header.method.as_str() { - "GET" => return routes::read_one(state, id), - "DELETE" => return routes::delete_one(state, id), - _ => {} - } - } - - if VERIFY_PATH.is_match(&request_header.path) { - match request_header.method.as_str() { - "POST" => return routes::verify(state, body), - _ => {} - } - } - - if HISTORY_PATH.is_match(&request_header.path) { - match request_header.method.as_str() { - "DELETE" => return routes::delete_history(state), - _ => {} - } - } - - routes::serve(state, request_header, body).await -} - -/// Get request path parameters. -fn get_path_param(regex: &Regex, idx: usize, path: &str) -> Result { - let cap = regex.captures(path); - if cap.is_none() { - return Err(format!( - "Error capturing parameter from request path: {}", - path - )); - } - let cap = cap.unwrap(); - - let id = cap.get(idx); - if id.is_none() { - return Err(format!( - "Error capturing resource id in request path: {}", - path - )); - } - let id = id.unwrap().as_str(); - - let id = id.parse::(); - if let Err(e) = id { - return Err(format!("Error parsing id as a number: {}", e)); - } - let id = id.unwrap(); - - Ok(id) -} - -/// Creates a default error response. -fn error_response(body: String) -> HyperResponse { - HyperResponse::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::from(body)) - .expect("Cannot build route error response") -} - -static BASE_PATH: &'static str = "/__httpmock__"; +// We want to expose this error to the user +pub type HttpMockServer = MockServer>; -lazy_static! { - static ref PING_PATH: Regex = Regex::new(&format!(r"^{}/ping$", BASE_PATH)).unwrap(); - static ref MOCKS_PATH: Regex = Regex::new(&format!(r"^{}/mocks$", BASE_PATH)).unwrap(); - static ref MOCK_PATH: Regex = Regex::new(&format!(r"^{}/mocks/([0-9]+)$", BASE_PATH)).unwrap(); - static ref HISTORY_PATH: Regex = Regex::new(&format!(r"^{}/history$", BASE_PATH)).unwrap(); - static ref VERIFY_PATH: Regex = Regex::new(&format!(r"^{}/verify$", BASE_PATH)).unwrap(); +#[derive(Clone)] +pub struct RequestMetadata { + pub scheme: &'static str, } -#[cfg(test)] -mod test { - use std::collections::BTreeMap; - - use futures_util::TryStreamExt; - - use crate::server::{ - error_response, get_path_param, map_response, ServerResponse, HISTORY_PATH, MOCKS_PATH, - MOCK_PATH, PING_PATH, VERIFY_PATH, - }; - use crate::Regex; - use hyper::body::Bytes; - use hyper::Error; - - #[test] - fn route_regex_test() { - assert_eq!(MOCK_PATH.is_match("/__httpmock__/mocks/1"), true); - assert_eq!( - MOCK_PATH.is_match("/__httpmock__/mocks/1295473892374"), - true - ); - assert_eq!(MOCK_PATH.is_match("/__httpmock__/mocks/abc"), false); - assert_eq!(MOCK_PATH.is_match("/__httpmock__/mocks"), false); - assert_eq!(MOCK_PATH.is_match("/__httpmock__/mocks/345345/test"), false); - assert_eq!( - MOCK_PATH.is_match("test/__httpmock__/mocks/345345/test"), - false - ); - - assert_eq!(PING_PATH.is_match("/__httpmock__/ping"), true); - assert_eq!( - PING_PATH.is_match("/__httpmock__/ping/1295473892374"), - false - ); - assert_eq!(PING_PATH.is_match("test/ping/1295473892374"), false); - - assert_eq!(VERIFY_PATH.is_match("/__httpmock__/verify"), true); - assert_eq!( - VERIFY_PATH.is_match("/__httpmock__/verify/1295473892374"), - false - ); - assert_eq!(VERIFY_PATH.is_match("test/verify/1295473892374"), false); - - assert_eq!(HISTORY_PATH.is_match("/__httpmock__/history"), true); - println!("{:?}", HISTORY_PATH.as_str()); - - assert_eq!( - HISTORY_PATH.is_match("/__httpmock__/history/1295473892374"), - false - ); - assert_eq!(HISTORY_PATH.is_match("test/history/1295473892374"), false); - - assert_eq!(MOCKS_PATH.is_match("/__httpmock__/mocks"), true); - assert_eq!(MOCKS_PATH.is_match("/__httpmock__/mocks/5"), false); - assert_eq!(MOCKS_PATH.is_match("test/__httpmock__/mocks/5"), false); - assert_eq!(MOCKS_PATH.is_match("test/__httpmock__/mocks/567"), false); - } - - /// Make sure passing an empty string to the error response does not result in an error. - #[test] - fn error_response_test() { - let res = error_response("test".into()); - let (parts, body) = res.into_parts(); - - let body = async_std::task::block_on(async { - return match hyper::body::to_bytes(body).await { - Ok(bytes) => bytes.to_vec(), - Err(e) => panic!(e), - }; - }); - - assert_eq!(String::from_utf8(body).unwrap(), "test".to_string()) - } - - /// Makes sure an error is return if there is a header parsing error - #[test] - fn response_header_key_parsing_error_test() { - // Arrange - let mut headers = Vec::new(); - headers.push((";;;".to_string(), ";;;".to_string())); - - let res = ServerResponse { - body: Vec::new(), - status: 500, - headers, - }; - - // Act - let result = map_response(res); - - // Assert - assert_eq!(result.is_err(), true); - assert_eq!( - result - .err() - .unwrap() - .contains("Cannot create header from name"), - true - ); - } - - #[test] - fn get_path_param_regex_error_test() { - // Arrange - let re = Regex::new(r"^/__httpmock__/mocks/([0-9]+)$").unwrap(); - - // Act - let result = get_path_param(&re, 0, ""); - - // Assert - assert_eq!(result.is_err(), true); - assert_eq!( - result - .err() - .unwrap() - .contains("Error capturing parameter from request path"), - true - ); - } - - #[test] - fn get_path_param_index_error_test() { - // Arrange - let re = Regex::new(r"^/__httpmock__/mocks/([0-9]+)$").unwrap(); - - // Act - let result = get_path_param(&re, 5, "/__httpmock__/mocks/5"); - - // Assert - assert_eq!(result.is_err(), true); - assert_eq!( - "Error capturing resource id in request path: /__httpmock__/mocks/5", - result.err().unwrap() - ); - } - - #[test] - fn get_path_param_number_error_test() { - // Arrange - let re = Regex::new(r"^/__httpmock__/mocks/([0-9]+)$").unwrap(); - - // Act - let result = get_path_param(&re, 0, "/__httpmock__/mocks/9999999999999999999999999"); - - // Assert - assert_eq!(result.is_err(), true); - assert_eq!( - "Error parsing id as a number: invalid digit found in string", - result.err().unwrap() - ); +impl RequestMetadata { + pub fn new(scheme: &'static str) -> Self { + Self { scheme } } } diff --git a/src/server/persistence.rs b/src/server/persistence.rs new file mode 100644 index 00000000..03e13d68 --- /dev/null +++ b/src/server/persistence.rs @@ -0,0 +1,112 @@ +use bytes::{BufMut, Bytes, BytesMut}; +use std::{ + convert::{TryFrom, TryInto}, + fs::read_dir, + path::PathBuf, + str::FromStr, +}; + +use serde::Deserialize; + +use crate::common::data; +use serde_yaml::{Deserializer, Value as YamlValue}; +use thiserror::Error; + +use crate::{ + common::{ + data::{MockDefinition, StaticMockDefinition}, + util::read_file, + }, + server::{ + persistence::Error::{DeserializationError, FileReadError}, + state, + state::{Error::DataConversionError, StateManager}, + }, +}; + +#[derive(Error, Debug)] +pub enum Error { + #[error("cannot read from mock file: {0}")] + FileReadError(String), + #[error("cannot modify state: {0}")] + StateError(#[from] state::Error), + #[error("cannot deserialize YAML: {0}")] + DeserializationError(String), + #[error("cannot convert data structures: {0}")] + DataConversionError(#[from] data::Error), + #[error("unknown data store error")] + Unknown, +} + +pub fn read_static_mock_definitions(path_opt: PathBuf, state: &S) -> Result<(), Error> +where + S: StateManager + Send + Sync + 'static, +{ + for def in read_static_mocks(path_opt)? { + state.add_mock(def.try_into()?, true)?; + } + + Ok(()) +} + +fn read_static_mocks(path: PathBuf) -> Result, Error> { + let mut definitions: Vec = Vec::new(); + + let paths = read_dir(path).expect("cannot list files in directory"); + for file_path in paths { + let file_path = file_path.unwrap().path(); + if let Some(ext) = file_path.extension() { + if !"yaml".eq(ext) && !"yml".eq(ext) { + continue; + } + } + + log::info!( + "Loading static mock file from '{}'", + file_path.to_string_lossy() + ); + + let content = read_file(file_path).map_err(|err| FileReadError(err.to_string()))?; + let content = String::from_utf8(content).map_err(|err| FileReadError(err.to_string()))?; + + definitions.extend(deserialize_mock_defs_from_yaml(&content)?); + } + + return Ok(definitions); +} + +pub fn deserialize_mock_defs_from_yaml( + yaml_content: &str, +) -> Result, Error> { + let mut definitions = Vec::new(); + + for document in Deserializer::from_str(&yaml_content) { + let value = YamlValue::deserialize(document) + .map_err(|err| DeserializationError(err.to_string()))?; + + let definition: StaticMockDefinition = + serde_yaml::from_value(value).map_err(|err| DeserializationError(err.to_string()))?; + + definitions.push(definition); + } + + Ok(definitions) +} + +pub fn serialize_mock_defs_to_yaml(mocks: &Vec) -> Result { + let mut buffer = BytesMut::new(); + + for (idx, mock) in mocks.iter().enumerate() { + if idx > 0 { + buffer.put_slice(b"---\n"); + } + + let static_mock: StaticMockDefinition = StaticMockDefinition::try_from(mock) + .map_err(|err| DataConversionError(err.to_string()))?; + let yaml = serde_yaml::to_string(&static_mock) + .map_err(|err| DataConversionError(err.to_string()))?; + buffer.put_slice(yaml.as_bytes()); + } + + Ok(buffer.freeze()) +} diff --git a/src/server/server.rs b/src/server/server.rs new file mode 100644 index 00000000..268d5461 --- /dev/null +++ b/src/server/server.rs @@ -0,0 +1,466 @@ +use futures_util::{stream::StreamExt, FutureExt}; +use http::{Request, StatusCode}; +use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full}; +use hyper::body::{Bytes, Incoming}; +use std::{ + future::{pending, Future}, + net::SocketAddr, + path::PathBuf, + sync::Arc, +}; + +use hyper_util::server::conn::auto::Builder as ServerBuilder; + +use crate::server; +use hyper::{http, service::service_fn, upgrade::on as upgrade_on, Method, Response}; +use hyper_util::rt::tokio::TokioIo; +use thiserror::Error; +use tokio::{ + net::{TcpListener, TcpStream}, + sync::oneshot::Sender, + task::spawn, +}; + +use crate::server::{ + handler::Handler, + server::Error::{ + BufferError, LocalSocketAddrError, PublishSocketAddrError, RouterError, SocketBindError, + }, +}; + +use std::io; + +#[cfg(feature = "https")] +use rustls::ServerConfig; +#[cfg(feature = "https")] +use tokio_rustls::TlsAcceptor; + +#[derive(Error, Debug)] +pub enum Error { + #[error("cannot bind to socket addr {0}: {1}")] + SocketBindError(SocketAddr, std::io::Error), + #[error("cannot parse socket address: {0}")] + SocketAddrParseError(#[from] std::net::AddrParseError), + #[error("cannot obtain local error: {0}")] + LocalSocketAddrError(std::io::Error), + #[error("cannot send reserved TCP address to test thread {0}")] + PublishSocketAddrError(SocketAddr), + #[error("cannot create response: {0}")] + ResponseConstructionError(http::Error), + #[error("buffering error: {0}")] + BufferError(hyper::Error), + #[error("HTTP error: {0}")] + HTTPError(#[from] http::Error), + #[error("cannot process request: {0}")] + RouterError(#[from] server::handler::Error), + #[error("HTTPS error: {0}")] + TlsError(String), + #[error("Server configuration error: {0}")] + ConfigurationError(String), + #[error("Server I/O error: {0}")] + IOError(io::Error), + #[error("Server error: {0}")] + ServerError(#[from] hyper::Error), + #[error("Server error: {0}")] + ServerConnectionError(Box), + #[error("unknown data store error")] + Unknown, +} + +#[cfg(feature = "https")] +pub struct MockServerHttpsConfig { + pub cert_resolver_factory: Arc, +} + +pub struct MockServerConfig { + pub static_port: Option, + pub expose: bool, + pub print_access_log: bool, + #[cfg(feature = "https")] + pub https: MockServerHttpsConfig, +} + +/// The `MockServer` struct represents a mock server that can handle incoming HTTP requests. +pub struct MockServer +where + H: Handler + Send + Sync + 'static, +{ + handler: Box, + config: MockServerConfig, +} + +impl MockServer +where + H: Handler + Send + Sync + 'static, +{ + /// Creates a new `MockServer` instance with the given handler and configuration. + /// + /// # Parameters + /// - `handler`: A boxed handler that implements the `Handler` trait. + /// - `config`: The configuration settings for the mock server. + /// + /// # Returns + /// A `Result` containing the new `MockServer` instance or an `Error` if creation fails. + pub fn new(handler: Box, config: MockServerConfig) -> Result { + Ok(MockServer { handler, config }) + } + + /// Starts the mock server asynchronously. + pub async fn start(self) -> Result<(), Error> { + self.start_with_signals(None, pending()).await + } + + /// Starts the mock server asynchronously with support for handling external shutdown signals. + /// + /// # Parameters + /// - `socket_addr_sender`: An optional `Sender` to send the server's socket address once it's bound. + /// - `shutdown`: A future that resolves when the server should shut down. + /// + pub async fn start_with_signals( + self, + socket_addr_sender: Option>, + shutdown: F, + ) -> Result<(), Error> + where + F: Future, + { + let host = if self.config.expose { + "0.0.0.0" + } else { + "127.0.0.1" + }; + let addr: SocketAddr = + format!("{}:{}", host, self.config.static_port.unwrap_or(0)).parse()?; + let listener = TcpListener::bind(addr) + .await + .map_err(|e| SocketBindError(addr, e))?; + + if let Some(sender) = socket_addr_sender { + let addr = listener.local_addr().map_err(|e| LocalSocketAddrError(e))?; + sender + .send(addr) + .map_err(|addr| PublishSocketAddrError(addr))?; + } + + // **************************************************************************************** + // SERVER START + log::info!("Listening on {}", addr); + self.run_accept_loop(listener, shutdown).await + } + + pub async fn run_accept_loop(self, listener: TcpListener, shutdown: F) -> Result<(), Error> + where + F: Future, + { + let shutdown = shutdown.shared(); + let server = Arc::new(self); + + loop { + tokio::select! { + accepted = listener.accept() => { + match accepted { + Ok((tcp_stream, remote_address)) => { + let server = server.clone(); + spawn(async move { + if let Err(err) = server.handle_tcp_stream(tcp_stream, remote_address).await { + log::error!("{:?}", err); + } + }); + }, + Err(err) => { + log::error!("TCP error: {:?}", err); + }, + }; + } + _ = shutdown.clone() => { + break; + } + } + } + + Ok(()) + } + + async fn service( + self: Arc, + req: Request, + ) -> Result>, Error> { + log::trace!("New HTTP request received: {}", req.uri()); + + if req.method() == Method::CONNECT { + return Ok(Response::new(empty())); + // return handle_connect(req).await; + } + + let req = match buffer_request(req).await { + Ok(req) => req, + Err(err) => { + return error_response(StatusCode::INTERNAL_SERVER_ERROR, BufferError(err)); + } + }; + + match self.handler.handle(req).await { + Ok(response) => to_service_response(response), + Err(err) => error_response(StatusCode::INTERNAL_SERVER_ERROR, RouterError(err)), + } + } + + async fn handle_tcp_stream( + self: Arc, + tcp_stream: TcpStream, + remote_address: SocketAddr, + ) -> Result<(), Error> { + log::trace!("new TCP connection incoming"); + + #[cfg(feature = "https")] + { + let mut peek_buffer = TcpStreamPeekBuffer::new(&tcp_stream); + if is_encrypted(&mut peek_buffer, 0).await { + log::trace!("TCP connection seems to be TLS encrypted"); + + let tcp_address = tcp_stream.local_addr().map_err(|err| IOError(err))?; + + let cert_resolver = self.config.https.cert_resolver_factory.build(tcp_address); + let mut server_config = ServerConfig::builder() + .with_no_client_auth() + .with_cert_resolver(cert_resolver); + + #[cfg(feature = "http2")] + { + server_config.alpn_protocols = + vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()]; + } + #[cfg(not(feature = "http2"))] + { + server_config.alpn_protocols = vec![b"http/1.1".to_vec(), b"http/1.0".to_vec()]; + } + + let tls_acceptor = TlsAcceptor::from(Arc::new(server_config)); + let tls_stream = tls_acceptor.accept(tcp_stream).await.map_err(|e| { + TlsError(format!("Could not accept TLS from TCP stream: {:?}", e)) + })?; + + return serve_connection(self.clone(), tls_stream, "https").await; + } + + if log::max_level() >= log::LevelFilter::Trace { + let peeked_str = + String::from_utf8_lossy(&peek_buffer.buffer().to_vec()).to_string(); + log::trace!( + "TCP connection seems NOT to be TLS encrypted (based on peeked data: {}", + peeked_str + ); + } + } + + log::trace!("TCP connection is not TLS encrypted"); + + return serve_connection(self.clone(), tcp_stream, "http").await; + } +} + +async fn serve_connection( + server: Arc>, + stream: S, + scheme: &'static str, +) -> Result<(), Error> +where + H: Handler + Send + Sync + 'static, + S: AsyncRead + AsyncWrite + Unpin + Send + 'static, +{ + let mut server_builder = ServerBuilder::new(TokioExecutor::new()); + + server_builder.http1().preserve_header_case(true); + + server_builder.http2(); + //.enable_connect_protocol(); + + server_builder + .serve_connection_with_upgrades( + TokioIo::new(stream), + service_fn(|mut req| { + req.extensions_mut().insert(RequestMetadata::new(scheme)); + server.clone().service(req) + }), + ) + .await + .map_err(|err| ServerConnectionError(err)) +} + +async fn handle_connect( + req: Request, +) -> Result>, Error> { + if let Some(addr) = host_addr(req.uri()) { + spawn(async move { + match upgrade_on(req).await { + Ok(upgraded) => { + if let Err(e) = tunnel(upgraded, addr).await { + log::warn!("Proxy I/O error: {}", e); + } else { + log::info!("Proxied request"); + }; + } + Err(e) => { + log::warn!("Proxy upgrade error: {}", e) + } + } + }); + + Ok(Response::new(empty())) + } else { + log::warn!("CONNECT host is not socket addr: {:?}", req.uri()); + let mut resp = Response::new(full("CONNECT must be sent to a socket address")); + *resp.status_mut() = StatusCode::BAD_REQUEST; + + Ok(resp) + } +} + +async fn buffer_request(req: Request) -> Result, hyper::Error> { + let (parts, body) = req.into_parts(); + let body = body.collect().await?.to_bytes(); + return Ok(Request::from_parts(parts, body)); +} + +fn host_addr(uri: &http::Uri) -> Option { + uri.authority().and_then(|auth| Some(auth.to_string())) +} + +fn full>(chunk: T) -> BoxBody { + Full::new(chunk.into()) + .map_err(|never| match never {}) + .boxed() +} + +fn empty() -> BoxBody { + Empty::::new() + .map_err(|never| match never {}) + .boxed() +} + +async fn tunnel(upgraded: hyper::upgrade::Upgraded, addr: String) -> std::io::Result<()> { + let mut server = tokio::net::TcpStream::connect(addr).await?; + let mut upgraded = RecordingStream::new(TokioIo::new(upgraded)); + + let (from_client, from_server) = + tokio::io::copy_bidirectional(&mut server, &mut upgraded).await?; + + log::info!( + "client wrote {} bytes and received {} bytes. \n\nread:\n{}\n\n wrote: {}\n\n", + from_client, + from_server, + String::from_utf8_lossy(&upgraded.read_bytes), + String::from_utf8_lossy(&upgraded.written_bytes) + ); + + Ok(()) +} + +fn error_response( + code: StatusCode, + err: Error, +) -> Result>, Error> { + log::error!("failed to process request: {}", err.to_string()); + Ok(Response::builder() + .status(code) + .body(full(err.to_string()))?) +} + +fn to_service_response( + response: Response, +) -> Result>, Error> { + let (parts, body) = response.into_parts(); + Ok(Response::from_parts(parts, full(body))) +} + +use crate::server::Error::{IOError, ServerConnectionError, ServerError, TlsError, Unknown}; +use async_trait::async_trait; +use bytes::BytesMut; +use hyper_util::rt::TokioExecutor; +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +#[cfg(feature = "https")] +use crate::server::tls::{CertificateResolverFactory, TcpStreamPeekBuffer}; + +use crate::server::RequestMetadata; +#[cfg(feature = "https")] +use tls_detect::is_encrypted; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; + +struct RecordingStream { + stream: S, + read_bytes: BytesMut, // Buffer to store bytes read from the stream + written_bytes: BytesMut, // Buffer to store bytes written to the stream +} + +impl RecordingStream { + pub fn new(stream: S) -> Self { + RecordingStream { + stream, + read_bytes: BytesMut::new(), + written_bytes: BytesMut::new(), + } + } + + // Method to access the collected read bytes + pub fn get_read_bytes(&self) -> &[u8] { + &self.read_bytes + } + + // Method to access the collected written bytes + pub fn get_written_bytes(&self) -> &[u8] { + &self.written_bytes + } +} + +impl AsyncRead for RecordingStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let this = self.get_mut(); + let stream = Pin::new(&mut this.stream); + + let before = buf.filled().len(); + match stream.poll_read(cx, buf) { + Poll::Ready(Ok(())) => { + let after = buf.filled().len(); + let new_bytes = &buf.filled()[before..after]; + this.read_bytes.extend_from_slice(new_bytes); + Poll::Ready(Ok(())) + } + other => other, + } + } +} + +impl AsyncWrite for RecordingStream { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let this = self.get_mut(); + let stream = Pin::new(&mut this.stream); + + match stream.poll_write(cx, buf) { + Poll::Ready(Ok(size)) => { + this.written_bytes.extend_from_slice(&buf[..size]); + Poll::Ready(Ok(size)) + } + other => other, + } + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.get_mut().stream).poll_flush(cx) + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.get_mut().stream).poll_shutdown(cx) + } +} diff --git a/src/server/state.rs b/src/server/state.rs new file mode 100644 index 00000000..e558fafa --- /dev/null +++ b/src/server/state.rs @@ -0,0 +1,752 @@ +use crate::{ + common::{ + data, + data::{ + ActiveForwardingRule, ActiveMock, ActiveProxyRule, ActiveRecording, ClosestMatch, + Mismatch, MockDefinition, MockServerHttpResponse, RequestRequirements, + }, + }, + prelude::HttpMockRequest, + server::{ + matchers, + matchers::{all, Matcher}, + state::Error::{BodyMethodInvalid, DataConversionError, StaticMockError, ValidationError}, + }, +}; + +#[cfg(feature = "record")] +use crate::server::persistence::{deserialize_mock_defs_from_yaml, serialize_mock_defs_to_yaml}; + +use crate::common::data::{ForwardingRuleConfig, ProxyRuleConfig, RecordingRuleConfig}; +use bytes::Bytes; +use std::{ + collections::BTreeMap, + convert::{TryFrom, TryInto}, + sync::{Arc, Mutex}, + time::Duration, +}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("The mock is static and cannot be deleted")] + StaticMockError, + #[error("Validation error: request HTTP method GET or HEAD cannot have a body")] + BodyMethodInvalid, + #[error("cannot convert: {0}")] + DataConversionError(String), + #[error("validation error: {0}")] + ValidationError(String), + #[error("unknown error")] + Unknown, +} + +pub struct MockServerState { + history_limit: usize, + next_mock_id: usize, + next_forwarding_rule_id: usize, + next_proxy_rule_id: usize, + next_recording_id: usize, + pub mocks: BTreeMap, + pub history: Vec>, + pub matchers: Vec>, + pub forwarding_rules: BTreeMap, + pub proxy_rules: BTreeMap, + pub recordings: BTreeMap, +} + +impl MockServerState { + pub fn new(history_limit: usize) -> Self { + MockServerState { + mocks: BTreeMap::new(), + forwarding_rules: BTreeMap::new(), + proxy_rules: BTreeMap::new(), + recordings: BTreeMap::new(), + history_limit, + history: Vec::new(), + next_mock_id: 0, + next_forwarding_rule_id: 0, + next_proxy_rule_id: 0, + next_recording_id: 0, + matchers: matchers::all(), + } + } +} + +pub(crate) trait StateManager { + fn reset(&self); + fn add_mock(&self, definition: MockDefinition, is_static: bool) -> Result; + fn read_mock(&self, id: usize) -> Result, Error>; + fn delete_mock(&self, id: usize) -> Result; + fn delete_all_mocks(&self); + + fn delete_history(&self); + + fn verify(&self, requirements: &RequestRequirements) -> Result, Error>; + + fn serve_mock(&self, req: &HttpMockRequest) -> Result, Error>; + + fn create_forwarding_rule(&self, config: ForwardingRuleConfig) -> ActiveForwardingRule; + fn delete_forwarding_rule(&self, id: usize) -> Option; + fn delete_all_forwarding_rules(&self); + + fn create_proxy_rule(&self, constraints: ProxyRuleConfig) -> ActiveProxyRule; + fn delete_proxy_rule(&self, id: usize) -> Option; + fn delete_all_proxy_rules(&self); + + fn create_recording(&self, config: RecordingRuleConfig) -> ActiveRecording; + fn delete_recording(&self, recording_id: usize) -> Option; + fn delete_all_recordings(&self); + + #[cfg(feature = "record")] + fn export_recording(&self, id: usize) -> Result, Error>; + + #[cfg(feature = "record")] + fn load_mocks_from_recording(&self, recording_file_content: &str) -> Result, Error>; + + fn find_forward_rule<'a>( + &'a self, + req: &'a HttpMockRequest, + ) -> Result, Error>; + fn find_proxy_rule<'a>( + &'a self, + req: &'a HttpMockRequest, + ) -> Result, Error>; + fn record< + IntoResponse: TryInto, + >( + &self, + is_proxied: bool, + time_taken: Duration, + req: HttpMockRequest, + res: IntoResponse, + ) -> Result<(), Error>; +} + +pub struct HttpMockStateManager { + state: Mutex, +} + +impl HttpMockStateManager { + pub fn new(history_limit: usize) -> Self { + Self { + state: Mutex::new(MockServerState::new(history_limit)), + } + } +} + +impl Default for HttpMockStateManager { + fn default() -> Self { + HttpMockStateManager::new(usize::MAX) + } +} + +impl StateManager for HttpMockStateManager { + fn reset(&self) { + self.delete_all_mocks(); + self.delete_history(); + self.delete_all_forwarding_rules(); + self.delete_all_proxy_rules(); + self.delete_all_recordings(); + } + + fn add_mock(&self, definition: MockDefinition, is_static: bool) -> Result { + validate_request_requirements(&definition.request)?; + + let mut state = self.state.lock().unwrap(); + + let id = state.next_mock_id; + let active_mock = ActiveMock::new(id, definition, 0, is_static); + + log::debug!("Adding new mock with ID={}", id); + + state.mocks.insert(id, active_mock.clone()); + + state.next_mock_id += 1; + + Ok(active_mock) + } + + fn read_mock(&self, id: usize) -> Result, Error> { + let mut state = self.state.lock().unwrap(); + + let result = state.mocks.get(&id); + match result { + Some(found) => Ok(Some(found.clone())), + None => Ok(None), + } + } + + fn delete_mock(&self, id: usize) -> Result { + let mut state = self.state.lock().unwrap(); + + if let Some(m) = state.mocks.get(&id) { + if m.is_static { + return Err(StaticMockError); + } + } + + log::debug!("Deleting mock with id={}", id); + + Ok(state.mocks.remove(&id).is_some()) + } + + fn delete_all_mocks(&self) { + let mut state = self.state.lock().unwrap(); + + let ids: Vec = state + .mocks + .iter() + .filter(|(k, v)| !v.is_static) + .map(|(k, v)| *k) + .collect(); + + ids.iter().for_each(|k| { + state.mocks.remove(k); + }); + + log::trace!("Deleted all mocks"); + } + + fn delete_history(&self) { + let mut state = self.state.lock().unwrap(); + state.history.clear(); + log::trace!("Deleted request history"); + } + + fn verify(&self, requirements: &RequestRequirements) -> Result, Error> { + let mut state = self.state.lock().unwrap(); + + let non_matching_requests: Vec<&Arc> = state + .history + .iter() + .filter(|req| !request_matches(&state.matchers, req, requirements)) + .collect(); + + let request_distances = + get_distances(&non_matching_requests, &state.matchers, requirements); + let best_matches = get_min_distance_requests(&request_distances); + + let closes_match_request_idx = match best_matches.get(0) { + None => return Ok(None), + Some(idx) => *idx, + }; + + let req = non_matching_requests.get(closes_match_request_idx).unwrap(); + let mismatches = get_request_mismatches(req, &requirements, &state.matchers); + + Ok(Some(ClosestMatch { + request: HttpMockRequest::clone(&req), + request_index: closes_match_request_idx, + mismatches, + })) + } + + fn serve_mock(&self, req: &HttpMockRequest) -> Result, Error> { + let mut state = self.state.lock().unwrap(); + + let req = Arc::new(req.clone()); + + if state.history.len() > 100 { + // TODO: Make max history configurable + state.history.remove(0); + } + state.history.push(req.clone()); + + let result = state + .mocks + .values() + .find(|&mock| request_matches(&state.matchers, &req, &mock.definition.request)); + + let found_mock_id = match result { + Some(mock) => Some(mock.id), + None => None, + }; + + if let Some(found_id) = found_mock_id { + log::debug!( + "Matched mock with id={} to the following request: {:#?}", + found_id, + req + ); + + let mock = state.mocks.get_mut(&found_id).unwrap(); + mock.call_counter += 1; + + return Ok(Some(mock.definition.response.clone())); + } + + log::debug!( + "Could not match any mock to the following request: {:#?}", + req + ); + + Ok(None) + } + + fn create_forwarding_rule(&self, config: ForwardingRuleConfig) -> ActiveForwardingRule { + let mut state = self.state.lock().unwrap(); + + let rule = ActiveForwardingRule { + id: state.next_forwarding_rule_id, + config, + }; + + state.forwarding_rules.insert(rule.id, rule.clone()); + + state.next_forwarding_rule_id += 1; + + rule + } + + fn delete_forwarding_rule(&self, id: usize) -> Option { + let mut state = self.state.lock().unwrap(); + + let result = state.forwarding_rules.remove(&id); + + if result.is_some() { + log::debug!("Deleting proxy rule with id={}", id); + } else { + log::warn!( + "Could not delete proxy rule with id={} (no proxy rule with that id found)", + id + ); + } + + result + } + + fn delete_all_forwarding_rules(&self) { + let mut state = self.state.lock().unwrap(); + state.forwarding_rules.clear(); + + log::debug!("Deleted all forwarding rules"); + } + + fn create_proxy_rule(&self, config: ProxyRuleConfig) -> ActiveProxyRule { + let mut state = self.state.lock().unwrap(); + + let rule = ActiveProxyRule { + id: state.next_proxy_rule_id, + config, + }; + + state.proxy_rules.insert(rule.id, rule.clone()); + + state.next_proxy_rule_id += 1; + + rule + } + + fn delete_proxy_rule(&self, id: usize) -> Option { + let mut state = self.state.lock().unwrap(); + + let result = state.proxy_rules.remove(&id); + + if result.is_some() { + log::debug!("Deleting proxy rule with id={}", id); + } else { + log::warn!( + "Could not delete proxy rule with id={} (no proxy rule with that id found)", + id + ); + } + + result + } + + fn delete_all_proxy_rules(&self) { + let mut state = self.state.lock().unwrap(); + state.proxy_rules.clear(); + + log::debug!("Deleted all proxy rules"); + } + + fn create_recording(&self, config: RecordingRuleConfig) -> ActiveRecording { + let mut state = self.state.lock().unwrap(); + + let rec = ActiveRecording { + id: state.next_recording_id, + config, + mocks: Vec::new(), + }; + + state.recordings.insert(rec.id, rec.clone()); + + state.next_recording_id += 1; + + rec + } + + fn delete_recording(&self, id: usize) -> Option { + let mut state = self.state.lock().unwrap(); + + let result = state.recordings.remove(&id); + + if result.is_some() { + log::debug!("Deleting proxy rule with id={}", id); + } else { + log::warn!( + "Could not delete proxy rule with id={} (no proxy rule with that id found)", + id + ); + } + + result + } + + fn delete_all_recordings(&self) { + let mut state = self.state.lock().unwrap(); + state.recordings.clear(); + + log::debug!("Deleted all recorders"); + } + + #[cfg(feature = "record")] + fn export_recording(&self, id: usize) -> Result, Error> { + let mut state = self.state.lock().unwrap(); + + if let Some(rec) = state.recordings.get(&id) { + return Ok(Some( + serialize_mock_defs_to_yaml(&rec.mocks) + .map_err(|err| DataConversionError(err.to_string()))?, + )); + } + + Ok(None) + } + + #[cfg(feature = "record")] + fn load_mocks_from_recording(&self, recording_file_content: &str) -> Result, Error> { + let all_static_mock_defs = deserialize_mock_defs_from_yaml(recording_file_content) + .map_err(|err| DataConversionError(err.to_string()))?; + + if all_static_mock_defs.is_empty() { + return Err(ValidationError( + "no mock definitions could be found in the provided recording content".to_string(), + )); + } + + let mut mock_ids = Vec::with_capacity(all_static_mock_defs.len()); + + for static_mock_def in all_static_mock_defs { + let mock_def: MockDefinition = static_mock_def + .try_into() + .map_err(|err: data::Error| DataConversionError(err.to_string()))?; + + let active_mock = self.add_mock(mock_def, false)?; + mock_ids.push(active_mock.id); + } + + Ok(mock_ids) + } + + fn find_forward_rule<'a>( + &'a self, + req: &'a HttpMockRequest, + ) -> Result<(Option), Error> { + let mut state = self.state.lock().unwrap(); + + let result = state + .forwarding_rules + .values() + .find(|&rule| request_matches(&state.matchers, req, &rule.config.request_requirements)) + .cloned(); + + Ok(result) + } + + fn find_proxy_rule<'a>( + &'a self, + req: &'a HttpMockRequest, + ) -> Result, Error> { + let mut state = self.state.lock().unwrap(); + + let result = state + .proxy_rules + .values() + .find(|&rule| request_matches(&state.matchers, req, &rule.config.request_requirements)) + .cloned(); + + Ok(result) + } + + fn record< + IntoResponse: TryInto, + >( + &self, + is_proxied: bool, + time_taken: Duration, + req: HttpMockRequest, + res: IntoResponse, + ) -> Result<(), Error> { + let mut state = self.state.lock().unwrap(); + + let recording_ids: Vec = state + .recordings + .values() + .filter(|rec| request_matches(&state.matchers, &req, &rec.config.request_requirements)) + .map(|r| r.id) + .collect(); + + if recording_ids.is_empty() { + return Ok(()); + } + + let res = res + .try_into() + .map_err(|err| DataConversionError(err.to_string()))?; + + for id in recording_ids { + let rec = state.recordings.get_mut(&id).unwrap(); + let definition = + build_mock_definition(is_proxied, time_taken, &req, &res, &rec.config)?; + rec.mocks.push(definition); + } + + Ok(()) + } +} + +fn build_mock_definition( + is_proxied: bool, + time_taken: Duration, + request: &HttpMockRequest, + response: &MockServerHttpResponse, + config: &RecordingRuleConfig, +) -> Result { + // ************************************************************************************ + // Request + let mut headers = Vec::with_capacity(config.record_headers.len()); + for header_name in &config.record_headers { + let header_name_lowercase = header_name.to_lowercase(); + for (key, value) in request.headers() { + if let Some(key) = key { + if header_name_lowercase == key.to_string().to_lowercase() { + let value = value + .to_str() + .map_err(|err| DataConversionError(err.to_string()))?; + headers.push((header_name.to_string(), value.to_string())) + } + } + } + } + + let request = RequestRequirements { + /* Authority and scheme are assumed to always exist for proxies requests for the + following reasons: + + RFC 7230 - Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing + Section 5.3.2 (absolute-form): + The section clearly states that an absolute URI (absolute-form) must be used when the + request is made to a proxy. This inclusion of the full URI (including the scheme, + host, and optional port) ensures that the proxy can correctly interpret the destination + of the request without additional context. + Exact Text from RFC 7230: + The RFC says under Section 5.3.2: + + "absolute-form = absolute-URI" + "When making a request to a proxy, other than a CONNECT or server-wide + OPTIONS request (as detailed in Section 5.3.4), a client MUST send + the target URI in absolute-form as the request-target." + "An example absolute-form of request-line would be: + GET http://www.example.org/pub/WWW/TheProject.html HTTP/1.1" + */ + host: if is_proxied { + request.uri().host().map(|h| h.to_string()) + } else { + None + }, + host_not: None, + host_contains: None, + host_excludes: None, + host_prefix: None, + host_suffix: None, + host_prefix_not: None, + host_suffix_not: None, + host_matches: None, + port: if is_proxied { + request.uri().port().map(|h| h.as_u16()) + } else { + None + }, + scheme: if is_proxied { + request.uri().scheme().map(|h| h.to_string()) + } else { + None + }, + path: Some(request.uri().path().to_string()), + path_not: None, + path_includes: None, + path_excludes: None, + path_prefix: None, + path_suffix: None, + path_prefix_not: None, + path_suffix_not: None, + path_matches: None, + method: Some(request.method().to_string()), + header: if !headers.is_empty() { + Some(headers) + } else { + None + }, + header_not: None, + header_exists: None, + header_missing: None, + header_includes: None, + header_excludes: None, + header_prefix: None, + header_suffix: None, + header_prefix_not: None, + header_suffix_not: None, + header_matches: None, + header_count: None, + cookie: None, + cookie_not: None, + cookie_exists: None, + cookie_missing: None, + cookie_includes: None, + cookie_excludes: None, + cookie_prefix: None, + cookie_suffix: None, + cookie_prefix_not: None, + cookie_suffix_not: None, + cookie_matches: None, + cookie_count: None, + body: if request.body().is_empty() { + None + } else { + Some(request.body().clone()) + }, + json_body: None, + json_body_not: None, + json_body_includes: None, + body_includes: None, + body_excludes: None, + body_prefix: None, + body_suffix: None, + body_prefix_not: None, + body_suffix_not: None, + body_matches: None, + query_param_exists: None, + query_param_missing: None, + query_param_includes: None, + query_param_excludes: None, + query_param_prefix: None, + query_param_suffix: None, + query_param_prefix_not: None, + query_param_suffix_not: None, + query_param_matches: None, + query_param_count: None, + query_param: None, + form_urlencoded_tuple_exists: None, + form_urlencoded_tuple_missing: None, + form_urlencoded_tuple_includes: None, + form_urlencoded_tuple_excludes: None, + form_urlencoded_tuple_prefix: None, + form_urlencoded_tuple_suffix: None, + form_urlencoded_tuple_prefix_not: None, + form_urlencoded_tuple_suffix_not: None, + form_urlencoded_tuple_matches: None, + form_urlencoded_tuple_count: None, + form_urlencoded_tuple: None, + is_true: None, + scheme_not: None, + port_not: None, + method_not: None, + query_param_not: None, + body_not: None, + json_body_excludes: None, + form_urlencoded_tuple_not: None, + is_false: None, + }; + + // ************************************************************************************ + // Response + let mut response = response.clone(); + + if config.record_response_delays { + response.delay = Some(time_taken.as_millis() as u64) + } + + Ok(MockDefinition { request, response }) +} + +fn validate_request_requirements(req: &RequestRequirements) -> Result<(), Error> { + const NON_BODY_METHODS: &[&str] = &["GET", "HEAD"]; + + if let Some(_body) = &req.body { + if let Some(method) = &req.method { + if NON_BODY_METHODS.contains(&method.as_str()) { + return Err(BodyMethodInvalid); + } + } + } + Ok(()) +} + +fn request_matches( + matchers: &Vec>, + req: &HttpMockRequest, + request_requirements: &RequestRequirements, +) -> bool { + log::trace!("Matching incoming HTTP request"); + matchers + .iter() + .enumerate() + .all(|(i, x)| x.matches(req, request_requirements)) +} + +fn get_distances( + history: &Vec<&Arc>, + matchers: &Vec>, + mock_rr: &RequestRequirements, +) -> BTreeMap { + history + .iter() + .enumerate() + .map(|(idx, req)| (idx, get_request_distance(req, mock_rr, matchers))) + .collect() +} + +fn get_request_distance( + req: &Arc, + mock_request_requirements: &RequestRequirements, + matchers: &Vec>, +) -> usize { + matchers + .iter() + .map(|matcher| matcher.distance(req, mock_request_requirements)) + .sum() +} + +fn get_min_distance_requests(request_distances: &BTreeMap) -> Vec { + // Find the element with the maximum matches + let min_elem = request_distances + .iter() + .min_by(|(idx1, d1), (idx2, d2)| (**d1).cmp(d2)); + + let max = match min_elem { + None => return Vec::new(), + Some((_, n)) => *n, + }; + + request_distances + .into_iter() + .filter(|(idx, distance)| **distance == max) + .map(|(idx, _)| *idx) + .collect() +} + +fn get_request_mismatches( + req: &Arc, + mock_rr: &RequestRequirements, + matchers: &Vec>, +) -> Vec { + matchers + .iter() + .map(|mat| mat.mismatches(req, mock_rr)) + .flatten() + .into_iter() + .collect() +} diff --git a/src/server/tls.rs b/src/server/tls.rs new file mode 100644 index 00000000..1a2a9193 --- /dev/null +++ b/src/server/tls.rs @@ -0,0 +1,311 @@ +use rustls::pki_types::{CertificateDer, PrivateKeyDer}; + +use crate::server::tls::Error::{CaCertificateError, GenerateCertificateError}; +use async_trait::async_trait; +use rcgen::{Certificate, CertificateParams, KeyPair}; +use rustls::{ + crypto::ring::sign::any_supported_type, + server::{ClientHello, ResolvesServerCert}, + sign::CertifiedKey, +}; +use std::{ + collections::HashMap, + fmt::Debug, + io::Cursor, + net::SocketAddr, + sync::{Arc, Mutex, RwLock}, +}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("CA certificate error: {0}")] + CaCertificateError(String), + #[error("cannot generate certificate: {0}")] + GenerateCertificateError(String), +} + +pub trait CertificateResolverFactory { + fn build(&self, tcp_address: SocketAddr) -> Arc; +} + +struct SharedState { + certificates: RwLock>>, + locks: RwLock>>>, + ca_cert_str: String, + ca_key_str: String, +} + +impl std::fmt::Debug for SharedState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SharedState") + .field("certificates", &self.certificates.read().unwrap().keys()) + .field("locks", &self.locks.read().unwrap().keys()) + .field("ca_cert_str", &self.ca_cert_str) + .field("ca_key_str", &self.ca_key_str) + .finish() + } +} + +#[derive(Debug)] +pub struct GeneratingCertificateResolverFactory { + state: Arc, +} + +impl<'a> GeneratingCertificateResolverFactory { + pub fn new>( + ca_cert: IntoString, + ca_key: IntoString, + ) -> Result { + Ok(Self { + state: Arc::new(SharedState { + certificates: RwLock::new(HashMap::new()), + locks: RwLock::new(HashMap::new()), + ca_cert_str: ca_cert.into(), + ca_key_str: ca_key.into(), + }), + }) + } +} + +impl CertificateResolverFactory for GeneratingCertificateResolverFactory { + fn build(&self, tcp_address: SocketAddr) -> Arc { + Arc::new(GeneratingCertificateResolver { + state: self.state.clone(), + tcp_address, + }) + } +} + +#[derive(Debug)] +pub struct GeneratingCertificateResolver { + state: Arc, + tcp_address: SocketAddr, +} + +impl<'a> GeneratingCertificateResolver { + fn load_certificates(cert_pem: String) -> Result>, Error> { + let mut cert_pem_reader = Cursor::new(cert_pem.into_bytes()); + let mut certificates = Vec::new(); + let certs_iterator = rustls_pemfile::certs(&mut cert_pem_reader); + for cert_result in certs_iterator { + let cert = cert_result.map_err(|err| { + GenerateCertificateError(format!("cannot use generated certificate: {:?}", err)) + })?; // Propagate error if any + certificates.push(cert); + } + + Ok(certificates) + } + + fn load_private_key(key_pem: String) -> Result, Error> { + let mut cert_pem_reader = Cursor::new(key_pem.into_bytes()); + let private_key = rustls_pemfile::private_key(&mut cert_pem_reader) + .map_err(|err| { + GenerateCertificateError(format!("cannot use generated private key: {:?}", err)) + })? + .ok_or(GenerateCertificateError(format!( + "invalid generated private key" + )))?; + Ok(private_key) + } + + pub fn generate_host_certificate(&'a self, hostname: &str) -> Result, Error> { + // Create a key pair for the CA from the provided PEM + let ca_key = KeyPair::from_pem(&self.state.ca_key_str).map_err(|err| { + CaCertificateError(format!("Expected CA key to be provided in PEM format but failed to parse it (host: {}: error: {:?})", hostname, err)) + })?; + + // Set up certificate parameters for the new certificate + let mut params = CertificateParams::new(vec![hostname.to_owned()]); + + let key_pair = KeyPair::generate(&rcgen::PKCS_ECDSA_P256_SHA256).map_err(|err| { + GenerateCertificateError(format!( + "Cannot generate new key pair (host: {}: error: {:?})", + hostname, err + )) + })?; + + let serialized_key_pair = key_pair.serialize_pem(); + params.key_pair = Some(key_pair); + + // Create the new certificate + let cert = Certificate::from_params(params).map_err(|err| { + GenerateCertificateError(format!( + "Cannot generate new certificate (host: {}: error: {:?})", + hostname, err + )) + })?; + + // Serialize the new certificate, signing it with the CA's private key + let new_host_cert_params = CertificateParams::from_ca_cert_pem(&self.state.ca_cert_str, ca_key).map_err(|err| { + GenerateCertificateError(format!("Cannot create new host certificate parameters from CA certificate (host: {}: error: {:?})", hostname, err)) + })?; + + let new_host_cert = Certificate::from_params(new_host_cert_params).map_err(|err| { + GenerateCertificateError(format!( + "Cannot generate new host certificate (host: {}: error: {:?})", + hostname, err + )) + })?; + + let cert_pem = cert.serialize_pem_with_signer(&new_host_cert).map_err(|err| { + GenerateCertificateError(format!("Cannot sign generated host certificate with CA certificate (host: {}: error: {:?})", hostname, err)) + })?; + + // Convert the generated key and certificate into rustls-compatible formats + let private_key = Self::load_private_key(serialized_key_pair).map_err(|err| { + GenerateCertificateError(format!( + "Cannot convert generated key pair to private key for host (host: {}: error: {:?})", + hostname, err + )) + })?; + + let certificates = Self::load_certificates(cert_pem).map_err(|err| { + GenerateCertificateError(format!("Cannot convert generated generated cert PEN to list of certificates (host: {}: error: {:?})", hostname, err)) + })?; + + let signing_key = any_supported_type(&private_key).map_err(|err| { + GenerateCertificateError(format!( + "Cannot convert generated private key to signing key (host: {}: error: {:?})", + hostname, err + )) + })?; + + Ok(Arc::new(CertifiedKey::new(certificates, signing_key))) + } + + fn get_lock_for_hostname(&self, hostname: &str) -> Arc> { + let mut locks = self.state.locks.write().unwrap(); + locks + .entry(hostname.to_string()) + .or_insert_with(|| Arc::new(Mutex::new(()))) + .clone() + } + + fn generate(&self, hostname: &str) -> Result, Error> { + { + let configs = self.state.certificates.read().unwrap(); + if let Some(config) = configs.get(hostname) { + return Ok(config.clone()); + } + } + + let lock = self.get_lock_for_hostname(hostname); + let _guard = lock.lock(); + { + let certs = self.state.certificates.read().unwrap(); + if let Some(bundle) = certs.get(hostname) { + return Ok(bundle.clone()); + } + } + + let key = self.generate_host_certificate(hostname).unwrap(); + { + let mut certs = self.state.certificates.write().unwrap(); + certs.insert(hostname.to_string(), key.clone()); + } + + Ok(key) + } +} + +// TODO: Change ResolvesServerCert to acceptor so that async operations are supported +impl ResolvesServerCert for GeneratingCertificateResolver { + // TODO: This implementation is synchronous, which will cause synchronous locking to + // enable certificate caching (lock protected hash map, sync implementation). + // If you look at ResolvesServerCert, it suggests that for async IO, the Acceptor interface + // is recommended for usage. However, it seems to require a significantly larger implementation + // overhead than a ResolvesServerCert. For now, this is an accepted performance loss, but should + // definitely be looked into and improved later! + fn resolve(&self, client_hello: ClientHello) -> Option> { + if let Some(hostname) = client_hello.server_name() { + log::info!("have hostname: {}", hostname); + + return Some(self.generate(hostname).expect(&format!( + "Cannot generate certificate for host {}", + hostname + ))); + } + + // According to https://datatracker.ietf.org/doc/html/rfc6066#section-3 + // clients may choose to not include a server name (SNI extension) in TLS ClientHello + // messages. If there is no SNI extension, we assume the client used an IP address instead + // of a hostname. + let hostname = self.tcp_address.ip().to_string(); + log::info!("no hostname using: {}", hostname); + return Some( + self.generate(&hostname) + .expect(&format!("Cannot generate wildcard certificate")), + ); + } +} + +pub struct TcpStreamPeekBuffer<'a> { + stream: &'a tokio::net::TcpStream, + buffer: Vec, +} + +impl<'a> TcpStreamPeekBuffer<'a> { + pub fn new(stream: &'a tokio::net::TcpStream) -> Self { + TcpStreamPeekBuffer { + stream, + buffer: Vec::new(), + } + } + + pub fn buffer(&self) -> &[u8] { + return &self.buffer; + } + + pub async fn advance(&mut self, offset: usize) -> std::io::Result<()> { + if self.buffer.len() > offset { + return Ok(()); + } + + let required_size = offset + 1; + if required_size > self.buffer.len() { + self.buffer.resize(required_size, 0); + } + + let mut total_peeked = 0; + while total_peeked < required_size { + let peeked_now = self.stream.peek(&mut self.buffer[total_peeked..]).await?; + if peeked_now == 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + "EOF reached before offset", + )); + } + total_peeked += peeked_now; + } + + Ok(()) + } +} + +#[async_trait] +impl<'a> tls_detect::Read<'a> for TcpStreamPeekBuffer<'a> { + async fn read_byte(&mut self, from_offset: usize) -> std::io::Result { + self.advance(from_offset).await?; + Ok(self.buffer[from_offset]) + } + + async fn read_bytes( + &mut self, + from_offset: usize, + to_offset: usize, + ) -> std::io::Result> { + self.advance(to_offset).await?; + Ok(self.buffer[from_offset..to_offset].to_vec()) + } + + async fn read_u16_from_be(&mut self, offset: usize) -> std::io::Result { + let u16_bytes = self.read_bytes(offset, offset + 2).await?; + Ok(u16::from_be_bytes([u16_bytes[0], u16_bytes[1]])) + } + + async fn buffer_to(&mut self, limit: usize) -> std::io::Result<()> { + self.advance(limit).await + } +} diff --git a/src/server/util.rs b/src/server/util.rs index f38b1e95..6735441f 100644 --- a/src/server/util.rs +++ b/src/server/util.rs @@ -1,11 +1,10 @@ -use std::cmp::Ordering; -use std::collections::BTreeMap; +use std::{cmp::Ordering, collections::BTreeMap}; /// Extends a tree map to provide additional operations. pub(crate) trait TreeMapExtension where - K: std::cmp::Ord, - V: std::cmp::Ord, + K: Ord, + V: Ord, { /// Checks if a tree map contains another tree map. fn contains(&self, other: &BTreeMap) -> bool; @@ -17,8 +16,8 @@ where /// Implements [`TreeMapExtension`]. impl TreeMapExtension for BTreeMap where - K: std::cmp::Ord, - V: std::cmp::Ord, + K: Ord, + V: Ord, { fn contains(&self, other: &BTreeMap) -> bool { other.iter().all(|(k, v)| self.contains_entry(k, v)) @@ -75,9 +74,8 @@ impl StringTreeMapExtension for BTreeMap { #[cfg(test)] mod test { - use std::collections::BTreeMap; - use crate::server::util::{StringTreeMapExtension, TreeMapExtension}; + use std::collections::BTreeMap; #[test] fn tree_map_fully_contains_other() { diff --git a/src/server/web/handlers.rs b/src/server/web/handlers.rs deleted file mode 100644 index 9208f38a..00000000 --- a/src/server/web/handlers.rs +++ /dev/null @@ -1,733 +0,0 @@ -use std::cmp::Ordering; -use std::collections::BTreeMap; -use std::str::FromStr; -use std::sync::Arc; - -#[cfg(feature = "cookies")] -use basic_cookies::Cookie; -use serde_json::Value; - -use crate::common::data::{ - ActiveMock, ClosestMatch, HttpMockRequest, Mismatch, MockDefinition, MockServerHttpResponse, - RequestRequirements, -}; -use crate::server::matchers::Matcher; -use crate::server::util::{StringTreeMapExtension, TreeMapExtension}; -use crate::server::MockServerState; - -/// Contains HTTP methods which cannot have a body. -const NON_BODY_METHODS: &[&str] = &["GET", "HEAD"]; - -/// Adds a new mock to the internal state. -pub(crate) fn add_new_mock( - state: &MockServerState, - mock_def: MockDefinition, - is_static: bool, -) -> Result { - let result = validate_mock_definition(&mock_def); - - if let Err(error_msg) = result { - let error_msg = format!("Validation error: {}", error_msg); - return Err(error_msg); - } - - let mock_id = state.create_new_id(); - log::debug!("Adding new mock with ID={}", mock_id); - - let mut mocks = state.mocks.lock().unwrap(); - mocks.insert(mock_id, ActiveMock::new(mock_id, mock_def, is_static)); - - Result::Ok(mock_id) -} - -/// Reads exactly one mock object. -pub(crate) fn read_one_mock( - state: &MockServerState, - id: usize, -) -> Result, String> { - let mocks = state.mocks.lock().unwrap(); - let result = mocks.get(&id); - match result { - Some(found) => Ok(Some(found.clone())), - None => Ok(None), - } -} - -/// Deletes one mock by id. Returns the number of deleted elements. -pub(crate) fn delete_one_mock(state: &MockServerState, id: usize) -> Result { - let mut mocks = state.mocks.lock().unwrap(); - if let Some(m) = mocks.get(&id) { - if m.is_static { - return Err(format!("Cannot delete static mock with ID {}", id)); - } - } - let result = mocks.remove(&id); - - log::debug!("Deleted mock with id={}", id); - Result::Ok(result.is_some()) -} - -/// Deletes all mocks. -pub(crate) fn delete_all_mocks(state: &MockServerState) { - let mut mocks = state.mocks.lock().unwrap(); - let ids: Vec = mocks - .iter() - .filter(|(k, v)| !v.is_static) - .map(|(k, v)| *k) - .collect(); - - ids.iter().for_each(|k| { - mocks.remove(k); - }); - - log::trace!("Deleted all mocks"); -} - -/// Deletes the request history. -pub(crate) fn delete_history(state: &MockServerState) { - let mut mocks = state.history.lock().unwrap(); - mocks.clear(); - log::trace!("Deleted request history"); -} - -/// Finds a mock that matches the current request and serve a response according to the mock -/// specification. If no mock is found, an empty result is being returned. -pub(crate) fn find_mock( - state: &MockServerState, - req: HttpMockRequest, -) -> Result, String> { - let req = Arc::new(req); - { - let mut history = state.history.lock().unwrap(); - if history.len() > 100 { - history.remove(0); - } - history.push(req.clone()); - } - - let mut mocks = state.mocks.lock().unwrap(); - - let result = mocks - .values() - .find(|&mock| request_matches(&state, req.clone(), &mock.definition.request)); - - let found_mock_id = match result { - Some(mock) => Some(mock.id), - None => None, - }; - - if let Some(found_id) = found_mock_id { - log::debug!( - "Matched mock with id={} to the following request: {:#?}", - found_id, - req - ); - - let mock = mocks.get_mut(&found_id).unwrap(); - mock.call_counter += 1; - - return Ok(Some(mock.definition.response.clone())); - } - - log::debug!( - "Could not match any mock to the following request: {:#?}", - req - ); - - Result::Ok(None) -} - -/// Checks if a request matches a mock. -fn request_matches( - state: &MockServerState, - req: Arc, - mock: &RequestRequirements, -) -> bool { - log::trace!("Matching incoming HTTP request"); - state - .matchers - .iter() - .enumerate() - .all(|(i, x)| x.matches(&req, mock)) -} - -/// Deletes the request history. -pub(crate) fn verify( - state: &MockServerState, - mock_rr: &RequestRequirements, -) -> Result, String> { - let mut history = state.history.lock().unwrap(); - - let non_matching_requests: Vec<&Arc> = history - .iter() - .filter(|a| !request_matches(state, (*a).clone(), mock_rr)) - .collect(); - - let request_distances = get_distances(&non_matching_requests, &state.matchers, mock_rr); - let best_matches = get_min_distance_requests(&request_distances); - - let closes_match_request_idx = match best_matches.get(0) { - None => return Ok(None), - Some(idx) => *idx, - }; - - let req = non_matching_requests.get(closes_match_request_idx).unwrap(); - let mismatches = get_request_mismatches(req, &mock_rr, &state.matchers); - - Ok(Some(ClosestMatch { - request: HttpMockRequest::clone(&req), - request_index: closes_match_request_idx, - mismatches, - })) -} - -/// Validates a mock request. -fn validate_mock_definition(req: &MockDefinition) -> Result<(), String> { - if let Some(_body) = &req.request.body { - if let Some(method) = &req.request.method { - if NON_BODY_METHODS.contains(&method.as_str()) { - return Err(String::from( - "A body cannot be sent along with the specified method", - )); - } - } - } - Ok(()) -} - -// Remember the maximum number of matchers that successfully matched -fn get_distances( - history: &Vec<&Arc>, - matchers: &Vec>, - mock_rr: &RequestRequirements, -) -> BTreeMap { - history - .iter() - .enumerate() - .map(|(idx, req)| (idx, get_request_distance(req, mock_rr, matchers))) - .collect() -} - -fn get_request_mismatches( - req: &Arc, - mock_rr: &RequestRequirements, - matchers: &Vec>, -) -> Vec { - matchers - .iter() - .map(|mat| mat.mismatches(req, mock_rr)) - .flatten() - .into_iter() - .collect() -} - -fn get_request_distance( - req: &Arc, - mock_rr: &RequestRequirements, - matchers: &Vec>, -) -> usize { - matchers - .iter() - .map(|matcher| matcher.distance(req, mock_rr)) - .sum() -} - -// Remember the maximum number of matchers that successfully matched -fn get_min_distance_requests(request_distances: &BTreeMap) -> Vec { - // Find the element with the maximum matches - let min_elem = request_distances - .iter() - .min_by(|(idx1, d1), (idx2, d2)| (**d1).cmp(d2)); - - let max = match min_elem { - None => return Vec::new(), - Some((_, n)) => *n, - }; - - request_distances - .into_iter() - .filter(|(idx, distance)| **distance == max) - .map(|(idx, _)| *idx) - .collect() -} - -#[cfg(test)] -mod test { - use std::collections::BTreeMap; - use std::rc::Rc; - use std::sync::Arc; - - use regex::Regex; - - use crate::common::data::{ - HttpMockRequest, MockDefinition, MockServerHttpResponse, Pattern, RequestRequirements, - }; - use crate::server::web::handlers::{ - add_new_mock, read_one_mock, request_matches, validate_mock_definition, verify, - }; - use crate::server::MockServerState; - use crate::Method; - - /// TODO - #[test] - fn header_names_case_insensitive() {} - - /// TODO - #[test] - fn parsing_query_params_test() {} - - /// TODO - #[test] - fn parsing_query_contains_test() {} - - /// TODO - #[test] - fn header_exists_test() {} - - /// TODO - #[test] - fn path_contains_test() {} - - /// TODO - #[test] - fn path_pattern_test() {} - - #[test] - fn body_contains_test() { - // Arrange - let request1 = HttpMockRequest::new("GET".to_string(), "/test-path".to_string()) - .with_body("test".as_bytes().to_vec()); - let request2 = HttpMockRequest::new("GET".to_string(), "/test-path".to_string()) - .with_body("test".as_bytes().to_vec()); - - let requirements1 = RequestRequirements::new().with_body_contains(vec!["xxx".to_string()]); - let requirements2 = RequestRequirements::new().with_body_contains(vec!["es".to_string()]); - - // Act - let does_match1 = request_matches( - &MockServerState::default(), - Arc::new(request1), - &requirements1, - ); - let does_match2 = request_matches( - &MockServerState::default(), - Arc::new(request2), - &requirements2, - ); - - // Assert - assert_eq!(false, does_match1); - assert_eq!(true, does_match2); - } - - #[test] - fn body_matches_query_params_exact_test() { - // Arrange - let mut params1 = Vec::new(); - params1.push(("k".to_string(), "v".to_string())); - - let mut params2 = Vec::new(); - params2.push(("h".to_string(), "o".to_string())); - - let request1 = HttpMockRequest::new("GET".to_string(), "/test-path".to_string()) - .with_query_params(params1.clone()); - let request2 = HttpMockRequest::new("GET".to_string(), "/test-path".to_string()) - .with_query_params(params1.clone()); - - let requirements1 = RequestRequirements::new().with_query_param(params2); - let requirements2 = RequestRequirements::new().with_query_param(params1.clone()); - - // Act - let does_match1 = request_matches( - &MockServerState::default(), - Arc::new(request1), - &requirements1, - ); - let does_match2 = request_matches( - &MockServerState::default(), - Arc::new(request2), - &requirements2, - ); - - // Assert - assert_eq!(false, does_match1); - assert_eq!(true, does_match2); - } - - /// TODO - #[test] - fn body_contains_includes_json_test() {} - - /// TODO - #[test] - fn body_json_exact_match_test() {} - - /// This test makes sure that a request is considered "matched" if the paths of the - /// request and the mock are equal. - #[test] - fn request_matches_path_match() { - // Arrange - let req1 = HttpMockRequest::new("GET".to_string(), "/test-path".to_string()); - - let req2 = RequestRequirements::new().with_path("/test-path".to_string()); - - // Act - let does_match = request_matches(&MockServerState::default(), Arc::new(req1), &req2); - - // Assert - assert_eq!(true, does_match); - } - - /// This test makes sure that a request is considered "not matched" if the paths of the - /// request and the mock are not equal. - #[test] - fn request_matches_path_no_match() { - // Arrange - let req1 = HttpMockRequest::new("GET".to_string(), "/test-path".to_string()); - - let req2 = RequestRequirements::new().with_path("/another-path".to_string()); - - // Act - let does_match = request_matches(&MockServerState::default(), Arc::new(req1), &req2); - - // Assert - assert_eq!(false, does_match); - } - - /// This test makes sure that a request is considered "matched" if the methods of the - /// request and the mock are equal. - #[test] - fn request_matches_method_match() { - // Arrange - let req1 = HttpMockRequest::new("GET".to_string(), "/test".to_string()); - - let req2 = RequestRequirements::new().with_method("GET".to_string()); - - // Act - let does_match = request_matches(&MockServerState::default(), Arc::new(req1), &req2); - - // Assert - assert_eq!(true, does_match); - } - - /// This test makes sure that a request is considered "not matched" if the methods of the - /// request and the mock are not equal. - #[test] - fn request_matches_method_no_match() { - // Arrange - let req1 = HttpMockRequest::new("GET".to_string(), "/test".to_string()); - - let req2 = RequestRequirements::new().with_method("POST".to_string()); - - // Act - let does_match = request_matches(&MockServerState::default(), Arc::new(req1), &req2); - - // Assert - assert_eq!(false, does_match); - } - - /// This test makes sure that a request is considered "matched" if the bodies of both, - /// the request and the mock are present and have equal content. - #[test] - fn request_matches_body_match() { - // Arrange - let req1 = HttpMockRequest::new("GET".to_string(), "/test".to_string()) - .with_body("test".as_bytes().to_vec()); - - let req2 = RequestRequirements::new().with_body("test".to_string()); - - // Act - let does_match = request_matches(&MockServerState::default(), Arc::new(req1), &req2); - - // Assert - assert_eq!(true, does_match); - } - - /// This test makes sure that a request is considered "not matched" if the bodies of both, - /// the request and the mock are present, but do have different content. - #[test] - fn request_matches_body_no_match() { - // Arrange - let req1 = HttpMockRequest::new("GET".to_string(), "/test".to_string()) - .with_body("some text".as_bytes().to_vec()); - - let req2 = RequestRequirements::new().with_body("some other text".to_string()); - - // Act - let does_match = request_matches(&MockServerState::default(), Arc::new(req1), &req2); - - // Assert - assert_eq!(false, does_match); - } - - /// This test makes sure that a request is considered "matched" when the request contains - /// exactly the same as the mock expects. - #[test] - fn request_matches_headers_exact_match() { - // Arrange - let mut h1 = Vec::new(); - h1.push(("h1".to_string(), "v1".to_string())); - h1.push(("h2".to_string(), "v2".to_string())); - - let mut h2 = Vec::new(); - h2.push(("h1".to_string(), "v1".to_string())); - h2.push(("h2".to_string(), "v2".to_string())); - - let req1 = HttpMockRequest::new("GET".to_string(), "/test".to_string()).with_headers(h1); - - let req2 = RequestRequirements::new().with_headers(h2); - - // Act - let does_match = request_matches(&MockServerState::default(), Arc::new(req1), &req2); - - // Assert - assert_eq!(true, does_match); - } - - /// This test makes sure that a request is considered "not matched" when the request misses - /// headers. - #[test] - fn request_matches_query_param() { - // Arrange - let req1 = HttpMockRequest::new("GET".to_string(), "/test".to_string()) - .with_body("test".as_bytes().to_vec()); - - let req2 = RequestRequirements::new().with_body("test".to_string()); - - // Act - let does_match = request_matches(&MockServerState::default(), Arc::new(req1), &req2); - - // Assert - assert_eq!(true, does_match); - } - - /// This test makes sure that even the headers of a mock and a request differ, - /// the request still is considered "matched" when the request does contain more than - /// all expected headers that. Hence a request is allowed to contain headers that a mock - /// does not. - #[test] - fn request_matches_headers_match_superset() { - // Arrange - let mut h1 = Vec::new(); - h1.push(("h1".to_string(), "v1".to_string())); - h1.push(("h2".to_string(), "v2".to_string())); - - let mut h2 = Vec::new(); - h2.push(("h1".to_string(), "v1".to_string())); - - let req1 = HttpMockRequest::new("GET".to_string(), "/test".to_string()).with_headers(h1); - let req2 = RequestRequirements::new().with_headers(h2); - - // Act - let does_match = request_matches(&MockServerState::default(), Arc::new(req1), &req2); - - // Assert - assert_eq!(true, does_match); // matches, because request contains more headers than the mock expects - } - - /// This test makes sure that even the headers of a mock and a request differ, - /// the request still is considered "matched" when the mock does not expect any headers - /// at all. Hence a request is allowed to contain headers that a mock does not. - #[test] - fn request_matches_headers_no_match_empty() { - // Arrange - let mut req_headers = Vec::new(); - req_headers.push(("req_headers".to_string(), "v1".to_string())); - req_headers.push(("h2".to_string(), "v2".to_string())); - - let req = - HttpMockRequest::new("GET".to_string(), "/test".to_string()).with_headers(req_headers); - - let mock = RequestRequirements::new(); - - // Act - let does_match_1 = request_matches(&MockServerState::default(), Arc::new(req), &mock); - - // Assert - assert_eq!(true, does_match_1); // effectively empty because mock does not expect any headers - } - - /// This test makes sure no present headers on both sides, the mock and the request, are - /// considered equal. - #[test] - fn request_matches_headers_match_empty() { - // Arrange - let req1 = HttpMockRequest::new("GET".to_string(), "/test".to_string()); - let req2 = RequestRequirements::new(); - - // Act - let does_match = request_matches(&MockServerState::default(), Arc::new(req1), &req2); - - // Assert - assert_eq!(true, does_match); - } - - /// This test ensures that mock request cannot contain a request method that cannot - /// be sent along with a request body. - #[test] - fn validate_mock_definition_no_body_method() { - // Arrange - let req = RequestRequirements::new() - .with_path("/test".to_string()) - .with_method("GET".to_string()) - .with_body("test".to_string()); - - let res = MockServerHttpResponse { - body: None, - delay: None, - status: Some(418), - headers: None, - }; - - let smr = MockDefinition::new(req, res); - - // Act - let result = validate_mock_definition(&smr); - - // Assert - assert_eq!(true, result.is_err()); - assert_eq!( - true, - result - .unwrap_err() - .eq("A body cannot be sent along with the specified method") - ); - } - - /// This test ensures that mock request cannot contain an empty path. - #[test] - fn validate_mock_definition_no_path() { - // Arrange - let req = RequestRequirements::new(); - let res = MockServerHttpResponse { - body: None, - delay: None, - status: Some(418), - headers: None, - }; - - let smr = MockDefinition::new(req, res); - - // Act - let result = validate_mock_definition(&smr); - - // Assert - assert_eq!(true, result.is_ok()); - } - - /// This test ensures that mock validation is being invoked. - #[test] - fn add_new_mock_validation_error() { - // Arrange - let state = MockServerState::default(); - let mut req = RequestRequirements::new(); - req.method = Some("GET".into()); - req.body = Some("body".into()); - - let res = MockServerHttpResponse { - body: None, - delay: None, - status: Some(200), - headers: None, - }; - - let mock_def = MockDefinition::new(req, res); - - // Act - let result = add_new_mock(&state, mock_def, false); - - // Assert - assert_eq!(result.is_err(), true); - assert_eq!(result.err().unwrap().contains("Validation error"), true); - } - - /// This test ensures that reading a non-existent mock does not result in an error but an - /// empty result. - #[test] - fn read_one_returns_none_test() { - // Arrange - let state = MockServerState::default(); - - // Act - let result = read_one_mock(&state, 6); - - // Assert - assert_eq!(result.is_ok(), true); - assert_eq!(result.unwrap().is_none(), true); - } - - /// This test checks if matching "path_contains" is working as expected. - #[test] - fn not_match_path_contains_test() { - // Arrange - let msr = Arc::new(HttpMockRequest::new("GET".into(), "test".into())); - let mut mock1 = RequestRequirements::new(); - mock1.path_contains = Some(vec!["x".into()]); - let mut mock2 = RequestRequirements::new(); - mock2.path_contains = Some(vec!["es".into()]); - - // Act - let result1 = request_matches(&MockServerState::default(), msr.clone(), &mock1); - let result2 = request_matches(&MockServerState::default(), msr.clone(), &mock2); - - // Assert - assert_eq!(result1, false); - assert_eq!(result2, true); - } - - /// This test checks if matching "path_matches" is working as expected. - #[test] - fn not_match_path_matches_test() { - // Arrange - let msr = Arc::new(HttpMockRequest::new("GET".into(), "test".into())); - let mut mock1 = RequestRequirements::new(); - mock1.path_matches = Some(vec![Pattern::from_regex(Regex::new(r#"x"#).unwrap())]); - let mut mock2 = RequestRequirements::new(); - mock2.path_matches = Some(vec![Pattern::from_regex(Regex::new(r#"test"#).unwrap())]); - - // Act - let result1 = request_matches(&MockServerState::default(), msr.clone(), &mock1); - let result2 = request_matches(&MockServerState::default(), msr.clone(), &mock2); - - // Assert - assert_eq!(result1, false); - assert_eq!(result2, true); - } - - /// This test checks if distance has influence on verification. - #[test] - fn verify_test() { - // Arrange - let mut mock_server_state = MockServerState::default(); - { - let mut mocks = mock_server_state.history.lock().unwrap(); - // 1: close request - mocks.push(Arc::new(HttpMockRequest::new( - String::from("POST"), - String::from("/Brians"), - ))); - // 2: closest request - mocks.push(Arc::new(HttpMockRequest::new( - String::from("GET"), - String::from("/Briann"), - ))); - // 3: distant request - mocks.push(Arc::new(HttpMockRequest::new( - String::from("DELETE"), - String::from("/xxxxxxx/xxxxxx"), - ))); - } - - let mut rr = RequestRequirements::new(); - rr.method = Some("GET".to_string()); - rr.path = Some("/Briann".to_string()); - - // Act - let result = verify(&mock_server_state, &rr); - - // Assert - assert_eq!(result.as_ref().is_ok(), true); - assert_eq!(result.as_ref().unwrap().is_some(), true); - assert_eq!(result.as_ref().unwrap().as_ref().unwrap().request_index, 0); - } -} diff --git a/src/server/web/mod.rs b/src/server/web/mod.rs deleted file mode 100644 index 8cf6b43c..00000000 --- a/src/server/web/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub(crate) mod handlers; -pub(crate) mod routes; diff --git a/src/server/web/routes.rs b/src/server/web/routes.rs deleted file mode 100644 index 8ab54801..00000000 --- a/src/server/web/routes.rs +++ /dev/null @@ -1,196 +0,0 @@ -use std::collections::BTreeMap; - -use serde::Serialize; - -use crate::common::data::{ - ErrorResponse, HttpMockRequest, MockDefinition, MockRef, MockServerHttpResponse, - RequestRequirements, -}; -use crate::server::web::handlers; -use crate::server::{MockServerState, ServerRequestHeader, ServerResponse}; -use std::time::Instant; -use tokio::time::Duration; - -/// This route is responsible for adding a new mock -pub(crate) fn ping() -> Result { - create_response(200, None, None) -} - -/// This route is responsible for adding a new mock -pub(crate) fn add(state: &MockServerState, body: Vec) -> Result { - let mock_def: serde_json::Result = serde_json::from_slice(&body); - - if let Err(e) = mock_def { - return create_json_response(500, None, ErrorResponse::new(&e)); - } - let mock_def = mock_def.unwrap(); - - let result = handlers::add_new_mock(&state, mock_def, false); - - match result { - Err(e) => create_json_response(500, None, ErrorResponse::new(&e)), - Ok(mock_id) => create_json_response(201, None, MockRef { mock_id }), - } -} - -/// This route is responsible for deleting mocks -pub(crate) fn delete_one(state: &MockServerState, id: usize) -> Result { - let result = handlers::delete_one_mock(state, id); - match result { - Err(e) => create_json_response(500, None, ErrorResponse::new(&e)), - Ok(found) => { - if found { - create_response(202, None, None) - } else { - create_response(404, None, None) - } - } - } -} - -/// This route is responsible for deleting all mocks -pub(crate) fn delete_all_mocks(state: &MockServerState) -> Result { - handlers::delete_all_mocks(state); - create_response(202, None, None) -} - -/// This route is responsible for deleting all mocks -pub(crate) fn delete_history(state: &MockServerState) -> Result { - handlers::delete_history(state); - create_response(202, None, None) -} - -/// This route is responsible for deleting mocks -pub(crate) fn read_one(state: &MockServerState, id: usize) -> Result { - let handler_result = handlers::read_one_mock(state, id); - match handler_result { - Err(e) => create_json_response(500, None, ErrorResponse { message: e }), - Ok(mock_opt) => match mock_opt { - Some(mock) => create_json_response(200, None, mock), - None => create_response(404, None, None), - }, - } -} - -/// This route is responsible for verification -pub(crate) fn verify(state: &MockServerState, body: Vec) -> Result { - let mock_rr: serde_json::Result = serde_json::from_slice(&body); - if let Err(e) = mock_rr { - return create_json_response(500, None, ErrorResponse::new(&e)); - } - - match handlers::verify(&state, &mock_rr.unwrap()) { - Err(e) => create_json_response(500, None, ErrorResponse::new(&e)), - Ok(closest_match) => match closest_match { - None => create_response(404, None, None), - Some(cm) => create_json_response(200, None, cm), - }, - } -} - -/// This route is responsible for finding a mock that matches the current request and serve a -/// response according to the mock specification -pub(crate) async fn serve( - state: &MockServerState, - req: &ServerRequestHeader, - body: Vec, -) -> Result { - let handler_request_result = to_handler_request(&req, body); - let result = match handler_request_result { - Ok(handler_request) => { - let handler_response = handlers::find_mock(&state, handler_request); - let handler_response = postprocess_response(handler_response).await; - to_route_response(handler_response) - } - Err(e) => create_json_response(500, None, ErrorResponse::new(&e)), - }; - return result; -} - -/// Maps the result of the serve handler to an HTTP response which the web framework understands -fn to_route_response( - handler_result: Result, String>, -) -> Result { - match handler_result { - Err(e) => create_json_response(500 as u16, None, ErrorResponse { message: e }), - Ok(res) => match res { - None => create_json_response( - 404, - None, - ErrorResponse::new(&"Request did not match any route or mock"), - ), - Some(res) => create_response(res.status.unwrap_or(200), res.headers, res.body), - }, - } -} - -fn create_json_response( - status: u16, - headers: Option>, - body: T, -) -> Result -where - T: Serialize, -{ - let body = serde_json::to_vec(&body); - if let Err(e) = body { - return Err(format!("Cannot serialize body: {}", e)); - } - - let mut headers = headers.unwrap_or_default(); - headers.push(("content-type".to_string(), "application/json".to_string())); - - create_response(status, Some(headers), Some(body.unwrap())) -} - -fn create_response( - status: u16, - headers: Option>, - body: Option>, -) -> Result { - let headers = headers.unwrap_or_default(); - let body = body.unwrap_or_default(); - Ok(ServerResponse::new(status, headers, body)) -} - -/// Maps the request of the serve handler to a request representation which the handlers understand -fn to_handler_request(req: &ServerRequestHeader, body: Vec) -> Result { - let query_params = extract_query_params(&req.query); - if let Err(e) = query_params { - return Err(format!("error parsing query_params: {}", e)); - } - - let request = HttpMockRequest::new(req.method.to_string(), req.path.to_string()) - .with_headers(req.headers.clone()) - .with_query_params(query_params.unwrap()) - .with_body(body); - - Ok(request) -} - -/// Extracts all query parameters from the URI of the given request. -fn extract_query_params(query_string: &str) -> Result, String> { - // HACK: There doesn't seem to be a way to just parse Query string with `url` crate - // Lets just prefix a dummy URL for parsing. - let url = format!("http://dummy?{}", query_string); - let url = url::Url::parse(&url).map_err(|e| e.to_string())?; - - let query_params = url - .query_pairs() - .map(|(k, v)| (k.into(), v.into())) - .collect(); - - Ok(query_params) -} - -/// Processes the response -async fn postprocess_response( - result: Result, String>, -) -> Result, String> { - if let Ok(Some(response_def)) = &result { - if let Some(duration) = response_def.delay { - tokio::time::sleep(duration).await; - } - } - result -} diff --git a/src/standalone.rs b/src/standalone.rs deleted file mode 100644 index 00d1fb1c..00000000 --- a/src/standalone.rs +++ /dev/null @@ -1,157 +0,0 @@ -use std::fs; -use std::fs::read_dir; -use std::future::Future; -use std::path::PathBuf; -use std::process::Output; -use std::str::FromStr; -use std::sync::Arc; - -use regex::Regex; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use tokio::time::Duration; - -use crate::common::data::{MockDefinition, MockServerHttpResponse, Pattern, RequestRequirements}; -use crate::common::util::read_file; -use crate::server::web::handlers::add_new_mock; -use crate::server::{start_server, MockServerState}; -use crate::Method; - -#[derive(Debug, PartialEq, Serialize, Deserialize)] -pub struct NameValuePair { - name: String, - value: String, -} - -#[derive(Debug, Serialize, Deserialize)] -struct YAMLRequestRequirements { - pub path: Option, - pub path_contains: Option>, - pub path_matches: Option>, - pub method: Option, - pub header: Option>, - pub header_exists: Option>, - pub cookie: Option>, - pub cookie_exists: Option>, - pub body: Option, - pub json_body: Option, - pub json_body_partial: Option>, - pub body_contains: Option>, - pub body_matches: Option>, - pub query_param_exists: Option>, - pub query_param: Option>, - pub x_www_form_urlencoded_key_exists: Option>, - pub x_www_form_urlencoded_tuple: Option>, -} - -#[derive(Debug, Serialize, Deserialize)] -struct YAMLHTTPResponse { - pub status: Option, - pub header: Option>, - pub body: Option, - pub delay: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -struct YAMLMockDefinition { - when: YAMLRequestRequirements, - then: YAMLHTTPResponse, -} - -pub async fn start_standalone_server( - port: u16, - expose: bool, - static_mock_dir_path: Option, - print_access_log: bool, - history_limit: usize, - shutdown: F, -) -> Result<(), String> -where - F: Future, -{ - let state = Arc::new(MockServerState::new(history_limit)); - - #[cfg(feature = "standalone")] - static_mock_dir_path.map(|path| { - read_static_mocks(path) - .into_iter() - .map(|d| map_to_mock_definition(d)) - .for_each(|static_mock| { - add_new_mock(&state, static_mock, true).expect("cannot add static mock"); - }) - }); - - start_server(port, expose, &state, None, print_access_log, shutdown).await -} - -#[cfg(feature = "standalone")] -fn read_static_mocks(path: PathBuf) -> Vec { - let mut definitions = Vec::new(); - - let paths = read_dir(path).expect("cannot list files in directory"); - for file_path in paths { - let file_path = file_path.unwrap().path(); - if let Some(ext) = file_path.extension() { - if !"yaml".eq(ext) && !"yml".eq(ext) { - continue; - } - } - - log::info!( - "Loading static mock file from '{}'", - file_path.to_string_lossy() - ); - let content = read_file(file_path).expect("cannot read from file"); - let content = String::from_utf8(content).expect("cannot convert file content"); - - definitions.push(serde_yaml::from_str(&content).unwrap()); - } - - return definitions; -} - -#[cfg(feature = "standalone")] -fn map_to_mock_definition(yaml_definition: YAMLMockDefinition) -> MockDefinition { - MockDefinition { - request: RequestRequirements { - path: yaml_definition.when.path, - path_contains: yaml_definition.when.path_contains, - path_matches: to_pattern_vec(yaml_definition.when.path_matches), - method: yaml_definition.when.method.map(|m| m.to_string()), - headers: to_pair_vec(yaml_definition.when.header), - header_exists: yaml_definition.when.header_exists, - cookies: to_pair_vec(yaml_definition.when.cookie), - cookie_exists: yaml_definition.when.cookie_exists, - body: yaml_definition.when.body, - json_body: yaml_definition.when.json_body, - json_body_includes: yaml_definition.when.json_body_partial, - body_contains: yaml_definition.when.body_contains, - body_matches: to_pattern_vec(yaml_definition.when.body_matches), - query_param_exists: yaml_definition.when.query_param_exists, - query_param: to_pair_vec(yaml_definition.when.query_param), - x_www_form_urlencoded: to_pair_vec(yaml_definition.when.x_www_form_urlencoded_tuple), - x_www_form_urlencoded_key_exists: yaml_definition.when.x_www_form_urlencoded_key_exists, - matchers: None, - }, - response: MockServerHttpResponse { - status: yaml_definition.then.status, - headers: to_pair_vec(yaml_definition.then.header), - body: yaml_definition.then.body.map(|b| b.into_bytes()), - delay: yaml_definition.then.delay.map(|v| Duration::from_millis(v)), - }, - } -} - -#[cfg(feature = "standalone")] -fn to_pattern_vec(vec: Option>) -> Option> { - vec.map(|vec| { - vec.iter() - .map(|val| Pattern::from_regex(Regex::from_str(val).expect("cannot parse regex"))) - .collect() - }) -} - -#[cfg(feature = "standalone")] -fn to_pair_vec(kvp: Option>) -> Option> { - kvp.map(|vec| vec.into_iter().map(|nvp| (nvp.name, nvp.value)).collect()) -} diff --git a/tarpaulin.full.toml b/tarpaulin.full.toml new file mode 100644 index 00000000..eb28857e --- /dev/null +++ b/tarpaulin.full.toml @@ -0,0 +1,514 @@ +[none] +release = true + +[standalone] +features = "standalone" +release = true + +[cookies] +features = "cookies" +release = true + +[standalone.cookies] +features = "standalone,cookies" +release = true + +[remote] +features = "remote" +release = true + +[standalone.remote] +features = "standalone,remote" +release = true + +[cookies.remote] +features = "cookies,remote" +release = true + +[standalone.cookies.remote] +features = "standalone,cookies,remote" +release = true + +[proxy] +features = "proxy" +release = true + +[standalone.proxy] +features = "standalone,proxy" +release = true + +[cookies.proxy] +features = "cookies,proxy" +release = true + +[standalone.cookies.proxy] +features = "standalone,cookies,proxy" +release = true + +[remote.proxy] +features = "remote,proxy" +release = true + +[standalone.remote.proxy] +features = "standalone,remote,proxy" +release = true + +[cookies.remote.proxy] +features = "cookies,remote,proxy" +release = true + +[standalone.cookies.remote.proxy] +features = "standalone,cookies,remote,proxy" +release = true + +[record] +features = "record" +release = true + +[standalone.record] +features = "standalone,record" +release = true + +[cookies.record] +features = "cookies,record" +release = true + +[standalone.cookies.record] +features = "standalone,cookies,record" +release = true + +[remote.record] +features = "remote,record" +release = true + +[standalone.remote.record] +features = "standalone,remote,record" +release = true + +[cookies.remote.record] +features = "cookies,remote,record" +release = true + +[standalone.cookies.remote.record] +features = "standalone,cookies,remote,record" +release = true + +[proxy.record] +features = "proxy,record" +release = true + +[standalone.proxy.record] +features = "standalone,proxy,record" +release = true + +[cookies.proxy.record] +features = "cookies,proxy,record" +release = true + +[standalone.cookies.proxy.record] +features = "standalone,cookies,proxy,record" +release = true + +[remote.proxy.record] +features = "remote,proxy,record" +release = true + +[standalone.remote.proxy.record] +features = "standalone,remote,proxy,record" +release = true + +[cookies.remote.proxy.record] +features = "cookies,remote,proxy,record" +release = true + +[standalone.cookies.remote.proxy.record] +features = "standalone,cookies,remote,proxy,record" +release = true + +[https] +features = "https" +release = true + +[standalone.https] +features = "standalone,https" +release = true + +[cookies.https] +features = "cookies,https" +release = true + +[standalone.cookies.https] +features = "standalone,cookies,https" +release = true + +[remote.https] +features = "remote,https" +release = true + +[standalone.remote.https] +features = "standalone,remote,https" +release = true + +[cookies.remote.https] +features = "cookies,remote,https" +release = true + +[standalone.cookies.remote.https] +features = "standalone,cookies,remote,https" +release = true + +[proxy.https] +features = "proxy,https" +release = true + +[standalone.proxy.https] +features = "standalone,proxy,https" +release = true + +[cookies.proxy.https] +features = "cookies,proxy,https" +release = true + +[standalone.cookies.proxy.https] +features = "standalone,cookies,proxy,https" +release = true + +[remote.proxy.https] +features = "remote,proxy,https" +release = true + +[standalone.remote.proxy.https] +features = "standalone,remote,proxy,https" +release = true + +[cookies.remote.proxy.https] +features = "cookies,remote,proxy,https" +release = true + +[standalone.cookies.remote.proxy.https] +features = "standalone,cookies,remote,proxy,https" +release = true + +[record.https] +features = "record,https" +release = true + +[standalone.record.https] +features = "standalone,record,https" +release = true + +[cookies.record.https] +features = "cookies,record,https" +release = true + +[standalone.cookies.record.https] +features = "standalone,cookies,record,https" +release = true + +[remote.record.https] +features = "remote,record,https" +release = true + +[standalone.remote.record.https] +features = "standalone,remote,record,https" +release = true + +[cookies.remote.record.https] +features = "cookies,remote,record,https" +release = true + +[standalone.cookies.remote.record.https] +features = "standalone,cookies,remote,record,https" +release = true + +[proxy.record.https] +features = "proxy,record,https" +release = true + +[standalone.proxy.record.https] +features = "standalone,proxy,record,https" +release = true + +[cookies.proxy.record.https] +features = "cookies,proxy,record,https" +release = true + +[standalone.cookies.proxy.record.https] +features = "standalone,cookies,proxy,record,https" +release = true + +[remote.proxy.record.https] +features = "remote,proxy,record,https" +release = true + +[standalone.remote.proxy.record.https] +features = "standalone,remote,proxy,record,https" +release = true + +[cookies.remote.proxy.record.https] +features = "cookies,remote,proxy,record,https" +release = true + +[standalone.cookies.remote.proxy.record.https] +features = "standalone,cookies,remote,proxy,record,https" +release = true + +[http2] +features = "http2" +release = true + +[standalone.http2] +features = "standalone,http2" +release = true + +[cookies.http2] +features = "cookies,http2" +release = true + +[standalone.cookies.http2] +features = "standalone,cookies,http2" +release = true + +[remote.http2] +features = "remote,http2" +release = true + +[standalone.remote.http2] +features = "standalone,remote,http2" +release = true + +[cookies.remote.http2] +features = "cookies,remote,http2" +release = true + +[standalone.cookies.remote.http2] +features = "standalone,cookies,remote,http2" +release = true + +[proxy.http2] +features = "proxy,http2" +release = true + +[standalone.proxy.http2] +features = "standalone,proxy,http2" +release = true + +[cookies.proxy.http2] +features = "cookies,proxy,http2" +release = true + +[standalone.cookies.proxy.http2] +features = "standalone,cookies,proxy,http2" +release = true + +[remote.proxy.http2] +features = "remote,proxy,http2" +release = true + +[standalone.remote.proxy.http2] +features = "standalone,remote,proxy,http2" +release = true + +[cookies.remote.proxy.http2] +features = "cookies,remote,proxy,http2" +release = true + +[standalone.cookies.remote.proxy.http2] +features = "standalone,cookies,remote,proxy,http2" +release = true + +[record.http2] +features = "record,http2" +release = true + +[standalone.record.http2] +features = "standalone,record,http2" +release = true + +[cookies.record.http2] +features = "cookies,record,http2" +release = true + +[standalone.cookies.record.http2] +features = "standalone,cookies,record,http2" +release = true + +[remote.record.http2] +features = "remote,record,http2" +release = true + +[standalone.remote.record.http2] +features = "standalone,remote,record,http2" +release = true + +[cookies.remote.record.http2] +features = "cookies,remote,record,http2" +release = true + +[standalone.cookies.remote.record.http2] +features = "standalone,cookies,remote,record,http2" +release = true + +[proxy.record.http2] +features = "proxy,record,http2" +release = true + +[standalone.proxy.record.http2] +features = "standalone,proxy,record,http2" +release = true + +[cookies.proxy.record.http2] +features = "cookies,proxy,record,http2" +release = true + +[standalone.cookies.proxy.record.http2] +features = "standalone,cookies,proxy,record,http2" +release = true + +[remote.proxy.record.http2] +features = "remote,proxy,record,http2" +release = true + +[standalone.remote.proxy.record.http2] +features = "standalone,remote,proxy,record,http2" +release = true + +[cookies.remote.proxy.record.http2] +features = "cookies,remote,proxy,record,http2" +release = true + +[standalone.cookies.remote.proxy.record.http2] +features = "standalone,cookies,remote,proxy,record,http2" +release = true + +[https.http2] +features = "https,http2" +release = true + +[standalone.https.http2] +features = "standalone,https,http2" +release = true + +[cookies.https.http2] +features = "cookies,https,http2" +release = true + +[standalone.cookies.https.http2] +features = "standalone,cookies,https,http2" +release = true + +[remote.https.http2] +features = "remote,https,http2" +release = true + +[standalone.remote.https.http2] +features = "standalone,remote,https,http2" +release = true + +[cookies.remote.https.http2] +features = "cookies,remote,https,http2" +release = true + +[standalone.cookies.remote.https.http2] +features = "standalone,cookies,remote,https,http2" +release = true + +[proxy.https.http2] +features = "proxy,https,http2" +release = true + +[standalone.proxy.https.http2] +features = "standalone,proxy,https,http2" +release = true + +[cookies.proxy.https.http2] +features = "cookies,proxy,https,http2" +release = true + +[standalone.cookies.proxy.https.http2] +features = "standalone,cookies,proxy,https,http2" +release = true + +[remote.proxy.https.http2] +features = "remote,proxy,https,http2" +release = true + +[standalone.remote.proxy.https.http2] +features = "standalone,remote,proxy,https,http2" +release = true + +[cookies.remote.proxy.https.http2] +features = "cookies,remote,proxy,https,http2" +release = true + +[standalone.cookies.remote.proxy.https.http2] +features = "standalone,cookies,remote,proxy,https,http2" +release = true + +[record.https.http2] +features = "record,https,http2" +release = true + +[standalone.record.https.http2] +features = "standalone,record,https,http2" +release = true + +[cookies.record.https.http2] +features = "cookies,record,https,http2" +release = true + +[standalone.cookies.record.https.http2] +features = "standalone,cookies,record,https,http2" +release = true + +[remote.record.https.http2] +features = "remote,record,https,http2" +release = true + +[standalone.remote.record.https.http2] +features = "standalone,remote,record,https,http2" +release = true + +[cookies.remote.record.https.http2] +features = "cookies,remote,record,https,http2" +release = true + +[standalone.cookies.remote.record.https.http2] +features = "standalone,cookies,remote,record,https,http2" +release = true + +[proxy.record.https.http2] +features = "proxy,record,https,http2" +release = true + +[standalone.proxy.record.https.http2] +features = "standalone,proxy,record,https,http2" +release = true + +[cookies.proxy.record.https.http2] +features = "cookies,proxy,record,https,http2" +release = true + +[standalone.cookies.proxy.record.https.http2] +features = "standalone,cookies,proxy,record,https,http2" +release = true + +[remote.proxy.record.https.http2] +features = "remote,proxy,record,https,http2" +release = true + +[standalone.remote.proxy.record.https.http2] +features = "standalone,remote,proxy,record,https,http2" +release = true + +[cookies.remote.proxy.record.https.http2] +features = "cookies,remote,proxy,record,https,http2" +release = true + +[standalone.cookies.remote.proxy.record.https.http2] +features = "standalone,cookies,remote,proxy,record,https,http2" +release = true + +[report] +coveralls = "coveralls_key" +out = ["Html", "Xml"] diff --git a/tarpaulin.toml b/tarpaulin.toml new file mode 100644 index 00000000..59a12c39 --- /dev/null +++ b/tarpaulin.toml @@ -0,0 +1,12 @@ + +[remote] +features = "remote,proxy,http2,record,cookies" +release = false + +[standalone] +features = "standalone,color,cookies,proxy,record,http2" +release = true + +[report] +coveralls = "coveralls_key" +out = ["Html", "Xml"] diff --git a/tests/examples/binary_body_tests.rs b/tests/examples/binary_body_tests.rs index bb0e640e..c1375b26 100644 --- a/tests/examples/binary_body_tests.rs +++ b/tests/examples/binary_body_tests.rs @@ -1,5 +1,4 @@ use httpmock::prelude::*; -use isahc::Body; use std::io::Read; #[test] @@ -15,16 +14,18 @@ fn binary_body_test() { }); // Act - let mut response = isahc::get(server.url("/hello")).unwrap(); + let mut response = reqwest::blocking::get(&server.url("/hello")).unwrap(); // Assert m.assert(); assert_eq!(response.status(), 200); - assert_eq!(body_to_vec(response.body_mut()), binary_content.to_vec()); + assert_eq!(body_to_vec(&mut response), binary_content.to_vec()); } -fn body_to_vec(body: &mut Body) -> Vec { +fn body_to_vec(response: &mut reqwest::blocking::Response) -> Vec { let mut buf: Vec = Vec::new(); - body.read_to_end(&mut buf).expect("Cannot read from body"); + response + .read_to_end(&mut buf) + .expect("Cannot read from body"); buf } diff --git a/tests/examples/cookie_tests.rs b/tests/examples/cookie_tests.rs index 5db64790..7e7c6828 100644 --- a/tests/examples/cookie_tests.rs +++ b/tests/examples/cookie_tests.rs @@ -1,5 +1,5 @@ use httpmock::prelude::*; -use isahc::{prelude::*, Request}; +use reqwest::blocking::Client; #[test] fn cookie_matching_test() { @@ -7,21 +7,21 @@ fn cookie_matching_test() { let server = MockServer::start(); let mock = server.mock(|when, then| { - when.method(GET) + when.method("GET") .path("/") .cookie_exists("SESSIONID") .cookie("SESSIONID", "298zf09hf012fh2"); then.status(200); }); - // Act: Send the request and deserialize the response to JSON - let response = Request::get(&format!("http://{}", server.address())) + // Act: Send the request with cookies + let client = Client::new(); + let response = client + .get(&format!("http://{}", server.address())) .header( "Cookie", "OTHERCOOKIE1=01234; SESSIONID=298zf09hf012fh2; OTHERCOOKIE2=56789; HttpOnly", ) - .body(()) - .unwrap() .send() .unwrap(); diff --git a/tests/examples/custom_request_matcher_tests.rs b/tests/examples/custom_request_matcher_tests.rs index 6a588ecf..d7742dc5 100644 --- a/tests/examples/custom_request_matcher_tests.rs +++ b/tests/examples/custom_request_matcher_tests.rs @@ -1,21 +1,19 @@ use httpmock::prelude::*; -use isahc::get; #[test] -// TODO: Implement custom matcher fn my_custom_request_matcher_test() { // Arrange let server = MockServer::start(); let mock = server.mock(|when, then| { - when.matches(|req| req.path.to_lowercase().ends_with("test")); - then.status(200); + when.is_true(|req| req.uri().path().ends_with("Test")); + then.status(201); }); - // Act: Send the HTTP request - let response = get(server.url("/thisIsMyTest")).unwrap(); + // Act: Send the HTTP request using reqwest + let response = reqwest::blocking::get(&server.url("/thisIsMyTest")).unwrap(); // Assert mock.assert(); - assert_eq!(response.status(), 200); + assert_eq!(response.status(), 201); } diff --git a/tests/examples/delay_tests.rs b/tests/examples/delay_tests.rs index 3edd54d5..3dc06521 100644 --- a/tests/examples/delay_tests.rs +++ b/tests/examples/delay_tests.rs @@ -1,5 +1,4 @@ use httpmock::prelude::*; -use isahc::get; use std::time::{Duration, SystemTime}; #[test] @@ -15,8 +14,8 @@ fn delay_test() { then.status(200).delay(delay); }); - // Act: Send the HTTP request - let response = get(server.url("/delay")).unwrap(); + // Act: Send the HTTP request using reqwest + let response = reqwest::blocking::get(&server.url("/delay")).unwrap(); // Assert mock.assert(); diff --git a/tests/examples/delete_mock_tests.rs b/tests/examples/delete_mock_tests.rs index 596c9f4a..a141e3d0 100644 --- a/tests/examples/delete_mock_tests.rs +++ b/tests/examples/delete_mock_tests.rs @@ -1,5 +1,4 @@ use httpmock::prelude::*; -use isahc::get; #[test] fn explicit_delete_mock_test() { @@ -7,12 +6,12 @@ fn explicit_delete_mock_test() { let server = MockServer::start(); let mut m = server.mock(|when, then| { - when.method(GET).path("/health"); + when.method("GET").path("/health"); then.status(205); }); - // Act: Send the HTTP request - let response = get(&format!( + // Act: Send the HTTP request using reqwest + let response = reqwest::blocking::get(&format!( "http://{}:{}/health", server.host(), server.port() @@ -26,8 +25,8 @@ fn explicit_delete_mock_test() { // Delete the mock and send the request again m.delete(); - let response = get(&format!("http://{}/health", server.address())).unwrap(); + let response = reqwest::blocking::get(&format!("http://{}/health", server.address())).unwrap(); - // Assert that the request failed, because the mock has been deleted + // Assert that the request failed because the mock has been deleted assert_eq!(response.status(), 404); } diff --git a/tests/examples/file_body_tests.rs b/tests/examples/file_body_tests.rs index 74473d58..dd26ced7 100644 --- a/tests/examples/file_body_tests.rs +++ b/tests/examples/file_body_tests.rs @@ -1,5 +1,4 @@ use httpmock::prelude::*; -use isahc::prelude::*; #[test] fn file_body_test() { @@ -12,7 +11,7 @@ fn file_body_test() { }); // Act - let mut response = isahc::get(server.url("/hello")).unwrap(); + let response = reqwest::blocking::get(&server.url("/hello")).unwrap(); // Assert m.assert(); diff --git a/tests/examples/forwarding_tests.rs b/tests/examples/forwarding_tests.rs new file mode 100644 index 00000000..59f1acba --- /dev/null +++ b/tests/examples/forwarding_tests.rs @@ -0,0 +1,70 @@ +use httpmock::prelude::*; +use reqwest::blocking::Client; + +// @example-start: forwarding +#[cfg(feature = "proxy")] +#[test] +fn forwarding_test() { + // We will create this mock server to simulate a real service (e.g., GitHub, AWS, etc.). + let target_server = MockServer::start(); + target_server.mock(|when, then| { + when.any_request(); + then.status(200).body("Hi from fake GitHub!"); + }); + + // Let's create our mock server for the test + let server = MockServer::start(); + + // We configure our server to forward the request to the target host instead of + // answering with a mocked response. The 'when' variable lets you configure + // rules under which forwarding should take place. + server.forward_to(target_server.base_url(), |rule| { + rule.filter(|when| { + when.any_request(); // We want all requests to be forwarded. + }); + }); + + // Now let's send an HTTP request to the mock server. The request will be forwarded + // to the target host, as we configured before. + let client = Client::new(); + + // Since the request was forwarded, we should see the target host's response. + let response = client.get(server.url("/get")).send().unwrap(); + assert_eq!(response.status().as_u16(), 200); + assert_eq!(response.text().unwrap(), "Hi from fake GitHub!"); +} +// @example-end + +// @example-start: forwarding-github +#[cfg(feature = "proxy")] +#[test] +fn forward_to_github_test() { + // Let's create our mock server for the test + let server = MockServer::start(); + + // We configure our server to forward the request to the target + // host instead of answering with a mocked response. The 'when' + // variable lets you configure rules under which forwarding + // should take place. + server.forward_to("https://api.github.com", |rule| { + rule.filter(|when| { + when.any_request(); // Ensure all requests are forwarded. + }); + }); + + // Now let's send an HTTP request to the mock server. The request + // will be forwarded to the GitHub API, as we configured before. + let client = Client::new(); + + let response = client + .get(server.url("/repos/torvalds/linux")) + // GitHub requires us to send a user agent header + .header("User-Agent", "httpmock-test") + .send() + .unwrap(); + + // Since the request was forwarded, we should see a GitHub API response. + assert_eq!(response.status().as_u16(), 200); + assert_eq!(true, response.text().unwrap().contains("\"private\":false")); +} +// @example-end diff --git a/tests/examples/getting_started_tests.rs b/tests/examples/getting_started_tests.rs index 3c19badf..a4c2e7d8 100644 --- a/tests/examples/getting_started_tests.rs +++ b/tests/examples/getting_started_tests.rs @@ -1,8 +1,7 @@ -use httpmock::prelude::*; -use isahc::{get, get_async}; - #[test] fn getting_started_test() { + use httpmock::prelude::*; + // Start a lightweight mock server. let server = MockServer::start(); @@ -17,7 +16,7 @@ fn getting_started_test() { }); // Send an HTTP request to the mock server. This simulates your code. - let response = get(server.url("/translate?word=hello")).unwrap(); + let response = reqwest::blocking::get(server.url("/translate?word=hello")).unwrap(); // Ensure the specified mock was called. hello_mock.assert(); @@ -26,27 +25,37 @@ fn getting_started_test() { assert_eq!(response.status(), 200); } -#[async_std::test] +#[tokio::test] async fn async_getting_started_test() { - // Start a local mock server for exclusive use by this test function. + use httpmock::prelude::*; + + // Start a lightweight mock server. let server = MockServer::start_async().await; - // Create a mock on the mock server. The mock will return HTTP status code 200 whenever - // the mock server receives a GET-request with path "/hello". // Create a mock on the server. - let hello_mock = server + let mock = server .mock_async(|when, then| { - when.method("GET").path("/hello"); - then.status(200); + when.method(GET) + .path("/translate") + .query_param("word", "hello"); + then.status(200) + .header("content-type", "text/html; charset=UTF-8") + .body("Привет"); }) .await; // Send an HTTP request to the mock server. This simulates your code. - let url = format!("http://{}/hello", server.address()); - let response = get_async(&url).await.unwrap(); + let client = reqwest::Client::new(); + let response = client + .get(server.url("/translate?word=hello")) + .send() + .await + .unwrap(); + + // Ensure the specified mock was called exactly one time (or fail with a + // detailed error description). + mock.assert(); - // Ensure the specified mock responded exactly one time. - hello_mock.assert_async().await; - // Ensure the mock server did respond as specified above. + // Ensure the mock server did respond as specified. assert_eq!(response.status(), 200); } diff --git a/tests/examples/headers_tests.rs b/tests/examples/headers_tests.rs index 77cc4974..3d95fe97 100644 --- a/tests/examples/headers_tests.rs +++ b/tests/examples/headers_tests.rs @@ -1,5 +1,5 @@ use httpmock::prelude::*; -use isahc::{prelude::*, Request}; +use reqwest::blocking::Client; #[test] fn headers_test() { @@ -13,11 +13,11 @@ fn headers_test() { then.status(201).header("Content-Length", "0"); }); - // Act: Send the request and deserialize the response to JSON - let response = Request::post(&format!("http://{}/test", server.address())) + // Act: Send the request using reqwest + let client = Client::new(); + let response = client + .post(&format!("http://{}/test", server.address())) .header("Authorization", "token 123456789") - .body(()) - .unwrap() .send() .unwrap(); @@ -34,3 +34,28 @@ fn headers_test() { "0" ); } + +#[test] +fn headers_test_2() { + // Arrange + let server = MockServer::start(); + + // Create a mock that expects at least 2 headers whose keys match the regex "^X-Custom-Header.*" + // and values match the regex "value.*" + let mock = server.mock(|when, then| { + when.header_count("^X-Custom-Header.*", "value.*", 2); + then.status(200); // Respond with a 200 status code if the condition is met + }); + + // Act: Make a request that includes the required headers using reqwest + let client = Client::new(); + client + .post(&format!("http://{}/test", server.address())) + .header("x-custom-header-1", "value1") + .header("X-Custom-Header-2", "value2") + .send() + .unwrap(); + + // Assert: Verify that the mock was called at least once + mock.assert(); +} diff --git a/tests/examples/https_tests.rs b/tests/examples/https_tests.rs new file mode 100644 index 00000000..9530331a --- /dev/null +++ b/tests/examples/https_tests.rs @@ -0,0 +1,62 @@ +#[cfg(feature = "https")] +#[tokio::test] +async fn test_http_get_request() { + use httpmock::MockServer; + + // Arrange + let server = MockServer::start_async().await; + + server + .mock_async(|when, then| { + when.any_request(); + then.header("X-Hello", "test").status(200); + }) + .await; + + let base_url = format!("https://{}", server.address()); + + let client = reqwest::Client::new(); + let res = client.get(&base_url).send().await.unwrap(); + + assert_eq!(res.status(), 200, "HTTP status should be 200 OK"); +} +#[cfg(feature = "https")] +#[cfg(feature = "remote")] +#[tokio::test] +async fn https_test_reqwest() { + use httpmock::MockServer; + use reqwest::{tls::Certificate, Client}; + use std::{fs::read, path::PathBuf}; + + // Arrange + let server = MockServer::connect_async("localhost:5050").await; + + server + .mock_async(|when, then| { + when.any_request(); + then.header("X-Hello", "test").status(200); + }) + .await; + + let base_url = format!("https://localhost:{}", server.address().port()); + + // Load the CA certificate from the project path + let project_dir = env!("CARGO_MANIFEST_DIR"); + let cert_path = PathBuf::from(project_dir).join("certs/ca.pem"); + let cert = Certificate::from_pem(&read(cert_path).unwrap()).unwrap(); + + // Build the client with the CA certificate + let client = Client::builder() + .add_root_certificate(cert) + .build() + .unwrap(); + + let res = client.get(&base_url).send().await.unwrap(); + + assert_eq!(res.status(), 200); + assert_eq!( + res.headers().get("X-Hello").unwrap().to_str().unwrap(), + "test" + ); + assert!(base_url.starts_with("https://")); +} diff --git a/tests/examples/json_body_tests.rs b/tests/examples/json_body_tests.rs index 278084a6..e81f637e 100644 --- a/tests/examples/json_body_tests.rs +++ b/tests/examples/json_body_tests.rs @@ -1,5 +1,5 @@ use httpmock::prelude::*; -use isahc::{prelude::*, Request}; +use reqwest::blocking::Client; use serde_json::{json, Value}; #[test] @@ -18,19 +18,21 @@ fn json_value_body_test() { }); // Act: Send the request and deserialize the response to JSON - let mut response = Request::post(&format!("http://{}/users", server.address())) + let client = Client::new(); + let response = client + .post(&format!("http://{}/users", server.address())) .header("content-type", "application/json") .body(json!({ "name": "Fred" }).to_string()) - .unwrap() .send() .unwrap(); + let status = response.status().as_u16(); let user: Value = serde_json::from_str(&response.text().unwrap()).expect("cannot deserialize JSON"); // Assert m.assert(); - assert_eq!(response.status(), 201); + assert_eq!(status, 201); assert_eq!(user.as_object().unwrap().get("name").unwrap(), "Hans"); } @@ -60,7 +62,9 @@ fn json_body_object_serde_test() { }); // Act: Send the request and deserialize the response to JSON - let mut response = Request::post(&format!("http://{}/users", server.address())) + let client = Client::new(); + let response = client + .post(&format!("http://{}/users", server.address())) .header("content-type", "application/json") .body( json!(&TestUser { @@ -68,16 +72,16 @@ fn json_body_object_serde_test() { }) .to_string(), ) - .unwrap() .send() .unwrap(); + let status = response.status().as_u16(); let user: TestUser = serde_json::from_str(&response.text().unwrap()).expect("cannot deserialize JSON"); // Assert m.assert(); - assert_eq!(response.status(), 201); + assert_eq!(status, 201); assert_eq!(user.name, "Hans"); } @@ -100,7 +104,7 @@ fn partial_json_body_test() { // Arranging the test by creating HTTP mocks. let m = server.mock(|when, then| { - when.method(POST).path("/users").json_body_partial( + when.method(POST).path("/users").json_body_includes( r#" { "child" : { @@ -113,8 +117,10 @@ fn partial_json_body_test() { }); // Simulates application that makes the request to the mock. + let client = Client::new(); let uri = format!("http://{}/users", m.server_address()); - let response = Request::post(&uri) + let response = client + .post(&uri) .header("content-type", "application/json") .header("User-Agent", "rust-test") .body( @@ -126,7 +132,6 @@ fn partial_json_body_test() { }) .unwrap(), ) - .unwrap() .send() .unwrap(); diff --git a/tests/examples/mod.rs b/tests/examples/mod.rs index fc42eb46..5a23fb1a 100644 --- a/tests/examples/mod.rs +++ b/tests/examples/mod.rs @@ -4,17 +4,18 @@ mod custom_request_matcher_tests; mod delay_tests; mod delete_mock_tests; mod file_body_tests; +mod forwarding_tests; mod getting_started_tests; mod headers_tests; +mod https_tests; mod json_body_tests; mod multi_server_tests; +mod proxy_tests; mod query_param_tests; +mod record_and_playback_tests; mod reset_tests; mod showcase_tests; +mod standalone_tests; mod string_body_tests; mod url_matching_tests; - -#[cfg(feature = "remote")] -mod standalone_tests; -#[cfg(feature = "remote")] mod x_www_form_urlencoded_tests; diff --git a/tests/examples/multi_server_tests.rs b/tests/examples/multi_server_tests.rs index 12def364..57a034a8 100644 --- a/tests/examples/multi_server_tests.rs +++ b/tests/examples/multi_server_tests.rs @@ -1,11 +1,8 @@ use httpmock::prelude::*; -use isahc::config::RedirectPolicy; -use isahc::prelude::*; -use isahc::HttpClientBuilder; +use reqwest::{blocking::Client, redirect::Policy}; #[test] fn multi_server_test() { - // Arrange let server1 = MockServer::start(); let server2 = MockServer::start(); @@ -21,15 +18,13 @@ fn multi_server_test() { then.status(200); }); - // Act: Send the HTTP request with an HTTP client that automatically follows redirects! - let http_client = HttpClientBuilder::new() - .redirect_policy(RedirectPolicy::Follow) + let client = Client::builder() + .redirect(Policy::limited(10)) .build() .unwrap(); - let response = http_client.get(server1.url("/redirectTest")).unwrap(); + let response = client.get(&server1.url("/redirectTest")).send().unwrap(); - // Assert redirect_mock.assert(); target_mock.assert(); assert_eq!(response.status(), 200); diff --git a/tests/examples/proxy_tests.rs b/tests/examples/proxy_tests.rs new file mode 100644 index 00000000..5656d9a2 --- /dev/null +++ b/tests/examples/proxy_tests.rs @@ -0,0 +1,46 @@ +use httpmock::prelude::*; +use reqwest::blocking::Client; + +#[cfg(feature = "proxy")] +#[test] +fn proxy_test() { + env_logger::try_init().unwrap(); + + // We will create this mock server to simulate a real service (e.g., GitHub, AWS, etc.). + let target_server = MockServer::start(); + target_server.mock(|when, then| { + when.any_request(); + then.status(200).body("Hi from fake GitHub!"); + }); + + // Let's create our mock server for the test + let proxy_server = MockServer::start(); + + // We configure our server to proxy the request to the target host instead of + // answering with a mocked response. The 'when' variable lets you configure + // rules under which requests are allowed to be proxied. If you do not restrict, + // any request will be proxied. + proxy_server.proxy(|rule| { + rule.filter(|when| { + // Here we only allow to proxy requests to our target server. + when.host(target_server.host()).port(target_server.port()); + }); + }); + + // The following will send a request to the mock server. The request will be forwarded + // to the target host, as we configured before. + let client = Client::builder() + .proxy(reqwest::Proxy::all(proxy_server.base_url()).unwrap()) // <<- Here we configure to use a proxy server + .build() + .unwrap(); + + // Since the request was forwarded, we should see the target host's response. + let response = client.get(target_server.url("/get")).send().unwrap(); + + // Extract the status code before calling .text() which consumes the response + let status_code = response.status().as_u16(); + let response_text = response.text().unwrap(); // Store the text response in a variable + + assert_eq!("Hi from fake GitHub!", response_text); // Use the stored text for comparison + assert_eq!(status_code, 200); // Now compare the status code +} diff --git a/tests/examples/query_param_tests.rs b/tests/examples/query_param_tests.rs index d7114edf..a8685a30 100644 --- a/tests/examples/query_param_tests.rs +++ b/tests/examples/query_param_tests.rs @@ -1,6 +1,4 @@ use httpmock::prelude::*; -use isahc::get as http_get; -use ureq::get as httpget; #[test] fn url_param_matching_test() { @@ -13,8 +11,8 @@ fn url_param_matching_test() { then.status(200); }); - // Act: Send the request and deserialize the response to JSON - http_get(server.url("/search?query=Metallica")).unwrap(); + // Act: Send the request using the fully qualified path + reqwest::blocking::get(&server.url("/search?query=Metallica")).unwrap(); // Assert m.assert(); @@ -31,8 +29,8 @@ fn url_param_urlencoded_matching_test() { then.status(200); }); - // Act: Send the request - http_get(server.url("/search?query=Mot%C3%B6rhead")).unwrap(); + // Act: Send the request using the fully qualified path + reqwest::blocking::get(&server.url("/search?query=Mot%C3%B6rhead")).unwrap(); // Assert m.assert(); @@ -49,10 +47,8 @@ fn url_param_unencoded_matching_test() { then.status(200); }); - // Act: Send the request - httpget(&server.url("/search?query=Motörhead")) - .send_string("") - .unwrap(); + // Act: Send the request using the fully qualified path + reqwest::blocking::get(&server.url("/search?query=Motörhead")).unwrap(); // Assert m.assert(); @@ -68,10 +64,8 @@ fn url_param_encoding_issue_56() { then.status(200); }); - // Act: Send the request - httpget(&server.url("/search?query=Metallica+is+cool")) - .send_string("") - .unwrap(); + // Act: Send the request using the fully qualified path + reqwest::blocking::get(&server.url("/search?query=Metallica+is+cool")).unwrap(); // Assert m.assert(); diff --git a/tests/examples/record_and_playback_tests.rs b/tests/examples/record_and_playback_tests.rs new file mode 100644 index 00000000..c024887f --- /dev/null +++ b/tests/examples/record_and_playback_tests.rs @@ -0,0 +1,206 @@ +use httpmock::prelude::*; +use reqwest::blocking::Client; + +#[cfg(feature = "record")] +#[test] +fn record_with_forwarding_test() { + let target_server = MockServer::start(); + target_server.mock(|when, then| { + when.any_request(); + then.status(200).body("Hi from fake GitHub!"); + }); + + let recording_server = MockServer::start(); + + recording_server.forward_to(target_server.base_url(), |rule| { + rule.filter(|when| { + when.path("/hello"); + }); + }); + + let recording = recording_server.record(|rule| { + rule.record_response_delays(true) + .record_request_headers(vec!["Accept", "Content-Type"]) + .filter(|when| { + when.path("/hello"); + }); + }); + + let github_client = Client::builder().build().unwrap(); + + let response = github_client + .get(format!("{}/hello", recording_server.base_url())) + .send() + .unwrap(); + assert_eq!(response.text().unwrap(), "Hi from fake GitHub!"); + + let target_path = recording.save("my_test_scenario").unwrap(); + + let playback_server = MockServer::start(); + + playback_server.playback(target_path); + + let response = github_client + .get(format!("{}/hello", playback_server.base_url())) + .send() + .unwrap(); + assert_eq!(response.text().unwrap(), "Hi from fake GitHub!"); +} + +// @example-start: record-proxy-github +#[cfg(all(feature = "proxy", feature = "experimental"))] +#[test] +fn record_with_proxy_test() { + // Start a mock server to act as a proxy for the HTTP client + let server = MockServer::start(); + + // Configure the mock server to proxy all incoming requests + server.proxy(|rule| { + rule.filter(|when| { + when.any_request(); // Intercept all requests + }); + }); + + // Set up recording on the mock server to capture all proxied + // requests and responses + let recording = server.record(|rule| { + rule.filter(|when| { + when.any_request(); // Record all requests + }); + }); + + // Create an HTTP client configured to route requests + // through the mock proxy server + let github_client = Client::builder() + // Set the proxy URL to the mock server's URL + .proxy(reqwest::Proxy::all(server.base_url()).unwrap()) + .build() + .unwrap(); + + // Send a GET request using the client, which will be proxied by the mock server + let response = github_client.get(server.base_url()).send().unwrap(); + + // Verify that the response matches the expected mock response + assert_eq!(response.text().unwrap(), "This is a mock response"); + + // Save the recorded HTTP interactions to a file for future reference or testing + recording + .save("my_scenario_name") + .expect("could not save the recording"); +} +// @example-end + +// @example-start: record-forwarding-github +#[cfg(feature = "record")] +#[test] +fn record_github_api_with_forwarding_test() { + // Let's create our mock server for the test + let server = MockServer::start(); + + // We configure our server to forward the request to the target + // host instead of answering with a mocked response. The 'when' + // variable lets you configure rules under which forwarding + // should take place. + server.forward_to("https://api.github.com", |rule| { + rule.filter(|when| { + when.any_request(); // Ensure all requests are forwarded. + }); + }); + + let recording = server.record(|rule| { + rule + // Specify which headers to record. + // Only the headers listed here will be captured and stored + // as part of the recorded mock. This selective recording is + // necessary because some headers may vary between requests + // and could cause issues when replaying the mock later. + // For instance, headers like 'Authorization' or 'Date' may + // change with each request. + .record_request_header("User-Agent") + .filter(|when| { + when.any_request(); // Ensure all requests are recorded. + }); + }); + + // Now let's send an HTTP request to the mock server. The request + // will be forwarded to the GitHub API, as we configured before. + let client = Client::new(); + + let response = client + .get(server.url("/repos/torvalds/linux")) + // GitHub requires us to send a user agent header + .header("User-Agent", "httpmock-test") + .send() + .unwrap(); + + // Since the request was forwarded, we should see a GitHub API response. + assert_eq!(response.status().as_u16(), 200); + assert_eq!(true, response.text().unwrap().contains("\"private\":false")); + + // Save the recording to + // "target/httpmock/recordings/github-torvalds-scenario_.yaml". + recording + .save("github-torvalds-scenario") + .expect("cannot store scenario on disk"); +} +// @example-end + +// @example-start: playback-forwarding-github +#[cfg(feature = "record")] +#[test] +fn playback_github_api() { + // Start a mock server for the test + let server = MockServer::start(); + + // Configure the mock server to forward requests to the target + // host (GitHub API) instead of responding with a mock. The 'rule' + // parameter allows you to define conditions under which forwarding + // should occur. + server.forward_to("https://api.github.com", |rule| { + rule.filter(|when| { + when.any_request(); // Forward all requests. + }); + }); + + // Set up recording to capture all forwarded requests and responses + let recording = server.record(|rule| { + rule.filter(|when| { + when.any_request(); // Record all requests and responses. + }); + }); + + // Send an HTTP request to the mock server, which will be forwarded + // to the GitHub API + let client = Client::new(); + let response = client + .get(server.url("/repos/torvalds/linux")) + // GitHub requires a User-Agent header + .header("User-Agent", "httpmock-test") + .send() + .unwrap(); + + // Assert that the response from the forwarded request is as expected + assert_eq!(response.status().as_u16(), 200); + assert!(response.text().unwrap().contains("\"private\":false")); + + // Save the recorded interactions to a file + let target_path = recording + .save("github-torvalds-scenario") + .expect("Failed to save the recording to disk"); + + // Start a new mock server instance for playback + let playback_server = MockServer::start(); + + // Load the recorded interactions into the new mock server + playback_server.playback(target_path); + + // Send a request to the playback server and verify the response + // matches the recorded data + let response = client + .get(playback_server.url("/repos/torvalds/linux")) + .send() + .unwrap(); + assert_eq!(response.status().as_u16(), 200); + assert!(response.text().unwrap().contains("\"private\":false")); +} +// @example-end diff --git a/tests/examples/reset_tests.rs b/tests/examples/reset_tests.rs index 90a82f8f..d4692a52 100644 --- a/tests/examples/reset_tests.rs +++ b/tests/examples/reset_tests.rs @@ -1,12 +1,9 @@ use httpmock::prelude::*; -use isahc::get; -#[async_std::test] +#[tokio::test] async fn reset_server_test() { - // Start a lightweight mock server. let server = MockServer::start(); - // Create a mock on the server that will be reset later server.mock(|when, then| { when.method("GET") .path("/translate") @@ -16,10 +13,8 @@ async fn reset_server_test() { .body("Привет"); }); - // Delete all previously created mocks server.reset_async().await; - // Create a new mock that will replace the previous one let hello_mock = server.mock(|when, then| { when.method("GET") .path("/translate") @@ -29,12 +24,10 @@ async fn reset_server_test() { .body("Привет"); }); - // Send an HTTP request to the mock server. This simulates your code. - let response = get(server.url("/translate?word=hello")).unwrap(); + let response = reqwest::get(&server.url("/translate?word=hello")) + .await + .unwrap(); - // Ensure the specified mock was called. hello_mock.assert(); - - // Ensure the mock server did respond as specified. assert_eq!(response.status(), 200); } diff --git a/tests/examples/showcase_tests.rs b/tests/examples/showcase_tests.rs index 111b50fc..9202274e 100644 --- a/tests/examples/showcase_tests.rs +++ b/tests/examples/showcase_tests.rs @@ -1,49 +1,47 @@ use httpmock::prelude::*; -use isahc::{prelude::*, Request}; +use regex::Regex; +use reqwest::blocking::Client; use serde_json::json; #[test] fn showcase_test() { - // This is a temporary type that we will use for this test #[derive(serde::Serialize, serde::Deserialize)] struct TransferItem { number: usize, } - // Arrange let server = MockServer::start(); let m = server.mock(|when, then| { when.method(POST) .path("/test") - .path_contains("test") + .path_includes("test") .query_param("myQueryParam", "überschall") .query_param_exists("myQueryParam") .path_matches(Regex::new(r#"test"#).unwrap()) .header("content-type", "application/json") .header_exists("User-Agent") .body("{\"number\":5}") - .body_contains("number") + .body_includes("number") .body_matches(Regex::new(r#"(\d+)"#).unwrap()) .json_body(json!({ "number": 5 })) - .matches(|req: &HttpMockRequest| req.path.contains("es")); + .is_true(|req: &HttpMockRequest| req.uri().path().contains("es")); then.status(200); }); - // Act: Send the HTTP request let uri = format!( "http://{}/test?myQueryParam=%C3%BCberschall", server.address() ); - let response = Request::post(&uri) + let client = Client::new(); + let response = client + .post(&uri) .header("content-type", "application/json") .header("User-Agent", "rust-test") .body(serde_json::to_string(&TransferItem { number: 5 }).unwrap()) - .unwrap() .send() .unwrap(); - // Assert m.assert(); assert_eq!(response.status(), 200); } diff --git a/tests/examples/standalone_tests.rs b/tests/examples/standalone_tests.rs index c986680d..8ce093f1 100644 --- a/tests/examples/standalone_tests.rs +++ b/tests/examples/standalone_tests.rs @@ -1,18 +1,17 @@ -use httpmock::prelude::*; -use isahc::{get_async, Body, Request, RequestExt}; -use std::io::Read; - -use crate::simulate_standalone_server; - #[test] +#[cfg(feature = "remote")] fn standalone_test() { + use crate::with_standalone_server; + use httpmock::MockServer; + use reqwest::blocking::Client; + // Arrange - // This starts up a standalone server in the background running on port 5000 - simulate_standalone_server(); + // This starts up a standalone server in the background running on port 5050 + with_standalone_server(); // Instead of creating a new MockServer using new(), we connect to an existing remote instance. - let server = MockServer::connect("localhost:5000"); + let server = MockServer::connect("localhost:5050"); let search_mock = server.mock(|when, then| { when.path("/search").body("wow so large".repeat(1000000)); @@ -20,9 +19,10 @@ fn standalone_test() { }); // Act: Send the HTTP request - let response = Request::post(server.url("/search")) + let client = Client::new(); + let response = client + .post(&server.url("/search")) .body("wow so large".repeat(1000000)) - .unwrap() .send() .unwrap(); @@ -31,59 +31,73 @@ fn standalone_test() { assert_eq!(response.status(), 202); } -#[async_std::test] +#[cfg(feature = "remote")] +#[tokio::test] async fn async_standalone_test() { + use crate::with_standalone_server; + use httpmock::MockServer; + use reqwest::Client; + // Arrange - // This starts up a standalone server in the background running on port 5000 - simulate_standalone_server(); + // This starts up a standalone server in the background running on port 5050 + with_standalone_server(); // Instead of creating a new MockServer using connect_from_env_async(), we connect by // reading the host and port from the environment (HTTPMOCK_HOST / HTTPMOCK_PORT) or - // falling back to defaults (localhost on port 5000) + // falling back to defaults (localhost on port 5050) let server = MockServer::connect_from_env_async().await; - let mut search_mock = server + let search_mock = server .mock_async(|when, then| { - when.path_contains("/search") + when.path_includes("/search") .query_param("query", "metallica"); then.status(202); }) .await; // Act: Send the HTTP request - let response = get_async(&format!( - "http://{}/search?query=metallica", - server.address() - )) - .await - .unwrap(); + let client = Client::new(); + let response = client + .get(&format!( + "http://{}/search?query=metallica", + server.address() + )) + .send() + .await + .unwrap(); // Assert 1 assert_eq!(response.status(), 202); - assert_eq!(search_mock.hits_async().await, 1); + assert_eq!(search_mock.calls_async().await, 1); // Act 2: Delete the mock and send a request to show that it is not present on the server anymore - search_mock.delete(); - let response = get_async(&format!( - "http://{}:{}/search?query=metallica", - server.host(), - server.port() - )) - .await - .unwrap(); + search_mock.delete_async().await; + let response = client + .get(&format!( + "http://{}:{}/search?query=metallica", + server.host(), + server.port() + )) + .send() + .await + .unwrap(); // Assert: The mock was not found assert_eq!(response.status(), 404); } +#[cfg(feature = "remote")] #[test] #[should_panic] fn unsupported_features() { + use crate::with_standalone_server; + use httpmock::MockServer; + // Arrange - // This starts up a standalone server in the background running on port 5000 - simulate_standalone_server(); + // This starts up a standalone server in the background running on port 5050 + with_standalone_server(); // Instead of creating a new MockServer using connect_from_env(), we connect by reading the // host and port from the environment (HTTPMOCK_HOST / HTTPMOCK_PORT) or falling back to defaults @@ -92,16 +106,21 @@ fn unsupported_features() { // Creating this mock will panic because expect_match is not supported when using // a remote mock server. let _ = server.mock(|when, _then| { - when.matches(|_| true); + when.is_true(|_| true); }); } +#[cfg(feature = "remote")] #[test] fn binary_body_standalone_test() { + use crate::with_standalone_server; + use httpmock::MockServer; + use reqwest::blocking::get; + // Arrange - // This starts up a standalone server in the background running on port 5000 - simulate_standalone_server(); + // This starts up a standalone server in the background running on port 5050 + with_standalone_server(); let binary_content = b"\x80\x02\x03\xF0\x90\x80"; @@ -112,16 +131,14 @@ fn binary_body_standalone_test() { }); // Act - let mut response = isahc::get(server.url("/hello")).unwrap(); + let mut response = get(&server.url("/hello")).unwrap(); // Assert m.assert(); assert_eq!(response.status(), 200); - assert_eq!(body_to_vec(response.body_mut()), binary_content.to_vec()); -} -fn body_to_vec(body: &mut Body) -> Vec { let mut buf: Vec = Vec::new(); - body.read_to_end(&mut buf).expect("Cannot read from body"); - buf + response.copy_to(&mut buf).expect("Cannot read from body"); + + assert_eq!(buf, binary_content.to_vec()); } diff --git a/tests/examples/string_body_tests.rs b/tests/examples/string_body_tests.rs index 52ca0f11..6ca75181 100644 --- a/tests/examples/string_body_tests.rs +++ b/tests/examples/string_body_tests.rs @@ -1,5 +1,6 @@ use httpmock::prelude::*; -use isahc::{prelude::*, Request}; +use regex::Regex; +use reqwest::blocking::Client; #[test] fn body_test() { @@ -10,15 +11,16 @@ fn body_test() { when.method(POST) .path("/books") .body("The Fellowship of the Ring") - .body_contains("Ring") + .body_includes("Ring") .body_matches(Regex::new("Fellowship").unwrap()); then.status(201).body("The Lord of the Rings"); }); - // Act: Send the request and deserialize the response to JSON - let response = Request::post(&format!("http://{}/books", server.address())) + // Act: Send the request + let client = Client::new(); + let response = client + .post(&format!("http://{}/books", server.address())) .body("The Fellowship of the Ring") - .unwrap() .send() .unwrap(); diff --git a/tests/examples/url_matching_tests.rs b/tests/examples/url_matching_tests.rs index 22d082f1..f955952a 100644 --- a/tests/examples/url_matching_tests.rs +++ b/tests/examples/url_matching_tests.rs @@ -1,5 +1,5 @@ use httpmock::prelude::*; -use isahc::get; +use regex::Regex; #[test] fn url_matching_test() { @@ -8,13 +8,13 @@ fn url_matching_test() { let m = server.mock(|when, then| { when.path("/appointments/20200922") - .path_contains("appointments") - .path_matches(Regex::new(r"\d{4}\d{2}\d{2}$").unwrap()); + .path_includes("appointments") + .path_matches(Regex::new(r"\d{8}$").unwrap()); then.status(201); }); - // Act: Send the request and deserialize the response to JSON - get(server.url("/appointments/20200922")).unwrap(); + // Act: Send the request + reqwest::blocking::get(&server.url("/appointments/20200922")).unwrap(); // Assert m.assert(); diff --git a/tests/examples/x_www_form_urlencoded_tests.rs b/tests/examples/x_www_form_urlencoded_tests.rs index 3588d452..76b36f82 100644 --- a/tests/examples/x_www_form_urlencoded_tests.rs +++ b/tests/examples/x_www_form_urlencoded_tests.rs @@ -1,26 +1,27 @@ use httpmock::prelude::*; -use isahc::{prelude::*, Request}; +use reqwest::blocking::Client; #[test] -fn body_test() { +fn body_test_xxx_form_url_encoded() { // Arrange - let server = MockServer::connect("127.0.0.1:5000"); + let server = MockServer::start(); let m = server.mock(|when, then| { when.method(POST) .path("/example") .header("content-type", "application/x-www-form-urlencoded") - .x_www_form_urlencoded_tuple("name", "Peter Griffin") - .x_www_form_urlencoded_tuple("town", "Quahog") - .x_www_form_urlencoded_key_exists("name") - .x_www_form_urlencoded_key_exists("town"); + .form_urlencoded_tuple("name", "Peter Griffin") + .form_urlencoded_tuple("town", "Quahog") + .form_urlencoded_tuple_exists("name") + .form_urlencoded_tuple_exists("town"); then.status(202); }); - let response = Request::post(server.url("/example")) + let client = Client::new(); + let response = client + .post(&server.url("/example")) .header("content-type", "application/x-www-form-urlencoded") .body("name=Peter%20Griffin&town=Quahog") - .unwrap() .send() .unwrap(); diff --git a/tests/internal/runtimes_test.rs b/tests/internal/runtimes_test.rs deleted file mode 100644 index 711c902c..00000000 --- a/tests/internal/runtimes_test.rs +++ /dev/null @@ -1,42 +0,0 @@ -use httpmock::prelude::*; -use isahc::get_async; - -#[test] -fn all_runtimes_test() { - // Tokio - assert_eq!( - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(test_fn()), - 202 - ); - - // actix - assert_eq!(actix_rt::Runtime::new().unwrap().block_on(test_fn()), 202); - - // async_std - assert_eq!(async_std::task::block_on(test_fn()), 202); -} - -async fn test_fn() -> u16 { - // Instead of creating a new MockServer using new(), we connect to an existing remote instance. - let server = MockServer::start_async().await; - - let search_mock = server - .mock_async(|when, then| { - when.path("/test"); - then.status(202); - }) - .await; - - // Act: Send the HTTP request - let response = get_async(server.url("/test")).await.unwrap(); - - // Assert - search_mock.assert_async().await; - assert_eq!(response.status(), 202); - - response.status().as_u16() -} diff --git a/tests/lib.rs b/tests/lib.rs index ba58a6ad..a531c9f6 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,31 +1,39 @@ -#[macro_use] extern crate lazy_static; -use std::sync::Mutex; -use std::thread::{spawn, JoinHandle}; - -use httpmock::standalone::start_standalone_server; +use httpmock::server::{HttpMockServer, HttpMockServerBuilder}; +use std::{sync::Mutex, thread}; use tokio::task::LocalSet; - mod examples; -mod internal; +mod matchers; +mod misc; -/// ==================================================================================== /// The rest of this file is only required to simulate that a standalone mock server is -/// running somewhere else. The tests above will is. -/// ==================================================================================== -pub fn simulate_standalone_server() { - let _unused = STANDALONE_SERVER.lock().unwrap_or_else(|e| e.into_inner()); -} +/// running somewhere else. +pub fn with_standalone_server() { + let disable_server = std::env::var("HTTPMOCK_TESTS_DISABLE_SIMULATED_STANDALONE_SERVER") + .unwrap_or_else(|_| "0".to_string()); -lazy_static! { - static ref STANDALONE_SERVER: Mutex>> = Mutex::new(spawn(|| { - let srv = - start_standalone_server(5000, false, None, false, usize::MAX, std::future::pending()); - let mut runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap(); - LocalSet::new().block_on(&mut runtime, srv) - })); + if disable_server == "1" { + log::info!("Skipping creating a simulated mock server."); + return; + } + + let mut started = SERVER_STARTED.lock().unwrap(); + if !*started { + thread::spawn(move || { + let srv: HttpMockServer = HttpMockServerBuilder::new() + .port(5050) + .build() + .expect("cannot create mock server"); + + let mut runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + LocalSet::new().block_on(&mut runtime, srv.start()) + }); + } + *started = true } + +static SERVER_STARTED: Mutex = Mutex::new(false); diff --git a/tests/matchers/body.rs b/tests/matchers/body.rs new file mode 100644 index 00000000..176fd929 --- /dev/null +++ b/tests/matchers/body.rs @@ -0,0 +1,419 @@ +use crate::matchers::{expect_fails_with2, SingleValueMatcherDataSet}; +use httpmock::{MockServer, When}; + +#[test] +fn body() { + for (idx, data) in generate_data().attribute.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.body(data.expect), + data.actual, + data.failure_msg.clone(), + ) + } +} + +#[test] +fn body_fail_message() { + run_test( + "fail message format", + |when| when.body("test"), + "not-test", + Some(vec![ + "Expected body equals:", + "test", + "", + "Received:", + "not-test", + "", + "Diff:", + "---| test", + "+++| not-test", + ]), + ) +} + +#[test] +fn body_not() { + for (idx, data) in generate_data().attribute_not.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.body_not(data.expect), + data.actual, + data.failure_msg.clone(), + ) + } +} + +#[test] +fn body_not_fail_message() { + run_test( + "fail message format", + |when| when.body_not("test"), + "test", + Some(vec![ + "Expected body not equal to:", + "test", + "", + "Received:", + "test", + "", + "Diff:", + " | test", + "", + "Matcher: body_not", + ]), + ) +} + +#[test] +fn body_includes() { + for (idx, data) in generate_data().attribute_includes.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.body_includes(data.expect), + data.actual, + data.failure_msg.clone(), + ) + } +} + +#[test] +fn body_includes_fail_message() { + run_test( + "fail message format", + |when| when.body_includes("x"), + "a\n. \n test \n abc \nline", + Some(vec![ + "Expected body includes:", + "x", + "", + "Received:", + "a", + ".", + " test", + " abc", + "line", + "", + "Diff:", + "---| x", + "+++| a", + "+++| .", + "+++| test", + "+++| abc", + "+++| line", + ]), + ) +} + +#[test] +fn body_includes_multiline() { + let expect = "\"onclick\": \"CreateDoc()\",\n \"value\": \"New\""; + let actual = r#" +{ + "menu": { + "id": "file", + "popup": { + "menuitem": [ + { + "onclick": "CreateDoc()", + "value": "New" + }, + { + "onclick": "OpenDoc()", + "value": "Open" + }, + { + "onclick": "SaveDoc()", + "value": "Save" + } + ] + }, + "value": "File" + } +} +"#; + + run_test( + "multi-line body", + |when| when.body_includes(expect), + actual, + None, + ); +} + +#[test] +fn body_excludes() { + for (idx, data) in generate_data().attribute_excludes.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.body_excludes(data.expect), + data.actual, + data.failure_msg.clone(), + ) + } +} + +#[test] +fn body_excludes_fail_message() { + run_test( + "fail message format", + |when| when.body_excludes("test"), + "a\n. \n test \n abc \nline", + Some(vec![ + "Expected body excludes:", + "test", + "", + "Received:", + "a", + ".", + " test", + " abc", + "line", + "", + "Diff:", + "---| test", + "+++| a", + "+++| .", + "+++| test", + "+++| abc", + "+++| line", + ]), + ) +} + +#[test] +fn body_prefix() { + for (idx, data) in generate_data().attribute_prefix.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.body_prefix(data.expect), + data.actual, + data.failure_msg.clone(), + ) + } +} + +#[test] +fn body_prefix_fail_message() { + run_test( + "fail message format", + |when| when.body_prefix("test"), + "a test", + Some(vec![ + "Expected body has prefix:", + "test", + "", + "Received:", + "a test", + "", + "Diff:", + "---| test", + "+++| a test", + "", + "Matcher: body_prefix", + ]), + ) +} + +#[test] +fn body_prefix_not() { + for (idx, data) in generate_data().attribute_prefix_not.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.body_prefix_not(data.expect), + data.actual, + data.failure_msg.clone(), + ) + } +} + +#[test] +fn body_prefix_not_fail_message() { + run_test( + "fail message format", + |when| when.body_prefix_not("test"), + "test it is", + Some(vec![ + "Expected body prefix not:", + "test", + "", + "Received:", + "test it is", + "", + "Diff:", + "---| test", + "+++| test it is", + "", + "Matcher: body_prefix_not", + ]), + ) +} + +#[test] +fn body_suffix() { + for (idx, data) in generate_data().attribute_suffix.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.body_suffix(data.expect), + data.actual, + data.failure_msg.clone(), + ) + } +} + +#[test] +fn body_suffix_fail_message() { + run_test( + "fail message format", + |when| when.body_suffix("test"), + "it is test not", + Some(vec![ + "Expected body has suffix:", + "test", + "", + "Received:", + "it is test not", + "", + "Diff:", + "---| test", + "+++| it is test not", + ]), + ) +} + +#[test] +fn body_suffix_not() { + for (idx, data) in generate_data().attribute_suffix_not.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.body_suffix_not(data.expect), + data.actual, + data.failure_msg.clone(), + ) + } +} + +#[test] +fn body_suffix_not_fail_message() { + run_test( + "fail message format", + |when| when.body_suffix_not("test"), + "it is test", + Some(vec![ + "Expected body suffix not:", + "test", + "", + "Received:", + "it is test", + "", + "Diff:", + "---| test", + "+++| it is test", + ]), + ) +} + +#[test] +fn body_matches() { + for (idx, data) in generate_data().attribute_matches.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.body_matches(data.expect), + data.actual, + data.failure_msg.clone(), + ) + } +} + +#[test] +fn body_matches_fail_message() { + run_test( + "fail message format", + |when| when.body_matches("def"), + "abcghijklmn", + Some(vec![ + "Expected body matches regex:", + "def", + "", + "Received:", + "abcghijklmn", + "", + "Diff:", + "---| def", + "+++| abcghijklmn", + "", + "Matcher: body_matches", + ]), + ) +} + +fn generate_data() -> SingleValueMatcherDataSet<&'static str, &'static str> { + SingleValueMatcherDataSet::generate("body", "Body Mismatch", true) +} + +fn run_test( + name: S, + set_expectation: F, + actual: &'static str, + error_msg: Option>, +) where + F: Fn(When) -> When + std::panic::UnwindSafe + std::panic::RefUnwindSafe, + S: Into, +{ + println!("{}", name.into()); + + let run = || { + // Arrange + let server = MockServer::start(); + + let m = server.mock(|when, then| { + set_expectation(when); + then.status(200); + }); + + // Act + let response = reqwest::blocking::Client::new() + .get(server.url("/test")) + .body(actual) + .send() + .unwrap(); + + // Assert + m.assert(); + assert_eq!(response.status(), 200); + }; + + if let Some(err_msg) = error_msg { + expect_fails_with2(err_msg, run); + } else { + run(); + } +} diff --git a/tests/matchers/cookies.rs b/tests/matchers/cookies.rs new file mode 100644 index 00000000..ae1f4fc6 --- /dev/null +++ b/tests/matchers/cookies.rs @@ -0,0 +1,252 @@ +use crate::matchers::{expect_fails_with2, MultiValueMatcherTestSet}; +use http::{HeaderMap, HeaderValue}; +use httpmock::{MockServer, When}; + +#[test] +#[cfg(feature = "cookies")] +fn cookie() { + for (idx, data) in generate_data().attribute.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.cookie(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +#[cfg(feature = "cookies")] +fn cookie_not() { + for (idx, data) in generate_data().attribute_not.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.cookie_not(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +#[cfg(feature = "cookies")] +fn cookie_exists() { + for (idx, data) in generate_data().attribute_exists.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.cookie_exists(data.expect), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +#[cfg(feature = "cookies")] +fn cookie_missing() { + for (idx, data) in generate_data().attribute_missing.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.cookie_missing(data.expect), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +#[cfg(feature = "cookies")] +fn cookie_includes() { + for (idx, data) in generate_data().attribute_includes.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.cookie_includes(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +#[cfg(feature = "cookies")] +fn cookie_excludes() { + for (idx, data) in generate_data().attribute_excludes.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.cookie_excludes(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +#[cfg(feature = "cookies")] +fn cookie_prefix() { + for (idx, data) in generate_data().attribute_prefix.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.cookie_prefix(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +#[cfg(feature = "cookies")] +fn cookie_suffix() { + for (idx, data) in generate_data().attribute_suffix.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.cookie_suffix(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +#[cfg(feature = "cookies")] +fn cookie_prefix_not() { + for (idx, data) in generate_data().attribute_prefix_not.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.cookie_prefix_not(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +#[cfg(feature = "cookies")] +fn cookie_suffix_not() { + for (idx, data) in generate_data().attribute_suffix_not.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.cookie_suffix_not(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +#[cfg(feature = "cookies")] +fn cookie_matches() { + for (idx, data) in generate_data().attribute_matches.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.cookie_matches(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +#[cfg(feature = "cookies")] +fn cookie_count() { + for (idx, data) in generate_data().attribute_count.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.cookie_count(data.expect.0, data.expect.1, data.expect.2), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +fn generate_data() -> MultiValueMatcherTestSet<&'static str, &'static str, usize, &'static str> { + MultiValueMatcherTestSet::generate("cookie", "Cookie Mismatch", false) +} + +fn run_test( + name: S, + set_expectation: F, + actual: Vec<(&'static str, &'static str)>, + error_msg: Option>, +) where + F: Fn(When) -> When + std::panic::UnwindSafe + std::panic::RefUnwindSafe, + S: Into, +{ + println!("{}", name.into()); + + let run = || { + // Arrange + let server = MockServer::start(); + + let m = server.mock(|when, then| { + set_expectation(when); + then.status(200); + }); + + // Act + let mut content = Vec::new(); + for (key, value) in actual { + if value.contains(' ') || value.contains(';') || value.contains(',') { + content.push(format!("{}={}", key, value.replace('"', "\\\""))) + } else { + content.push(format!("{}={}", key, value)) + } + } + + let value = content.join(";"); + + let mut headers = HeaderMap::new(); + headers.insert("cookie", HeaderValue::from_str(&value).unwrap()); + + let response = reqwest::blocking::Client::new() + .get(server.url("/test")) + .headers(headers) + .send() + .unwrap(); + + // Assert + m.assert(); + assert_eq!(response.status(), 200); + }; + + if let Some(err_msg) = error_msg { + expect_fails_with2(err_msg, run); + } else { + run(); + } +} diff --git a/tests/matchers/headers.rs b/tests/matchers/headers.rs new file mode 100644 index 00000000..412817ac --- /dev/null +++ b/tests/matchers/headers.rs @@ -0,0 +1,229 @@ +use crate::matchers::{expect_fails_with2, MultiValueMatcherTestSet}; +use httpmock::{MockServer, When}; + +#[test] +fn header() { + for (idx, data) in generate_data().attribute.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.header(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn header_not() { + for (idx, data) in generate_data().attribute_not.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.header_not(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn header_exists() { + for (idx, data) in generate_data().attribute_exists.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.header_exists(data.expect), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn header_missing() { + for (idx, data) in generate_data().attribute_missing.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.header_missing(data.expect), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn header_includes() { + for (idx, data) in generate_data().attribute_includes.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.header_includes(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn header_excludes() { + for (idx, data) in generate_data().attribute_excludes.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.header_excludes(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn header_prefix() { + for (idx, data) in generate_data().attribute_prefix.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.header_prefix(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn header_suffix() { + for (idx, data) in generate_data().attribute_suffix.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.header_suffix(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn header_prefix_not() { + for (idx, data) in generate_data().attribute_prefix_not.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.header_prefix_not(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn header_suffix_not() { + for (idx, data) in generate_data().attribute_suffix_not.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.header_suffix_not(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn header_matches() { + for (idx, data) in generate_data().attribute_matches.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.header_matches(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn header_count() { + for (idx, data) in generate_data().attribute_count.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.header_count(data.expect.0, data.expect.1, data.expect.2), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +fn generate_data() -> MultiValueMatcherTestSet<&'static str, &'static str, usize, &'static str> { + MultiValueMatcherTestSet::generate("header", "Header Mismatch", false) +} + +fn run_test( + name: S, + set_expectation: F, + actual: Vec<(&'static str, &'static str)>, + error_msg: Option>, +) where + F: Fn(When) -> When + std::panic::UnwindSafe + std::panic::RefUnwindSafe, + S: Into, +{ + println!("{}", name.into()); + + let run = || { + // Arrange + let server = MockServer::start(); + + let m = server.mock(|when, then| { + set_expectation(when); + then.status(200); + }); + + // Act + // Build the request with custom headers + let client = reqwest::blocking::Client::new(); + let mut request_builder = client.get(&server.url("/test")); + + for (key, value) in actual { + request_builder = request_builder.header(key, value); + } + + let response = request_builder.send().unwrap(); + + // Assert + m.assert(); + assert_eq!(response.status(), 200); + }; + + if let Some(err_msg) = error_msg { + expect_fails_with2(err_msg, run); + } else { + run(); + } +} diff --git a/tests/matchers/host.rs b/tests/matchers/host.rs new file mode 100644 index 00000000..cd22ee34 --- /dev/null +++ b/tests/matchers/host.rs @@ -0,0 +1,147 @@ +use crate::matchers::expect_fails_with2; +use httpmock::{MockServer, When}; + +#[cfg(feature = "proxy")] +#[test] +fn path_success_table_test() { + struct TestData { + expectation: fn(when: When) -> When, + } + + let tests = vec![ + TestData { + expectation: |when| when.host("127.0.0.1"), + }, + TestData { + expectation: |when| when.host("localhost"), + }, + TestData { + expectation: |when| when.host("LOCALHOST"), + }, + TestData { + expectation: |when| when.host_not("127.0.0.2"), + }, + TestData { + expectation: |when| when.host_includes("7.0.0"), + }, + TestData { + expectation: |when| when.host_excludes("28.0.0"), + }, + TestData { + expectation: |when| when.host_prefix("127"), + }, + TestData { + expectation: |when| when.host_prefix_not("128"), + }, + TestData { + expectation: |when| when.host_suffix(".0.1"), + }, + TestData { + expectation: |when| when.host_suffix_not("0.0.2"), + }, + TestData { + expectation: |when| when.host_matches(".*27.*"), + }, + ]; + + for (idx, test_data) in tests.iter().enumerate() { + println!("Running test case with index '{}'", idx); + + let target_server = MockServer::start(); + target_server.mock(|when, then| { + when.any_request(); + then.status(200); + }); + + let proxy_server = MockServer::start(); + + proxy_server.proxy(|rule| { + rule.filter(|when| { + (test_data.expectation)(when).port(target_server.port()); + }); + }); + + let client = reqwest::blocking::Client::builder() + .proxy(reqwest::Proxy::all(proxy_server.base_url()).unwrap()) + .build() + .unwrap(); + + let response = client.get(&target_server.url("/get")).send().unwrap(); + assert_eq!(response.status(), 200); + } +} + +#[cfg(feature = "proxy")] +#[test] +fn path_failure_table_test() { + pub struct TestData { + expectation: fn(when: When) -> When, + failure_message: Vec<&'static str>, + } + + let tests = vec![ + TestData { + expectation: |when| when.host("127.0.0.2"), + failure_message: vec!["No request has been received by the mock server"], + }, + TestData { + expectation: |when| when.host_not("127.0.0.1"), + failure_message: vec!["No request has been received by the mock server"], + }, + TestData { + expectation: |when| when.host_includes("192"), + failure_message: vec!["No request has been received by the mock server"], + }, + TestData { + expectation: |when| when.host_excludes("127"), + failure_message: vec!["No request has been received by the mock server"], + }, + TestData { + expectation: |when| when.host_prefix("192"), + failure_message: vec!["No request has been received by the mock server"], + }, + TestData { + expectation: |when| when.host_prefix_not("127"), + failure_message: vec!["No request has been received by the mock server"], + }, + TestData { + expectation: |when| when.host_suffix("2"), + failure_message: vec!["No request has been received by the mock server"], + }, + TestData { + expectation: |when| when.host_suffix_not("1"), + failure_message: vec!["No request has been received by the mock server"], + }, + ]; + + for (idx, test_data) in tests.iter().enumerate() { + println!("Running test case with index '{}'", idx); + + let err_msg = test_data.failure_message.clone(); + + expect_fails_with2(err_msg, || { + let target_server = MockServer::start(); + let m = target_server.mock(|when, then| { + when.any_request(); + then.status(200); + }); + + let proxy_server = MockServer::start(); + proxy_server.proxy(|rule| { + rule.filter(|when| { + (test_data.expectation)(when).port(target_server.port()); + }); + }); + + let client = reqwest::blocking::Client::builder() + .proxy(reqwest::Proxy::all(proxy_server.base_url()).unwrap()) + .build() + .unwrap(); + + let response = client.get(&target_server.url("/get")).send().unwrap(); + assert_eq!(404, response.status()); + + m.assert(); + }); + } +} diff --git a/tests/matchers/method.rs b/tests/matchers/method.rs new file mode 100644 index 00000000..40827598 --- /dev/null +++ b/tests/matchers/method.rs @@ -0,0 +1,96 @@ +use crate::matchers::expect_fails_with; +use httpmock::{ + Method::{GET, POST}, + MockServer, +}; +use reqwest::blocking::get; + +#[test] +fn success_method() { + // Arrange + let server = MockServer::start(); + + let m = server.mock(|when, then| { + when.method(GET); + then.status(200); + }); + + // Act + let response = get(&server.base_url()).unwrap(); + + // Assert + m.assert(); + assert_eq!(response.status(), 200); +} + +#[test] +fn failure_method() { + expect_fails_with( + || { + // Arrange + let server = MockServer::start(); + + let m = server.mock(|when, then| { + when.method(POST); + then.status(200); + }); + + // Act + get(&server.base_url()).unwrap(); + + m.assert() + }, + vec![ + "Method Mismatch", + "Expected method equals", + "POST", + "Received", + "GET", + ], + ) +} + +#[test] +fn success_method_not() { + // Arrange + let server = MockServer::start(); + + let m = server.mock(|when, then| { + when.method_not(POST); + then.status(200); + }); + + // Act + let response = get(&server.base_url()).unwrap(); + + // Assert + m.assert(); + assert_eq!(response.status(), 200); +} + +#[test] +fn failure_method_not() { + expect_fails_with( + || { + // Arrange + let server = MockServer::start(); + + let m = server.mock(|when, then| { + when.method_not(GET); + then.status(200); + }); + + // Act + get(&server.base_url()).unwrap(); + + m.assert() + }, + vec![ + "Method Mismatch", + "Expected method not equal to", + "GET", + "Received", + "GET", + ], + ) +} diff --git a/tests/matchers/mod.rs b/tests/matchers/mod.rs new file mode 100644 index 00000000..68cc5159 --- /dev/null +++ b/tests/matchers/mod.rs @@ -0,0 +1,775 @@ +mod body; +mod cookies; +mod headers; +mod host; +mod method; +mod path; +mod port; +mod query_param; +mod scheme; +mod urlencoded_body; + +use std::{ + convert::TryInto, + panic::{self, AssertUnwindSafe, UnwindSafe}, +}; + +pub fn expect_fails_with(f: F, expected_texts: Vec<&str>) +where + F: FnOnce() + UnwindSafe, +{ + let result = panic::catch_unwind(AssertUnwindSafe(f)); + + match result { + Err(err) => { + let err_msg: &str = if let Some(err_msg) = err.downcast_ref::() { + err_msg + } else if let Some(err_msg) = err.downcast_ref::<&str>() { + err_msg + } else { + panic!( + "Expected error message containing:\n{:?}\nBut got a different type of panic.", + expected_texts + ); + }; + + // Check that all expected texts appear in order in the error message + let mut start_index = 0; + for expected_text in &expected_texts { + if let Some(index) = err_msg[start_index..].find(expected_text) { + start_index += index + expected_text.len(); + } else { + panic!( + "Expected error message to contain in order:\n{:?}\nBut got:\n{}", + expected_texts, err_msg + ); + } + } + } + _ => panic!( + "Expected panic with error message containing in order:\n{:?}", + expected_texts + ), + } +} + +pub fn expect_fails_with2(expected_texts: V, f: F) +where + F: FnOnce() + panic::UnwindSafe, + V: Into>, + S: ToString, +{ + // Convert expected texts into a Vec + let expected_texts: Vec = expected_texts + .into() + .into_iter() + .map(|s| s.to_string()) + .collect(); + + // Suppress panic output for this invocation + let default_hook = panic::take_hook(); + panic::set_hook(Box::new(|_| {})); + + // Catch panic and unwind safely + let result = panic::catch_unwind(AssertUnwindSafe(f)); + + // Restore the default panic hook + panic::set_hook(default_hook); + + match result { + Err(err) => { + // Extract the error message from the panic + let err_msg: &str = if let Some(err_msg) = err.downcast_ref::() { + err_msg + } else if let Some(err_msg) = err.downcast_ref::<&str>() { + err_msg + } else { + panic!( + "Expected error message containing:\n{:?}\nBut got a different type of panic.", + expected_texts + ); + }; + + // Check that all expected texts appear in order in the error message + let mut start_index = 0; + for expected_text in &expected_texts { + if let Some(index) = err_msg[start_index..].find(expected_text) { + start_index += index + expected_text.len(); + } else { + panic!( + "Expected error message to contain in order:\n{:?}\nBut got:\n{}", + expected_texts, err_msg + ); + } + } + } + Ok(_) => panic!( + "Expected panic with error message containing in order:\n{:?}", + expected_texts + ), + } +} + +enum SubstringPart { + Prefix, + Mid, + Suffix, +} + +fn substring_of(s: S, part: SubstringPart) -> String { + let s = s.to_string(); + + let len = s.len(); + let half_length = len / 2; + + match part { + SubstringPart::Prefix => (&s[..half_length]).to_string(), + SubstringPart::Mid => { + let start = (len - half_length) / 2; + (&s[start..start + half_length]).to_string() + } + SubstringPart::Suffix => (&s[len - half_length..]).to_string(), + } +} + +fn inverse_char(c: char) -> char { + match c { + 'a'..='z' => (b'z' - (c as u8 - b'a')) as char, + 'A'..='Z' => (b'Z' - (c as u8 - b'A')) as char, + '0'..='9' => (b'9' - (c as u8 - b'0')) as char, + _ => c, + } +} + +fn string_inverse(s: S) -> String { + s.to_string().chars().map(inverse_char).collect() +} + +#[derive(Debug)] +pub struct MultiValueMatcherData +where + K: Into, + V: Into, + M: Into, +{ + scenario_name: String, + expect: ExpectedValue, + actual: Vec<(K, V)>, + failure_msg: Option>, +} + +#[derive(Debug)] +pub struct MultiValueMatcherTestSet +where + K: Into, + V: Into, + C: TryInto, + M: Into, +{ + attribute: Vec>, + attribute_not: Vec>, + attribute_exists: Vec>, + attribute_missing: Vec>, + attribute_includes: Vec>, + attribute_excludes: Vec>, + attribute_prefix: Vec>, + attribute_suffix: Vec>, + attribute_prefix_not: Vec>, + attribute_suffix_not: Vec>, + attribute_matches: Vec>, + attribute_count: Vec>, +} + +impl MultiValueMatcherTestSet<&'static str, &'static str, usize, &'static str> { + pub fn generate( + entity: &'static str, + mismatch_header: &'static str, + case_sensitive: bool, + ) -> Self { + return MultiValueMatcherTestSet { + attribute: vec![ + MultiValueMatcherData { + scenario_name: format!("{} where 'word' equals 'hello'", entity), + expect: ("word", "hello"), + actual: vec![("lang", "en"), ("word", "hello"), ("short", "hi")], + failure_msg: None, + }, + MultiValueMatcherData { + scenario_name: format!("{} with multiple keys in the request not matching", entity), + expect: ("word", "hello world"), + actual: vec![ + ("lang", "en"), + ("weird", "hello world"), + ("short", "hi"), + ("word", "hallo welt"), + ], + failure_msg: Some(vec![ + mismatch_header, + "key", + "equals", + "word", + "value", + "equals", + "hello world", + entity, + ]), + }], + attribute_not: vec![ + MultiValueMatcherData { + scenario_name: format!("{}_not where 'word' does not equal 'hello'", entity), + expect: ("word", "hello"), + actual: vec![("word", "hallo")], + failure_msg: None, + }, + MultiValueMatcherData { + scenario_name: format!("{}_not where 'word' is empty", entity), + expect: ("word", "hello"), + actual: vec![("word", "")], + failure_msg: None, + }, + MultiValueMatcherData { + scenario_name: format!("{}_not where 'word' does not exactly equal 'hello'", entity), + expect: ("word", "hello"), + actual: vec![("word", "hello world")], + failure_msg: None, + }, + MultiValueMatcherData { + scenario_name: format!("{}_not with correct value but missing key", entity), + expect: ("hello", "world"), + actual: vec![("wrong_key", "world")], + failure_msg: Some(vec![ + mismatch_header, + "Expected", + "key", + "equals", + "hello", + "value", + "not equal to", + "world", + "Received", + "wrong_key=world", + entity, + "_not", + ]), + }, + MultiValueMatcherData { + scenario_name: format!("{}_not with one key non-matching key", entity), + expect: ("hello", "world"), + actual: vec![("hello", "world")], + failure_msg: Some(vec![ + mismatch_header, + "Expected", + "key", + "equals", + "hello", + "value", + "not equal to", + "world", + "Received", + "hello=world", + entity, + "_not", + ]), + }, + MultiValueMatcherData { + scenario_name: format!("{}_not where 'word' key should not match 'hello' but is not present", entity), + expect: ("word", "hello"), + actual: vec![("not_word", "hello world")], + failure_msg: Some(vec![ + mismatch_header, + "Expected", "key", "equals", "word", + "value", "not equal to", "hello", + "Received", "not_word=hello world", + entity, + "_not", + ]), + }, + ], + attribute_exists: vec![ + MultiValueMatcherData { + scenario_name: format!("{}_exists where 'word' is present with value", entity), + expect: "word", + actual: vec![("word", "hello")], + failure_msg: None, + }, + MultiValueMatcherData { + scenario_name: format!("{}_exists where 'word' is present without value", entity), + expect: "word", + actual: vec![("word", "")], + failure_msg: None, + }, + MultiValueMatcherData { + scenario_name: format!("{}_exists where parameter should be present but is missing", entity), + expect: "word", + actual: vec![("wald", "word"), ("world", "hello")], + failure_msg: Some(vec![ + mismatch_header, + "Expected", "key", "equals", "word", + "to be in the request, but none was provided", + entity, + "_exists", + ]), + }, + ], + attribute_missing: vec![ + MultiValueMatcherData { + scenario_name: format!("{}_missing where 'word' is absent", entity), + expect: "word", + actual: vec![("something", "different")], + failure_msg: None, + }, + MultiValueMatcherData { + scenario_name: format!("{}_missing where parameter 'word' should not be present but is found", entity), + expect: "word", + actual: vec![("welt", "different"), ("word", "")], // Capturing 'word' as empty + failure_msg: Some(vec![ + mismatch_header, + "Expected", "key", "not equal to", "word", + "not to be present, but the request contained it", + entity, + "_missing", + ]), + }, + ], + attribute_includes: vec![ + MultiValueMatcherData { + scenario_name: format!("{}_includes where 'word' includes 'ello'", entity), + expect: ("word", "ello"), + actual: vec![("word", "hello")], + failure_msg: None, + }, + MultiValueMatcherData { + scenario_name: format!("{}_includes where 'word' value should include 'ello'", entity), + expect: ("word", "ello"), + actual: vec![("word", "world")], // Actual value that fails to meet the expectation + failure_msg: Some(vec![ + mismatch_header, + "Expected", "key", "equals", "word", + "value", "includes", "ello", + "Received", "word=world", + entity, + "_includes", + ]), + }, + ], + attribute_excludes: vec![ + MultiValueMatcherData { + scenario_name: format!("{}_excludes where 'word' excludes 'ello'", entity), + expect: ("word", "ello"), + actual: vec![("word", "hallo")], + failure_msg: None, + }, + MultiValueMatcherData { + scenario_name: format!("{}_excludes where 'word' value should exclude 'ello'", entity), + expect: ("word", "ello"), + actual: vec![("word", "hello")], + failure_msg: Some(vec![ + mismatch_header, + "Expected", "key", "equals", "word", + "value", "excludes", "ello", + "Received", "word=hello", + entity, + "_excludes", + ]), + }, + ], + attribute_prefix: vec![ + MultiValueMatcherData { + scenario_name: format!("{}_prefix where 'word' starts with 'ha'", entity), + expect: ("word", "ha"), + actual: vec![("word", "hallo")], + failure_msg: None, + }, + MultiValueMatcherData { + scenario_name: format!("{}_prefix where 'word' value should start with 'ha'", entity), + expect: ("word", "ha"), + actual: vec![("word", "hello")], // Actual value that correctly matches the prefix condition + failure_msg: Some(vec![ + mismatch_header, + "Expected", "key", "equals", "word", + "value", "prefix", "ha", + "Received", "word=hello", + entity, + "_prefix", + ]), + }, + ], + attribute_suffix: vec![ + MultiValueMatcherData { + scenario_name: format!("{}_suffix where 'word' ends with 'llo'", entity), + expect: ("word", "llo"), + actual: vec![("word", "hello")], + failure_msg: None, + }, + MultiValueMatcherData { + scenario_name: format!("{}_suffix where 'word' value should end with 'llo'", entity), + expect: ("word", "llo"), + actual: vec![("word", "world")], // Actual value that fails to meet the suffix condition + failure_msg: Some(vec![ + mismatch_header, + "Expected", "key", "equals", "word", + "value", "suffix", "llo", + "Received", "word=world", + entity, + "_suffix", + ]), + }, + ], + attribute_prefix_not: vec![ + MultiValueMatcherData { + scenario_name: format!("{}_prefix_not where 'word' does not start with 'ha'", entity), + expect: ("word", "ha"), + actual: vec![("word", "hello")], + failure_msg: None, + }, + MultiValueMatcherData { + scenario_name: format!("{}_prefix_not where 'word' value should not start with 'ha'", entity), + expect: ("word", "ha"), + actual: vec![("word", "hallo")], // Actual value that incorrectly matches the prefix condition + failure_msg: Some(vec![ + mismatch_header, + "Expected", "key", "equals", "word", + "value", "prefix not", "ha", + "Received", "word=hallo", + entity, + "_prefix_not", + ]), + }, + ], + attribute_suffix_not: vec![ + MultiValueMatcherData { + scenario_name: format!("{}_suffix_not where 'word' does not end with 'll'", entity), + expect: ("word", "ll"), + actual: vec![("word", "hallo")], + failure_msg: None, + }, + MultiValueMatcherData { + scenario_name: format!("{}_suffix_not where 'word' value should not end with 'ld'", entity), + expect: ("word", "ld"), + actual: vec![("word", "world")], // Actual value that incorrectly matches the suffix condition + failure_msg: Some(vec![ + mismatch_header, + "Expected", "key", "equals", "word", + "value", "suffix not", "ld", + "Received", "word=world", + entity, + "_suffix_not", + ]), + }, + ], + attribute_matches: vec![ + MultiValueMatcherData { + scenario_name: format!("{}_matches where key matches '.*ll.*' and value matches '.*or.*'", entity), + expect: (".*ll.*", ".*or.*"), + actual: vec![("hello", "world")], + failure_msg: None, + }, + MultiValueMatcherData { + scenario_name: format!("{}_matches where key and value should match regex patterns", entity), + expect: (".*ll.*", ".*or.*"), + actual: vec![("hello", "peter")], // Actual key-value that fails to match the expected regex patterns fully + failure_msg: Some(vec![ + mismatch_header, + "Expected", "key", "matches regex", ".*ll.*", + "value", "matches regex", ".*or.*", + "Received", "hello=peter", + entity, + "_matches", + ]), + }, + MultiValueMatcherData { + scenario_name: format!("{}_matches where key and value should match regex patterns again", entity), + expect: (".*ll.*", ".*or.*"), + actual: vec![("peter", "world")], // Actual key-value that fails both expected regex conditions + failure_msg: Some(vec![ + mismatch_header, + "Expected", "key", "matches regex", ".*ll.*", + "value", "matches regex", ".*or.*", + "Received", "peter=world", + entity, + "_matches", + ]), + }, + ], + attribute_count: vec![ + MultiValueMatcherData { + scenario_name: format!("{}_count where key matches '.*el.*' and value matches '.*al.*' appears 2 times", entity), + expect: (".*el.*", ".*al.*", 2), + actual: vec![("hello", "peter"), ("hello", "wallie"), ("nothing", ""), ("hello", ""), ("hello", "metallica")], + failure_msg: None, + }, + MultiValueMatcherData { + scenario_name: format!("{}_count where key matches '.*el.*' and value matches '.*al.*' appears 2 times", entity), + expect: (".*el.*", ".*al.*", 2), + actual: vec![("hello", "peter"), ("hello", "wallie"), ("nothing", ""), ("hello", ""), ("hello", "metallica")], + failure_msg: None, + }, + MultiValueMatcherData { + scenario_name: format!("{}_count where parameters should match key and value regex and appear a specified number of times", entity), + expect: (".*ll.*", ".*", 10), + actual: vec![("hello", "peter"), ("hello", "wallie"), ("nothing", ""), ("hello", ""), ("hello", "metallica")], + failure_msg: Some(vec![ + mismatch_header, + "Expected", "key", "matches regex", ".*ll.*", + "value", "matches regex", ".*", + "to appear 10 times but appeared 4", + entity, + "_count", + ]), + }, + ], + }; + } +} + +#[derive(Debug)] +pub struct SingleValueMatcherData +where + V: Into, + M: Into, +{ + scenario_name: String, + expect: ExpectedValue, + actual: V, + failure_msg: Option>, +} + +#[derive(Debug)] +pub struct SingleValueMatcherDataSet +where + V: Into, + M: Into, +{ + attribute: Vec>, + attribute_not: Vec>, + attribute_includes: Vec>, + attribute_excludes: Vec>, + attribute_prefix: Vec>, + attribute_suffix: Vec>, + attribute_prefix_not: Vec>, + attribute_suffix_not: Vec>, + attribute_matches: Vec>, +} + +impl SingleValueMatcherDataSet<&'static str, &'static str> { + pub fn generate( + entity: &'static str, + mismatch_header: &'static str, + case_sensitive: bool, + ) -> Self { + return SingleValueMatcherDataSet { + attribute: vec![ + SingleValueMatcherData { + scenario_name: format!("{} TODO", entity), + expect: "test", + actual: "test", + failure_msg: None, + }, + SingleValueMatcherData { + scenario_name: format!("{} TODO", entity), + expect: "test", + actual: "not-test", + failure_msg: Some(vec![ + mismatch_header, + "Expected", + entity, + "equals", + "test", + "Received", + "not-test", + ]), + }, + ], + attribute_not: vec![ + SingleValueMatcherData { + scenario_name: format!("{}_not TODO", entity), + expect: "test", + actual: "twist", + failure_msg: None, + }, + SingleValueMatcherData { + scenario_name: format!("{}_not TODO", entity), + expect: "test", + actual: "test", + failure_msg: Some(vec![ + mismatch_header, + "Expected", + entity, + "not equal to", + "test", + "Received", + "test", + ]), + }, + ], + attribute_includes: vec![ + SingleValueMatcherData { + scenario_name: format!("{}_includes TODO", entity), + expect: "is-a", + actual: "this-is-a-value", + failure_msg: None, + }, + SingleValueMatcherData { + scenario_name: format!("{}_includes TODO", entity), + expect: "dog", + actual: "tomato", + failure_msg: Some(vec![ + mismatch_header, + "Expected", + entity, + "includes", + "dog", + "Received", + "tomato", + ]), + }, + ], + attribute_excludes: vec![ + SingleValueMatcherData { + scenario_name: format!("{}_excludes TODO", entity), + expect: "is-a", + actual: "this-is-the-value", + failure_msg: None, + }, + SingleValueMatcherData { + scenario_name: format!("{}_excludes TODO", entity), + expect: "na", + actual: "banana", + failure_msg: Some(vec![ + mismatch_header, + "Expected", + entity, + "excludes", + "na", + "Received", + "banana", + ]), + }, + ], + attribute_prefix: vec![ + SingleValueMatcherData { + scenario_name: format!("{}_prefix TODO", entity), + expect: "this", + actual: "this-is-the-value", + failure_msg: None, + }, + SingleValueMatcherData { + scenario_name: format!("{}_prefix TODO", entity), + expect: "thi", + actual: "that", + failure_msg: Some(vec![ + mismatch_header, + "Expected", + entity, + "has prefix", + "thi", + "Received", + "that", + ]), + }, + ], + attribute_suffix: vec![ + SingleValueMatcherData { + scenario_name: format!("{}_includes TODO", entity), + expect: "value", + actual: "this-is-the-value", + failure_msg: None, + }, + SingleValueMatcherData { + scenario_name: format!("{}_includes TODO", entity), + expect: "bear", + actual: "banana", + failure_msg: Some(vec![ + mismatch_header, + "Expected", + entity, + "suffix", + "bear", + "Received", + "banana", + ]), + }, + ], + attribute_prefix_not: vec![ + SingleValueMatcherData { + scenario_name: format!("{}_prefix_not TODO", entity), + expect: "value", + actual: "that-is-the-value", + failure_msg: None, + }, + SingleValueMatcherData { + scenario_name: format!("{}_prefix_not TODO", entity), + expect: "this", + actual: "this-is-the-value", + failure_msg: Some(vec![ + mismatch_header, + "Expected", + entity, + "prefix not", + "this", + "Received", + "this-is-the-value", + ]), + }, + ], + attribute_suffix_not: vec![ + SingleValueMatcherData { + scenario_name: format!("{}_suffix_not TODO", entity), + expect: "thing", + actual: "that-is-the-value", + failure_msg: None, + }, + SingleValueMatcherData { + scenario_name: format!("{}_suffix_not TODO", entity), + expect: "to", + actual: "potato", + failure_msg: Some(vec![ + mismatch_header, + "Expected", + entity, + "suffix not", + "to", + "Received", + "potato", + ]), + }, + ], + attribute_matches: vec![ + SingleValueMatcherData { + scenario_name: format!("{}_matches TODO", entity), + expect: ".*ll.*", + actual: "hello", + failure_msg: None, + }, + SingleValueMatcherData { + scenario_name: format!("{}_matches TODO", entity), + expect: ".*is-the.*", + actual: "giggity", + failure_msg: Some(vec![ + mismatch_header, + "Expected", + entity, + "matches", + ".*is-the.*", + "Received", + "giggity", + ]), + }, + ], + }; + } +} + +fn to_urlencoded_query_string(params: Vec<(&str, &str)>) -> String { + params + .into_iter() + .map(|(key, value)| { + format!( + "{}={}", + urlencoding::encode(key), + urlencoding::encode(value) + ) + }) + .collect::>() + .join("&") +} diff --git a/tests/matchers/path.rs b/tests/matchers/path.rs new file mode 100644 index 00000000..069fa85f --- /dev/null +++ b/tests/matchers/path.rs @@ -0,0 +1,179 @@ +use crate::matchers::{expect_fails_with2, SingleValueMatcherDataSet}; +use httpmock::{MockServer, When}; + +#[test] +fn path() { + for (idx, data) in generate_data().attribute.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.path(format!("/{}", data.expect)), + data.actual, + data.failure_msg.clone(), + ) + } +} + +#[test] +fn path_not() { + for (idx, data) in generate_data().attribute_not.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.path_not(format!("/{}", data.expect)), + data.actual, + data.failure_msg.clone(), + ) + } +} + +#[test] +fn path_includes() { + for (idx, data) in generate_data().attribute_includes.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.path_includes(data.expect), + data.actual, + data.failure_msg.clone(), + ) + } +} + +#[test] +fn path_excludes() { + for (idx, data) in generate_data().attribute_excludes.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.path_excludes(data.expect), + data.actual, + data.failure_msg.clone(), + ) + } +} + +#[test] +fn path_prefix() { + for (idx, data) in generate_data().attribute_prefix.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.path_prefix(format!("/{}", data.expect)), + data.actual, + data.failure_msg.clone(), + ) + } +} + +#[test] +fn path_prefix_not() { + for (idx, data) in generate_data().attribute_prefix_not.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.path_prefix_not(format!("/{}", data.expect)), + data.actual, + data.failure_msg.clone(), + ) + } +} + +#[test] +fn path_suffix() { + for (idx, data) in generate_data().attribute_suffix.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.path_suffix(data.expect), + data.actual, + data.failure_msg.clone(), + ) + } +} + +#[test] +fn path_suffix_not() { + for (idx, data) in generate_data().attribute_suffix_not.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.path_suffix_not(data.expect), + data.actual, + data.failure_msg.clone(), + ) + } +} + +#[test] +fn path_matches() { + for (idx, data) in generate_data().attribute_matches.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.path_matches(data.expect), + data.actual, + data.failure_msg.clone(), + ) + } +} + +fn generate_data() -> SingleValueMatcherDataSet<&'static str, &'static str> { + SingleValueMatcherDataSet::generate("path", "Path Mismatch", true) +} + +fn run_test( + name: S, + set_expectation: F, + actual: &'static str, + error_msg: Option>, +) where + F: Fn(When) -> When + std::panic::UnwindSafe + std::panic::RefUnwindSafe, + S: Into, +{ + println!("{}", name.into()); + + let run = || { + // Arrange + let server = MockServer::start(); + + let m = server.mock(|when, then| { + set_expectation(when); + then.status(200); + }); + + // Act + let response = reqwest::blocking::Client::new() + .get(server.url(format!("/{}", actual))) + .send() + .unwrap(); + + // Assert + m.assert(); + assert_eq!(response.status(), 200); + }; + + if let Some(err_msg) = error_msg { + expect_fails_with2(err_msg, run); + } else { + run(); + } +} diff --git a/tests/matchers/port.rs b/tests/matchers/port.rs new file mode 100644 index 00000000..7a2c6e50 --- /dev/null +++ b/tests/matchers/port.rs @@ -0,0 +1,113 @@ +use crate::matchers::expect_fails_with; +use httpmock::MockServer; +use reqwest::blocking::get; + +#[test] +fn host_tests() { + // Arrange + let server = MockServer::start(); + let port = server.port(); + + let m = server.mock(|when, then| { + when.port(port); + then.status(200); + }); + + // Act + let response = get(&server.base_url()).unwrap(); + + // Assert + m.assert(); + assert_eq!(response.status(), 200); +} + +#[test] +fn host_failure() { + expect_fails_with( + || { + // Arrange + let server = MockServer::start(); + + let m = server.mock(|when, then| { + when.port(0); // explicitly matching against port 0 will always fail + then.status(200); + }); + + // Act + get(&server.base_url()).unwrap(); + + m.assert() + }, + vec!["Port Mismatch", "Expected port equals", "0", "Received"], + ) +} + +#[test] +fn host_not_success_name() { + // Arrange + let server = MockServer::start(); + + let m = server.mock(|when, then| { + when.port_not(0); // Port is never 0, so this will always match. + then.status(200); + }); + + // Act + let response = get(&server.base_url()).unwrap(); + + // Assert + m.assert(); + assert_eq!(response.status(), 200); +} + +#[test] +fn host_not_failure() { + expect_fails_with( + || { + // Arrange + let server = MockServer::start(); + let port = server.port(); + + let m = server.mock(|when, then| { + when.host_not("127.0.0.1"); + then.status(200); + }); + + // Act + get(&server.base_url()).unwrap(); + + m.assert() + }, + vec![ + "Host Mismatch", + "Expected host not equal to", + "127.0.0.1", + "Received", + "127.0.0.1", + ], + ); + + expect_fails_with( + || { + // Arrange + let server = MockServer::start(); + + let m = server.mock(|when, then| { + when.host_not("localhost"); + then.status(200); + }); + + // Act + get(&server.base_url()).unwrap(); + + m.assert() + }, + vec![ + "Host Mismatch", + "Expected host not equal to", + "localhost", + "Received", + "127.0.0.1", + ], + ); +} diff --git a/tests/matchers/query_param.rs b/tests/matchers/query_param.rs new file mode 100644 index 00000000..34eee150 --- /dev/null +++ b/tests/matchers/query_param.rs @@ -0,0 +1,222 @@ +use crate::matchers::{expect_fails_with2, to_urlencoded_query_string, MultiValueMatcherTestSet}; +use httpmock::{MockServer, When}; + +#[test] +fn query_param() { + for (idx, data) in generate_data().attribute.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.query_param(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn query_param_not() { + for (idx, data) in generate_data().attribute_not.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.query_param_not(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn query_param_exists() { + for (idx, data) in generate_data().attribute_exists.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.query_param_exists(data.expect), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn query_param_missing() { + for (idx, data) in generate_data().attribute_missing.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.query_param_missing(data.expect), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn query_param_includes() { + for (idx, data) in generate_data().attribute_includes.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.query_param_includes(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn query_param_excludes() { + for (idx, data) in generate_data().attribute_excludes.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.query_param_excludes(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn query_param_prefix() { + for (idx, data) in generate_data().attribute_prefix.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.query_param_prefix(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn query_param_suffix() { + for (idx, data) in generate_data().attribute_suffix.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.query_param_suffix(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn query_param_prefix_not() { + for (idx, data) in generate_data().attribute_prefix_not.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.query_param_prefix_not(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn query_param_suffix_not() { + for (idx, data) in generate_data().attribute_suffix_not.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.query_param_suffix_not(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn query_param_matches() { + for (idx, data) in generate_data().attribute_matches.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.query_param_matches(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn query_param_count() { + for (idx, data) in generate_data().attribute_count.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.query_param_count(data.expect.0, data.expect.1, data.expect.2), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +fn generate_data() -> MultiValueMatcherTestSet<&'static str, &'static str, usize, &'static str> { + MultiValueMatcherTestSet::generate("query_param", "Query Parameter Mismatch", false) +} + +fn run_test( + name: S, + set_expectation: F, + actual: Vec<(&'static str, &'static str)>, + error_msg: Option>, +) where + F: Fn(When) -> When + std::panic::UnwindSafe + std::panic::RefUnwindSafe, + S: Into, +{ + println!("{}", name.into()); + + let run = || { + // Arrange + let server = MockServer::start(); + + let m = server.mock(|when, then| { + set_expectation(when); + then.status(200); + }); + + // Act + let url = server.url(&format!("/test?{}", to_urlencoded_query_string(actual))); + let response = reqwest::blocking::get(&url).unwrap(); + + // Assert + m.assert(); + assert_eq!(response.status(), 200); + }; + + if let Some(err_msg) = error_msg { + expect_fails_with2(err_msg, run); + } else { + run(); + } +} diff --git a/tests/matchers/scheme.rs b/tests/matchers/scheme.rs new file mode 100644 index 00000000..4d974b2c --- /dev/null +++ b/tests/matchers/scheme.rs @@ -0,0 +1,95 @@ +use crate::matchers::expect_fails_with; +use httpmock::MockServer; +use reqwest::blocking::get; + +#[test] +fn scheme_tests() { + // Arrange + let server = MockServer::start(); + + let m = server.mock(|when, then| { + when.scheme("http"); + then.status(200); + }); + + // Act + let response = get(&server.base_url()).unwrap(); + + // Assert + m.assert(); + assert_eq!(response.status(), 200); +} + +#[test] +fn scheme_failure() { + expect_fails_with( + || { + // Arrange + let server = MockServer::start(); + + let m = server.mock(|when, then| { + when.scheme("https"); + then.status(200); + }); + + // Act + get(&server.base_url()).unwrap(); + + m.assert() + }, + vec![ + "Scheme Mismatch", + "Expected", + "scheme equals", + "https", + "Received", + "http", + ], + ) +} + +#[test] +fn scheme_not_tests() { + // Arrange + let server = MockServer::start(); + + let m = server.mock(|when, then| { + when.scheme_not("https"); + then.status(200); + }); + + // Act + let response = get(&server.base_url()).unwrap(); + + // Assert + m.assert(); + assert_eq!(response.status(), 200); +} + +#[test] +fn scheme_not_failure() { + expect_fails_with( + || { + // Arrange + let server = MockServer::start(); + + let m = server.mock(|when, then| { + when.scheme_not("http"); + then.status(200); + }); + + // Act + get(&server.base_url()).unwrap(); + + m.assert() + }, + vec![ + "Scheme Mismatch", + "Expected", + "scheme not equal to", + "http", + "Received", + "http", + ], + ) +} diff --git a/tests/matchers/urlencoded_body.rs b/tests/matchers/urlencoded_body.rs new file mode 100644 index 00000000..0316b8bc --- /dev/null +++ b/tests/matchers/urlencoded_body.rs @@ -0,0 +1,244 @@ +use crate::matchers::{expect_fails_with2, MultiValueMatcherTestSet}; +use httpmock::{MockServer, When}; + +#[test] +fn form_urlencoded_tuple() { + for (idx, data) in generate_data().attribute.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.form_urlencoded_tuple(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] +fn form_urlencoded_tuple_not() { + for (idx, data) in generate_data().attribute_not.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.form_urlencoded_tuple_not(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] + +fn form_urlencoded_tuple_exists() { + for (idx, data) in generate_data().attribute_exists.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.form_urlencoded_tuple_exists(data.expect), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] + +fn form_urlencoded_tuple_missing() { + for (idx, data) in generate_data().attribute_missing.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.form_urlencoded_tuple_missing(data.expect), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] + +fn form_urlencoded_tuple_includes() { + for (idx, data) in generate_data().attribute_includes.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.form_urlencoded_tuple_includes(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] + +fn form_urlencoded_tuple_excludes() { + for (idx, data) in generate_data().attribute_excludes.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.form_urlencoded_tuple_excludes(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] + +fn form_urlencoded_tuple_prefix() { + for (idx, data) in generate_data().attribute_prefix.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.form_urlencoded_tuple_prefix(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] + +fn form_urlencoded_tuple_suffix() { + for (idx, data) in generate_data().attribute_suffix.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.form_urlencoded_tuple_suffix(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] + +fn form_urlencoded_tuple_prefix_not() { + for (idx, data) in generate_data().attribute_prefix_not.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.form_urlencoded_tuple_prefix_not(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] + +fn form_urlencoded_tuple_suffix_not() { + for (idx, data) in generate_data().attribute_suffix_not.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.form_urlencoded_tuple_suffix_not(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] + +fn form_urlencoded_tuple_matches() { + for (idx, data) in generate_data().attribute_matches.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.form_urlencoded_tuple_matches(data.expect.0, data.expect.1), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +#[test] + +fn form_urlencoded_tuple_count() { + for (idx, data) in generate_data().attribute_count.iter().enumerate() { + run_test( + format!( + "Running test case with index '{}' and test data: {:?}", + idx, data + ), + |when| when.form_urlencoded_tuple_count(data.expect.0, data.expect.1, data.expect.2), + data.actual.clone(), + data.failure_msg.clone(), + ) + } +} + +fn generate_data() -> MultiValueMatcherTestSet<&'static str, &'static str, usize, &'static str> { + MultiValueMatcherTestSet::generate( + "form_urlencoded_tuple", + "Form-urlencoded Body Mismatch", + false, + ) +} + +fn run_test( + name: S, + set_expectation: F, + actual: Vec<(&'static str, &'static str)>, + error_msg: Option>, +) where + F: Fn(When) -> When + std::panic::UnwindSafe + std::panic::RefUnwindSafe, + S: Into, +{ + println!("{}", name.into()); + + let run = || { + // Arrange + let server = MockServer::start(); + + let m = server.mock(|when, then| { + set_expectation(when); + then.status(200); + }); + + // Act + let mut params = form_urlencoded::Serializer::new(String::new()); + for (key, value) in actual { + params.append_pair(key, value); + } + + let response = reqwest::blocking::Client::new() + .post(server.url("/test")) + .body(params.finish()) + .send() + .unwrap(); + + // Assert + m.assert(); + assert_eq!(response.status(), 200); + }; + + if let Some(err_msg) = error_msg { + expect_fails_with2(err_msg, run); + } else { + run(); + } +} diff --git a/tests/internal/extensions_test.rs b/tests/misc/extensions_test.rs similarity index 92% rename from tests/internal/extensions_test.rs rename to tests/misc/extensions_test.rs index 286b9956..aa3c2ae5 100644 --- a/tests/internal/extensions_test.rs +++ b/tests/misc/extensions_test.rs @@ -1,7 +1,6 @@ extern crate httpmock; -use self::httpmock::prelude::*; -use self::httpmock::Mock; +use self::httpmock::{prelude::*, Mock}; use std::cell::RefCell; // Test for issue https://github.com/alexliesenfeld/httpmock/issues/26 @@ -33,5 +32,5 @@ fn wrapper_test() { drop(mock); let mock = Mock::new(sw.mocks.borrow_mut().get(0).unwrap().id, &sw.server); - mock.hits(); + mock.calls(); } diff --git a/tests/internal/large_body_test.rs b/tests/misc/large_body_test.rs similarity index 52% rename from tests/internal/large_body_test.rs rename to tests/misc/large_body_test.rs index bc0ed181..31330231 100644 --- a/tests/internal/large_body_test.rs +++ b/tests/misc/large_body_test.rs @@ -1,27 +1,29 @@ use httpmock::prelude::*; -use isahc::{Request, RequestExt}; +use reqwest::blocking::Client; -use crate::simulate_standalone_server; +use crate::with_standalone_server; #[test] fn large_body_test() { // Arrange - // This starts up a standalone server in the background running on port 5000 - simulate_standalone_server(); + // This starts up a standalone server in the background running on port 5050 + with_standalone_server(); // Instead of creating a new MockServer using new(), we connect to an existing remote instance. - let server = MockServer::connect("localhost:5000"); + let server = MockServer::connect("localhost:5050"); let search_mock = server.mock(|when, then| { - when.path("/search").body("wow so large".repeat(1000000)); // ~12 MB body + when.path("/search") + .body("wow so large".repeat(1024 * 1024 * 10)); // 10 MB body then.status(202); }); // Act: Send the HTTP request - let response = Request::post(server.url("/search")) - .body("wow so large".repeat(1000000)) // ~12 MB body - .unwrap() + let client = Client::new(); + let response = client + .post(&server.url("/search")) + .body("wow so large".repeat(1024 * 1024 * 10)) // 10 MB body .send() .unwrap(); diff --git a/tests/internal/loop_test.rs b/tests/misc/loop_test.rs similarity index 72% rename from tests/internal/loop_test.rs rename to tests/misc/loop_test.rs index 92d71d8a..d9d41d78 100644 --- a/tests/internal/loop_test.rs +++ b/tests/misc/loop_test.rs @@ -1,21 +1,21 @@ extern crate httpmock; use httpmock::prelude::*; -use isahc::get; +use reqwest::blocking::get; #[cfg(feature = "remote")] -use crate::simulate_standalone_server; +use crate::with_standalone_server; #[cfg(feature = "remote")] #[test] fn loop_with_standalone_test() { // Arrange - // This starts up a standalone server in the background running on port 5000 - simulate_standalone_server(); + // This starts up a standalone server in the background running on port 5050 + with_standalone_server(); // Instead of creating a new MockServer using new(), we connect to an existing remote instance. - let server = MockServer::connect("localhost:5000"); + let server = MockServer::connect("localhost:5050"); for x in 0..1000 { let search_mock = server.mock(|when, then| { @@ -24,7 +24,7 @@ fn loop_with_standalone_test() { }); // Act: Send the HTTP request - let response = get(server.url(&format!("/test/{}", x))).unwrap(); + let response = get(&server.url(&format!("/test/{}", x))).unwrap(); // Assert search_mock.assert(); @@ -36,12 +36,12 @@ fn loop_with_standalone_test() { fn loop_with_local_test() { // Arrange - // Instead of creating a new MockServer using new(), we connect to an existing remote instance. + // Create a new local MockServer instance. let server = MockServer::start(); let _mock = server.mock(|when, then| { when.path("/test") - .path_contains("test") + .path_includes("test") .query_param("myQueryParam", "überschall"); then.status(202); }); @@ -53,11 +53,10 @@ fn loop_with_local_test() { }); // Act: Send the HTTP request - let response = get(server.url(&format!("/test/{}", x))).unwrap(); + let response = get(&server.url(&format!("/test/{}", x))).unwrap(); // Assert search_mock.assert(); - assert_eq!(response.status(), 202); } } diff --git a/tests/internal/mod.rs b/tests/misc/mod.rs similarity index 66% rename from tests/internal/mod.rs rename to tests/misc/mod.rs index 6733e7a2..97cdc025 100644 --- a/tests/internal/mod.rs +++ b/tests/misc/mod.rs @@ -2,4 +2,5 @@ mod extensions_test; #[cfg(feature = "remote")] mod large_body_test; mod loop_test; +#[cfg(all(feature = "proxy", feature = "remote"))] mod runtimes_test; diff --git a/tests/misc/runtimes_test.rs b/tests/misc/runtimes_test.rs new file mode 100644 index 00000000..a8b3ed85 --- /dev/null +++ b/tests/misc/runtimes_test.rs @@ -0,0 +1,72 @@ +use crate::with_standalone_server; +use httpmock::prelude::*; +use reqwest::Client; + +#[test] +fn all_runtimes_test() { + return; // TODO: This needs to be fixed. New HTTP client requires tokio runtime! + + with_standalone_server(); + + // Tokio + assert_eq!( + tokio::runtime::Runtime::new().unwrap().block_on(test_fn()), + 202 + ); + + // Actix + assert_eq!(actix_rt::Runtime::new().unwrap().block_on(test_fn()), 202); + + // async_std + assert_eq!(async_std::task::block_on(test_fn()), 202); +} + +async fn test_fn() -> u16 { + // We will create this mock server to simulate a real service (e.g., GitHub, AWS, etc.). + let server3 = MockServer::start_async().await; + server3 + .mock_async(|when, then| { + when.any_request(); + then.status(202).body("Hi from fake GitHub!"); + }) + .await; + + let server2 = MockServer::connect_async("localhost:5050").await; + server2 + .forward_to_async(server3.base_url(), |rule| { + rule.filter(|when| { + when.any_request(); // We want all requests to be proxied. + }); + }) + .await; + + // Let's create our mock server for the test + let server1 = MockServer::start_async().await; + + // We configure our server to proxy the request to the target host instead of + // answering with a mocked response. The 'when' variable lets you configure + // rules under which requests are proxied. + server1 + .proxy_async(|rule| { + rule.filter(|when| { + when.any_request(); // We want all requests to be proxied. + }); + }) + .await; + + // The following will send a request to the mock server. The request will be forwarded + // to the target host, as we configured before. + let client = Client::builder() + .proxy(reqwest::Proxy::all(server1.base_url()).unwrap()) // Configure to use a proxy server + .build() + .unwrap(); + + // Since the request was forwarded, we should see the target host's response. + let response = client.get(server2.url("/get")).send().await.unwrap(); + let status_code = response.status().as_u16(); + + assert_eq!("Hi from fake GitHub!", response.text().await.unwrap()); + assert_eq!(status_code, 202); + + status_code +} diff --git a/tests/resources/simple_static_mock.yaml b/tests/resources/simple_static_mock.yaml new file mode 100644 index 00000000..477a7449 --- /dev/null +++ b/tests/resources/simple_static_mock.yaml @@ -0,0 +1,6 @@ +when: + method: GET + path: /static-mock/examples/simple +then: + status: 200 + json_body: '{ "response" : "hello" }' diff --git a/tools/Cargo.toml b/tools/Cargo.toml new file mode 100644 index 00000000..60d415a2 --- /dev/null +++ b/tools/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "tools" +version = "0.1.0" +edition = "2018" + +[dependencies] +httpmock = { path = ".." , features = ["default", "standalone", "color", "cookies", "remote", "remote-https", "proxy", "https", "http2", "record", "experimental"]} +serde_json = "1.0" +syn = { version = "1.0", features = ["full"] } +proc-macro2 = { version = "1.0", features = ["default", "span-locations"] } +quote = "1.0" + +[[bin]] +name = "extract_docs" +path = "src/extract_docs.rs" + +[[bin]] +name = "extract_code" +path = "src/extract_code.rs" + +[[bin]] +name = "extract_groups" +path = "src/extract_groups.rs" + +[[bin]] +name = "extract_example_tests" +path = "src/extract_example_tests.rs" \ No newline at end of file diff --git a/tools/src/extract_code.rs b/tools/src/extract_code.rs new file mode 100644 index 00000000..3e3bbcef --- /dev/null +++ b/tools/src/extract_code.rs @@ -0,0 +1,78 @@ +use serde_json::json; +use std::fs; +use syn::{File, Item, ItemImpl}; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn main() { + let file_content = fs::read_to_string("../src/api/spec.rs").expect("Unable to read file"); + let syntax_tree: File = syn::parse_file(&file_content).expect("Unable to parse file"); + + let mut when_docs = serde_json::Map::new(); + let mut then_docs = serde_json::Map::new(); + + for item in syntax_tree.items { + if let Item::Impl(ItemImpl { self_ty, items, .. }) = &item { + if let syn::Type::Path(type_path) = &**self_ty { + let ident = &type_path.path.segments.last().unwrap().ident; + if ident == "When" { + extract_docs_for_impl(&mut when_docs, items); + } else if ident == "Then" { + extract_docs_for_impl(&mut then_docs, items); + } + } + } + } + + let json_output = json!({ + "when": when_docs, + "then": then_docs + }); + + let json_output_str = serde_json::to_string_pretty(&json_output).expect("Unable to serialize JSON"); + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + + fs::write("target/generated/code_examples.json", json_output_str).expect("Unable to write file"); +} + +fn extract_docs_for_impl(docs: &mut serde_json::Map, items: &Vec) { + for item in items { + if let syn::ImplItem::Method(method) = item { + if let Some(example) = extract_code_example(&method.attrs) { + docs.insert(method.sig.ident.to_string(), json!(example)); + } + } + } +} + +fn extract_code_example(attrs: &Vec) -> Option { + let mut example = String::new(); + let mut in_code_block = false; + + for attr in attrs { + if attr.path.is_ident("doc") { + if let Ok(meta) = attr.parse_meta() { + if let syn::Meta::NameValue(nv) = meta { + if let syn::Lit::Str(lit) = nv.lit { + let doc_line = lit.value(); + if doc_line.trim().starts_with("```rust") { + example.push_str("```rust\n"); + in_code_block = true; + } else if doc_line.trim().starts_with("```") && in_code_block { + example.push_str("```\n"); + in_code_block = false; + } else if in_code_block { + example.push_str(&doc_line); + example.push('\n'); + } + } + } + } + } + } + + if example.is_empty() { + None + } else { + Some(example) + } +} diff --git a/tools/src/extract_docs.rs b/tools/src/extract_docs.rs new file mode 100644 index 00000000..3c236fb8 --- /dev/null +++ b/tools/src/extract_docs.rs @@ -0,0 +1,72 @@ +mod extract_example_tests; + +use serde_json::json; +use std::fs; +use syn::{File, Item, ItemImpl}; +use std::time::{SystemTime, UNIX_EPOCH}; +use syn::spanned::Spanned; +use std::collections::BTreeMap; + +fn main() { + let file_content = fs::read_to_string("../src/api/spec.rs").expect("Unable to read file"); + let syntax_tree: File = syn::parse_file(&file_content).expect("Unable to parse file"); + + let mut when_docs = BTreeMap::new(); + let mut then_docs = BTreeMap::new(); + + for item in syntax_tree.items { + if let Item::Impl(ItemImpl { self_ty, items, .. }) = &item { + if let syn::Type::Path(type_path) = &**self_ty { + let ident = &type_path.path.segments.last().unwrap().ident; + if ident == "When" { + extract_docs_for_impl(&mut when_docs, items); + } else if ident == "Then" { + extract_docs_for_impl(&mut then_docs, items); + } + } + } + } + + let json_output = json!({ + "when": when_docs, + "then": then_docs + }); + + let json_output_str = serde_json::to_string_pretty(&json_output).expect("Unable to serialize JSON"); + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + + fs::write("target/generated/docs.json", json_output_str).expect("Unable to write file"); +} + +fn extract_docs_for_impl(docs: &mut BTreeMap, items: &Vec) { + for item in items { + if let syn::ImplItem::Method(method) = item { + let method_name = method.sig.ident.to_string(); + let method_docs = extract_docs(&method.attrs); + + docs.insert(method_name, method_docs); + } + } +} + +fn extract_docs(attrs: &Vec) -> String { + let mut doc_string = String::new(); + for attr in attrs { + if attr.path.is_ident("doc") { + if let Ok(meta) = attr.parse_meta() { + if let syn::Meta::NameValue(nv) = meta { + if let syn::Lit::Str(lit) = nv.lit { + let trimmed_line = if lit.value().starts_with(' ') { + lit.value()[1..].to_owned() + } else { + lit.value().to_owned() + }; + doc_string.push_str(&trimmed_line); + doc_string.push('\n'); + } + } + } + } + } + doc_string +} diff --git a/tools/src/extract_example_tests.rs b/tools/src/extract_example_tests.rs new file mode 100644 index 00000000..a754e9f3 --- /dev/null +++ b/tools/src/extract_example_tests.rs @@ -0,0 +1,49 @@ +use std::fs; +use std::io::{self, BufRead, BufReader}; +use std::collections::HashMap; + +fn main() { + let directory_path = "../tests/examples"; + let paths = fs::read_dir(directory_path).expect("Unable to read directory"); + + let mut example_map: HashMap = HashMap::new(); + + for path in paths { + let path = path.expect("Error reading path").path(); + if path.is_file() { + let file = fs::File::open(&path).expect("Unable to open file"); + let reader = BufReader::new(file); + + let mut recording = false; + let mut example_code = Vec::new(); + let mut example_id = String::new(); + + for line in reader.lines() { + let line = line.expect("Error reading line"); + if line.contains("// @example-start:") { + recording = true; + // Extract the ID or name from the line + example_id = line.split(':').nth(1).unwrap_or("").trim().to_string(); + example_code.clear(); + } else if line.contains("// @example-end") { + if recording { + recording = false; + // Insert the collected code into the map with the example ID as the key + example_map.insert(example_id.clone(), example_code.join("\n")); + } + } else if recording { + example_code.push(line); + } + } + } + } + + // Ensure the target directory exists + fs::create_dir_all("target").expect("Unable to create target directory"); + + // Serialize the example map to JSON + let json_output_str = serde_json::to_string_pretty(&example_map).expect("Unable to serialize JSON"); + + // Write the output to 'target/extract_example_tests.json' + fs::write("target/generated/example_tests.json", json_output_str).expect("Unable to write file"); +} diff --git a/tools/src/extract_groups.rs b/tools/src/extract_groups.rs new file mode 100644 index 00000000..7b0e5439 --- /dev/null +++ b/tools/src/extract_groups.rs @@ -0,0 +1,72 @@ +use serde_json::json; +use std::fs; +use syn::{File, Item, ItemImpl}; +use std::time::{SystemTime, UNIX_EPOCH}; +use syn::spanned::Spanned; + +fn main() { + let file_content = fs::read_to_string("../src/api/spec.rs").expect("Unable to read file"); + let syntax_tree: File = syn::parse_file(&file_content).expect("Unable to parse file"); + + let mut when_docs = vec![]; + let mut then_docs = vec![]; + + for item in syntax_tree.items { + if let Item::Impl(ItemImpl { self_ty, items, .. }) = &item { + if let syn::Type::Path(type_path) = &**self_ty { + let ident = &type_path.path.segments.last().unwrap().ident; + if ident == "When" { + extract_docs_for_impl(&mut when_docs, items, &file_content); + } else if ident == "Then" { + extract_docs_for_impl(&mut then_docs, items, &file_content); + } + } + } + } + + let json_output = json!({ + "when": when_docs, + "then": then_docs + }); + + let json_output_str = serde_json::to_string_pretty(&json_output).expect("Unable to serialize JSON"); + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + + fs::write("target/generated/groups.json", json_output_str).expect("Unable to write file"); +} + +fn extract_docs_for_impl(docs: &mut Vec, items: &Vec, file_content: &str) { + for item in items { + if let syn::ImplItem::Method(method) = item { + let method_name = method.sig.ident.to_string(); + let method_span = method.span().end(); + let line_number = method_span.line - 1; + + println!("Processing method: {}", method_name); + + if let Some(group) = extract_group_marker(file_content, line_number) { + docs.push(json!({ + "method": method_name, + "group": group, + })); + } else { + println!("No group marker found for method: {}", method_name); + docs.push(json!({ + "method": method_name, + "group": "No group", + })); + } + } + } +} + +fn extract_group_marker(file_content: &str, line_number: usize) -> Option { + let lines: Vec<&str> = file_content.lines().collect(); + if line_number + 1 < lines.len() { + let marker_line = lines[line_number + 1].trim(); + if marker_line.starts_with("// @docs-group:") { + return Some(marker_line.trim_start_matches("// @docs-group:").trim().to_string()); + } + } + None +} \ No newline at end of file