#![deny(missing_docs)]
#[cfg(feature = "default")]
pub use rusoto_dynamodb_default as dynamodb;
#[cfg(feature = "rustls")]
pub use rusoto_dynamodb_rustls as dynamodb;
use bytes::Bytes;
use dynamodb::AttributeValue;
use std::{
borrow::Cow,
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
};
#[cfg(feature = "uuid")]
use uuid::Uuid;
pub mod error;
mod ext;
pub mod retry;
pub use crate::{ext::DynamoDbExt, retry::Retries};
pub use crate::error::AttributeError;
pub type Attributes = HashMap<String, AttributeValue>;
pub trait Item: Into<Attributes> + FromAttributes {
fn key(&self) -> Attributes;
}
pub trait Attribute: Sized {
fn into_attr(self: Self) -> AttributeValue;
fn from_attr(value: AttributeValue) -> Result<Self, AttributeError>;
}
pub trait FromAttributes: Sized {
fn from_attrs(attrs: Attributes) -> Result<Self, AttributeError>;
}
#[allow(clippy::implicit_hasher)]
impl<A: Attribute> FromAttributes for HashMap<String, A> {
fn from_attrs(attrs: Attributes) -> Result<Self, AttributeError> {
attrs
.into_iter()
.try_fold(HashMap::new(), |mut result, (k, v)| {
result.insert(k, A::from_attr(v)?);
Ok(result)
})
}
}
impl<A: Attribute> FromAttributes for BTreeMap<String, A> {
fn from_attrs(attrs: Attributes) -> Result<Self, AttributeError> {
attrs
.into_iter()
.try_fold(BTreeMap::new(), |mut result, (k, v)| {
result.insert(k, A::from_attr(v)?);
Ok(result)
})
}
}
impl<T: Item> Attribute for T {
fn into_attr(self: Self) -> AttributeValue {
AttributeValue {
m: Some(self.into()),
..AttributeValue::default()
}
}
fn from_attr(value: AttributeValue) -> Result<Self, AttributeError> {
value
.m
.ok_or(AttributeError::InvalidType)
.and_then(T::from_attrs)
}
}
#[allow(clippy::implicit_hasher)]
impl<A: Attribute> Attribute for HashMap<String, A> {
fn into_attr(self: Self) -> AttributeValue {
AttributeValue {
m: Some(self.into_iter().map(|(k, v)| (k, v.into_attr())).collect()),
..AttributeValue::default()
}
}
fn from_attr(value: AttributeValue) -> Result<Self, AttributeError> {
value
.m
.ok_or(AttributeError::InvalidType)
.and_then(Self::from_attrs)
}
}
impl<A: Attribute> Attribute for BTreeMap<String, A> {
fn into_attr(self: Self) -> AttributeValue {
AttributeValue {
m: Some(self.into_iter().map(|(k, v)| (k, v.into_attr())).collect()),
..AttributeValue::default()
}
}
fn from_attr(value: AttributeValue) -> Result<Self, AttributeError> {
value
.m
.ok_or(AttributeError::InvalidType)
.and_then(Self::from_attrs)
}
}
#[cfg(feature = "uuid")]
impl Attribute for Uuid {
fn into_attr(self: Self) -> AttributeValue {
AttributeValue {
s: Some(self.to_hyphenated().to_string()),
..AttributeValue::default()
}
}
fn from_attr(value: AttributeValue) -> Result<Self, AttributeError> {
value
.s
.ok_or(AttributeError::InvalidType)
.and_then(|s| Uuid::parse_str(s.as_str()).map_err(|_| AttributeError::InvalidFormat))
}
}
impl Attribute for String {
fn into_attr(self: Self) -> AttributeValue {
AttributeValue {
s: Some(self),
..AttributeValue::default()
}
}
fn from_attr(value: AttributeValue) -> Result<Self, AttributeError> {
value.s.ok_or(AttributeError::InvalidType)
}
}
impl<'a> Attribute for Cow<'a, str> {
fn into_attr(self: Self) -> AttributeValue {
AttributeValue {
s: Some(match self {
Cow::Owned(o) => o,
Cow::Borrowed(b) => b.to_owned(),
}),
..AttributeValue::default()
}
}
fn from_attr(value: AttributeValue) -> Result<Self, AttributeError> {
value.s.map(Cow::Owned).ok_or(AttributeError::InvalidType)
}
}
#[allow(clippy::implicit_hasher)]
impl Attribute for HashSet<String> {
fn into_attr(mut self: Self) -> AttributeValue {
AttributeValue {
ss: Some(self.drain().collect()),
..AttributeValue::default()
}
}
fn from_attr(value: AttributeValue) -> Result<Self, AttributeError> {
value
.ss
.ok_or(AttributeError::InvalidType)
.map(|mut value| value.drain(..).collect())
}
}
impl Attribute for BTreeSet<String> {
fn into_attr(self: Self) -> AttributeValue {
AttributeValue {
ss: Some(self.into_iter().collect()),
..AttributeValue::default()
}
}
fn from_attr(value: AttributeValue) -> Result<Self, AttributeError> {
value
.ss
.ok_or(AttributeError::InvalidType)
.map(|mut value| value.drain(..).collect())
}
}
#[allow(clippy::implicit_hasher)]
impl Attribute for HashSet<Vec<u8>> {
fn into_attr(mut self: Self) -> AttributeValue {
AttributeValue {
bs: Some(self.drain().map(Bytes::from).collect()),
..AttributeValue::default()
}
}
fn from_attr(value: AttributeValue) -> Result<Self, AttributeError> {
value
.bs
.ok_or(AttributeError::InvalidType)
.map(|mut value| value.drain(..).map(|bs| bs.as_ref().to_vec()).collect())
}
}
impl Attribute for bool {
fn into_attr(self: Self) -> AttributeValue {
AttributeValue {
bool: Some(self),
..AttributeValue::default()
}
}
fn from_attr(value: AttributeValue) -> Result<Self, AttributeError> {
value.bool.ok_or(AttributeError::InvalidType)
}
}
impl Attribute for bytes::Bytes {
fn into_attr(self: Self) -> AttributeValue {
AttributeValue {
b: Some(self),
..AttributeValue::default()
}
}
fn from_attr(value: AttributeValue) -> Result<Self, AttributeError> {
value.b.ok_or(AttributeError::InvalidType)
}
}
impl Attribute for Vec<u8> {
fn into_attr(self: Self) -> AttributeValue {
AttributeValue {
b: Some(self.into()),
..AttributeValue::default()
}
}
fn from_attr(value: AttributeValue) -> Result<Self, AttributeError> {
value
.b
.ok_or(AttributeError::InvalidType)
.map(|bs| bs.as_ref().to_vec())
}
}
impl<A: Attribute> Attribute for Vec<A> {
fn into_attr(mut self: Self) -> AttributeValue {
AttributeValue {
l: Some(self.drain(..).map(|s| s.into_attr()).collect()),
..AttributeValue::default()
}
}
fn from_attr(value: AttributeValue) -> Result<Self, AttributeError> {
value
.l
.ok_or(AttributeError::InvalidType)?
.into_iter()
.map(Attribute::from_attr)
.collect()
}
}
impl<T: Attribute> Attribute for Option<T> {
fn into_attr(self: Self) -> AttributeValue {
match self {
Some(value) => value.into_attr(),
_ => AttributeValue::default(),
}
}
fn from_attr(value: AttributeValue) -> Result<Self, AttributeError> {
match Attribute::from_attr(value) {
Ok(value) => Ok(Some(value)),
Err(AttributeError::InvalidType) => Ok(None),
Err(err) => Err(err),
}
}
}
macro_rules! numeric_attr {
($type:ty) => {
impl Attribute for $type {
fn into_attr(self) -> AttributeValue {
AttributeValue {
n: Some(self.to_string()),
..AttributeValue::default()
}
}
fn from_attr(value: AttributeValue) -> Result<Self, AttributeError> {
value
.n
.ok_or(AttributeError::InvalidType)
.and_then(|num| num.parse().map_err(|_| AttributeError::InvalidFormat))
}
}
};
}
macro_rules! numeric_set_attr {
($type:ty => $collection:ty) => {
impl Attribute for $collection {
fn into_attr(self) -> crate::AttributeValue {
AttributeValue {
ns: Some(self.iter().map(|item| item.to_string()).collect()),
..AttributeValue::default()
}
}
fn from_attr(value: AttributeValue) -> Result<Self, AttributeError> {
let mut nums = value.ns.ok_or(AttributeError::InvalidType)?;
let mut results: Vec<Result<$type, AttributeError>> = nums
.drain(..)
.map(|ns| ns.parse().map_err(|_| AttributeError::InvalidFormat))
.collect();
results.drain(..).collect()
}
}
};
}
numeric_attr!(u16);
numeric_attr!(i16);
numeric_attr!(u32);
numeric_attr!(i32);
numeric_attr!(u64);
numeric_attr!(i64);
numeric_attr!(f32);
numeric_attr!(f64);
numeric_set_attr!(u16 => HashSet<u16>);
numeric_set_attr!(u16 => BTreeSet<u16>);
numeric_set_attr!(i16 => HashSet<i16>);
numeric_set_attr!(i16 => BTreeSet<i16>);
numeric_set_attr!(u32 => HashSet<u32>);
numeric_set_attr!(u32 => BTreeSet<u32>);
numeric_set_attr!(i32 => HashSet<i32>);
numeric_set_attr!(i32 => BTreeSet<i32>);
numeric_set_attr!(i64 => HashSet<i64>);
numeric_set_attr!(i64 => BTreeSet<i64>);
numeric_set_attr!(u64 => HashSet<u64>);
numeric_set_attr!(u64 => BTreeSet<u64>);
#[macro_export]
macro_rules! attr_map {
(@single $($x:tt)*) => (());
(@count $($rest:expr),*) => (<[()]>::len(&[$(attr_map!(@single $rest)),*]));
($($key:expr => $value:expr,)+) => { attr_map!($($key => $value),+) };
($($key:expr => $value:expr),*) => {
{
let _cap = attr_map!(@count $($key),*);
let mut _map: ::std::collections::HashMap<String, ::dynomite::dynamodb::AttributeValue> =
::std::collections::HashMap::with_capacity(_cap);
{
use ::dynomite::Attribute;
$(
let _ = _map.insert($key.into(), $value.into_attr());
)*
}
_map
}
};
}
#[cfg(feature = "derive")]
#[allow(unused_imports)]
#[macro_use]
extern crate dynomite_derive;
#[cfg(feature = "derive")]
#[doc(hidden)]
pub use dynomite_derive::*;
#[cfg(test)]
mod test {
use super::*;
use maplit::{btreemap, btreeset, hashmap};
#[test]
fn uuid_attr() {
let value = Uuid::new_v4();
assert_eq!(Ok(value), Uuid::from_attr(value.into_attr()));
}
#[test]
fn uuid_invalid_attr() {
assert_eq!(
Err(AttributeError::InvalidType),
Uuid::from_attr(AttributeValue {
bool: Some(true),
..AttributeValue::default()
})
);
}
#[test]
fn option_some_attr() {
let value = Some(1);
assert_eq!(Ok(value), Attribute::from_attr(value.into_attr()));
}
#[test]
fn option_none_attr() {
let value: Option<u32> = Default::default();
assert_eq!(Ok(value), Attribute::from_attr(value.into_attr()));
}
#[test]
fn option_invalid_attr() {
assert_eq!(
Ok(None),
Option::<u32>::from_attr(AttributeValue {
bool: Some(true),
..AttributeValue::default()
})
);
}
#[test]
fn bool_attr() {
let value = true;
assert_eq!(Ok(value), bool::from_attr(value.into_attr()));
}
#[test]
fn string_attr() {
let value = "test".to_string();
assert_eq!(
Ok(value.clone()),
String::from_attr(value.clone().into_attr())
);
}
#[test]
fn bytes_attr_from_attr() {
let value = Bytes::from("test");
assert_eq!(
Ok(value.clone()),
Bytes::from_attr(value.clone().into_attr())
);
}
#[test]
fn byte_vec_attr_from_attr() {
let value = b"test".to_vec();
assert_eq!(
Ok(value.clone()),
Vec::<u8>::from_attr(value.clone().into_attr())
);
}
#[test]
fn numeric_into_attr() {
assert_eq!(
serde_json::to_string(&1.into_attr()).unwrap(),
r#"{"N":"1"}"#
);
}
#[test]
fn numeric_from_attr() {
assert_eq!(
Attribute::from_attr(serde_json::from_str::<AttributeValue>(r#"{"N":"1"}"#).unwrap()),
Ok(1)
);
}
#[test]
fn string_into_attr() {
assert_eq!(
serde_json::to_string(&"foo".to_string().into_attr()).unwrap(),
r#"{"S":"foo"}"#
);
}
#[test]
fn string_from_attr() {
assert_eq!(
Attribute::from_attr(serde_json::from_str::<AttributeValue>(r#"{"S":"foo"}"#).unwrap()),
Ok("foo".to_string())
);
}
#[test]
fn cow_str_into_attr() {
assert_eq!(
serde_json::to_string(&Cow::Borrowed("foo").into_attr()).unwrap(),
r#"{"S":"foo"}"#
);
}
#[test]
fn cow_str_from_attr() {
assert_eq!(
Attribute::from_attr(serde_json::from_str::<AttributeValue>(r#"{"S":"foo"}"#).unwrap()),
Ok(Cow::Borrowed("foo"))
);
}
#[test]
fn byte_vec_into_attr() {
assert_eq!(
serde_json::to_string(&b"foo".to_vec().into_attr()).unwrap(),
r#"{"B":"Zm9v"}"#
);
}
#[test]
fn byte_vec_from_attr() {
assert_eq!(
Attribute::from_attr(
serde_json::from_str::<AttributeValue>(r#"{"B":"Zm9v"}"#).unwrap()
),
Ok(b"foo".to_vec())
);
}
#[test]
fn bytes_into_attr() {
assert_eq!(
serde_json::to_string(&Bytes::from("foo").into_attr()).unwrap(),
r#"{"B":"Zm9v"}"#
);
}
#[test]
fn bytes_from_attr() {
assert_eq!(
Attribute::from_attr(
serde_json::from_str::<AttributeValue>(r#"{"B":"Zm9v"}"#).unwrap()
),
Ok(Bytes::from("foo"))
);
}
#[test]
fn numeric_set_into_attr() {
assert_eq!(
serde_json::to_string(&btreeset! { 1,2,3 }.into_attr()).unwrap(),
r#"{"NS":["1","2","3"]}"#
);
}
#[test]
fn numeric_set_from_attr() {
assert_eq!(
Attribute::from_attr(
serde_json::from_str::<AttributeValue>(r#"{"NS":["1","2","3"]}"#).unwrap()
),
Ok(btreeset! { 1,2,3 })
);
}
#[test]
fn numeric_vec_into_attr() {
assert_eq!(
serde_json::to_string(&vec![1, 2, 3, 3].into_attr()).unwrap(),
r#"{"L":[{"N":"1"},{"N":"2"},{"N":"3"},{"N":"3"}]}"#
);
}
#[test]
fn numeric_vec_from_attr() {
assert_eq!(
Attribute::from_attr(
serde_json::from_str::<AttributeValue>(
r#"{"L":[{"N":"1"},{"N":"2"},{"N":"3"},{"N":"3"}]}"#
)
.unwrap()
),
Ok(vec![1, 2, 3, 3])
);
}
#[test]
fn string_set_into_attr() {
assert_eq!(
serde_json::to_string(
&btreeset! { "a".to_string(), "b".to_string(), "c".to_string() }.into_attr()
)
.unwrap(),
r#"{"SS":["a","b","c"]}"#
);
}
#[test]
fn string_set_from_attr() {
assert_eq!(
Attribute::from_attr(
serde_json::from_str::<AttributeValue>(r#"{"SS":["a","b","c"]}"#).unwrap()
),
Ok(btreeset! { "a".to_string(), "b".to_string(), "c".to_string() })
);
}
#[test]
fn string_vec_into_attr() {
assert_eq!(
serde_json::to_string(
&vec! { "a".to_string(), "b".to_string(), "c".to_string() }.into_attr()
)
.unwrap(),
r#"{"L":[{"S":"a"},{"S":"b"},{"S":"c"}]}"#
);
}
#[test]
fn string_vec_from_attr() {
assert_eq!(
Attribute::from_attr(
serde_json::from_str::<AttributeValue>(r#"{"L":[{"S":"a"},{"S":"b"},{"S":"c"}]}"#)
.unwrap()
),
Ok(vec! { "a".to_string(), "b".to_string(), "c".to_string() })
);
}
#[test]
fn hashmap_into_attr() {
assert_eq!(
serde_json::to_string(&hashmap! { "foo".to_string() => 1 }.into_attr()).unwrap(),
r#"{"M":{"foo":{"N":"1"}}}"#
);
}
#[test]
fn hashmap_from_attr() {
assert_eq!(
Attribute::from_attr(
serde_json::from_str::<AttributeValue>(r#"{"M":{"foo":{"N":"1"}}}"#).unwrap()
),
Ok(hashmap! { "foo".to_string() => 1 })
);
}
#[test]
fn btreemap_into_attr() {
assert_eq!(
serde_json::to_string(&btreemap! { "foo".to_string() => 1 }.into_attr()).unwrap(),
r#"{"M":{"foo":{"N":"1"}}}"#
);
}
#[test]
fn btreemap_from_attr() {
assert_eq!(
Attribute::from_attr(
serde_json::from_str::<AttributeValue>(r#"{"M":{"foo":{"N":"1"}}}"#).unwrap()
),
Ok(btreemap! { "foo".to_string() => 1 })
);
}
}