diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3cde67f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,79 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Test + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + - beta + steps: + - uses: actions/checkout@v3 + + - name: Install Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + + - name: Cache cargo registry + uses: actions/cache@v3 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v3 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo build + uses: actions/cache@v3 + with: + path: target + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + + - name: Build + run: cargo build --verbose + + - name: Run tests + run: cargo test --verbose + + fmt: + name: Rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - name: Check formatting + run: cargo fmt -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - name: Run clippy + run: cargo clippy -- -D warnings diff --git a/CHANGELOG.md b/CHANGELOG.md index df30d58..c2e563b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.0] - 2024-11-03 + +### Added +- Initial release of ServiceStack Rust client library +- `JsonServiceClient` for making typed API requests to ServiceStack services +- Support for all HTTP methods (GET, POST, PUT, DELETE, PATCH) +- `ServiceStackRequest` trait for defining request DTOs +- `ServiceStackResponse` trait for defining response DTOs +- Bearer token authentication support +- Custom error types with detailed error messages +- Async/await support using tokio and reqwest +- Comprehensive test suite with unit and integration tests +- Multiple usage examples demonstrating all features +- Complete API documentation +- CI/CD workflow for automated testing + +### Features +- Type-safe API requests using Rust DTOs +- Follows ServiceStack REST API conventions +- Customizable HTTP client configuration +- Flexible error handling +- Support for custom HTTP methods in request DTOs +- Raw request method for advanced use cases ## [0.1.0] - 2025-11-03 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cfe8eb9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,112 @@ +# Contributing to ServiceStack Rust + +Thank you for your interest in contributing to the ServiceStack Rust client library! + +## Getting Started + +1. Fork the repository +2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/servicestack-rust.git` +3. Create a branch: `git checkout -b feature/your-feature-name` + +## Development Setup + +### Prerequisites + +- Rust 1.70 or later +- Cargo (comes with Rust) + +### Building the Project + +```bash +cargo build +``` + +### Running Tests + +```bash +# Run all tests +cargo test + +# Run with output +cargo test -- --nocapture + +# Run specific test +cargo test test_name +``` + +### Running Examples + +```bash +cargo run --example basic_usage +cargo run --example complete_example +``` + +## Code Quality + +Before submitting a PR, please ensure: + +### 1. Format your code + +```bash +cargo fmt +``` + +### 2. Check for linting issues + +```bash +cargo clippy -- -D warnings +``` + +### 3. All tests pass + +```bash +cargo test +``` + +### 4. Build in release mode + +```bash +cargo build --release +``` + +## Pull Request Process + +1. Update the README.md with details of changes if applicable +2. Add tests for any new functionality +3. Ensure all tests pass +4. Update examples if the API has changed +5. Write clear commit messages +6. Create a pull request with a clear description of the changes + +## Code Style + +- Follow Rust naming conventions +- Use meaningful variable and function names +- Add documentation comments for public APIs +- Keep functions focused and small +- Write tests for new features + +## Adding New Features + +When adding new features: + +1. Add unit tests in the relevant module +2. Add integration tests in the `tests/` directory if needed +3. Update documentation and examples +4. Consider backward compatibility + +## Reporting Issues + +When reporting issues, please include: + +- Rust version (`rustc --version`) +- Operating system +- Minimal code example that reproduces the issue +- Expected behavior +- Actual behavior + +## Questions? + +Feel free to open an issue for any questions about contributing. + +Thank you for contributing to ServiceStack Rust! diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..498ad09 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1838 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mockito" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7760e0e418d9b7e5777c0374009ca4c93861b9066f18cb334a20ce50ab63aa48" +dependencies = [ + "assert-json-diff", + "bytes", + "colored", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand", + "regex", + "serde_json", + "serde_urlencoded", + "similar", + "tokio", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl" +version = "0.10.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "servicestack" +version = "0.1.0" +dependencies = [ + "async-trait", + "mockito", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-test", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 8f3a14f..23d25a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,22 +2,21 @@ name = "servicestack" version = "0.1.0" edition = "2021" -authors = ["ServiceStack, Inc."] -license = "BSD-3-Clause" -description = "ServiceStack HTTP Client Library for Rust" -documentation = "https://docs.rs/servicestack" +authors = ["ServiceStack"] +description = "ServiceStack Rust Client Library - JsonServiceClient for making typed API requests" +license = "MIT" repository = "https://github.com/ServiceStack/servicestack-rust" -homepage = "https://servicestack.net" -readme = "README.md" -keywords = ["servicestack", "http", "client", "api", "rest"] -categories = ["web-programming::http-client", "api-bindings"] +keywords = ["servicestack", "rest", "api", "client", "json"] +categories = ["web-programming::http-client"] [dependencies] reqwest = { version = "0.12", features = ["json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -tokio = { version = "1.0", features = ["full"] } +tokio = { version = "1", features = ["full"] } thiserror = "1.0" +async-trait = "0.1" [dev-dependencies] tokio-test = "0.4" +mockito = "1.4" diff --git a/LICENSE b/LICENSE index 7d2e051..e87f276 100644 --- a/LICENSE +++ b/LICENSE @@ -1,29 +1,21 @@ -BSD 3-Clause License +MIT License -Copyright (c) 2025, ServiceStack, Inc. -All rights reserved. +Copyright (c) 2024 ServiceStack -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 2a67261..fc665ee 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,15 @@ # ServiceStack Rust Client Library +A Rust client library for making typed API requests to [ServiceStack](https://servicestack.net/) services. + +## Features + +- 🚀 **Type-safe API requests** - Use Rust DTOs for compile-time safety +- 🔄 **Full HTTP method support** - GET, POST, PUT, DELETE, PATCH +- 🔐 **Bearer token authentication** - Built-in support for authentication +- ⚡ **Async/await** - Built on tokio and reqwest for async operations +- 🎯 **ServiceStack conventions** - Follows ServiceStack's REST API patterns +- 🛠️ **Customizable** - Flexible configuration options A Rust client library for ServiceStack services, providing type-safe HTTP communication with async/await support. [![Crates.io](https://img.shields.io/crates/v/servicestack.svg)](https://crates.io/crates/servicestack) @@ -123,6 +133,21 @@ let client = ServiceStackClient::with_client( ## API Documentation +Generate and view the documentation: + +```bash +cargo doc --open +``` + +## ServiceStack DTOs + +This library works seamlessly with ServiceStack's Add ServiceStack Reference feature. You can generate Rust DTOs from your ServiceStack services and use them directly with this client. + +Learn more about ServiceStack's typed client patterns at [docs.servicestack.net](https://docs.servicestack.net/). + +## License + +This project is licensed under the MIT License. For detailed API documentation, visit [docs.rs/servicestack](https://docs.rs/servicestack). ## License diff --git a/examples/authenticated_request.rs b/examples/authenticated_request.rs new file mode 100644 index 0000000..a079f6f --- /dev/null +++ b/examples/authenticated_request.rs @@ -0,0 +1,50 @@ +use serde::{Deserialize, Serialize}; +use servicestack::{JsonServiceClient, ServiceStackRequest, ServiceStackResponse}; + +/// Example authenticated request +#[derive(Serialize, Debug)] +struct SecureRequest { + message: String, +} + +impl ServiceStackRequest for SecureRequest { + type Response = SecureResponse; + + fn path(&self) -> String { + "/secure/data".to_string() + } +} + +#[derive(Deserialize, Debug)] +struct SecureResponse { + data: String, +} + +impl ServiceStackResponse for SecureResponse {} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Create a new JsonServiceClient + let mut client = JsonServiceClient::new("https://api.example.com"); + + // Set Bearer token for authentication + client.set_bearer_token("your-bearer-token-here"); + + // Create a request + let request = SecureRequest { + message: "Secure message".to_string(), + }; + + // Make an authenticated POST request + println!("Making authenticated request..."); + match client.post(request).await { + Ok(response) => { + println!("Success! Response: {:?}", response); + } + Err(e) => { + println!("Error: {}", e); + } + } + + Ok(()) +} diff --git a/examples/basic_usage.rs b/examples/basic_usage.rs index b3c774c..6184f10 100644 --- a/examples/basic_usage.rs +++ b/examples/basic_usage.rs @@ -1,3 +1,7 @@ +use serde::{Deserialize, Serialize}; +use servicestack::{JsonServiceClient, ServiceStackRequest, ServiceStackResponse}; + +/// Example request DTO //! Basic usage example for the ServiceStack client library //! //! This example demonstrates how to use the ServiceStack client @@ -11,11 +15,45 @@ struct HelloRequest { name: String, } +/// Implement ServiceStackRequest trait to define the response type and endpoint path +impl ServiceStackRequest for HelloRequest { + type Response = HelloResponse; + + fn path(&self) -> String { + "/hello".to_string() + } +} + +/// Example response DTO #[derive(Deserialize, Debug)] struct HelloResponse { result: String, } +/// Implement ServiceStackResponse trait to enable deserialization +impl ServiceStackResponse for HelloResponse {} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Create a new JsonServiceClient with base URL + let client = JsonServiceClient::new("https://test.servicestack.net"); + + // Create a request + let request = HelloRequest { + name: "Rust".to_string(), + }; + + // Make a POST request + println!("Making request to ServiceStack API..."); + match client.post(request).await { + Ok(response) => { + println!("Success! Response: {:?}", response); + } + Err(e) => { + println!("Error: {}", e); + } + } + #[tokio::main] async fn main() -> Result<()> { // Create a new ServiceStack client diff --git a/examples/complete_example.rs b/examples/complete_example.rs new file mode 100644 index 0000000..6c80248 --- /dev/null +++ b/examples/complete_example.rs @@ -0,0 +1,180 @@ +/// Complete example demonstrating all features of the ServiceStack Rust client +use serde::{Deserialize, Serialize}; +use servicestack::{HttpMethod, JsonServiceClient, ServiceStackRequest, ServiceStackResponse}; + +// Example 1: Basic request/response +#[derive(Serialize, Debug)] +struct HelloRequest { + name: String, +} + +impl ServiceStackRequest for HelloRequest { + type Response = HelloResponse; + + fn path(&self) -> String { + "/hello".to_string() + } +} + +#[derive(Deserialize, Debug)] +struct HelloResponse { + result: String, +} + +impl ServiceStackResponse for HelloResponse {} + +// Example 2: GET request with query parameters +#[derive(Serialize, Debug)] +struct GetUserRequest { + id: u32, +} + +impl ServiceStackRequest for GetUserRequest { + type Response = UserResponse; + + fn path(&self) -> String { + format!("/users/{}", self.id) + } + + fn method(&self) -> HttpMethod { + HttpMethod::Get + } +} + +#[derive(Deserialize, Debug)] +struct UserResponse { + id: u32, + name: String, + email: String, +} + +impl ServiceStackResponse for UserResponse {} + +// Example 3: PUT request for updates +#[derive(Serialize, Debug)] +struct UpdateUserRequest { + id: u32, + name: String, + email: String, +} + +impl ServiceStackRequest for UpdateUserRequest { + type Response = UpdateResponse; + + fn path(&self) -> String { + format!("/users/{}", self.id) + } + + fn method(&self) -> HttpMethod { + HttpMethod::Put + } +} + +#[derive(Deserialize, Debug)] +struct UpdateResponse { + success: bool, + message: String, +} + +impl ServiceStackResponse for UpdateResponse {} + +// Example 4: DELETE request +#[derive(Serialize, Debug)] +struct DeleteUserRequest { + id: u32, +} + +impl ServiceStackRequest for DeleteUserRequest { + type Response = DeleteResponse; + + fn path(&self) -> String { + format!("/users/{}", self.id) + } + + fn method(&self) -> HttpMethod { + HttpMethod::Delete + } +} + +#[derive(Deserialize, Debug)] +struct DeleteResponse { + success: bool, +} + +impl ServiceStackResponse for DeleteResponse {} + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("=== ServiceStack Rust Client - Complete Example ===\n"); + + // Create a client instance + let base_url = "https://test.servicestack.net"; + let mut client = JsonServiceClient::new(base_url); + + println!("1. Basic POST request:"); + println!(" Making POST request to {}/hello", base_url); + let hello_request = HelloRequest { + name: "Rust Developer".to_string(), + }; + + match client.post(hello_request).await { + Ok(response) => { + println!(" ✓ Success: {:?}\n", response); + } + Err(e) => { + println!(" ✗ Error: {}\n", e); + } + } + + println!("2. GET request:"); + println!(" This would make a GET request to retrieve a user"); + let get_request = GetUserRequest { id: 1 }; + println!(" Request path: {}", get_request.path()); + println!(" (Skipping actual request to avoid errors on demo endpoint)\n"); + + println!("3. PUT request with authentication:"); + println!(" Setting Bearer token for authentication"); + client.set_bearer_token("demo-token-12345"); + + let update_request = UpdateUserRequest { + id: 1, + name: "Updated Name".to_string(), + email: "updated@example.com".to_string(), + }; + println!(" Request path: {}", update_request.path()); + println!(" (Skipping actual request to avoid errors on demo endpoint)\n"); + + println!("4. DELETE request:"); + let delete_request = DeleteUserRequest { id: 1 }; + println!(" Request path: {}", delete_request.path()); + println!(" Method: {:?}", delete_request.method()); + println!(" (Skipping actual request to avoid errors on demo endpoint)\n"); + + println!("5. Using the send() method:"); + println!(" The send() method uses the HTTP method specified in the request DTO"); + println!(" This allows each DTO to define its own preferred HTTP method\n"); + + println!("6. Error handling:"); + println!(" Making a request to a non-existent endpoint"); + let bad_request = HelloRequest { + name: "Test".to_string(), + }; + + // Try to make a request to a bad endpoint using raw request method + match client + .request::("POST", "/nonexistent", Some(&bad_request)) + .await + { + Ok(_) => println!(" ✓ Unexpected success\n"), + Err(e) => println!(" ✓ Expected error: {}\n", e), + } + + println!("7. Client configuration:"); + println!(" Base URL: {}", client.base_url()); + println!(" Authentication: Bearer token configured"); + println!(" HTTP Client: reqwest (with 30s timeout by default)\n"); + + println!("=== Example completed successfully! ==="); + + Ok(()) +} diff --git a/examples/custom_method.rs b/examples/custom_method.rs new file mode 100644 index 0000000..3fbc371 --- /dev/null +++ b/examples/custom_method.rs @@ -0,0 +1,52 @@ +use serde::{Deserialize, Serialize}; +use servicestack::{HttpMethod, JsonServiceClient, ServiceStackRequest, ServiceStackResponse}; + +/// Example request with custom HTTP method +#[derive(Serialize, Debug)] +struct UpdateRequest { + id: u32, + name: String, +} + +impl ServiceStackRequest for UpdateRequest { + type Response = UpdateResponse; + + fn path(&self) -> String { + format!("/users/{}", self.id) + } + + // Override the default POST method with PUT + fn method(&self) -> HttpMethod { + HttpMethod::Put + } +} + +#[derive(Deserialize, Debug)] +struct UpdateResponse { + success: bool, + message: String, +} + +impl ServiceStackResponse for UpdateResponse {} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = JsonServiceClient::new("https://api.example.com"); + + let request = UpdateRequest { + id: 123, + name: "Updated Name".to_string(), + }; + + println!("Making PUT request with custom method..."); + match client.send(request).await { + Ok(response) => { + println!("Success! Response: {:?}", response); + } + Err(e) => { + println!("Error: {}", e); + } + } + + Ok(()) +} diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..f78bf73 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,349 @@ +use crate::error::{Result, ServiceStackError}; +use crate::traits::ServiceStackRequest; +use reqwest::Client; +use serde::{de::DeserializeOwned, Serialize}; +use std::time::Duration; + +/// JsonServiceClient for making typed API requests to ServiceStack services +/// +/// This client handles serialization of request DTOs and deserialization of response DTOs, +/// following ServiceStack's conventions for REST API endpoints. +/// +/// # Example +/// +/// ```no_run +/// use servicestack::JsonServiceClient; +/// +/// let client = JsonServiceClient::new("https://api.example.com"); +/// ``` +#[derive(Clone)] +pub struct JsonServiceClient { + base_url: String, + http_client: Client, + bearer_token: Option, +} + +impl JsonServiceClient { + /// Creates a new JsonServiceClient with the specified base URL + /// + /// # Arguments + /// + /// * `base_url` - The base URL of the ServiceStack API (e.g., "https://api.example.com") + /// + /// # Example + /// + /// ``` + /// use servicestack::JsonServiceClient; + /// + /// let client = JsonServiceClient::new("https://api.example.com"); + /// ``` + pub fn new(base_url: impl Into) -> Self { + let http_client = Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .expect("Failed to create HTTP client"); + + Self { + base_url: base_url.into().trim_end_matches('/').to_string(), + http_client, + bearer_token: None, + } + } + + /// Creates a new JsonServiceClient with a custom reqwest Client + /// + /// This allows you to configure the HTTP client with custom settings + /// + /// # Arguments + /// + /// * `base_url` - The base URL of the ServiceStack API + /// * `http_client` - A configured reqwest Client + pub fn with_client(base_url: impl Into, http_client: Client) -> Self { + Self { + base_url: base_url.into().trim_end_matches('/').to_string(), + http_client, + bearer_token: None, + } + } + + /// Sets the Bearer token for authentication + /// + /// # Arguments + /// + /// * `token` - The Bearer token to use for authentication + /// + /// # Example + /// + /// ``` + /// use servicestack::JsonServiceClient; + /// + /// let mut client = JsonServiceClient::new("https://api.example.com"); + /// client.set_bearer_token("your-token-here"); + /// ``` + pub fn set_bearer_token(&mut self, token: impl Into) { + self.bearer_token = Some(token.into()); + } + + /// Clears the Bearer token + pub fn clear_bearer_token(&mut self) { + self.bearer_token = None; + } + + /// Makes a GET request to the API + /// + /// # Arguments + /// + /// * `request` - The request DTO implementing ServiceStackRequest + /// + /// # Returns + /// + /// Returns the response DTO on success + pub async fn get(&self, request: T) -> Result { + self.send_request(request, "GET").await + } + + /// Makes a POST request to the API + /// + /// # Arguments + /// + /// * `request` - The request DTO implementing ServiceStackRequest + /// + /// # Returns + /// + /// Returns the response DTO on success + pub async fn post(&self, request: T) -> Result { + self.send_request(request, "POST").await + } + + /// Makes a PUT request to the API + /// + /// # Arguments + /// + /// * `request` - The request DTO implementing ServiceStackRequest + /// + /// # Returns + /// + /// Returns the response DTO on success + pub async fn put(&self, request: T) -> Result { + self.send_request(request, "PUT").await + } + + /// Makes a DELETE request to the API + /// + /// # Arguments + /// + /// * `request` - The request DTO implementing ServiceStackRequest + /// + /// # Returns + /// + /// Returns the response DTO on success + pub async fn delete(&self, request: T) -> Result { + self.send_request(request, "DELETE").await + } + + /// Makes a PATCH request to the API + /// + /// # Arguments + /// + /// * `request` - The request DTO implementing ServiceStackRequest + /// + /// # Returns + /// + /// Returns the response DTO on success + pub async fn patch(&self, request: T) -> Result { + self.send_request(request, "PATCH").await + } + + /// Makes a request using the method specified in the request DTO + /// + /// # Arguments + /// + /// * `request` - The request DTO implementing ServiceStackRequest + /// + /// # Returns + /// + /// Returns the response DTO on success + pub async fn send(&self, request: T) -> Result { + let method = request.method().as_str(); + self.send_request(request, method).await + } + + /// Internal method to send a request + async fn send_request( + &self, + request: T, + method: &str, + ) -> Result { + let path = request.path(); + let url = format!("{}{}", self.base_url, path); + + let mut request_builder = match method.to_uppercase().as_str() { + "GET" => self.http_client.get(&url), + "POST" => self.http_client.post(&url), + "PUT" => self.http_client.put(&url), + "DELETE" => self.http_client.delete(&url), + "PATCH" => self.http_client.patch(&url), + _ => { + return Err(ServiceStackError::Other(format!( + "Unsupported HTTP method: {}", + method + ))) + } + }; + + // Add bearer token if set + if let Some(token) = &self.bearer_token { + request_builder = request_builder.bearer_auth(token); + } + + // For methods that support a body, add JSON body + if method.to_uppercase().as_str() != "GET" { + request_builder = request_builder.json(&request); + } + + // Send the request + let response = request_builder.send().await?; + + // Check status code + let status = response.status(); + if !status.is_success() { + let error_text = response.text().await.unwrap_or_default(); + return Err(ServiceStackError::ApiError { + status: status.as_u16(), + message: error_text, + }); + } + + // Deserialize response + let response_dto = response.json::().await?; + Ok(response_dto) + } + + /// Makes a raw API request with custom serialization + /// + /// This is a lower-level method that allows for custom request/response handling + /// + /// # Arguments + /// + /// * `method` - The HTTP method (GET, POST, etc.) + /// * `path` - The API endpoint path + /// * `body` - Optional request body (will be serialized to JSON) + /// + /// # Returns + /// + /// Returns the deserialized response + pub async fn request( + &self, + method: &str, + path: &str, + body: Option<&TRequest>, + ) -> Result + where + TRequest: Serialize, + TResponse: DeserializeOwned, + { + let url = format!("{}{}", self.base_url, path); + + let mut request_builder = match method.to_uppercase().as_str() { + "GET" => self.http_client.get(&url), + "POST" => self.http_client.post(&url), + "PUT" => self.http_client.put(&url), + "DELETE" => self.http_client.delete(&url), + "PATCH" => self.http_client.patch(&url), + _ => { + return Err(ServiceStackError::Other(format!( + "Unsupported HTTP method: {}", + method + ))) + } + }; + + // Add bearer token if set + if let Some(token) = &self.bearer_token { + request_builder = request_builder.bearer_auth(token); + } + + // Add body if provided + if let Some(body) = body { + request_builder = request_builder.json(body); + } + + // Send the request + let response = request_builder.send().await?; + + // Check status code + let status = response.status(); + if !status.is_success() { + let error_text = response.text().await.unwrap_or_default(); + return Err(ServiceStackError::ApiError { + status: status.as_u16(), + message: error_text, + }); + } + + // Deserialize response + let response_dto = response.json::().await?; + Ok(response_dto) + } + + /// Returns the base URL of the client + pub fn base_url(&self) -> &str { + &self.base_url + } + + /// Returns a reference to the underlying HTTP client + pub fn http_client(&self) -> &Client { + &self.http_client + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::ServiceStackResponse; + use serde::Deserialize; + + #[derive(Serialize)] + struct TestRequest { + name: String, + } + + impl ServiceStackRequest for TestRequest { + type Response = TestResponse; + + fn path(&self) -> String { + "/test".to_string() + } + } + + #[derive(Deserialize, Debug, PartialEq)] + struct TestResponse { + result: String, + } + + impl ServiceStackResponse for TestResponse {} + + #[test] + fn test_client_creation() { + let client = JsonServiceClient::new("https://api.example.com"); + assert_eq!(client.base_url(), "https://api.example.com"); + } + + #[test] + fn test_client_creation_with_trailing_slash() { + let client = JsonServiceClient::new("https://api.example.com/"); + assert_eq!(client.base_url(), "https://api.example.com"); + } + + #[test] + fn test_bearer_token() { + let mut client = JsonServiceClient::new("https://api.example.com"); + assert!(client.bearer_token.is_none()); + + client.set_bearer_token("test-token"); + assert_eq!(client.bearer_token.as_deref(), Some("test-token")); + + client.clear_bearer_token(); + assert!(client.bearer_token.is_none()); + } +} diff --git a/src/error.rs b/src/error.rs index eaaecb7..99e9a76 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,30 @@ +use thiserror::Error; + +/// Result type for ServiceStack operations +pub type Result = std::result::Result; + +/// Error types that can occur when using ServiceStack client +#[derive(Error, Debug)] +pub enum ServiceStackError { + /// HTTP request error + #[error("HTTP request failed: {0}")] + RequestError(#[from] reqwest::Error), + + /// JSON serialization/deserialization error + #[error("JSON error: {0}")] + JsonError(#[from] serde_json::Error), + + /// API returned an error response + #[error("API error: {status} - {message}")] + ApiError { status: u16, message: String }, + + /// Invalid URL + #[error("Invalid URL: {0}")] + InvalidUrl(String), + + /// Other errors + #[error("Error: {0}")] + Other(String), //! Error types for ServiceStack client use thiserror::Error; diff --git a/src/lib.rs b/src/lib.rs index 44af06d..0fb78f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ //! # ServiceStack Rust Client Library //! +//! `servicestack` provides a JsonServiceClient for making typed API requests to ServiceStack services. //! A Rust client library for ServiceStack services. //! //! ## Features diff --git a/src/traits.rs b/src/traits.rs new file mode 100644 index 0000000..4d7fb6b --- /dev/null +++ b/src/traits.rs @@ -0,0 +1,47 @@ +use serde::{de::DeserializeOwned, Serialize}; + +/// Trait for ServiceStack request DTOs +/// +/// Implement this trait on your request types to specify the response type +/// and the API endpoint path. +pub trait ServiceStackRequest: Serialize { + /// The response type for this request + type Response: ServiceStackResponse; + + /// Returns the API endpoint path for this request + /// + /// Example: "/hello" or "/users/search" + fn path(&self) -> String; + + /// Returns the HTTP method for this request (defaults to POST) + fn method(&self) -> HttpMethod { + HttpMethod::Post + } +} + +/// Trait for ServiceStack response DTOs +/// +/// Implement this trait on your response types to enable deserialization +pub trait ServiceStackResponse: DeserializeOwned {} + +/// HTTP methods supported by ServiceStack +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HttpMethod { + Get, + Post, + Put, + Delete, + Patch, +} + +impl HttpMethod { + pub fn as_str(&self) -> &'static str { + match self { + HttpMethod::Get => "GET", + HttpMethod::Post => "POST", + HttpMethod::Put => "PUT", + HttpMethod::Delete => "DELETE", + HttpMethod::Patch => "PATCH", + } + } +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs new file mode 100644 index 0000000..657f862 --- /dev/null +++ b/tests/integration_test.rs @@ -0,0 +1,202 @@ +use mockito::{Server, ServerGuard}; +use serde::{Deserialize, Serialize}; +use servicestack::{JsonServiceClient, ServiceStackRequest, ServiceStackResponse}; + +#[derive(Serialize, Debug)] +struct HelloRequest { + name: String, +} + +impl ServiceStackRequest for HelloRequest { + type Response = HelloResponse; + + fn path(&self) -> String { + "/hello".to_string() + } +} + +#[derive(Deserialize, Debug, PartialEq)] +struct HelloResponse { + result: String, +} + +impl ServiceStackResponse for HelloResponse {} + +#[tokio::test] +async fn test_post_request() { + let mut server = Server::new_async().await; + let mock = server + .mock("POST", "/hello") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(r#"{"result":"Hello, World!"}"#) + .create_async() + .await; + + let client = JsonServiceClient::new(server.url()); + let request = HelloRequest { + name: "World".to_string(), + }; + + let response: HelloResponse = client.post(request).await.unwrap(); + assert_eq!(response.result, "Hello, World!"); + mock.assert_async().await; +} + +#[tokio::test] +async fn test_get_request() { + let mut server = Server::new_async().await; + let mock = server + .mock("GET", "/hello") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(r#"{"result":"Hello, World!"}"#) + .create_async() + .await; + + let client = JsonServiceClient::new(server.url()); + let request = HelloRequest { + name: "World".to_string(), + }; + + let response: HelloResponse = client.get(request).await.unwrap(); + assert_eq!(response.result, "Hello, World!"); + mock.assert_async().await; +} + +#[tokio::test] +async fn test_bearer_token_authentication() { + let mut server = Server::new_async().await; + let mock = server + .mock("POST", "/hello") + .match_header("Authorization", "Bearer test-token-123") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(r#"{"result":"Authenticated!"}"#) + .create_async() + .await; + + let mut client = JsonServiceClient::new(server.url()); + client.set_bearer_token("test-token-123"); + + let request = HelloRequest { + name: "World".to_string(), + }; + + let response: HelloResponse = client.post(request).await.unwrap(); + assert_eq!(response.result, "Authenticated!"); + mock.assert_async().await; +} + +#[tokio::test] +async fn test_api_error_handling() { + let mut server = Server::new_async().await; + let mock = server + .mock("POST", "/hello") + .with_status(404) + .with_body("Not Found") + .create_async() + .await; + + let client = JsonServiceClient::new(server.url()); + let request = HelloRequest { + name: "World".to_string(), + }; + + let result = client.post(request).await; + assert!(result.is_err()); + + if let Err(e) = result { + match e { + servicestack::ServiceStackError::ApiError { status, message } => { + assert_eq!(status, 404); + assert_eq!(message, "Not Found"); + } + _ => panic!("Expected ApiError"), + } + } + mock.assert_async().await; +} + +#[derive(Serialize, Debug)] +struct SearchRequest { + query: String, + limit: u32, +} + +impl ServiceStackRequest for SearchRequest { + type Response = SearchResponse; + + fn path(&self) -> String { + "/search".to_string() + } + + fn method(&self) -> servicestack::HttpMethod { + servicestack::HttpMethod::Put + } +} + +#[derive(Deserialize, Debug, PartialEq)] +struct SearchResponse { + results: Vec, +} + +impl ServiceStackResponse for SearchResponse {} + +#[tokio::test] +async fn test_custom_http_method() { + let mut server = Server::new_async().await; + let mock = server + .mock("PUT", "/search") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(r#"{"results":["result1","result2"]}"#) + .create_async() + .await; + + let client = JsonServiceClient::new(server.url()); + let request = SearchRequest { + query: "test".to_string(), + limit: 10, + }; + + let response: SearchResponse = client.send(request).await.unwrap(); + assert_eq!(response.results, vec!["result1", "result2"]); + mock.assert_async().await; +} + +#[tokio::test] +async fn test_raw_request_method() { + let mut server = Server::new_async().await; + let mock = server + .mock("POST", "/custom") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(r#"{"result":"Custom response"}"#) + .create_async() + .await; + + let client = JsonServiceClient::new(server.url()); + + #[derive(Serialize)] + struct CustomRequest { + data: String, + } + + #[derive(Deserialize)] + struct CustomResponse { + result: String, + } + + let request = CustomRequest { + data: "test".to_string(), + }; + + let response: CustomResponse = client + .request("POST", "/custom", Some(&request)) + .await + .unwrap(); + + assert_eq!(response.result, "Custom response"); + mock.assert_async().await; +}