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 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 let billing_mode_str = request.billing_mode.as_deref().unwrap_or("PROVISIONED");
224 storage
225 .insert_table_metadata(&crate::storage::CreateTableMetadata {
226 table_name: &request.table_name,
227 key_schema: &key_schema_json,
228 attribute_definitions: &attr_defs_json,
229 gsi_definitions: gsi_json.as_deref(),
230 lsi_definitions: lsi_json.as_deref(),
231 provisioned_throughput: pt_json.as_deref(),
232 created_at: now,
233 sse_specification: sse_json.as_deref(),
234 table_class: request.table_class.as_deref(),
235 deletion_protection_enabled: deletion_protection,
236 billing_mode: Some(billing_mode_str),
237 on_demand_throughput: on_demand_json.as_deref(),
238 })
239 .await?;
240
241 storage.create_data_table(&request.table_name).await?;
242
243 if let Some(ref gsis) = request.global_secondary_indexes {
244 for gsi in gsis {
245 storage
246 .create_gsi_table(&request.table_name, &gsi.index_name)
247 .await?;
248 }
249 }
250
251 if let Some(ref lsis) = request.local_secondary_indexes {
252 for lsi in lsis {
253 storage
254 .create_lsi_table(&request.table_name, &lsi.index_name)
255 .await?;
256 }
257 }
258
259 if let Some(ref spec) = request.stream_specification {
260 if spec.stream_enabled {
261 let view_type = spec
262 .stream_view_type
263 .as_deref()
264 .unwrap_or("NEW_AND_OLD_IMAGES");
265 let label = streams::generate_stream_label(storage.clock());
266 storage
267 .enable_stream(&request.table_name, view_type, &label)
268 .await?;
269 }
270 }
271
272 if let Some(ref tags) = request.tags {
273 if !tags.is_empty() {
274 storage.set_tags(&request.table_name, tags).await?;
275 }
276 }
277
278 let meta = storage
279 .get_table_metadata(&request.table_name)
280 .await?
281 .ok_or_else(|| {
282 DynoxideError::InternalServerError("Table metadata not found after creation".into())
283 })?;
284
285 let mut desc = build_table_description(&meta, Some(0), Some(0));
286 desc.table_status = "CREATING".to_string();
289
290 let billing_mode_str = request.billing_mode.as_deref().unwrap_or("PROVISIONED");
292 if billing_mode_str == "PROVISIONED" {
293 desc.billing_mode_summary = None;
294 desc.table_throughput_mode_summary = None;
295 } else if billing_mode_str == "PAY_PER_REQUEST" {
296 desc.billing_mode_summary = Some(crate::actions::BillingModeSummary {
297 billing_mode: "PAY_PER_REQUEST".to_string(),
298 last_update_to_pay_per_request_date_time: None,
299 });
300 desc.table_throughput_mode_summary = Some(crate::actions::TableThroughputModeSummary {
301 table_throughput_mode: "PAY_PER_REQUEST".to_string(),
302 last_update_to_pay_per_request_date_time: None,
303 });
304 desc.provisioned_throughput = Some(crate::actions::TableProvisionedThroughputDescription {
306 read_capacity_units: 0,
307 write_capacity_units: 0,
308 number_of_decreases_today: 0,
309 last_increase_date_time: None,
310 last_decrease_date_time: None,
311 });
312 }
313
314 if let Some(ref mut gsis) = desc.global_secondary_indexes {
316 for gsi in gsis {
317 gsi.index_status = "CREATING".to_string();
318 }
319 }
320
321 if request.deletion_protection_enabled.is_none() {
324 desc.deletion_protection_enabled = None;
325 }
326
327 Ok(CreateTableResponse {
328 table_description: desc,
329 })
330}
331
332fn ve(msg: String) -> DynoxideError {
334 DynoxideError::ValidationException(msg)
335}
336
337fn validate_typed_request(request: &CreateTableRequest) -> Result<()> {
354 if request.table_name.is_empty() {
355 return Err(DynoxideError::ValidationException(
356 "The parameter 'TableName' is required but was not present in the request".to_string(),
357 ));
358 }
359 if request.table_name.len() < 3 || request.table_name.len() > 255 {
360 return Err(DynoxideError::ValidationException(
361 "TableName must be at least 3 characters long and at most 255 characters long"
362 .to_string(),
363 ));
364 }
365
366 if !request
368 .table_name
369 .chars()
370 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
371 {
372 return Err(DynoxideError::ValidationException(format!(
373 "1 validation error detected: Value '{}' at 'tableName' failed to satisfy constraint: \
374 Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
375 request.table_name
376 )));
377 }
378
379 let billing_mode_str = request.billing_mode.as_deref().unwrap_or("PROVISIONED");
381 if billing_mode_str == "PAY_PER_REQUEST" && request.provisioned_throughput.is_some() {
382 return Err(DynoxideError::ValidationException(
383 "One or more parameter values were invalid: Neither ReadCapacityUnits nor \
384 WriteCapacityUnits can be specified when BillingMode is PAY_PER_REQUEST"
385 .to_string(),
386 ));
387 }
388
389 if let Some(ref pt) = request.provisioned_throughput {
391 const MAX_THROUGHPUT: i64 = 1_000_000_000_000;
392 let rcu = pt.read_capacity_units.unwrap_or(0);
393 let wcu = pt.write_capacity_units.unwrap_or(0);
394 if rcu > MAX_THROUGHPUT {
395 return Err(DynoxideError::ValidationException(format!(
396 "Given value {} for ReadCapacityUnits is out of bounds",
397 rcu
398 )));
399 }
400 if wcu > MAX_THROUGHPUT {
401 return Err(DynoxideError::ValidationException(format!(
402 "Given value {} for WriteCapacityUnits is out of bounds",
403 wcu
404 )));
405 }
406 }
407
408 if request.billing_mode.is_some()
413 && billing_mode_str == "PROVISIONED"
414 && request.provisioned_throughput.is_none()
415 {
416 return Err(DynoxideError::ValidationException(
417 "One or more parameter values were invalid: ReadCapacityUnits and \
418 WriteCapacityUnits must both be specified when BillingMode is PROVISIONED"
419 .to_string(),
420 ));
421 }
422
423 validate_key_attrs_in_defs(&request.key_schema, &request.attribute_definitions).map_err(ve)?;
425
426 validate_key_schema_structure(&request.key_schema).map_err(ve)?;
428
429 if let Some(ref lsis) = request.local_secondary_indexes {
431 if lsis.is_empty() {
432 return Err(ve(
433 "One or more parameter values were invalid: List of LocalSecondaryIndexes is empty"
434 .to_string(),
435 ));
436 }
437 }
438 if let Some(ref gsis) = request.global_secondary_indexes {
439 if gsis.is_empty() {
440 return Err(ve(
441 "One or more parameter values were invalid: List of GlobalSecondaryIndexes is empty"
442 .to_string(),
443 ));
444 }
445 }
446
447 if let Some(ref lsis) = request.local_secondary_indexes {
449 validate_lsi_list(lsis, &request.key_schema, &request.attribute_definitions).map_err(ve)?;
450 }
451
452 if let Some(ref gsis) = request.global_secondary_indexes {
454 let bm = request.billing_mode.as_deref().unwrap_or("PROVISIONED");
455 validate_gsi_list(gsis, &request.attribute_definitions, bm).map_err(ve)?;
456 }
457
458 check_cross_index_duplicates(
460 &request.local_secondary_indexes,
461 &request.global_secondary_indexes,
462 )
463 .map_err(ve)?;
464
465 validate_attr_def_count(
467 &request.key_schema,
468 &request.attribute_definitions,
469 &request.local_secondary_indexes,
470 &request.global_secondary_indexes,
471 )
472 .map_err(ve)?;
473
474 if let Some(ref spec) = request.stream_specification {
478 if !spec.stream_enabled && spec.stream_view_type.is_some() {
479 return Err(DynoxideError::ValidationException(
480 "One or more parameter values were invalid: Table is being created with a stream \
481 disabled, UpdateViewType should not be specified"
482 .to_string(),
483 ));
484 }
485 }
486
487 Ok(())
488}
489
490fn check_cross_index_duplicates(
491 lsis: &Option<Vec<LocalSecondaryIndex>>,
492 gsis: &Option<Vec<GlobalSecondaryIndex>>,
493) -> std::result::Result<(), String> {
494 if let (Some(lsis), Some(gsis)) = (lsis, gsis) {
495 let mut all_names = std::collections::HashSet::new();
496 for lsi in lsis {
497 all_names.insert(&lsi.index_name);
498 }
499 for gsi in gsis {
500 if !all_names.insert(&gsi.index_name) {
501 return Err(format!(
502 "One or more parameter values were invalid: Duplicate index name: {}",
503 gsi.index_name
504 ));
505 }
506 }
507 }
508 Ok(())
509}
510
511fn validate_raw_and_build(raw: RawRequest) -> std::result::Result<CreateTableRequest, String> {
514 if raw.table_name.is_none() {
516 return Err(
517 "The parameter 'TableName' is required but was not present in the request".to_string(),
518 );
519 }
520
521 let name_errors = crate::validation::table_name_constraint_errors(
525 raw.table_name.as_deref(),
526 crate::validation::TableNameContext::CreateTable,
527 );
528 if !name_errors.is_empty() {
529 let msg = format!(
530 "{} validation error{} detected: {}",
531 name_errors.len(),
532 if name_errors.len() > 1 { "s" } else { "" },
533 name_errors.join("; ")
534 );
535 return Err(msg);
536 }
537 let table_name = raw.table_name.unwrap();
538
539 let mut errors = Vec::new();
540
541 if let Some(ref bm) = raw.billing_mode {
542 if bm != "PROVISIONED" && bm != "PAY_PER_REQUEST" {
543 errors.push(format!(
544 "Value '{}' at 'billingMode' failed to satisfy constraint: \
545 Member must satisfy enum value set: [PROVISIONED, PAY_PER_REQUEST]",
546 bm
547 ));
548 }
549 }
550
551 collect_pt_errors(&raw.provisioned_throughput, &mut errors);
552 collect_ks_errors(&raw.key_schema, &mut errors);
553 collect_ad_errors(&raw.attribute_definitions, &mut errors);
554 collect_lsi_errors(&raw.local_secondary_indexes, &mut errors);
555 collect_gsi_errors(&raw.global_secondary_indexes, &mut errors);
556
557 errors.truncate(10);
559
560 if !errors.is_empty() {
561 let prefix = format!(
562 "{} validation error{} detected: ",
563 errors.len(),
564 if errors.len() == 1 { "" } else { "s" }
565 );
566 return Err(format!("{}{}", prefix, errors.join("; ")));
567 }
568
569 let billing_mode_str = raw.billing_mode.as_deref().unwrap_or("PROVISIONED");
571 if billing_mode_str == "PAY_PER_REQUEST" && raw.provisioned_throughput.is_some() {
572 return Err(
573 "One or more parameter values were invalid: Neither ReadCapacityUnits nor \
574 WriteCapacityUnits can be specified when BillingMode is PAY_PER_REQUEST"
575 .to_string(),
576 );
577 }
578
579 if let Some(ref pt) = raw.provisioned_throughput {
581 if let Some(obj) = pt.as_object() {
582 let rcu = obj
583 .get("ReadCapacityUnits")
584 .and_then(|v| v.as_i64())
585 .unwrap_or(0);
586 let wcu = obj
587 .get("WriteCapacityUnits")
588 .and_then(|v| v.as_i64())
589 .unwrap_or(0);
590 const MAX_THROUGHPUT: i64 = 1_000_000_000_000;
591 if rcu > MAX_THROUGHPUT {
592 return Err(format!(
593 "Given value {} for ReadCapacityUnits is out of bounds",
594 rcu
595 ));
596 }
597 if wcu > MAX_THROUGHPUT {
598 return Err(format!(
599 "Given value {} for WriteCapacityUnits is out of bounds",
600 wcu
601 ));
602 }
603 }
604 }
605
606 if raw.billing_mode.as_deref() == Some("PROVISIONED") && raw.provisioned_throughput.is_none() {
608 return Err(
609 "One or more parameter values were invalid: ReadCapacityUnits and \
610 WriteCapacityUnits must both be specified when BillingMode is PROVISIONED"
611 .to_string(),
612 );
613 }
614
615 let key_schema: Vec<KeySchemaElement> = raw
617 .key_schema
618 .as_ref()
619 .map(|v| serde_json::from_value(v.clone()))
620 .transpose()
621 .map_err(|e| e.to_string())?
622 .unwrap_or_default();
623 let attribute_definitions: Vec<AttributeDefinition> = raw
624 .attribute_definitions
625 .as_ref()
626 .map(|v| serde_json::from_value(v.clone()))
627 .transpose()
628 .map_err(|e| e.to_string())?
629 .unwrap_or_default();
630 let provisioned_throughput: Option<ProvisionedThroughput> = raw
631 .provisioned_throughput
632 .as_ref()
633 .map(|v| serde_json::from_value(v.clone()))
634 .transpose()
635 .map_err(|e| e.to_string())?;
636 let global_secondary_indexes: Option<Vec<GlobalSecondaryIndex>> = raw
637 .global_secondary_indexes
638 .as_ref()
639 .map(|v| serde_json::from_value(v.clone()))
640 .transpose()
641 .map_err(|e| e.to_string())?;
642 let local_secondary_indexes: Option<Vec<LocalSecondaryIndex>> = raw
643 .local_secondary_indexes
644 .as_ref()
645 .map(|v| serde_json::from_value(v.clone()))
646 .transpose()
647 .map_err(|e| e.to_string())?;
648
649 Ok(CreateTableRequest {
650 table_name,
651 key_schema,
652 attribute_definitions,
653 global_secondary_indexes,
654 local_secondary_indexes,
655 billing_mode: raw.billing_mode,
656 provisioned_throughput,
657 stream_specification: raw.stream_specification,
658 sse_specification: raw.sse_specification,
659 table_class: raw.table_class,
660 tags: raw.tags,
661 deletion_protection_enabled: raw.deletion_protection_enabled,
662 on_demand_throughput: raw.on_demand_throughput,
663 })
664}
665
666fn collect_pt_errors(pt_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
669 if let Some(v) = pt_val {
670 if let Some(obj) = v.as_object() {
671 let wcu = obj.get("WriteCapacityUnits");
672 let rcu = obj.get("ReadCapacityUnits");
673 if wcu.is_none() || wcu == Some(&serde_json::Value::Null) {
674 errors.push("Value null at 'provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must not be null".to_string());
675 } else if let Some(w) = wcu.and_then(|v| v.as_i64()) {
676 if w < 1 {
677 errors.push(format!("Value '{}' at 'provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", w));
678 }
679 }
680 if rcu.is_none() || rcu == Some(&serde_json::Value::Null) {
681 errors.push("Value null at 'provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must not be null".to_string());
682 } else if let Some(r) = rcu.and_then(|v| v.as_i64()) {
683 if r < 1 {
684 errors.push(format!("Value '{}' at 'provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", r));
685 }
686 }
687 }
688 }
689}
690
691fn collect_ks_errors(ks_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
692 match ks_val {
693 None => {
694 errors.push(
695 "Value null at 'keySchema' failed to satisfy constraint: Member must not be null"
696 .to_string(),
697 );
698 }
699 Some(v) => {
700 if let Some(arr) = v.as_array() {
701 if arr.is_empty() {
702 errors.push("Value '[]' at 'keySchema' failed to satisfy constraint: Member must have length greater than or equal to 1".to_string());
703 } else if arr.len() > 2 {
704 let dump = render_key_schema_java_toString(arr);
705 errors.push(format!("Value '{}' at 'keySchema' failed to satisfy constraint: Member must have length less than or equal to 2", dump));
706 }
707 for (i, elem) in arr.iter().enumerate().take(10) {
708 collect_ks_elem_errors(elem, i + 1, errors);
709 }
710 }
711 }
712 }
713}
714
715fn collect_ks_elem_errors(elem: &serde_json::Value, idx: usize, errors: &mut Vec<String>) {
716 if let Some(obj) = elem.as_object() {
717 if !obj.contains_key("AttributeName")
718 || obj.get("AttributeName") == Some(&serde_json::Value::Null)
719 {
720 errors.push(format!("Value null at 'keySchema.{}.member.attributeName' failed to satisfy constraint: Member must not be null", idx));
721 }
722 let kt = obj.get("KeyType");
723 if kt.is_none() || kt == Some(&serde_json::Value::Null) {
724 errors.push(format!("Value null at 'keySchema.{}.member.keyType' failed to satisfy constraint: Member must not be null", idx));
725 } else if let Some(s) = kt.and_then(|v| v.as_str()) {
726 if s != "HASH" && s != "RANGE" {
727 errors.push(format!("Value '{}' at 'keySchema.{}.member.keyType' failed to satisfy constraint: Member must satisfy enum value set: [HASH, RANGE]", s, idx));
728 }
729 }
730 }
731}
732
733#[allow(non_snake_case)]
739fn render_key_schema_java_toString(arr: &[serde_json::Value]) -> String {
740 let parts: Vec<String> = arr
741 .iter()
742 .map(|elem| {
743 let an = elem
744 .get("AttributeName")
745 .and_then(|v| v.as_str())
746 .unwrap_or("");
747 let kt = elem.get("KeyType").and_then(|v| v.as_str()).unwrap_or("");
748 format!("KeySchemaElement(attributeName={an}, keyType={kt})")
749 })
750 .collect();
751 format!("[{}]", parts.join(", "))
752}
753
754fn collect_ad_errors(ad_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
755 match ad_val {
756 None => {
757 errors.push("Value null at 'attributeDefinitions' failed to satisfy constraint: Member must not be null".to_string());
758 }
759 Some(v) => {
760 if let Some(arr) = v.as_array() {
761 for (i, elem) in arr.iter().enumerate() {
762 if let Some(obj) = elem.as_object() {
763 if !obj.contains_key("AttributeName")
764 || obj.get("AttributeName") == Some(&serde_json::Value::Null)
765 {
766 errors.push(format!("Value null at 'attributeDefinitions.{}.member.attributeName' failed to satisfy constraint: Member must not be null", i + 1));
767 }
768 let at = obj.get("AttributeType");
769 if at.is_none() || at == Some(&serde_json::Value::Null) {
770 errors.push(format!("Value null at 'attributeDefinitions.{}.member.attributeType' failed to satisfy constraint: Member must not be null", i + 1));
771 } else if let Some(s) = at.and_then(|v| v.as_str()) {
772 if s != "S" && s != "N" && s != "B" {
773 errors.push(format!("Value '{}' at 'attributeDefinitions.{}.member.attributeType' failed to satisfy constraint: Member must satisfy enum value set: [B, N, S]", s, i + 1));
774 }
775 }
776 }
777 }
778 }
779 }
780 }
781}
782
783fn collect_lsi_errors(lsi_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
784 if let Some(v) = lsi_val {
785 if let Some(arr) = v.as_array() {
786 for (i, elem) in arr.iter().enumerate().take(10) {
787 if let Some(obj) = elem.as_object() {
788 if !obj.contains_key("IndexName")
790 || obj.get("IndexName") == Some(&serde_json::Value::Null)
791 {
792 errors.push(format!("Value null at 'localSecondaryIndexes.{}.member.indexName' failed to satisfy constraint: Member must not be null", i + 1));
793 } else if let Some(name) = obj.get("IndexName").and_then(|v| v.as_str()) {
794 collect_idx_name_errors(name, "localSecondaryIndexes", i + 1, errors);
795 }
796 if !obj.contains_key("KeySchema")
797 || obj.get("KeySchema") == Some(&serde_json::Value::Null)
798 {
799 errors.push(format!("Value null at 'localSecondaryIndexes.{}.member.keySchema' failed to satisfy constraint: Member must not be null", i + 1));
800 } else if let Some(ks) = obj.get("KeySchema").and_then(|v| v.as_array()) {
801 if ks.is_empty() {
802 errors.push(format!("Value '[]' at 'localSecondaryIndexes.{}.member.keySchema' failed to satisfy constraint: Member must have length greater than or equal to 1", i + 1));
803 }
804 }
805 if !obj.contains_key("Projection")
806 || obj.get("Projection") == Some(&serde_json::Value::Null)
807 {
808 errors.push(format!("Value null at 'localSecondaryIndexes.{}.member.projection' failed to satisfy constraint: Member must not be null", i + 1));
809 } else if let Some(p) = obj.get("Projection").and_then(|v| v.as_object()) {
810 collect_proj_errors(p, &format!("localSecondaryIndexes.{}", i + 1), errors);
811 }
812 }
813 }
814 }
815 }
816}
817
818fn collect_gsi_errors(gsi_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
819 if let Some(v) = gsi_val {
820 if let Some(arr) = v.as_array() {
821 for (i, elem) in arr.iter().enumerate().take(10) {
822 if let Some(obj) = elem.as_object() {
823 if !obj.contains_key("KeySchema")
825 || obj.get("KeySchema") == Some(&serde_json::Value::Null)
826 {
827 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.keySchema' failed to satisfy constraint: Member must not be null", i + 1));
828 } else if let Some(ks) = obj.get("KeySchema").and_then(|v| v.as_array()) {
829 if ks.is_empty() {
830 errors.push(format!("Value '[]' at 'globalSecondaryIndexes.{}.member.keySchema' failed to satisfy constraint: Member must have length greater than or equal to 1", i + 1));
831 }
832 }
833 if !obj.contains_key("Projection")
834 || obj.get("Projection") == Some(&serde_json::Value::Null)
835 {
836 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.projection' failed to satisfy constraint: Member must not be null", i + 1));
837 } else if let Some(p) = obj.get("Projection").and_then(|v| v.as_object()) {
838 collect_proj_errors(
839 p,
840 &format!("globalSecondaryIndexes.{}", i + 1),
841 errors,
842 );
843 }
844 if !obj.contains_key("IndexName")
845 || obj.get("IndexName") == Some(&serde_json::Value::Null)
846 {
847 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.indexName' failed to satisfy constraint: Member must not be null", i + 1));
848 } else if let Some(name) = obj.get("IndexName").and_then(|v| v.as_str()) {
849 collect_idx_name_errors(name, "globalSecondaryIndexes", i + 1, errors);
850 }
851 if let Some(pt) = obj.get("ProvisionedThroughput").and_then(|v| v.as_object()) {
853 let wcu = pt.get("WriteCapacityUnits");
854 let rcu = pt.get("ReadCapacityUnits");
855 if let Some(w) = wcu.and_then(|v| v.as_i64()) {
856 if w < 1 {
857 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));
858 }
859 } else if wcu.is_none() || wcu == Some(&serde_json::Value::Null) {
860 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must not be null", i + 1));
861 }
862 if let Some(r) = rcu.and_then(|v| v.as_i64()) {
863 if r < 1 {
864 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));
865 }
866 } else if rcu.is_none() || rcu == Some(&serde_json::Value::Null) {
867 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must not be null", i + 1));
868 }
869 }
870 }
871 }
872 }
873 }
874}
875
876fn collect_idx_name_errors(name: &str, prefix: &str, idx: usize, errors: &mut Vec<String>) {
877 if !name
878 .chars()
879 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
880 {
881 errors.push(format!("Value '{}' at '{}.{}.member.indexName' failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+", name, prefix, idx));
882 }
883 if name.len() < 3 {
884 errors.push(format!("Value '{}' at '{}.{}.member.indexName' failed to satisfy constraint: Member must have length greater than or equal to 3", name, prefix, idx));
885 }
886 if name.len() > 255 {
887 errors.push(format!("Value '{}' at '{}.{}.member.indexName' failed to satisfy constraint: Member must have length less than or equal to 255", name, prefix, idx));
888 }
889}
890
891fn collect_proj_errors(
892 proj: &serde_json::Map<String, serde_json::Value>,
893 prefix: &str,
894 errors: &mut Vec<String>,
895) {
896 if let Some(pt) = proj.get("ProjectionType") {
897 if let Some(s) = pt.as_str() {
898 if s != "ALL" && s != "KEYS_ONLY" && s != "INCLUDE" {
899 errors.push(format!("Value '{}' at '{}.member.projection.projectionType' failed to satisfy constraint: Member must satisfy enum value set: [ALL, INCLUDE, KEYS_ONLY]", s, prefix));
900 }
901 }
902 }
903 if let Some(nka) = proj.get("NonKeyAttributes") {
904 if let Some(arr) = nka.as_array() {
905 if arr.is_empty() {
906 errors.push(format!("Value '[]' at '{}.member.projection.nonKeyAttributes' failed to satisfy constraint: Member must have length greater than or equal to 1", prefix));
907 }
908 }
909 }
910}
911
912fn validate_key_schema_structure(ks: &[KeySchemaElement]) -> std::result::Result<(), String> {
915 if ks.is_empty() {
916 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());
917 }
918 if ks[0].key_type != KeyType::HASH {
919 return Err(
920 "Invalid KeySchema: The first KeySchemaElement is not a HASH key type".to_string(),
921 );
922 }
923 if ks.len() == 2 && ks[0].attribute_name == ks[1].attribute_name {
924 return Err(
925 "Both the Hash Key and the Range Key element in the KeySchema have the same name"
926 .to_string(),
927 );
928 }
929 if ks.len() == 2 && ks[1].key_type != KeyType::RANGE {
930 return Err(
931 "Invalid KeySchema: The second KeySchemaElement is not a RANGE key type".to_string(),
932 );
933 }
934 Ok(())
935}
936
937fn validate_key_attrs_in_defs(
938 ks: &[KeySchemaElement],
939 defs: &[AttributeDefinition],
940) -> std::result::Result<(), String> {
941 let missing: Vec<&str> = ks
943 .iter()
944 .filter(|k| !defs.iter().any(|d| d.attribute_name == k.attribute_name))
945 .map(|k| k.attribute_name.as_str())
946 .collect();
947
948 if missing.is_empty() {
949 let has_dup_names = ks.len() == 2 && ks[0].attribute_name == ks[1].attribute_name;
952 if has_dup_names {
953 return Err(
954 "Invalid KeySchema: Some index key attribute have no definition".to_string(),
955 );
956 }
957 return Ok(());
958 }
959
960 let has_dup_names = ks.len() == 2 && ks[0].attribute_name == ks[1].attribute_name;
966 let has_dup_types = ks.len() == 2 && ks[0].key_type == ks[1].key_type;
967 let use_generic = defs.is_empty() || ks.len() >= 2 || has_dup_names || has_dup_types;
968
969 if use_generic {
970 return Err("Invalid KeySchema: Some index key attribute have no definition".to_string());
971 }
972
973 let key_names: Vec<&str> = missing.to_vec();
975 let def_names: Vec<&str> = defs.iter().map(|d| d.attribute_name.as_str()).collect();
976 Err(format!(
977 "One or more parameter values were invalid: Some index key attributes are not defined in \
978 AttributeDefinitions. Keys: [{}], AttributeDefinitions: [{}]",
979 key_names.join(", "),
980 def_names.join(", ")
981 ))
982}
983
984fn validate_attr_def_count(
985 ks: &[KeySchemaElement],
986 defs: &[AttributeDefinition],
987 lsis: &Option<Vec<LocalSecondaryIndex>>,
988 gsis: &Option<Vec<GlobalSecondaryIndex>>,
989) -> std::result::Result<(), String> {
990 let mut all_key_attrs = std::collections::HashSet::new();
991 for k in ks {
992 all_key_attrs.insert(k.attribute_name.as_str());
993 }
994 if let Some(lsis) = lsis {
995 for lsi in lsis {
996 for k in &lsi.key_schema {
997 all_key_attrs.insert(k.attribute_name.as_str());
998 }
999 }
1000 }
1001 if let Some(gsis) = gsis {
1002 for gsi in gsis {
1003 for k in &gsi.key_schema {
1004 all_key_attrs.insert(k.attribute_name.as_str());
1005 }
1006 }
1007 }
1008 if defs.len() != all_key_attrs.len() {
1009 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());
1010 }
1011 Ok(())
1012}
1013
1014fn validate_lsi_list(
1015 lsis: &[LocalSecondaryIndex],
1016 ks: &[KeySchemaElement],
1017 defs: &[AttributeDefinition],
1018) -> std::result::Result<(), String> {
1019 if !ks.iter().any(|k| k.key_type == KeyType::RANGE) {
1022 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());
1023 }
1024
1025 let def_names: Vec<&str> = defs.iter().map(|d| d.attribute_name.as_str()).collect();
1027 let mut missing_keys = Vec::new();
1028 for lsi in lsis {
1029 for k in &lsi.key_schema {
1030 if !def_names.contains(&k.attribute_name.as_str())
1031 && !missing_keys.contains(&k.attribute_name.as_str())
1032 {
1033 missing_keys.push(k.attribute_name.as_str());
1034 }
1035 }
1036 }
1037 if !missing_keys.is_empty() {
1038 let mut all_keys = Vec::new();
1039 for lsi in lsis {
1040 for k in &lsi.key_schema {
1041 if !all_keys.contains(&k.attribute_name.as_str()) {
1042 all_keys.push(k.attribute_name.as_str());
1043 }
1044 }
1045 }
1046 return Err(format!(
1047 "One or more parameter values were invalid: Some index key attributes are not defined in AttributeDefinitions. Keys: [{}], AttributeDefinitions: [{}]",
1048 all_keys.join(", "),
1049 def_names.join(", ")
1050 ));
1051 }
1052
1053 for lsi in lsis {
1055 validate_lsi_structure(lsi, ks)?;
1056 }
1057
1058 let mut seen = std::collections::HashSet::new();
1060 for lsi in lsis {
1061 if !seen.insert(&lsi.index_name) {
1062 return Err(format!(
1063 "One or more parameter values were invalid: Duplicate index name: {}",
1064 lsi.index_name
1065 ));
1066 }
1067 }
1068
1069 if lsis.len() > 5 {
1071 return Err("One or more parameter values were invalid: Number of LocalSecondaryIndexes exceeds per-table limit of 5".to_string());
1072 }
1073
1074 Ok(())
1075}
1076
1077fn validate_gsi_list(
1078 gsis: &[GlobalSecondaryIndex],
1079 defs: &[AttributeDefinition],
1080 bm: &str,
1081) -> std::result::Result<(), String> {
1082 let def_names: Vec<&str> = defs.iter().map(|d| d.attribute_name.as_str()).collect();
1086 let mut missing_keys = Vec::new();
1087 for gsi in gsis {
1088 for k in &gsi.key_schema {
1089 if !def_names.contains(&k.attribute_name.as_str())
1090 && !missing_keys.contains(&k.attribute_name.as_str())
1091 {
1092 missing_keys.push(k.attribute_name.as_str());
1093 }
1094 }
1095 }
1096 if !missing_keys.is_empty() {
1097 let mut all_keys = Vec::new();
1098 for gsi in gsis {
1099 for k in &gsi.key_schema {
1100 if !all_keys.contains(&k.attribute_name.as_str()) {
1101 all_keys.push(k.attribute_name.as_str());
1102 }
1103 }
1104 }
1105 return Err(format!(
1106 "One or more parameter values were invalid: Some index key attributes are not defined in AttributeDefinitions. Keys: [{}], AttributeDefinitions: [{}]",
1107 all_keys.join(", "),
1108 def_names.join(", ")
1109 ));
1110 }
1111
1112 for gsi in gsis {
1114 validate_gsi_structure(gsi)?;
1115 }
1116
1117 let mut seen = std::collections::HashSet::new();
1119 for gsi in gsis {
1120 if !seen.insert(&gsi.index_name) {
1121 return Err(format!(
1122 "One or more parameter values were invalid: Duplicate index name: {}",
1123 gsi.index_name
1124 ));
1125 }
1126 }
1127
1128 if gsis.len() > 20 {
1130 return Err("One or more parameter values were invalid: GlobalSecondaryIndex count exceeds the per-table limit of 20".to_string());
1131 }
1132
1133 if bm == "PAY_PER_REQUEST" {
1135 for gsi in gsis {
1136 if gsi.provisioned_throughput.is_some() {
1137 return Err(format!(
1138 "One or more parameter values were invalid: ProvisionedThroughput should not be specified for index: {} when BillingMode is PAY_PER_REQUEST",
1139 gsi.index_name
1140 ));
1141 }
1142 }
1143 }
1144
1145 Ok(())
1146}
1147
1148fn validate_lsi_structure(
1149 lsi: &LocalSecondaryIndex,
1150 table_ks: &[KeySchemaElement],
1151) -> std::result::Result<(), String> {
1152 validate_key_schema_structure(&lsi.key_schema)?;
1154
1155 let lsi_sk = lsi.key_schema.iter().find(|k| k.key_type == KeyType::RANGE);
1157 if lsi_sk.is_none() {
1158 return Err(format!(
1159 "One or more parameter values were invalid: Index KeySchema does not have a range key for index: {}",
1160 lsi.index_name
1161 ));
1162 }
1163
1164 let table_pk = table_ks
1166 .iter()
1167 .find(|k| k.key_type == KeyType::HASH)
1168 .map(|k| k.attribute_name.as_str());
1169 let lsi_pk = lsi
1170 .key_schema
1171 .iter()
1172 .find(|k| k.key_type == KeyType::HASH)
1173 .map(|k| k.attribute_name.as_str());
1174 if lsi_pk != table_pk {
1175 return Err(format!(
1176 "One or more parameter values were invalid: \
1177 Index KeySchema does not have the same leading hash key as table KeySchema \
1178 for index: {}. index hash key: {}, table hash key: {}",
1179 lsi.index_name,
1180 lsi_pk.unwrap_or("null"),
1181 table_pk.unwrap_or("null")
1182 ));
1183 }
1184
1185 validate_proj_structure(&lsi.projection)?;
1187
1188 Ok(())
1189}
1190
1191fn validate_gsi_structure(gsi: &GlobalSecondaryIndex) -> std::result::Result<(), String> {
1192 validate_key_schema_structure(&gsi.key_schema)?;
1193 validate_proj_structure(&gsi.projection)?;
1194 Ok(())
1195}
1196
1197fn validate_proj_structure(p: &Projection) -> std::result::Result<(), String> {
1198 match &p.projection_type {
1203 None => Err(
1204 "One or more parameter values were invalid: Unknown ProjectionType: null".to_string(),
1205 ),
1206 Some(ProjectionType::ALL) => match &p.non_key_attributes {
1207 Some(_) => Err("One or more parameter values were invalid: ProjectionType is ALL, but NonKeyAttributes is specified".to_string()),
1208 None => Ok(()),
1209 },
1210 Some(ProjectionType::KEYS_ONLY) => match &p.non_key_attributes {
1211 Some(_) => Err("One or more parameter values were invalid: ProjectionType is KEYS_ONLY, but NonKeyAttributes is specified".to_string()),
1212 None => Ok(()),
1213 },
1214 Some(ProjectionType::INCLUDE) => match &p.non_key_attributes {
1215 None => Err("One or more parameter values were invalid: ProjectionType is INCLUDE, but NonKeyAttributes is not specified".to_string()),
1216 Some(nka) if nka.is_empty() => Err("One or more parameter values were invalid: NonKeyAttributes must not be empty".to_string()),
1217 Some(_) => Ok(()),
1218 },
1219 }
1220}