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 if let [single] = errors {
127 return Some(envelope_message(single));
128 }
129 Some(format!(
130 "{} validation errors detected: {}",
131 errors.len(),
132 errors.join("; ")
133 ))
134}
135
136pub(crate) fn envelope_message(msg: &str) -> String {
139 format!("1 validation error detected: {msg}")
140}
141
142pub(crate) fn envelope_request_validation(err: DynoxideError) -> DynoxideError {
148 match err {
149 DynoxideError::EnvelopedValidation(msg) => {
150 DynoxideError::ValidationException(envelope_message(&msg))
151 }
152 other => other,
153 }
154}
155
156#[cfg(any(
166 feature = "http-server",
167 feature = "mcp-server",
168 feature = "wasm-sqlite",
169 test
170))]
171pub(crate) fn strip_request_validation_tag(err: DynoxideError) -> DynoxideError {
172 match err {
173 DynoxideError::EnvelopedValidation(msg) => DynoxideError::ValidationException(msg),
174 other => other,
175 }
176}
177
178#[cfg(any(feature = "http-server", feature = "wasm-sqlite", test))]
194pub(crate) fn resolve_request_validation_tag(operation: &str, err: DynoxideError) -> DynoxideError {
195 match operation {
196 "PutItem" | "UpdateItem" => envelope_request_validation(err),
197 _ => strip_request_validation_tag(err),
198 }
199}
200
201#[derive(Debug)]
208pub(crate) enum ClassifiedValidationError {
209 Bare(DynoxideError),
212 Enveloped(String),
215}
216
217impl ClassifiedValidationError {
218 pub(crate) fn bare(message: impl Into<String>) -> Self {
220 Self::Bare(DynoxideError::ValidationException(message.into()))
221 }
222
223 pub(crate) fn enveloped(message: impl Into<String>) -> Self {
226 Self::Enveloped(message.into())
227 }
228
229 pub(crate) fn into_tagged(self) -> DynoxideError {
233 match self {
234 Self::Enveloped(msg) => DynoxideError::EnvelopedValidation(msg),
235 Self::Bare(err) => err,
236 }
237 }
238}
239
240impl From<ClassifiedValidationError> for DynoxideError {
241 fn from(e: ClassifiedValidationError) -> Self {
242 match e {
243 ClassifiedValidationError::Enveloped(msg) => DynoxideError::ValidationException(msg),
244 ClassifiedValidationError::Bare(err) => err,
245 }
246 }
247}
248
249impl From<DynoxideError> for ClassifiedValidationError {
250 fn from(error: DynoxideError) -> Self {
253 Self::Bare(error)
254 }
255}
256
257pub fn validate_key_schema(key_schema: &[KeySchemaElement]) -> Result<()> {
262 if key_schema.is_empty() || key_schema.len() > 2 {
263 return Err(DynoxideError::ValidationException(
264 "1 validation error detected: Value null at 'keySchema' failed to satisfy constraint: \
265 Member must have length less than or equal to 2"
266 .to_string(),
267 ));
268 }
269
270 if key_schema[0].key_type != KeyType::HASH {
272 return Err(DynoxideError::ValidationException(
273 "Invalid KeySchema: The first KeySchemaElement is not a HASH key type".to_string(),
274 ));
275 }
276
277 if key_schema.len() == 2 && key_schema[0].attribute_name == key_schema[1].attribute_name {
279 return Err(DynoxideError::ValidationException(
280 "Both the Hash Key and the Range Key element in the KeySchema have the same name"
281 .to_string(),
282 ));
283 }
284
285 if key_schema.len() == 2 && key_schema[1].key_type != KeyType::RANGE {
287 return Err(DynoxideError::ValidationException(
288 "Invalid KeySchema: The second KeySchemaElement is not a RANGE key type".to_string(),
289 ));
290 }
291
292 Ok(())
293}
294
295pub fn validate_attribute_definitions(defs: &[AttributeDefinition]) -> Result<()> {
297 if defs.is_empty() {
298 return Err(DynoxideError::ValidationException(
299 "1 validation error detected: Value null at 'attributeDefinitions' failed to satisfy \
300 constraint: Member must have length greater than or equal to 1"
301 .to_string(),
302 ));
303 }
304
305 for def in defs {
306 match def.attribute_type {
307 ScalarAttributeType::S | ScalarAttributeType::N | ScalarAttributeType::B => {}
308 }
309 }
310
311 Ok(())
312}
313
314pub fn validate_key_attributes_in_definitions(
316 key_schema: &[KeySchemaElement],
317 definitions: &[AttributeDefinition],
318) -> Result<()> {
319 for key_elem in key_schema {
320 let found = definitions
321 .iter()
322 .any(|def| def.attribute_name == key_elem.attribute_name);
323 if !found {
324 return Err(DynoxideError::ValidationException(format!(
325 "One or more parameter values were invalid: Some index key attributes are not \
326 defined in AttributeDefinitions. Keys: [{}], AttributeDefinitions: [{}]",
327 key_elem.attribute_name,
328 definitions
329 .iter()
330 .map(|d| d.attribute_name.as_str())
331 .collect::<Vec<_>>()
332 .join(", ")
333 )));
334 }
335 }
336
337 Ok(())
338}
339
340pub fn validate_gsi(
346 gsi: &GlobalSecondaryIndex,
347 request_definitions: &[AttributeDefinition],
348) -> Result<()> {
349 if gsi.index_name.len() < 3 || gsi.index_name.len() > 255 {
351 return Err(DynoxideError::ValidationException(format!(
352 "1 validation error detected: Value '{}' at 'globalSecondaryIndexes.1.member.indexName' \
353 failed to satisfy constraint: Member must have length greater than or equal to 3",
354 gsi.index_name
355 )));
356 }
357
358 if !gsi
360 .index_name
361 .chars()
362 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
363 {
364 return Err(DynoxideError::ValidationException(format!(
365 "1 validation error detected: Value '{}' at 'globalSecondaryIndexes.1.member.indexName' \
366 failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
367 gsi.index_name
368 )));
369 }
370
371 validate_key_schema(&gsi.key_schema)?;
373
374 validate_projection(&gsi.projection, &gsi.index_name)?;
376
377 validate_key_attributes_in_definitions(&gsi.key_schema, request_definitions)?;
379
380 Ok(())
381}
382
383pub fn validate_projection(projection: &crate::types::Projection, _index_name: &str) -> Result<()> {
389 match &projection.projection_type {
390 None => {
391 return Err(DynoxideError::ValidationException(
392 "One or more parameter values were invalid: Unknown ProjectionType: null"
393 .to_string(),
394 ));
395 }
396 Some(pt) => {
397 if let Some(ref nka) = projection.non_key_attributes {
398 match pt {
400 crate::types::ProjectionType::ALL => {
401 return Err(DynoxideError::ValidationException(
402 "One or more parameter values were invalid: \
403 ProjectionType is ALL, but NonKeyAttributes is specified"
404 .to_string(),
405 ));
406 }
407 crate::types::ProjectionType::KEYS_ONLY => {
408 return Err(DynoxideError::ValidationException(
409 "One or more parameter values were invalid: \
410 ProjectionType is KEYS_ONLY, but NonKeyAttributes is specified"
411 .to_string(),
412 ));
413 }
414 crate::types::ProjectionType::INCLUDE => {
415 if nka.is_empty() {
417 return Err(DynoxideError::ValidationException(
418 "One or more parameter values were invalid: \
419 NonKeyAttributes must not be empty"
420 .to_string(),
421 ));
422 }
423 }
424 }
425 }
426 }
427 }
428 Ok(())
429}
430
431pub fn partition_key_name(key_schema: &[KeySchemaElement]) -> Option<&str> {
433 key_schema
434 .iter()
435 .find(|k| k.key_type == KeyType::HASH)
436 .map(|k| k.attribute_name.as_str())
437}
438
439const MAX_NESTING_DEPTH: usize = 32;
443
444const NESTING_LIMIT_MESSAGE: &str = "Nesting Levels have exceeded supported limits: Attributes in the item have nested levels beyond supported limit";
447
448pub fn validate_item_attribute_values(item: &Item) -> crate::Result<()> {
464 validate_item_attribute_values_classified(item).map_err(Into::into)
465}
466
467pub(crate) fn validate_item_attribute_values_classified(
473 item: &Item,
474) -> std::result::Result<(), ClassifiedValidationError> {
475 for value in item.values() {
476 validate_attribute_value(value, 0)?;
477 }
478 Ok(())
479}
480
481fn validate_attribute_value(
482 value: &AttributeValue,
483 depth: usize,
484) -> std::result::Result<(), ClassifiedValidationError> {
485 if depth >= MAX_NESTING_DEPTH {
486 return Err(ClassifiedValidationError::bare(NESTING_LIMIT_MESSAGE));
487 }
488 match value {
489 AttributeValue::SS(set) if set.is_empty() => Err(ClassifiedValidationError::enveloped(
490 "One or more parameter values were invalid: An string set may not be empty",
491 )),
492 AttributeValue::NS(set) if set.is_empty() => Err(ClassifiedValidationError::enveloped(
493 "One or more parameter values were invalid: An number set may not be empty",
494 )),
495 AttributeValue::BS(set) if set.is_empty() => Err(ClassifiedValidationError::enveloped(
496 "One or more parameter values were invalid: Binary sets should not be empty",
497 )),
498 AttributeValue::SS(set) if !set.is_empty() => {
499 let mut seen = std::collections::HashSet::new();
500 for s in set {
501 if !seen.insert(s.clone()) {
502 let display: Vec<&str> = set.iter().map(|s| s.as_str()).collect();
503 return Err(ClassifiedValidationError::enveloped(format!(
504 "One or more parameter values were invalid: Input collection [{}] contains duplicates.",
505 display.join(", ")
506 )));
507 }
508 }
509 Ok(())
510 }
511 AttributeValue::BS(set) if !set.is_empty() => {
512 let mut seen = std::collections::HashSet::new();
513 for b in set {
514 if !seen.insert(b.clone()) {
515 use base64::Engine;
516 let display: Vec<String> = set
517 .iter()
518 .map(|s| base64::engine::general_purpose::STANDARD.encode(s))
519 .collect();
520 return Err(ClassifiedValidationError::enveloped(format!(
521 "One or more parameter values were invalid: Input collection [{}]of type BS contains duplicates.",
522 display.join(", ")
523 )));
524 }
525 }
526 Ok(())
527 }
528 AttributeValue::NS(set) if !set.is_empty() => {
529 for n in set {
530 crate::types::validate_dynamo_number(n)?;
531 }
532 let mut seen = std::collections::HashSet::new();
534 for n in set {
535 let normalized = crate::types::normalize_dynamo_number(n);
536 if !seen.insert(normalized) {
537 return Err(ClassifiedValidationError::enveloped(
538 "Input collection contains duplicates",
539 ));
540 }
541 }
542 Ok(())
543 }
544 AttributeValue::N(n) => {
545 crate::types::validate_dynamo_number(n)?;
546 Ok(())
547 }
548 AttributeValue::L(list) => {
549 for v in list {
550 validate_attribute_value(v, depth + 1)?;
551 }
552 Ok(())
553 }
554 AttributeValue::M(map) => {
555 for v in map.values() {
556 validate_attribute_value(v, depth + 1)?;
557 }
558 Ok(())
559 }
560 _ => Ok(()),
561 }
562}
563
564pub fn validate_nesting_depth(value: &AttributeValue) -> Result<()> {
573 check_nesting_depth(value, 0)
574}
575
576fn check_nesting_depth(value: &AttributeValue, depth: usize) -> Result<()> {
577 if depth >= MAX_NESTING_DEPTH {
578 return Err(DynoxideError::ValidationException(
579 NESTING_LIMIT_MESSAGE.to_string(),
580 ));
581 }
582 match value {
583 AttributeValue::L(list) => list
584 .iter()
585 .try_for_each(|v| check_nesting_depth(v, depth + 1)),
586 AttributeValue::M(map) => map
587 .values()
588 .try_for_each(|v| check_nesting_depth(v, depth + 1)),
589 _ => Ok(()),
590 }
591}
592
593pub fn validate_key_attribute_values(key: &Item) -> Result<()> {
602 for value in key.values() {
603 validate_key_attr_value(value)?;
604 }
605 Ok(())
606}
607
608fn validate_key_attr_value(value: &AttributeValue) -> Result<()> {
609 match value {
610 AttributeValue::SS(set) if set.is_empty() => {
611 return Err(DynoxideError::ValidationException(
612 "One or more parameter values were invalid: An string set may not be empty"
613 .to_string(),
614 ));
615 }
616 AttributeValue::NS(set) if set.is_empty() => {
617 return Err(DynoxideError::ValidationException(
618 "One or more parameter values were invalid: An number set may not be empty"
619 .to_string(),
620 ));
621 }
622 AttributeValue::BS(set) if set.is_empty() => {
623 return Err(DynoxideError::ValidationException(
624 "One or more parameter values were invalid: Binary sets should not be empty"
625 .to_string(),
626 ));
627 }
628 AttributeValue::SS(set) => {
629 let mut seen = std::collections::HashSet::new();
631 for s in set {
632 if !seen.insert(s.clone()) {
633 let display: Vec<&str> = set.iter().map(|s| s.as_str()).collect();
634 return Err(DynoxideError::ValidationException(format!(
635 "One or more parameter values were invalid: \
636 Input collection [{}] contains duplicates.",
637 display.join(", ")
638 )));
639 }
640 }
641 }
642 AttributeValue::NS(set) if !set.is_empty() => {
643 for n in set {
645 crate::types::validate_dynamo_number(n)?;
646 }
647 let mut seen = std::collections::HashSet::new();
648 for n in set {
649 let normalized = crate::types::normalize_dynamo_number(n);
650 if !seen.insert(normalized) {
651 return Err(DynoxideError::ValidationException(
652 "Input collection contains duplicates".to_string(),
653 ));
654 }
655 }
656 }
657 AttributeValue::BS(set) => {
658 let mut seen = std::collections::HashSet::new();
660 for b in set {
661 if !seen.insert(b.clone()) {
662 use base64::Engine;
663 let display: Vec<String> = set
664 .iter()
665 .map(|s| base64::engine::general_purpose::STANDARD.encode(s))
666 .collect();
667 return Err(DynoxideError::ValidationException(format!(
668 "One or more parameter values were invalid: \
669 Input collection [{}]of type BS contains duplicates.",
670 display.join(", ")
671 )));
672 }
673 }
674 }
675 AttributeValue::N(n) => {
676 crate::types::validate_dynamo_number(n)?;
677 }
678 _ => {}
679 }
680 Ok(())
681}
682
683pub fn normalize_item_sets(item: &mut Item) {
691 for value in item.values_mut() {
692 normalize_attribute_sets(value);
693 }
694}
695
696fn normalize_attribute_sets(value: &mut AttributeValue) {
697 match value {
698 AttributeValue::N(n) => {
699 *n = crate::types::normalize_dynamo_number(n);
700 }
701 AttributeValue::SS(set) => {
702 let mut seen = std::collections::HashSet::new();
703 set.retain(|s| seen.insert(s.clone()));
704 }
705 AttributeValue::NS(set) => {
706 let mut seen = std::collections::HashSet::new();
707 set.retain(|n| seen.insert(normalize_number_for_dedup(n)));
708 for n in set.iter_mut() {
710 *n = crate::types::normalize_dynamo_number(n);
711 }
712 }
713 AttributeValue::BS(set) => {
714 let mut seen = std::collections::HashSet::new();
715 set.retain(|b| seen.insert(b.clone()));
716 }
717 AttributeValue::L(list) => {
718 for v in list.iter_mut() {
719 normalize_attribute_sets(v);
720 }
721 }
722 AttributeValue::M(map) => {
723 for v in map.values_mut() {
724 normalize_attribute_sets(v);
725 }
726 }
727 _ => {}
728 }
729}
730
731fn normalize_number_for_dedup(n: &str) -> String {
735 let trimmed = n.trim();
736 let negative = trimmed.starts_with('-');
737 let abs_str = if negative { &trimmed[1..] } else { trimmed };
738
739 let (digits, exponent) = crate::types::parse_number_parts(abs_str);
740
741 if digits.is_empty() {
742 return "0".to_string();
743 }
744
745 let mantissa: String = digits.iter().map(|&d| (b'0' + d) as char).collect();
746 let sign = if negative { "-" } else { "" };
747 format!("{sign}{mantissa}E{exponent}")
748}
749
750pub fn validate_lsi(
752 lsi: &crate::types::LocalSecondaryIndex,
753 table_key_schema: &[KeySchemaElement],
754 all_definitions: &[AttributeDefinition],
755) -> Result<()> {
756 if lsi.index_name.len() < 3 || lsi.index_name.len() > 255 {
758 return Err(DynoxideError::ValidationException(format!(
759 "1 validation error detected: Value '{}' at 'localSecondaryIndexes.1.member.indexName' \
760 failed to satisfy constraint: Member must have length greater than or equal to 3",
761 lsi.index_name
762 )));
763 }
764
765 if !lsi
767 .index_name
768 .chars()
769 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
770 {
771 return Err(DynoxideError::ValidationException(format!(
772 "1 validation error detected: Value '{}' at 'localSecondaryIndexes.1.member.indexName' \
773 failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
774 lsi.index_name
775 )));
776 }
777
778 validate_key_schema(&lsi.key_schema)?;
780
781 validate_projection(&lsi.projection, &lsi.index_name)?;
783
784 let lsi_pk = lsi
786 .key_schema
787 .iter()
788 .find(|k| k.key_type == KeyType::HASH)
789 .map(|k| k.attribute_name.as_str());
790 let lsi_sk = lsi
791 .key_schema
792 .iter()
793 .find(|k| k.key_type == KeyType::RANGE)
794 .map(|k| k.attribute_name.as_str());
795
796 let table_pk = partition_key_name(table_key_schema);
797 let table_sk = sort_key_name(table_key_schema);
798
799 if lsi_pk != table_pk {
801 return Err(DynoxideError::ValidationException(
802 "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(),
803 ));
804 }
805
806 if lsi_sk.is_some() && lsi_sk == table_sk {
808 return Err(DynoxideError::ValidationException(
809 "One or more parameter values were invalid: Index KeySchema: The index KeySchema must not be the same as the table KeySchema".to_string(),
810 ));
811 }
812
813 validate_key_attributes_in_definitions(&lsi.key_schema, all_definitions)?;
815
816 Ok(())
817}
818
819pub fn sort_key_name(key_schema: &[KeySchemaElement]) -> Option<&str> {
821 key_schema
822 .iter()
823 .find(|k| k.key_type == KeyType::RANGE)
824 .map(|k| k.attribute_name.as_str())
825}
826
827#[cfg(test)]
828mod tests {
829 use super::*;
830
831 fn hash_key(name: &str) -> KeySchemaElement {
832 KeySchemaElement {
833 attribute_name: name.to_string(),
834 key_type: KeyType::HASH,
835 }
836 }
837
838 fn range_key(name: &str) -> KeySchemaElement {
839 KeySchemaElement {
840 attribute_name: name.to_string(),
841 key_type: KeyType::RANGE,
842 }
843 }
844
845 fn attr_def(name: &str, attr_type: ScalarAttributeType) -> AttributeDefinition {
846 AttributeDefinition {
847 attribute_name: name.to_string(),
848 attribute_type: attr_type,
849 }
850 }
851
852 #[test]
853 fn test_valid_table_name() {
854 assert!(validate_table_name("MyTable").is_ok());
855 assert!(validate_table_name("my-table.v2").is_ok());
856 assert!(validate_table_name("a_b").is_ok());
857 }
858
859 #[test]
860 fn test_short_table_name_accepted_for_read_write() {
861 assert!(validate_table_name("ab").is_ok());
865 assert!(validate_table_name("a").is_ok());
866 }
867
868 #[test]
869 fn test_empty_table_name_rejected_for_read_write() {
870 let err = validate_table_name("").unwrap_err().to_string();
871 assert!(err.contains("Member must have length greater than or equal to 1"));
872 assert!(!err.contains("greater than or equal to 3"));
873 }
874
875 #[test]
876 fn test_invalid_table_name_bad_chars() {
877 assert!(validate_table_name("my table").is_err());
878 assert!(validate_table_name("my@table").is_err());
879 }
880
881 #[test]
882 fn test_create_table_context_keeps_min_length_3() {
883 let errs = table_name_constraint_errors(Some("ab"), TableNameContext::CreateTable);
884 assert!(
885 errs.iter()
886 .any(|e| e.contains("Member must have length greater than or equal to 3"))
887 );
888 }
889
890 #[test]
891 fn test_valid_key_schema() {
892 let schema = vec![hash_key("pk")];
893 assert!(validate_key_schema(&schema).is_ok());
894
895 let schema = vec![hash_key("pk"), range_key("sk")];
896 assert!(validate_key_schema(&schema).is_ok());
897 }
898
899 #[test]
900 fn test_invalid_key_schema_empty() {
901 assert!(validate_key_schema(&[]).is_err());
902 }
903
904 #[test]
905 fn test_invalid_key_schema_no_hash() {
906 let schema = vec![range_key("sk")];
907 assert!(validate_key_schema(&schema).is_err());
908 }
909
910 #[test]
911 fn test_valid_key_attributes_in_definitions() {
912 let schema = vec![hash_key("pk"), range_key("sk")];
913 let defs = vec![
914 attr_def("pk", ScalarAttributeType::S),
915 attr_def("sk", ScalarAttributeType::N),
916 ];
917 assert!(validate_key_attributes_in_definitions(&schema, &defs).is_ok());
918 }
919
920 #[test]
921 fn test_missing_key_attribute_in_definitions() {
922 let schema = vec![hash_key("pk"), range_key("sk")];
923 let defs = vec![attr_def("pk", ScalarAttributeType::S)];
924 assert!(validate_key_attributes_in_definitions(&schema, &defs).is_err());
925 }
926
927 #[test]
928 fn test_partition_key_name() {
929 let schema = vec![hash_key("pk"), range_key("sk")];
930 assert_eq!(partition_key_name(&schema), Some("pk"));
931 }
932
933 #[test]
934 fn test_sort_key_name() {
935 let schema = vec![hash_key("pk"), range_key("sk")];
936 assert_eq!(sort_key_name(&schema), Some("sk"));
937
938 let schema = vec![hash_key("pk")];
939 assert_eq!(sort_key_name(&schema), None);
940 }
941
942 #[test]
943 fn test_envelope_request_validation_wraps_tagged_error() {
944 let msg = "Value '' at 'expressionAttributeNames' failed to satisfy constraint: \
945 Map value must satisfy constraint";
946 let err = envelope_request_validation(DynoxideError::EnvelopedValidation(msg.to_string()));
947 match err {
948 DynoxideError::ValidationException(m) => {
949 assert_eq!(m, format!("1 validation error detected: {msg}"));
950 }
951 other => panic!("expected ValidationException, got {other:?}"),
952 }
953 }
954
955 #[test]
956 fn test_envelope_request_validation_passes_other_errors_through() {
957 let plain = envelope_request_validation(DynoxideError::ValidationException("msg".into()));
958 assert!(matches!(
959 &plain,
960 DynoxideError::ValidationException(m) if m == "msg"
961 ));
962
963 let key_empty =
964 envelope_request_validation(DynoxideError::KeyEmptyValueValidation("msg".into()));
965 assert!(matches!(
966 &key_empty,
967 DynoxideError::KeyEmptyValueValidation(m) if m == "msg"
968 ));
969
970 let not_found =
971 envelope_request_validation(DynoxideError::ResourceNotFoundException("msg".into()));
972 assert!(matches!(
973 ¬_found,
974 DynoxideError::ResourceNotFoundException(m) if m == "msg"
975 ));
976 }
977
978 #[test]
979 fn test_strip_request_validation_tag_untags_without_envelope() {
980 let err = strip_request_validation_tag(DynoxideError::EnvelopedValidation("msg".into()));
981 assert!(matches!(
982 &err,
983 DynoxideError::ValidationException(m) if m == "msg"
984 ));
985 }
986
987 #[test]
988 fn test_strip_request_validation_tag_passes_other_errors_through() {
989 let plain = strip_request_validation_tag(DynoxideError::ValidationException("msg".into()));
990 assert!(matches!(
991 &plain,
992 DynoxideError::ValidationException(m) if m == "msg"
993 ));
994
995 let not_found =
996 strip_request_validation_tag(DynoxideError::ResourceNotFoundException("msg".into()));
997 assert!(matches!(
998 ¬_found,
999 DynoxideError::ResourceNotFoundException(m) if m == "msg"
1000 ));
1001 }
1002}