Skip to content

Commit 6d48ab9

Browse files
Copilotmythz
andcommitted
Implement ServiceStack JsonServiceClient for Rust with typed DTOs
Co-authored-by: mythz <[email protected]>
1 parent 60ea71e commit 6d48ab9

File tree

13 files changed

+3129
-2
lines changed

13 files changed

+3129
-2
lines changed

Cargo.lock

Lines changed: 1838 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "servicestack"
3+
version = "0.1.0"
4+
edition = "2021"
5+
authors = ["ServiceStack"]
6+
description = "ServiceStack Rust Client Library - JsonServiceClient for making typed API requests"
7+
license = "MIT"
8+
repository = "https://github.com/ServiceStack/servicestack-rust"
9+
keywords = ["servicestack", "rest", "api", "client", "json"]
10+
categories = ["web-programming::http-client"]
11+
12+
[dependencies]
13+
reqwest = { version = "0.12", features = ["json"] }
14+
serde = { version = "1.0", features = ["derive"] }
15+
serde_json = "1.0"
16+
tokio = { version = "1", features = ["full"] }
17+
thiserror = "1.0"
18+
async-trait = "0.1"
19+
20+
[dev-dependencies]
21+
tokio-test = "0.4"
22+
mockito = "1.4"

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 ServiceStack
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 244 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,244 @@
1-
# servicestack-rust
2-
ServiceStack Client Rust Library
1+
# ServiceStack Rust Client Library
2+
3+
A Rust client library for making typed API requests to [ServiceStack](https://servicestack.net/) services.
4+
5+
## Features
6+
7+
- 🚀 **Type-safe API requests** - Use Rust DTOs for compile-time safety
8+
- 🔄 **Full HTTP method support** - GET, POST, PUT, DELETE, PATCH
9+
- 🔐 **Bearer token authentication** - Built-in support for authentication
10+
-**Async/await** - Built on tokio and reqwest for async operations
11+
- 🎯 **ServiceStack conventions** - Follows ServiceStack's REST API patterns
12+
- 🛠️ **Customizable** - Flexible configuration options
13+
14+
## Installation
15+
16+
Add this to your `Cargo.toml`:
17+
18+
```toml
19+
[dependencies]
20+
servicestack = "0.1"
21+
serde = { version = "1.0", features = ["derive"] }
22+
tokio = { version = "1", features = ["full"] }
23+
```
24+
25+
## Quick Start
26+
27+
```rust
28+
use serde::{Deserialize, Serialize};
29+
use servicestack::{JsonServiceClient, ServiceStackRequest, ServiceStackResponse};
30+
31+
// Define your request DTO
32+
#[derive(Serialize)]
33+
struct Hello {
34+
name: String,
35+
}
36+
37+
// Implement ServiceStackRequest trait
38+
impl ServiceStackRequest for Hello {
39+
type Response = HelloResponse;
40+
41+
fn path(&self) -> String {
42+
"/hello".to_string()
43+
}
44+
}
45+
46+
// Define your response DTO
47+
#[derive(Deserialize, Debug)]
48+
struct HelloResponse {
49+
result: String,
50+
}
51+
52+
impl ServiceStackResponse for HelloResponse {}
53+
54+
#[tokio::main]
55+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
56+
// Create a client
57+
let client = JsonServiceClient::new("https://test.servicestack.net");
58+
59+
// Make a request
60+
let request = Hello { name: "World".to_string() };
61+
let response: HelloResponse = client.post(request).await?;
62+
63+
println!("Response: {:?}", response);
64+
Ok(())
65+
}
66+
```
67+
68+
## Usage
69+
70+
### Creating a Client
71+
72+
```rust
73+
use servicestack::JsonServiceClient;
74+
75+
// Basic client
76+
let client = JsonServiceClient::new("https://api.example.com");
77+
78+
// With custom reqwest client
79+
let http_client = reqwest::Client::builder()
80+
.timeout(std::time::Duration::from_secs(60))
81+
.build()?;
82+
let client = JsonServiceClient::with_client("https://api.example.com", http_client);
83+
```
84+
85+
### Making Requests
86+
87+
#### POST Request
88+
89+
```rust
90+
let request = Hello { name: "Rust".to_string() };
91+
let response: HelloResponse = client.post(request).await?;
92+
```
93+
94+
#### GET Request
95+
96+
```rust
97+
let request = GetUser { id: 123 };
98+
let response: UserResponse = client.get(request).await?;
99+
```
100+
101+
#### PUT Request
102+
103+
```rust
104+
let request = UpdateUser { id: 123, name: "John".to_string() };
105+
let response: UpdateResponse = client.put(request).await?;
106+
```
107+
108+
#### DELETE Request
109+
110+
```rust
111+
let request = DeleteUser { id: 123 };
112+
let response: DeleteResponse = client.delete(request).await?;
113+
```
114+
115+
#### Using the request method specified in the DTO
116+
117+
```rust
118+
// The DTO can specify its own HTTP method
119+
impl ServiceStackRequest for MyRequest {
120+
type Response = MyResponse;
121+
122+
fn path(&self) -> String {
123+
"/my-endpoint".to_string()
124+
}
125+
126+
fn method(&self) -> HttpMethod {
127+
HttpMethod::Put // Custom method
128+
}
129+
}
130+
131+
// Use the send method to use the DTO's specified method
132+
let response: MyResponse = client.send(request).await?;
133+
```
134+
135+
### Authentication
136+
137+
```rust
138+
// Set bearer token
139+
let mut client = JsonServiceClient::new("https://api.example.com");
140+
client.set_bearer_token("your-token-here");
141+
142+
// Make authenticated requests
143+
let response = client.post(request).await?;
144+
145+
// Clear token
146+
client.clear_bearer_token();
147+
```
148+
149+
### Error Handling
150+
151+
```rust
152+
use servicestack::ServiceStackError;
153+
154+
match client.post(request).await {
155+
Ok(response) => println!("Success: {:?}", response),
156+
Err(ServiceStackError::ApiError { status, message }) => {
157+
println!("API error {}: {}", status, message);
158+
}
159+
Err(ServiceStackError::RequestError(e)) => {
160+
println!("Request error: {}", e);
161+
}
162+
Err(e) => println!("Other error: {}", e),
163+
}
164+
```
165+
166+
### Custom HTTP Methods
167+
168+
You can specify custom HTTP methods by implementing the `method()` function in your request DTO:
169+
170+
```rust
171+
use servicestack::HttpMethod;
172+
173+
impl ServiceStackRequest for CustomRequest {
174+
type Response = CustomResponse;
175+
176+
fn path(&self) -> String {
177+
"/custom".to_string()
178+
}
179+
180+
fn method(&self) -> HttpMethod {
181+
HttpMethod::Patch
182+
}
183+
}
184+
```
185+
186+
### Raw Requests
187+
188+
For more control, you can use the raw request method:
189+
190+
```rust
191+
let response: MyResponse = client
192+
.request("POST", "/custom-endpoint", Some(&request_body))
193+
.await?;
194+
```
195+
196+
## Examples
197+
198+
See the [examples](examples/) directory for more usage examples:
199+
200+
- [basic_usage.rs](examples/basic_usage.rs) - Simple request/response example
201+
- [authenticated_request.rs](examples/authenticated_request.rs) - Using bearer token authentication
202+
- [custom_method.rs](examples/custom_method.rs) - Custom HTTP methods
203+
204+
Run examples with:
205+
206+
```bash
207+
cargo run --example basic_usage
208+
```
209+
210+
## Testing
211+
212+
Run the test suite:
213+
214+
```bash
215+
cargo test
216+
```
217+
218+
Run with output:
219+
220+
```bash
221+
cargo test -- --nocapture
222+
```
223+
224+
## API Documentation
225+
226+
Generate and view the documentation:
227+
228+
```bash
229+
cargo doc --open
230+
```
231+
232+
## ServiceStack DTOs
233+
234+
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.
235+
236+
Learn more about ServiceStack's typed client patterns at [docs.servicestack.net](https://docs.servicestack.net/).
237+
238+
## License
239+
240+
This project is licensed under the MIT License.
241+
242+
## Contributing
243+
244+
Contributions are welcome! Please feel free to submit a Pull Request.

examples/authenticated_request.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use serde::{Deserialize, Serialize};
2+
use servicestack::{JsonServiceClient, ServiceStackRequest, ServiceStackResponse};
3+
4+
/// Example authenticated request
5+
#[derive(Serialize, Debug)]
6+
struct SecureRequest {
7+
message: String,
8+
}
9+
10+
impl ServiceStackRequest for SecureRequest {
11+
type Response = SecureResponse;
12+
13+
fn path(&self) -> String {
14+
"/secure/data".to_string()
15+
}
16+
}
17+
18+
#[derive(Deserialize, Debug)]
19+
struct SecureResponse {
20+
data: String,
21+
}
22+
23+
impl ServiceStackResponse for SecureResponse {}
24+
25+
#[tokio::main]
26+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
27+
// Create a new JsonServiceClient
28+
let mut client = JsonServiceClient::new("https://api.example.com");
29+
30+
// Set Bearer token for authentication
31+
client.set_bearer_token("your-bearer-token-here");
32+
33+
// Create a request
34+
let request = SecureRequest {
35+
message: "Secure message".to_string(),
36+
};
37+
38+
// Make an authenticated POST request
39+
println!("Making authenticated request...");
40+
match client.post(request).await {
41+
Ok(response) => {
42+
println!("Success! Response: {:?}", response);
43+
}
44+
Err(e) => {
45+
println!("Error: {}", e);
46+
}
47+
}
48+
49+
Ok(())
50+
}

