1use std::collections::BTreeMap;
4use serde::de::DeserializeOwned;
5use serde::Serialize;
6use serde_json::{json, Value};
7
8use mime::{
9 APPLICATION_JSON,
10 APPLICATION_OCTET_STREAM,
11 APPLICATION_WWW_FORM_URLENCODED,
12 MULTIPART_FORM_DATA,
13 TEXT_EVENT_STREAM,
14 TEXT_PLAIN_UTF_8
15};
16
17use super::{
18 schema::{OpenApiSchema, Probe},
19 param::OpenApiParameter,
20 op::OpenApiOperation,
21};
22
23#[derive(Clone, Debug, Default)]
25pub struct OpenApiRouteConfig {
26 tags: Vec<String>,
27 docs: Option<Vec<String>>,
28 summary: Option<String>,
29 description: Option<String>,
30 operation_id: Option<String>,
31 request_schema: Option<OpenApiSchema>,
32 response_schema: Option<Option<OpenApiSchema>>,
33 request_example: Option<Value>,
34 response_example: Option<Option<Value>>,
35 request_content_type: Option<String>,
36 response_content_type: Option<Option<String>>,
37 extra_parameters: Vec<OpenApiParameter>,
38}
39
40impl OpenApiRouteConfig {
41 pub(crate) fn docs(&self) -> Option<&[String]> {
43 self.docs.as_deref()
44 }
45
46 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
48 self.tags.push(tag.into());
49 self
50 }
51
52 pub fn with_tags<I, T>(mut self, tags: I) -> Self
54 where
55 I: IntoIterator<Item = T>,
56 T: Into<String>,
57 {
58 self.tags.extend(tags.into_iter().map(Into::into));
59 self
60 }
61
62 pub fn with_doc(mut self, doc: impl Into<String>) -> Self {
64 self.docs.get_or_insert_with(Vec::new).push(doc.into());
65 self
66 }
67
68 pub fn with_docs<I, S>(mut self, docs: I) -> Self
70 where
71 I: IntoIterator<Item = S>,
72 S: Into<String>,
73 {
74 self.docs
75 .get_or_insert_with(Vec::new)
76 .extend(docs.into_iter().map(Into::into));
77 self
78 }
79
80 pub fn with_summary(mut self, summary: impl Into<String>) -> Self {
82 self.summary = Some(summary.into());
83 self
84 }
85
86 pub fn with_description(mut self, description: impl Into<String>) -> Self {
88 self.description = Some(description.into());
89 self
90 }
91
92 pub fn with_operation_id(mut self, operation_id: impl Into<String>) -> Self {
94 self.operation_id = Some(operation_id.into());
95 self
96 }
97
98 pub fn with_request_schema(mut self, schema: OpenApiSchema) -> Self {
100 self.request_schema = Some(schema);
101 self
102 }
103
104 pub fn with_response_schema(mut self, schema: OpenApiSchema) -> Self {
106 self.response_schema = Some(Some(schema));
107 self
108 }
109
110 pub fn consumes_named_path<T: DeserializeOwned>(self) -> Self {
112 self.with_named_path_parameters_from_deserialize::<T>()
113 }
114
115 pub fn consumes_query<T: DeserializeOwned>(self) -> Self {
117 self.with_query_parameters_from_deserialize::<T>()
118 }
119
120 pub fn consumes_json<T: DeserializeOwned>(self) -> Self {
122 self.with_request_type_from_deserialize::<T>(APPLICATION_JSON.as_ref())
123 }
124
125 pub fn consumes_form<T: DeserializeOwned>(self) -> Self {
127 self.with_request_type_from_deserialize::<T>(APPLICATION_WWW_FORM_URLENCODED.as_ref())
128 }
129
130 pub fn consumes_multipart(self) -> Self {
132 self.with_multipart_request()
133 }
134
135 pub fn consumes_stream(self) -> Self {
137 self.with_stream_request()
138 }
139
140 pub fn produces_text(self) -> Self {
142 self.with_text_response()
143 }
144
145 pub fn produces_no_schema(self) -> Self {
147 self.with_empty_response()
148 }
149
150 pub fn produces_json<T: Serialize + Default>(self) -> Self {
152 self.produces_json_example(T::default())
153 }
154
155 pub fn produces_empty_json(self) -> Self {
157 self.with_json_response()
158 }
159
160 pub fn produces_form<T: Serialize + Default>(self) -> Self {
162 self.produces_form_example(T::default())
163 }
164
165 pub fn produces_empty_form(self) -> Self {
167 self.with_form_response()
168 }
169
170 pub fn produces_stream(self) -> Self {
172 self.with_stream_response()
173 }
174
175 pub fn produces_sse(self) -> Self {
177 self.with_sse_response()
178 }
179
180 pub fn produces_json_example<T: Serialize>(mut self, example: T) -> Self {
182 let example = serde_json::to_value(example).unwrap_or_else(|_| json!({}));
183
184 self.response_schema = Some(Some(OpenApiSchema::from_example(&example)));
185 self.response_example = Some(Some(example));
186 self.response_content_type = Some(Some(APPLICATION_JSON.to_string()));
187
188 self
189 }
190
191 pub fn produces_form_example<T: Serialize>(mut self, example: T) -> Self {
193 let example = serde_json::to_value(example).unwrap_or_else(|_| json!({}));
194
195 let encoded = match &example {
196 Value::Object(map) => serde_urlencoded::to_string(map).unwrap_or_default(),
197 _ => String::new(),
198 };
199
200 self.response_schema = Some(Some(OpenApiSchema::string()));
201 self.response_example = Some(Some(Value::String(encoded)));
202
203 self.response_content_type = Some(Some(APPLICATION_WWW_FORM_URLENCODED.to_string()));
204
205 self
206 }
207
208 fn with_multipart_request(mut self) -> Self {
210 self.request_schema
211 .get_or_insert_with(OpenApiSchema::multipart);
212 self.request_content_type = Some(MULTIPART_FORM_DATA.to_string());
213 self
214 }
215
216 fn with_stream_request(mut self) -> Self {
218 self.request_schema
219 .get_or_insert_with(OpenApiSchema::binary);
220 self.request_content_type = Some(APPLICATION_OCTET_STREAM.to_string());
221 self
222 }
223
224 fn with_json_response(mut self) -> Self {
226 self.response_schema
227 .get_or_insert_with(|| Some(OpenApiSchema::object()));
228 self.response_content_type = Some(Some(APPLICATION_JSON.to_string()));
229 self
230 }
231
232 fn with_form_response(mut self) -> Self {
234 self.response_schema
235 .get_or_insert_with(|| Some(OpenApiSchema::object()));
236 self.response_content_type = Some(Some(APPLICATION_WWW_FORM_URLENCODED.to_string()));
237 self
238 }
239
240 fn with_text_response(mut self) -> Self {
242 self.response_schema
243 .get_or_insert_with(|| Some(OpenApiSchema::string()));
244 self.response_content_type = Some(Some(TEXT_PLAIN_UTF_8.to_string()));
245 self
246 }
247
248 fn with_empty_response(mut self) -> Self {
250 self.response_schema = Some(None);
251 self.response_content_type = Some(None);
252 self.response_example = Some(None);
253 self
254 }
255
256 fn with_sse_response(mut self) -> Self {
258 self.response_schema
259 .get_or_insert_with(|| Some(OpenApiSchema::string().with_title("SSE stream")));
260 self.response_content_type = Some(Some(TEXT_EVENT_STREAM.to_string()));
261 self
262 }
263
264 fn with_stream_response(mut self) -> Self {
266 self.response_schema
267 .get_or_insert_with(|| Some(OpenApiSchema::binary()));
268 self.response_content_type = Some(Some(APPLICATION_OCTET_STREAM.to_string()));
269 self
270 }
271
272 fn with_query_parameter(mut self, name: String, schema: OpenApiSchema, required: bool) -> Self {
273 self.extra_parameters.push(OpenApiParameter {
274 name,
275 location: "query".to_string(),
276 required,
277 schema,
278 });
279 self
280 }
281
282 fn with_path_parameter(mut self, name: String, schema: OpenApiSchema) -> Self {
283 self.extra_parameters.push(OpenApiParameter {
284 name,
285 location: "path".to_string(),
286 required: true,
287 schema,
288 });
289 self
290 }
291
292 fn with_request_type_from_deserialize<T: DeserializeOwned>(
294 mut self,
295 content_type: &str,
296 ) -> Self {
297 if let Some((schema, example)) = schema_and_example_from_deserialize::<T>() {
298 self.request_schema = Some(schema.with_title(type_display_name::<T>()));
299 self.request_example = Some(example);
300 }
301 self.request_content_type = Some(content_type.to_string());
302 self
303 }
304
305 fn with_query_parameters_from_deserialize<T: DeserializeOwned>(mut self) -> Self {
306 if let Some((schema, _)) = schema_and_example_from_deserialize::<T>()
307 && let Some(properties) = schema.properties
308 {
309 let required = schema.required.unwrap_or_default();
310
311 for (name, property_schema) in properties {
312 let is_required = required.iter().any(|f| f == &name);
313 self = self.with_query_parameter(name, property_schema, is_required);
314 }
315 }
316 self
317 }
318
319 fn with_named_path_parameters_from_deserialize<T: DeserializeOwned>(mut self) -> Self {
320 if let Some((schema, _)) = schema_and_example_from_deserialize::<T>()
321 && let Some(properties) = schema.properties
322 {
323 for (name, property_schema) in properties {
324 self = self.with_path_parameter(name, property_schema);
325 }
326 }
327 self
328 }
329
330 pub fn merge(mut self, other: &Self) -> Self {
332 if self.tags.is_empty() {
333 self.tags = other.tags.clone();
334 } else {
335 self.tags.extend(other.tags.clone());
336 }
337 if self.summary.is_none() {
338 self.summary = other.summary.clone();
339 }
340 if self.description.is_none() {
341 self.description = other.description.clone();
342 }
343 if self.operation_id.is_none() {
344 self.operation_id = other.operation_id.clone();
345 }
346 if self.request_schema.is_none() {
347 self.request_schema = other.request_schema.clone();
348 }
349 if self.response_schema.is_none() {
350 self.response_schema = other.response_schema.clone();
351 }
352 if self.request_example.is_none() {
353 self.request_example = other.request_example.clone();
354 }
355 if self.response_example.is_none() {
356 self.response_example = other.response_example.clone();
357 }
358 if self.request_content_type.is_none() {
359 self.request_content_type = other.request_content_type.clone();
360 }
361 if self.response_content_type.is_none() {
362 self.response_content_type = other.response_content_type.clone();
363 }
364 if !other.extra_parameters.is_empty() {
365 self.extra_parameters.extend(other.extra_parameters.clone());
366 }
367 match (&mut self.docs, &other.docs) {
368 (None, Some(d)) => self.docs = Some(d.clone()),
369 (Some(dst), Some(src)) => {
370 for s in src {
371 if !dst.iter().any(|x| x == s) {
372 dst.push(s.clone());
373 }
374 }
375 }
376 _ => {}
377 }
378 self
379 }
380
381 pub(super) fn apply_to_operation(
382 &self,
383 operation: &mut OpenApiOperation,
384 schemas: &mut BTreeMap<String, OpenApiSchema>
385 ) {
386 if let Some(summary) = &self.summary {
387 operation.summary = Some(summary.clone());
388 }
389 if let Some(description) = &self.description {
390 operation.description = Some(description.clone());
391 }
392 if let Some(operation_id) = &self.operation_id {
393 operation.operation_id = Some(operation_id.clone());
394 }
395
396 if !self.tags.is_empty() {
397 operation.tags = Some(self.tags.clone());
398 }
399
400 if self.request_schema.is_some() || self.request_example.is_some() {
401 let mut schema = self.request_schema.clone().unwrap_or_else(OpenApiSchema::object);
402 let example = self.request_example.clone();
403 let content_type = self.request_content_type.as_deref().unwrap_or(APPLICATION_JSON.as_ref());
404
405 schema = intern_schema_if_object_named(schema, schemas);
406 operation.set_request_body(schema, example, content_type);
407 }
408
409 let touched = self.response_schema.is_some()
410 || self.response_example.is_some()
411 || self.response_content_type.is_some();
412
413 if touched {
414 let schema_clear = matches!(self.response_schema, Some(None));
415 let ct_clear = matches!(self.response_content_type, Some(None));
416 let example_clear = matches!(self.response_example, Some(None));
417
418 if schema_clear || ct_clear || example_clear {
419 operation.clear_response_body();
420 } else {
421 let mut schema = match &self.response_schema {
422 Some(Some(s)) => s.clone(),
423 _ => OpenApiSchema::object(),
424 };
425
426 let example: Option<Value> = match &self.response_example {
427 Some(Some(v)) => Some(v.clone()),
428 _ => None,
429 };
430
431 let content_type: &str = match &self.response_content_type {
432 Some(Some(ct)) => ct.as_str(),
433 _ => APPLICATION_JSON.as_ref(),
434 };
435
436 schema = intern_schema_if_object_named(schema, schemas);
437 operation.set_response_body(schema, example, content_type);
438 }
439 }
440
441 if !self.extra_parameters.is_empty() {
442 let params = operation.parameters.get_or_insert_with(Vec::new);
443
444 for p in &self.extra_parameters {
445 if let Some(existing) = params
446 .iter_mut()
447 .find(|x| x.name == p.name && x.location == p.location)
448 {
449 *existing = p.clone();
450 } else {
451 params.push(p.clone());
452 }
453 }
454 }
455 }
456}
457
458fn intern_schema_if_object_named(
459 mut schema: OpenApiSchema,
460 schemas: &mut BTreeMap<String, OpenApiSchema>
461) -> OpenApiSchema {
462 if schema.schema_ref.is_some() {
463 return schema;
464 }
465
466 let base = match schema.title.clone() {
467 Some(t) if !t.is_empty() => t,
468 _ => return schema,
469 };
470
471 if schema.properties.is_none() {
472 return schema;
473 }
474
475 let mut name = base.clone();
476 if schemas.contains_key(&name) {
477 let mut i = 2;
478 while schemas.contains_key(&format!("{base}_{i}")) {
479 i += 1;
480 }
481 name = format!("{base}_{i}");
482 }
483
484 schema.title = None;
485
486 schemas
487 .entry(name.clone())
488 .or_insert(schema);
489
490 OpenApiSchema::reference(&name)
491}
492
493fn schema_and_example_from_deserialize<T: DeserializeOwned>() -> Option<(OpenApiSchema, Value)> {
494 let mut probe = Probe::new();
495 let _ = T::deserialize(&mut probe);
496 probe.finish()
497}
498
499fn type_display_name<T>() -> String {
500 std::any::type_name::<T>()
501 .rsplit("::")
502 .next()
503 .unwrap_or("Model")
504 .to_string()
505}
506
507#[cfg(test)]
508#[allow(unused)]
509mod tests {
510 use super::OpenApiRouteConfig;
511 use crate::{op::OpenApiOperation, schema::OpenApiSchema};
512 use serde::{Deserialize, Serialize};
513 use serde_json::json;
514 use std::collections::BTreeMap;
515
516 #[derive(Deserialize)]
517 struct Payload {
518 name: String,
519 age: u64,
520 }
521
522 #[derive(Serialize, Default)]
523 struct ResponsePayload {
524 message: String,
525 }
526
527 #[derive(Deserialize)]
528 struct OptionalQuery {
529 required_name: String,
530 optional_age: Option<()>,
531 }
532
533 #[test]
534 fn consumes_query_marks_non_optional_fields_as_required() {
535 let cfg = OpenApiRouteConfig::default().consumes_query::<OptionalQuery>();
536
537 let required_name = cfg
538 .extra_parameters
539 .iter()
540 .find(|p| p.name == "required_name")
541 .expect("required_name param");
542 let optional_age = cfg
543 .extra_parameters
544 .iter()
545 .find(|p| p.name == "optional_age")
546 .expect("optional_age param");
547
548 assert!(required_name.required);
549 assert!(!optional_age.required);
550 }
551
552 #[test]
553 fn infer_request_schema_from_deserialize_type() {
554 let cfg = OpenApiRouteConfig::default()
555 .with_request_type_from_deserialize::<Payload>("application/json");
556
557 let schema = cfg
558 .request_schema
559 .expect("request schema should be inferred");
560 let props = schema.properties.expect("object properties should exist");
561
562 assert!(props.contains_key("name"));
563 assert!(props.contains_key("age"));
564 assert_eq!(
565 cfg.request_content_type.as_deref(),
566 Some("application/json")
567 );
568 }
569
570 #[test]
571 fn infer_query_parameters_from_deserialize_type() {
572 let cfg = OpenApiRouteConfig::default().with_query_parameters_from_deserialize::<Payload>();
573
574 assert_eq!(cfg.extra_parameters.len(), 2);
575 assert!(cfg.extra_parameters.iter().any(|p| p.name == "name"));
576 assert!(cfg.extra_parameters.iter().any(|p| p.name == "age"));
577 assert!(cfg.extra_parameters.iter().all(|p| p.location == "query"));
578 }
579
580 #[test]
581 fn merge_combines_docs_and_preserves_existing_values() {
582 let base = OpenApiRouteConfig::default()
583 .with_tag("base")
584 .with_summary("base summary")
585 .with_doc("v1");
586 let other = OpenApiRouteConfig::default()
587 .with_tag("extra")
588 .with_summary("other summary")
589 .with_doc("v1")
590 .with_doc("admin");
591
592 let merged = base.merge(&other);
593
594 assert_eq!(merged.summary.as_deref(), Some("base summary"));
595 assert_eq!(merged.tags.as_slice(), ["base", "extra"]);
596 assert_eq!(merged.docs.expect("docs"), vec!["v1", "admin"]);
597 }
598
599 #[test]
600 fn apply_to_operation_adds_content_and_interns_named_schema() {
601 let cfg = OpenApiRouteConfig::default()
602 .with_request_schema(
603 OpenApiSchema::object()
604 .with_title("CreateUser")
605 .with_property("name", OpenApiSchema::string()),
606 )
607 .with_response_schema(
608 OpenApiSchema::object()
609 .with_title("User")
610 .with_property("id", OpenApiSchema::integer()),
611 )
612 .produces_json_example(ResponsePayload {
613 message: "ok".to_string(),
614 })
615 .with_summary("create user")
616 .with_operation_id("createUser");
617
618 let mut op = OpenApiOperation::default();
619 let mut schemas = BTreeMap::new();
620
621 cfg.apply_to_operation(&mut op, &mut schemas);
622
623 let op_json = serde_json::to_value(op).expect("serialize operation");
624 assert_eq!(op_json["summary"], "create user");
625 assert_eq!(op_json["operationId"], "createUser");
626 assert!(
627 op_json["requestBody"]["content"]
628 .get("application/json")
629 .is_some()
630 );
631 assert_eq!(
632 op_json["responses"]["200"]["content"]["application/json"]["example"],
633 json!({"message":"ok"})
634 );
635
636 assert!(schemas.contains_key("CreateUser"));
637 }
638
639 #[test]
640 fn produces_no_schema_clears_content() {
641 let cfg = OpenApiRouteConfig::default()
642 .produces_json::<ResponsePayload>()
643 .produces_no_schema();
644
645 let mut op = OpenApiOperation::default();
646 cfg.apply_to_operation(&mut op, &mut BTreeMap::new());
647
648 let op_json = serde_json::to_value(op).expect("serialize operation");
649 assert!(
650 op_json["responses"]["200"]
651 .get("content")
652 .is_none()
653 );
654 }
655
656 #[test]
657 fn with_tags_extends_existing_tags() {
658 let cfg = OpenApiRouteConfig::default()
659 .with_tag("base")
660 .with_tags(["one", "two"]);
661
662 assert_eq!(cfg.tags, vec!["base", "one", "two"]);
663 }
664
665 #[test]
666 fn with_docs_initializes_and_extends_docs() {
667 let cfg = OpenApiRouteConfig::default()
668 .with_docs(["v1", "admin"])
669 .with_doc("internal");
670
671 assert_eq!(cfg.docs.expect("docs"), vec!["v1", "admin", "internal"]);
672 }
673
674 #[test]
675 fn with_description_sets_operation_description() {
676 let cfg = OpenApiRouteConfig::default().with_description("desc");
677 assert_eq!(cfg.description.as_deref(), Some("desc"));
678 }
679
680 #[test]
681 fn consumes_methods_set_request_content_type() {
682 let query_cfg = OpenApiRouteConfig::default().consumes_query::<Payload>();
683 assert_eq!(query_cfg.extra_parameters.len(), 2);
684
685 let json_cfg = OpenApiRouteConfig::default().consumes_json::<Payload>();
686 assert_eq!(
687 json_cfg.request_content_type.as_deref(),
688 Some("application/json")
689 );
690 assert!(json_cfg.request_schema.is_some());
691
692 let form_cfg = OpenApiRouteConfig::default().consumes_form::<Payload>();
693 assert_eq!(
694 form_cfg.request_content_type.as_deref(),
695 Some("application/x-www-form-urlencoded")
696 );
697 assert!(form_cfg.request_schema.is_some());
698
699 let multipart_cfg = OpenApiRouteConfig::default().consumes_multipart();
700 assert_eq!(
701 multipart_cfg.request_content_type.as_deref(),
702 Some("multipart/form-data")
703 );
704
705 let stream_cfg = OpenApiRouteConfig::default().consumes_stream();
706 assert_eq!(
707 stream_cfg.request_content_type.as_deref(),
708 Some("application/octet-stream")
709 );
710 assert_eq!(
711 stream_cfg.request_schema.expect("schema").format.as_deref(),
712 Some("binary")
713 );
714 }
715
716 #[test]
717 fn produces_helpers_set_expected_response_schema_and_content_type() {
718 let text_cfg = OpenApiRouteConfig::default().produces_text();
719 assert_eq!(
720 text_cfg
721 .response_content_type
722 .as_ref()
723 .and_then(|x| x.as_deref()),
724 Some("text/plain; charset=utf-8")
725 );
726 assert_eq!(
727 text_cfg
728 .response_schema
729 .expect("schema")
730 .expect("schema")
731 .schema_type
732 .as_deref(),
733 Some("string")
734 );
735
736 let empty_json_cfg = OpenApiRouteConfig::default().produces_empty_json();
737 assert_eq!(
738 empty_json_cfg
739 .response_content_type
740 .as_ref()
741 .and_then(|x| x.as_deref()),
742 Some("application/json")
743 );
744 assert_eq!(
745 empty_json_cfg
746 .response_schema
747 .expect("schema")
748 .expect("schema")
749 .schema_type
750 .as_deref(),
751 Some("object")
752 );
753
754 let form_cfg = OpenApiRouteConfig::default().produces_form::<ResponsePayload>();
755 assert_eq!(
756 form_cfg
757 .response_content_type
758 .as_ref()
759 .and_then(|x| x.as_deref()),
760 Some("application/x-www-form-urlencoded")
761 );
762 assert_eq!(
763 form_cfg
764 .response_schema
765 .expect("schema")
766 .expect("schema")
767 .schema_type
768 .as_deref(),
769 Some("string")
770 );
771
772 let empty_form_cfg = OpenApiRouteConfig::default().produces_empty_form();
773 assert_eq!(
774 empty_form_cfg
775 .response_content_type
776 .as_ref()
777 .and_then(|x| x.as_deref()),
778 Some("application/x-www-form-urlencoded")
779 );
780 assert_eq!(
781 empty_form_cfg
782 .response_schema
783 .expect("schema")
784 .expect("schema")
785 .schema_type
786 .as_deref(),
787 Some("object")
788 );
789
790 let stream_cfg = OpenApiRouteConfig::default().produces_stream();
791 assert_eq!(
792 stream_cfg
793 .response_content_type
794 .as_ref()
795 .and_then(|x| x.as_deref()),
796 Some("application/octet-stream")
797 );
798 assert_eq!(
799 stream_cfg
800 .response_schema
801 .expect("schema")
802 .expect("schema")
803 .format
804 .as_deref(),
805 Some("binary")
806 );
807
808 let sse_cfg = OpenApiRouteConfig::default().produces_sse();
809 assert_eq!(
810 sse_cfg
811 .response_content_type
812 .as_ref()
813 .and_then(|x| x.as_deref()),
814 Some("text/event-stream")
815 );
816 assert_eq!(
817 sse_cfg.response_schema
818 .expect("schema")
819 .expect("schema")
820 .title
821 .as_deref(),
822 Some("SSE stream")
823 );
824 }
825
826 #[test]
827 fn produces_form_example_encodes_object_into_string_example() {
828 #[derive(Serialize)]
829 struct FormOut {
830 first: String,
831 second: i32,
832 }
833
834 let cfg = OpenApiRouteConfig::default().produces_form_example(FormOut {
835 first: "hello".to_string(),
836 second: 7,
837 });
838
839 assert_eq!(
840 cfg.response_content_type
841 .as_ref()
842 .and_then(|x| x.as_deref()),
843 Some("application/x-www-form-urlencoded")
844 );
845 assert_eq!(
846 cfg.response_schema
847 .expect("schema")
848 .expect("schema")
849 .schema_type
850 .as_deref(),
851 Some("string")
852 );
853 assert_eq!(
854 cfg.response_example,
855 Some(Some(serde_json::Value::String(
856 "first=hello&second=7".to_string()
857 )))
858 );
859 }
860}