1use utoipa::openapi::RefOr;
4use utoipa::openapi::path::Parameter;
5use utoipa::openapi::path::{ParameterBuilder, ParameterIn};
6
7pub fn extract_parameters_from_schema(
9 param_schema: &serde_json::Value,
10 route_path: &str,
11) -> Result<Vec<RefOr<Parameter>>, String> {
12 let mut parameters = Vec::new();
13
14 let path_params = extract_path_param_names(route_path);
15
16 let properties = param_schema
17 .get("properties")
18 .and_then(|p| p.as_object())
19 .ok_or_else(|| "Parameter schema missing 'properties' field".to_string())?;
20
21 let required = param_schema
22 .get("required")
23 .and_then(|r| r.as_array())
24 .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>())
25 .unwrap_or_default();
26
27 for (name, schema) in properties {
28 let is_required = required.contains(&name.as_str());
29 let param_in = if path_params.contains(&name.as_str()) {
30 ParameterIn::Path
31 } else {
32 ParameterIn::Query
33 };
34
35 let openapi_schema = crate::openapi::schema_conversion::json_value_to_schema(schema)?;
36
37 let is_path_param = matches!(param_in, ParameterIn::Path);
38
39 let param = ParameterBuilder::new()
40 .name(name)
41 .parameter_in(param_in)
42 .required(if is_path_param || is_required {
43 utoipa::openapi::Required::True
44 } else {
45 utoipa::openapi::Required::False
46 })
47 .schema(Some(openapi_schema))
48 .build();
49
50 parameters.push(RefOr::T(param));
51 }
52
53 Ok(parameters)
54}
55
56pub fn extract_path_param_names(route: &str) -> Vec<&str> {
58 route
59 .split('/')
60 .filter_map(|segment| {
61 if segment.starts_with('{') && segment.ends_with('}') {
62 Some(&segment[1..segment.len() - 1])
63 } else {
64 None
65 }
66 })
67 .collect()
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use serde_json::json;
74
75 #[test]
76 fn test_extract_path_param_names() {
77 let names = extract_path_param_names("/users/{id}/posts/{post_id}");
78 assert_eq!(names, vec!["id", "post_id"]);
79
80 let names = extract_path_param_names("/users");
81 assert_eq!(names, Vec::<&str>::new());
82
83 let names = extract_path_param_names("/users/{user_id}");
84 assert_eq!(names, vec!["user_id"]);
85 }
86
87 #[test]
88 fn test_extract_parameters_from_schema_path_params() {
89 let param_schema = json!({
90 "type": "object",
91 "properties": {
92 "user_id": { "type": "integer" },
93 "post_id": { "type": "integer" }
94 },
95 "required": ["user_id", "post_id"]
96 });
97
98 let result = extract_parameters_from_schema(¶m_schema, "/users/{user_id}/posts/{post_id}");
99 assert!(result.is_ok());
100
101 let params = result.unwrap();
102 assert_eq!(params.len(), 2);
103
104 for param in params {
105 if let RefOr::T(p) = param {
106 assert!(matches!(p.parameter_in, ParameterIn::Path));
107 assert!(matches!(p.required, utoipa::openapi::Required::True));
108 }
109 }
110 }
111
112 #[test]
113 fn test_extract_parameters_from_schema_query_params() {
114 let param_schema = json!({
115 "type": "object",
116 "properties": {
117 "page": { "type": "integer" },
118 "limit": { "type": "integer" },
119 "search": { "type": "string" }
120 },
121 "required": ["page"]
122 });
123
124 let result = extract_parameters_from_schema(¶m_schema, "/users");
125 assert!(result.is_ok());
126
127 let params = result.unwrap();
128 assert_eq!(params.len(), 3);
129
130 for param in ¶ms {
131 if let RefOr::T(p) = param {
132 assert!(matches!(p.parameter_in, ParameterIn::Query));
133 }
134 }
135
136 for param in params {
137 if let RefOr::T(p) = param {
138 if p.name == "page" {
139 assert!(matches!(p.required, utoipa::openapi::Required::True));
140 } else {
141 assert!(matches!(p.required, utoipa::openapi::Required::False));
142 }
143 }
144 }
145 }
146
147 #[test]
148 fn test_extract_parameters_from_schema_mixed() {
149 let param_schema = json!({
150 "type": "object",
151 "properties": {
152 "user_id": { "type": "integer" },
153 "page": { "type": "integer" },
154 "limit": { "type": "integer" }
155 },
156 "required": ["user_id"]
157 });
158
159 let result = extract_parameters_from_schema(¶m_schema, "/users/{user_id}");
160 assert!(result.is_ok());
161
162 let params = result.unwrap();
163 assert_eq!(params.len(), 3);
164
165 for param in params {
166 if let RefOr::T(p) = param {
167 if p.name == "user_id" {
168 assert!(matches!(p.parameter_in, ParameterIn::Path));
169 assert!(matches!(p.required, utoipa::openapi::Required::True));
170 } else {
171 assert!(matches!(p.parameter_in, ParameterIn::Query));
172 assert!(matches!(p.required, utoipa::openapi::Required::False));
173 }
174 }
175 }
176 }
177
178 #[test]
179 fn test_extract_parameters_error_on_missing_properties() {
180 let param_schema = json!({
181 "type": "object"
182 });
183
184 let result = extract_parameters_from_schema(¶m_schema, "/users");
185 assert!(result.is_err());
186 if let Err(err) = result {
187 assert!(err.contains("properties"));
188 }
189 }
190
191 #[test]
192 fn test_extract_parameters_with_format_specifiers() {
193 let param_schema = json!({
194 "type": "object",
195 "properties": {
196 "user_id": { "type": "string", "format": "uuid" },
197 "created_at": { "type": "string", "format": "date-time" },
198 "birth_date": { "type": "string", "format": "date" },
199 "email": { "type": "string", "format": "email" },
200 "website": { "type": "string", "format": "uri" }
201 },
202 "required": ["user_id"]
203 });
204
205 let result = extract_parameters_from_schema(¶m_schema, "/users");
206 assert!(result.is_ok());
207
208 let params: Vec<RefOr<Parameter>> = result.unwrap();
209 assert_eq!(params.len(), 5);
210
211 for param in params {
212 if let RefOr::T(p) = param {
213 assert!(matches!(p.parameter_in, ParameterIn::Query));
214 if p.name == "user_id" {
215 assert!(matches!(p.required, utoipa::openapi::Required::True));
216 } else {
217 assert!(matches!(p.required, utoipa::openapi::Required::False));
218 }
219 }
220 }
221 }
222
223 #[test]
224 fn test_extract_parameters_with_nullable_optional() {
225 let param_schema = json!({
226 "type": "object",
227 "properties": {
228 "search": { "type": "string" },
229 "filter": { "type": "string" }
230 },
231 "required": []
232 });
233
234 let result = extract_parameters_from_schema(¶m_schema, "/items");
235 assert!(result.is_ok());
236
237 let params: Vec<RefOr<Parameter>> = result.unwrap();
238 assert_eq!(params.len(), 2);
239
240 for param in params {
241 if let RefOr::T(p) = param {
242 assert!(matches!(p.required, utoipa::openapi::Required::False));
243 }
244 }
245 }
246
247 #[test]
248 fn test_extract_parameters_array_parameter() {
249 let param_schema = json!({
250 "type": "object",
251 "properties": {
252 "tags": {
253 "type": "array",
254 "items": { "type": "string" }
255 },
256 "ids": {
257 "type": "array",
258 "items": { "type": "integer" }
259 }
260 },
261 "required": ["tags"]
262 });
263
264 let result = extract_parameters_from_schema(¶m_schema, "/search");
265 assert!(result.is_ok());
266
267 let params: Vec<RefOr<Parameter>> = result.unwrap();
268 assert_eq!(params.len(), 2);
269
270 for param in params {
271 if let RefOr::T(p) = param {
272 if p.name == "tags" {
273 assert!(matches!(p.required, utoipa::openapi::Required::True));
274 } else if p.name == "ids" {
275 assert!(matches!(p.required, utoipa::openapi::Required::False));
276 }
277 }
278 }
279 }
280
281 #[test]
282 fn test_extract_parameters_empty_properties() {
283 let param_schema = json!({
284 "type": "object",
285 "properties": {},
286 "required": []
287 });
288
289 let result = extract_parameters_from_schema(¶m_schema, "/items");
290 assert!(result.is_ok());
291
292 let params: Vec<RefOr<Parameter>> = result.unwrap();
293 assert_eq!(params.len(), 0);
294 }
295
296 #[test]
297 fn test_extract_parameters_with_multiple_path_params() {
298 let param_schema = json!({
299 "type": "object",
300 "properties": {
301 "org_id": { "type": "string" },
302 "team_id": { "type": "string" },
303 "member_id": { "type": "string" },
304 "page": { "type": "integer" }
305 },
306 "required": ["org_id", "team_id", "member_id"]
307 });
308
309 let result =
310 extract_parameters_from_schema(¶m_schema, "/orgs/{org_id}/teams/{team_id}/members/{member_id}");
311 assert!(result.is_ok());
312
313 let params: Vec<RefOr<Parameter>> = result.unwrap();
314 assert_eq!(params.len(), 4);
315
316 let mut path_count: i32 = 0;
317 let mut query_count: i32 = 0;
318
319 for param in params {
320 if let RefOr::T(p) = param {
321 if matches!(p.parameter_in, ParameterIn::Path) {
322 path_count += 1;
323 assert!(matches!(p.required, utoipa::openapi::Required::True));
324 } else {
325 query_count += 1;
326 }
327 }
328 }
329
330 assert_eq!(path_count, 3);
331 assert_eq!(query_count, 1);
332 }
333
334 #[test]
335 fn test_extract_parameters_with_numeric_types() {
336 let param_schema = json!({
337 "type": "object",
338 "properties": {
339 "count": { "type": "integer" },
340 "score": { "type": "number" },
341 "active": { "type": "boolean" }
342 },
343 "required": ["count"]
344 });
345
346 let result = extract_parameters_from_schema(¶m_schema, "/stats");
347 assert!(result.is_ok());
348
349 let params: Vec<RefOr<Parameter>> = result.unwrap();
350 assert_eq!(params.len(), 3);
351
352 for param in params {
353 if let RefOr::T(p) = param {
354 assert!(matches!(p.parameter_in, ParameterIn::Query));
355 if p.name == "count" {
356 assert!(matches!(p.required, utoipa::openapi::Required::True));
357 }
358 }
359 }
360 }
361
362 #[test]
363 fn test_extract_parameters_required_field_parsing() {
364 let param_schema = json!({
365 "type": "object",
366 "properties": {
367 "id": { "type": "integer" },
368 "name": { "type": "string" },
369 "email": { "type": "string" },
370 "age": { "type": "integer" }
371 },
372 "required": ["id", "name"]
373 });
374
375 let result = extract_parameters_from_schema(¶m_schema, "/items");
376 assert!(result.is_ok());
377
378 let params: Vec<RefOr<Parameter>> = result.unwrap();
379 assert_eq!(params.len(), 4);
380
381 let required_names: Vec<&str> = vec!["id", "name"];
382
383 for param in params {
384 if let RefOr::T(p) = param {
385 if required_names.contains(&p.name.as_str()) {
386 assert!(matches!(p.required, utoipa::openapi::Required::True));
387 } else {
388 assert!(matches!(p.required, utoipa::openapi::Required::False));
389 }
390 }
391 }
392 }
393
394 #[test]
395 fn test_extract_parameters_single_path_param_override_required() {
396 let param_schema = json!({
397 "type": "object",
398 "properties": {
399 "id": { "type": "integer" },
400 "query": { "type": "string" }
401 },
402 "required": ["query"]
403 });
404
405 let result = extract_parameters_from_schema(¶m_schema, "/items/{id}");
406 assert!(result.is_ok());
407
408 let params: Vec<RefOr<Parameter>> = result.unwrap();
409 assert_eq!(params.len(), 2);
410
411 for param in params {
412 if let RefOr::T(p) = param {
413 if p.name == "id" {
414 assert!(matches!(p.parameter_in, ParameterIn::Path));
415 assert!(matches!(p.required, utoipa::openapi::Required::True));
416 } else if p.name == "query" {
417 assert!(matches!(p.parameter_in, ParameterIn::Query));
418 assert!(matches!(p.required, utoipa::openapi::Required::True));
419 }
420 }
421 }
422 }
423
424 #[test]
425 fn test_extract_parameters_nested_object_schema() {
426 let param_schema = json!({
427 "type": "object",
428 "properties": {
429 "filter": {
430 "type": "object",
431 "properties": {
432 "status": { "type": "string" },
433 "priority": { "type": "integer" }
434 },
435 "required": ["status"]
436 }
437 },
438 "required": ["filter"]
439 });
440
441 let result = extract_parameters_from_schema(¶m_schema, "/tasks");
442 assert!(result.is_ok());
443
444 let params: Vec<RefOr<Parameter>> = result.unwrap();
445 assert_eq!(params.len(), 1);
446
447 if let Some(RefOr::T(p)) = params.first() {
448 assert_eq!(p.name, "filter");
449 assert!(matches!(p.parameter_in, ParameterIn::Query));
450 assert!(matches!(p.required, utoipa::openapi::Required::True));
451 }
452 }
453
454 #[test]
455 fn test_extract_parameters_with_special_characters_in_names() {
456 let param_schema = json!({
457 "type": "object",
458 "properties": {
459 "user_id": { "type": "string" },
460 "api_key": { "type": "string" },
461 "x_custom_header": { "type": "string" }
462 },
463 "required": ["user_id"]
464 });
465
466 let result = extract_parameters_from_schema(¶m_schema, "/data");
467 assert!(result.is_ok());
468
469 let params: Vec<RefOr<Parameter>> = result.unwrap();
470 assert_eq!(params.len(), 3);
471
472 let param_names: Vec<String> = params
473 .iter()
474 .filter_map(|p| match p {
475 RefOr::T(param) => Some(param.name.clone()),
476 RefOr::Ref(_) => None,
477 })
478 .collect();
479
480 assert!(param_names.contains(&"user_id".to_string()));
481 assert!(param_names.contains(&"api_key".to_string()));
482 assert!(param_names.contains(&"x_custom_header".to_string()));
483 }
484
485 #[test]
486 fn test_extract_parameters_with_mismatched_required_field() {
487 let param_schema = json!({
488 "type": "object",
489 "properties": {
490 "id": { "type": "integer" },
491 "name": { "type": "string" }
492 },
493 "required": ["id", "nonexistent_field"]
494 });
495
496 let result = extract_parameters_from_schema(¶m_schema, "/items");
497 assert!(result.is_ok());
498
499 let params: Vec<RefOr<Parameter>> = result.unwrap();
500 assert_eq!(params.len(), 2);
501
502 for param in params {
503 if let RefOr::T(p) = param {
504 if p.name == "id" {
505 assert!(matches!(p.required, utoipa::openapi::Required::True));
506 }
507 }
508 }
509 }
510
511 #[test]
512 fn test_extract_path_param_names_with_special_segments() {
513 let names: Vec<&str> =
514 extract_path_param_names("/api/v1/users/{user_id}/posts/{post_id}/comments/{comment_id}");
515 assert_eq!(names, vec!["user_id", "post_id", "comment_id"]);
516 }
517
518 #[test]
519 fn test_extract_path_param_names_no_params() {
520 let names: Vec<&str> = extract_path_param_names("/api/users/list");
521 assert!(names.is_empty());
522 }
523
524 #[test]
525 fn test_extract_path_param_names_single_param_end() {
526 let names: Vec<&str> = extract_path_param_names("/resource/{id}");
527 assert_eq!(names, vec!["id"]);
528 }
529
530 #[test]
531 fn test_extract_path_param_names_numeric_param_names() {
532 let names: Vec<&str> = extract_path_param_names("/items/{id1}/sub/{id2}");
533 assert_eq!(names, vec!["id1", "id2"]);
534 }
535}