1use crate::errors::{DynoxideError, Result};
2use crate::types::{
3 AttributeDefinition, AttributeValue, GlobalSecondaryIndex, Item, KeySchemaElement, KeyType,
4 ScalarAttributeType,
5};
6
7pub fn validate_table_name(name: &str) -> Result<()> {
12 let errors = table_name_constraint_errors(Some(name));
13 if errors.is_empty() {
14 return Ok(());
15 }
16 let count = errors.len();
17 let msg = format!(
18 "{count} validation error{} detected: {}",
19 if count == 1 { "" } else { "s" },
20 errors.join("; ")
21 );
22 Err(DynoxideError::ValidationException(msg))
23}
24
25pub fn table_name_constraint_errors(table_name: Option<&str>) -> Vec<String> {
31 let mut errors = Vec::new();
32 match table_name {
33 None => {
34 errors.push(
35 "Value null at 'tableName' failed to satisfy constraint: \
36 Member must not be null"
37 .to_string(),
38 );
39 }
40 Some(name) => {
41 if name.is_empty()
42 || !name
43 .chars()
44 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
45 {
46 errors.push(format!(
47 "Value '{}' at 'tableName' failed to satisfy constraint: \
48 Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
49 name
50 ));
51 }
52 if name.len() < 3 {
53 errors.push(format!(
54 "Value '{}' at 'tableName' failed to satisfy constraint: \
55 Member must have length greater than or equal to 3",
56 name
57 ));
58 }
59 if name.len() > 255 {
60 errors.push(format!(
61 "Value '{}' at 'tableName' failed to satisfy constraint: \
62 Member must have length less than or equal to 255",
63 name
64 ));
65 }
66 }
67 }
68 errors
69}
70
71pub fn format_validation_errors(errors: &[String]) -> Option<String> {
75 if errors.is_empty() {
76 return None;
77 }
78 let prefix = format!(
79 "{} validation error{} detected: ",
80 errors.len(),
81 if errors.len() == 1 { "" } else { "s" }
82 );
83 Some(format!("{}{}", prefix, errors.join("; ")))
84}
85
86pub fn validate_key_schema(key_schema: &[KeySchemaElement]) -> Result<()> {
91 if key_schema.is_empty() || key_schema.len() > 2 {
92 return Err(DynoxideError::ValidationException(
93 "1 validation error detected: Value null at 'keySchema' failed to satisfy constraint: \
94 Member must have length less than or equal to 2"
95 .to_string(),
96 ));
97 }
98
99 if key_schema[0].key_type != KeyType::HASH {
101 return Err(DynoxideError::ValidationException(
102 "Invalid KeySchema: The first KeySchemaElement is not a HASH key type".to_string(),
103 ));
104 }
105
106 if key_schema.len() == 2 && key_schema[0].attribute_name == key_schema[1].attribute_name {
108 return Err(DynoxideError::ValidationException(
109 "Both the Hash Key and the Range Key element in the KeySchema have the same name"
110 .to_string(),
111 ));
112 }
113
114 if key_schema.len() == 2 && key_schema[1].key_type != KeyType::RANGE {
116 return Err(DynoxideError::ValidationException(
117 "Invalid KeySchema: The second KeySchemaElement is not a RANGE key type".to_string(),
118 ));
119 }
120
121 Ok(())
122}
123
124pub fn validate_attribute_definitions(defs: &[AttributeDefinition]) -> Result<()> {
126 if defs.is_empty() {
127 return Err(DynoxideError::ValidationException(
128 "1 validation error detected: Value null at 'attributeDefinitions' failed to satisfy \
129 constraint: Member must have length greater than or equal to 1"
130 .to_string(),
131 ));
132 }
133
134 for def in defs {
135 match def.attribute_type {
136 ScalarAttributeType::S | ScalarAttributeType::N | ScalarAttributeType::B => {}
137 }
138 }
139
140 Ok(())
141}
142
143pub fn validate_key_attributes_in_definitions(
145 key_schema: &[KeySchemaElement],
146 definitions: &[AttributeDefinition],
147) -> Result<()> {
148 for key_elem in key_schema {
149 let found = definitions
150 .iter()
151 .any(|def| def.attribute_name == key_elem.attribute_name);
152 if !found {
153 return Err(DynoxideError::ValidationException(format!(
154 "One or more parameter values were invalid: Some index key attributes are not \
155 defined in AttributeDefinitions. Keys: [{}], AttributeDefinitions: [{}]",
156 key_elem.attribute_name,
157 definitions
158 .iter()
159 .map(|d| d.attribute_name.as_str())
160 .collect::<Vec<_>>()
161 .join(", ")
162 )));
163 }
164 }
165
166 Ok(())
167}
168
169pub fn validate_gsi(
171 gsi: &GlobalSecondaryIndex,
172 all_definitions: &[AttributeDefinition],
173) -> Result<()> {
174 if gsi.index_name.len() < 3 || gsi.index_name.len() > 255 {
176 return Err(DynoxideError::ValidationException(format!(
177 "1 validation error detected: Value '{}' at 'globalSecondaryIndexes.1.member.indexName' \
178 failed to satisfy constraint: Member must have length greater than or equal to 3",
179 gsi.index_name
180 )));
181 }
182
183 if !gsi
185 .index_name
186 .chars()
187 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
188 {
189 return Err(DynoxideError::ValidationException(format!(
190 "1 validation error detected: Value '{}' at 'globalSecondaryIndexes.1.member.indexName' \
191 failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
192 gsi.index_name
193 )));
194 }
195
196 validate_key_schema(&gsi.key_schema)?;
198
199 validate_projection(&gsi.projection, &gsi.index_name)?;
201
202 validate_key_attributes_in_definitions(&gsi.key_schema, all_definitions)?;
204
205 Ok(())
206}
207
208pub fn validate_projection(projection: &crate::types::Projection, _index_name: &str) -> Result<()> {
214 match &projection.projection_type {
215 None => {
216 return Err(DynoxideError::ValidationException(
217 "One or more parameter values were invalid: Unknown ProjectionType: null"
218 .to_string(),
219 ));
220 }
221 Some(pt) => {
222 if let Some(ref nka) = projection.non_key_attributes {
223 match pt {
225 crate::types::ProjectionType::ALL => {
226 return Err(DynoxideError::ValidationException(
227 "One or more parameter values were invalid: \
228 ProjectionType is ALL, but NonKeyAttributes is specified"
229 .to_string(),
230 ));
231 }
232 crate::types::ProjectionType::KEYS_ONLY => {
233 return Err(DynoxideError::ValidationException(
234 "One or more parameter values were invalid: \
235 ProjectionType is KEYS_ONLY, but NonKeyAttributes is specified"
236 .to_string(),
237 ));
238 }
239 crate::types::ProjectionType::INCLUDE => {
240 if nka.is_empty() {
242 return Err(DynoxideError::ValidationException(
243 "One or more parameter values were invalid: \
244 NonKeyAttributes must not be empty"
245 .to_string(),
246 ));
247 }
248 }
249 }
250 }
251 }
252 }
253 Ok(())
254}
255
256pub fn partition_key_name(key_schema: &[KeySchemaElement]) -> Option<&str> {
258 key_schema
259 .iter()
260 .find(|k| k.key_type == KeyType::HASH)
261 .map(|k| k.attribute_name.as_str())
262}
263
264const MAX_NESTING_DEPTH: usize = 32;
266
267pub fn validate_item_attribute_values(item: &Item) -> Result<()> {
283 for value in item.values() {
284 validate_attribute_value(value, 0)?;
285 }
286 Ok(())
287}
288
289fn validate_attribute_value(value: &AttributeValue, depth: usize) -> Result<()> {
290 if depth > MAX_NESTING_DEPTH {
291 return Err(DynoxideError::ValidationException(
292 "Nesting level exceeds limit of 32".to_string(),
293 ));
294 }
295 match value {
296 AttributeValue::NULL(b) if !b => Err(DynoxideError::ValidationException(
297 "One or more parameter values were invalid: \
298 Null attribute value types must have the value of true"
299 .to_string(),
300 )),
301 AttributeValue::SS(set) if set.is_empty() => Err(DynoxideError::ValidationException(
302 "One or more parameter values were invalid: An string set may not be empty"
303 .to_string(),
304 )),
305 AttributeValue::NS(set) if set.is_empty() => Err(DynoxideError::ValidationException(
306 "One or more parameter values were invalid: An number set may not be empty"
307 .to_string(),
308 )),
309 AttributeValue::BS(set) if set.is_empty() => Err(DynoxideError::ValidationException(
310 "One or more parameter values were invalid: Binary sets should not be empty"
311 .to_string(),
312 )),
313 AttributeValue::SS(set) if !set.is_empty() => {
314 let mut seen = std::collections::HashSet::new();
315 for s in set {
316 if !seen.insert(s.clone()) {
317 let display: Vec<&str> = set.iter().map(|s| s.as_str()).collect();
318 return Err(DynoxideError::ValidationException(format!(
319 "One or more parameter values were invalid: Input collection [{}] contains duplicates.",
320 display.join(", ")
321 )));
322 }
323 }
324 Ok(())
325 }
326 AttributeValue::BS(set) if !set.is_empty() => {
327 let mut seen = std::collections::HashSet::new();
328 for b in set {
329 if !seen.insert(b.clone()) {
330 use base64::Engine;
331 let display: Vec<String> = set
332 .iter()
333 .map(|s| base64::engine::general_purpose::STANDARD.encode(s))
334 .collect();
335 return Err(DynoxideError::ValidationException(format!(
336 "One or more parameter values were invalid: Input collection [{}]of type BS contains duplicates.",
337 display.join(", ")
338 )));
339 }
340 }
341 Ok(())
342 }
343 AttributeValue::NS(set) if !set.is_empty() => {
344 for n in set {
345 crate::types::validate_dynamo_number(n)?;
346 }
347 let mut seen = std::collections::HashSet::new();
349 for n in set {
350 let normalized = crate::types::normalize_dynamo_number(n);
351 if !seen.insert(normalized) {
352 return Err(DynoxideError::ValidationException(
353 "Input collection contains duplicates".to_string(),
354 ));
355 }
356 }
357 Ok(())
358 }
359 AttributeValue::N(n) => {
360 crate::types::validate_dynamo_number(n)?;
361 Ok(())
362 }
363 AttributeValue::L(list) => {
364 for v in list {
365 validate_attribute_value(v, depth + 1)?;
366 }
367 Ok(())
368 }
369 AttributeValue::M(map) => {
370 for v in map.values() {
371 validate_attribute_value(v, depth + 1)?;
372 }
373 Ok(())
374 }
375 _ => Ok(()),
376 }
377}
378
379pub fn validate_key_attribute_values(key: &Item) -> Result<()> {
389 for value in key.values() {
390 validate_key_attr_value(value)?;
391 }
392 Ok(())
393}
394
395fn validate_key_attr_value(value: &AttributeValue) -> Result<()> {
396 match value {
397 AttributeValue::NULL(b) if !b => {
398 return Err(DynoxideError::ValidationException(
399 "One or more parameter values were invalid: \
400 Null attribute value types must have the value of true"
401 .to_string(),
402 ));
403 }
404 AttributeValue::SS(set) if set.is_empty() => {
405 return Err(DynoxideError::ValidationException(
406 "One or more parameter values were invalid: An string set may not be empty"
407 .to_string(),
408 ));
409 }
410 AttributeValue::NS(set) if set.is_empty() => {
411 return Err(DynoxideError::ValidationException(
412 "One or more parameter values were invalid: An number set may not be empty"
413 .to_string(),
414 ));
415 }
416 AttributeValue::BS(set) if set.is_empty() => {
417 return Err(DynoxideError::ValidationException(
418 "One or more parameter values were invalid: Binary sets should not be empty"
419 .to_string(),
420 ));
421 }
422 AttributeValue::SS(set) => {
423 let mut seen = std::collections::HashSet::new();
425 for s in set {
426 if !seen.insert(s.clone()) {
427 let display: Vec<&str> = set.iter().map(|s| s.as_str()).collect();
428 return Err(DynoxideError::ValidationException(format!(
429 "One or more parameter values were invalid: \
430 Input collection [{}] contains duplicates.",
431 display.join(", ")
432 )));
433 }
434 }
435 }
436 AttributeValue::NS(set) if !set.is_empty() => {
437 for n in set {
439 crate::types::validate_dynamo_number(n)?;
440 }
441 let mut seen = std::collections::HashSet::new();
442 for n in set {
443 let normalized = crate::types::normalize_dynamo_number(n);
444 if !seen.insert(normalized) {
445 return Err(DynoxideError::ValidationException(
446 "Input collection contains duplicates".to_string(),
447 ));
448 }
449 }
450 }
451 AttributeValue::BS(set) => {
452 let mut seen = std::collections::HashSet::new();
454 for b in set {
455 if !seen.insert(b.clone()) {
456 use base64::Engine;
457 let display: Vec<String> = set
458 .iter()
459 .map(|s| base64::engine::general_purpose::STANDARD.encode(s))
460 .collect();
461 return Err(DynoxideError::ValidationException(format!(
462 "One or more parameter values were invalid: \
463 Input collection [{}]of type BS contains duplicates.",
464 display.join(", ")
465 )));
466 }
467 }
468 }
469 AttributeValue::N(n) => {
470 crate::types::validate_dynamo_number(n)?;
471 }
472 _ => {}
473 }
474 Ok(())
475}
476
477pub fn normalize_item_sets(item: &mut Item) {
485 for value in item.values_mut() {
486 normalize_attribute_sets(value);
487 }
488}
489
490fn normalize_attribute_sets(value: &mut AttributeValue) {
491 match value {
492 AttributeValue::N(n) => {
493 *n = crate::types::normalize_dynamo_number(n);
494 }
495 AttributeValue::SS(set) => {
496 let mut seen = std::collections::HashSet::new();
497 set.retain(|s| seen.insert(s.clone()));
498 }
499 AttributeValue::NS(set) => {
500 let mut seen = std::collections::HashSet::new();
501 set.retain(|n| seen.insert(normalize_number_for_dedup(n)));
502 for n in set.iter_mut() {
504 *n = crate::types::normalize_dynamo_number(n);
505 }
506 }
507 AttributeValue::BS(set) => {
508 let mut seen = std::collections::HashSet::new();
509 set.retain(|b| seen.insert(b.clone()));
510 }
511 AttributeValue::L(list) => {
512 for v in list.iter_mut() {
513 normalize_attribute_sets(v);
514 }
515 }
516 AttributeValue::M(map) => {
517 for v in map.values_mut() {
518 normalize_attribute_sets(v);
519 }
520 }
521 _ => {}
522 }
523}
524
525fn normalize_number_for_dedup(n: &str) -> String {
529 let trimmed = n.trim();
530 let negative = trimmed.starts_with('-');
531 let abs_str = if negative { &trimmed[1..] } else { trimmed };
532
533 let (digits, exponent) = crate::types::parse_number_parts(abs_str);
534
535 if digits.is_empty() {
536 return "0".to_string();
537 }
538
539 let mantissa: String = digits.iter().map(|&d| (b'0' + d) as char).collect();
540 let sign = if negative { "-" } else { "" };
541 format!("{sign}{mantissa}E{exponent}")
542}
543
544pub fn validate_lsi(
546 lsi: &crate::types::LocalSecondaryIndex,
547 table_key_schema: &[KeySchemaElement],
548 all_definitions: &[AttributeDefinition],
549) -> Result<()> {
550 if lsi.index_name.len() < 3 || lsi.index_name.len() > 255 {
552 return Err(DynoxideError::ValidationException(format!(
553 "1 validation error detected: Value '{}' at 'localSecondaryIndexes.1.member.indexName' \
554 failed to satisfy constraint: Member must have length greater than or equal to 3",
555 lsi.index_name
556 )));
557 }
558
559 if !lsi
561 .index_name
562 .chars()
563 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
564 {
565 return Err(DynoxideError::ValidationException(format!(
566 "1 validation error detected: Value '{}' at 'localSecondaryIndexes.1.member.indexName' \
567 failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
568 lsi.index_name
569 )));
570 }
571
572 validate_key_schema(&lsi.key_schema)?;
574
575 validate_projection(&lsi.projection, &lsi.index_name)?;
577
578 let lsi_pk = lsi
580 .key_schema
581 .iter()
582 .find(|k| k.key_type == KeyType::HASH)
583 .map(|k| k.attribute_name.as_str());
584 let lsi_sk = lsi
585 .key_schema
586 .iter()
587 .find(|k| k.key_type == KeyType::RANGE)
588 .map(|k| k.attribute_name.as_str());
589
590 let table_pk = partition_key_name(table_key_schema);
591 let table_sk = sort_key_name(table_key_schema);
592
593 if lsi_pk != table_pk {
595 return Err(DynoxideError::ValidationException(
596 "One or more parameter values were invalid: Table KeySchema: The AttributeValue for a key attribute for the table must match the AttributeValue definition".to_string(),
597 ));
598 }
599
600 if lsi_sk.is_some() && lsi_sk == table_sk {
602 return Err(DynoxideError::ValidationException(
603 "One or more parameter values were invalid: Index KeySchema: The index KeySchema must not be the same as the table KeySchema".to_string(),
604 ));
605 }
606
607 validate_key_attributes_in_definitions(&lsi.key_schema, all_definitions)?;
609
610 Ok(())
611}
612
613pub fn sort_key_name(key_schema: &[KeySchemaElement]) -> Option<&str> {
615 key_schema
616 .iter()
617 .find(|k| k.key_type == KeyType::RANGE)
618 .map(|k| k.attribute_name.as_str())
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624
625 fn hash_key(name: &str) -> KeySchemaElement {
626 KeySchemaElement {
627 attribute_name: name.to_string(),
628 key_type: KeyType::HASH,
629 }
630 }
631
632 fn range_key(name: &str) -> KeySchemaElement {
633 KeySchemaElement {
634 attribute_name: name.to_string(),
635 key_type: KeyType::RANGE,
636 }
637 }
638
639 fn attr_def(name: &str, attr_type: ScalarAttributeType) -> AttributeDefinition {
640 AttributeDefinition {
641 attribute_name: name.to_string(),
642 attribute_type: attr_type,
643 }
644 }
645
646 #[test]
647 fn test_valid_table_name() {
648 assert!(validate_table_name("MyTable").is_ok());
649 assert!(validate_table_name("my-table.v2").is_ok());
650 assert!(validate_table_name("a_b").is_ok());
651 }
652
653 #[test]
654 fn test_invalid_table_name_too_short() {
655 assert!(validate_table_name("ab").is_err());
656 }
657
658 #[test]
659 fn test_invalid_table_name_bad_chars() {
660 assert!(validate_table_name("my table").is_err());
661 assert!(validate_table_name("my@table").is_err());
662 }
663
664 #[test]
665 fn test_valid_key_schema() {
666 let schema = vec![hash_key("pk")];
667 assert!(validate_key_schema(&schema).is_ok());
668
669 let schema = vec![hash_key("pk"), range_key("sk")];
670 assert!(validate_key_schema(&schema).is_ok());
671 }
672
673 #[test]
674 fn test_invalid_key_schema_empty() {
675 assert!(validate_key_schema(&[]).is_err());
676 }
677
678 #[test]
679 fn test_invalid_key_schema_no_hash() {
680 let schema = vec![range_key("sk")];
681 assert!(validate_key_schema(&schema).is_err());
682 }
683
684 #[test]
685 fn test_valid_key_attributes_in_definitions() {
686 let schema = vec![hash_key("pk"), range_key("sk")];
687 let defs = vec![
688 attr_def("pk", ScalarAttributeType::S),
689 attr_def("sk", ScalarAttributeType::N),
690 ];
691 assert!(validate_key_attributes_in_definitions(&schema, &defs).is_ok());
692 }
693
694 #[test]
695 fn test_missing_key_attribute_in_definitions() {
696 let schema = vec![hash_key("pk"), range_key("sk")];
697 let defs = vec![attr_def("pk", ScalarAttributeType::S)];
698 assert!(validate_key_attributes_in_definitions(&schema, &defs).is_err());
699 }
700
701 #[test]
702 fn test_partition_key_name() {
703 let schema = vec![hash_key("pk"), range_key("sk")];
704 assert_eq!(partition_key_name(&schema), Some("pk"));
705 }
706
707 #[test]
708 fn test_sort_key_name() {
709 let schema = vec![hash_key("pk"), range_key("sk")];
710 assert_eq!(sort_key_name(&schema), Some("sk"));
711
712 let schema = vec![hash_key("pk")];
713 assert_eq!(sort_key_name(&schema), None);
714 }
715}