Rust
The Rust client is built on tonic and Tokio. It exposes async IngestClient, OutletClient, and (in the typed wrappers) per-API methods.
- Runtime crate:
dataspine-client(depends ontonic,prost,tokio,uuid,config). ProvidesConfig,ConfigLoader,IngestClient,OutletClient, the bidirectional stream adapters that handle chunking and reauthentication, and theapply_authenticationhelper that injectsAuthorization: Bearermetadata. - Per-data-product crate: emitted by the Dataspine code generator. The placeholder used throughout this page is the crate
acme-orders-client, exposing the typedOrdersClientfor thecom.acme.ordersdata product owned by an organizationacme.
Installation
[dependencies]
dataspine-client = "0.1"
acme-orders-client = "0.1"
tokio = { version = "1", features = ["full"] }
uuid = { version = "1", features = ["v4"] }
The per-data-product crate has the runtime crate as a dependency; pin compatible versions of both.
Configuration
The runtime library reads configuration from (in priority order):
~/.dataspine/configand~/.dataspine/ingest.config.dataspine.configand.dataspine.ingest.configin the current working directory- environment variables prefixed
DATASPINE_
Fields:
region(required)client_name(optional, used for logs)endpoint_url(optional, defaults tohttps://api.{region}.dataspine.tech)- Auth — exactly one of:
token = "..."— a static JWT.token_file = "/path/to/token"— file-backed token, refreshed every 60 seconds.ssh_ed25519_token_exchange_endpoint = "...",ssh_ed25519_principal_id = "...",ssh_ed25519_private_key_file = "...",ssh_ed25519_public_key_file = "..."— SSH-Ed25519 PoP token exchange.
# .dataspine.config
region = "eu-central-1"
client_name = "orders-app"
token_file = "/var/run/secrets/dataspine/token"
use dataspine_client::config::{behavior_version::BehaviorVersion, config_loader::ConfigLoader};
let config = ConfigLoader::new(BehaviorVersion::latest()).load().await?;
config carries an authentication_provider (a tokio::sync::watch::Receiver<AuthenticationStatus>) that all clients subscribe to so a token refresh is observed by every active stream.
Typed client
The generator emits one struct per data product:
use acme_orders_client::OrdersClient;
use uuid::Uuid;
let data_product_id = Uuid::parse_str("00000000-0000-0000-0000-000000000000")?;
let mut orders = OrdersClient::new(&config, data_product_id).await?;
It builds the underlying IngestClient and OutletClient and pre-resolves the operation IDs declared by the data product manifest.
API call
use acme_orders_client::operations::{place_order::PlaceOrder, place_order::Order};
let response = orders
.place_order(PlaceOrder {
customer_id: Uuid::parse_str("00000000-0000-0000-0000-000000000000")?,
items: vec![place_order::Item { sku: "BOOK-001".into(), quantity: 1 }],
})
.await?;
match response {
Ok(success) => println!("placed: {}", success.payload.id),
Err(error) => eprintln!("failed: {} {}", error.status as i32, error.message),
}
The typed wrapper returns Result<ApiSuccessResponse<T>, ApiErrorResponse>. The error variant carries an ApiErrorCode that maps the ApiStatusCode values in the gRPC wire reference.
Ingest
use dataspine_client::ingest_client::ingest_client::IngestClient;
use acme_orders_client::types::OrderEvent;
let mut channel = orders.open_order_event_ingest().await?;
let (mut tx, mut rx) = channel.create_batch_channel(()).await?;
tx.send(OrderEvent { /* ... */ }).await?;
tx.send(OrderEvent { /* ... */ }).await?;
tx.end().await?;
while let Some(response) = rx.recv().await {
println!("delivery result: {:?}", response);
}
channel.join().await?;
AppendMessageChannel multiplexes many batches over one bidirectional gRPC stream. create_batch_channel returns a (BatchChannelSender<O>, BatchChannelReceiver<O>) pair where the per-batch O is opaque metadata you supply (e.g. an offset to commit on success).
Outlet
use dataspine_client::outlet_client::outlet_client::{OffsetType, OutletClient};
use futures_util::StreamExt;
let mut outlet = orders.subscribe_to_order_events().await?;
let mut stream = outlet.poll(orders.order_events_outlet_version_id(), OffsetType::Latest).await?;
while let Some(event) = stream.next().await {
let event: OrderEvent = event?;
println!("{event:?}");
}
JoiningStream joins chunked frames into single logical messages. The poll honors back-pressure: it only requests the next frame after the consumer pulls the previous one.
Reauthentication
The runtime watches Config::authentication_provider. When the watch channel ticks with a new token, every active ingest and outlet stream pushes a Reauthenticate envelope (see the gRPC wire reference) so the server can re-validate without dropping the stream. Unary API calls always pick up the latest token because apply_authentication reads from the same channel.
Shutdown
Drop the OrdersClient (or its constituent IngestClient / OutletClient). Background tasks for reauth and the multiplexer terminate when their channels close.
REST
Today the Rust client uses gRPC for all API operations. There is no equivalent of the TypeScript RestApiClient. If you need to invoke a supports_raw_response operation from Rust today, use reqwest (or another HTTP client) against the REST wire reference (see raw response operations). A typed wrapper for raw responses is on the roadmap.