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 integer range if present.
95pub fn validate_optional_range_i64(
96    field: &str,
97    value: Option<i64>,
98    min: i64,
99    max: i64,
100) -> Result<(), AwsServiceError> {
101    if let Some(v) = value {
102        validate_range_i64(field, v, min, max)?;
103    }
104    Ok(())
105}
106
107/// Validate an optional numeric JSON value is within [min, max].
108///
109/// Unlike `validate_optional_range_i64`, this takes a raw `serde_json::Value` reference
110/// so it can detect non-null, non-integer values (e.g. strings, large unsigned numbers)
111/// that would silently become `None` via `as_i64()`.
112pub fn validate_optional_json_range(
113    field: &str,
114    value: &serde_json::Value,
115    min: i64,
116    max: i64,
117) -> Result<(), AwsServiceError> {
118    if value.is_null() {
119        return Ok(());
120    }
121    let n = value.as_i64().ok_or_else(|| {
122        AwsServiceError::aws_error(
123            StatusCode::BAD_REQUEST,
124            "ValidationException",
125            format!(
126                "Value at '{}' failed to satisfy constraint: \
127                 Member must have value between {} and {}",
128                field, min, max,
129            ),
130        )
131    })?;
132    validate_range_i64(field, n, min, max)
133}
134
135/// Parse a string as an i64 for range validation, returning a `ValidationException`
136/// when the value is present but not a valid integer.
137///
138/// Use this instead of `.parse::<i64>().ok()` + `validate_optional_range_i64` to
139/// catch non-numeric strings that would silently fall through to defaults.
140pub fn parse_optional_i64_param(
141    field: &str,
142    value: Option<&str>,
143) -> Result<Option<i64>, AwsServiceError> {
144    match value {
145        None => Ok(None),
146        Some(s) => {
147            let n = s.parse::<i64>().map_err(|_| {
148                AwsServiceError::aws_error(
149                    StatusCode::BAD_REQUEST,
150                    "ValidationException",
151                    format!(
152                        "Value '{}' at '{}' failed to satisfy constraint: \
153                         Member must be a number",
154                        s, field,
155                    ),
156                )
157            })?;
158            Ok(Some(n))
159        }
160    }
161}
162
163/// Validate an optional enum value if present.
164pub fn validate_optional_enum(
165    field: &str,
166    value: Option<&str>,
167    allowed: &[&str],
168) -> Result<(), AwsServiceError> {
169    if let Some(v) = value {
170        validate_enum(field, v, allowed)?;
171    }
172    Ok(())
173}
174
175/// Validate an optional enum from a JSON value, rejecting non-string types.
176///
177/// Unlike [`validate_optional_enum`], this takes a raw [`serde_json::Value`] so it can
178/// distinguish between a missing/null field (ok to skip) and a non-string value (error).
179pub fn validate_optional_enum_value(
180    field: &str,
181    value: &serde_json::Value,
182    allowed: &[&str],
183) -> Result<(), AwsServiceError> {
184    if value.is_null() {
185        return Ok(());
186    }
187    let s = value.as_str().ok_or_else(|| {
188        AwsServiceError::aws_error(
189            StatusCode::BAD_REQUEST,
190            "SerializationException",
191            format!("Value for '{}' must be a string", field),
192        )
193    })?;
194    validate_enum(field, s, allowed)
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn validate_optional_json_range_rejects_non_integer() {
203        let val = serde_json::json!("abc");
204        let result = validate_optional_json_range("limit", &val, 1, 100);
205        assert!(result.is_err());
206    }
207
208    #[test]
209    fn validate_optional_json_range_rejects_large_unsigned() {
210        let val = serde_json::json!(u64::MAX);
211        let result = validate_optional_json_range("limit", &val, 1, 1000);
212        assert!(result.is_err());
213    }
214
215    #[test]
216    fn validate_optional_json_range_allows_null() {
217        let val = serde_json::Value::Null;
218        let result = validate_optional_json_range("limit", &val, 1, 100);
219        assert!(result.is_ok());
220    }
221
222    #[test]
223    fn validate_optional_json_range_validates_range() {
224        let val = serde_json::json!(0);
225        let result = validate_optional_json_range("limit", &val, 1, 100);
226        assert!(result.is_err());
227
228        let val = serde_json::json!(50);
229        let result = validate_optional_json_range("limit", &val, 1, 100);
230        assert!(result.is_ok());
231    }
232
233    #[test]
234    fn parse_optional_i64_param_rejects_non_numeric() {
235        let result = parse_optional_i64_param("maxItems", Some("abc"));
236        assert!(result.is_err());
237    }
238
239    #[test]
240    fn parse_optional_i64_param_allows_none() {
241        let result = parse_optional_i64_param("maxItems", None);
242        assert!(result.is_ok());
243        assert_eq!(result.unwrap(), None);
244    }
245
246    #[test]
247    fn parse_optional_i64_param_parses_valid_number() {
248        let result = parse_optional_i64_param("maxItems", Some("42"));
249        assert!(result.is_ok());
250        assert_eq!(result.unwrap(), Some(42));
251    }
252}