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;
314
315pub fn validate_item_attribute_values(item: &Item) -> Result<()> {
331 for value in item.values() {
332 validate_attribute_value(value, 0)?;
333 }
334 Ok(())
335}
336
337fn validate_attribute_value(value: &AttributeValue, depth: usize) -> Result<()> {
338 if depth > MAX_NESTING_DEPTH {
339 return Err(DynoxideError::ValidationException(
340 "Nesting level exceeds limit of 32".to_string(),
341 ));
342 }
343 match value {
344 AttributeValue::NULL(b) if !b => Err(DynoxideError::ValidationException(
345 "One or more parameter values were invalid: \
346 Null attribute value types must have the value of true"
347 .to_string(),
348 )),
349 AttributeValue::SS(set) if set.is_empty() => Err(DynoxideError::ValidationException(
350 "One or more parameter values were invalid: An string set may not be empty"
351 .to_string(),
352 )),
353 AttributeValue::NS(set) if set.is_empty() => Err(DynoxideError::ValidationException(
354 "One or more parameter values were invalid: An number set may not be empty"
355 .to_string(),
356 )),
357 AttributeValue::BS(set) if set.is_empty() => Err(DynoxideError::ValidationException(
358 "One or more parameter values were invalid: Binary sets should not be empty"
359 .to_string(),
360 )),
361 AttributeValue::SS(set) if !set.is_empty() => {
362 let mut seen = std::collections::HashSet::new();
363 for s in set {
364 if !seen.insert(s.clone()) {
365 let display: Vec<&str> = set.iter().map(|s| s.as_str()).collect();
366 return Err(DynoxideError::ValidationException(format!(
367 "One or more parameter values were invalid: Input collection [{}] contains duplicates.",
368 display.join(", ")
369 )));
370 }
371 }
372 Ok(())
373 }
374 AttributeValue::BS(set) if !set.is_empty() => {
375 let mut seen = std::collections::HashSet::new();
376 for b in set {
377 if !seen.insert(b.clone()) {
378 use base64::Engine;
379 let display: Vec<String> = set
380 .iter()
381 .map(|s| base64::engine::general_purpose::STANDARD.encode(s))
382 .collect();
383 return Err(DynoxideError::ValidationException(format!(
384 "One or more parameter values were invalid: Input collection [{}]of type BS contains duplicates.",
385 display.join(", ")
386 )));
387 }
388 }
389 Ok(())
390 }
391 AttributeValue::NS(set) if !set.is_empty() => {
392 for n in set {
393 crate::types::validate_dynamo_number(n)?;
394 }
395 let mut seen = std::collections::HashSet::new();
397 for n in set {
398 let normalized = crate::types::normalize_dynamo_number(n);
399 if !seen.insert(normalized) {
400 return Err(DynoxideError::ValidationException(
401 "Input collection contains duplicates".to_string(),
402 ));
403 }
404 }
405 Ok(())
406 }
407 AttributeValue::N(n) => {
408 crate::types::validate_dynamo_number(n)?;
409 Ok(())
410 }
411 AttributeValue::L(list) => {
412 for v in list {
413 validate_attribute_value(v, depth + 1)?;
414 }
415 Ok(())
416 }
417 AttributeValue::M(map) => {
418 for v in map.values() {
419 validate_attribute_value(v, depth + 1)?;
420 }
421 Ok(())
422 }
423 _ => Ok(()),
424 }
425}
426
427pub fn validate_key_attribute_values(key: &Item) -> Result<()> {
437 for value in key.values() {
438 validate_key_attr_value(value)?;
439 }
440 Ok(())
441}
442
443fn validate_key_attr_value(value: &AttributeValue) -> Result<()> {
444 match value {
445 AttributeValue::NULL(b) if !b => {
446 return Err(DynoxideError::ValidationException(
447 "One or more parameter values were invalid: \
448 Null attribute value types must have the value of true"
449 .to_string(),
450 ));
451 }
452 AttributeValue::SS(set) if set.is_empty() => {
453 return Err(DynoxideError::ValidationException(
454 "One or more parameter values were invalid: An string set may not be empty"
455 .to_string(),
456 ));
457 }
458 AttributeValue::NS(set) if set.is_empty() => {
459 return Err(DynoxideError::ValidationException(
460 "One or more parameter values were invalid: An number set may not be empty"
461 .to_string(),
462 ));
463 }
464 AttributeValue::BS(set) if set.is_empty() => {
465 return Err(DynoxideError::ValidationException(
466 "One or more parameter values were invalid: Binary sets should not be empty"
467 .to_string(),
468 ));
469 }
470 AttributeValue::SS(set) => {
471 let mut seen = std::collections::HashSet::new();
473 for s in set {
474 if !seen.insert(s.clone()) {
475 let display: Vec<&str> = set.iter().map(|s| s.as_str()).collect();
476 return Err(DynoxideError::ValidationException(format!(
477 "One or more parameter values were invalid: \
478 Input collection [{}] contains duplicates.",
479 display.join(", ")
480 )));
481 }
482 }
483 }
484 AttributeValue::NS(set) if !set.is_empty() => {
485 for n in set {
487 crate::types::validate_dynamo_number(n)?;
488 }
489 let mut seen = std::collections::HashSet::new();
490 for n in set {
491 let normalized = crate::types::normalize_dynamo_number(n);
492 if !seen.insert(normalized) {
493 return Err(DynoxideError::ValidationException(
494 "Input collection contains duplicates".to_string(),
495 ));
496 }
497 }
498 }
499 AttributeValue::BS(set) => {
500 let mut seen = std::collections::HashSet::new();
502 for b in set {
503 if !seen.insert(b.clone()) {
504 use base64::Engine;
505 let display: Vec<String> = set
506 .iter()
507 .map(|s| base64::engine::general_purpose::STANDARD.encode(s))
508 .collect();
509 return Err(DynoxideError::ValidationException(format!(
510 "One or more parameter values were invalid: \
511 Input collection [{}]of type BS contains duplicates.",
512 display.join(", ")
513 )));
514 }
515 }
516 }
517 AttributeValue::N(n) => {
518 crate::types::validate_dynamo_number(n)?;
519 }
520 _ => {}
521 }
522 Ok(())
523}
524
525pub fn normalize_item_sets(item: &mut Item) {
533 for value in item.values_mut() {
534 normalize_attribute_sets(value);
535 }
536}
537
538fn normalize_attribute_sets(value: &mut AttributeValue) {
539 match value {
540 AttributeValue::N(n) => {
541 *n = crate::types::normalize_dynamo_number(n);
542 }
543 AttributeValue::SS(set) => {
544 let mut seen = std::collections::HashSet::new();
545 set.retain(|s| seen.insert(s.clone()));
546 }
547 AttributeValue::NS(set) => {
548 let mut seen = std::collections::HashSet::new();
549 set.retain(|n| seen.insert(normalize_number_for_dedup(n)));
550 for n in set.iter_mut() {
552 *n = crate::types::normalize_dynamo_number(n);
553 }
554 }
555 AttributeValue::BS(set) => {
556 let mut seen = std::collections::HashSet::new();
557 set.retain(|b| seen.insert(b.clone()));
558 }
559 AttributeValue::L(list) => {
560 for v in list.iter_mut() {
561 normalize_attribute_sets(v);
562 }
563 }
564 AttributeValue::M(map) => {
565 for v in map.values_mut() {
566 normalize_attribute_sets(v);
567 }
568 }
569 _ => {}
570 }
571}
572
573fn normalize_number_for_dedup(n: &str) -> String {
577 let trimmed = n.trim();
578 let negative = trimmed.starts_with('-');
579 let abs_str = if negative { &trimmed[1..] } else { trimmed };
580
581 let (digits, exponent) = crate::types::parse_number_parts(abs_str);
582
583 if digits.is_empty() {
584 return "0".to_string();
585 }
586
587 let mantissa: String = digits.iter().map(|&d| (b'0' + d) as char).collect();
588 let sign = if negative { "-" } else { "" };
589 format!("{sign}{mantissa}E{exponent}")
590}
591
592pub fn validate_lsi(
594 lsi: &crate::types::LocalSecondaryIndex,
595 table_key_schema: &[KeySchemaElement],
596 all_definitions: &[AttributeDefinition],
597) -> Result<()> {
598 if lsi.index_name.len() < 3 || lsi.index_name.len() > 255 {
600 return Err(DynoxideError::ValidationException(format!(
601 "1 validation error detected: Value '{}' at 'localSecondaryIndexes.1.member.indexName' \
602 failed to satisfy constraint: Member must have length greater than or equal to 3",
603 lsi.index_name
604 )));
605 }
606
607 if !lsi
609 .index_name
610 .chars()
611 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
612 {
613 return Err(DynoxideError::ValidationException(format!(
614 "1 validation error detected: Value '{}' at 'localSecondaryIndexes.1.member.indexName' \
615 failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
616 lsi.index_name
617 )));
618 }
619
620 validate_key_schema(&lsi.key_schema)?;
622
623 validate_projection(&lsi.projection, &lsi.index_name)?;
625
626 let lsi_pk = lsi
628 .key_schema
629 .iter()
630 .find(|k| k.key_type == KeyType::HASH)
631 .map(|k| k.attribute_name.as_str());
632 let lsi_sk = lsi
633 .key_schema
634 .iter()
635 .find(|k| k.key_type == KeyType::RANGE)
636 .map(|k| k.attribute_name.as_str());
637
638 let table_pk = partition_key_name(table_key_schema);
639 let table_sk = sort_key_name(table_key_schema);
640
641 if lsi_pk != table_pk {
643 return Err(DynoxideError::ValidationException(
644 "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(),
645 ));
646 }
647
648 if lsi_sk.is_some() && lsi_sk == table_sk {
650 return Err(DynoxideError::ValidationException(
651 "One or more parameter values were invalid: Index KeySchema: The index KeySchema must not be the same as the table KeySchema".to_string(),
652 ));
653 }
654
655 validate_key_attributes_in_definitions(&lsi.key_schema, all_definitions)?;
657
658 Ok(())
659}
660
661pub fn sort_key_name(key_schema: &[KeySchemaElement]) -> Option<&str> {
663 key_schema
664 .iter()
665 .find(|k| k.key_type == KeyType::RANGE)
666 .map(|k| k.attribute_name.as_str())
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672
673 fn hash_key(name: &str) -> KeySchemaElement {
674 KeySchemaElement {
675 attribute_name: name.to_string(),
676 key_type: KeyType::HASH,
677 }
678 }
679
680 fn range_key(name: &str) -> KeySchemaElement {
681 KeySchemaElement {
682 attribute_name: name.to_string(),
683 key_type: KeyType::RANGE,
684 }
685 }
686
687 fn attr_def(name: &str, attr_type: ScalarAttributeType) -> AttributeDefinition {
688 AttributeDefinition {
689 attribute_name: name.to_string(),
690 attribute_type: attr_type,
691 }
692 }
693
694 #[test]
695 fn test_valid_table_name() {
696 assert!(validate_table_name("MyTable").is_ok());
697 assert!(validate_table_name("my-table.v2").is_ok());
698 assert!(validate_table_name("a_b").is_ok());
699 }
700
701 #[test]
702 fn test_short_table_name_accepted_for_read_write() {
703 assert!(validate_table_name("ab").is_ok());
707 assert!(validate_table_name("a").is_ok());
708 }
709
710 #[test]
711 fn test_empty_table_name_rejected_for_read_write() {
712 let err = validate_table_name("").unwrap_err().to_string();
713 assert!(err.contains("Member must have length greater than or equal to 1"));
714 assert!(!err.contains("greater than or equal to 3"));
715 }
716
717 #[test]
718 fn test_invalid_table_name_bad_chars() {
719 assert!(validate_table_name("my table").is_err());
720 assert!(validate_table_name("my@table").is_err());
721 }
722
723 #[test]
724 fn test_create_table_context_keeps_min_length_3() {
725 let errs = table_name_constraint_errors(Some("ab"), TableNameContext::CreateTable);
726 assert!(
727 errs.iter()
728 .any(|e| e.contains("Member must have length greater than or equal to 3"))
729 );
730 }
731
732 #[test]
733 fn test_valid_key_schema() {
734 let schema = vec![hash_key("pk")];
735 assert!(validate_key_schema(&schema).is_ok());
736
737 let schema = vec![hash_key("pk"), range_key("sk")];
738 assert!(validate_key_schema(&schema).is_ok());
739 }
740
741 #[test]
742 fn test_invalid_key_schema_empty() {
743 assert!(validate_key_schema(&[]).is_err());
744 }
745
746 #[test]
747 fn test_invalid_key_schema_no_hash() {
748 let schema = vec![range_key("sk")];
749 assert!(validate_key_schema(&schema).is_err());
750 }
751
752 #[test]
753 fn test_valid_key_attributes_in_definitions() {
754 let schema = vec![hash_key("pk"), range_key("sk")];
755 let defs = vec![
756 attr_def("pk", ScalarAttributeType::S),
757 attr_def("sk", ScalarAttributeType::N),
758 ];
759 assert!(validate_key_attributes_in_definitions(&schema, &defs).is_ok());
760 }
761
762 #[test]
763 fn test_missing_key_attribute_in_definitions() {
764 let schema = vec![hash_key("pk"), range_key("sk")];
765 let defs = vec![attr_def("pk", ScalarAttributeType::S)];
766 assert!(validate_key_attributes_in_definitions(&schema, &defs).is_err());
767 }
768
769 #[test]
770 fn test_partition_key_name() {
771 let schema = vec![hash_key("pk"), range_key("sk")];
772 assert_eq!(partition_key_name(&schema), Some("pk"));
773 }
774
775 #[test]
776 fn test_sort_key_name() {
777 let schema = vec![hash_key("pk"), range_key("sk")];
778 assert_eq!(sort_key_name(&schema), Some("sk"));
779
780 let schema = vec![hash_key("pk")];
781 assert_eq!(sort_key_name(&schema), None);
782 }
783}