1use crate::actions::{TableDescription, build_table_description};
2use crate::errors::{DynoxideError, Result};
3use crate::storage_backend::{BackendError, 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 mut request: CreateTableRequest,
94) -> Result<CreateTableResponse> {
95 if request.on_demand_throughput.as_ref().is_some_and(|odt| {
99 odt.max_read_request_units.is_none() && odt.max_write_request_units.is_none()
100 }) {
101 request.on_demand_throughput = None;
102 }
103
104 validate_typed_request(&request)?;
106
107 if let Some(ref bm) = request.billing_mode {
108 if bm != "PROVISIONED" && bm != "PAY_PER_REQUEST" {
109 return Err(DynoxideError::ValidationException(format!(
110 "1 validation error detected: Value '{bm}' at 'billingMode' failed to satisfy \
111 constraint: Member must satisfy enum value set: \
112 [PROVISIONED, PAY_PER_REQUEST]"
113 )));
114 }
115 }
116
117 if let Some(ref tc) = request.table_class {
118 if tc != "STANDARD" && tc != "STANDARD_INFREQUENT_ACCESS" {
119 return Err(DynoxideError::ValidationException(format!(
120 "1 validation error detected: Value '{tc}' at 'tableClass' failed to satisfy \
121 constraint: Member must satisfy enum value set: \
122 [STANDARD, STANDARD_INFREQUENT_ACCESS]"
123 )));
124 }
125 }
126
127 if let Some(ref odt) = request.on_demand_throughput {
133 let members = [
134 ("MaxReadRequestUnits", odt.max_read_request_units),
135 ("MaxWriteRequestUnits", odt.max_write_request_units),
136 ];
137 if let Some((member, _)) = members.iter().find(|(_, v)| v.is_some()) {
138 let billing_mode_str = request.billing_mode.as_deref().unwrap_or("PROVISIONED");
139 if billing_mode_str == "PROVISIONED" {
140 return Err(DynoxideError::ValidationException(format!(
141 "One or more parameter values were invalid: {member} for \
142 OnDemandThroughput cannot be specified when table BillingMode \
143 is PROVISIONED."
144 )));
145 }
146 }
147 for (member, value) in members {
148 if value.is_some_and(|v| v < 1) {
149 return Err(DynoxideError::ValidationException(format!(
150 "One or more parameter values were invalid: Requested {member} for \
151 OnDemandThroughput for table is outside of valid range"
152 )));
153 }
154 }
155 }
156
157 if storage.table_exists(&request.table_name).await? {
158 return Err(DynoxideError::ResourceInUseException(format!(
159 "Table already exists: {}",
160 request.table_name
161 )));
162 }
163
164 let now = SystemTime::now()
165 .duration_since(UNIX_EPOCH)
166 .unwrap_or_default()
167 .as_secs() as i64;
168
169 let key_schema_json = serde_json::to_string(&request.key_schema)
170 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
171 let attr_defs_json = serde_json::to_string(&request.attribute_definitions)
172 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
173 let gsi_json = request
174 .global_secondary_indexes
175 .as_ref()
176 .map(serde_json::to_string)
177 .transpose()
178 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
179 let lsi_json = request
180 .local_secondary_indexes
181 .as_ref()
182 .map(serde_json::to_string)
183 .transpose()
184 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
185 let pt_json = request
186 .provisioned_throughput
187 .as_ref()
188 .map(serde_json::to_string)
189 .transpose()
190 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
191 let normalized_sse = request.sse_specification.as_ref().map(|spec| {
196 if spec.enabled == Some(true) {
197 crate::types::SseSpecification {
198 enabled: Some(true),
199 sse_type: spec.sse_type.clone().or_else(|| Some("KMS".to_string())),
200 kms_master_key_id: spec.kms_master_key_id.clone().or_else(|| {
201 Some(crate::streams::kms_key_arn(
202 &uuid::Uuid::new_v4().to_string(),
203 ))
204 }),
205 }
206 } else {
207 spec.clone()
208 }
209 });
210 let sse_json = normalized_sse
211 .as_ref()
212 .map(serde_json::to_string)
213 .transpose()
214 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
215 let on_demand_json = request
216 .on_demand_throughput
217 .as_ref()
218 .map(serde_json::to_string)
219 .transpose()
220 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
221 let deletion_protection = request.deletion_protection_enabled.unwrap_or(false);
222
223 if let Some(ref spec) = request.stream_specification {
228 if spec.stream_enabled && !storage.supports_streams() {
229 return Err(BackendError::Unsupported {
230 capability: "streams",
231 }
232 .into());
233 }
234 }
235 if request.tags.as_ref().is_some_and(|tags| !tags.is_empty()) && !storage.supports_tags() {
236 return Err(BackendError::Unsupported { capability: "tags" }.into());
237 }
238
239 let billing_mode_str = request.billing_mode.as_deref().unwrap_or("PROVISIONED");
240 storage
241 .insert_table_metadata(&crate::storage::CreateTableMetadata {
242 table_name: &request.table_name,
243 key_schema: &key_schema_json,
244 attribute_definitions: &attr_defs_json,
245 gsi_definitions: gsi_json.as_deref(),
246 lsi_definitions: lsi_json.as_deref(),
247 provisioned_throughput: pt_json.as_deref(),
248 created_at: now,
249 sse_specification: sse_json.as_deref(),
250 table_class: request.table_class.as_deref(),
251 deletion_protection_enabled: deletion_protection,
252 billing_mode: Some(billing_mode_str),
253 on_demand_throughput: on_demand_json.as_deref(),
254 })
255 .await?;
256
257 storage.create_data_table(&request.table_name).await?;
258
259 if let Some(ref gsis) = request.global_secondary_indexes {
260 for gsi in gsis {
261 storage
262 .create_gsi_table(&request.table_name, &gsi.index_name)
263 .await?;
264 }
265 }
266
267 if let Some(ref lsis) = request.local_secondary_indexes {
268 for lsi in lsis {
269 storage
270 .create_lsi_table(&request.table_name, &lsi.index_name)
271 .await?;
272 }
273 }
274
275 if let Some(ref spec) = request.stream_specification {
276 if spec.stream_enabled {
277 let view_type = spec
278 .stream_view_type
279 .as_deref()
280 .unwrap_or("NEW_AND_OLD_IMAGES");
281 let label = streams::generate_stream_label(storage.clock());
282 storage
283 .enable_stream(&request.table_name, view_type, &label)
284 .await?;
285 }
286 }
287
288 if let Some(ref tags) = request.tags {
289 if !tags.is_empty() {
290 storage.set_tags(&request.table_name, tags).await?;
291 }
292 }
293
294 let meta = storage
295 .get_table_metadata(&request.table_name)
296 .await?
297 .ok_or_else(|| {
298 DynoxideError::InternalServerError("Table metadata not found after creation".into())
299 })?;
300
301 let mut desc = build_table_description(&meta, Some(0), Some(0));
302 desc.table_status = "CREATING".to_string();
305
306 let billing_mode_str = request.billing_mode.as_deref().unwrap_or("PROVISIONED");
308 if billing_mode_str == "PROVISIONED" {
309 desc.billing_mode_summary = None;
310 desc.table_throughput_mode_summary = None;
311 } else if billing_mode_str == "PAY_PER_REQUEST" {
312 desc.billing_mode_summary = Some(crate::actions::BillingModeSummary {
313 billing_mode: "PAY_PER_REQUEST".to_string(),
314 last_update_to_pay_per_request_date_time: None,
315 });
316 desc.table_throughput_mode_summary = Some(crate::actions::TableThroughputModeSummary {
317 table_throughput_mode: "PAY_PER_REQUEST".to_string(),
318 last_update_to_pay_per_request_date_time: None,
319 });
320 desc.provisioned_throughput = Some(crate::actions::TableProvisionedThroughputDescription {
322 read_capacity_units: 0,
323 write_capacity_units: 0,
324 number_of_decreases_today: 0,
325 last_increase_date_time: None,
326 last_decrease_date_time: None,
327 });
328 }
329
330 if let Some(ref mut gsis) = desc.global_secondary_indexes {
332 for gsi in gsis {
333 gsi.index_status = "CREATING".to_string();
334 }
335 }
336
337 if request.deletion_protection_enabled.is_none() {
340 desc.deletion_protection_enabled = None;
341 }
342
343 Ok(CreateTableResponse {
344 table_description: desc,
345 })
346}
347
348fn ve(msg: String) -> DynoxideError {
350 DynoxideError::ValidationException(msg)
351}
352
353fn validate_typed_request(request: &CreateTableRequest) -> Result<()> {
370 if request.table_name.is_empty() {
371 return Err(DynoxideError::ValidationException(
372 "The parameter 'TableName' is required but was not present in the request".to_string(),
373 ));
374 }
375 if request.table_name.len() < 3 || request.table_name.len() > 255 {
376 return Err(DynoxideError::ValidationException(
377 "TableName must be at least 3 characters long and at most 255 characters long"
378 .to_string(),
379 ));
380 }
381
382 if !request
384 .table_name
385 .chars()
386 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
387 {
388 return Err(DynoxideError::ValidationException(format!(
389 "1 validation error detected: Value '{}' at 'tableName' failed to satisfy constraint: \
390 Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
391 request.table_name
392 )));
393 }
394
395 let billing_mode_str = request.billing_mode.as_deref().unwrap_or("PROVISIONED");
397 if billing_mode_str == "PAY_PER_REQUEST" && request.provisioned_throughput.is_some() {
398 return Err(DynoxideError::ValidationException(
399 "One or more parameter values were invalid: Neither ReadCapacityUnits nor \
400 WriteCapacityUnits can be specified when BillingMode is PAY_PER_REQUEST"
401 .to_string(),
402 ));
403 }
404
405 if let Some(ref pt) = request.provisioned_throughput {
407 const MAX_THROUGHPUT: i64 = 1_000_000_000_000;
408 let rcu = pt.read_capacity_units.unwrap_or(0);
409 let wcu = pt.write_capacity_units.unwrap_or(0);
410 if rcu > MAX_THROUGHPUT {
411 return Err(DynoxideError::ValidationException(format!(
412 "Given value {} for ReadCapacityUnits is out of bounds",
413 rcu
414 )));
415 }
416 if wcu > MAX_THROUGHPUT {
417 return Err(DynoxideError::ValidationException(format!(
418 "Given value {} for WriteCapacityUnits is out of bounds",
419 wcu
420 )));
421 }
422 }
423
424 if request.billing_mode.is_some()
429 && billing_mode_str == "PROVISIONED"
430 && request.provisioned_throughput.is_none()
431 {
432 return Err(DynoxideError::ValidationException(
433 "One or more parameter values were invalid: ReadCapacityUnits and \
434 WriteCapacityUnits must both be specified when BillingMode is PROVISIONED"
435 .to_string(),
436 ));
437 }
438
439 validate_key_attrs_in_defs(&request.key_schema, &request.attribute_definitions).map_err(ve)?;
441
442 validate_key_schema_structure(&request.key_schema).map_err(ve)?;
444
445 if let Some(ref lsis) = request.local_secondary_indexes {
447 if lsis.is_empty() {
448 return Err(ve(
449 "One or more parameter values were invalid: List of LocalSecondaryIndexes is empty"
450 .to_string(),
451 ));
452 }
453 }
454 if let Some(ref gsis) = request.global_secondary_indexes {
455 if gsis.is_empty() {
456 return Err(ve(
457 "One or more parameter values were invalid: List of GlobalSecondaryIndexes is empty"
458 .to_string(),
459 ));
460 }
461 }
462
463 if let Some(ref lsis) = request.local_secondary_indexes {
465 validate_lsi_list(lsis, &request.key_schema, &request.attribute_definitions).map_err(ve)?;
466 }
467
468 if let Some(ref gsis) = request.global_secondary_indexes {
470 let bm = request.billing_mode.as_deref().unwrap_or("PROVISIONED");
471 validate_gsi_list(gsis, &request.attribute_definitions, bm).map_err(ve)?;
472 }
473
474 check_cross_index_duplicates(
476 &request.local_secondary_indexes,
477 &request.global_secondary_indexes,
478 )
479 .map_err(ve)?;
480
481 validate_attr_def_count(
483 &request.key_schema,
484 &request.attribute_definitions,
485 &request.local_secondary_indexes,
486 &request.global_secondary_indexes,
487 )
488 .map_err(ve)?;
489
490 if let Some(ref spec) = request.stream_specification {
494 if !spec.stream_enabled && spec.stream_view_type.is_some() {
495 return Err(DynoxideError::ValidationException(
496 "One or more parameter values were invalid: Table is being created with a stream \
497 disabled, UpdateViewType should not be specified"
498 .to_string(),
499 ));
500 }
501 }
502
503 Ok(())
504}
505
506fn check_cross_index_duplicates(
507 lsis: &Option<Vec<LocalSecondaryIndex>>,
508 gsis: &Option<Vec<GlobalSecondaryIndex>>,
509) -> std::result::Result<(), String> {
510 if let (Some(lsis), Some(gsis)) = (lsis, gsis) {
511 let mut all_names = std::collections::HashSet::new();
512 for lsi in lsis {
513 all_names.insert(&lsi.index_name);
514 }
515 for gsi in gsis {
516 if !all_names.insert(&gsi.index_name) {
517 return Err(format!(
518 "One or more parameter values were invalid: Duplicate index name: {}",
519 gsi.index_name
520 ));
521 }
522 }
523 }
524 Ok(())
525}
526
527fn validate_raw_and_build(raw: RawRequest) -> std::result::Result<CreateTableRequest, String> {
530 if raw.table_name.is_none() {
532 return Err(
533 "The parameter 'TableName' is required but was not present in the request".to_string(),
534 );
535 }
536
537 let name_errors = crate::validation::table_name_constraint_errors(
541 raw.table_name.as_deref(),
542 crate::validation::TableNameContext::CreateTable,
543 );
544 if !name_errors.is_empty() {
545 let msg = format!(
546 "{} validation error{} detected: {}",
547 name_errors.len(),
548 if name_errors.len() > 1 { "s" } else { "" },
549 name_errors.join("; ")
550 );
551 return Err(msg);
552 }
553 let table_name = raw.table_name.unwrap();
554
555 let mut errors = Vec::new();
556
557 if let Some(ref bm) = raw.billing_mode {
558 if bm != "PROVISIONED" && bm != "PAY_PER_REQUEST" {
559 errors.push(format!(
560 "Value '{}' at 'billingMode' failed to satisfy constraint: \
561 Member must satisfy enum value set: [PROVISIONED, PAY_PER_REQUEST]",
562 bm
563 ));
564 }
565 }
566
567 collect_pt_errors(&raw.provisioned_throughput, &mut errors);
568 collect_ks_errors(&raw.key_schema, &mut errors);
569 collect_ad_errors(&raw.attribute_definitions, &mut errors);
570 collect_lsi_errors(&raw.local_secondary_indexes, &mut errors);
571 collect_gsi_errors(&raw.global_secondary_indexes, &mut errors);
572
573 errors.truncate(10);
575
576 if !errors.is_empty() {
577 let prefix = format!(
578 "{} validation error{} detected: ",
579 errors.len(),
580 if errors.len() == 1 { "" } else { "s" }
581 );
582 return Err(format!("{}{}", prefix, errors.join("; ")));
583 }
584
585 let billing_mode_str = raw.billing_mode.as_deref().unwrap_or("PROVISIONED");
587 if billing_mode_str == "PAY_PER_REQUEST" && raw.provisioned_throughput.is_some() {
588 return Err(
589 "One or more parameter values were invalid: Neither ReadCapacityUnits nor \
590 WriteCapacityUnits can be specified when BillingMode is PAY_PER_REQUEST"
591 .to_string(),
592 );
593 }
594
595 if let Some(ref pt) = raw.provisioned_throughput {
597 if let Some(obj) = pt.as_object() {
598 let rcu = obj
599 .get("ReadCapacityUnits")
600 .and_then(|v| v.as_i64())
601 .unwrap_or(0);
602 let wcu = obj
603 .get("WriteCapacityUnits")
604 .and_then(|v| v.as_i64())
605 .unwrap_or(0);
606 const MAX_THROUGHPUT: i64 = 1_000_000_000_000;
607 if rcu > MAX_THROUGHPUT {
608 return Err(format!(
609 "Given value {} for ReadCapacityUnits is out of bounds",
610 rcu
611 ));
612 }
613 if wcu > MAX_THROUGHPUT {
614 return Err(format!(
615 "Given value {} for WriteCapacityUnits is out of bounds",
616 wcu
617 ));
618 }
619 }
620 }
621
622 if raw.billing_mode.as_deref() == Some("PROVISIONED") && raw.provisioned_throughput.is_none() {
624 return Err(
625 "One or more parameter values were invalid: ReadCapacityUnits and \
626 WriteCapacityUnits must both be specified when BillingMode is PROVISIONED"
627 .to_string(),
628 );
629 }
630
631 let key_schema: Vec<KeySchemaElement> = raw
633 .key_schema
634 .as_ref()
635 .map(|v| serde_json::from_value(v.clone()))
636 .transpose()
637 .map_err(|e| e.to_string())?
638 .unwrap_or_default();
639 let attribute_definitions: Vec<AttributeDefinition> = raw
640 .attribute_definitions
641 .as_ref()
642 .map(|v| serde_json::from_value(v.clone()))
643 .transpose()
644 .map_err(|e| e.to_string())?
645 .unwrap_or_default();
646 let provisioned_throughput: Option<ProvisionedThroughput> = raw
647 .provisioned_throughput
648 .as_ref()
649 .map(|v| serde_json::from_value(v.clone()))
650 .transpose()
651 .map_err(|e| e.to_string())?;
652 let global_secondary_indexes: Option<Vec<GlobalSecondaryIndex>> = raw
653 .global_secondary_indexes
654 .as_ref()
655 .map(|v| serde_json::from_value(v.clone()))
656 .transpose()
657 .map_err(|e| e.to_string())?;
658 let local_secondary_indexes: Option<Vec<LocalSecondaryIndex>> = raw
659 .local_secondary_indexes
660 .as_ref()
661 .map(|v| serde_json::from_value(v.clone()))
662 .transpose()
663 .map_err(|e| e.to_string())?;
664
665 Ok(CreateTableRequest {
666 table_name,
667 key_schema,
668 attribute_definitions,
669 global_secondary_indexes,
670 local_secondary_indexes,
671 billing_mode: raw.billing_mode,
672 provisioned_throughput,
673 stream_specification: raw.stream_specification,
674 sse_specification: raw.sse_specification,
675 table_class: raw.table_class,
676 tags: raw.tags,
677 deletion_protection_enabled: raw.deletion_protection_enabled,
678 on_demand_throughput: raw.on_demand_throughput,
679 })
680}
681
682fn collect_pt_errors(pt_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
685 if let Some(v) = pt_val {
686 if let Some(obj) = v.as_object() {
687 let wcu = obj.get("WriteCapacityUnits");
688 let rcu = obj.get("ReadCapacityUnits");
689 if wcu.is_none() || wcu == Some(&serde_json::Value::Null) {
690 errors.push("Value null at 'provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must not be null".to_string());
691 } else if let Some(w) = wcu.and_then(|v| v.as_i64()) {
692 if w < 1 {
693 errors.push(format!("Value '{}' at 'provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", w));
694 }
695 }
696 if rcu.is_none() || rcu == Some(&serde_json::Value::Null) {
697 errors.push("Value null at 'provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must not be null".to_string());
698 } else if let Some(r) = rcu.and_then(|v| v.as_i64()) {
699 if r < 1 {
700 errors.push(format!("Value '{}' at 'provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", r));
701 }
702 }
703 }
704 }
705}
706
707fn collect_ks_errors(ks_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
708 match ks_val {
709 None => {
710 errors.push(
711 "Value null at 'keySchema' failed to satisfy constraint: Member must not be null"
712 .to_string(),
713 );
714 }
715 Some(v) => {
716 if let Some(arr) = v.as_array() {
717 if arr.is_empty() {
718 errors.push("Value '[]' at 'keySchema' failed to satisfy constraint: Member must have length greater than or equal to 1".to_string());
719 } else if arr.len() > 2 {
720 let dump = render_key_schema_java_toString(arr);
721 errors.push(format!("Value '{}' at 'keySchema' failed to satisfy constraint: Member must have length less than or equal to 2", dump));
722 }
723 for (i, elem) in arr.iter().enumerate().take(10) {
724 collect_ks_elem_errors(elem, i + 1, errors);
725 }
726 }
727 }
728 }
729}
730
731fn collect_ks_elem_errors(elem: &serde_json::Value, idx: usize, errors: &mut Vec<String>) {
732 if let Some(obj) = elem.as_object() {
733 if !obj.contains_key("AttributeName")
734 || obj.get("AttributeName") == Some(&serde_json::Value::Null)
735 {
736 errors.push(format!("Value null at 'keySchema.{}.member.attributeName' failed to satisfy constraint: Member must not be null", idx));
737 }
738 let kt = obj.get("KeyType");
739 if kt.is_none() || kt == Some(&serde_json::Value::Null) {
740 errors.push(format!("Value null at 'keySchema.{}.member.keyType' failed to satisfy constraint: Member must not be null", idx));
741 } else if let Some(s) = kt.and_then(|v| v.as_str()) {
742 if s != "HASH" && s != "RANGE" {
743 errors.push(format!("Value '{}' at 'keySchema.{}.member.keyType' failed to satisfy constraint: Member must satisfy enum value set: [HASH, RANGE]", s, idx));
744 }
745 }
746 }
747}
748
749#[allow(non_snake_case)]
755fn render_key_schema_java_toString(arr: &[serde_json::Value]) -> String {
756 let parts: Vec<String> = arr
757 .iter()
758 .map(|elem| {
759 let an = elem
760 .get("AttributeName")
761 .and_then(|v| v.as_str())
762 .unwrap_or("");
763 let kt = elem.get("KeyType").and_then(|v| v.as_str()).unwrap_or("");
764 format!("KeySchemaElement(attributeName={an}, keyType={kt})")
765 })
766 .collect();
767 format!("[{}]", parts.join(", "))
768}
769
770fn collect_ad_errors(ad_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
771 match ad_val {
772 None => {
773 errors.push("Value null at 'attributeDefinitions' failed to satisfy constraint: Member must not be null".to_string());
774 }
775 Some(v) => {
776 if let Some(arr) = v.as_array() {
777 for (i, elem) in arr.iter().enumerate() {
778 if let Some(obj) = elem.as_object() {
779 if !obj.contains_key("AttributeName")
780 || obj.get("AttributeName") == Some(&serde_json::Value::Null)
781 {
782 errors.push(format!("Value null at 'attributeDefinitions.{}.member.attributeName' failed to satisfy constraint: Member must not be null", i + 1));
783 }
784 let at = obj.get("AttributeType");
785 if at.is_none() || at == Some(&serde_json::Value::Null) {
786 errors.push(format!("Value null at 'attributeDefinitions.{}.member.attributeType' failed to satisfy constraint: Member must not be null", i + 1));
787 } else if let Some(s) = at.and_then(|v| v.as_str()) {
788 if s != "S" && s != "N" && s != "B" {
789 errors.push(format!("Value '{}' at 'attributeDefinitions.{}.member.attributeType' failed to satisfy constraint: Member must satisfy enum value set: [B, N, S]", s, i + 1));
790 }
791 }
792 }
793 }
794 }
795 }
796 }
797}
798
799fn collect_lsi_errors(lsi_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
800 if let Some(v) = lsi_val {
801 if let Some(arr) = v.as_array() {
802 for (i, elem) in arr.iter().enumerate().take(10) {
803 if let Some(obj) = elem.as_object() {
804 if !obj.contains_key("IndexName")
806 || obj.get("IndexName") == Some(&serde_json::Value::Null)
807 {
808 errors.push(format!("Value null at 'localSecondaryIndexes.{}.member.indexName' failed to satisfy constraint: Member must not be null", i + 1));
809 } else if let Some(name) = obj.get("IndexName").and_then(|v| v.as_str()) {
810 collect_idx_name_errors(name, "localSecondaryIndexes", i + 1, errors);
811 }
812 if !obj.contains_key("KeySchema")
813 || obj.get("KeySchema") == Some(&serde_json::Value::Null)
814 {
815 errors.push(format!("Value null at 'localSecondaryIndexes.{}.member.keySchema' failed to satisfy constraint: Member must not be null", i + 1));
816 } else if let Some(ks) = obj.get("KeySchema").and_then(|v| v.as_array()) {
817 if ks.is_empty() {
818 errors.push(format!("Value '[]' at 'localSecondaryIndexes.{}.member.keySchema' failed to satisfy constraint: Member must have length greater than or equal to 1", i + 1));
819 }
820 }
821 if !obj.contains_key("Projection")
822 || obj.get("Projection") == Some(&serde_json::Value::Null)
823 {
824 errors.push(format!("Value null at 'localSecondaryIndexes.{}.member.projection' failed to satisfy constraint: Member must not be null", i + 1));
825 } else if let Some(p) = obj.get("Projection").and_then(|v| v.as_object()) {
826 collect_proj_errors(p, &format!("localSecondaryIndexes.{}", i + 1), errors);
827 }
828 }
829 }
830 }
831 }
832}
833
834fn collect_gsi_errors(gsi_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
835 if let Some(v) = gsi_val {
836 if let Some(arr) = v.as_array() {
837 for (i, elem) in arr.iter().enumerate().take(10) {
838 if let Some(obj) = elem.as_object() {
839 if !obj.contains_key("KeySchema")
841 || obj.get("KeySchema") == Some(&serde_json::Value::Null)
842 {
843 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.keySchema' failed to satisfy constraint: Member must not be null", i + 1));
844 } else if let Some(ks) = obj.get("KeySchema").and_then(|v| v.as_array()) {
845 if ks.is_empty() {
846 errors.push(format!("Value '[]' at 'globalSecondaryIndexes.{}.member.keySchema' failed to satisfy constraint: Member must have length greater than or equal to 1", i + 1));
847 }
848 }
849 if !obj.contains_key("Projection")
850 || obj.get("Projection") == Some(&serde_json::Value::Null)
851 {
852 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.projection' failed to satisfy constraint: Member must not be null", i + 1));
853 } else if let Some(p) = obj.get("Projection").and_then(|v| v.as_object()) {
854 collect_proj_errors(
855 p,
856 &format!("globalSecondaryIndexes.{}", i + 1),
857 errors,
858 );
859 }
860 if !obj.contains_key("IndexName")
861 || obj.get("IndexName") == Some(&serde_json::Value::Null)
862 {
863 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.indexName' failed to satisfy constraint: Member must not be null", i + 1));
864 } else if let Some(name) = obj.get("IndexName").and_then(|v| v.as_str()) {
865 collect_idx_name_errors(name, "globalSecondaryIndexes", i + 1, errors);
866 }
867 if let Some(pt) = obj.get("ProvisionedThroughput").and_then(|v| v.as_object()) {
869 let wcu = pt.get("WriteCapacityUnits");
870 let rcu = pt.get("ReadCapacityUnits");
871 if let Some(w) = wcu.and_then(|v| v.as_i64()) {
872 if w < 1 {
873 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));
874 }
875 } else if wcu.is_none() || wcu == Some(&serde_json::Value::Null) {
876 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must not be null", i + 1));
877 }
878 if let Some(r) = rcu.and_then(|v| v.as_i64()) {
879 if r < 1 {
880 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));
881 }
882 } else if rcu.is_none() || rcu == Some(&serde_json::Value::Null) {
883 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must not be null", i + 1));
884 }
885 }
886 }
887 }
888 }
889 }
890}
891
892fn collect_idx_name_errors(name: &str, prefix: &str, idx: usize, errors: &mut Vec<String>) {
893 if !name
894 .chars()
895 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
896 {
897 errors.push(format!("Value '{}' at '{}.{}.member.indexName' failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+", name, prefix, idx));
898 }
899 if name.len() < 3 {
900 errors.push(format!("Value '{}' at '{}.{}.member.indexName' failed to satisfy constraint: Member must have length greater than or equal to 3", name, prefix, idx));
901 }
902 if name.len() > 255 {
903 errors.push(format!("Value '{}' at '{}.{}.member.indexName' failed to satisfy constraint: Member must have length less than or equal to 255", name, prefix, idx));
904 }
905}
906
907fn collect_proj_errors(
908 proj: &serde_json::Map<String, serde_json::Value>,
909 prefix: &str,
910 errors: &mut Vec<String>,
911) {
912 if let Some(pt) = proj.get("ProjectionType") {
913 if let Some(s) = pt.as_str() {
914 if s != "ALL" && s != "KEYS_ONLY" && s != "INCLUDE" {
915 errors.push(format!("Value '{}' at '{}.member.projection.projectionType' failed to satisfy constraint: Member must satisfy enum value set: [ALL, INCLUDE, KEYS_ONLY]", s, prefix));
916 }
917 }
918 }
919 if let Some(nka) = proj.get("NonKeyAttributes") {
920 if let Some(arr) = nka.as_array() {
921 if arr.is_empty() {
922 errors.push(format!("Value '[]' at '{}.member.projection.nonKeyAttributes' failed to satisfy constraint: Member must have length greater than or equal to 1", prefix));
923 }
924 }
925 }
926}
927
928fn validate_key_schema_structure(ks: &[KeySchemaElement]) -> std::result::Result<(), String> {
931 if ks.is_empty() {
932 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());
933 }
934 if ks[0].key_type != KeyType::HASH {
935 return Err(
936 "Invalid KeySchema: The first KeySchemaElement is not a HASH key type".to_string(),
937 );
938 }
939 if ks.len() == 2 && ks[0].attribute_name == ks[1].attribute_name {
940 return Err(
941 "Both the Hash Key and the Range Key element in the KeySchema have the same name"
942 .to_string(),
943 );
944 }
945 if ks.len() == 2 && ks[1].key_type != KeyType::RANGE {
946 return Err(
947 "Invalid KeySchema: The second KeySchemaElement is not a RANGE key type".to_string(),
948 );
949 }
950 Ok(())
951}
952
953fn validate_key_attrs_in_defs(
954 ks: &[KeySchemaElement],
955 defs: &[AttributeDefinition],
956) -> std::result::Result<(), String> {
957 let missing: Vec<&str> = ks
959 .iter()
960 .filter(|k| !defs.iter().any(|d| d.attribute_name == k.attribute_name))
961 .map(|k| k.attribute_name.as_str())
962 .collect();
963
964 if missing.is_empty() {
965 let has_dup_names = ks.len() == 2 && ks[0].attribute_name == ks[1].attribute_name;
968 if has_dup_names {
969 return Err(
970 "Invalid KeySchema: Some index key attribute have no definition".to_string(),
971 );
972 }
973 return Ok(());
974 }
975
976 let has_dup_names = ks.len() == 2 && ks[0].attribute_name == ks[1].attribute_name;
982 let has_dup_types = ks.len() == 2 && ks[0].key_type == ks[1].key_type;
983 let use_generic = defs.is_empty() || ks.len() >= 2 || has_dup_names || has_dup_types;
984
985 if use_generic {
986 return Err("Invalid KeySchema: Some index key attribute have no definition".to_string());
987 }
988
989 let key_names: Vec<&str> = missing.to_vec();
991 let def_names: Vec<&str> = defs.iter().map(|d| d.attribute_name.as_str()).collect();
992 Err(format!(
993 "One or more parameter values were invalid: Some index key attributes are not defined in \
994 AttributeDefinitions. Keys: [{}], AttributeDefinitions: [{}]",
995 key_names.join(", "),
996 def_names.join(", ")
997 ))
998}
999
1000fn validate_attr_def_count(
1001 ks: &[KeySchemaElement],
1002 defs: &[AttributeDefinition],
1003 lsis: &Option<Vec<LocalSecondaryIndex>>,
1004 gsis: &Option<Vec<GlobalSecondaryIndex>>,
1005) -> std::result::Result<(), String> {
1006 let mut all_key_attrs = std::collections::HashSet::new();
1007 for k in ks {
1008 all_key_attrs.insert(k.attribute_name.as_str());
1009 }
1010 if let Some(lsis) = lsis {
1011 for lsi in lsis {
1012 for k in &lsi.key_schema {
1013 all_key_attrs.insert(k.attribute_name.as_str());
1014 }
1015 }
1016 }
1017 if let Some(gsis) = gsis {
1018 for gsi in gsis {
1019 for k in &gsi.key_schema {
1020 all_key_attrs.insert(k.attribute_name.as_str());
1021 }
1022 }
1023 }
1024 if defs.len() != all_key_attrs.len() {
1025 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());
1026 }
1027 Ok(())
1028}
1029
1030fn validate_lsi_list(
1031 lsis: &[LocalSecondaryIndex],
1032 ks: &[KeySchemaElement],
1033 defs: &[AttributeDefinition],
1034) -> std::result::Result<(), String> {
1035 if !ks.iter().any(|k| k.key_type == KeyType::RANGE) {
1038 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());
1039 }
1040
1041 let def_names: Vec<&str> = defs.iter().map(|d| d.attribute_name.as_str()).collect();
1043 let mut missing_keys = Vec::new();
1044 for lsi in lsis {
1045 for k in &lsi.key_schema {
1046 if !def_names.contains(&k.attribute_name.as_str())
1047 && !missing_keys.contains(&k.attribute_name.as_str())
1048 {
1049 missing_keys.push(k.attribute_name.as_str());
1050 }
1051 }
1052 }
1053 if !missing_keys.is_empty() {
1054 let mut all_keys = Vec::new();
1055 for lsi in lsis {
1056 for k in &lsi.key_schema {
1057 if !all_keys.contains(&k.attribute_name.as_str()) {
1058 all_keys.push(k.attribute_name.as_str());
1059 }
1060 }
1061 }
1062 return Err(format!(
1063 "One or more parameter values were invalid: Some index key attributes are not defined in AttributeDefinitions. Keys: [{}], AttributeDefinitions: [{}]",
1064 all_keys.join(", "),
1065 def_names.join(", ")
1066 ));
1067 }
1068
1069 for lsi in lsis {
1071 validate_lsi_structure(lsi, ks)?;
1072 }
1073
1074 let mut seen = std::collections::HashSet::new();
1076 for lsi in lsis {
1077 if !seen.insert(&lsi.index_name) {
1078 return Err(format!(
1079 "One or more parameter values were invalid: Duplicate index name: {}",
1080 lsi.index_name
1081 ));
1082 }
1083 }
1084
1085 if lsis.len() > 5 {
1087 return Err("One or more parameter values were invalid: Number of LocalSecondaryIndexes exceeds per-table limit of 5".to_string());
1088 }
1089
1090 Ok(())
1091}
1092
1093fn validate_gsi_list(
1094 gsis: &[GlobalSecondaryIndex],
1095 defs: &[AttributeDefinition],
1096 bm: &str,
1097) -> std::result::Result<(), String> {
1098 let def_names: Vec<&str> = defs.iter().map(|d| d.attribute_name.as_str()).collect();
1102 let mut missing_keys = Vec::new();
1103 for gsi in gsis {
1104 for k in &gsi.key_schema {
1105 if !def_names.contains(&k.attribute_name.as_str())
1106 && !missing_keys.contains(&k.attribute_name.as_str())
1107 {
1108 missing_keys.push(k.attribute_name.as_str());
1109 }
1110 }
1111 }
1112 if !missing_keys.is_empty() {
1113 let mut all_keys = Vec::new();
1114 for gsi in gsis {
1115 for k in &gsi.key_schema {
1116 if !all_keys.contains(&k.attribute_name.as_str()) {
1117 all_keys.push(k.attribute_name.as_str());
1118 }
1119 }
1120 }
1121 return Err(format!(
1122 "One or more parameter values were invalid: Some index key attributes are not defined in AttributeDefinitions. Keys: [{}], AttributeDefinitions: [{}]",
1123 all_keys.join(", "),
1124 def_names.join(", ")
1125 ));
1126 }
1127
1128 for gsi in gsis {
1130 validate_gsi_structure(gsi)?;
1131 }
1132
1133 let mut seen = std::collections::HashSet::new();
1135 for gsi in gsis {
1136 if !seen.insert(&gsi.index_name) {
1137 return Err(format!(
1138 "One or more parameter values were invalid: Duplicate index name: {}",
1139 gsi.index_name
1140 ));
1141 }
1142 }
1143
1144 if gsis.len() > 20 {
1146 return Err("One or more parameter values were invalid: GlobalSecondaryIndex count exceeds the per-table limit of 20".to_string());
1147 }
1148
1149 if bm == "PAY_PER_REQUEST" {
1151 for gsi in gsis {
1152 if gsi.provisioned_throughput.is_some() {
1153 return Err(format!(
1154 "One or more parameter values were invalid: ProvisionedThroughput should not be specified for index: {} when BillingMode is PAY_PER_REQUEST",
1155 gsi.index_name
1156 ));
1157 }
1158 }
1159 }
1160
1161 Ok(())
1162}
1163
1164fn validate_lsi_structure(
1165 lsi: &LocalSecondaryIndex,
1166 table_ks: &[KeySchemaElement],
1167) -> std::result::Result<(), String> {
1168 validate_key_schema_structure(&lsi.key_schema)?;
1170
1171 let lsi_sk = lsi.key_schema.iter().find(|k| k.key_type == KeyType::RANGE);
1173 if lsi_sk.is_none() {
1174 return Err(format!(
1175 "One or more parameter values were invalid: Index KeySchema does not have a range key for index: {}",
1176 lsi.index_name
1177 ));
1178 }
1179
1180 let table_pk = table_ks
1182 .iter()
1183 .find(|k| k.key_type == KeyType::HASH)
1184 .map(|k| k.attribute_name.as_str());
1185 let lsi_pk = lsi
1186 .key_schema
1187 .iter()
1188 .find(|k| k.key_type == KeyType::HASH)
1189 .map(|k| k.attribute_name.as_str());
1190 if lsi_pk != table_pk {
1191 return Err(format!(
1192 "One or more parameter values were invalid: \
1193 Index KeySchema does not have the same leading hash key as table KeySchema \
1194 for index: {}. index hash key: {}, table hash key: {}",
1195 lsi.index_name,
1196 lsi_pk.unwrap_or("null"),
1197 table_pk.unwrap_or("null")
1198 ));
1199 }
1200
1201 validate_proj_structure(&lsi.projection)?;
1203
1204 Ok(())
1205}
1206
1207fn validate_gsi_structure(gsi: &GlobalSecondaryIndex) -> std::result::Result<(), String> {
1208 validate_key_schema_structure(&gsi.key_schema)?;
1209 validate_proj_structure(&gsi.projection)?;
1210 Ok(())
1211}
1212
1213fn validate_proj_structure(p: &Projection) -> std::result::Result<(), String> {
1214 match &p.projection_type {
1219 None => Err(
1220 "One or more parameter values were invalid: Unknown ProjectionType: null".to_string(),
1221 ),
1222 Some(ProjectionType::ALL) => match &p.non_key_attributes {
1223 Some(_) => Err("One or more parameter values were invalid: ProjectionType is ALL, but NonKeyAttributes is specified".to_string()),
1224 None => Ok(()),
1225 },
1226 Some(ProjectionType::KEYS_ONLY) => match &p.non_key_attributes {
1227 Some(_) => Err("One or more parameter values were invalid: ProjectionType is KEYS_ONLY, but NonKeyAttributes is specified".to_string()),
1228 None => Ok(()),
1229 },
1230 Some(ProjectionType::INCLUDE) => match &p.non_key_attributes {
1231 None => Err("One or more parameter values were invalid: ProjectionType is INCLUDE, but NonKeyAttributes is not specified".to_string()),
1232 Some(nka) if nka.is_empty() => Err("One or more parameter values were invalid: NonKeyAttributes must not be empty".to_string()),
1233 Some(_) => Ok(()),
1234 },
1235 }
1236}