Skip to main content

dynoxide/actions/
create_table.rs

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/// Internal raw deserialization struct — uses serde_json::Value for fields
13/// that participate in DynamoDB's multi-field constraint validation.
14#[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/// Public request type — fully validated, typed fields.
45/// Can be constructed directly (programmatic use) or deserialized from JSON.
46#[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
63/// Custom Deserialize that does loose JSON parsing first, validates, then builds typed fields.
64/// Validation errors use "VALIDATION:" prefix so server.rs converts them to ValidationException.
65impl<'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    // Structural validation (runs for both programmatic and JSON paths)
96    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    // Normalise the SSE spec so a DescribeTable round-trip matches AWS: when
143    // encryption is enabled without an explicit type/key, AWS reports SSEType=KMS
144    // and a KMS key ARN (the AWS-managed `aws/dynamodb` key). Persisting the
145    // synthesised key id keeps the reported ARN stable across DescribeTable calls.
146    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    // CreateTable response shows CREATING status (table is usable immediately
238    // but DynamoDB API contract says newly-created tables start as CREATING)
239    desc.table_status = "CREATING".to_string();
240
241    // Override billing mode fields based on the actual request
242    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        // Ensure provisioned throughput shows zeros for PAY_PER_REQUEST
256        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    // Set all GSI statuses to CREATING for newly created tables
266    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    // Remove DeletionProtectionEnabled from response if not explicitly set
273    // (DynamoDB doesn't include it in basic CreateTable response)
274    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
283/// Convert a String error to DynoxideError::ValidationException.
284fn ve(msg: String) -> DynoxideError {
285    DynoxideError::ValidationException(msg)
286}
287
288/// Validate a programmatically-constructed request (used when not deserialised from JSON).
289///
290/// The validation order matches DynamoDB's actual behaviour (as verified by the Dynalite
291/// conformance suite):
292///
293/// 1. Table name (missing, length, pattern)
294/// 2. BillingMode + ProvisionedThroughput consistency
295/// 3. ProvisionedThroughput out-of-bounds
296/// 4. Missing ProvisionedThroughput (default PROVISIONED billing)
297/// 5. Key attribute definition checks ("Invalid KeySchema" / detailed missing-attr message)
298/// 6. Key schema structure (duplicate names, wrong types)
299/// 7. Empty LSI/GSI lists
300/// 8. LSI/GSI structural validation (key schema, projections, duplicates, limits)
301/// 9. Cross-index duplicate names
302/// 10. Attribute definition count mismatch
303/// 11. StreamSpecification consistency (a disabled stream must not set a view type)
304fn 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    // Table name pattern
318    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    // BillingMode + ProvisionedThroughput consistency
331    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    // ProvisionedThroughput out-of-bounds
341    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    // Missing ProvisionedThroughput when billing mode is explicitly PROVISIONED.
360    // For the programmatic API, when BillingMode is not specified we default to
361    // PAY_PER_REQUEST for convenience. The HTTP/JSON path (validate_raw_and_build)
362    // applies the stricter DynamoDB default of PROVISIONED.
363    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    // Key attribute definition checks (before key schema structure)
375    validate_key_attrs_in_defs(&request.key_schema, &request.attribute_definitions).map_err(ve)?;
376
377    // Key schema structure
378    validate_key_schema_structure(&request.key_schema).map_err(ve)?;
379
380    // Empty LSI/GSI lists (before structural validation and attr count)
381    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    // LSI structural validation
399    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    // GSI structural validation
404    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    // Cross-index duplicate names (checked before attr def count)
410    check_cross_index_duplicates(
411        &request.local_secondary_indexes,
412        &request.global_secondary_indexes,
413    )
414    .map_err(ve)?;
415
416    // Attribute definition count (last of the key/index checks)
417    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    // StreamSpecification consistency: a disabled stream must not carry a view
426    // type. Real DynamoDB rejects the combination, because a view type only has
427    // meaning when the stream is enabled.
428    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
462// ---- Raw JSON validation (for deserialization path) ----
463
464fn validate_raw_and_build(raw: RawRequest) -> std::result::Result<CreateTableRequest, String> {
465    // Missing TableName is a different error format from invalid TableName
466    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    // Use the shared constraint error collector for table name validation.
473    // This produces the correct multi-field constraint format for empty,
474    // too-short, too-long, or invalid-pattern table names.
475    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    // DynamoDB caps multi-field constraint errors at 10
509    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    // BillingMode + ProvisionedThroughput consistency (HTTP path only)
521    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    // ProvisionedThroughput out-of-bounds (after multi-field but before struct checks)
531    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    // Missing ProvisionedThroughput when BillingMode is explicitly PROVISIONED.
558    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    // Parse typed structures
567    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
617// ---- Multi-field constraint error collectors ----
618
619fn 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/// Render a KeySchema array the way AWS does in validation messages: Java's
685/// default toString() shape over the SDK's KeySchemaElement model, e.g.
686/// `[KeySchemaElement(attributeName=pk, keyType=HASH), KeySchemaElement(attributeName=sk, keyType=RANGE)]`.
687/// Missing attribute fields render as the empty string, matching what AWS
688/// emits when the upstream object hasn't been populated.
689#[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                    // Order: indexName, keySchema, projection
740                    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                    // Order for GSI: keySchema, projection, indexName
775                    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                    // GSI ProvisionedThroughput
803                    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
863// ---- Structural validation helpers ----
864
865fn 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    // Collect missing key attribute names
893    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        // Even if no keys are missing, check for structural issues (dup names/types)
901        // which DynamoDB reports as generic "no definition" when defs exist
902        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    // Use generic message when:
912    // - defs is empty (fewer defs than unique key attrs)
913    // - key schema has 2 elements (regardless of structural validity)
914    // - key schema has structural issues (dup names, dup types)
915    // Use detailed message only when defs is non-empty AND key schema has 1 element
916    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    // Detailed message for single-key schema with non-empty defs
925    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    // Empty check is done earlier in validate_typed_request
971
972    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    // Check missing attribute definitions across all LSI keys
977    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    // Structural validation for each LSI
1005    for lsi in lsis {
1006        validate_lsi_structure(lsi, ks)?;
1007    }
1008
1009    // Duplicate index names
1010    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    // Count limit
1021    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    // Empty check is done earlier in validate_typed_request
1034
1035    // Check missing attribute definitions across all GSI keys
1036    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    // Structural validation for each GSI
1064    for gsi in gsis {
1065        validate_gsi_structure(gsi)?;
1066    }
1067
1068    // Duplicate index names
1069    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    // Count limit
1080    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    // PAY_PER_REQUEST billing mode check
1085    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    // Key schema structure first
1104    validate_key_schema_structure(&lsi.key_schema)?;
1105
1106    // Range key presence (before projection, per DynamoDB ordering)
1107    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    // Hash key must match table hash key (before projection)
1116    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    // Projection (after range key and hash key checks)
1137    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    // Matched on the projection type regardless of whether NonKeyAttributes is
1150    // present, so the INCLUDE-without-list case (DynamoDB rejects it) is
1151    // reachable. ALL and KEYS_ONLY must not carry a list; INCLUDE requires a
1152    // non-empty one. Reached by both GSI and LSI validation.
1153    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}