1use crate::errors::{DynoxideError, Result};
2use crate::types::{
3 AttributeDefinition, AttributeValue, GlobalSecondaryIndex, Item, KeySchemaElement, KeyType,
4 ScalarAttributeType,
5};
6
7#[derive(Copy, Clone, Debug)]
15pub enum TableNameContext {
16 CreateTable,
18 ReadWrite,
20}
21
22pub fn validate_table_name(name: &str) -> Result<()> {
30 let errors = table_name_constraint_errors(Some(name), TableNameContext::ReadWrite);
31 if errors.is_empty() {
32 return Ok(());
33 }
34 let count = errors.len();
35 let msg = format!(
36 "{count} validation error{} detected: {}",
37 if count == 1 { "" } else { "s" },
38 errors.join("; ")
39 );
40 Err(DynoxideError::ValidationException(msg))
41}
42
43pub fn table_name_constraint_errors(
49 table_name: Option<&str>,
50 context: TableNameContext,
51) -> Vec<String> {
52 let mut errors = Vec::new();
53 match table_name {
54 None => {
55 errors.push(
56 "Value null at 'tableName' failed to satisfy constraint: \
57 Member must not be null"
58 .to_string(),
59 );
60 }
61 Some(name) => match context {
62 TableNameContext::CreateTable => {
63 if name.is_empty()
64 || !name
65 .chars()
66 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
67 {
68 errors.push(format!(
69 "Value '{}' at 'tableName' failed to satisfy constraint: \
70 Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
71 name
72 ));
73 }
74 if name.len() < 3 {
75 errors.push(format!(
76 "Value '{}' at 'tableName' failed to satisfy constraint: \
77 Member must have length greater than or equal to 3",
78 name
79 ));
80 }
81 if name.len() > 255 {
82 errors.push(format!(
83 "Value '{}' at 'tableName' failed to satisfy constraint: \
84 Member must have length less than or equal to 255",
85 name
86 ));
87 }
88 }
89 TableNameContext::ReadWrite => {
90 if name.is_empty() {
91 errors.push(
92 "Value '' at 'tableName' failed to satisfy constraint: \
93 Member must have length greater than or equal to 1"
94 .to_string(),
95 );
96 } else if !name
97 .chars()
98 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
99 {
100 errors.push(format!(
101 "Value '{}' at 'tableName' failed to satisfy constraint: \
102 Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
103 name
104 ));
105 }
106 if name.len() > 255 {
107 errors.push(format!(
108 "Value '{}' at 'tableName' failed to satisfy constraint: \
109 Member must have length less than or equal to 255",
110 name
111 ));
112 }
113 }
114 },
115 }
116 errors
117}
118
119pub fn format_validation_errors(errors: &[String]) -> Option<String> {
123 if errors.is_empty() {
124 return None;
125 }
126 let prefix = format!(
127 "{} validation error{} detected: ",
128 errors.len(),
129 if errors.len() == 1 { "" } else { "s" }
130 );
131 Some(format!("{}{}", prefix, errors.join("; ")))
132}
133
134pub fn validate_key_schema(key_schema: &[KeySchemaElement]) -> Result<()> {
139 if key_schema.is_empty() || key_schema.len() > 2 {
140 return Err(DynoxideError::ValidationException(
141 "1 validation error detected: Value null at 'keySchema' failed to satisfy constraint: \
142 Member must have length less than or equal to 2"
143 .to_string(),
144 ));
145 }
146
147 if key_schema[0].key_type != KeyType::HASH {
149 return Err(DynoxideError::ValidationException(
150 "Invalid KeySchema: The first KeySchemaElement is not a HASH key type".to_string(),
151 ));
152 }
153
154 if key_schema.len() == 2 && key_schema[0].attribute_name == key_schema[1].attribute_name {
156 return Err(DynoxideError::ValidationException(
157 "Both the Hash Key and the Range Key element in the KeySchema have the same name"
158 .to_string(),
159 ));
160 }
161
162 if key_schema.len() == 2 && key_schema[1].key_type != KeyType::RANGE {
164 return Err(DynoxideError::ValidationException(
165 "Invalid KeySchema: The second KeySchemaElement is not a RANGE key type".to_string(),
166 ));
167 }
168
169 Ok(())
170}
171
172pub fn validate_attribute_definitions(defs: &[AttributeDefinition]) -> Result<()> {
174 if defs.is_empty() {
175 return Err(DynoxideError::ValidationException(
176 "1 validation error detected: Value null at 'attributeDefinitions' failed to satisfy \
177 constraint: Member must have length greater than or equal to 1"
178 .to_string(),
179 ));
180 }
181
182 for def in defs {
183 match def.attribute_type {
184 ScalarAttributeType::S | ScalarAttributeType::N | ScalarAttributeType::B => {}
185 }
186 }
187
188 Ok(())
189}
190
191pub fn validate_key_attributes_in_definitions(
193 key_schema: &[KeySchemaElement],
194 definitions: &[AttributeDefinition],
195) -> Result<()> {
196 for key_elem in key_schema {
197 let found = definitions
198 .iter()
199 .any(|def| def.attribute_name == key_elem.attribute_name);
200 if !found {
201 return Err(DynoxideError::ValidationException(format!(
202 "One or more parameter values were invalid: Some index key attributes are not \
203 defined in AttributeDefinitions. Keys: [{}], AttributeDefinitions: [{}]",
204 key_elem.attribute_name,
205 definitions
206 .iter()
207 .map(|d| d.attribute_name.as_str())
208 .collect::<Vec<_>>()
209 .join(", ")
210 )));
211 }
212 }
213
214 Ok(())
215}
216
217pub fn validate_gsi(
219 gsi: &GlobalSecondaryIndex,
220 all_definitions: &[AttributeDefinition],
221) -> Result<()> {
222 if gsi.index_name.len() < 3 || gsi.index_name.len() > 255 {
224 return Err(DynoxideError::ValidationException(format!(
225 "1 validation error detected: Value '{}' at 'globalSecondaryIndexes.1.member.indexName' \
226 failed to satisfy constraint: Member must have length greater than or equal to 3",
227 gsi.index_name
228 )));
229 }
230
231 if !gsi
233 .index_name
234 .chars()
235 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
236 {
237 return Err(DynoxideError::ValidationException(format!(
238 "1 validation error detected: Value '{}' at 'globalSecondaryIndexes.1.member.indexName' \
239 failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
240 gsi.index_name
241 )));
242 }
243
244 validate_key_schema(&gsi.key_schema)?;
246
247 validate_projection(&gsi.projection, &gsi.index_name)?;
249
250 validate_key_attributes_in_definitions(&gsi.key_schema, all_definitions)?;
252
253 Ok(())
254}
255
256pub fn validate_projection(projection: &crate::types::Projection, _index_name: &str) -> Result<()> {
262 match &projection.projection_type {
263 None => {
264 return Err(DynoxideError::ValidationException(
265 "One or more parameter values were invalid: Unknown ProjectionType: null"
266 .to_string(),
267 ));
268 }
269 Some(pt) => {
270 if let Some(ref nka) = projection.non_key_attributes {
271 match pt {
273 crate::types::ProjectionType::ALL => {
274 return Err(DynoxideError::ValidationException(
275 "One or more parameter values were invalid: \
276 ProjectionType is ALL, but NonKeyAttributes is specified"
277 .to_string(),
278 ));
279 }
280 crate::types::ProjectionType::KEYS_ONLY => {
281 return Err(DynoxideError::ValidationException(
282 "One or more parameter values were invalid: \
283 ProjectionType is KEYS_ONLY, but NonKeyAttributes is specified"
284 .to_string(),
285 ));
286 }
287 crate::types::ProjectionType::INCLUDE => {
288 if nka.is_empty() {
290 return Err(DynoxideError::ValidationException(
291 "One or more parameter values were invalid: \
292 NonKeyAttributes must not be empty"
293 .to_string(),
294 ));
295 }
296 }
297 }
298 }
299 }
300 }
301 Ok(())
302}
303
304pub fn partition_key_name(key_schema: &[KeySchemaElement]) -> Option<&str> {
306 key_schema
307 .iter()
308 .find(|k| k.key_type == KeyType::HASH)
309 .map(|k| k.attribute_name.as_str())
310}
311
312const MAX_NESTING_DEPTH: usize = 32;
316
317const NESTING_LIMIT_MESSAGE: &str = "Nesting Levels have exceeded supported limits: Attributes in the item have nested levels beyond supported limit";
320
321pub fn validate_item_attribute_values(item: &Item) -> Result<()> {
337 for value in item.values() {
338 validate_attribute_value(value, 0)?;
339 }
340 Ok(())
341}
342
343fn validate_attribute_value(value: &AttributeValue, depth: usize) -> Result<()> {
344 if depth >= MAX_NESTING_DEPTH {
345 return Err(DynoxideError::ValidationException(
346 NESTING_LIMIT_MESSAGE.to_string(),
347 ));
348 }
349 match value {
350 AttributeValue::SS(set) if set.is_empty() => Err(DynoxideError::ValidationException(
351 "One or more parameter values were invalid: An string set may not be empty"
352 .to_string(),
353 )),
354 AttributeValue::NS(set) if set.is_empty() => Err(DynoxideError::ValidationException(
355 "One or more parameter values were invalid: An number set may not be empty"
356 .to_string(),
357 )),
358 AttributeValue::BS(set) if set.is_empty() => Err(DynoxideError::ValidationException(
359 "One or more parameter values were invalid: Binary sets should not be empty"
360 .to_string(),
361 )),
362 AttributeValue::SS(set) if !set.is_empty() => {
363 let mut seen = std::collections::HashSet::new();
364 for s in set {
365 if !seen.insert(s.clone()) {
366 let display: Vec<&str> = set.iter().map(|s| s.as_str()).collect();
367 return Err(DynoxideError::ValidationException(format!(
368 "One or more parameter values were invalid: Input collection [{}] contains duplicates.",
369 display.join(", ")
370 )));
371 }
372 }
373 Ok(())
374 }
375 AttributeValue::BS(set) if !set.is_empty() => {
376 let mut seen = std::collections::HashSet::new();
377 for b in set {
378 if !seen.insert(b.clone()) {
379 use base64::Engine;
380 let display: Vec<String> = set
381 .iter()
382 .map(|s| base64::engine::general_purpose::STANDARD.encode(s))
383 .collect();
384 return Err(DynoxideError::ValidationException(format!(
385 "One or more parameter values were invalid: Input collection [{}]of type BS contains duplicates.",
386 display.join(", ")
387 )));
388 }
389 }
390 Ok(())
391 }
392 AttributeValue::NS(set) if !set.is_empty() => {
393 for n in set {
394 crate::types::validate_dynamo_number(n)?;
395 }
396 let mut seen = std::collections::HashSet::new();
398 for n in set {
399 let normalized = crate::types::normalize_dynamo_number(n);
400 if !seen.insert(normalized) {
401 return Err(DynoxideError::ValidationException(
402 "Input collection contains duplicates".to_string(),
403 ));
404 }
405 }
406 Ok(())
407 }
408 AttributeValue::N(n) => {
409 crate::types::validate_dynamo_number(n)?;
410 Ok(())
411 }
412 AttributeValue::L(list) => {
413 for v in list {
414 validate_attribute_value(v, depth + 1)?;
415 }
416 Ok(())
417 }
418 AttributeValue::M(map) => {
419 for v in map.values() {
420 validate_attribute_value(v, depth + 1)?;
421 }
422 Ok(())
423 }
424 _ => Ok(()),
425 }
426}
427
428pub fn validate_nesting_depth(value: &AttributeValue) -> Result<()> {
437 check_nesting_depth(value, 0)
438}
439
440fn check_nesting_depth(value: &AttributeValue, depth: usize) -> Result<()> {
441 if depth >= MAX_NESTING_DEPTH {
442 return Err(DynoxideError::ValidationException(
443 NESTING_LIMIT_MESSAGE.to_string(),
444 ));
445 }
446 match value {
447 AttributeValue::L(list) => list
448 .iter()
449 .try_for_each(|v| check_nesting_depth(v, depth + 1)),
450 AttributeValue::M(map) => map
451 .values()
452 .try_for_each(|v| check_nesting_depth(v, depth + 1)),
453 _ => Ok(()),
454 }
455}
456
457pub fn validate_key_attribute_values(key: &Item) -> Result<()> {
466 for value in key.values() {
467 validate_key_attr_value(value)?;
468 }
469 Ok(())
470}
471
472fn validate_key_attr_value(value: &AttributeValue) -> Result<()> {
473 match value {
474 AttributeValue::SS(set) if set.is_empty() => {
475 return Err(DynoxideError::ValidationException(
476 "One or more parameter values were invalid: An string set may not be empty"
477 .to_string(),
478 ));
479 }
480 AttributeValue::NS(set) if set.is_empty() => {
481 return Err(DynoxideError::ValidationException(
482 "One or more parameter values were invalid: An number set may not be empty"
483 .to_string(),
484 ));
485 }
486 AttributeValue::BS(set) if set.is_empty() => {
487 return Err(DynoxideError::ValidationException(
488 "One or more parameter values were invalid: Binary sets should not be empty"
489 .to_string(),
490 ));
491 }
492 AttributeValue::SS(set) => {
493 let mut seen = std::collections::HashSet::new();
495 for s in set {
496 if !seen.insert(s.clone()) {
497 let display: Vec<&str> = set.iter().map(|s| s.as_str()).collect();
498 return Err(DynoxideError::ValidationException(format!(
499 "One or more parameter values were invalid: \
500 Input collection [{}] contains duplicates.",
501 display.join(", ")
502 )));
503 }
504 }
505 }
506 AttributeValue::NS(set) if !set.is_empty() => {
507 for n in set {
509 crate::types::validate_dynamo_number(n)?;
510 }
511 let mut seen = std::collections::HashSet::new();
512 for n in set {
513 let normalized = crate::types::normalize_dynamo_number(n);
514 if !seen.insert(normalized) {
515 return Err(DynoxideError::ValidationException(
516 "Input collection contains duplicates".to_string(),
517 ));
518 }
519 }
520 }
521 AttributeValue::BS(set) => {
522 let mut seen = std::collections::HashSet::new();
524 for b in set {
525 if !seen.insert(b.clone()) {
526 use base64::Engine;
527 let display: Vec<String> = set
528 .iter()
529 .map(|s| base64::engine::general_purpose::STANDARD.encode(s))
530 .collect();
531 return Err(DynoxideError::ValidationException(format!(
532 "One or more parameter values were invalid: \
533 Input collection [{}]of type BS contains duplicates.",
534 display.join(", ")
535 )));
536 }
537 }
538 }
539 AttributeValue::N(n) => {
540 crate::types::validate_dynamo_number(n)?;
541 }
542 _ => {}
543 }
544 Ok(())
545}
546
547pub fn normalize_item_sets(item: &mut Item) {
555 for value in item.values_mut() {
556 normalize_attribute_sets(value);
557 }
558}
559
560fn normalize_attribute_sets(value: &mut AttributeValue) {
561 match value {
562 AttributeValue::N(n) => {
563 *n = crate::types::normalize_dynamo_number(n);
564 }
565 AttributeValue::SS(set) => {
566 let mut seen = std::collections::HashSet::new();
567 set.retain(|s| seen.insert(s.clone()));
568 }
569 AttributeValue::NS(set) => {
570 let mut seen = std::collections::HashSet::new();
571 set.retain(|n| seen.insert(normalize_number_for_dedup(n)));
572 for n in set.iter_mut() {
574 *n = crate::types::normalize_dynamo_number(n);
575 }
576 }
577 AttributeValue::BS(set) => {
578 let mut seen = std::collections::HashSet::new();
579 set.retain(|b| seen.insert(b.clone()));
580 }
581 AttributeValue::L(list) => {
582 for v in list.iter_mut() {
583 normalize_attribute_sets(v);
584 }
585 }
586 AttributeValue::M(map) => {
587 for v in map.values_mut() {
588 normalize_attribute_sets(v);
589 }
590 }
591 _ => {}
592 }
593}
594
595fn normalize_number_for_dedup(n: &str) -> String {
599 let trimmed = n.trim();
600 let negative = trimmed.starts_with('-');
601 let abs_str = if negative { &trimmed[1..] } else { trimmed };
602
603 let (digits, exponent) = crate::types::parse_number_parts(abs_str);
604
605 if digits.is_empty() {
606 return "0".to_string();
607 }
608
609 let mantissa: String = digits.iter().map(|&d| (b'0' + d) as char).collect();
610 let sign = if negative { "-" } else { "" };
611 format!("{sign}{mantissa}E{exponent}")
612}
613
614pub fn validate_lsi(
616 lsi: &crate::types::LocalSecondaryIndex,
617 table_key_schema: &[KeySchemaElement],
618 all_definitions: &[AttributeDefinition],
619) -> Result<()> {
620 if lsi.index_name.len() < 3 || lsi.index_name.len() > 255 {
622 return Err(DynoxideError::ValidationException(format!(
623 "1 validation error detected: Value '{}' at 'localSecondaryIndexes.1.member.indexName' \
624 failed to satisfy constraint: Member must have length greater than or equal to 3",
625 lsi.index_name
626 )));
627 }
628
629 if !lsi
631 .index_name
632 .chars()
633 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
634 {
635 return Err(DynoxideError::ValidationException(format!(
636 "1 validation error detected: Value '{}' at 'localSecondaryIndexes.1.member.indexName' \
637 failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
638 lsi.index_name
639 )));
640 }
641
642 validate_key_schema(&lsi.key_schema)?;
644
645 validate_projection(&lsi.projection, &lsi.index_name)?;
647
648 let lsi_pk = lsi
650 .key_schema
651 .iter()
652 .find(|k| k.key_type == KeyType::HASH)
653 .map(|k| k.attribute_name.as_str());
654 let lsi_sk = lsi
655 .key_schema
656 .iter()
657 .find(|k| k.key_type == KeyType::RANGE)
658 .map(|k| k.attribute_name.as_str());
659
660 let table_pk = partition_key_name(table_key_schema);
661 let table_sk = sort_key_name(table_key_schema);
662
663 if lsi_pk != table_pk {
665 return Err(DynoxideError::ValidationException(
666 "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(),
667 ));
668 }
669
670 if lsi_sk.is_some() && lsi_sk == table_sk {
672 return Err(DynoxideError::ValidationException(
673 "One or more parameter values were invalid: Index KeySchema: The index KeySchema must not be the same as the table KeySchema".to_string(),
674 ));
675 }
676
677 validate_key_attributes_in_definitions(&lsi.key_schema, all_definitions)?;
679
680 Ok(())
681}
682
683pub fn sort_key_name(key_schema: &[KeySchemaElement]) -> Option<&str> {
685 key_schema
686 .iter()
687 .find(|k| k.key_type == KeyType::RANGE)
688 .map(|k| k.attribute_name.as_str())
689}
690
691#[cfg(test)]
692mod tests {
693 use super::*;
694
695 fn hash_key(name: &str) -> KeySchemaElement {
696 KeySchemaElement {
697 attribute_name: name.to_string(),
698 key_type: KeyType::HASH,
699 }
700 }
701
702 fn range_key(name: &str) -> KeySchemaElement {
703 KeySchemaElement {
704 attribute_name: name.to_string(),
705 key_type: KeyType::RANGE,
706 }
707 }
708
709 fn attr_def(name: &str, attr_type: ScalarAttributeType) -> AttributeDefinition {
710 AttributeDefinition {
711 attribute_name: name.to_string(),
712 attribute_type: attr_type,
713 }
714 }
715
716 #[test]
717 fn test_valid_table_name() {
718 assert!(validate_table_name("MyTable").is_ok());
719 assert!(validate_table_name("my-table.v2").is_ok());
720 assert!(validate_table_name("a_b").is_ok());
721 }
722
723 #[test]
724 fn test_short_table_name_accepted_for_read_write() {
725 assert!(validate_table_name("ab").is_ok());
729 assert!(validate_table_name("a").is_ok());
730 }
731
732 #[test]
733 fn test_empty_table_name_rejected_for_read_write() {
734 let err = validate_table_name("").unwrap_err().to_string();
735 assert!(err.contains("Member must have length greater than or equal to 1"));
736 assert!(!err.contains("greater than or equal to 3"));
737 }
738
739 #[test]
740 fn test_invalid_table_name_bad_chars() {
741 assert!(validate_table_name("my table").is_err());
742 assert!(validate_table_name("my@table").is_err());
743 }
744
745 #[test]
746 fn test_create_table_context_keeps_min_length_3() {
747 let errs = table_name_constraint_errors(Some("ab"), TableNameContext::CreateTable);
748 assert!(
749 errs.iter()
750 .any(|e| e.contains("Member must have length greater than or equal to 3"))
751 );
752 }
753
754 #[test]
755 fn test_valid_key_schema() {
756 let schema = vec![hash_key("pk")];
757 assert!(validate_key_schema(&schema).is_ok());
758
759 let schema = vec![hash_key("pk"), range_key("sk")];
760 assert!(validate_key_schema(&schema).is_ok());
761 }
762
763 #[test]
764 fn test_invalid_key_schema_empty() {
765 assert!(validate_key_schema(&[]).is_err());
766 }
767
768 #[test]
769 fn test_invalid_key_schema_no_hash() {
770 let schema = vec![range_key("sk")];
771 assert!(validate_key_schema(&schema).is_err());
772 }
773
774 #[test]
775 fn test_valid_key_attributes_in_definitions() {
776 let schema = vec![hash_key("pk"), range_key("sk")];
777 let defs = vec![
778 attr_def("pk", ScalarAttributeType::S),
779 attr_def("sk", ScalarAttributeType::N),
780 ];
781 assert!(validate_key_attributes_in_definitions(&schema, &defs).is_ok());
782 }
783
784 #[test]
785 fn test_missing_key_attribute_in_definitions() {
786 let schema = vec![hash_key("pk"), range_key("sk")];
787 let defs = vec![attr_def("pk", ScalarAttributeType::S)];
788 assert!(validate_key_attributes_in_definitions(&schema, &defs).is_err());
789 }
790
791 #[test]
792 fn test_partition_key_name() {
793 let schema = vec![hash_key("pk"), range_key("sk")];
794 assert_eq!(partition_key_name(&schema), Some("pk"));
795 }
796
797 #[test]
798 fn test_sort_key_name() {
799 let schema = vec![hash_key("pk"), range_key("sk")];
800 assert_eq!(sort_key_name(&schema), Some("sk"));
801
802 let schema = vec![hash_key("pk")];
803 assert_eq!(sort_key_name(&schema), None);
804 }
805}