Skip to main content

fakecloud_core/
validation.rs

1//! Generic input validation helpers for AWS-compatible error responses.
2//!
3//! All validators return `AwsServiceError` with `ValidationException` and 400 status.
4
5use crate::service::AwsServiceError;
6use http::StatusCode;
7
8/// Validate a string's length is within [min, max].
9pub fn validate_string_length(
10    field: &str,
11    value: &str,
12    min: usize,
13    max: usize,
14) -> Result<(), AwsServiceError> {
15    let len = value.len();
16    if len < min || len > max {
17        return Err(AwsServiceError::aws_error(
18            StatusCode::BAD_REQUEST,
19            "ValidationException",
20            format!(
21                "Value at '{}' failed to satisfy constraint: \
22                 Member must have length between {} and {}",
23                field, min, max,
24            ),
25        ));
26    }
27    Ok(())
28}
29
30/// Validate an integer is within [min, max].
31pub fn validate_range_i64(
32    field: &str,
33    value: i64,
34    min: i64,
35    max: i64,
36) -> Result<(), AwsServiceError> {
37    if value < min || value > max {
38        return Err(AwsServiceError::aws_error(
39            StatusCode::BAD_REQUEST,
40            "ValidationException",
41            format!(
42                "Value '{}' at '{}' failed to satisfy constraint: \
43                 Member must have value between {} and {}",
44                value, field, min, max,
45            ),
46        ));
47    }
48    Ok(())
49}
50
51/// Validate a string is one of the allowed enum values.
52pub fn validate_enum(field: &str, value: &str, allowed: &[&str]) -> Result<(), AwsServiceError> {
53    if !allowed.contains(&value) {
54        return Err(AwsServiceError::aws_error(
55            StatusCode::BAD_REQUEST,
56            "ValidationException",
57            format!(
58                "Value '{}' at '{}' failed to satisfy constraint: \
59                 Member must satisfy enum value set: [{}]",
60                value,
61                field,
62                allowed.join(", "),
63            ),
64        ));
65    }
66    Ok(())
67}
68
69/// Validate that a required field is present (not null/missing).
70pub fn validate_required(field: &str, value: &serde_json::Value) -> Result<(), AwsServiceError> {
71    if value.is_null() {
72        return Err(AwsServiceError::aws_error(
73            StatusCode::BAD_REQUEST,
74            "ValidationException",
75            format!("{} is required", field),
76        ));
77    }
78    Ok(())
79}
80
81/// Validate an optional string's length if present.
82pub fn validate_optional_string_length(
83    field: &str,
84    value: Option<&str>,
85    min: usize,
86    max: usize,
87) -> Result<(), AwsServiceError> {
88    if let Some(v) = value {
89        validate_string_length(field, v, min, max)?;
90    }
91    Ok(())
92}
93
94/// Validate an optional string length from a JSON value, rejecting non-string types.
95///
96/// Unlike [`validate_optional_string_length`], this takes a raw [`serde_json::Value`]
97/// so non-string inputs (numbers, arrays, objects) surface as a
98/// `SerializationException` instead of silently being treated as missing via
99/// `Value::as_str() -> None`.
100pub fn validate_optional_string_length_value(
101    field: &str,
102    value: &serde_json::Value,
103    min: usize,
104    max: usize,
105) -> Result<(), AwsServiceError> {
106    if value.is_null() {
107        return Ok(());
108    }
109    let s = value.as_str().ok_or_else(|| {
110        AwsServiceError::aws_error(
111            StatusCode::BAD_REQUEST,
112            "SerializationException",
113            format!("Value for '{}' must be a string", field),
114        )
115    })?;
116    validate_string_length(field, s, min, max)
117}
118
119/// Validate an optional integer range if present.
120pub fn validate_optional_range_i64(
121    field: &str,
122    value: Option<i64>,
123    min: i64,
124    max: i64,
125) -> Result<(), AwsServiceError> {
126    if let Some(v) = value {
127        validate_range_i64(field, v, min, max)?;
128    }
129    Ok(())
130}
131
132/// Validate an optional numeric JSON value is within [min, max].
133///
134/// Unlike `validate_optional_range_i64`, this takes a raw `serde_json::Value` reference
135/// so it can detect non-null, non-integer values (e.g. strings, large unsigned numbers)
136/// that would silently become `None` via `as_i64()`.
137pub fn validate_optional_json_range(
138    field: &str,
139    value: &serde_json::Value,
140    min: i64,
141    max: i64,
142) -> Result<(), AwsServiceError> {
143    if value.is_null() {
144        return Ok(());
145    }
146    let n = value.as_i64().ok_or_else(|| {
147        AwsServiceError::aws_error(
148            StatusCode::BAD_REQUEST,
149            "ValidationException",
150            format!(
151                "Value at '{}' failed to satisfy constraint: \
152                 Member must have value between {} and {}",
153                field, min, max,
154            ),
155        )
156    })?;
157    validate_range_i64(field, n, min, max)
158}
159
160/// Parse a string as an i64 for range validation, returning a `ValidationException`
161/// when the value is present but not a valid integer.
162///
163/// Use this instead of `.parse::<i64>().ok()` + `validate_optional_range_i64` to
164/// catch non-numeric strings that would silently fall through to defaults.
165pub fn parse_optional_i64_param(
166    field: &str,
167    value: Option<&str>,
168) -> Result<Option<i64>, AwsServiceError> {
169    match value {
170        None => Ok(None),
171        Some(s) => {
172            let n = s.parse::<i64>().map_err(|_| {
173                AwsServiceError::aws_error(
174                    StatusCode::BAD_REQUEST,
175                    "ValidationException",
176                    format!(
177                        "Value '{}' at '{}' failed to satisfy constraint: \
178                         Member must be a number",
179                        s, field,
180                    ),
181                )
182            })?;
183            Ok(Some(n))
184        }
185    }
186}
187
188/// Validate an optional enum value if present.
189pub fn validate_optional_enum(
190    field: &str,
191    value: Option<&str>,
192    allowed: &[&str],
193) -> Result<(), AwsServiceError> {
194    if let Some(v) = value {
195        validate_enum(field, v, allowed)?;
196    }
197    Ok(())
198}
199
200/// Validate an optional enum from a JSON value, rejecting non-string types.
201///
202/// Unlike [`validate_optional_enum`], this takes a raw [`serde_json::Value`] so it can
203/// distinguish between a missing/null field (ok to skip) and a non-string value (error).
204pub fn validate_optional_enum_value(
205    field: &str,
206    value: &serde_json::Value,
207    allowed: &[&str],
208) -> Result<(), AwsServiceError> {
209    if value.is_null() {
210        return Ok(());
211    }
212    let s = value.as_str().ok_or_else(|| {
213        AwsServiceError::aws_error(
214            StatusCode::BAD_REQUEST,
215            "SerializationException",
216            format!("Value for '{}' must be a string", field),
217        )
218    })?;
219    validate_enum(field, s, allowed)
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn validate_optional_json_range_rejects_non_integer() {
228        let val = serde_json::json!("abc");
229        let result = validate_optional_json_range("limit", &val, 1, 100);
230        assert!(result.is_err());
231    }
232
233    #[test]
234    fn validate_optional_json_range_rejects_large_unsigned() {
235        let val = serde_json::json!(u64::MAX);
236        let result = validate_optional_json_range("limit", &val, 1, 1000);
237        assert!(result.is_err());
238    }
239
240    #[test]
241    fn validate_optional_json_range_allows_null() {
242        let val = serde_json::Value::Null;
243        let result = validate_optional_json_range("limit", &val, 1, 100);
244        assert!(result.is_ok());
245    }
246
247    #[test]
248    fn validate_optional_json_range_validates_range() {
249        let val = serde_json::json!(0);
250        let result = validate_optional_json_range("limit", &val, 1, 100);
251        assert!(result.is_err());
252
253        let val = serde_json::json!(50);
254        let result = validate_optional_json_range("limit", &val, 1, 100);
255        assert!(result.is_ok());
256    }
257
258    #[test]
259    fn parse_optional_i64_param_rejects_non_numeric() {
260        let result = parse_optional_i64_param("maxItems", Some("abc"));
261        assert!(result.is_err());
262    }
263
264    #[test]
265    fn parse_optional_i64_param_allows_none() {
266        let result = parse_optional_i64_param("maxItems", None);
267        assert!(result.is_ok());
268        assert_eq!(result.unwrap(), None);
269    }
270
271    #[test]
272    fn parse_optional_i64_param_parses_valid_number() {
273        let result = parse_optional_i64_param("maxItems", Some("42"));
274        assert!(result.is_ok());
275        assert_eq!(result.unwrap(), Some(42));
276    }
277}