Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

servicestack-rust

Typed Rust Client Library for consuming ServiceStack APIs.

  • Typed Request/Response DTOs, generated from any ServiceStack API
  • Response Types inferred from the Request DTO — no turbofish, no serde_json::Value
  • Structured ResponseStatus errors with field validation errors
  • Auth with Basic Auth, API Keys, JWT Bearer Tokens, Refresh Tokens and Session Cookies
  • Batched Requests, one-way Requests and multipart file uploads
  • Async by default, with an optional sync client in servicestack::blocking

Install

cargo add servicestack

Or in Cargo.toml:

[dependencies]
servicestack = "0.1"
serde = { version = "1", features = ["derive"] }

Requires Rust 1.88+.

Features

Feature Description
blocking Sync client in servicestack::blocking for CLIs and scripts
multipart multipart/form-data file uploads
servicestack = { version = "0.1", features = ["blocking"] }

Generate Typed DTOs

Generate the Rust DTOs of any ServiceStack API with the get-dtos tool:

npx get-dtos rust https://blazor-vue.web-templates.io

Which downloads a dtos.rs containing the typed DTOs of the remote API:

use servicestack::*;
use serde::{Serialize, Deserialize};

// @Route("/hello/{Name}")
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
#[serde(default)]
pub struct Hello {
    pub name: Option<String>,
}

impl IRequest for Hello {
    const NAME: &'static str = "Hello";
    const VERB: &'static str = "GET";
}
impl IReturn for Hello { type Response = HelloResponse; }

The generated IRequest/IReturn impls are what let the client infer each API's Response Type and the HTTP Method it should be sent with.

Usage

use servicestack::JsonServiceClient;
mod dtos; use dtos::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = JsonServiceClient::new("https://blazor-vue.web-templates.io");

    let res = client.send(&Hello { name: Some("World".into()) }).await?; // res is a HelloResponse
    println!("{}", res.result);
    Ok(())
}

send uses the HTTP Method the API is annotated with, use get, post, put, patch or delete to send a Request DTO with a specific HTTP Method:

let res = client.post(&Hello { name: Some("World".into()) }).await?;

APIs that don't return a Response Body are sent with send_void:

client.send_void(&DeleteBooking { id: 1 }).await?;

Sync client

Enable the blocking feature for the same API without async/await:

use servicestack::blocking::JsonServiceClient;

let client = JsonServiceClient::new("https://blazor-vue.web-templates.io");
let res = client.send(&Hello { name: Some("World".into()) })?;

AutoQuery

AutoQuery APIs return a typed QueryResponse<T>, with the query params of their base type flattened into the Request DTO:

use servicestack::QueryDb;

let res = client.send(&QueryBookings {
    query_db: QueryDb { take: Some(5), order_by_desc: Some("id".into()), ..Default::default() },
    ..Default::default()
}).await?;

for booking in res.results { // booking is a Booking
    println!("{} {}", booking.id, booking.name);
}

Error Handling

Failed API Requests return an Error containing the HTTP Status Code and the API's structured ResponseStatus:

match client.send(&CreateBooking::default()).await {
    Ok(res) => println!("{}", res.id),
    Err(err) => {
        println!("{:?}", err.status_code());         // Some(400)
        println!("{:?}", err.error_code());          // Some("NotEmpty")
        println!("{:?}", err.error_message());       // Some("'Name' must not be empty.")
        println!("{:?}", err.field_error("Name"));   // Some("'Name' must not be empty.")
        println!("{}", err.is_unauthorized());       // false
    }
}

Alternatively api returns errors in its result instead of a separate Err:

let api = client.api(&CreateBooking::default()).await;
if api.failed() {
    println!("{:?} {:?}", api.error_code(), api.field_error("Name"));
} else {
    println!("{}", api.response().unwrap().id);
}

Authentication

API Keys and JWTs are sent in the Bearer Token Authorization header:

let mut client = JsonServiceClient::new("https://example.org");
client.set_bearer_token("ak-87949de37e894627a9f6173154e7cafa");

HTTP Basic Auth credentials:

client.set_credentials("username", "password");

Sign in with ServiceStack's Authenticate API, which maintains the authenticated Session in the client's cookie jar and uses any Bearer Token the Server returns:

let auth = client.authenticate("username", "password").await?;

When a Refresh Token is configured, expired Bearer Tokens are transparently refreshed and the failed Request retried:

client.set_refresh_token(&refresh_token);

Transparently handle 401 Unauthorized Responses

If the Server returns a 401 Unauthorized Response either because the client was unauthenticated or its Bearer Token or API Key had expired, use set_on_authentication_required to re-configure the client before the original Request is automatically retried:

client.set_on_authentication_required(|client| {
    Box::pin(async move {
        client.authenticate("username", "password").await?;
        Ok(())
    })
});

// Automatically retries Requests returning 401 Responses
let res = client.send(&Secured::default()).await?;

A configured Refresh Token takes precedence over the callback, which is only used when no Refresh Token is set or refreshing it failed.

Batched Requests

let responses = client.send_all(&[
    Hello { name: Some("A".into()) },
    Hello { name: Some("B".into()) },
]).await?;

Or send a Request to a one-way endpoint that ignores its Response:

client.publish(&Hello { name: Some("World".into()) }).await?;

File Uploads

Enable the multipart feature:

use servicestack::UploadFile;

let res = client.post_files_with_request(&UploadPhoto { album: "Holiday".into() }, vec![
    UploadFile {
        field_name: "file".into(),
        file_name: "photo.png".into(),
        content_type: Some("image/png".into()),
        data: std::fs::read("photo.png")?,
    },
]).await?;

Custom URLs

let res: HelloResponse = client.get_url("/hello/World").await?;
let res: HelloResponse = client.post_url("/hello", &Hello { name: Some("World".into()) }).await?;
let csv: String = client.get_url_string("/api/QueryBookings.csv").await?;

Client Configuration

let mut client = JsonServiceClient::new("https://example.org");
client.set_header("X-Custom", "Value");
client.set_user_agent("my-app/1.0");

// Or supply a pre-configured reqwest Client for timeouts, proxies and TLS
let http = reqwest::Client::builder()
    .timeout(std::time::Duration::from_secs(30))
    .build()?;
let client = JsonServiceClient::with_client("https://example.org", http);

JsonServiceClient::new sends Requests to ServiceStack's pre-defined /api route. Use JsonServiceClient::new_json_service_client for older ServiceStack instances that only have the /json/reply routes enabled, or set_base_path for a custom base path.

Examples

cargo run --example hello
cargo run --example hello_blocking --features blocking

Tests

cargo test --all-features              # unit tests
cargo test --all-features -- --ignored # integration tests against test.servicestack.net

Releasing

Releases are cut with npm scripts and published by the release GitHub Action:

npm run bump              # 0.1.0 -> 0.1.1 (also `-- minor`, `-- major`, `-- 1.2.3`)
# describe the release in CHANGELOG.md, then
npm run release

Or in a single step:

npm run release -- patch

npm run release tags the version, pushes it and creates the GitHub Release, which triggers the workflow that runs the tests and publishes it to crates.io.

License

BSD-3-Clause. See LICENSE.

About

ServiceStack Client Rust Library

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages