Getting Started with Rust
This guide walks you through building a complete EvidentSource application using the Rust SDK.
Prerequisites
Section titled “Prerequisites”- Rust 1.84 or later
- For WASM components:
rustup target add wasm32-wasip2 - An EvidentSource server instance (local or cloud)
Quick Start with Templates
Section titled “Quick Start with Templates”The fastest way to get started is using the project template with cargo-generate:
# Install cargo-generate if neededcargo install cargo-generate
# Create a new project from templatecargo generate --git https://github.com/evidentsystems/evidentsource-sdks \ rust/functions/templates/evidentsource-state-management \ --name my-appThe template prompts for:
- project_name: Your project name (e.g.,
my-app) - database_name: Target database name (e.g.,
my-database) - first_state_change: Initial state change name (e.g.,
open-account) - first_state_view: Initial state view name (e.g.,
account-summary)
This creates a complete project with:
- domain/ - Shared types for events and commands
- state-changes/ - Example state change component
- state-views/ - Example state view component
- build_all.sh - Build all WASM components
- install.sh - Deploy components to EvidentSource
- CLAUDE.md - AI assistant context for development
cd my-app./build_all.sh # Build WASM components./install.sh --all # Deploy to serverSee the Template Guide for full documentation.
Manual Project Setup
Section titled “Manual Project Setup”If you prefer to set up manually or need a client-only application:
1. Create a New Project
Section titled “1. Create a New Project”cargo new my-banking-appcd my-banking-app2. Add Dependencies
Section titled “2. Add Dependencies”Update Cargo.toml:
[package]name = "my-banking-app"version = "0.1.0"edition = "2021"
[dependencies]evidentsource-client = { git = "https://github.com/evidentsystems/evidentsource-sdks", branch = "main" }tokio = { version = "1", features = ["full"] }serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"uuid = { version = "1.0", features = ["v4"] }
[lib]crate-type = ["cdylib"]
[profile.release]opt-level = "s"lto = trueConnecting to EvidentSource
Section titled “Connecting to EvidentSource”Create a client and connect to the server:
use evidentsource_client::{EvidentSource, Connection};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { // Connect to the server let es = EvidentSource::connect("http://localhost:50051").await?;Authentication
Section titled “Authentication”For production deployments, you’ll need to authenticate with a JWT token. See Authentication for details on the trust model.
use evidentsource_client::{EvidentSource, Credentials};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { // Bearer token authentication (production) let token = std::env::var("EVS_TOKEN")?; let es = EvidentSource::connect_with_auth( "https://api.example.com:50051", Credentials::BearerToken(token), ).await?;
// DevMode for local development (no auth required) let es = EvidentSource::connect("http://localhost:50051").await?;
// Create or connect to a database let db_name = "banking".to_string(); es.create_database(db_name.clone()).await.ok(); // Ignore if exists
let conn = es.connect_database(db_name).await?;
println!("Connected to database at revision {}", conn.revision());
// Execute a state change let db = conn.execute_state_change( "open-account", 1, serde_json::json!({ "accountId": "acct-001", "customerName": "Alice Smith", "initialDeposit": 1000.00 }), ).await?;
println!("New revision: {}", db.revision());
// Query a state view let account = db.view_state::<AccountSummary>( "account-summary", 1, Some(&[("account_id", "acct-001")]), ).await?;
if let Some(acc) = account { println!("Balance: ${:.2}", acc.balance); }
Ok(())}
#[derive(Debug, serde::Deserialize)]struct AccountSummary { id: String, name: String, balance: f64,}Building a State Change
Section titled “Building a State Change”State changes handle commands and emit events. Create a new crate for your state change:
cargo new --lib state-changes/open-accountUpdate state-changes/open-account/Cargo.toml:
[package]name = "open-account"version = "0.1.0"edition = "2021"
[lib]crate-type = ["cdylib"]
[dependencies]evidentsource-functions = { git = "https://github.com/evidentsystems/evidentsource-sdks", branch = "main" }serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"uuid = { version = "1.0", features = ["v4"] }Implement the state change in src/lib.rs:
use evidentsource_functions::prelude::*;use serde::{Deserialize, Serialize};use uuid::Uuid;
#[derive(Debug, Deserialize)]#[serde(rename_all = "camelCase")]struct OpenAccountCommand { account_id: String, customer_name: String, initial_deposit: f64,}
#[derive(Debug, Serialize)]#[serde(rename_all = "camelCase")]struct AccountOpened { account_id: String, customer_name: String,}
#[derive(Debug, Serialize)]#[serde(rename_all = "camelCase")]struct AccountCredited { amount: f64, description: String,}
#[state_change]fn open_account( db: &impl Database, cmd: OpenAccountCommand, _meta: StateChangeMetadata,) -> StateChangeResult { // Validate initial deposit if cmd.initial_deposit < 0.0 { return Err(StateChangeError::validation( "Initial deposit cannot be negative" )); }
// Check if account already exists let existing = db.view_state::<serde_json::Value>( "account-summary", 1, &[("account_id", &cmd.account_id)], )?;
if existing.is_some() { return Err(StateChangeError::conflict("Account already exists")); }
let mut result = DecideResult::new();
// Emit AccountOpened event let opened_event = CloudEventBuilder::new() .id(Uuid::new_v4().to_string()) .event_type("com.banking.account.opened") .subject(&cmd.account_id) .stream("accounts") .json_data(&AccountOpened { account_id: cmd.account_id.clone(), customer_name: cmd.customer_name, })? .build();
result = result.event(opened_event);
// Emit initial deposit if non-zero if cmd.initial_deposit > 0.0 { let credit_event = CloudEventBuilder::new() .id(Uuid::new_v4().to_string()) .event_type("com.banking.account.credited") .subject(&cmd.account_id) .stream("accounts") .json_data(&AccountCredited { amount: cmd.initial_deposit, description: "Initial deposit".to_string(), })? .build();
result = result.event(credit_event); }
// DCB constraint: ensure account doesn't already exist result = result.condition(fail_if_events_match( Subject::equals(&cmd.account_id) .and(EventType::equals("com.banking.account.opened")) ));
Ok(result)}Build the WASM component:
cd state-changes/open-accountcargo build --release --target wasm32-wasip2Building a State View
Section titled “Building a State View”State views materialize read models from events. Create a new crate:
cargo new --lib state-views/account-summaryImplement the state view:
use evidentsource_functions::prelude::*;use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]struct AccountSummary { id: String, name: String, balance: f64, is_open: bool,}
#[state_view]fn account_summary(state: Option<AccountSummary>, events: Vec<Event>) -> Option<AccountSummary> { let mut summary = state.unwrap_or_default();
for event in events { match event.event_type() { "com.banking.account.opened" => { if let Ok(data) = event.parse_data::<AccountOpenedData>() { summary.id = data.account_id; summary.name = data.customer_name; summary.is_open = true; } } "com.banking.account.credited" => { if let Ok(data) = event.parse_data::<AccountCreditedData>() { summary.balance += data.amount; } } "com.banking.account.debited" => { if let Ok(data) = event.parse_data::<AccountDebitedData>() { summary.balance -= data.amount; } } "com.banking.account.closed" => { summary.is_open = false; } _ => {} } }
Some(summary)}
#[derive(Deserialize)]#[serde(rename_all = "camelCase")]struct AccountOpenedData { account_id: String, customer_name: String,}
#[derive(Deserialize)]struct AccountCreditedData { amount: f64,}
#[derive(Deserialize)]struct AccountDebitedData { amount: f64,}Deploying Components
Section titled “Deploying Components”Deploy your WASM components to the EvidentSource server:
# Create databasecurl -X POST http://localhost:8080/api/v1/databases/banking
# Install state changecurl -X POST http://localhost:8080/api/v1/databases/banking/state-changes/open-account/versions/1 \ --data-binary @target/wasm32-wasip2/release/open_account.wasm \ -H "Content-Type: application/wasm"
# Install state viewcurl -X POST http://localhost:8080/api/v1/databases/banking/state-views/account-summary/versions/1 \ --data-binary @target/wasm32-wasip2/release/account_summary.wasm \ -H "Content-Type: application/wasm"Testing
Section titled “Testing”Use the mock database for unit testing:
#[cfg(test)]mod tests { use super::*; use evidentsource_functions::testing::MockDatabase;
#[test] fn test_open_account_success() { let db = MockDatabase::new("test-db").with_revision(100);
let cmd = OpenAccountCommand { account_id: "acct-001".to_string(), customer_name: "Alice".to_string(), initial_deposit: 100.0, };
let result = open_account(&db, cmd, StateChangeMetadata::default());
assert!(result.is_ok()); let decide = result.unwrap(); assert_eq!(decide.events.len(), 2); // opened + credited }
#[test] fn test_open_account_negative_deposit() { let db = MockDatabase::new("test-db");
let cmd = OpenAccountCommand { account_id: "acct-001".to_string(), customer_name: "Alice".to_string(), initial_deposit: -50.0, };
let result = open_account(&db, cmd, StateChangeMetadata::default());
assert!(result.is_err()); }}Next Steps
Section titled “Next Steps”- Explore the Savings Account example for a complete banking application
- Learn about bi-temporal queries for historical analysis
- Read about DCB constraints for optimistic concurrency control