diff --git a/.gitignore b/.gitignore index ad67955..2ca44c9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # will have compiled files and executables debug target +Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk @@ -19,3 +20,8 @@ target # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + + +# Added by cargo + +/target diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..df30d58 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog + +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] - 2025-11-03 + +### Added +- Initial release of ServiceStack Rust HTTP Client Library +- Core `ServiceStackClient` for making HTTP requests +- Support for all HTTP methods: GET, POST, PUT, DELETE, PATCH +- Async/await support with tokio +- JSON serialization/deserialization with serde +- Type-safe request/response handling +- Comprehensive documentation and examples +- Error handling with custom error types +- Basic usage example demonstrating client functionality + +[0.1.0]: https://github.com/ServiceStack/servicestack-rust/releases/tag/v0.1.0 diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8f3a14f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +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" +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"] + +[dependencies] +reqwest = { version = "0.12", features = ["json"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tokio = { version = "1.0", features = ["full"] } +thiserror = "1.0" + +[dev-dependencies] +tokio-test = "0.4" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7d2e051 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2025, ServiceStack, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +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. diff --git a/PUBLISHING.md b/PUBLISHING.md new file mode 100644 index 0000000..5e6fca1 --- /dev/null +++ b/PUBLISHING.md @@ -0,0 +1,102 @@ +# Publishing to crates.io + +This document describes how to publish the ServiceStack Rust client library to crates.io. + +## Prerequisites + +1. Create an account on [crates.io](https://crates.io/) +2. Get your API token from [crates.io/me](https://crates.io/me) +3. Configure your token locally: + ```bash + cargo login + ``` + +## Pre-publication Checklist + +Before publishing, ensure: + +- [ ] All tests pass: `cargo test` +- [ ] Documentation builds without warnings: `cargo doc --no-deps` +- [ ] Package builds successfully: `cargo package` +- [ ] Version number is correct in `Cargo.toml` +- [ ] `CHANGELOG.md` is updated +- [ ] `README.md` is accurate and up-to-date +- [ ] License file exists and is correct +- [ ] All code is committed and pushed to GitHub + +## Package Verification + +1. Build and test the package: + ```bash + cargo build + cargo test + ``` + +2. Verify the package contents: + ```bash + cargo package --list + ``` + +3. Build the package to check for issues: + ```bash + cargo package + ``` + +4. Test the packaged version: + ```bash + cargo package + cd target/package + cargo test + ``` + +## Publishing + +Once all checks pass, publish to crates.io: + +```bash +cargo publish +``` + +This will: +1. Package your crate +2. Upload it to crates.io +3. Make it available for download via `cargo install` + +## Post-Publication + +After publishing: + +1. Create a GitHub release: + - Tag: `v0.1.0` + - Title: `ServiceStack Rust v0.1.0` + - Description: Copy from CHANGELOG.md + +2. Verify the package appears on crates.io: + - Visit https://crates.io/crates/servicestack + - Check that documentation is generated at https://docs.rs/servicestack + +3. Test installation: + ```bash + cargo new test-project + cd test-project + cargo add servicestack + cargo build + ``` + +## Version Updates + +For future releases: + +1. Update version in `Cargo.toml` +2. Update `CHANGELOG.md` with new changes +3. Commit and push changes +4. Run through the pre-publication checklist +5. Publish with `cargo publish` +6. Create a new GitHub release + +## Troubleshooting + +- **Publishing fails**: Check that all required fields in `Cargo.toml` are filled +- **Documentation fails to build**: Run `cargo doc` locally to see errors +- **Tests fail**: Run `cargo test` to identify and fix failing tests +- **Version conflict**: Ensure version number is incremented from last published version diff --git a/README.md b/README.md index 121c2b2..2a67261 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,138 @@ -# servicestack-rust -ServiceStack Client Rust Library +# ServiceStack Rust Client Library + +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) +[![Documentation](https://docs.rs/servicestack/badge.svg)](https://docs.rs/servicestack) +[![License](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](LICENSE) + +## Features + +- 🚀 **Async/await support** - Built on tokio for efficient async operations +- 📦 **Type-safe** - Leverages Rust's type system with serde for serialization +- 🔧 **Flexible** - Support for all HTTP methods (GET, POST, PUT, DELETE, PATCH) +- 🛡️ **Reliable** - Built on reqwest for robust HTTP communication +- 📖 **Well-documented** - Comprehensive API documentation and examples + +## Installation + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +servicestack = "0.1.0" +tokio = { version = "1.0", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +``` + +## Quick Start + +```rust +use servicestack::{ServiceStackClient, Result}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize)] +struct HelloRequest { + name: String, +} + +#[derive(Deserialize)] +struct HelloResponse { + result: String, +} + +#[tokio::main] +async fn main() -> Result<()> { + // Create a new client + let client = ServiceStackClient::new("https://example.org"); + + // Make a POST request + let request = HelloRequest { + name: "World".to_string() + }; + let response: HelloResponse = client.post("/hello", &request).await?; + + println!("{}", response.result); + Ok(()) +} +``` + +## Usage Examples + +### GET Request + +```rust +use servicestack::{ServiceStackClient, Result}; +use serde::Deserialize; + +#[derive(Deserialize)] +struct User { + id: u64, + name: String, +} + +async fn get_user(client: &ServiceStackClient, id: u64) -> Result { + client.get(&format!("/users/{}", id)).await +} +``` + +### POST Request + +```rust +use servicestack::{ServiceStackClient, Result}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize)] +struct CreateUserRequest { + name: String, + email: String, +} + +#[derive(Deserialize)] +struct CreateUserResponse { + id: u64, + name: String, +} + +async fn create_user(client: &ServiceStackClient) -> Result { + let request = CreateUserRequest { + name: "John Doe".to_string(), + email: "john@example.com".to_string(), + }; + client.post("/users", &request).await +} +``` + +### Custom Client Configuration + +```rust +use servicestack::ServiceStackClient; +use reqwest::Client; +use std::time::Duration; + +let custom_client = Client::builder() + .timeout(Duration::from_secs(60)) + .build() + .unwrap(); + +let client = ServiceStackClient::with_client( + "https://api.example.com", + custom_client +); +``` + +## API Documentation + +For detailed API documentation, visit [docs.rs/servicestack](https://docs.rs/servicestack). + +## License + +This project is licensed under the BSD-3-Clause License - see the LICENSE file for details. + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## About ServiceStack + +[ServiceStack](https://servicestack.net) is a simple, fast, versatile and highly-productive full-featured Web and Web Services Framework. diff --git a/examples/basic_usage.rs b/examples/basic_usage.rs new file mode 100644 index 0000000..b3c774c --- /dev/null +++ b/examples/basic_usage.rs @@ -0,0 +1,44 @@ +//! Basic usage example for the ServiceStack client library +//! +//! This example demonstrates how to use the ServiceStack client +//! to make HTTP requests to a ServiceStack service. + +use servicestack::{ServiceStackClient, Result}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Debug)] +struct HelloRequest { + name: String, +} + +#[derive(Deserialize, Debug)] +struct HelloResponse { + result: String, +} + +#[tokio::main] +async fn main() -> Result<()> { + // Create a new ServiceStack client + let client = ServiceStackClient::new("https://test.servicestack.net"); + + // Example 1: Simple GET request + println!("Example 1: GET request"); + match client.get::("/hello/World").await { + Ok(response) => println!("Response: {:?}", response), + Err(e) => eprintln!("Error: {}", e), + } + + // Example 2: POST request with JSON body + println!("\nExample 2: POST request"); + let request = HelloRequest { + name: "Rust".to_string(), + }; + + match client.post::<_, HelloResponse>("/hello", &request).await { + Ok(response) => println!("Response: {:?}", response), + Err(e) => eprintln!("Error: {}", e), + } + + println!("\nExamples completed!"); + Ok(()) +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..eaaecb7 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,29 @@ +//! Error types for ServiceStack client + +use thiserror::Error; + +/// Result type alias for ServiceStack operations +pub type Result = std::result::Result; + +/// Error types that can occur when using the ServiceStack client +#[derive(Error, Debug)] +pub enum Error { + /// HTTP request error + #[error("HTTP request failed: {0}")] + Request(#[from] reqwest::Error), + + /// JSON serialization/deserialization error + #[error("JSON error: {0}")] + Json(#[from] serde_json::Error), + + /// Generic error with custom message + #[error("{0}")] + Message(String), +} + +impl Error { + /// Create a new error with a custom message + pub fn message>(msg: S) -> Self { + Error::Message(msg.into()) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..44af06d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,242 @@ +//! # ServiceStack Rust Client Library +//! +//! A Rust client library for ServiceStack services. +//! +//! ## Features +//! +//! - Async/await support with tokio +//! - JSON serialization with serde +//! - Type-safe request/response handling +//! - Built on reqwest for reliable HTTP communication +//! +//! ## Example +//! +//! ```no_run +//! use servicestack::{ServiceStackClient, Result}; +//! use serde::{Deserialize, Serialize}; +//! +//! #[derive(Serialize)] +//! struct HelloRequest { +//! name: String, +//! } +//! +//! #[derive(Deserialize)] +//! struct HelloResponse { +//! result: String, +//! } +//! +//! #[tokio::main] +//! async fn main() -> Result<()> { +//! let client = ServiceStackClient::new("https://example.org"); +//! let request = HelloRequest { name: "World".to_string() }; +//! let response: HelloResponse = client.post("/hello", &request).await?; +//! println!("{}", response.result); +//! Ok(()) +//! } +//! ``` + +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +pub mod error; +pub use error::{Error, Result}; + +/// ServiceStack HTTP client for making requests to ServiceStack services +#[derive(Debug, Clone)] +pub struct ServiceStackClient { + base_url: String, + client: Client, +} + +impl ServiceStackClient { + /// Create a new ServiceStack client with the given base URL + /// + /// # Arguments + /// + /// * `base_url` - The base URL of the ServiceStack service (e.g., ) + /// + /// # Example + /// + /// ``` + /// use servicestack::ServiceStackClient; + /// + /// let client = ServiceStackClient::new("https://api.example.com"); + /// ``` + pub fn new>(base_url: S) -> Self { + let client = Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .expect("Failed to build HTTP client"); + + Self { + base_url: base_url.into(), + client, + } + } + + /// Build a full URL from the base URL and path + fn build_url(&self, path: &str) -> String { + format!("{}{}", self.base_url, path) + } + + /// Send a request and deserialize the JSON response + async fn send_json(&self, response: reqwest::Response) -> Result + where + T: for<'de> Deserialize<'de>, + { + let response = response.error_for_status()?; + let result = response.json::().await?; + Ok(result) + } + + /// Create a new ServiceStack client with a custom reqwest Client + /// + /// # Arguments + /// + /// * `base_url` - The base URL of the ServiceStack service + /// * `client` - A pre-configured reqwest Client + /// + /// # Example + /// + /// ``` + /// use servicestack::ServiceStackClient; + /// use reqwest::Client; + /// + /// let custom_client = Client::builder() + /// .timeout(std::time::Duration::from_secs(60)) + /// .build() + /// .unwrap(); + /// + /// let client = ServiceStackClient::with_client("https://api.example.com", custom_client); + /// ``` + pub fn with_client>(base_url: S, client: Client) -> Self { + Self { + base_url: base_url.into(), + client, + } + } + + /// Make a GET request to the specified path + /// + /// # Arguments + /// + /// * `path` - The API endpoint path (e.g., "/hello/World") + /// + /// # Returns + /// + /// A deserialized response of type `T` + pub async fn get(&self, path: &str) -> Result + where + T: for<'de> Deserialize<'de>, + { + let url = self.build_url(path); + let response = self.client.get(&url).send().await?; + self.send_json(response).await + } + + /// Make a POST request with a JSON body + /// + /// # Arguments + /// + /// * `path` - The API endpoint path (e.g., "/hello") + /// * `body` - The request body to be serialized as JSON + /// + /// # Returns + /// + /// A deserialized response of type `T` + pub async fn post(&self, path: &str, body: &S) -> Result + where + S: Serialize, + T: for<'de> Deserialize<'de>, + { + let url = self.build_url(path); + let response = self.client.post(&url).json(body).send().await?; + self.send_json(response).await + } + + /// Make a PUT request with a JSON body + /// + /// # Arguments + /// + /// * `path` - The API endpoint path + /// * `body` - The request body to be serialized as JSON + /// + /// # Returns + /// + /// A deserialized response of type `T` + pub async fn put(&self, path: &str, body: &S) -> Result + where + S: Serialize, + T: for<'de> Deserialize<'de>, + { + let url = self.build_url(path); + let response = self.client.put(&url).json(body).send().await?; + self.send_json(response).await + } + + /// Make a DELETE request + /// + /// # Arguments + /// + /// * `path` - The API endpoint path + /// + /// # Returns + /// + /// A deserialized response of type `T` + pub async fn delete(&self, path: &str) -> Result + where + T: for<'de> Deserialize<'de>, + { + let url = self.build_url(path); + let response = self.client.delete(&url).send().await?; + self.send_json(response).await + } + + /// Make a PATCH request with a JSON body + /// + /// # Arguments + /// + /// * `path` - The API endpoint path + /// * `body` - The request body to be serialized as JSON + /// + /// # Returns + /// + /// A deserialized response of type `T` + pub async fn patch(&self, path: &str, body: &S) -> Result + where + S: Serialize, + T: for<'de> Deserialize<'de>, + { + let url = self.build_url(path); + let response = self.client.patch(&url).json(body).send().await?; + self.send_json(response).await + } + + /// Get the base URL of this client + pub fn base_url(&self) -> &str { + &self.base_url + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_client_creation() { + let client = ServiceStackClient::new("https://api.example.com"); + assert_eq!(client.base_url(), "https://api.example.com"); + } + + #[test] + fn test_client_with_custom_client() { + let custom_client = Client::builder() + .timeout(Duration::from_secs(60)) + .build() + .unwrap(); + + let client = ServiceStackClient::with_client("https://api.example.com", custom_client); + assert_eq!(client.base_url(), "https://api.example.com"); + } +}