examples/basic_usage.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use serde::{Deserialize, Serialize};
2+
use servicestack::{JsonServiceClient, ServiceStackRequest, ServiceStackResponse};
3+
4+
/// Example request DTO
5+
#[derive(Serialize, Debug)]
6+
struct HelloRequest {
7+
name: String,
8+
}
9+
10+
/// Implement ServiceStackRequest trait to define the response type and endpoint path
11+
impl ServiceStackRequest for HelloRequest {
12+
type Response = HelloResponse;
13+
14+
fn path(&self) -> String {
15+
"/hello".to_string()
16+
}
17+
}
18+
19+
/// Example response DTO
20+
#[derive(Deserialize, Debug)]
21+
struct HelloResponse {
22+
result: String,
23+
}
24+
25+
/// Implement ServiceStackResponse trait to enable deserialization
26+
impl ServiceStackResponse for HelloResponse {}
27+
28+
#[tokio::main]
29+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
30+
// Create a new JsonServiceClient with base URL
31+
let client = JsonServiceClient::new("https://test.servicestack.net");
32+
33+
// Create a request
34+
let request = HelloRequest {
35+
name: "Rust".to_string(),
36+
};
37+
38+
// Make a POST request
39+
println!("Making request to ServiceStack API...");
40+
match client.post(request).await {
41+
Ok(response) => {
42+
println!("Success! Response: {:?}", response);
43+
}
44+
Err(e) => {
45+
println!("Error: {}", e);
46+
}
47+
}
48+
49+
Ok(())
50+
}

0 commit comments

Comments
 (0)