# dynamodel [![Version](https://img.shields.io/crates/v/dynamodel)](https://crates.io/crates/dynamodel) [![License](https://img.shields.io/crates/l/dynamodel)](LICENSE) [![Test](https://img.shields.io/github/actions/workflow/status/kaicoh/dynamodel/ci.yml)](https://github.com/kaicoh/dynamodel/actions/workflows/ci.yml) This library provides a derive macro to implement conversions between your object and `HashMap`. ## Derive macro `Dynamodel` The `Dynamodel` derive macro implements these three traits to use [`aws-sdk-dynamodb`](https://crates.io/crates/aws-sdk-dynamodb) more comfortably. * `Into>` * `TryFrom>` * The `AttributeValueConvertible` trait enables the types that implement it to be converted from and to `AttributeValue`. ``` #[derive(Dynamodel)] Convertible struct YourStruct { ... } <===========> HashMap #[derive(Dynamodel)] Convertible enum YourEnum { ... } <===========> HashMap ``` ### 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 ```rust 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 = [ ("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. let converted: HashMap = person.clone().into(); assert_eq!(converted, item); // Convert from HashMap 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`](https://crates.io/crates/serde) crate, you can modify the default behavior through attributes like this. ```rust use dynamodel::{Dynamodel, ConvertError}; use std::collections::HashMap; use aws_sdk_dynamodb::{types::AttributeValue, primitives::Blob}; // Vec 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 } fn to_blob(value: Vec) -> AttributeValue { AttributeValue::B(Blob::new(value)) } fn from_blob(value: &AttributeValue) -> Result, 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` | ## Example ### Single-table design The following diagram shows that both `Video` and `VideoStats` are stored in the same table. ![videos table](https://github.com/kaicoh/dynamodel/blob/images/videos_table.png?raw=true) ```rust #[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 { [ ("SK".to_string(), AttributeValue::S("VideoStats".into())), ].into() } } ``` And suppose you want to add a `VideoComment` object that is sortable by timestamp, like this. ![video comments object](https://github.com/kaicoh/dynamodel/blob/images/videos_table_2.png?raw=true) ```rust #[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 { 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](https://github.com/kaicoh/dynamodel/wiki). ## License This software is released under the [MIT License](LICENSE).