dynamodb_tester/
config.rs1use std::{fs::File, io::BufReader, path::Path};
2
3use anyhow::{Error, Result};
4use aws_sdk_dynamodb::{
5 input::CreateTableInput,
6 model::{
7 AttributeDefinition, GlobalSecondaryIndex, KeySchemaElement, KeyType, LocalSecondaryIndex,
8 Projection, ProjectionType, ProvisionedThroughput, ScalarAttributeType,
9 },
10};
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct TableConfig {
15 pub table_name: String,
16 pub pk: TableAttr,
17 #[serde(default)]
18 pub sk: Option<TableAttr>,
19 #[serde(default)]
20 pub attrs: Vec<TableAttr>,
21 #[serde(default)]
22 pub gsis: Vec<TableGsi>,
23 #[serde(default)]
24 pub lsis: Vec<TableLsi>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct TableAttr {
29 pub name: String,
30 #[serde(rename = "type")]
31 pub attr_type: AttrType,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
35pub enum AttrType {
36 S,
37 N,
38 B,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct TableGsi {
43 pub name: String,
44 pub pk: TableAttr,
45 #[serde(default)]
46 pub sk: Option<TableAttr>,
47 #[serde(default)]
48 pub attrs: Vec<String>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct TableLsi {
53 pub name: String,
54 pub pk: TableAttr,
56 pub sk: TableAttr,
57 #[serde(default)]
58 pub attrs: Vec<String>,
59}
60
61impl From<AttrType> for ScalarAttributeType {
62 fn from(attr_type: AttrType) -> Self {
63 match attr_type {
64 AttrType::S => ScalarAttributeType::S,
65 AttrType::N => ScalarAttributeType::N,
66 AttrType::B => ScalarAttributeType::B,
67 }
68 }
69}
70
71impl From<TableAttr> for AttributeDefinition {
72 fn from(attr: TableAttr) -> Self {
73 let attr_type = attr.attr_type.into();
74 AttributeDefinition::builder()
75 .attribute_name(attr.name)
76 .attribute_type(attr_type)
77 .build()
78 }
79}
80
81impl TableAttr {
82 fn to_pk(&self) -> KeySchemaElement {
83 KeySchemaElement::builder()
84 .attribute_name(self.name.clone())
85 .key_type(KeyType::Hash)
86 .build()
87 }
88
89 fn to_sk(&self) -> KeySchemaElement {
90 KeySchemaElement::builder()
91 .attribute_name(self.name.clone())
92 .key_type(KeyType::Range)
93 .build()
94 }
95}
96
97impl From<TableGsi> for GlobalSecondaryIndex {
98 fn from(gsi: TableGsi) -> Self {
99 let pk = gsi.pk.to_pk();
100 let sk = gsi.sk.map(|sk| sk.to_sk());
101
102 let key_schema = if let Some(sk) = sk {
103 vec![pk, sk]
104 } else {
105 vec![pk]
106 };
107 let pt = ProvisionedThroughput::builder()
108 .read_capacity_units(5)
109 .write_capacity_units(5)
110 .build();
111 GlobalSecondaryIndex::builder()
112 .set_key_schema(Some(key_schema))
113 .projection(
114 Projection::builder()
115 .projection_type(ProjectionType::Include)
116 .set_non_key_attributes(Some(gsi.attrs))
117 .build(),
118 )
119 .provisioned_throughput(pt)
120 .index_name(gsi.name)
121 .build()
122 }
123}
124
125impl From<TableLsi> for LocalSecondaryIndex {
126 fn from(lsi: TableLsi) -> Self {
127 let pk = lsi.pk.to_pk();
128 let sk = lsi.sk.to_sk();
129 let key_schema = vec![pk, sk];
130 let projection = if lsi.attrs.is_empty() {
131 Projection::builder()
132 .projection_type(ProjectionType::All)
133 .build()
134 } else {
135 Projection::builder()
136 .projection_type(ProjectionType::Include)
137 .set_non_key_attributes(Some(lsi.attrs))
138 .build()
139 };
140 LocalSecondaryIndex::builder()
141 .set_key_schema(Some(key_schema))
142 .projection(projection)
143 .index_name(lsi.name)
144 .build()
145 }
146}
147
148impl TryFrom<TableConfig> for CreateTableInput {
149 type Error = Error;
150 fn try_from(config: TableConfig) -> Result<Self> {
151 let pk = config.pk.to_pk();
152 let sk = config.sk.as_ref().map(|sk| sk.to_sk());
153
154 let key_schema = if let Some(sk) = sk {
155 vec![pk, sk]
156 } else {
157 vec![pk]
158 };
159
160 let mut attrs = config.attrs.clone();
162 attrs.push(config.pk);
163 if let Some(sk) = config.sk {
164 attrs.push(sk);
165 }
166 let attrs = attrs.into_iter().map(AttributeDefinition::from).collect();
167
168 let gsis = config
169 .gsis
170 .into_iter()
171 .map(GlobalSecondaryIndex::from)
172 .collect();
173
174 let lsis = config
175 .lsis
176 .into_iter()
177 .map(LocalSecondaryIndex::from)
178 .collect();
179
180 let pt = ProvisionedThroughput::builder()
181 .read_capacity_units(5)
182 .write_capacity_units(5)
183 .build();
184 let input = CreateTableInput::builder()
185 .table_name(config.table_name)
186 .set_key_schema(Some(key_schema))
187 .set_attribute_definitions(Some(attrs))
188 .set_global_secondary_indexes(Some(gsis))
189 .set_local_secondary_indexes(Some(lsis))
190 .provisioned_throughput(pt);
191
192 Ok(input.build()?)
193 }
194}
195
196impl TableConfig {
197 pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
198 let file = File::open(path)?;
199 let reader = BufReader::new(file);
200 let config = serde_yaml::from_reader(reader)?;
201 Ok(config)
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn config_could_be_loaded() {
211 let config = TableConfig::load_from_file("fixtures/config.yml").unwrap();
212
213 assert_eq!(config.table_name, "users");
214 assert_eq!(config.pk.name, "pk");
215 assert_eq!(config.pk.attr_type, AttrType::S);
216
217 let input = CreateTableInput::try_from(config).unwrap();
218 assert_eq!(input.attribute_definitions().unwrap().len(), 5);
219 assert_eq!(input.global_secondary_indexes().unwrap().len(), 1);
220 assert_eq!(input.local_secondary_indexes().unwrap().len(), 1);
221 }
222}