1use std::pin::Pin;
13use std::sync::Arc;
14
15use indexmap::IndexMap;
16use vantage_core::Result;
17
18use super::types::AttributeValue;
19
20pub type ValueListFuture =
23 Pin<Box<dyn std::future::Future<Output = Result<Vec<AttributeValue>>> + Send>>;
24
25pub type ValueListFn = Arc<dyn Fn() -> ValueListFuture + Send + Sync>;
28
29#[derive(Clone)]
35pub enum DynamoCondition {
36 Expr {
37 expression: String,
38 names: IndexMap<String, String>,
39 values: IndexMap<String, AttributeValue>,
40 },
41 In {
42 field: String,
43 values: ValueListFn,
44 },
45 And(Vec<DynamoCondition>),
46}
47
48impl std::fmt::Debug for DynamoCondition {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 Self::Expr {
52 expression,
53 names,
54 values,
55 } => f
56 .debug_struct("Expr")
57 .field("expression", expression)
58 .field("names", names)
59 .field("values", values)
60 .finish(),
61 Self::In { field, .. } => f
62 .debug_struct("In")
63 .field("field", field)
64 .finish_non_exhaustive(),
65 Self::And(conds) => f.debug_tuple("And").field(conds).finish(),
66 }
67 }
68}
69
70impl DynamoCondition {
71 pub fn eq(field: impl Into<String>, value: impl Into<AttributeValue>) -> Self {
73 let field = field.into();
74 let mut names = IndexMap::new();
75 let mut values = IndexMap::new();
76 names.insert("#f".to_string(), field);
77 values.insert(":v".to_string(), value.into());
78 Self::Expr {
79 expression: "#f = :v".to_string(),
80 names,
81 values,
82 }
83 }
84
85 pub fn begins_with(field: impl Into<String>, prefix: impl Into<String>) -> Self {
88 let mut names = IndexMap::new();
89 let mut values = IndexMap::new();
90 names.insert("#f".to_string(), field.into());
91 values.insert(":v".to_string(), AttributeValue::S(prefix.into()));
92 Self::Expr {
93 expression: "begins_with(#f, :v)".to_string(),
94 names,
95 values,
96 }
97 }
98}
99
100#[derive(Debug, Clone, Default)]
102pub struct ResolvedFilter {
103 pub expression: String,
104 pub names: IndexMap<String, String>,
105 pub values: IndexMap<String, AttributeValue>,
106}
107
108impl ResolvedFilter {
109 pub fn is_empty(&self) -> bool {
110 self.expression.is_empty()
111 }
112}
113
114pub async fn resolve_conditions<'a, I>(conditions: I) -> Result<ResolvedFilter>
117where
118 I: IntoIterator<Item = &'a DynamoCondition>,
119{
120 let mut state = MangleState::default();
121 let mut fragments = Vec::new();
122 for cond in conditions {
123 if let Some(frag) = resolve_one(cond, &mut state).await? {
124 fragments.push(frag);
125 }
126 }
127 let expression = match fragments.len() {
128 0 => String::new(),
129 1 => fragments.into_iter().next().unwrap(),
130 _ => fragments
131 .into_iter()
132 .map(|f| format!("({})", f))
133 .collect::<Vec<_>>()
134 .join(" AND "),
135 };
136 Ok(ResolvedFilter {
137 expression,
138 names: state.names,
139 values: state.values,
140 })
141}
142
143fn resolve_one<'a>(
146 cond: &'a DynamoCondition,
147 state: &'a mut MangleState,
148) -> Pin<Box<dyn std::future::Future<Output = Result<Option<String>>> + Send + 'a>> {
149 Box::pin(async move {
150 match cond {
151 DynamoCondition::Expr {
152 expression,
153 names,
154 values,
155 } => {
156 let mut rendered = expression.clone();
157 for (placeholder, name) in names {
158 let new_ph = state.fresh_name(name.clone());
159 rendered = replace_placeholder(&rendered, placeholder, &new_ph);
160 }
161 for (placeholder, value) in values {
162 let new_ph = state.fresh_value(value.clone());
163 rendered = replace_placeholder(&rendered, placeholder, &new_ph);
164 }
165 Ok(Some(rendered))
166 }
167 DynamoCondition::In { field, values } => {
168 let resolved = (values)().await?;
169 if resolved.is_empty() {
170 let name_ph = state.fresh_name(field.clone());
174 return Ok(Some(format!(
175 "attribute_not_exists({}) AND attribute_exists({})",
176 name_ph, name_ph
177 )));
178 }
179 let name_ph = state.fresh_name(field.clone());
180 let value_phs: Vec<String> =
181 resolved.into_iter().map(|v| state.fresh_value(v)).collect();
182 Ok(Some(format!("{} IN ({})", name_ph, value_phs.join(", "))))
183 }
184 DynamoCondition::And(children) => {
185 let mut parts = Vec::new();
186 for child in children {
187 if let Some(p) = resolve_one(child, state).await? {
188 parts.push(p);
189 }
190 }
191 Ok(if parts.is_empty() {
192 None
193 } else {
194 Some(parts.join(" AND "))
195 })
196 }
197 }
198 })
199}
200
201fn replace_placeholder(s: &str, from: &str, to: &str) -> String {
204 s.replace(from, to)
205}
206
207#[derive(Default)]
208struct MangleState {
209 names: IndexMap<String, String>,
210 values: IndexMap<String, AttributeValue>,
211 name_seq: usize,
212 value_seq: usize,
213}
214
215impl MangleState {
216 fn fresh_name(&mut self, attr: String) -> String {
217 let ph = format!("#n{}", self.name_seq);
218 self.name_seq += 1;
219 self.names.insert(ph.clone(), attr);
220 ph
221 }
222
223 fn fresh_value(&mut self, value: AttributeValue) -> String {
224 let ph = format!(":v{}", self.value_seq);
225 self.value_seq += 1;
226 self.values.insert(ph.clone(), value);
227 ph
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[tokio::test]
236 async fn empty_input_yields_empty_filter() {
237 let r = resolve_conditions(std::iter::empty()).await.unwrap();
238 assert!(r.is_empty());
239 }
240
241 #[tokio::test]
242 async fn single_eq_renders_with_mangled_placeholders() {
243 let cond = DynamoCondition::eq("name", AttributeValue::S("Alice".into()));
244 let r = resolve_conditions(std::iter::once(&cond)).await.unwrap();
245 assert_eq!(r.expression, "#n0 = :v0");
246 assert_eq!(r.names.get("#n0").unwrap(), "name");
247 assert_eq!(
248 r.values.get(":v0").unwrap(),
249 &AttributeValue::S("Alice".into())
250 );
251 }
252
253 #[tokio::test]
254 async fn two_eqs_get_unique_placeholders() {
255 let a = DynamoCondition::eq("name", AttributeValue::S("Alice".into()));
256 let b = DynamoCondition::eq("city", AttributeValue::S("Riga".into()));
257 let r = resolve_conditions([&a, &b]).await.unwrap();
258 assert_eq!(r.expression, "(#n0 = :v0) AND (#n1 = :v1)");
259 assert_eq!(r.names.get("#n0").unwrap(), "name");
260 assert_eq!(r.names.get("#n1").unwrap(), "city");
261 }
262
263 #[tokio::test]
264 async fn deferred_in_resolves_to_in_expression() {
265 let values = Arc::new(|| -> ValueListFuture {
266 Box::pin(async move {
267 Ok(vec![
268 AttributeValue::S("a".into()),
269 AttributeValue::S("b".into()),
270 ])
271 })
272 });
273 let cond = DynamoCondition::In {
274 field: "bakery_id".to_string(),
275 values,
276 };
277 let r = resolve_conditions(std::iter::once(&cond)).await.unwrap();
278 assert_eq!(r.expression, "#n0 IN (:v0, :v1)");
279 assert_eq!(r.names.get("#n0").unwrap(), "bakery_id");
280 assert_eq!(r.values.len(), 2);
281 }
282}