Skip to main content

fakecloud_dynamodb/service/helpers/
schemas.rs

1//! dynamodb helpers `schemas` concerns (audit-2026-05-19).
2
3use super::*;
4
5pub(crate) fn parse_key_schema(val: &Value) -> Result<Vec<KeySchemaElement>, AwsServiceError> {
6    let arr = val.as_array().ok_or_else(|| {
7        AwsServiceError::aws_error(
8            StatusCode::BAD_REQUEST,
9            "ValidationException",
10            "KeySchema is required",
11        )
12    })?;
13    Ok(arr
14        .iter()
15        .map(|elem| KeySchemaElement {
16            attribute_name: elem["AttributeName"]
17                .as_str()
18                .unwrap_or_default()
19                .to_string(),
20            key_type: elem["KeyType"].as_str().unwrap_or("HASH").to_string(),
21        })
22        .collect())
23}
24
25pub(crate) fn parse_attribute_definitions(
26    val: &Value,
27) -> Result<Vec<AttributeDefinition>, AwsServiceError> {
28    let arr = val.as_array().ok_or_else(|| {
29        AwsServiceError::aws_error(
30            StatusCode::BAD_REQUEST,
31            "ValidationException",
32            "AttributeDefinitions is required",
33        )
34    })?;
35    Ok(arr
36        .iter()
37        .map(|elem| AttributeDefinition {
38            attribute_name: elem["AttributeName"]
39                .as_str()
40                .unwrap_or_default()
41                .to_string(),
42            attribute_type: elem["AttributeType"].as_str().unwrap_or("S").to_string(),
43        })
44        .collect())
45}
46
47pub(crate) fn parse_provisioned_throughput(
48    val: &Value,
49) -> Result<ProvisionedThroughput, AwsServiceError> {
50    Ok(ProvisionedThroughput {
51        read_capacity_units: val["ReadCapacityUnits"].as_i64().unwrap_or(5),
52        write_capacity_units: val["WriteCapacityUnits"].as_i64().unwrap_or(5),
53    })
54}
55
56pub fn parse_gsi(val: &Value, billing_mode: &str) -> Vec<GlobalSecondaryIndex> {
57    let Some(arr) = val.as_array() else {
58        return Vec::new();
59    };
60    arr.iter()
61        .filter_map(|g| {
62            Some(GlobalSecondaryIndex {
63                index_name: g["IndexName"].as_str()?.to_string(),
64                key_schema: parse_key_schema(&g["KeySchema"]).ok()?,
65                projection: parse_projection(&g["Projection"]),
66                provisioned_throughput: Some(parse_gsi_throughput(
67                    &g["ProvisionedThroughput"],
68                    billing_mode,
69                )),
70                on_demand_throughput: parse_on_demand_throughput(&g["OnDemandThroughput"]),
71            })
72        })
73        .collect()
74}
75
76/// Resolve the provisioned-throughput slot for a GSI on a CreateTable or
77/// UpdateTable Create action. Real DynamoDB returns `{0, 0}` for GSIs on
78/// PAY_PER_REQUEST tables regardless of whether the caller sent a
79/// `ProvisionedThroughput` block, and the Terraform provider's `flatten`
80/// code keys `name`/`read_capacity`/`write_capacity` off the presence of
81/// that field — returning `None` would desynchronise state.
82pub fn parse_gsi_throughput(val: &Value, billing_mode: &str) -> ProvisionedThroughput {
83    if billing_mode == "PAY_PER_REQUEST" {
84        return ProvisionedThroughput {
85            read_capacity_units: 0,
86            write_capacity_units: 0,
87        };
88    }
89    ProvisionedThroughput {
90        read_capacity_units: val["ReadCapacityUnits"].as_i64().unwrap_or(5),
91        write_capacity_units: val["WriteCapacityUnits"].as_i64().unwrap_or(5),
92    }
93}
94
95pub fn parse_lsi(val: &Value) -> Vec<LocalSecondaryIndex> {
96    let Some(arr) = val.as_array() else {
97        return Vec::new();
98    };
99    arr.iter()
100        .filter_map(|l| {
101            Some(LocalSecondaryIndex {
102                index_name: l["IndexName"].as_str()?.to_string(),
103                key_schema: parse_key_schema(&l["KeySchema"]).ok()?,
104                projection: parse_projection(&l["Projection"]),
105            })
106        })
107        .collect()
108}
109
110pub fn parse_tags(val: &Value) -> BTreeMap<String, String> {
111    let mut tags = BTreeMap::new();
112    if let Some(arr) = val.as_array() {
113        for tag in arr {
114            if let (Some(k), Some(v)) = (tag["Key"].as_str(), tag["Value"].as_str()) {
115                tags.insert(k.to_string(), v.to_string());
116            }
117        }
118    }
119    tags
120}
121
122pub(crate) fn parse_expression_attribute_names(body: &Value) -> HashMap<String, String> {
123    let mut names = HashMap::new();
124    if let Some(obj) = body["ExpressionAttributeNames"].as_object() {
125        for (k, v) in obj {
126            if let Some(s) = v.as_str() {
127                names.insert(k.clone(), s.to_string());
128            }
129        }
130    }
131    names
132}
133
134pub(crate) fn parse_expression_attribute_values(body: &Value) -> HashMap<String, Value> {
135    let mut values = HashMap::new();
136    if let Some(obj) = body["ExpressionAttributeValues"].as_object() {
137        for (k, v) in obj {
138            values.insert(k.clone(), v.clone());
139        }
140    }
141    values
142}