1use crate::actions::{TableDescription, build_table_description};
2use crate::errors::{DynoxideError, Result};
3use crate::storage_backend::StorageBackend;
4use crate::streams;
5use crate::types::{
6 AttributeDefinition, GlobalSecondaryIndex, KeySchemaElement, KeyType, LocalSecondaryIndex,
7 Projection, ProjectionType, ProvisionedThroughput,
8};
9use serde::{Deserialize, Serialize};
10use web_time::{SystemTime, UNIX_EPOCH};
11
12#[derive(Debug, Default, Deserialize)]
15struct RawRequest {
16 #[serde(rename = "TableName", default)]
17 table_name: Option<String>,
18 #[serde(rename = "KeySchema", default)]
19 key_schema: Option<serde_json::Value>,
20 #[serde(rename = "AttributeDefinitions", default)]
21 attribute_definitions: Option<serde_json::Value>,
22 #[serde(rename = "GlobalSecondaryIndexes", default)]
23 global_secondary_indexes: Option<serde_json::Value>,
24 #[serde(rename = "LocalSecondaryIndexes", default)]
25 local_secondary_indexes: Option<serde_json::Value>,
26 #[serde(rename = "BillingMode", default)]
27 billing_mode: Option<String>,
28 #[serde(rename = "ProvisionedThroughput", default)]
29 provisioned_throughput: Option<serde_json::Value>,
30 #[serde(rename = "StreamSpecification", default)]
31 stream_specification: Option<StreamSpecification>,
32 #[serde(rename = "SSESpecification", default)]
33 sse_specification: Option<crate::types::SseSpecification>,
34 #[serde(rename = "TableClass", default)]
35 table_class: Option<String>,
36 #[serde(rename = "Tags", default)]
37 tags: Option<Vec<crate::types::Tag>>,
38 #[serde(rename = "DeletionProtectionEnabled", default)]
39 deletion_protection_enabled: Option<bool>,
40 #[serde(rename = "OnDemandThroughput", default)]
41 on_demand_throughput: Option<crate::types::OnDemandThroughput>,
42}
43
44#[derive(Debug, Default)]
47pub struct CreateTableRequest {
48 pub table_name: String,
49 pub key_schema: Vec<KeySchemaElement>,
50 pub attribute_definitions: Vec<AttributeDefinition>,
51 pub global_secondary_indexes: Option<Vec<GlobalSecondaryIndex>>,
52 pub local_secondary_indexes: Option<Vec<LocalSecondaryIndex>>,
53 pub billing_mode: Option<String>,
54 pub provisioned_throughput: Option<ProvisionedThroughput>,
55 pub stream_specification: Option<StreamSpecification>,
56 pub sse_specification: Option<crate::types::SseSpecification>,
57 pub table_class: Option<String>,
58 pub tags: Option<Vec<crate::types::Tag>>,
59 pub deletion_protection_enabled: Option<bool>,
60 pub on_demand_throughput: Option<crate::types::OnDemandThroughput>,
61}
62
63impl<'de> serde::Deserialize<'de> for CreateTableRequest {
66 fn deserialize<D: serde::Deserializer<'de>>(
67 deserializer: D,
68 ) -> std::result::Result<Self, D::Error> {
69 let raw = RawRequest::deserialize(deserializer)?;
70 match validate_raw_and_build(raw) {
71 Ok(req) => Ok(req),
72 Err(msg) => Err(serde::de::Error::custom(format!("VALIDATION:{}", msg))),
73 }
74 }
75}
76
77#[derive(Debug, Default, Deserialize)]
78pub struct StreamSpecification {
79 #[serde(rename = "StreamEnabled", alias = "stream_enabled")]
80 pub stream_enabled: bool,
81 #[serde(rename = "StreamViewType", alias = "stream_view_type", default)]
82 pub stream_view_type: Option<String>,
83}
84
85#[derive(Debug, Default, Serialize)]
86pub struct CreateTableResponse {
87 #[serde(rename = "TableDescription")]
88 pub table_description: TableDescription,
89}
90
91pub async fn execute<S: StorageBackend>(
92 storage: &S,
93 request: CreateTableRequest,
94) -> Result<CreateTableResponse> {
95 validate_typed_request(&request)?;
97
98 if let Some(ref tc) = request.table_class {
99 if tc != "STANDARD" && tc != "STANDARD_INFREQUENT_ACCESS" {
100 return Err(DynoxideError::ValidationException(format!(
101 "1 validation error detected: Value '{tc}' at 'tableClass' failed to satisfy \
102 constraint: Member must satisfy enum value set: \
103 [STANDARD, STANDARD_INFREQUENT_ACCESS]"
104 )));
105 }
106 }
107
108 if storage.table_exists(&request.table_name).await? {
109 return Err(DynoxideError::ResourceInUseException(format!(
110 "Table already exists: {}",
111 request.table_name
112 )));
113 }
114
115 let now = SystemTime::now()
116 .duration_since(UNIX_EPOCH)
117 .unwrap_or_default()
118 .as_secs() as i64;
119
120 let key_schema_json = serde_json::to_string(&request.key_schema)
121 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
122 let attr_defs_json = serde_json::to_string(&request.attribute_definitions)
123 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
124 let gsi_json = request
125 .global_secondary_indexes
126 .as_ref()
127 .map(serde_json::to_string)
128 .transpose()
129 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
130 let lsi_json = request
131 .local_secondary_indexes
132 .as_ref()
133 .map(serde_json::to_string)
134 .transpose()
135 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
136 let pt_json = request
137 .provisioned_throughput
138 .as_ref()
139 .map(serde_json::to_string)
140 .transpose()
141 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
142 let normalized_sse = request.sse_specification.as_ref().map(|spec| {
147 if spec.enabled == Some(true) {
148 crate::types::SseSpecification {
149 enabled: Some(true),
150 sse_type: spec.sse_type.clone().or_else(|| Some("KMS".to_string())),
151 kms_master_key_id: spec.kms_master_key_id.clone().or_else(|| {
152 Some(crate::streams::kms_key_arn(
153 &uuid::Uuid::new_v4().to_string(),
154 ))
155 }),
156 }
157 } else {
158 spec.clone()
159 }
160 });
161 let sse_json = normalized_sse
162 .as_ref()
163 .map(serde_json::to_string)
164 .transpose()
165 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
166 let on_demand_json = request
167 .on_demand_throughput
168 .as_ref()
169 .map(serde_json::to_string)
170 .transpose()
171 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
172 let deletion_protection = request.deletion_protection_enabled.unwrap_or(false);
173
174 let billing_mode_str = request.billing_mode.as_deref().unwrap_or("PROVISIONED");
175 storage
176 .insert_table_metadata(&crate::storage::CreateTableMetadata {
177 table_name: &request.table_name,
178 key_schema: &key_schema_json,
179 attribute_definitions: &attr_defs_json,
180 gsi_definitions: gsi_json.as_deref(),
181 lsi_definitions: lsi_json.as_deref(),
182 provisioned_throughput: pt_json.as_deref(),
183 created_at: now,
184 sse_specification: sse_json.as_deref(),
185 table_class: request.table_class.as_deref(),
186 deletion_protection_enabled: deletion_protection,
187 billing_mode: Some(billing_mode_str),
188 on_demand_throughput: on_demand_json.as_deref(),
189 })
190 .await?;
191
192 storage.create_data_table(&request.table_name).await?;
193
194 if let Some(ref gsis) = request.global_secondary_indexes {
195 for gsi in gsis {
196 storage
197 .create_gsi_table(&request.table_name, &gsi.index_name)
198 .await?;
199 }
200 }
201
202 if let Some(ref lsis) = request.local_secondary_indexes {
203 for lsi in lsis {
204 storage
205 .create_lsi_table(&request.table_name, &lsi.index_name)
206 .await?;
207 }
208 }
209
210 if let Some(ref spec) = request.stream_specification {
211 if spec.stream_enabled {
212 let view_type = spec
213 .stream_view_type
214 .as_deref()
215 .unwrap_or("NEW_AND_OLD_IMAGES");
216 let label = streams::generate_stream_label(storage.clock());
217 storage
218 .enable_stream(&request.table_name, view_type, &label)
219 .await?;
220 }
221 }
222
223 if let Some(ref tags) = request.tags {
224 if !tags.is_empty() {
225 storage.set_tags(&request.table_name, tags).await?;
226 }
227 }
228
229 let meta = storage
230 .get_table_metadata(&request.table_name)
231 .await?
232 .ok_or_else(|| {
233 DynoxideError::InternalServerError("Table metadata not found after creation".into())
234 })?;
235
236 let mut desc = build_table_description(&meta, Some(0), Some(0));
237 desc.table_status = "CREATING".to_string();
240
241 let billing_mode_str = request.billing_mode.as_deref().unwrap_or("PROVISIONED");
243 if billing_mode_str == "PROVISIONED" {
244 desc.billing_mode_summary = None;
245 desc.table_throughput_mode_summary = None;
246 } else if billing_mode_str == "PAY_PER_REQUEST" {
247 desc.billing_mode_summary = Some(crate::actions::BillingModeSummary {
248 billing_mode: "PAY_PER_REQUEST".to_string(),
249 last_update_to_pay_per_request_date_time: None,
250 });
251 desc.table_throughput_mode_summary = Some(crate::actions::TableThroughputModeSummary {
252 table_throughput_mode: "PAY_PER_REQUEST".to_string(),
253 last_update_to_pay_per_request_date_time: None,
254 });
255 desc.provisioned_throughput = Some(crate::actions::TableProvisionedThroughputDescription {
257 read_capacity_units: 0,
258 write_capacity_units: 0,
259 number_of_decreases_today: 0,
260 last_increase_date_time: None,
261 last_decrease_date_time: None,
262 });
263 }
264
265 if let Some(ref mut gsis) = desc.global_secondary_indexes {
267 for gsi in gsis {
268 gsi.index_status = "CREATING".to_string();
269 }
270 }
271
272 if request.deletion_protection_enabled.is_none() {
275 desc.deletion_protection_enabled = None;
276 }
277
278 Ok(CreateTableResponse {
279 table_description: desc,
280 })
281}
282
283fn ve(msg: String) -> DynoxideError {
285 DynoxideError::ValidationException(msg)
286}
287
288fn validate_typed_request(request: &CreateTableRequest) -> Result<()> {
305 if request.table_name.is_empty() {
306 return Err(DynoxideError::ValidationException(
307 "The parameter 'TableName' is required but was not present in the request".to_string(),
308 ));
309 }
310 if request.table_name.len() < 3 || request.table_name.len() > 255 {
311 return Err(DynoxideError::ValidationException(
312 "TableName must be at least 3 characters long and at most 255 characters long"
313 .to_string(),
314 ));
315 }
316
317 if !request
319 .table_name
320 .chars()
321 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
322 {
323 return Err(DynoxideError::ValidationException(format!(
324 "1 validation error detected: Value '{}' at 'tableName' failed to satisfy constraint: \
325 Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
326 request.table_name
327 )));
328 }
329
330 let billing_mode_str = request.billing_mode.as_deref().unwrap_or("PROVISIONED");
332 if billing_mode_str == "PAY_PER_REQUEST" && request.provisioned_throughput.is_some() {
333 return Err(DynoxideError::ValidationException(
334 "One or more parameter values were invalid: Neither ReadCapacityUnits nor \
335 WriteCapacityUnits can be specified when BillingMode is PAY_PER_REQUEST"
336 .to_string(),
337 ));
338 }
339
340 if let Some(ref pt) = request.provisioned_throughput {
342 const MAX_THROUGHPUT: i64 = 1_000_000_000_000;
343 let rcu = pt.read_capacity_units.unwrap_or(0);
344 let wcu = pt.write_capacity_units.unwrap_or(0);
345 if rcu > MAX_THROUGHPUT {
346 return Err(DynoxideError::ValidationException(format!(
347 "Given value {} for ReadCapacityUnits is out of bounds",
348 rcu
349 )));
350 }
351 if wcu > MAX_THROUGHPUT {
352 return Err(DynoxideError::ValidationException(format!(
353 "Given value {} for WriteCapacityUnits is out of bounds",
354 wcu
355 )));
356 }
357 }
358
359 if request.billing_mode.is_some()
364 && billing_mode_str == "PROVISIONED"
365 && request.provisioned_throughput.is_none()
366 {
367 return Err(DynoxideError::ValidationException(
368 "One or more parameter values were invalid: ReadCapacityUnits and \
369 WriteCapacityUnits must both be specified when BillingMode is PROVISIONED"
370 .to_string(),
371 ));
372 }
373
374 validate_key_attrs_in_defs(&request.key_schema, &request.attribute_definitions).map_err(ve)?;
376
377 validate_key_schema_structure(&request.key_schema).map_err(ve)?;
379
380 if let Some(ref lsis) = request.local_secondary_indexes {
382 if lsis.is_empty() {
383 return Err(ve(
384 "One or more parameter values were invalid: List of LocalSecondaryIndexes is empty"
385 .to_string(),
386 ));
387 }
388 }
389 if let Some(ref gsis) = request.global_secondary_indexes {
390 if gsis.is_empty() {
391 return Err(ve(
392 "One or more parameter values were invalid: List of GlobalSecondaryIndexes is empty"
393 .to_string(),
394 ));
395 }
396 }
397
398 if let Some(ref lsis) = request.local_secondary_indexes {
400 validate_lsi_list(lsis, &request.key_schema, &request.attribute_definitions).map_err(ve)?;
401 }
402
403 if let Some(ref gsis) = request.global_secondary_indexes {
405 let bm = request.billing_mode.as_deref().unwrap_or("PROVISIONED");
406 validate_gsi_list(gsis, &request.attribute_definitions, bm).map_err(ve)?;
407 }
408
409 check_cross_index_duplicates(
411 &request.local_secondary_indexes,
412 &request.global_secondary_indexes,
413 )
414 .map_err(ve)?;
415
416 validate_attr_def_count(
418 &request.key_schema,
419 &request.attribute_definitions,
420 &request.local_secondary_indexes,
421 &request.global_secondary_indexes,
422 )
423 .map_err(ve)?;
424
425 if let Some(ref spec) = request.stream_specification {
429 if !spec.stream_enabled && spec.stream_view_type.is_some() {
430 return Err(DynoxideError::ValidationException(
431 "One or more parameter values were invalid: Table is being created with a stream \
432 disabled, UpdateViewType should not be specified"
433 .to_string(),
434 ));
435 }
436 }
437
438 Ok(())
439}
440
441fn check_cross_index_duplicates(
442 lsis: &Option<Vec<LocalSecondaryIndex>>,
443 gsis: &Option<Vec<GlobalSecondaryIndex>>,
444) -> std::result::Result<(), String> {
445 if let (Some(lsis), Some(gsis)) = (lsis, gsis) {
446 let mut all_names = std::collections::HashSet::new();
447 for lsi in lsis {
448 all_names.insert(&lsi.index_name);
449 }
450 for gsi in gsis {
451 if !all_names.insert(&gsi.index_name) {
452 return Err(format!(
453 "One or more parameter values were invalid: Duplicate index name: {}",
454 gsi.index_name
455 ));
456 }
457 }
458 }
459 Ok(())
460}
461
462fn validate_raw_and_build(raw: RawRequest) -> std::result::Result<CreateTableRequest, String> {
465 if raw.table_name.is_none() {
467 return Err(
468 "The parameter 'TableName' is required but was not present in the request".to_string(),
469 );
470 }
471
472 let name_errors = crate::validation::table_name_constraint_errors(
476 raw.table_name.as_deref(),
477 crate::validation::TableNameContext::CreateTable,
478 );
479 if !name_errors.is_empty() {
480 let msg = format!(
481 "{} validation error{} detected: {}",
482 name_errors.len(),
483 if name_errors.len() > 1 { "s" } else { "" },
484 name_errors.join("; ")
485 );
486 return Err(msg);
487 }
488 let table_name = raw.table_name.unwrap();
489
490 let mut errors = Vec::new();
491
492 if let Some(ref bm) = raw.billing_mode {
493 if bm != "PROVISIONED" && bm != "PAY_PER_REQUEST" {
494 errors.push(format!(
495 "Value '{}' at 'billingMode' failed to satisfy constraint: \
496 Member must satisfy enum value set: [PROVISIONED, PAY_PER_REQUEST]",
497 bm
498 ));
499 }
500 }
501
502 collect_pt_errors(&raw.provisioned_throughput, &mut errors);
503 collect_ks_errors(&raw.key_schema, &mut errors);
504 collect_ad_errors(&raw.attribute_definitions, &mut errors);
505 collect_lsi_errors(&raw.local_secondary_indexes, &mut errors);
506 collect_gsi_errors(&raw.global_secondary_indexes, &mut errors);
507
508 errors.truncate(10);
510
511 if !errors.is_empty() {
512 let prefix = format!(
513 "{} validation error{} detected: ",
514 errors.len(),
515 if errors.len() == 1 { "" } else { "s" }
516 );
517 return Err(format!("{}{}", prefix, errors.join("; ")));
518 }
519
520 let billing_mode_str = raw.billing_mode.as_deref().unwrap_or("PROVISIONED");
522 if billing_mode_str == "PAY_PER_REQUEST" && raw.provisioned_throughput.is_some() {
523 return Err(
524 "One or more parameter values were invalid: Neither ReadCapacityUnits nor \
525 WriteCapacityUnits can be specified when BillingMode is PAY_PER_REQUEST"
526 .to_string(),
527 );
528 }
529
530 if let Some(ref pt) = raw.provisioned_throughput {
532 if let Some(obj) = pt.as_object() {
533 let rcu = obj
534 .get("ReadCapacityUnits")
535 .and_then(|v| v.as_i64())
536 .unwrap_or(0);
537 let wcu = obj
538 .get("WriteCapacityUnits")
539 .and_then(|v| v.as_i64())
540 .unwrap_or(0);
541 const MAX_THROUGHPUT: i64 = 1_000_000_000_000;
542 if rcu > MAX_THROUGHPUT {
543 return Err(format!(
544 "Given value {} for ReadCapacityUnits is out of bounds",
545 rcu
546 ));
547 }
548 if wcu > MAX_THROUGHPUT {
549 return Err(format!(
550 "Given value {} for WriteCapacityUnits is out of bounds",
551 wcu
552 ));
553 }
554 }
555 }
556
557 if raw.billing_mode.as_deref() == Some("PROVISIONED") && raw.provisioned_throughput.is_none() {
559 return Err(
560 "One or more parameter values were invalid: ReadCapacityUnits and \
561 WriteCapacityUnits must both be specified when BillingMode is PROVISIONED"
562 .to_string(),
563 );
564 }
565
566 let key_schema: Vec<KeySchemaElement> = raw
568 .key_schema
569 .as_ref()
570 .map(|v| serde_json::from_value(v.clone()))
571 .transpose()
572 .map_err(|e| e.to_string())?
573 .unwrap_or_default();
574 let attribute_definitions: Vec<AttributeDefinition> = raw
575 .attribute_definitions
576 .as_ref()
577 .map(|v| serde_json::from_value(v.clone()))
578 .transpose()
579 .map_err(|e| e.to_string())?
580 .unwrap_or_default();
581 let provisioned_throughput: Option<ProvisionedThroughput> = raw
582 .provisioned_throughput
583 .as_ref()
584 .map(|v| serde_json::from_value(v.clone()))
585 .transpose()
586 .map_err(|e| e.to_string())?;
587 let global_secondary_indexes: Option<Vec<GlobalSecondaryIndex>> = raw
588 .global_secondary_indexes
589 .as_ref()
590 .map(|v| serde_json::from_value(v.clone()))
591 .transpose()
592 .map_err(|e| e.to_string())?;
593 let local_secondary_indexes: Option<Vec<LocalSecondaryIndex>> = raw
594 .local_secondary_indexes
595 .as_ref()
596 .map(|v| serde_json::from_value(v.clone()))
597 .transpose()
598 .map_err(|e| e.to_string())?;
599
600 Ok(CreateTableRequest {
601 table_name,
602 key_schema,
603 attribute_definitions,
604 global_secondary_indexes,
605 local_secondary_indexes,
606 billing_mode: raw.billing_mode,
607 provisioned_throughput,
608 stream_specification: raw.stream_specification,
609 sse_specification: raw.sse_specification,
610 table_class: raw.table_class,
611 tags: raw.tags,
612 deletion_protection_enabled: raw.deletion_protection_enabled,
613 on_demand_throughput: raw.on_demand_throughput,
614 })
615}
616
617fn collect_pt_errors(pt_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
620 if let Some(v) = pt_val {
621 if let Some(obj) = v.as_object() {
622 let wcu = obj.get("WriteCapacityUnits");
623 let rcu = obj.get("ReadCapacityUnits");
624 if wcu.is_none() || wcu == Some(&serde_json::Value::Null) {
625 errors.push("Value null at 'provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must not be null".to_string());
626 } else if let Some(w) = wcu.and_then(|v| v.as_i64()) {
627 if w < 1 {
628 errors.push(format!("Value '{}' at 'provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", w));
629 }
630 }
631 if rcu.is_none() || rcu == Some(&serde_json::Value::Null) {
632 errors.push("Value null at 'provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must not be null".to_string());
633 } else if let Some(r) = rcu.and_then(|v| v.as_i64()) {
634 if r < 1 {
635 errors.push(format!("Value '{}' at 'provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", r));
636 }
637 }
638 }
639 }
640}
641
642fn collect_ks_errors(ks_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
643 match ks_val {
644 None => {
645 errors.push(
646 "Value null at 'keySchema' failed to satisfy constraint: Member must not be null"
647 .to_string(),
648 );
649 }
650 Some(v) => {
651 if let Some(arr) = v.as_array() {
652 if arr.is_empty() {
653 errors.push("Value '[]' at 'keySchema' failed to satisfy constraint: Member must have length greater than or equal to 1".to_string());
654 } else if arr.len() > 2 {
655 let dump = render_key_schema_java_toString(arr);
656 errors.push(format!("Value '{}' at 'keySchema' failed to satisfy constraint: Member must have length less than or equal to 2", dump));
657 }
658 for (i, elem) in arr.iter().enumerate().take(10) {
659 collect_ks_elem_errors(elem, i + 1, errors);
660 }
661 }
662 }
663 }
664}
665
666fn collect_ks_elem_errors(elem: &serde_json::Value, idx: usize, errors: &mut Vec<String>) {
667 if let Some(obj) = elem.as_object() {
668 if !obj.contains_key("AttributeName")
669 || obj.get("AttributeName") == Some(&serde_json::Value::Null)
670 {
671 errors.push(format!("Value null at 'keySchema.{}.member.attributeName' failed to satisfy constraint: Member must not be null", idx));
672 }
673 let kt = obj.get("KeyType");
674 if kt.is_none() || kt == Some(&serde_json::Value::Null) {
675 errors.push(format!("Value null at 'keySchema.{}.member.keyType' failed to satisfy constraint: Member must not be null", idx));
676 } else if let Some(s) = kt.and_then(|v| v.as_str()) {
677 if s != "HASH" && s != "RANGE" {
678 errors.push(format!("Value '{}' at 'keySchema.{}.member.keyType' failed to satisfy constraint: Member must satisfy enum value set: [HASH, RANGE]", s, idx));
679 }
680 }
681 }
682}
683
684#[allow(non_snake_case)]
690fn render_key_schema_java_toString(arr: &[serde_json::Value]) -> String {
691 let parts: Vec<String> = arr
692 .iter()
693 .map(|elem| {
694 let an = elem
695 .get("AttributeName")
696 .and_then(|v| v.as_str())
697 .unwrap_or("");
698 let kt = elem.get("KeyType").and_then(|v| v.as_str()).unwrap_or("");
699 format!("KeySchemaElement(attributeName={an}, keyType={kt})")
700 })
701 .collect();
702 format!("[{}]", parts.join(", "))
703}
704
705fn collect_ad_errors(ad_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
706 match ad_val {
707 None => {
708 errors.push("Value null at 'attributeDefinitions' failed to satisfy constraint: Member must not be null".to_string());
709 }
710 Some(v) => {
711 if let Some(arr) = v.as_array() {
712 for (i, elem) in arr.iter().enumerate() {
713 if let Some(obj) = elem.as_object() {
714 if !obj.contains_key("AttributeName")
715 || obj.get("AttributeName") == Some(&serde_json::Value::Null)
716 {
717 errors.push(format!("Value null at 'attributeDefinitions.{}.member.attributeName' failed to satisfy constraint: Member must not be null", i + 1));
718 }
719 let at = obj.get("AttributeType");
720 if at.is_none() || at == Some(&serde_json::Value::Null) {
721 errors.push(format!("Value null at 'attributeDefinitions.{}.member.attributeType' failed to satisfy constraint: Member must not be null", i + 1));
722 } else if let Some(s) = at.and_then(|v| v.as_str()) {
723 if s != "S" && s != "N" && s != "B" {
724 errors.push(format!("Value '{}' at 'attributeDefinitions.{}.member.attributeType' failed to satisfy constraint: Member must satisfy enum value set: [B, N, S]", s, i + 1));
725 }
726 }
727 }
728 }
729 }
730 }
731 }
732}
733
734fn collect_lsi_errors(lsi_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
735 if let Some(v) = lsi_val {
736 if let Some(arr) = v.as_array() {
737 for (i, elem) in arr.iter().enumerate().take(10) {
738 if let Some(obj) = elem.as_object() {
739 if !obj.contains_key("IndexName")
741 || obj.get("IndexName") == Some(&serde_json::Value::Null)
742 {
743 errors.push(format!("Value null at 'localSecondaryIndexes.{}.member.indexName' failed to satisfy constraint: Member must not be null", i + 1));
744 } else if let Some(name) = obj.get("IndexName").and_then(|v| v.as_str()) {
745 collect_idx_name_errors(name, "localSecondaryIndexes", i + 1, errors);
746 }
747 if !obj.contains_key("KeySchema")
748 || obj.get("KeySchema") == Some(&serde_json::Value::Null)
749 {
750 errors.push(format!("Value null at 'localSecondaryIndexes.{}.member.keySchema' failed to satisfy constraint: Member must not be null", i + 1));
751 } else if let Some(ks) = obj.get("KeySchema").and_then(|v| v.as_array()) {
752 if ks.is_empty() {
753 errors.push(format!("Value '[]' at 'localSecondaryIndexes.{}.member.keySchema' failed to satisfy constraint: Member must have length greater than or equal to 1", i + 1));
754 }
755 }
756 if !obj.contains_key("Projection")
757 || obj.get("Projection") == Some(&serde_json::Value::Null)
758 {
759 errors.push(format!("Value null at 'localSecondaryIndexes.{}.member.projection' failed to satisfy constraint: Member must not be null", i + 1));
760 } else if let Some(p) = obj.get("Projection").and_then(|v| v.as_object()) {
761 collect_proj_errors(p, &format!("localSecondaryIndexes.{}", i + 1), errors);
762 }
763 }
764 }
765 }
766 }
767}
768
769fn collect_gsi_errors(gsi_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
770 if let Some(v) = gsi_val {
771 if let Some(arr) = v.as_array() {
772 for (i, elem) in arr.iter().enumerate().take(10) {
773 if let Some(obj) = elem.as_object() {
774 if !obj.contains_key("KeySchema")
776 || obj.get("KeySchema") == Some(&serde_json::Value::Null)
777 {
778 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.keySchema' failed to satisfy constraint: Member must not be null", i + 1));
779 } else if let Some(ks) = obj.get("KeySchema").and_then(|v| v.as_array()) {
780 if ks.is_empty() {
781 errors.push(format!("Value '[]' at 'globalSecondaryIndexes.{}.member.keySchema' failed to satisfy constraint: Member must have length greater than or equal to 1", i + 1));
782 }
783 }
784 if !obj.contains_key("Projection")
785 || obj.get("Projection") == Some(&serde_json::Value::Null)
786 {
787 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.projection' failed to satisfy constraint: Member must not be null", i + 1));
788 } else if let Some(p) = obj.get("Projection").and_then(|v| v.as_object()) {
789 collect_proj_errors(
790 p,
791 &format!("globalSecondaryIndexes.{}", i + 1),
792 errors,
793 );
794 }
795 if !obj.contains_key("IndexName")
796 || obj.get("IndexName") == Some(&serde_json::Value::Null)
797 {
798 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.indexName' failed to satisfy constraint: Member must not be null", i + 1));
799 } else if let Some(name) = obj.get("IndexName").and_then(|v| v.as_str()) {
800 collect_idx_name_errors(name, "globalSecondaryIndexes", i + 1, errors);
801 }
802 if let Some(pt) = obj.get("ProvisionedThroughput").and_then(|v| v.as_object()) {
804 let wcu = pt.get("WriteCapacityUnits");
805 let rcu = pt.get("ReadCapacityUnits");
806 if let Some(w) = wcu.and_then(|v| v.as_i64()) {
807 if w < 1 {
808 errors.push(format!("Value '{}' at 'globalSecondaryIndexes.{}.member.provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", w, i + 1));
809 }
810 } else if wcu.is_none() || wcu == Some(&serde_json::Value::Null) {
811 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must not be null", i + 1));
812 }
813 if let Some(r) = rcu.and_then(|v| v.as_i64()) {
814 if r < 1 {
815 errors.push(format!("Value '{}' at 'globalSecondaryIndexes.{}.member.provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", r, i + 1));
816 }
817 } else if rcu.is_none() || rcu == Some(&serde_json::Value::Null) {
818 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must not be null", i + 1));
819 }
820 }
821 }
822 }
823 }
824 }
825}
826
827fn collect_idx_name_errors(name: &str, prefix: &str, idx: usize, errors: &mut Vec<String>) {
828 if !name
829 .chars()
830 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
831 {
832 errors.push(format!("Value '{}' at '{}.{}.member.indexName' failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+", name, prefix, idx));
833 }
834 if name.len() < 3 {
835 errors.push(format!("Value '{}' at '{}.{}.member.indexName' failed to satisfy constraint: Member must have length greater than or equal to 3", name, prefix, idx));
836 }
837 if name.len() > 255 {
838 errors.push(format!("Value '{}' at '{}.{}.member.indexName' failed to satisfy constraint: Member must have length less than or equal to 255", name, prefix, idx));
839 }
840}
841
842fn collect_proj_errors(
843 proj: &serde_json::Map<String, serde_json::Value>,
844 prefix: &str,
845 errors: &mut Vec<String>,
846) {
847 if let Some(pt) = proj.get("ProjectionType") {
848 if let Some(s) = pt.as_str() {
849 if s != "ALL" && s != "KEYS_ONLY" && s != "INCLUDE" {
850 errors.push(format!("Value '{}' at '{}.member.projection.projectionType' failed to satisfy constraint: Member must satisfy enum value set: [ALL, INCLUDE, KEYS_ONLY]", s, prefix));
851 }
852 }
853 }
854 if let Some(nka) = proj.get("NonKeyAttributes") {
855 if let Some(arr) = nka.as_array() {
856 if arr.is_empty() {
857 errors.push(format!("Value '[]' at '{}.member.projection.nonKeyAttributes' failed to satisfy constraint: Member must have length greater than or equal to 1", prefix));
858 }
859 }
860 }
861}
862
863fn validate_key_schema_structure(ks: &[KeySchemaElement]) -> std::result::Result<(), String> {
866 if ks.is_empty() {
867 return Err("1 validation error detected: Value null at 'keySchema' failed to satisfy constraint: Member must have length less than or equal to 2".to_string());
868 }
869 if ks[0].key_type != KeyType::HASH {
870 return Err(
871 "Invalid KeySchema: The first KeySchemaElement is not a HASH key type".to_string(),
872 );
873 }
874 if ks.len() == 2 && ks[0].attribute_name == ks[1].attribute_name {
875 return Err(
876 "Both the Hash Key and the Range Key element in the KeySchema have the same name"
877 .to_string(),
878 );
879 }
880 if ks.len() == 2 && ks[1].key_type != KeyType::RANGE {
881 return Err(
882 "Invalid KeySchema: The second KeySchemaElement is not a RANGE key type".to_string(),
883 );
884 }
885 Ok(())
886}
887
888fn validate_key_attrs_in_defs(
889 ks: &[KeySchemaElement],
890 defs: &[AttributeDefinition],
891) -> std::result::Result<(), String> {
892 let missing: Vec<&str> = ks
894 .iter()
895 .filter(|k| !defs.iter().any(|d| d.attribute_name == k.attribute_name))
896 .map(|k| k.attribute_name.as_str())
897 .collect();
898
899 if missing.is_empty() {
900 let has_dup_names = ks.len() == 2 && ks[0].attribute_name == ks[1].attribute_name;
903 if has_dup_names {
904 return Err(
905 "Invalid KeySchema: Some index key attribute have no definition".to_string(),
906 );
907 }
908 return Ok(());
909 }
910
911 let has_dup_names = ks.len() == 2 && ks[0].attribute_name == ks[1].attribute_name;
917 let has_dup_types = ks.len() == 2 && ks[0].key_type == ks[1].key_type;
918 let use_generic = defs.is_empty() || ks.len() >= 2 || has_dup_names || has_dup_types;
919
920 if use_generic {
921 return Err("Invalid KeySchema: Some index key attribute have no definition".to_string());
922 }
923
924 let key_names: Vec<&str> = missing.to_vec();
926 let def_names: Vec<&str> = defs.iter().map(|d| d.attribute_name.as_str()).collect();
927 Err(format!(
928 "One or more parameter values were invalid: Some index key attributes are not defined in \
929 AttributeDefinitions. Keys: [{}], AttributeDefinitions: [{}]",
930 key_names.join(", "),
931 def_names.join(", ")
932 ))
933}
934
935fn validate_attr_def_count(
936 ks: &[KeySchemaElement],
937 defs: &[AttributeDefinition],
938 lsis: &Option<Vec<LocalSecondaryIndex>>,
939 gsis: &Option<Vec<GlobalSecondaryIndex>>,
940) -> std::result::Result<(), String> {
941 let mut all_key_attrs = std::collections::HashSet::new();
942 for k in ks {
943 all_key_attrs.insert(k.attribute_name.as_str());
944 }
945 if let Some(lsis) = lsis {
946 for lsi in lsis {
947 for k in &lsi.key_schema {
948 all_key_attrs.insert(k.attribute_name.as_str());
949 }
950 }
951 }
952 if let Some(gsis) = gsis {
953 for gsi in gsis {
954 for k in &gsi.key_schema {
955 all_key_attrs.insert(k.attribute_name.as_str());
956 }
957 }
958 }
959 if defs.len() != all_key_attrs.len() {
960 return Err("One or more parameter values were invalid: Number of attributes in KeySchema does not exactly match number of attributes defined in AttributeDefinitions".to_string());
961 }
962 Ok(())
963}
964
965fn validate_lsi_list(
966 lsis: &[LocalSecondaryIndex],
967 ks: &[KeySchemaElement],
968 defs: &[AttributeDefinition],
969) -> std::result::Result<(), String> {
970 if !ks.iter().any(|k| k.key_type == KeyType::RANGE) {
973 return Err("One or more parameter values were invalid: Table KeySchema does not have a range key, which is required when specifying a LocalSecondaryIndex".to_string());
974 }
975
976 let def_names: Vec<&str> = defs.iter().map(|d| d.attribute_name.as_str()).collect();
978 let mut missing_keys = Vec::new();
979 for lsi in lsis {
980 for k in &lsi.key_schema {
981 if !def_names.contains(&k.attribute_name.as_str())
982 && !missing_keys.contains(&k.attribute_name.as_str())
983 {
984 missing_keys.push(k.attribute_name.as_str());
985 }
986 }
987 }
988 if !missing_keys.is_empty() {
989 let mut all_keys = Vec::new();
990 for lsi in lsis {
991 for k in &lsi.key_schema {
992 if !all_keys.contains(&k.attribute_name.as_str()) {
993 all_keys.push(k.attribute_name.as_str());
994 }
995 }
996 }
997 return Err(format!(
998 "One or more parameter values were invalid: Some index key attributes are not defined in AttributeDefinitions. Keys: [{}], AttributeDefinitions: [{}]",
999 all_keys.join(", "),
1000 def_names.join(", ")
1001 ));
1002 }
1003
1004 for lsi in lsis {
1006 validate_lsi_structure(lsi, ks)?;
1007 }
1008
1009 let mut seen = std::collections::HashSet::new();
1011 for lsi in lsis {
1012 if !seen.insert(&lsi.index_name) {
1013 return Err(format!(
1014 "One or more parameter values were invalid: Duplicate index name: {}",
1015 lsi.index_name
1016 ));
1017 }
1018 }
1019
1020 if lsis.len() > 5 {
1022 return Err("One or more parameter values were invalid: Number of LocalSecondaryIndexes exceeds per-table limit of 5".to_string());
1023 }
1024
1025 Ok(())
1026}
1027
1028fn validate_gsi_list(
1029 gsis: &[GlobalSecondaryIndex],
1030 defs: &[AttributeDefinition],
1031 bm: &str,
1032) -> std::result::Result<(), String> {
1033 let def_names: Vec<&str> = defs.iter().map(|d| d.attribute_name.as_str()).collect();
1037 let mut missing_keys = Vec::new();
1038 for gsi in gsis {
1039 for k in &gsi.key_schema {
1040 if !def_names.contains(&k.attribute_name.as_str())
1041 && !missing_keys.contains(&k.attribute_name.as_str())
1042 {
1043 missing_keys.push(k.attribute_name.as_str());
1044 }
1045 }
1046 }
1047 if !missing_keys.is_empty() {
1048 let mut all_keys = Vec::new();
1049 for gsi in gsis {
1050 for k in &gsi.key_schema {
1051 if !all_keys.contains(&k.attribute_name.as_str()) {
1052 all_keys.push(k.attribute_name.as_str());
1053 }
1054 }
1055 }
1056 return Err(format!(
1057 "One or more parameter values were invalid: Some index key attributes are not defined in AttributeDefinitions. Keys: [{}], AttributeDefinitions: [{}]",
1058 all_keys.join(", "),
1059 def_names.join(", ")
1060 ));
1061 }
1062
1063 for gsi in gsis {
1065 validate_gsi_structure(gsi)?;
1066 }
1067
1068 let mut seen = std::collections::HashSet::new();
1070 for gsi in gsis {
1071 if !seen.insert(&gsi.index_name) {
1072 return Err(format!(
1073 "One or more parameter values were invalid: Duplicate index name: {}",
1074 gsi.index_name
1075 ));
1076 }
1077 }
1078
1079 if gsis.len() > 20 {
1081 return Err("One or more parameter values were invalid: GlobalSecondaryIndex count exceeds the per-table limit of 20".to_string());
1082 }
1083
1084 if bm == "PAY_PER_REQUEST" {
1086 for gsi in gsis {
1087 if gsi.provisioned_throughput.is_some() {
1088 return Err(format!(
1089 "One or more parameter values were invalid: ProvisionedThroughput should not be specified for index: {} when BillingMode is PAY_PER_REQUEST",
1090 gsi.index_name
1091 ));
1092 }
1093 }
1094 }
1095
1096 Ok(())
1097}
1098
1099fn validate_lsi_structure(
1100 lsi: &LocalSecondaryIndex,
1101 table_ks: &[KeySchemaElement],
1102) -> std::result::Result<(), String> {
1103 validate_key_schema_structure(&lsi.key_schema)?;
1105
1106 let lsi_sk = lsi.key_schema.iter().find(|k| k.key_type == KeyType::RANGE);
1108 if lsi_sk.is_none() {
1109 return Err(format!(
1110 "One or more parameter values were invalid: Index KeySchema does not have a range key for index: {}",
1111 lsi.index_name
1112 ));
1113 }
1114
1115 let table_pk = table_ks
1117 .iter()
1118 .find(|k| k.key_type == KeyType::HASH)
1119 .map(|k| k.attribute_name.as_str());
1120 let lsi_pk = lsi
1121 .key_schema
1122 .iter()
1123 .find(|k| k.key_type == KeyType::HASH)
1124 .map(|k| k.attribute_name.as_str());
1125 if lsi_pk != table_pk {
1126 return Err(format!(
1127 "One or more parameter values were invalid: \
1128 Index KeySchema does not have the same leading hash key as table KeySchema \
1129 for index: {}. index hash key: {}, table hash key: {}",
1130 lsi.index_name,
1131 lsi_pk.unwrap_or("null"),
1132 table_pk.unwrap_or("null")
1133 ));
1134 }
1135
1136 validate_proj_structure(&lsi.projection)?;
1138
1139 Ok(())
1140}
1141
1142fn validate_gsi_structure(gsi: &GlobalSecondaryIndex) -> std::result::Result<(), String> {
1143 validate_key_schema_structure(&gsi.key_schema)?;
1144 validate_proj_structure(&gsi.projection)?;
1145 Ok(())
1146}
1147
1148fn validate_proj_structure(p: &Projection) -> std::result::Result<(), String> {
1149 match &p.projection_type {
1154 None => Err(
1155 "One or more parameter values were invalid: Unknown ProjectionType: null".to_string(),
1156 ),
1157 Some(ProjectionType::ALL) => match &p.non_key_attributes {
1158 Some(_) => Err("One or more parameter values were invalid: ProjectionType is ALL, but NonKeyAttributes is specified".to_string()),
1159 None => Ok(()),
1160 },
1161 Some(ProjectionType::KEYS_ONLY) => match &p.non_key_attributes {
1162 Some(_) => Err("One or more parameter values were invalid: ProjectionType is KEYS_ONLY, but NonKeyAttributes is specified".to_string()),
1163 None => Ok(()),
1164 },
1165 Some(ProjectionType::INCLUDE) => match &p.non_key_attributes {
1166 None => Err("One or more parameter values were invalid: ProjectionType is INCLUDE, but NonKeyAttributes is not specified".to_string()),
1167 Some(nka) if nka.is_empty() => Err("One or more parameter values were invalid: NonKeyAttributes must not be empty".to_string()),
1168 Some(_) => Ok(()),
1169 },
1170 }
1171}