Skip to main content

leptos_next_metadata/api/contracts/
rules.rs

1//! Validation rule implementations
2//!
3//! This module provides pluggable validation rules for API contract validation.
4
5use super::types::ValidationError;
6use serde_json::Value;
7use std::collections::HashMap;
8
9/// Trait for custom validation rules
10pub trait ValidationRule: Send + Sync {
11    fn name(&self) -> &str;
12
13    fn validate_request(
14        &self,
15        method: &str,
16        path: &str,
17        headers: &HashMap<String, String>,
18        body: Option<&Value>,
19    ) -> Result<(), ValidationError>;
20
21    fn validate_response(
22        &self,
23        _method: &str,
24        _path: &str,
25        _status_code: u16,
26        _headers: &HashMap<String, String>,
27        _body: Option<&Value>,
28    ) -> Result<(), ValidationError> {
29        // Default: no response validation
30        Ok(())
31    }
32}
33
34/// Validates basic schema compliance
35#[derive(Clone)]
36pub struct SchemaComplianceRule;
37
38impl ValidationRule for SchemaComplianceRule {
39    fn name(&self) -> &str {
40        "schema-compliance"
41    }
42
43    fn validate_request(
44        &self,
45        method: &str,
46        _path: &str,
47        headers: &HashMap<String, String>,
48        body: Option<&Value>,
49    ) -> Result<(), ValidationError> {
50        // Validate Content-Type header for requests with body
51        if body.is_some() && method != "GET" && !headers.contains_key("content-type") {
52            return Err(ValidationError::MissingHeader {
53                header: "content-type".to_string(),
54            });
55        }
56
57        Ok(())
58    }
59}
60
61/// Validates required fields are present
62#[derive(Clone)]
63pub struct RequiredFieldsRule;
64
65impl ValidationRule for RequiredFieldsRule {
66    fn name(&self) -> &str {
67        "required-fields"
68    }
69
70    fn validate_request(
71        &self,
72        _method: &str,
73        _path: &str,
74        _headers: &HashMap<String, String>,
75        body: Option<&Value>,
76    ) -> Result<(), ValidationError> {
77        if let Some(Value::Object(obj)) = body {
78            // Check for common required fields in metadata
79            if !obj.contains_key("title") && !obj.contains_key("description") {
80                return Err(ValidationError::MissingRequiredField {
81                    field: "title or description".to_string(),
82                });
83            }
84        }
85
86        Ok(())
87    }
88}
89
90/// Validates data types match schema constraints
91#[derive(Clone)]
92pub struct TypeConstraintsRule;
93
94impl ValidationRule for TypeConstraintsRule {
95    fn name(&self) -> &str {
96        "type-constraints"
97    }
98
99    fn validate_request(
100        &self,
101        _method: &str,
102        _path: &str,
103        _headers: &HashMap<String, String>,
104        body: Option<&Value>,
105    ) -> Result<(), ValidationError> {
106        if let Some(Value::Object(obj)) = body {
107            // Validate string fields
108            for (key, value) in obj {
109                if key.ends_with("_url") {
110                    if let Value::String(url) = value {
111                        if !self.is_valid_url(url) {
112                            return Err(ValidationError::InvalidFormat {
113                                field: key.clone(),
114                                expected: "valid URL".to_string(),
115                                actual: url.clone(),
116                            });
117                        }
118                    }
119                }
120            }
121        }
122
123        Ok(())
124    }
125}
126
127impl TypeConstraintsRule {
128    fn is_valid_url(&self, url: &str) -> bool {
129        url::Url::parse(url).is_ok()
130    }
131}
132
133/// Validates string formats (email, url, date, etc.)
134#[derive(Clone)]
135pub struct FormatValidationRule;
136
137impl ValidationRule for FormatValidationRule {
138    fn name(&self) -> &str {
139        "format-validation"
140    }
141
142    fn validate_request(
143        &self,
144        _method: &str,
145        _path: &str,
146        _headers: &HashMap<String, String>,
147        body: Option<&Value>,
148    ) -> Result<(), ValidationError> {
149        if let Some(Value::Object(obj)) = body {
150            for (key, value) in obj {
151                if let Value::String(s) = value {
152                    if key.contains("email") && !self.is_valid_email(s) {
153                        return Err(ValidationError::InvalidFormat {
154                            field: key.clone(),
155                            expected: "valid email".to_string(),
156                            actual: s.clone(),
157                        });
158                    }
159                }
160            }
161        }
162
163        Ok(())
164    }
165}
166
167impl FormatValidationRule {
168    fn is_valid_email(&self, email: &str) -> bool {
169        // Simple email validation
170        email.contains('@') && email.contains('.') && email.len() > 5
171    }
172}
173
174/// Validates OpenGraph specific constraints
175#[derive(Clone)]
176pub struct OpenGraphValidationRule;
177
178impl ValidationRule for OpenGraphValidationRule {
179    fn name(&self) -> &str {
180        "open-graph-validation"
181    }
182
183    fn validate_request(
184        &self,
185        _method: &str,
186        _path: &str,
187        _headers: &HashMap<String, String>,
188        body: Option<&Value>,
189    ) -> Result<(), ValidationError> {
190        if let Some(Value::Object(obj)) = body {
191            if let Some(Value::Object(og)) = obj.get("open_graph") {
192                // Validate OpenGraph image URL
193                if let Some(Value::Object(image)) = og.get("image") {
194                    if let Some(Value::String(url)) = image.get("url") {
195                        if !self.is_valid_url(url) {
196                            return Err(ValidationError::InvalidFormat {
197                                field: "open_graph.image.url".to_string(),
198                                expected: "valid URL".to_string(),
199                                actual: url.clone(),
200                            });
201                        }
202                    }
203                }
204
205                // Validate OpenGraph title length
206                if let Some(Value::String(title)) = og.get("title") {
207                    if title.len() > 95 {
208                        return Err(ValidationError::FieldTooLong {
209                            field: "open_graph.title".to_string(),
210                            max_length: 95,
211                            actual_length: title.len(),
212                        });
213                    }
214                }
215            }
216        }
217
218        Ok(())
219    }
220}
221
222impl OpenGraphValidationRule {
223    fn is_valid_url(&self, url: &str) -> bool {
224        url::Url::parse(url).is_ok()
225    }
226}
227
228/// Validates Twitter Card specific constraints
229#[derive(Clone)]
230pub struct TwitterValidationRule;
231
232impl ValidationRule for TwitterValidationRule {
233    fn name(&self) -> &str {
234        "twitter-validation"
235    }
236
237    fn validate_request(
238        &self,
239        _method: &str,
240        _path: &str,
241        _headers: &HashMap<String, String>,
242        body: Option<&Value>,
243    ) -> Result<(), ValidationError> {
244        if let Some(Value::Object(obj)) = body {
245            if let Some(Value::Object(twitter)) = obj.get("twitter") {
246                // Validate Twitter card type
247                if let Some(Value::String(card)) = twitter.get("card") {
248                    let valid_cards = ["summary", "summary_large_image", "app", "player"];
249                    if !valid_cards.contains(&card.as_str()) {
250                        return Err(ValidationError::InvalidValue {
251                            field: "twitter.card".to_string(),
252                            expected: format!("one of: {}", valid_cards.join(", ")),
253                            actual: card.clone(),
254                        });
255                    }
256                }
257
258                // Validate Twitter image URL
259                if let Some(Value::String(image)) = twitter.get("image") {
260                    if !self.is_valid_url(image) {
261                        return Err(ValidationError::InvalidFormat {
262                            field: "twitter.image".to_string(),
263                            expected: "valid URL".to_string(),
264                            actual: image.clone(),
265                        });
266                    }
267                }
268            }
269        }
270
271        Ok(())
272    }
273}
274
275impl TwitterValidationRule {
276    fn is_valid_url(&self, url: &str) -> bool {
277        url::Url::parse(url).is_ok()
278    }
279}
280
281/// Enum for all validation rules to make them cloneable
282#[derive(Clone)]
283pub enum ValidationRuleEnum {
284    SchemaCompliance(SchemaComplianceRule),
285    RequiredFields(RequiredFieldsRule),
286    TypeConstraints(TypeConstraintsRule),
287    FormatValidation(FormatValidationRule),
288    OpenGraphValidation(OpenGraphValidationRule),
289    TwitterValidation(TwitterValidationRule),
290}
291
292impl ValidationRule for ValidationRuleEnum {
293    fn name(&self) -> &str {
294        match self {
295            ValidationRuleEnum::SchemaCompliance(rule) => rule.name(),
296            ValidationRuleEnum::RequiredFields(rule) => rule.name(),
297            ValidationRuleEnum::TypeConstraints(rule) => rule.name(),
298            ValidationRuleEnum::FormatValidation(rule) => rule.name(),
299            ValidationRuleEnum::OpenGraphValidation(rule) => rule.name(),
300            ValidationRuleEnum::TwitterValidation(rule) => rule.name(),
301        }
302    }
303
304    fn validate_request(
305        &self,
306        method: &str,
307        path: &str,
308        headers: &HashMap<String, String>,
309        body: Option<&Value>,
310    ) -> Result<(), ValidationError> {
311        match self {
312            ValidationRuleEnum::SchemaCompliance(rule) => {
313                rule.validate_request(method, path, headers, body)
314            }
315            ValidationRuleEnum::RequiredFields(rule) => {
316                rule.validate_request(method, path, headers, body)
317            }
318            ValidationRuleEnum::TypeConstraints(rule) => {
319                rule.validate_request(method, path, headers, body)
320            }
321            ValidationRuleEnum::FormatValidation(rule) => {
322                rule.validate_request(method, path, headers, body)
323            }
324            ValidationRuleEnum::OpenGraphValidation(rule) => {
325                rule.validate_request(method, path, headers, body)
326            }
327            ValidationRuleEnum::TwitterValidation(rule) => {
328                rule.validate_request(method, path, headers, body)
329            }
330        }
331    }
332}