leptos_next_metadata/api/contracts/
rules.rs1use super::types::ValidationError;
6use serde_json::Value;
7use std::collections::HashMap;
8
9pub 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 Ok(())
31 }
32}
33
34#[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 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#[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 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#[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 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#[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 email.contains('@') && email.contains('.') && email.len() > 5
171 }
172}
173
174#[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 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 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#[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 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 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#[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}