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;use evidentsource_core::domain::DatabaseName;
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { // Connect to the server let es = EvidentSource::connect_to_server("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};use evidentsource_client::client::DatabaseAtRevisionTyped;use evidentsource_core::domain::{DatabaseName, StateViewName, EventAttribute};
#[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_to_server("http://localhost:50051").await?;
// Create or connect to a database let db_name = DatabaseName::new("banking")?; es.create_database(db_name.clone()).await.ok(); // Ignore if exists
let conn = es.connect(&db_name).await?;
println!("Connected to database at revision {}", conn.local_database().revision());
// Execute a state change using builder pattern let db = conn .state_change("open-account", 1) .json(&serde_json::json!({ "accountId": "acct-001", "customerName": "Alice Smith", "initialDeposit": 1000.00 }))? .execute() .await?;
println!("New revision: {}", db.revision());
// Query a state view let sv_name = StateViewName::new("account-summary")?; let account: Option<AccountSummary> = db.view_state_opt(&sv_name, 1).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};
// Command type - parsed from JSON request body#[derive(Debug, Deserialize)]#[serde(rename_all = "camelCase")]pub struct OpenAccountCommand { pub account_id: String, pub customer_name: String, pub initial_deposit: f64,}
// Implement TryFrom<Command> to parse the commandimpl TryFrom<Command> for OpenAccountCommand { type Error = String; fn try_from(cmd: Command) -> Result<Self, Self::Error> { serde_json::from_slice(cmd.body()) .map_err(|e| format!("Failed to parse command: {}", e)) }}
// Domain events - implement Into<ProspectiveEvent> for each#[derive(Debug, Serialize)]#[serde(rename_all = "camelCase")]pub struct AccountOpened { pub account_id: String, pub customer_name: String,}
#[derive(Debug, Serialize)]#[serde(rename_all = "camelCase")]pub struct AccountCredited { pub account_id: String, pub amount: f64, pub description: String,}
// The state change function - annotated with the macro#[evidentsource_functions::state_change(name = "open-account")]fn decide( db: &Database, cmd: OpenAccountCommand, _metadata: &StateChangeMetadata,) -> Result<(NonEmpty<impl Into<ProspectiveEvent>>, Vec<AppendCondition>), StateChangeError> { // Validate initial deposit if cmd.initial_deposit <= 0.0 { return Err(StateChangeError::Validation( "Initial deposit must be greater than zero".into(), )); }
// Check if account already exists via state view let existing = db.view_state("account-summary", 1, &[("account_id", &cmd.account_id)])?; if existing.is_some() { return Err(StateChangeError::Conflict("Account already exists".into())); }
// Build domain events let events = vec![ AccountOpened { account_id: cmd.account_id.clone(), customer_name: cmd.customer_name, }.into_prospective_event("com.banking.account.opened", &cmd.account_id), AccountCredited { account_id: cmd.account_id.clone(), amount: cmd.initial_deposit, description: "Initial deposit".into(), }.into_prospective_event("com.banking.account.credited", &cmd.account_id), ];
// DCB constraint: account must not already exist let constraints = vec![ AppendCondition::max( EventSelector::subject_equals(&cmd.account_id) .map_err(|e| StateChangeError::Internal(e.to_string()))? .and( EventSelector::event_type_equals("com.banking.account.opened") .map_err(|e| StateChangeError::Internal(e.to_string()))?, ), 0, // Max 0 matching events = must not exist ), ];
Ok((NonEmpty::from_vec(events).expect("Events produced"), constraints))}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};
// State type - serialized to/from JSON#[derive(Debug, Default, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]pub struct AccountSummary { pub id: String, pub name: String, pub balance: f64, pub is_open: bool,}
// Event data types for parsing#[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,}
// Convert Event to typed domain eventenum AccountEvent { Opened(AccountOpenedData), Credited(AccountCreditedData), Debited(AccountDebitedData), Closed, Unknown,}
impl TryFrom<&Event> for AccountEvent { type Error = String; fn try_from(event: &Event) -> Result<Self, Self::Error> { match event.event_type() { "com.banking.account.opened" => { let data: AccountOpenedData = serde_json::from_slice( event.data().ok_or("Missing data")?.as_bytes() ).map_err(|e| e.to_string())?; Ok(AccountEvent::Opened(data)) } "com.banking.account.credited" => { let data: AccountCreditedData = serde_json::from_slice( event.data().ok_or("Missing data")?.as_bytes() ).map_err(|e| e.to_string())?; Ok(AccountEvent::Credited(data)) } "com.banking.account.debited" => { let data: AccountDebitedData = serde_json::from_slice( event.data().ok_or("Missing data")?.as_bytes() ).map_err(|e| e.to_string())?; Ok(AccountEvent::Debited(data)) } "com.banking.account.closed" => Ok(AccountEvent::Closed), _ => Ok(AccountEvent::Unknown), } }}
// The state view function - processes one event at a time#[evidentsource_functions::state_view(name = "account-summary")]fn evolve(mut state: AccountSummary, event: AccountEvent) -> AccountSummary { match event { AccountEvent::Opened(data) => { state.id = data.account_id; state.name = data.customer_name; state.is_open = true; } AccountEvent::Credited(data) => { state.balance += data.amount; } AccountEvent::Debited(data) => { state.balance -= data.amount; } AccountEvent::Closed => { state.is_open = false; } AccountEvent::Unknown => {} } state}Deploying Components
Section titled “Deploying Components”Deploy your WASM components to the EvidentSource server:
# Create databasecurl -X POST http://localhost:3000/api/v1/databases/banking
# Install state changecurl -X POST http://localhost:3000/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:3000/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