4 releases
Uses new Rust 2024
| 0.10.0 | Feb 27, 2026 |
|---|---|
| 0.1.4 | Feb 10, 2026 |
| 0.1.1 | Feb 6, 2026 |
| 0.1.0 | Feb 6, 2026 |
#1782 in Database interfaces
50KB
1K
SLoC
aymond
A batteries-included client wrapper for DynamoDB
Builds upon the existing AWS SDK DynamoDB client, providing a high-level interface, somewhat akin to the DynamoDB Enhanced Java Client. Utilizes code generation to create tailored interfaces for items, pushing as much validation as possible to compile time.
Quickstart
Items are described by structs:
#[aymond(item, table)]
struct Car {
#[aymond(hash_key)]
make: String,
#[aymond(sort_key)]
model: String,
hp: i16,
}
Table instances are used for interactions:
let aymond = Aymond::new_with_local_config("http://localhost:8000", "us-west-2");
let table = CarTable::new(&aymond, "my-table-name");
// Create a table in local DynamoDB, based on our item schema
table.create(false).await.expect("Failed to create");
Write items with put:
let it = Car {
make: "Porsche".to_string(),
model: "911".to_string(),
hp: 518,
};
table.put().item(it).send().await.expect("Failed to write");
Read items with get, query, scan (and more!):
let req = table.get().make("Porsche").model("911");
let _: Option<Car> = req.send().await.expect("Failed to read");
Usage
Attribute types
aymond maps each attributes Rust type to the corresponding DynamoDB type
Scalars
| Rust | DynamoDB |
|---|---|
| String | AttributeValue::S |
| i32 | AttributeValue::N |
| Vec<u8> | AttributeValue::B |
| HashSet<String> | AttributeValue::Ss |
| HashSet<Vec<u8>> | AttributeValue::Bs |
| Vec<String> | AttributeValue::L |
| Nested items | AttributeValue::M |
Nested items
#[aymond(item, table)]
struct Student {
#[aymond(hash_key)]
name: String,
grades: Grades,
}
#[aymond(nested_item)]
struct Grades {
fall: i32,
winter: i32,
spring: i32,
}
Operations
Most relevant DynamoDB actions should be implemented. Below is based on the Car struct from quickstart. Since function names are code-generated from an item's attributes, examples can't be entirely generic.
Get
let req = table.get().make("Porsche").model("911");
let _: Option<Car> = req.send().await.unwrap();
Put
let it = Car {
make: "Porsche".to_string(),
model: "911".to_string(),
hp: 518,
};
table.put().item(it).send().await.unwrap();
Query
let req = table.query().make("Porsche").model_begins_with("9");
let _: Vec<Car> = req.send().await.map(|e| e.ok().unwrap()).collect().await;
Scan
let req = table.scan();
let _: Vec<Car> = req.send().await.map(|e| e.ok().unwrap()).collect().await;
Update
let _: Result<(), _> = table
.update()
.make("Porsche")
.model("911")
.expression(|e| e.hp().set(541i16))
.send()
.await;
Batch get
let _: Vec<Car> = table
.batch_get()
.make_and_model("Porsche", "911")
.make_and_model("Honda", "Civic")
.send()
.await
.unwrap();
Batch write
let _: Result<(), _> = table
.batch_write()
.put(Car {
make: "Honda".to_string(),
model: "Civic".to_string(),
hp: 150,
})
.delete()
.make("Porsche")
.model("911")
.send()
.await;
Advanced features
Transactions
The Aymond instance can be used to build and send transactions using TransactWriteItems. These can span tables and use the same builders as individual requests:
let _: Result<(), _> = aymond
.tx()
.update(
table
.update()
.make("Honda")
.model("Civic")
.expression(|e| e.hp().set(200i16)),
)
.put(table.put().item(Car {
make: "Tesla".to_string(),
model: "Model Y".to_string(),
hp: 460,
}))
.delete(table.delete_item().make("Porsche").model("911"))
.send()
.await;
Optimistic locking
Items can define a #[aymond(version)] attribute:
#[aymond(item, table)]
struct Item {
#[aymond(hash_key)]
id: String,
#[aymond(version)]
ver: i3,
}
When set, operations like table.delete().item(<>) and table.put().item(<>) will enforce version checking.
In the case of put(), the version number will be incremented during write -- for example, if the input to put() had ver: 6, we'd generate a condition expression that ensures DynamoDB currently has 6 and overwrite it with 7. Version 0 is treated as a sentinel value that ensures object creation.
If you want to bypass versioning on a specific request, you can do that with a condition expression -- table.put().item(<>).condition(|c| c.disable_versioning()).
Condition/update expressions
Both types of expressions support:
- Deep nesting with list and map access
- Type awareness: string properties will have a
begins_withmethod while numeric types wont
To illustrate, take for example this item:
#[aymond(item, table)]
struct Person {
#[aymond(hash_key)]
name: String,
address: Address,
ssn: Vec<i32>,
}
#[aymond(nested_item)]
struct Address {
street: String,
city: String,
state: String,
}
Expressions like these could be used, seeking into both lists and nested items:
table
.update()
.name("John Doe")
.expression(|e| {
e.address().city().set("Seattle")
.and(e.address().state().set("WA"))
.and(e.phone().index(2).add(3))
})
.condition(|e| {
e.address().street().begins_with("123")
.and(e.phone().index(0).gt(100))
})
Development
The tests assume that DynamoDB local is available on port 8000 -- start it with any container runtime:
container run --name dynamodb-local -d -p 8000:8000 amazon/dynamodb-local
The integration tests can be ran with:
cargo test -p aymond-test
Dependencies
~29MB
~389K SLoC