Rust client

This document describes how to use the Rust client library to communicate with a Volt via websocket.

Install packages

You will need to add the following dependencies to your Cargo.toml file:

[dependencies]
serde_json = "1.0.120"
tokio = "1.38.1"
volt-client-rs = "0.1.12"

The repository is available on GitHub.

Usage

In order to be able to initialise a connection to a Volt, you will need to obtain a client configuration.

How your application obtains or stores the client configuration is not discussed here, needless to say you need to be careful not to expose keys or other sensitive data. For the purposes of this document, we assume the configuration is stored in a local file.

Create a client

Begin by creating a tdx Volt client instance:

use volt_client_rs::{
volt_client::VoltClient,
};
#[tokio::main]
async fn main() {
// Load the configuration JSON from a file in the current directory.
let config_json = match std::fs::read_to_string("volt.config.json") {
Ok(config_json) => config_json,
Err(e) => {
println!("failed to read config file: {:?}", e);
return;
}
};
// Create the Volt client.
let mut client = match VoltClient::create(&config_json).await {
Ok(client) => client,
Err(e) => {
println!("failed to create API client: {:?}", e);
return;
}
};
//
// Issue some API calls here...
//
unary(&mut client).await;
connection(&mut client).await;
streaming(&mut client).await;
// Keep the main thread alive to listen for incoming messages
tokio::signal::ctrl_c()
.await
.expect("Failed to listen for ctrl_c signal");
println!("exiting");
}

Once initialised successfully, you can issue any API call.

Refer to the API documentation for details of the method names and corresponding request and response messages. Each API method expects a JSON object representing the request as input, and returns a JSON object as indicated by the response message type. The JSON representation of the protobuf message format is intuitive.

Unary calls

All unary calls will either resolve with a JSON response object matching that defined by the API protobuf, or reject if there is a problem with the websocket transport or an error status on the response object.

An example of issuing a unary call is shown below:

use serde_json::json;
use volt_client_rs::{
volt_client::VoltClient,
};
pub async fn unary(client: &mut VoltClient) {
// Prepare the unary request.
let sql_request = json!({
"start" : {
"database_id": "@db",
"statement": "select * from netflix_titles limit 100",
}
});
// Issue the unary request.
let execute_json = match client.sql_execute_json(&sql_request, "").await {
Ok(sql_rpc) => sql_rpc,
Err(e) => {
println!("failed to execute SQL: {:?}", e);
return;
}
};
println!(
"received SQL response: {}",
serde_json::to_string_pretty(&execute_json).unwrap()
);
}

Streaming calls

The promise/future model does not fit well with streaming calls since they are long-lived.

For this reason, all streaming calls (client streaming, server streaming, and bi-directional streaming) return a call object that is used to manage the call.

The call object emits events as interactions with the underlying websocket occur, and there are 3 events of interest:

  • “error” emitted when an error occurs on the call
  • “data” emitted when a response is received from the Volt
  • “end” emitted when the call ends

An example of a streaming call is shown below:

use serde_json::json;
use volt_client_rs::{
volt_client::VoltClient,
websocket_rpc::{Event, EventType},
};
pub async fn streaming(client: &mut VoltClient) {
// Prepare the subscribe request.
let subscribe_request = json!({
"wire_id": "@wire",
});
// Issue the subscribe request.
let subscribe_call = match client.subscribe_wire(&subscribe_request).await {
Ok(connect1) => connect1,
Err(e) => {
println!("failed to connect: {:?}", e);
return;
}
};
{
// Register a callback for the `Data` event.
let subscribe_rpc = subscribe_call.lock().await;
subscribe_rpc.on(EventType::Data, |response| {
if let Event::Data(data) = response {
// Do something with the data
println!("received wire response: {:?}", data);
}
});
}
}

Connection

The library supports creating a bi-directional Connect call to the Volt. This can be used to register to receive events from the Volt and to implement services that the Volt or other clients can call.

An example of creating a connection is shown below:

use serde_json::json;
use volt_client_rs::{
volt_client::VoltClient,
websocket_rpc::{Event, EventType},
};
pub async fn connection(client: &mut VoltClient) {
// Start a `connect` call to the Volt.
let connect_call = match client.connect(&serde_json::Value::Null).await {
Ok(connect1) => connect1,
Err(e) => {
println!("failed to connect: {:?}", e);
return;
}
};
{
// Register a callback for the `Data` event.
let connect_rpc = connect_call.lock().await;
connect_rpc.on(EventType::Data, |response| {
if let Event::Data(data) = response {
// Do something with the data
println!("main: received data response: {:?}", data);
}
});
// Send the initial connect request.
let connect_request = json!({
"hello": {
"subscribe_resource_events": true,
"accept_invocation": false,
"ping_interval": 10000,
},
});
let _ = connect_rpc.send(&connect_request).await;
}
}