1use crate::expressions::tokenizer::{Token, TokenStream, near_window_tokenizer, tokenize};
7use crate::expressions::{
8 PathElement, TrackedExpressionAttributes, resolve_path, resolve_path_elements,
9};
10use crate::types::AttributeValue;
11use std::collections::HashMap;
12
13#[derive(Debug, Clone)]
15pub struct ProjectionExpr {
16 pub paths: Vec<Vec<PathElement>>,
17}
18
19pub fn parse(expr: &str) -> Result<ProjectionExpr, String> {
21 let tokens = match tokenize(expr) {
22 Ok(t) => t,
23 Err(err) => {
24 let bad = &expr[err.position..err.position + err.bad_len];
27 let near = near_window_tokenizer(expr, err.position);
28 return Err(format!(
29 r#"Invalid ProjectionExpression: Syntax error; token: "{bad}", near: "{near}""#
30 ));
31 }
32 };
33 let mut stream = TokenStream::new(tokens);
34
35 let mut paths = Vec::new();
36
37 if stream.at_end() {
38 return Ok(ProjectionExpr { paths });
39 }
40
41 paths.push(parse_path(&mut stream).map_err(|e| projection_parser_error(expr, &mut stream, e))?);
42
43 while matches!(stream.peek(), Some(Token::Comma)) {
44 stream.next();
45 paths.push(
46 parse_path(&mut stream).map_err(|e| projection_parser_error(expr, &mut stream, e))?,
47 );
48 }
49
50 if !stream.at_end() {
51 return Err(format!(
52 "Unexpected token in ProjectionExpression: {}",
53 stream.peek().unwrap()
54 ));
55 }
56
57 Ok(ProjectionExpr { paths })
58}
59
60fn projection_parser_error(_expr: &str, _stream: &mut TokenStream, msg: String) -> String {
66 format!("Invalid ProjectionExpression: {msg}")
67}
68
69pub fn apply(
72 item: &HashMap<String, AttributeValue>,
73 projection: &ProjectionExpr,
74 tracker: &TrackedExpressionAttributes,
75 key_attrs: &[String],
76) -> Result<HashMap<String, AttributeValue>, String> {
77 let mut result = HashMap::new();
78
79 for key_attr in key_attrs {
81 if let Some(val) = item.get(key_attr) {
82 result.insert(key_attr.clone(), val.clone());
83 }
84 }
85
86 for raw_path in &projection.paths {
88 let resolved = resolve_path_elements(raw_path, tracker)?;
89 if let Some(val) = resolve_path(item, &resolved) {
90 insert_at_path(&mut result, &resolved, val);
91 }
92 }
93
94 Ok(result)
95}
96
97fn parse_path(stream: &mut TokenStream) -> Result<Vec<PathElement>, String> {
98 let first = match stream.next() {
99 Some(Token::Identifier(name)) => {
100 if super::reserved::is_reserved_keyword(name) {
101 return Err(format!(
102 "Attribute name is a reserved keyword; reserved keyword: {name}"
103 ));
104 }
105 PathElement::Attribute(name.clone())
106 }
107 Some(Token::NameRef(name)) => PathElement::Attribute(name.clone()),
108 Some(t) => return Err(format!("Expected attribute name, got {t}")),
109 None => return Err("Expected attribute name, got end of expression".to_string()),
110 };
111
112 let mut path = vec![first];
113
114 loop {
115 match stream.peek() {
116 Some(Token::Dot) => {
117 stream.next();
118 match stream.next() {
119 Some(Token::Identifier(name)) => {
120 if super::reserved::is_reserved_keyword(name) {
121 return Err(format!(
122 "Attribute name is a reserved keyword; reserved keyword: {name}"
123 ));
124 }
125 path.push(PathElement::Attribute(name.clone()));
126 }
127 Some(Token::NameRef(name)) => {
128 path.push(PathElement::Attribute(name.clone()));
129 }
130 Some(t) => return Err(format!("Expected attribute name after '.', got {t}")),
131 None => return Err("Expected attribute name after '.'".to_string()),
132 }
133 }
134 Some(Token::LBracket) => {
135 stream.next();
136 match stream.next() {
137 Some(Token::Number(n)) => {
138 let idx: usize = n.parse().map_err(|_| format!("Invalid index: {n}"))?;
139 path.push(PathElement::Index(idx));
140 }
141 Some(t) => return Err(format!("Expected number in brackets, got {t}")),
142 None => return Err("Expected number in brackets".to_string()),
143 }
144 stream.expect(&Token::RBracket)?;
145 }
146 _ => break,
147 }
148 }
149
150 Ok(path)
151}
152
153fn default_for_next(next: &PathElement) -> AttributeValue {
155 match next {
156 PathElement::Attribute(_) => AttributeValue::M(HashMap::new()),
157 PathElement::Index(_) => AttributeValue::L(Vec::new()),
158 }
159}
160
161pub(crate) fn insert_at_path(
165 result: &mut HashMap<String, AttributeValue>,
166 path: &[PathElement],
167 value: AttributeValue,
168) {
169 if path.is_empty() {
170 return;
171 }
172
173 if path.len() == 1 {
174 if let PathElement::Attribute(name) = &path[0] {
175 result.insert(name.clone(), value);
176 }
177 return;
178 }
179
180 if let PathElement::Attribute(name) = &path[0] {
182 let entry = result
183 .entry(name.clone())
184 .or_insert_with(|| default_for_next(&path[1]));
185 insert_nested(entry, &path[1..], value);
186 }
187}
188
189fn insert_nested(current: &mut AttributeValue, path: &[PathElement], value: AttributeValue) {
190 if path.is_empty() {
191 return;
192 }
193
194 if path.len() == 1 {
195 match &path[0] {
196 PathElement::Attribute(name) => {
197 if let AttributeValue::M(map) = current {
198 map.insert(name.clone(), value);
199 }
200 }
201 PathElement::Index(_) => {
202 if let AttributeValue::L(list) = current {
203 list.push(value);
204 }
205 }
206 }
207 return;
208 }
209
210 match &path[0] {
211 PathElement::Attribute(name) => {
212 if let AttributeValue::M(map) = current {
213 let entry = map
214 .entry(name.clone())
215 .or_insert_with(|| default_for_next(&path[1]));
216 insert_nested(entry, &path[1..], value);
217 }
218 }
219 PathElement::Index(_) => {
220 if let AttributeValue::L(list) = current {
221 list.push(default_for_next(&path[1]));
223 let last = list.last_mut().unwrap();
224 insert_nested(last, &path[1..], value);
225 }
226 }
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 fn make_item(pairs: &[(&str, AttributeValue)]) -> HashMap<String, AttributeValue> {
235 pairs
236 .iter()
237 .map(|(k, v)| (k.to_string(), v.clone()))
238 .collect()
239 }
240
241 #[test]
242 fn test_parse_simple() {
243 let proj = parse("Title, Price, Color").unwrap();
244 assert_eq!(proj.paths.len(), 3);
245 }
246
247 #[test]
248 fn test_parse_nested() {
249 let proj = parse("ProductReviews.FiveStar").unwrap();
250 assert_eq!(proj.paths[0].len(), 2);
251 assert_eq!(
252 proj.paths[0][0],
253 PathElement::Attribute("ProductReviews".into())
254 );
255 assert_eq!(proj.paths[0][1], PathElement::Attribute("FiveStar".into()));
256 }
257
258 #[test]
259 fn test_parse_with_index() {
260 let proj = parse("RelatedItems[0]").unwrap();
261 assert_eq!(proj.paths[0].len(), 2);
262 assert_eq!(proj.paths[0][1], PathElement::Index(0));
263 }
264
265 #[test]
266 fn test_apply_simple() {
267 let proj = parse("label").unwrap();
268 let item = make_item(&[
269 ("pk", AttributeValue::S("key1".into())),
270 ("label", AttributeValue::S("Alice".into())),
271 ("age", AttributeValue::N("30".into())),
272 ]);
273 let no_names = None;
274 let no_values = None;
275 let tracker = TrackedExpressionAttributes::new(&no_names, &no_values);
276 let result = apply(&item, &proj, &tracker, &["pk".to_string()]).unwrap();
277 assert!(result.contains_key("pk")); assert!(result.contains_key("label")); assert!(!result.contains_key("age")); }
281
282 #[test]
283 fn test_apply_nested() {
284 let mut nested = HashMap::new();
285 nested.insert("nested_val".to_string(), AttributeValue::S("value".into()));
286 nested.insert("extra".to_string(), AttributeValue::S("skip".into()));
287
288 let proj = parse("payload.nested_val").unwrap();
289 let item = make_item(&[
290 ("pk", AttributeValue::S("key1".into())),
291 ("payload", AttributeValue::M(nested)),
292 ]);
293 let no_names = None;
294 let no_values = None;
295 let tracker = TrackedExpressionAttributes::new(&no_names, &no_values);
296 let result = apply(&item, &proj, &tracker, &["pk".to_string()]).unwrap();
297 assert!(result.contains_key("payload"));
298 if let AttributeValue::M(map) = &result["payload"] {
299 assert!(map.contains_key("nested_val"));
300 assert!(!map.contains_key("extra"));
301 } else {
302 panic!("Expected map");
303 }
304 }
305
306 #[test]
307 fn test_apply_with_name_refs() {
308 let proj = parse("#n").unwrap();
309 let item = make_item(&[
310 ("pk", AttributeValue::S("key1".into())),
311 ("name", AttributeValue::S("Alice".into())),
312 ]);
313 let names = Some(HashMap::from([("#n".to_string(), "name".to_string())]));
314 let no_values = None;
315 let tracker = TrackedExpressionAttributes::new(&names, &no_values);
316 let result = apply(&item, &proj, &tracker, &["pk".to_string()]).unwrap();
317 assert!(result.contains_key("name"));
318 }
319
320 #[test]
321 fn test_apply_missing_attribute() {
322 let proj = parse("nonexistent").unwrap();
323 let item = make_item(&[("pk", AttributeValue::S("key1".into()))]);
324 let no_names = None;
325 let no_values = None;
326 let tracker = TrackedExpressionAttributes::new(&no_names, &no_values);
327 let result = apply(&item, &proj, &tracker, &["pk".to_string()]).unwrap();
328 assert!(!result.contains_key("nonexistent"));
329 assert!(result.contains_key("pk")); }
331}