7 releases (4 breaking)
0.5.1 | Jun 7, 2024 |
---|---|
0.5.0 | Jun 6, 2024 |
0.4.0 | Jun 3, 2024 |
0.3.1 | May 26, 2024 |
0.1.0 | May 18, 2024 |
#410 in Rust patterns
30KB
309 lines
dynamodel
This library provides a derive macro to implement conversions between
your object and HashMap<String, AttributeValue>
.
Derive macro Dynamodel
The Dynamodel
derive macro implements these three traits to use
aws-sdk-dynamodb
more comfortably.
Into<HashMap<String, AttributeValue>>
TryFrom<HashMap<String, AttributeValue>>
- The
AttributeValueConvertible
trait enables the types that implement it to be converted from and toAttributeValue
.
#[derive(Dynamodel)] Convertible
struct YourStruct { ... } <===========> HashMap<String, AttributeValue>
#[derive(Dynamodel)] Convertible
enum YourEnum { ... } <===========> HashMap<String, AttributeValue>
Requirements to use Dynamodel
To use the Dynamodel
macro, all types of your object's fields must implement
the AttributeValueConvertible
trait.
By default, these types automatically implement the AttributeValueConvertible
trait, so no additional code is required when using these types.
Type | AttributeValue variant |
---|---|
String |
AttributeValue::S("...") |
u8, u16, u32, u64, u128, usize i8, i16, i32, i64, i128, isize f32, f64 |
AttributeValue::N("...") |
bool |
AttributeValue::Bool(...) |
Vec of any types that implement AttributeValueConvertible |
AttributeValue::L([...]) |
Any types that implement Dynamodel macro |
AttributeValue::M({ ... }) |
The last row of the above table shows that once you apply the Dynamodel
macro to your object,
it also implements the AttributeValueConvertible
trait for your object.
So, you can create nested structures of objects that apply the Dynamodel
macro.
If you want to use additional types, you need to implement the AttributeValueConvertible
trait for your type.
Usage
use dynamodel::Dynamodel;
use std::collections::HashMap;
use aws_sdk_dynamodb::types::AttributeValue;
#[derive(Dynamodel, Debug, Clone, PartialEq)]
struct Person {
first_name: String,
last_name: String,
age: u8,
}
let person = Person {
first_name: "Kanji".into(),
last_name: "Tanaka".into(),
age: 23,
};
let item: HashMap<String, AttributeValue> = [
("first_name".to_string(), AttributeValue::S("Kanji".into())),
("last_name".to_string(), AttributeValue::S("Tanaka".into())),
("age".to_string(), AttributeValue::N("23".into()))
].into();
// Convert from Person into HashMap<String, AttributeValue>.
let converted: HashMap<String, AttributeValue> = person.clone().into();
assert_eq!(converted, item);
// Convert from HashMap<String, AttributeValue> into Person.
// This conversion uses std::convert::TryFrom trait, so this returns a Result.
let converted: Person = item.try_into().unwrap();
assert_eq!(converted, person);
Modifying the default behavior
Like the Serde
crate, you can modify the default behavior
through attributes like this.
use dynamodel::{Dynamodel, ConvertError};
use std::collections::HashMap;
use aws_sdk_dynamodb::{types::AttributeValue, primitives::Blob};
// Vec<u8> is converted to AttributeValue::L by default,
// but this case, the `data` field is converted to AttributeValue::B.
#[derive(Dynamodel)]
struct BinaryData {
#[dynamodel(into = "to_blob", try_from = "from_blob")]
data: Vec<u8>
}
fn to_blob(value: Vec<u8>) -> AttributeValue {
AttributeValue::B(Blob::new(value))
}
fn from_blob(value: &AttributeValue) -> Result<Vec<u8>, ConvertError> {
value.as_b()
.map(|b| b.clone().into_inner())
.map_err(|err| ConvertError::AttributeValueUnmatched("B".to_string(), err.clone()))
}
The function definition must meet these conditions.
Field attribute | Argument | Return |
---|---|---|
#[dynamodel(into = "...")] |
field type |
AttributeValue |
#[dynamodel(try_from = "...")] |
&AttributeValue |
Result<field type, ConvertError> |
Example
Single-table design
The following diagram shows that both Video
and VideoStats
are stored in the same table.
#[derive(Dynamodel)]
#[dynamodel(extra = "VideoStats::sort_key", rename_all = "PascalCase")]
struct VideoStats {
#[dynamodel(rename = "PK")]
id: String,
view_count: u64,
}
impl VideoStats {
fn sort_key(&self) -> HashMap<String, AttributeValue> {
[
("SK".to_string(), AttributeValue::S("VideoStats".into())),
].into()
}
}
And suppose you want to add a VideoComment
object that is sortable by timestamp, like this.
#[derive(Dynamodel)]
#[dynamodel(rename_all = "PascalCase")]
struct VideoComment {
#[dynamodel(rename = "PK")]
id: String,
#[dynamodel(rename = "SK", into = "sort_key", try_from = "get_timestamp")]
timestamp: String,
content: String,
}
fn sort_key(timestamp: String) -> AttributeValue {
AttributeValue::S(format!("VideoComment#{timestamp}"))
}
fn get_timestamp(value: &AttributeValue) -> Result<String, ConvertError> {
value.as_s()
.map(|v| v.split('#').last().unwrap().to_string())
.map_err(|e| ConvertError::AttributeValueUnmatched("S".into(), e.clone()))
}
More features
For more features, refer to this wiki.
License
This software is released under the MIT License.
Dependencies
~15–21MB
~273K SLoC