mockforge_bench/conformance/
schema_validator.rs1use openapiv3::{ReferenceOr, Schema, SchemaKind, StringFormat, Type, VariantOrUnknownOrEmpty};
6
7pub struct SchemaValidatorGenerator;
9
10impl SchemaValidatorGenerator {
11 pub fn generate_validation(schema: &Schema) -> String {
16 Self::generate_for_schema(schema, "body")
17 }
18
19 fn generate_for_schema(schema: &Schema, var: &str) -> String {
20 match &schema.schema_kind {
21 SchemaKind::Type(Type::Object(obj)) => {
22 let mut checks = vec![format!("typeof {} === 'object'", var)];
23 checks.push(format!("{} !== null", var));
24
25 for field in &obj.required {
27 checks.push(format!("'{}' in {}", field, var));
28 }
29
30 for (name, prop_ref) in &obj.properties {
32 if let ReferenceOr::Item(prop_schema) = prop_ref {
33 let prop_var = format!("{}['{}']", var, name);
34 let type_check = Self::generate_type_check(prop_schema, &prop_var);
35 if !type_check.is_empty() {
36 if obj.required.contains(name) {
38 checks.push(type_check);
39 } else {
40 checks.push(format!(
41 "({} === undefined || {})",
42 prop_var, type_check
43 ));
44 }
45 }
46 }
47 }
48
49 checks.join(" && ")
50 }
51 SchemaKind::Type(Type::Array(arr)) => {
52 let mut checks = vec![format!("Array.isArray({})", var)];
53
54 if let Some(ReferenceOr::Item(item_schema)) = &arr.items {
55 let item_check = Self::generate_type_check(item_schema, &format!("{}[0]", var));
56 if !item_check.is_empty() {
57 checks.push(format!("({}.length === 0 || {})", var, item_check));
59 }
60 }
61
62 checks.join(" && ")
63 }
64 SchemaKind::Type(Type::String(s)) => {
65 let mut checks = vec![format!("typeof {} === 'string'", var)];
66
67 let format_str = match &s.format {
69 VariantOrUnknownOrEmpty::Item(StringFormat::Date) => Some("date"),
70 VariantOrUnknownOrEmpty::Item(StringFormat::DateTime) => Some("date-time"),
71 VariantOrUnknownOrEmpty::Unknown(f) => Some(f.as_str()),
72 _ => None,
73 };
74 if let Some(fmt) = format_str {
75 if let Some(regex) = Self::format_regex(fmt) {
76 checks.push(format!("{}.match({})", var, regex));
77 }
78 }
79
80 if !s.enumeration.is_empty() {
82 let values: Vec<String> = s
83 .enumeration
84 .iter()
85 .filter_map(|v| v.as_ref().map(|s| format!("'{}'", s)))
86 .collect();
87 if !values.is_empty() {
88 checks.push(format!("[{}].includes({})", values.join(","), var));
89 }
90 }
91
92 if let Some(min) = s.min_length {
94 checks.push(format!("{}.length >= {}", var, min));
95 }
96 if let Some(max) = s.max_length {
97 checks.push(format!("{}.length <= {}", var, max));
98 }
99
100 checks.join(" && ")
101 }
102 SchemaKind::Type(Type::Integer(i)) => {
103 let mut checks = vec![format!("typeof {} === 'number'", var)];
104 checks.push(format!("Number.isInteger({})", var));
105
106 if let Some(min) = i.minimum {
107 checks.push(format!("{} >= {}", var, min));
108 }
109 if let Some(max) = i.maximum {
110 checks.push(format!("{} <= {}", var, max));
111 }
112 if !i.enumeration.is_empty() {
113 let values: Vec<String> =
114 i.enumeration.iter().filter_map(|v| v.map(|n| n.to_string())).collect();
115 if !values.is_empty() {
116 checks.push(format!("[{}].includes({})", values.join(","), var));
117 }
118 }
119
120 checks.join(" && ")
121 }
122 SchemaKind::Type(Type::Number(n)) => {
123 let mut checks = vec![format!("typeof {} === 'number'", var)];
124
125 if let Some(min) = n.minimum {
126 checks.push(format!("{} >= {}", var, min));
127 }
128 if let Some(max) = n.maximum {
129 checks.push(format!("{} <= {}", var, max));
130 }
131
132 checks.join(" && ")
133 }
134 SchemaKind::Type(Type::Boolean(_)) => {
135 format!("typeof {} === 'boolean'", var)
136 }
137 _ => "true".to_string(), }
139 }
140
141 fn generate_type_check(schema: &Schema, var: &str) -> String {
143 match &schema.schema_kind {
144 SchemaKind::Type(Type::String(_)) => format!("typeof {} === 'string'", var),
145 SchemaKind::Type(Type::Integer(_)) => format!("typeof {} === 'number'", var),
146 SchemaKind::Type(Type::Number(_)) => format!("typeof {} === 'number'", var),
147 SchemaKind::Type(Type::Boolean(_)) => format!("typeof {} === 'boolean'", var),
148 SchemaKind::Type(Type::Array(_)) => format!("Array.isArray({})", var),
149 SchemaKind::Type(Type::Object(_)) => {
150 format!("typeof {} === 'object' && {} !== null", var, var)
151 }
152 _ => String::new(),
153 }
154 }
155
156 fn format_regex(format: &str) -> Option<&'static str> {
158 match format {
159 "email" => Some(r#"/^[^\s@]+@[^\s@]+\.[^\s@]+$/"#),
160 "uuid" => Some(r#"/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i"#),
161 "date" => Some(r#"/^\d{4}-\d{2}-\d{2}$/"#),
162 "date-time" => Some(r#"/^\d{4}-\d{2}-\d{2}T/"#),
163 "uri" | "url" => Some(r#"/^https?:\/\//"#),
164 "ipv4" => Some(r#"/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/"#),
165 "ipv6" => Some(r#"/^[0-9a-fA-F:]+$/"#),
166 _ => None,
167 }
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use openapiv3::*;
175
176 fn string_schema() -> Schema {
177 Schema {
178 schema_data: SchemaData::default(),
179 schema_kind: SchemaKind::Type(Type::String(StringType::default())),
180 }
181 }
182
183 fn integer_schema() -> Schema {
184 Schema {
185 schema_data: SchemaData::default(),
186 schema_kind: SchemaKind::Type(Type::Integer(IntegerType::default())),
187 }
188 }
189
190 #[test]
191 fn test_string_validation() {
192 let js = SchemaValidatorGenerator::generate_validation(&string_schema());
193 assert!(js.contains("typeof body === 'string'"));
194 }
195
196 #[test]
197 fn test_integer_validation() {
198 let js = SchemaValidatorGenerator::generate_validation(&integer_schema());
199 assert!(js.contains("typeof body === 'number'"));
200 assert!(js.contains("Number.isInteger(body)"));
201 }
202
203 #[test]
204 fn test_boolean_validation() {
205 let schema = Schema {
206 schema_data: SchemaData::default(),
207 schema_kind: SchemaKind::Type(Type::Boolean(BooleanType::default())),
208 };
209 let js = SchemaValidatorGenerator::generate_validation(&schema);
210 assert_eq!(js, "typeof body === 'boolean'");
211 }
212
213 #[test]
214 fn test_object_validation() {
215 let mut obj = ObjectType::default();
216 obj.required.push("name".to_string());
217 obj.properties
218 .insert("name".to_string(), ReferenceOr::Item(Box::new(string_schema())));
219 obj.properties
220 .insert("age".to_string(), ReferenceOr::Item(Box::new(integer_schema())));
221
222 let schema = Schema {
223 schema_data: SchemaData::default(),
224 schema_kind: SchemaKind::Type(Type::Object(obj)),
225 };
226
227 let js = SchemaValidatorGenerator::generate_validation(&schema);
228 assert!(js.contains("typeof body === 'object'"));
229 assert!(js.contains("'name' in body"));
230 assert!(js.contains("typeof body['name'] === 'string'"));
231 assert!(js.contains("body['age'] === undefined || typeof body['age'] === 'number'"));
233 }
234
235 #[test]
236 fn test_array_validation() {
237 let arr = ArrayType {
238 items: Some(ReferenceOr::Item(Box::new(string_schema()))),
239 min_items: None,
240 max_items: None,
241 unique_items: false,
242 };
243
244 let schema = Schema {
245 schema_data: SchemaData::default(),
246 schema_kind: SchemaKind::Type(Type::Array(arr)),
247 };
248
249 let js = SchemaValidatorGenerator::generate_validation(&schema);
250 assert!(js.contains("Array.isArray(body)"));
251 assert!(js.contains("typeof body[0] === 'string'"));
252 }
253
254 #[test]
255 fn test_format_regex() {
256 assert!(SchemaValidatorGenerator::format_regex("email").is_some());
257 assert!(SchemaValidatorGenerator::format_regex("uuid").is_some());
258 assert!(SchemaValidatorGenerator::format_regex("date").is_some());
259 assert!(SchemaValidatorGenerator::format_regex("date-time").is_some());
260 assert!(SchemaValidatorGenerator::format_regex("uri").is_some());
261 assert!(SchemaValidatorGenerator::format_regex("ipv4").is_some());
262 assert!(SchemaValidatorGenerator::format_regex("ipv6").is_some());
263 assert!(SchemaValidatorGenerator::format_regex("unknown").is_none());
264 }
265
266 #[test]
267 fn test_string_with_date_format() {
268 let schema = Schema {
269 schema_data: SchemaData::default(),
270 schema_kind: SchemaKind::Type(Type::String(StringType {
271 format: VariantOrUnknownOrEmpty::Item(StringFormat::Date),
272 ..Default::default()
273 })),
274 };
275
276 let js = SchemaValidatorGenerator::generate_validation(&schema);
277 assert!(js.contains("typeof body === 'string'"));
278 assert!(js.contains(".match("));
279 }
280
281 #[test]
282 fn test_integer_with_range() {
283 let int = IntegerType {
284 minimum: Some(0),
285 maximum: Some(100),
286 ..Default::default()
287 };
288
289 let schema = Schema {
290 schema_data: SchemaData::default(),
291 schema_kind: SchemaKind::Type(Type::Integer(int)),
292 };
293
294 let js = SchemaValidatorGenerator::generate_validation(&schema);
295 assert!(js.contains("body >= 0"));
296 assert!(js.contains("body <= 100"));
297 }
298
299 #[test]
300 fn test_number_validation() {
301 let num = NumberType {
302 minimum: Some(0.0),
303 maximum: Some(99.9),
304 ..Default::default()
305 };
306
307 let schema = Schema {
308 schema_data: SchemaData::default(),
309 schema_kind: SchemaKind::Type(Type::Number(num)),
310 };
311
312 let js = SchemaValidatorGenerator::generate_validation(&schema);
313 assert!(js.contains("typeof body === 'number'"));
314 assert!(js.contains("body >= 0"));
315 assert!(js.contains("body <= 99.9"));
316 }
317}