1use crate::errors::{DynoxideError, Result};
2use crate::types::{
3 AttributeDefinition, AttributeValue, GlobalSecondaryIndex, Item, KeySchemaElement, KeyType,
4 ScalarAttributeType,
5};
6
7#[derive(Copy, Clone, Debug)]
19pub enum TableNameContext {
20 CreateTable,
22 ReadWrite,
24}
25
26pub fn validate_table_name(name: &str) -> Result<()> {
34 let errors = table_name_constraint_errors(Some(name), TableNameContext::ReadWrite);
35 if errors.is_empty() {
36 return Ok(());
37 }
38 let count = errors.len();
39 let msg = format!(
40 "{count} validation error{} detected: {}",
41 if count == 1 { "" } else { "s" },
42 errors.join("; ")
43 );
44 Err(DynoxideError::ValidationException(msg))
45}
46
47pub fn table_name_constraint_errors(
53 table_name: Option<&str>,
54 context: TableNameContext,
55) -> Vec<String> {
56 let mut errors = Vec::new();
57 match table_name {
58 None => {
59 errors.push(
60 "Value null at 'tableName' failed to satisfy constraint: \
61 Member must not be null"
62 .to_string(),
63 );
64 }
65 Some(name) => match context {
66 TableNameContext::CreateTable => {
67 if name.is_empty()
68 || !name
69 .chars()
70 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
71 {
72 errors.push(format!(
73 "Value '{}' at 'tableName' failed to satisfy constraint: \
74 Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
75 name
76 ));
77 }
78 if name.len() < 3 {
79 errors.push(format!(
80 "Value '{}' at 'tableName' failed to satisfy constraint: \
81 Member must have length greater than or equal to 3",
82 name
83 ));
84 }
85 if name.len() > 255 {
86 errors.push(format!(
87 "Value '{}' at 'tableName' failed to satisfy constraint: \
88 Member must have length less than or equal to 255",
89 name
90 ));
91 }
92 }
93 TableNameContext::ReadWrite => {
94 if name.is_empty() {
95 errors.push(
96 "Value '' at 'tableName' failed to satisfy constraint: \
97 Member must have length greater than or equal to 1"
98 .to_string(),
99 );
100 } else if !name
101 .chars()
102 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
103 {
104 errors.push(format!(
105 "Value '{}' at 'tableName' failed to satisfy constraint: \
106 Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
107 name
108 ));
109 }
110 if name.len() > 255 {
111 errors.push(format!(
112 "Value '{}' at 'tableName' failed to satisfy constraint: \
113 Member must have length less than or equal to 255",
114 name
115 ));
116 }
117 }
118 },
119 }
120 errors
121}
122
123pub(crate) const EMPTY_TABLE_NAME_REJECTION: &str = "Value at 'TableName' failed to satisfy \
131 constraint: Member must have length greater than or equal to 1";
132
133fn short_table_name_error(name: &str) -> String {
134 format!(
135 "Value '{name}' at 'tableName' failed to satisfy constraint: \
136 Member must have length greater than or equal to 3"
137 )
138}
139
140fn long_table_name_error(name: &str) -> String {
141 format!(
142 "Value '{name}' at 'tableName' failed to satisfy constraint: \
143 Member must have length less than or equal to 255"
144 )
145}
146
147fn table_name_pattern_error(name: &str) -> String {
148 format!(
149 "Value '{name}' at 'tableName' failed to satisfy constraint: \
150 Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+"
151 )
152}
153
154pub(crate) fn first_table_name_error(name: &str) -> Option<String> {
160 if name.len() < 3 {
161 Some(short_table_name_error(name))
162 } else if name.len() > 255 {
163 Some(long_table_name_error(name))
164 } else if !table_name_pattern_holds(name) {
165 Some(table_name_pattern_error(name))
166 } else {
167 None
168 }
169}
170
171pub(crate) fn item_table_name_errors(table_name: Option<&str>) -> Vec<String> {
177 match table_name {
178 None => table_name_constraint_errors(None, TableNameContext::ReadWrite),
179 Some("") => vec![EMPTY_TABLE_NAME_REJECTION.to_string()],
180 Some(name) => first_table_name_error(name).into_iter().collect(),
181 }
182}
183
184pub(crate) fn validate_item_table_name(name: &str) -> Result<()> {
188 match format_validation_errors(&item_table_name_errors(Some(name))) {
189 Some(msg) => Err(DynoxideError::ValidationException(msg)),
190 None => Ok(()),
191 }
192}
193
194pub(crate) fn update_item_table_name_errors(table_name: Option<&str>) -> Vec<String> {
199 let name = match table_name {
200 None => return table_name_constraint_errors(None, TableNameContext::ReadWrite),
201 Some("") => return vec![EMPTY_TABLE_NAME_REJECTION.to_string()],
202 Some(name) => name,
203 };
204 let mut errors = Vec::new();
205 if !table_name_pattern_holds(name) {
206 errors.push(table_name_pattern_error(name));
207 }
208 if name.len() < 3 {
209 errors.push(short_table_name_error(name));
210 } else if name.len() > 255 {
211 errors.push(long_table_name_error(name));
212 }
213 errors
214}
215
216pub(crate) fn return_values_on_condition_check_failure_rejection(
224 value: Option<&str>,
225 path: &str,
226) -> Option<String> {
227 let value = value?;
228 if ["ALL_OLD", "NONE"].contains(&value) {
229 return None;
230 }
231 Some(format!(
232 "Value '{value}' at '{path}' failed to satisfy constraint: \
233 Member must satisfy enum value set: [ALL_OLD, NONE]"
234 ))
235}
236
237pub(crate) fn conditional_operator_misuse(
246 operator: Option<&str>,
247 conditions: usize,
248) -> Option<&'static str> {
249 let operator = operator?;
250 match conditions {
251 0 => Some("ConditionalOperator cannot be used without Filter or Expected"),
252 1 => Some(
253 "ConditionalOperator can only be used when Filter or Expected has two or more \
254 elements",
255 ),
256 _ if !["AND", "OR"].contains(&operator) => {
261 Some("Failed to satisfy constraint: Member must satisfy enum value set: [ALL, OR]")
262 }
263 _ => None,
264 }
265}
266
267pub(crate) fn legacy_condition_count(conditions: Option<&serde_json::Value>) -> usize {
270 conditions
271 .and_then(|value| value.as_object())
272 .map_or(0, |object| object.len())
273}
274
275pub fn format_validation_errors(errors: &[String]) -> Option<String> {
279 if errors.is_empty() {
280 return None;
281 }
282 if let [single] = errors {
283 return Some(envelope_message(single));
284 }
285 Some(format!(
286 "{} validation errors detected: {}",
287 errors.len(),
288 errors.join("; ")
289 ))
290}
291
292pub(crate) fn envelope_message(msg: &str) -> String {
295 format!("1 validation error detected: {msg}")
296}
297
298pub(crate) fn return_consumed_capacity_rejection(mode: Option<&str>) -> Option<String> {
316 let mode = mode?;
317 if ["INDEXES", "TOTAL", "NONE"].contains(&mode) {
318 return None;
319 }
320 Some(format!(
321 "Value '{mode}' at 'returnConsumedCapacity' failed to satisfy constraint: \
322 Member must satisfy enum value set: [INDEXES, TOTAL, NONE]"
323 ))
324}
325
326#[cfg(feature = "mcp-server")]
330pub(crate) fn reject_bad_capacity_mode(mode: Option<&str>) -> Result<()> {
331 match return_consumed_capacity_rejection(mode) {
332 Some(msg) => Err(DynoxideError::ValidationException(envelope_message(&msg))),
333 None => Ok(()),
334 }
335}
336
337pub(crate) const RETURN_VALUES: [&str; 5] =
339 ["ALL_NEW", "UPDATED_OLD", "ALL_OLD", "NONE", "UPDATED_NEW"];
340
341pub(crate) const RETURN_VALUES_NOT_HONOURED: &str = "ReturnValues can only be ALL_OLD or NONE";
344
345pub(crate) const RETURN_VALUES_REJECTION: &str = "Failed to satisfy constraint: Member must \
354 satisfy enum value set: [ALL_OLD, UPDATED_OLD, ALL_NEW, UPDATE_NEW, NONE]";
355
356pub(crate) fn return_values_value_rejection(value: &str) -> String {
360 format!(
361 "Value '{value}' at 'returnValues' failed to satisfy constraint: \
362 Member must satisfy enum value set: [ALL_NEW, UPDATED_OLD, ALL_OLD, NONE, UPDATED_NEW]"
363 )
364}
365
366pub(crate) fn conditional_operator_rejection(operator: Option<&str>) -> Option<String> {
373 let operator = operator?;
374 if ["OR", "AND"].contains(&operator) {
375 return None;
376 }
377 Some(format!(
378 "Value '{operator}' at 'conditionalOperator' failed to satisfy constraint: \
379 Member must satisfy enum value set: [OR, AND]"
380 ))
381}
382
383pub(crate) fn return_item_collection_metrics_rejection(mode: Option<&str>) -> Option<String> {
388 let mode = mode?;
389 if ["SIZE", "NONE"].contains(&mode) {
390 return None;
391 }
392 Some(format!(
393 "Value '{mode}' at 'returnItemCollectionMetrics' failed to satisfy constraint: \
394 Member must satisfy enum value set: [SIZE, NONE]"
395 ))
396}
397
398fn table_name_pattern_holds(name: &str) -> bool {
399 !name.is_empty()
400 && name
401 .chars()
402 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
403}
404
405pub(crate) fn member_table_name_is_valid(name: &str) -> bool {
411 (3..=255).contains(&name.len()) && table_name_pattern_holds(name)
412}
413
414pub(crate) fn member_table_name_errors(name: &str, path: &str) -> Vec<String> {
420 let mut errors = Vec::new();
421 if name.len() < 3 {
422 errors.push(format!(
423 "Value '{name}' at '{path}' failed to satisfy constraint: \
424 Member must have length greater than or equal to 3"
425 ));
426 } else if name.len() > 255 {
427 errors.push(format!(
428 "Value '{name}' at '{path}' failed to satisfy constraint: \
429 Member must have length less than or equal to 255"
430 ));
431 }
432 if !table_name_pattern_holds(name) {
433 errors.push(format!(
434 "Value '{name}' at '{path}' failed to satisfy constraint: \
435 Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+"
436 ));
437 }
438 errors
439}
440
441pub(crate) const REQUEST_ITEMS_KEY_CONSTRAINT: &str = "Map keys must satisfy constraint: \
445 [Member must have length less than or equal to 255, Member must have length greater \
446 than or equal to 3, Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+]";
447
448pub(crate) fn envelope_request_validation(err: DynoxideError) -> DynoxideError {
454 match err {
455 DynoxideError::EnvelopedValidation(msg) => {
456 DynoxideError::ValidationException(envelope_message(&msg))
457 }
458 other => other,
459 }
460}
461
462pub(crate) fn single_write_layer(expected: Option<&serde_json::Value>) -> ValueLayer {
467 if crate::types::current_value_layer() == ValueLayer::Older
468 || expected.is_some_and(crate::types::reads_on_older_layer)
469 {
470 ValueLayer::Older
471 } else {
472 ValueLayer::Current
473 }
474}
475
476pub(crate) fn resolve_for_layer(layer: ValueLayer, err: DynoxideError) -> DynoxideError {
480 match (layer, err) {
481 (ValueLayer::Older, DynoxideError::EnvelopedValidation(msg)) => {
482 DynoxideError::ValidationException(msg)
483 }
484 (_, err) => envelope_request_validation(err),
485 }
486}
487
488pub(crate) fn tag_for_envelope(err: DynoxideError) -> DynoxideError {
492 match err {
493 DynoxideError::ValidationException(msg) => DynoxideError::EnvelopedValidation(msg),
494 other => other,
495 }
496}
497
498#[cfg(any(
508 feature = "http-server",
509 feature = "mcp-server",
510 feature = "wasm-sqlite",
511 test
512))]
513pub(crate) fn strip_request_validation_tag(err: DynoxideError) -> DynoxideError {
514 match err {
515 DynoxideError::EnvelopedValidation(msg) => DynoxideError::ValidationException(msg),
516 other => other,
517 }
518}
519
520#[cfg(any(feature = "http-server", feature = "wasm-sqlite", test))]
537pub(crate) fn resolve_request_validation_tag(operation: &str, err: DynoxideError) -> DynoxideError {
538 match operation {
539 "PutItem" | "UpdateItem" | "DeleteItem" => envelope_request_validation(err),
540 _ => strip_request_validation_tag(err),
541 }
542}
543
544#[derive(Debug)]
551pub(crate) enum ClassifiedValidationError {
552 Bare(DynoxideError),
555 Enveloped(String),
558}
559
560impl ClassifiedValidationError {
561 pub(crate) fn bare(message: impl Into<String>) -> Self {
563 Self::Bare(DynoxideError::ValidationException(message.into()))
564 }
565
566 pub(crate) fn enveloped(message: impl Into<String>) -> Self {
569 Self::Enveloped(message.into())
570 }
571
572 pub(crate) fn into_tagged(self) -> DynoxideError {
576 match self {
577 Self::Enveloped(msg) => DynoxideError::EnvelopedValidation(msg),
578 Self::Bare(err) => err,
579 }
580 }
581}
582
583impl From<ClassifiedValidationError> for DynoxideError {
584 fn from(e: ClassifiedValidationError) -> Self {
585 match e {
586 ClassifiedValidationError::Enveloped(msg) => DynoxideError::ValidationException(msg),
587 ClassifiedValidationError::Bare(err) => err,
588 }
589 }
590}
591
592impl From<DynoxideError> for ClassifiedValidationError {
593 fn from(error: DynoxideError) -> Self {
596 Self::Bare(error)
597 }
598}
599
600pub fn validate_key_schema(key_schema: &[KeySchemaElement]) -> Result<()> {
605 if key_schema.is_empty() || key_schema.len() > 2 {
606 return Err(DynoxideError::ValidationException(
607 "1 validation error detected: Value null at 'keySchema' failed to satisfy constraint: \
608 Member must have length less than or equal to 2"
609 .to_string(),
610 ));
611 }
612
613 if key_schema[0].key_type != KeyType::HASH {
615 return Err(DynoxideError::ValidationException(
616 "Invalid KeySchema: The first KeySchemaElement is not a HASH key type".to_string(),
617 ));
618 }
619
620 if key_schema.len() == 2 && key_schema[0].attribute_name == key_schema[1].attribute_name {
622 return Err(DynoxideError::ValidationException(
623 "Both the Hash Key and the Range Key element in the KeySchema have the same name"
624 .to_string(),
625 ));
626 }
627
628 if key_schema.len() == 2 && key_schema[1].key_type != KeyType::RANGE {
630 return Err(DynoxideError::ValidationException(
631 "Invalid KeySchema: The second KeySchemaElement is not a RANGE key type".to_string(),
632 ));
633 }
634
635 Ok(())
636}
637
638pub fn validate_attribute_definitions(defs: &[AttributeDefinition]) -> Result<()> {
640 if defs.is_empty() {
641 return Err(DynoxideError::ValidationException(
642 "1 validation error detected: Value null at 'attributeDefinitions' failed to satisfy \
643 constraint: Member must have length greater than or equal to 1"
644 .to_string(),
645 ));
646 }
647
648 for def in defs {
649 match def.attribute_type {
650 ScalarAttributeType::S | ScalarAttributeType::N | ScalarAttributeType::B => {}
651 }
652 }
653
654 Ok(())
655}
656
657pub(crate) fn java_hash_order<'a>(names: &[&'a str], sized_to_count: bool) -> Vec<&'a str> {
669 let mut seen = std::collections::HashSet::with_capacity(names.len());
670 let unique: Vec<&str> = names
671 .iter()
672 .copied()
673 .filter(|name| seen.insert(*name))
674 .collect();
675 let initial = if sized_to_count {
676 unique.len().max(1).next_power_of_two()
677 } else {
678 16
679 };
680 let capacity = java_table_capacity(initial, unique.len());
681 let mut ordered: Vec<(usize, usize, &str)> = unique
682 .iter()
683 .enumerate()
684 .map(|(added, name)| (java_hash_bucket(name, capacity), added, *name))
685 .collect();
686 ordered.sort_unstable();
687 ordered.into_iter().map(|(_, _, name)| name).collect()
688}
689
690pub(crate) fn java_table_capacity(initial: usize, len: usize) -> usize {
694 let mut capacity = initial;
695 while len * 4 > capacity * 3 {
696 capacity *= 2;
697 }
698 capacity
699}
700
701pub(crate) fn java_hash_bucket(name: &str, capacity: usize) -> usize {
705 let h = name.encode_utf16().fold(0u32, |h, unit| {
706 h.wrapping_mul(31).wrapping_add(u32::from(unit))
707 });
708 ((h ^ (h >> 16)) as usize) & (capacity - 1)
709}
710
711pub(crate) fn undefined_index_key_error(
720 key_schema: &[KeySchemaElement],
721 definitions: &[AttributeDefinition],
722) -> Option<String> {
723 let defined: Vec<&str> = definitions
724 .iter()
725 .map(|d| d.attribute_name.as_str())
726 .collect();
727 let keys: Vec<&str> = key_schema
728 .iter()
729 .map(|k| k.attribute_name.as_str())
730 .collect();
731 if keys.iter().all(|k| defined.contains(k)) {
732 return None;
733 }
734 let key_count = java_hash_order(&keys, true).len();
735 if defined.len() < key_count {
736 return Some("Invalid KeySchema: Some index key attribute have no definition".to_string());
737 }
738 Some(format!(
739 "One or more parameter values were invalid: Some index key attributes are not defined \
740 in AttributeDefinitions. Keys: [{}], AttributeDefinitions: [{}]",
741 java_hash_order(&keys, true).join(", "),
742 java_hash_order(&defined, true).join(", ")
743 ))
744}
745
746pub fn validate_key_attributes_in_definitions(
748 key_schema: &[KeySchemaElement],
749 definitions: &[AttributeDefinition],
750) -> Result<()> {
751 for key_elem in key_schema {
752 let found = definitions
753 .iter()
754 .any(|def| def.attribute_name == key_elem.attribute_name);
755 if !found {
756 return Err(DynoxideError::ValidationException(format!(
757 "One or more parameter values were invalid: Some index key attributes are not \
758 defined in AttributeDefinitions. Keys: [{}], AttributeDefinitions: [{}]",
759 key_elem.attribute_name,
760 definitions
761 .iter()
762 .map(|d| d.attribute_name.as_str())
763 .collect::<Vec<_>>()
764 .join(", ")
765 )));
766 }
767 }
768
769 Ok(())
770}
771
772pub fn check_gsi_key_schema(key_schema: &[KeySchemaElement]) -> std::result::Result<(), String> {
777 const MAX_PER_KIND: usize = 4;
778 let Some(first) = key_schema.first() else {
779 return Err(
780 "1 validation error detected: Value null at 'keySchema' failed to satisfy \
781 constraint: Member must have length less than or equal to 2"
782 .to_string(),
783 );
784 };
785 if first.key_type != KeyType::HASH {
786 return Err(
787 "Invalid KeySchema: The first KeySchemaElement is not a HASH key type".to_string(),
788 );
789 }
790 if let [hash, range] = key_schema
791 && range.key_type == KeyType::RANGE
792 && hash.attribute_name == range.attribute_name
793 {
794 return Err(
795 "Both the Hash Key and the Range Key element in the KeySchema have the same name"
796 .to_string(),
797 );
798 }
799 for (i, element) in key_schema.iter().enumerate() {
800 if key_schema[..i]
801 .iter()
802 .any(|earlier| earlier.attribute_name == element.attribute_name)
803 {
804 return Err(
805 "The KeySchema contains multiple key attributes with the same name".to_string(),
806 );
807 }
808 }
809 if key_schema
810 .windows(2)
811 .any(|w| w[0].key_type == KeyType::RANGE && w[1].key_type == KeyType::HASH)
812 {
813 return Err(
814 "One or more parameter values were invalid: All HASH key attributes must precede \
815 RANGE key attributes in the KeySchema"
816 .to_string(),
817 );
818 }
819 for (key_type, name) in [(KeyType::HASH, "HASH"), (KeyType::RANGE, "RANGE")] {
820 if key_schema.iter().filter(|k| k.key_type == key_type).count() > MAX_PER_KIND {
821 return Err(format!(
822 "One or more parameter values were invalid: The KeySchema exceeds the maximum \
823 allowed number of {name} key attributes"
824 ));
825 }
826 }
827 Ok(())
828}
829
830pub fn validate_gsi(
836 gsi: &GlobalSecondaryIndex,
837 request_definitions: &[AttributeDefinition],
838) -> Result<()> {
839 if gsi.index_name.len() < 3 || gsi.index_name.len() > 255 {
841 return Err(DynoxideError::ValidationException(format!(
842 "1 validation error detected: Value '{}' at 'globalSecondaryIndexes.1.member.indexName' \
843 failed to satisfy constraint: Member must have length greater than or equal to 3",
844 gsi.index_name
845 )));
846 }
847
848 if !gsi
850 .index_name
851 .chars()
852 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
853 {
854 return Err(DynoxideError::ValidationException(format!(
855 "1 validation error detected: Value '{}' at 'globalSecondaryIndexes.1.member.indexName' \
856 failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
857 gsi.index_name
858 )));
859 }
860
861 check_gsi_key_schema(&gsi.key_schema).map_err(DynoxideError::ValidationException)?;
863
864 validate_projection(&gsi.projection, &gsi.index_name)?;
866
867 if let Some(message) = undefined_index_key_error(&gsi.key_schema, request_definitions) {
869 return Err(DynoxideError::ValidationException(message));
870 }
871
872 Ok(())
873}
874
875pub fn validate_vector_index(
890 vix: &crate::types::VectorIndex,
891 request_definitions: &[AttributeDefinition],
892 member_path: &str,
893) -> Result<()> {
894 if vix.index_name.len() < 3 {
896 return Err(DynoxideError::ValidationException(format!(
897 "1 validation error detected: Value '{}' at '{member_path}.indexName' \
898 failed to satisfy constraint: Member must have length greater than or equal to 3",
899 vix.index_name
900 )));
901 }
902 if vix.index_name.len() > 255 {
903 return Err(DynoxideError::ValidationException(format!(
904 "1 validation error detected: Value '{}' at '{member_path}.indexName' \
905 failed to satisfy constraint: Member must have length less than or equal to 255",
906 vix.index_name
907 )));
908 }
909
910 if !vix
912 .index_name
913 .chars()
914 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
915 {
916 return Err(DynoxideError::ValidationException(format!(
917 "1 validation error detected: Value '{}' at '{member_path}.indexName' \
918 failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
919 vix.index_name
920 )));
921 }
922
923 match vix.distance_function.as_str() {
925 "COSINE" | "DOT_PRODUCT" | "EUCLIDEAN" => {}
926 other => {
927 return Err(DynoxideError::ValidationException(format!(
928 "1 validation error detected: Value '{other}' at \
929 '{member_path}.distanceFunction' failed to satisfy constraint: \
930 Member must satisfy enum value set: [COSINE, DOT_PRODUCT, EUCLIDEAN]"
931 )));
932 }
933 }
934
935 if vix.dimensions < 1 || vix.dimensions > 4096 {
939 return Err(DynoxideError::ValidationException(
940 "One or more parameter values were invalid: Number of dimensions must be between \
941 1 and 4096 inclusive."
942 .to_string(),
943 ));
944 }
945
946 validate_projection(&vix.projection, &vix.index_name)?;
948
949 if let Some(ref schema) = vix.search_schema {
953 for (elem_idx, elem) in schema.iter().enumerate() {
954 let elem_position = elem_idx + 1;
955 match elem.search_schema_element_type.as_str() {
956 "HASH" | "INLINE_FILTER" => {}
957 other => {
958 return Err(DynoxideError::ValidationException(format!(
959 "1 validation error detected: Value '{other}' at \
960 '{member_path}.searchSchema.{elem_position}.member.searchSchemaElementType' \
961 failed to satisfy constraint: Member must satisfy enum value set: \
962 [HASH, INLINE_FILTER]"
963 )));
964 }
965 }
966 if !request_definitions
967 .iter()
968 .any(|d| d.attribute_name == elem.attribute_name)
969 {
970 return Err(DynoxideError::ValidationException(
971 "One or more parameter values were invalid: One element in SearchSchema \
972 is not defined in attribute definitions"
973 .to_string(),
974 ));
975 }
976 }
977
978 let hash_count = schema
982 .iter()
983 .filter(|e| e.search_schema_element_type == "HASH")
984 .count();
985 if hash_count > 1 {
986 return Err(DynoxideError::ValidationException(format!(
987 "One or more parameter values were invalid: Value '{hash_count}' at \
988 'SearchSchema' failed to satisfy constraint: Member must have HASH count \
989 less than or equal to 1"
990 )));
991 }
992
993 let mut seen = std::collections::HashSet::new();
996 for elem in schema {
997 if !seen.insert(elem.attribute_name.as_str()) {
998 return Err(DynoxideError::ValidationException(
999 "One or more parameter values were invalid: SearchSchema contains a \
1000 duplicate AttributeName"
1001 .to_string(),
1002 ));
1003 }
1004 }
1005 }
1006
1007 Ok(())
1008}
1009
1010pub fn validate_projection(projection: &crate::types::Projection, _index_name: &str) -> Result<()> {
1016 match &projection.projection_type {
1017 None => {
1018 return Err(DynoxideError::ValidationException(
1019 "One or more parameter values were invalid: Unknown ProjectionType: null"
1020 .to_string(),
1021 ));
1022 }
1023 Some(pt) => {
1024 if let Some(ref nka) = projection.non_key_attributes {
1025 match pt {
1027 crate::types::ProjectionType::ALL => {
1028 return Err(DynoxideError::ValidationException(
1029 "One or more parameter values were invalid: \
1030 ProjectionType is ALL, but NonKeyAttributes is specified"
1031 .to_string(),
1032 ));
1033 }
1034 crate::types::ProjectionType::KEYS_ONLY => {
1035 return Err(DynoxideError::ValidationException(
1036 "One or more parameter values were invalid: \
1037 ProjectionType is KEYS_ONLY, but NonKeyAttributes is specified"
1038 .to_string(),
1039 ));
1040 }
1041 crate::types::ProjectionType::INCLUDE => {
1042 if nka.is_empty() {
1044 return Err(DynoxideError::ValidationException(
1045 "One or more parameter values were invalid: \
1046 NonKeyAttributes must not be empty"
1047 .to_string(),
1048 ));
1049 }
1050 }
1051 }
1052 }
1053 }
1054 }
1055 Ok(())
1056}
1057
1058pub fn partition_key_name(key_schema: &[KeySchemaElement]) -> Option<&str> {
1060 key_schema
1061 .iter()
1062 .find(|k| k.key_type == KeyType::HASH)
1063 .map(|k| k.attribute_name.as_str())
1064}
1065
1066const MAX_NESTING_DEPTH: usize = 32;
1070
1071pub(crate) const NESTING_LIMIT_MESSAGE: &str = "Nesting Levels have exceeded supported limits: Attributes in the item have nested levels beyond supported limit";
1074
1075pub(crate) const UPDATED_ITEM_NESTING_MESSAGE: &str =
1080 "Nesting Levels have exceeded supported limits";
1081
1082pub(crate) fn validate_updated_item_nesting(item: &Item) -> Result<()> {
1085 if item
1086 .values()
1087 .any(|value| validate_nesting_depth(value).is_err())
1088 {
1089 return Err(DynoxideError::ValidationException(
1090 UPDATED_ITEM_NESTING_MESSAGE.to_string(),
1091 ));
1092 }
1093 Ok(())
1094}
1095
1096pub fn validate_item_attribute_values(item: &Item) -> crate::Result<()> {
1112 validate_item_attribute_values_classified(item, ValueLayer::Older).map_err(Into::into)
1113}
1114
1115pub(crate) fn validate_item_attribute_values_classified(
1122 item: &Item,
1123 layer: ValueLayer,
1124) -> std::result::Result<(), ClassifiedValidationError> {
1125 for value in item.values() {
1126 validate_attribute_value(value, 0, layer).map_err(|e| match e {
1127 ClassifiedValidationError::Enveloped(msg)
1130 if msg == NESTING_LIMIT_MESSAGE && nesting_depth(value) > SECOND_NESTING_LIMIT =>
1131 {
1132 ClassifiedValidationError::bare(msg)
1133 }
1134 other => other,
1135 })?;
1136 }
1137 Ok(())
1138}
1139
1140pub(crate) fn validate_value_classified(
1143 value: &AttributeValue,
1144 layer: ValueLayer,
1145) -> std::result::Result<(), ClassifiedValidationError> {
1146 validate_attribute_value(value, 0, layer)
1147}
1148
1149pub(crate) fn find_set_error(value: &AttributeValue, layer: ValueLayer) -> Option<String> {
1152 const INVALID: &str = "One or more parameter values were invalid: ";
1153 match value {
1154 AttributeValue::SS(set) if set.is_empty() => {
1155 Some(format!("{INVALID}An string set may not be empty"))
1156 }
1157 AttributeValue::NS(set) if set.is_empty() => {
1158 Some(format!("{INVALID}An number set may not be empty"))
1159 }
1160 AttributeValue::BS(set) if set.is_empty() => {
1161 Some(format!("{INVALID}Binary sets should not be empty"))
1162 }
1163 AttributeValue::L(list) => list.iter().find_map(|v| find_set_error(v, layer)),
1164 AttributeValue::M(map) => map.values().find_map(|v| find_set_error(v, layer)),
1165 _ => duplicate_set_message(value, layer),
1166 }
1167}
1168
1169pub(crate) fn find_duplicate_set(value: &AttributeValue, layer: ValueLayer) -> Option<String> {
1172 match value {
1173 AttributeValue::L(list) => list.iter().find_map(|v| find_duplicate_set(v, layer)),
1174 AttributeValue::M(map) => map.values().find_map(|v| find_duplicate_set(v, layer)),
1175 _ => duplicate_set_message(value, layer),
1176 }
1177}
1178
1179pub(crate) const SECOND_NESTING_LIMIT: usize = 62;
1185
1186pub(crate) fn nesting_depth(value: &AttributeValue) -> usize {
1189 match value {
1190 AttributeValue::L(list) => 1 + list.iter().map(nesting_depth).max().unwrap_or(0),
1191 AttributeValue::M(map) => 1 + map.values().map(nesting_depth).max().unwrap_or(0),
1192 _ => 0,
1193 }
1194}
1195
1196pub(crate) fn validate_parameter_value(value: &AttributeValue) -> crate::Result<()> {
1200 validate_attribute_value_within(value, 0, None, ValueLayer::Older).map_err(Into::into)
1201}
1202
1203fn validate_attribute_value(
1204 value: &AttributeValue,
1205 depth: usize,
1206 layer: ValueLayer,
1207) -> std::result::Result<(), ClassifiedValidationError> {
1208 validate_attribute_value_within(value, depth, Some(MAX_NESTING_DEPTH), layer)
1209}
1210
1211fn validate_attribute_value_within(
1212 value: &AttributeValue,
1213 depth: usize,
1214 nesting_limit: Option<usize>,
1215 layer: ValueLayer,
1216) -> std::result::Result<(), ClassifiedValidationError> {
1217 let number = |n: &str| {
1218 layer
1219 .check_number(n)
1220 .map_err(|e| layer.classify_number_error(e))
1221 };
1222 if nesting_limit.is_some_and(|limit| depth >= limit) {
1226 return Err(ClassifiedValidationError::enveloped(NESTING_LIMIT_MESSAGE));
1227 }
1228 match value {
1229 AttributeValue::SS(set) if set.is_empty() => Err(ClassifiedValidationError::enveloped(
1230 "One or more parameter values were invalid: An string set may not be empty",
1231 )),
1232 AttributeValue::NS(set) if set.is_empty() => Err(ClassifiedValidationError::enveloped(
1233 "One or more parameter values were invalid: An number set may not be empty",
1234 )),
1235 AttributeValue::BS(set) if set.is_empty() => Err(ClassifiedValidationError::enveloped(
1236 "One or more parameter values were invalid: Binary sets should not be empty",
1237 )),
1238 AttributeValue::SS(_) | AttributeValue::BS(_) => {
1239 match duplicate_set_message(value, layer) {
1240 Some(message) => Err(ClassifiedValidationError::enveloped(message)),
1241 None => Ok(()),
1242 }
1243 }
1244 AttributeValue::NS(set) => {
1245 for n in set {
1246 number(n)?;
1247 }
1248 match duplicate_set_message(value, layer) {
1249 Some(message) => Err(ClassifiedValidationError::enveloped(message)),
1250 None => Ok(()),
1251 }
1252 }
1253 AttributeValue::N(n) => {
1254 number(n)?;
1255 Ok(())
1256 }
1257 AttributeValue::L(list) => {
1258 for v in list {
1259 validate_attribute_value_within(v, depth + 1, nesting_limit, layer)?;
1260 }
1261 Ok(())
1262 }
1263 AttributeValue::M(map) => {
1264 for v in map.values() {
1265 validate_attribute_value_within(v, depth + 1, nesting_limit, layer)?;
1266 }
1267 Ok(())
1268 }
1269 _ => Ok(()),
1270 }
1271}
1272
1273pub fn validate_nesting_depth(value: &AttributeValue) -> Result<()> {
1282 check_nesting_depth(value, 0)
1283}
1284
1285fn check_nesting_depth(value: &AttributeValue, depth: usize) -> Result<()> {
1286 if depth >= MAX_NESTING_DEPTH {
1287 return Err(DynoxideError::ValidationException(
1288 NESTING_LIMIT_MESSAGE.to_string(),
1289 ));
1290 }
1291 match value {
1292 AttributeValue::L(list) => list
1293 .iter()
1294 .try_for_each(|v| check_nesting_depth(v, depth + 1)),
1295 AttributeValue::M(map) => map
1296 .values()
1297 .try_for_each(|v| check_nesting_depth(v, depth + 1)),
1298 _ => Ok(()),
1299 }
1300}
1301
1302pub fn validate_key_attribute_values(key: &Item) -> Result<()> {
1311 validate_key_values(key, ValueLayer::Older).map_err(Into::into)
1312}
1313
1314pub(crate) fn validate_key_values(
1319 key: &Item,
1320 layer: ValueLayer,
1321) -> std::result::Result<(), ClassifiedValidationError> {
1322 for value in key.values() {
1323 validate_key_attr_value(value, layer)?;
1324 }
1325 Ok(())
1326}
1327
1328fn validate_key_attr_value(
1329 value: &AttributeValue,
1330 layer: ValueLayer,
1331) -> std::result::Result<(), ClassifiedValidationError> {
1332 match value {
1333 AttributeValue::SS(set) if set.is_empty() => {
1334 return Err(ClassifiedValidationError::bare(
1335 "One or more parameter values were invalid: An string set may not be empty",
1336 ));
1337 }
1338 AttributeValue::NS(set) if set.is_empty() => {
1339 return Err(ClassifiedValidationError::bare(
1340 "One or more parameter values were invalid: An number set may not be empty",
1341 ));
1342 }
1343 AttributeValue::BS(set) if set.is_empty() => {
1344 return Err(ClassifiedValidationError::bare(
1345 "One or more parameter values were invalid: Binary sets should not be empty",
1346 ));
1347 }
1348 AttributeValue::SS(_) | AttributeValue::BS(_) => {
1349 if let Some(message) = duplicate_set_message(value, layer) {
1350 return Err(ClassifiedValidationError::enveloped(message));
1351 }
1352 }
1353 AttributeValue::NS(set) => {
1354 for n in set {
1355 layer
1356 .check_number(n)
1357 .map_err(|e| layer.classify_number_error(e))?;
1358 }
1359 if let Some(message) = duplicate_set_message(value, layer) {
1360 return Err(ClassifiedValidationError::enveloped(message));
1361 }
1362 }
1363 AttributeValue::N(n) => {
1364 layer
1365 .check_number(n)
1366 .map_err(|e| layer.classify_number_error(e))?;
1367 }
1368 _ => {}
1369 }
1370 Ok(())
1371}
1372
1373#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1380pub(crate) enum ValueLayer {
1381 Current,
1387 Older,
1392}
1393
1394impl ValueLayer {
1395 #[cfg(any(feature = "http-server", feature = "wasm-sqlite", test))]
1398 pub(crate) fn for_operation(operation: &str) -> Self {
1399 match operation {
1400 "BatchWriteItem"
1401 | "TransactWriteItems"
1402 | "TransactGetItems"
1403 | "ExecuteStatement"
1404 | "BatchExecuteStatement"
1405 | "ExecuteTransaction" => Self::Older,
1406 _ => Self::Current,
1407 }
1408 }
1409
1410 pub(crate) fn check_number(self, n: &str) -> Result<()> {
1412 match self {
1413 Self::Current => crate::types::validate_current_number(n),
1414 Self::Older => crate::types::validate_parameter_number(n),
1415 }
1416 }
1417
1418 pub(crate) fn classify_number_error(self, e: DynoxideError) -> ClassifiedValidationError {
1421 match (self, e) {
1422 (Self::Current, DynoxideError::ValidationException(m)) => {
1423 ClassifiedValidationError::enveloped(m)
1424 }
1425 (_, e) => ClassifiedValidationError::Bare(e),
1426 }
1427 }
1428}
1429
1430pub(crate) fn duplicate_set_message(value: &AttributeValue, layer: ValueLayer) -> Option<String> {
1442 const INVALID: &str = "One or more parameter values were invalid: ";
1443 fn current_order<T: Ord + Clone>(members: &[T]) -> Option<Vec<T>> {
1446 let mut read = std::collections::BTreeSet::new();
1447 for (i, member) in members.iter().enumerate() {
1448 if !read.insert(member.clone()) {
1449 let mut out: Vec<T> = read.into_iter().collect();
1450 out.push(member.clone());
1451 out.extend(members[i + 1..].iter().cloned());
1452 return Some(out);
1453 }
1454 }
1455 None
1456 }
1457 fn has_repeat<T: std::hash::Hash + Eq>(members: impl IntoIterator<Item = T>) -> bool {
1458 let mut seen = std::collections::HashSet::new();
1459 members.into_iter().any(|m| !seen.insert(m))
1460 }
1461 use base64::Engine;
1462 let b64 = |b: &Vec<u8>| base64::engine::general_purpose::STANDARD.encode(b);
1463 match (value, layer) {
1464 (AttributeValue::SS(set), ValueLayer::Current) => current_order(set).map(|ordered| {
1465 let quoted: Vec<String> = ordered
1466 .iter()
1467 .map(|s| serde_json::to_string(s).unwrap_or_default())
1468 .collect();
1469 format!(
1470 "{INVALID}Input collection [{}] contains duplicates.",
1471 quoted.join(", ")
1472 )
1473 }),
1474 (AttributeValue::SS(set), ValueLayer::Older) => has_repeat(set.iter()).then(|| {
1475 format!(
1476 "{INVALID}Input collection [{}] contains duplicates.",
1477 set.join(", ")
1478 )
1479 }),
1480 (AttributeValue::BS(set), ValueLayer::Current) => current_order(set).map(|ordered| {
1481 let quoted: Vec<String> = ordered.iter().map(|b| format!("\"{}\"", b64(b))).collect();
1482 format!(
1483 "{INVALID}Input collection [{}]of type BS contains duplicates.",
1484 quoted.join(",")
1485 )
1486 }),
1487 (AttributeValue::BS(set), ValueLayer::Older) => has_repeat(set.iter()).then(|| {
1488 let shown: Vec<String> = set.iter().map(b64).collect();
1489 format!(
1490 "{INVALID}Input collection [{}]of type BS contains duplicates.",
1491 shown.join(", ")
1492 )
1493 }),
1494 (AttributeValue::NS(set), _) => {
1495 has_repeat(set.iter().map(|n| crate::types::normalize_dynamo_number(n))).then(|| {
1496 match layer {
1497 ValueLayer::Current => {
1498 format!("{INVALID}Input collection contains duplicates.")
1499 }
1500 ValueLayer::Older => "Input collection contains duplicates".to_string(),
1501 }
1502 })
1503 }
1504 _ => None,
1505 }
1506}
1507
1508pub fn normalize_item_sets(item: &mut Item) {
1519 for value in item.values_mut() {
1520 normalize_attribute_sets(value);
1521 }
1522}
1523
1524fn normalize_attribute_sets(value: &mut AttributeValue) {
1525 match value {
1526 AttributeValue::N(n) => {
1527 *n = crate::types::normalize_dynamo_number(n);
1528 }
1529 AttributeValue::SS(set) => {
1530 let mut seen = std::collections::HashSet::new();
1531 set.retain(|s| seen.insert(s.clone()));
1532 crate::set_order::sort_string_set(set);
1533 }
1534 AttributeValue::NS(set) => {
1535 let mut seen = std::collections::HashSet::new();
1536 set.retain(|n| seen.insert(normalize_number_for_dedup(n)));
1537 for n in set.iter_mut() {
1539 *n = crate::types::normalize_dynamo_number(n);
1540 }
1541 crate::set_order::sort_number_set(set);
1542 }
1543 AttributeValue::BS(set) => {
1544 let mut seen = std::collections::HashSet::new();
1545 set.retain(|b| seen.insert(b.clone()));
1546 crate::set_order::sort_binary_set(set);
1547 }
1548 AttributeValue::L(list) => {
1549 for v in list.iter_mut() {
1550 normalize_attribute_sets(v);
1551 }
1552 }
1553 AttributeValue::M(map) => {
1554 for v in map.values_mut() {
1555 normalize_attribute_sets(v);
1556 }
1557 }
1558 _ => {}
1559 }
1560}
1561
1562fn normalize_number_for_dedup(n: &str) -> String {
1566 let trimmed = n.trim();
1567 let negative = trimmed.starts_with('-');
1568 let abs_str = if negative { &trimmed[1..] } else { trimmed };
1569
1570 let (digits, exponent) = crate::types::parse_number_parts(abs_str);
1571
1572 if digits.is_empty() {
1573 return "0".to_string();
1574 }
1575
1576 let mantissa: String = digits.iter().map(|&d| (b'0' + d) as char).collect();
1577 let sign = if negative { "-" } else { "" };
1578 format!("{sign}{mantissa}E{exponent}")
1579}
1580
1581pub fn validate_lsi(
1583 lsi: &crate::types::LocalSecondaryIndex,
1584 table_key_schema: &[KeySchemaElement],
1585 all_definitions: &[AttributeDefinition],
1586) -> Result<()> {
1587 if lsi.index_name.len() < 3 || lsi.index_name.len() > 255 {
1589 return Err(DynoxideError::ValidationException(format!(
1590 "1 validation error detected: Value '{}' at 'localSecondaryIndexes.1.member.indexName' \
1591 failed to satisfy constraint: Member must have length greater than or equal to 3",
1592 lsi.index_name
1593 )));
1594 }
1595
1596 if !lsi
1598 .index_name
1599 .chars()
1600 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
1601 {
1602 return Err(DynoxideError::ValidationException(format!(
1603 "1 validation error detected: Value '{}' at 'localSecondaryIndexes.1.member.indexName' \
1604 failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
1605 lsi.index_name
1606 )));
1607 }
1608
1609 validate_key_schema(&lsi.key_schema)?;
1611
1612 validate_projection(&lsi.projection, &lsi.index_name)?;
1614
1615 let lsi_pk = lsi
1617 .key_schema
1618 .iter()
1619 .find(|k| k.key_type == KeyType::HASH)
1620 .map(|k| k.attribute_name.as_str());
1621 let lsi_sk = lsi
1622 .key_schema
1623 .iter()
1624 .find(|k| k.key_type == KeyType::RANGE)
1625 .map(|k| k.attribute_name.as_str());
1626
1627 let table_pk = partition_key_name(table_key_schema);
1628 let table_sk = sort_key_name(table_key_schema);
1629
1630 if lsi_pk != table_pk {
1632 return Err(DynoxideError::ValidationException(
1633 "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(),
1634 ));
1635 }
1636
1637 if lsi_sk.is_some() && lsi_sk == table_sk {
1639 return Err(DynoxideError::ValidationException(
1640 "One or more parameter values were invalid: Index KeySchema: The index KeySchema must not be the same as the table KeySchema".to_string(),
1641 ));
1642 }
1643
1644 validate_key_attributes_in_definitions(&lsi.key_schema, all_definitions)?;
1646
1647 Ok(())
1648}
1649
1650pub fn sort_key_name(key_schema: &[KeySchemaElement]) -> Option<&str> {
1652 key_schema
1653 .iter()
1654 .find(|k| k.key_type == KeyType::RANGE)
1655 .map(|k| k.attribute_name.as_str())
1656}
1657
1658#[cfg(test)]
1659mod tests {
1660 use super::*;
1661
1662 #[test]
1666 fn an_empty_gsi_key_schema_is_refused_by_the_shape_check() {
1667 assert_eq!(
1668 check_gsi_key_schema(&[]),
1669 Err(
1670 "1 validation error detected: Value null at 'keySchema' failed to satisfy \
1671 constraint: Member must have length less than or equal to 2"
1672 .to_string()
1673 )
1674 );
1675 }
1676
1677 #[test]
1678 fn java_hash_order_matches_the_captured_lists() {
1679 assert_eq!(
1682 java_hash_order(&["PK", "h", "a", "unused"], true),
1683 ["h", "a", "unused", "PK"]
1684 );
1685 assert_eq!(
1686 java_hash_order(&["PK", "a", "b", "c", "d", "u1"], true),
1687 ["a", "b", "PK", "c", "d", "u1"]
1688 );
1689 assert_eq!(java_hash_order(&["h", "a", "zz"], true), ["h", "zz", "a"]);
1690 assert_eq!(
1691 java_hash_order(&["pk", "sk", "gpk", "gsk"], false),
1692 ["gpk", "sk", "pk", "gsk"]
1693 );
1694 assert_eq!(
1695 java_hash_order(&["orderId", "customerId", "createdAt", "status"], false),
1696 ["createdAt", "orderId", "customerId", "status"]
1697 );
1698 }
1699
1700 fn hash_key(name: &str) -> KeySchemaElement {
1701 KeySchemaElement {
1702 attribute_name: name.to_string(),
1703 key_type: KeyType::HASH,
1704 }
1705 }
1706
1707 fn range_key(name: &str) -> KeySchemaElement {
1708 KeySchemaElement {
1709 attribute_name: name.to_string(),
1710 key_type: KeyType::RANGE,
1711 }
1712 }
1713
1714 fn attr_def(name: &str, attr_type: ScalarAttributeType) -> AttributeDefinition {
1715 AttributeDefinition {
1716 attribute_name: name.to_string(),
1717 attribute_type: attr_type,
1718 }
1719 }
1720
1721 #[test]
1722 fn test_valid_table_name() {
1723 assert!(validate_table_name("MyTable").is_ok());
1724 assert!(validate_table_name("my-table.v2").is_ok());
1725 assert!(validate_table_name("a_b").is_ok());
1726 }
1727
1728 #[test]
1729 fn test_short_table_name_accepted_for_read_write() {
1730 assert!(validate_table_name("ab").is_ok());
1734 assert!(validate_table_name("a").is_ok());
1735 }
1736
1737 #[test]
1738 fn test_empty_table_name_rejected_for_read_write() {
1739 let err = validate_table_name("").unwrap_err().to_string();
1740 assert!(err.contains("Member must have length greater than or equal to 1"));
1741 assert!(!err.contains("greater than or equal to 3"));
1742 }
1743
1744 #[test]
1745 fn test_invalid_table_name_bad_chars() {
1746 assert!(validate_table_name("my table").is_err());
1747 assert!(validate_table_name("my@table").is_err());
1748 }
1749
1750 #[test]
1751 fn test_create_table_context_keeps_min_length_3() {
1752 let errs = table_name_constraint_errors(Some("ab"), TableNameContext::CreateTable);
1753 assert!(
1754 errs.iter()
1755 .any(|e| e.contains("Member must have length greater than or equal to 3"))
1756 );
1757 }
1758
1759 #[test]
1760 fn test_valid_key_schema() {
1761 let schema = vec![hash_key("pk")];
1762 assert!(validate_key_schema(&schema).is_ok());
1763
1764 let schema = vec![hash_key("pk"), range_key("sk")];
1765 assert!(validate_key_schema(&schema).is_ok());
1766 }
1767
1768 #[test]
1769 fn test_invalid_key_schema_empty() {
1770 assert!(validate_key_schema(&[]).is_err());
1771 }
1772
1773 #[test]
1774 fn test_invalid_key_schema_no_hash() {
1775 let schema = vec![range_key("sk")];
1776 assert!(validate_key_schema(&schema).is_err());
1777 }
1778
1779 #[test]
1780 fn test_valid_key_attributes_in_definitions() {
1781 let schema = vec![hash_key("pk"), range_key("sk")];
1782 let defs = vec![
1783 attr_def("pk", ScalarAttributeType::S),
1784 attr_def("sk", ScalarAttributeType::N),
1785 ];
1786 assert!(validate_key_attributes_in_definitions(&schema, &defs).is_ok());
1787 }
1788
1789 #[test]
1790 fn test_missing_key_attribute_in_definitions() {
1791 let schema = vec![hash_key("pk"), range_key("sk")];
1792 let defs = vec![attr_def("pk", ScalarAttributeType::S)];
1793 assert!(validate_key_attributes_in_definitions(&schema, &defs).is_err());
1794 }
1795
1796 #[test]
1797 fn test_partition_key_name() {
1798 let schema = vec![hash_key("pk"), range_key("sk")];
1799 assert_eq!(partition_key_name(&schema), Some("pk"));
1800 }
1801
1802 #[test]
1803 fn test_sort_key_name() {
1804 let schema = vec![hash_key("pk"), range_key("sk")];
1805 assert_eq!(sort_key_name(&schema), Some("sk"));
1806
1807 let schema = vec![hash_key("pk")];
1808 assert_eq!(sort_key_name(&schema), None);
1809 }
1810
1811 #[test]
1812 fn test_envelope_request_validation_wraps_tagged_error() {
1813 let msg = "Value '' at 'expressionAttributeNames' failed to satisfy constraint: \
1814 Map value must satisfy constraint";
1815 let err = envelope_request_validation(DynoxideError::EnvelopedValidation(msg.to_string()));
1816 match err {
1817 DynoxideError::ValidationException(m) => {
1818 assert_eq!(m, format!("1 validation error detected: {msg}"));
1819 }
1820 other => panic!("expected ValidationException, got {other:?}"),
1821 }
1822 }
1823
1824 #[test]
1825 fn test_envelope_request_validation_passes_other_errors_through() {
1826 let plain = envelope_request_validation(DynoxideError::ValidationException("msg".into()));
1827 assert!(matches!(
1828 &plain,
1829 DynoxideError::ValidationException(m) if m == "msg"
1830 ));
1831
1832 let key_empty =
1833 envelope_request_validation(DynoxideError::KeyEmptyValueValidation("msg".into()));
1834 assert!(matches!(
1835 &key_empty,
1836 DynoxideError::KeyEmptyValueValidation(m) if m == "msg"
1837 ));
1838
1839 let not_found =
1840 envelope_request_validation(DynoxideError::ResourceNotFoundException("msg".into()));
1841 assert!(matches!(
1842 ¬_found,
1843 DynoxideError::ResourceNotFoundException(m) if m == "msg"
1844 ));
1845 }
1846
1847 #[test]
1848 fn test_strip_request_validation_tag_untags_without_envelope() {
1849 let err = strip_request_validation_tag(DynoxideError::EnvelopedValidation("msg".into()));
1850 assert!(matches!(
1851 &err,
1852 DynoxideError::ValidationException(m) if m == "msg"
1853 ));
1854 }
1855
1856 #[test]
1857 fn test_strip_request_validation_tag_passes_other_errors_through() {
1858 let plain = strip_request_validation_tag(DynoxideError::ValidationException("msg".into()));
1859 assert!(matches!(
1860 &plain,
1861 DynoxideError::ValidationException(m) if m == "msg"
1862 ));
1863
1864 let not_found =
1865 strip_request_validation_tag(DynoxideError::ResourceNotFoundException("msg".into()));
1866 assert!(matches!(
1867 ¬_found,
1868 DynoxideError::ResourceNotFoundException(m) if m == "msg"
1869 ));
1870 }
1871
1872 #[test]
1873 fn normalising_an_item_sorts_its_sets_in_place_at_every_depth() {
1874 let mut item: Item = [
1878 (
1879 "top".to_string(),
1880 AttributeValue::NS(vec!["3".into(), "-1".into(), "2".into()]),
1881 ),
1882 (
1883 "m".to_string(),
1884 AttributeValue::M(
1885 [(
1886 "ss".to_string(),
1887 AttributeValue::SS(vec!["b".into(), "a".into(), "c".into()]),
1888 )]
1889 .into(),
1890 ),
1891 ),
1892 (
1893 "l".to_string(),
1894 AttributeValue::L(vec![
1895 AttributeValue::NS(vec!["10".into(), "2".into(), "1e2".into()]),
1896 AttributeValue::BS(vec![vec![2], vec![0xff], vec![1]]),
1897 ]),
1898 ),
1899 ]
1900 .into();
1901 normalize_item_sets(&mut item);
1902
1903 assert_eq!(
1904 item["top"],
1905 AttributeValue::NS(vec!["-1".into(), "2".into(), "3".into()])
1906 );
1907 let AttributeValue::M(m) = &item["m"] else {
1908 panic!("m")
1909 };
1910 assert_eq!(
1911 m["ss"],
1912 AttributeValue::SS(vec!["a".into(), "b".into(), "c".into()])
1913 );
1914 let AttributeValue::L(l) = &item["l"] else {
1915 panic!("l")
1916 };
1917 assert_eq!(
1918 l[0],
1919 AttributeValue::NS(vec!["2".into(), "10".into(), "100".into()])
1920 );
1921 assert_eq!(l[1], AttributeValue::BS(vec![vec![1], vec![2], vec![0xff]]));
1922 }
1923}