1use crate::extensions::Extensions;
2use once_cell::sync::Lazy;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::BTreeMap;
6
7#[derive(Debug, Clone, Deserialize, Serialize)]
8pub struct OpenApiSpec {
9 pub openapi: String,
10 pub info: Info,
11 #[serde(rename = "jsonSchemaDialect", default)]
12 pub json_schema_dialect: Option<String>,
13 #[serde(default)]
14 pub servers: Option<Vec<Server>>,
15 #[serde(default, deserialize_with = "deserialize_lenient_path_map")]
16 pub paths: Option<BTreeMap<String, PathItem>>,
17 #[serde(default)]
18 pub webhooks: Option<BTreeMap<String, PathItem>>,
19 #[serde(default)]
20 pub components: Option<Components>,
21 #[serde(default)]
22 pub security: Option<Vec<BTreeMap<String, Vec<String>>>>,
23 #[serde(default)]
24 pub tags: Option<Vec<Tag>>,
25 #[serde(rename = "externalDocs", default)]
26 pub external_docs: Option<ExternalDocs>,
27 #[serde(rename = "$self", default)]
29 pub self_uri: Option<String>,
30 #[serde(flatten, default)]
31 pub extensions: Extensions,
32}
33
34fn deserialize_lenient_path_map<'de, D>(
40 deserializer: D,
41) -> Result<Option<BTreeMap<String, PathItem>>, D::Error>
42where
43 D: serde::Deserializer<'de>,
44{
45 let raw = Option::<BTreeMap<String, Value>>::deserialize(deserializer)?;
46 let Some(entries) = raw else {
47 return Ok(None);
48 };
49 let mut paths = BTreeMap::new();
50 for (key, value) in entries {
51 if value.is_object() || !key.starts_with("x-") {
52 let item =
53 serde_json::from_value::<PathItem>(value).map_err(serde::de::Error::custom)?;
54 paths.insert(key, item);
55 }
56 }
57 Ok(Some(paths))
58}
59
60#[derive(Debug, Clone, Deserialize, Serialize)]
61pub struct Info {
62 pub title: String,
63 #[serde(default)]
64 pub summary: Option<String>,
65 #[serde(default)]
66 pub description: Option<String>,
67 #[serde(rename = "termsOfService", default)]
68 pub terms_of_service: Option<String>,
69 #[serde(default)]
70 pub contact: Option<Value>,
71 #[serde(default)]
72 pub license: Option<Value>,
73 #[serde(default)]
74 pub version: Option<String>,
75 #[serde(flatten, default)]
76 pub extensions: Extensions,
77}
78
79#[derive(Debug, Clone, Deserialize, Serialize)]
80pub struct Components {
81 #[serde(default)]
82 pub schemas: Option<BTreeMap<String, Schema>>,
83 #[serde(default)]
84 pub responses: Option<BTreeMap<String, Response>>,
85 #[serde(default)]
86 pub parameters: Option<BTreeMap<String, Parameter>>,
87 #[serde(default)]
88 pub examples: Option<BTreeMap<String, Example>>,
89 #[serde(rename = "requestBodies", default)]
90 pub request_bodies: Option<BTreeMap<String, RequestBody>>,
91 #[serde(default)]
92 pub headers: Option<BTreeMap<String, Header>>,
93 #[serde(rename = "securitySchemes", default)]
94 pub security_schemes: Option<BTreeMap<String, SecurityScheme>>,
95 #[serde(default)]
96 pub links: Option<BTreeMap<String, Link>>,
97 #[serde(default)]
98 pub callbacks: Option<BTreeMap<String, Callback>>,
99 #[serde(rename = "pathItems", default)]
101 pub path_items: Option<BTreeMap<String, PathItem>>,
102 #[serde(rename = "mediaTypes", default)]
104 pub media_types: Option<BTreeMap<String, MediaType>>,
105 #[serde(flatten, default)]
106 pub extensions: Extensions,
107}
108
109#[derive(Debug, Clone, Deserialize, Serialize)]
110#[serde(untagged)]
111pub enum Schema {
112 Reference {
114 #[serde(rename = "$ref")]
115 reference: String,
116 #[serde(flatten)]
117 extra: BTreeMap<String, Value>,
118 },
119 RecursiveRef {
121 #[serde(rename = "$recursiveRef")]
122 recursive_ref: String,
123 #[serde(flatten)]
124 extra: BTreeMap<String, Value>,
125 },
126 DynamicRef {
132 #[serde(rename = "$dynamicRef")]
133 dynamic_ref: String,
134 #[serde(flatten)]
135 extra: BTreeMap<String, Value>,
136 },
137 OneOf {
139 #[serde(rename = "oneOf")]
140 one_of: Vec<Schema>,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 discriminator: Option<Discriminator>,
143 #[serde(flatten)]
144 details: SchemaDetails,
145 },
146 AnyOf {
148 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
149 schema_type: Option<SchemaType>,
150 #[serde(rename = "anyOf")]
151 any_of: Vec<Schema>,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 discriminator: Option<Discriminator>,
154 #[serde(flatten)]
155 details: SchemaDetails,
156 },
157 AllOf {
160 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
161 schema_type: Option<SchemaType>,
162 #[serde(rename = "allOf")]
163 all_of: Vec<Schema>,
164 #[serde(flatten)]
165 details: SchemaDetails,
166 },
167 TypedMulti {
172 #[serde(rename = "type")]
173 schema_types: Vec<SchemaType>,
174 #[serde(flatten)]
175 details: SchemaDetails,
176 },
177 Typed {
179 #[serde(rename = "type")]
180 schema_type: SchemaType,
181 #[serde(flatten)]
182 details: SchemaDetails,
183 },
184 Untyped {
186 #[serde(flatten)]
187 details: SchemaDetails,
188 },
189}
190
191#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
192#[serde(rename_all = "lowercase")]
193pub enum SchemaType {
194 String,
195 Integer,
196 Number,
197 Boolean,
198 Array,
199 Object,
200 #[serde(rename = "null")]
201 Null,
202}
203
204#[derive(Debug, Clone, Default, Deserialize, Serialize)]
205pub struct SchemaDetails {
206 #[serde(skip_serializing_if = "Option::is_none")]
207 pub description: Option<String>,
208 #[serde(skip_serializing_if = "Option::is_none")]
209 pub nullable: Option<bool>,
210
211 #[serde(rename = "$recursiveAnchor", skip_serializing_if = "Option::is_none")]
213 pub recursive_anchor: Option<bool>,
214
215 #[serde(rename = "$dynamicAnchor", skip_serializing_if = "Option::is_none")]
217 pub dynamic_anchor: Option<String>,
218 #[serde(rename = "$id", skip_serializing_if = "Option::is_none")]
219 pub schema_id: Option<String>,
220
221 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
223 pub enum_values: Option<Vec<Value>>,
224 #[serde(skip_serializing_if = "Option::is_none")]
225 pub format: Option<String>,
226 #[serde(skip_serializing_if = "Option::is_none")]
227 pub default: Option<Value>,
228 #[serde(
229 rename = "const",
230 default,
231 deserialize_with = "deserialize_present_value",
232 skip_serializing_if = "Option::is_none"
233 )]
234 pub const_value: Option<Value>,
235
236 #[serde(skip_serializing_if = "Option::is_none")]
238 pub properties: Option<BTreeMap<String, Schema>>,
239 #[serde(skip_serializing_if = "Option::is_none")]
240 pub required: Option<Vec<String>>,
241 #[serde(
242 rename = "additionalProperties",
243 skip_serializing_if = "Option::is_none"
244 )]
245 pub additional_properties: Option<AdditionalProperties>,
246
247 #[serde(skip_serializing_if = "Option::is_none")]
249 pub items: Option<Box<Schema>>,
250
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub minimum: Option<f64>,
254 #[serde(skip_serializing_if = "Option::is_none")]
255 pub maximum: Option<f64>,
256
257 #[serde(rename = "minLength", skip_serializing_if = "Option::is_none")]
259 pub min_length: Option<u64>,
260 #[serde(rename = "maxLength", skip_serializing_if = "Option::is_none")]
261 pub max_length: Option<u64>,
262 #[serde(skip_serializing_if = "Option::is_none")]
263 pub pattern: Option<String>,
264 #[serde(rename = "exclusiveMinimum", skip_serializing_if = "Option::is_none")]
268 pub exclusive_minimum: Option<ExclusiveBound>,
269 #[serde(rename = "exclusiveMaximum", skip_serializing_if = "Option::is_none")]
270 pub exclusive_maximum: Option<ExclusiveBound>,
271 #[serde(rename = "multipleOf", skip_serializing_if = "Option::is_none")]
272 pub multiple_of: Option<f64>,
273 #[serde(rename = "minItems", skip_serializing_if = "Option::is_none")]
274 pub min_items: Option<u64>,
275 #[serde(rename = "maxItems", skip_serializing_if = "Option::is_none")]
276 pub max_items: Option<u64>,
277 #[serde(rename = "uniqueItems", skip_serializing_if = "Option::is_none")]
278 pub unique_items: Option<bool>,
279 #[serde(rename = "minProperties", skip_serializing_if = "Option::is_none")]
280 pub min_properties: Option<u64>,
281 #[serde(rename = "maxProperties", skip_serializing_if = "Option::is_none")]
282 pub max_properties: Option<u64>,
283
284 #[serde(rename = "prefixItems", skip_serializing_if = "Option::is_none")]
286 pub prefix_items: Option<Vec<Schema>>,
287 #[serde(skip_serializing_if = "Option::is_none")]
288 pub contains: Option<Box<Schema>>,
289 #[serde(rename = "minContains", skip_serializing_if = "Option::is_none")]
290 pub min_contains: Option<u64>,
291 #[serde(rename = "maxContains", skip_serializing_if = "Option::is_none")]
292 pub max_contains: Option<u64>,
293
294 #[serde(rename = "patternProperties", skip_serializing_if = "Option::is_none")]
296 pub pattern_properties: Option<BTreeMap<String, Schema>>,
297 #[serde(rename = "propertyNames", skip_serializing_if = "Option::is_none")]
298 pub property_names: Option<Box<Schema>>,
299 #[serde(
300 rename = "unevaluatedProperties",
301 skip_serializing_if = "Option::is_none"
302 )]
303 pub unevaluated_properties: Option<AdditionalProperties>,
304 #[serde(rename = "unevaluatedItems", skip_serializing_if = "Option::is_none")]
305 pub unevaluated_items: Option<AdditionalProperties>,
306 #[serde(rename = "dependentRequired", skip_serializing_if = "Option::is_none")]
307 pub dependent_required: Option<BTreeMap<String, Vec<String>>>,
308 #[serde(rename = "dependentSchemas", skip_serializing_if = "Option::is_none")]
309 pub dependent_schemas: Option<BTreeMap<String, Schema>>,
310
311 #[serde(rename = "contentEncoding", skip_serializing_if = "Option::is_none")]
313 pub content_encoding: Option<String>,
314 #[serde(rename = "contentMediaType", skip_serializing_if = "Option::is_none")]
315 pub content_media_type: Option<String>,
316 #[serde(rename = "contentSchema", skip_serializing_if = "Option::is_none")]
317 pub content_schema: Option<Box<Schema>>,
318
319 #[serde(rename = "if", skip_serializing_if = "Option::is_none")]
321 pub if_schema: Option<Box<Schema>>,
322 #[serde(rename = "then", skip_serializing_if = "Option::is_none")]
323 pub then_schema: Option<Box<Schema>>,
324 #[serde(rename = "else", skip_serializing_if = "Option::is_none")]
325 pub else_schema: Option<Box<Schema>>,
326 #[serde(skip_serializing_if = "Option::is_none")]
327 pub not: Option<Box<Schema>>,
328
329 #[serde(skip_serializing_if = "Option::is_none")]
331 pub title: Option<String>,
332 #[serde(skip_serializing_if = "Option::is_none")]
333 pub deprecated: Option<bool>,
334 #[serde(rename = "readOnly", skip_serializing_if = "Option::is_none")]
335 pub read_only: Option<bool>,
336 #[serde(rename = "writeOnly", skip_serializing_if = "Option::is_none")]
337 pub write_only: Option<bool>,
338 #[serde(skip_serializing_if = "Option::is_none")]
339 pub examples: Option<Vec<Value>>,
340 #[serde(skip_serializing_if = "Option::is_none")]
341 pub example: Option<Value>,
342 #[serde(rename = "$comment", skip_serializing_if = "Option::is_none")]
344 pub comment: Option<String>,
345 #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
346 pub schema_keyword: Option<String>,
347 #[serde(rename = "$defs", skip_serializing_if = "Option::is_none")]
348 pub defs: Option<BTreeMap<String, Schema>>,
349
350 #[serde(flatten)]
353 pub extra: BTreeMap<String, Value>,
354}
355
356fn deserialize_present_value<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
357where
358 D: serde::Deserializer<'de>,
359{
360 Value::deserialize(deserializer).map(Some)
361}
362
363#[derive(Debug, Clone, Deserialize, Serialize)]
366#[serde(untagged)]
367pub enum ExclusiveBound {
368 Bool(bool),
369 Number(f64),
370}
371
372#[derive(Debug, Clone, Deserialize, Serialize)]
373#[serde(untagged)]
374pub enum AdditionalProperties {
375 Boolean(bool),
376 Schema(Box<Schema>),
377}
378
379#[derive(Debug, Clone, Deserialize, Serialize)]
381pub struct Example {
382 #[serde(default)]
383 pub summary: Option<String>,
384 #[serde(default)]
385 pub description: Option<String>,
386 #[serde(default)]
388 pub value: Option<Value>,
389 #[serde(rename = "externalValue", default)]
390 pub external_value: Option<String>,
391 #[serde(rename = "dataValue", default)]
393 pub data_value: Option<Value>,
394 #[serde(rename = "serializedValue", default)]
396 pub serialized_value: Option<String>,
397 #[serde(rename = "$ref", default)]
398 pub reference: Option<String>,
399 #[serde(flatten, default)]
400 pub extensions: Extensions,
401}
402
403#[derive(Debug, Clone, Deserialize, Serialize)]
405pub struct Link {
406 #[serde(rename = "operationRef", default)]
407 pub operation_ref: Option<String>,
408 #[serde(rename = "operationId", default)]
409 pub operation_id: Option<String>,
410 #[serde(default)]
411 pub parameters: Option<BTreeMap<String, Value>>,
412 #[serde(rename = "requestBody", default)]
413 pub request_body: Option<Value>,
414 #[serde(default)]
415 pub description: Option<String>,
416 #[serde(default)]
417 pub server: Option<Server>,
418 #[serde(rename = "$ref", default)]
419 pub reference: Option<String>,
420 #[serde(flatten, default)]
421 pub extensions: Extensions,
422}
423
424#[derive(Debug, Clone, Deserialize, Serialize)]
427#[serde(transparent)]
428pub struct Callback(pub BTreeMap<String, PathItem>);
429
430#[derive(Debug, Clone, Deserialize, Serialize)]
433pub struct Encoding {
434 #[serde(rename = "contentType", default)]
435 pub content_type: Option<String>,
436 #[serde(default)]
437 pub headers: Option<BTreeMap<String, Header>>,
438 #[serde(default)]
439 pub style: Option<String>,
440 #[serde(default)]
441 pub explode: Option<bool>,
442 #[serde(rename = "allowReserved", default)]
443 pub allow_reserved: Option<bool>,
444 #[serde(rename = "itemEncoding", default)]
446 pub item_encoding: Option<Box<Encoding>>,
447 #[serde(flatten, default)]
448 pub extensions: Extensions,
449}
450
451#[derive(Debug, Clone, Deserialize, Serialize)]
455pub struct Header {
456 #[serde(default)]
457 pub description: Option<String>,
458 #[serde(default)]
459 pub required: Option<bool>,
460 #[serde(default)]
461 pub deprecated: Option<bool>,
462 #[serde(rename = "allowEmptyValue", default)]
463 pub allow_empty_value: Option<bool>,
464 #[serde(default)]
465 pub style: Option<String>,
466 #[serde(default)]
467 pub explode: Option<bool>,
468 #[serde(rename = "allowReserved", default)]
469 pub allow_reserved: Option<bool>,
470 #[serde(default)]
471 pub schema: Option<Schema>,
472 #[serde(default)]
473 pub content: Option<BTreeMap<String, MediaType>>,
474 #[serde(default)]
475 pub example: Option<Value>,
476 #[serde(default)]
477 pub examples: Option<Value>,
478 #[serde(rename = "$ref", default)]
479 pub reference: Option<String>,
480 #[serde(flatten, default)]
481 pub extensions: Extensions,
482}
483
484#[derive(Debug, Clone, Deserialize, Serialize)]
488#[serde(tag = "type")]
489pub enum SecurityScheme {
490 #[serde(rename = "apiKey")]
491 ApiKey {
492 name: String,
493 #[serde(rename = "in")]
494 location: String, #[serde(default)]
496 description: Option<String>,
497 #[serde(default)]
499 deprecated: Option<bool>,
500 #[serde(flatten, default)]
501 extensions: Extensions,
502 },
503 #[serde(rename = "http")]
504 Http {
505 scheme: String, #[serde(rename = "bearerFormat", default)]
507 bearer_format: Option<String>,
508 #[serde(default)]
509 description: Option<String>,
510 #[serde(default)]
511 deprecated: Option<bool>,
512 #[serde(flatten, default)]
513 extensions: Extensions,
514 },
515 #[serde(rename = "mutualTLS")]
516 MutualTls {
517 #[serde(default)]
518 description: Option<String>,
519 #[serde(default)]
520 deprecated: Option<bool>,
521 #[serde(flatten, default)]
522 extensions: Extensions,
523 },
524 #[serde(rename = "oauth2")]
525 OAuth2 {
526 flows: Box<OAuthFlows>,
530 #[serde(default)]
531 description: Option<String>,
532 #[serde(rename = "oauth2MetadataUrl", default)]
534 oauth2_metadata_url: Option<String>,
535 #[serde(default)]
536 deprecated: Option<bool>,
537 #[serde(flatten, default)]
538 extensions: Extensions,
539 },
540 #[serde(rename = "openIdConnect")]
541 OpenIdConnect {
542 #[serde(rename = "openIdConnectUrl")]
543 open_id_connect_url: String,
544 #[serde(default)]
545 description: Option<String>,
546 #[serde(default)]
547 deprecated: Option<bool>,
548 #[serde(flatten, default)]
549 extensions: Extensions,
550 },
551}
552
553#[derive(Debug, Clone, Deserialize, Serialize)]
554pub struct OAuthFlows {
555 #[serde(default)]
556 pub implicit: Option<OAuthFlow>,
557 #[serde(default)]
558 pub password: Option<OAuthFlow>,
559 #[serde(rename = "clientCredentials", default)]
560 pub client_credentials: Option<OAuthFlow>,
561 #[serde(rename = "authorizationCode", default)]
562 pub authorization_code: Option<OAuthFlow>,
563 #[serde(rename = "deviceAuthorization", default)]
565 pub device_authorization: Option<OAuthFlow>,
566 #[serde(flatten, default)]
567 pub extensions: Extensions,
568}
569
570#[derive(Debug, Clone, Deserialize, Serialize)]
571pub struct OAuthFlow {
572 #[serde(rename = "authorizationUrl", default)]
573 pub authorization_url: Option<String>,
574 #[serde(rename = "tokenUrl", default)]
575 pub token_url: Option<String>,
576 #[serde(rename = "refreshUrl", default)]
577 pub refresh_url: Option<String>,
578 #[serde(rename = "deviceAuthorizationUrl", default)]
580 pub device_authorization_url: Option<String>,
581 pub scopes: BTreeMap<String, String>,
582 #[serde(flatten, default)]
583 pub extensions: Extensions,
584}
585
586#[derive(Debug, Clone, Deserialize, Serialize)]
588pub struct ExternalDocs {
589 pub url: String,
590 #[serde(default)]
591 pub description: Option<String>,
592 #[serde(flatten, default)]
593 pub extensions: Extensions,
594}
595
596#[derive(Debug, Clone, Deserialize, Serialize)]
598pub struct Tag {
599 pub name: String,
600 #[serde(default)]
602 pub summary: Option<String>,
603 #[serde(default)]
604 pub description: Option<String>,
605 #[serde(default)]
607 pub parent: Option<String>,
608 #[serde(default)]
612 pub kind: Option<String>,
613 #[serde(rename = "externalDocs", default)]
614 pub external_docs: Option<ExternalDocs>,
615 #[serde(flatten, default)]
616 pub extensions: Extensions,
617}
618
619#[derive(Debug, Clone, Deserialize, Serialize)]
622pub struct Server {
623 pub url: String,
624 #[serde(default)]
626 pub name: Option<String>,
627 #[serde(default)]
628 pub description: Option<String>,
629 #[serde(default)]
630 pub variables: Option<BTreeMap<String, ServerVariable>>,
631 #[serde(flatten, default)]
632 pub extensions: Extensions,
633}
634
635#[derive(Debug, Clone, Deserialize, Serialize)]
636pub struct ServerVariable {
637 #[serde(default)]
639 pub default: Option<String>,
640 #[serde(rename = "enum", default)]
641 pub enum_values: Option<Vec<String>>,
642 #[serde(default)]
643 pub description: Option<String>,
644 #[serde(flatten, default)]
645 pub extensions: Extensions,
646}
647
648#[derive(Debug, Clone, Deserialize, Serialize)]
649pub struct Discriminator {
650 #[serde(rename = "propertyName")]
651 pub property_name: String,
652 #[serde(default)]
653 pub mapping: Option<BTreeMap<String, String>>,
654 #[serde(rename = "defaultMapping", default)]
658 pub default_mapping: Option<String>,
659 #[serde(flatten, default)]
660 pub extensions: Extensions,
661}
662
663impl Schema {
664 pub fn schema_type(&self) -> Option<&SchemaType> {
668 match self {
669 Schema::Typed { schema_type, .. } => Some(schema_type),
670 Schema::TypedMulti { schema_types, .. } => schema_types
671 .iter()
672 .find(|t| **t != SchemaType::Null)
673 .or_else(|| schema_types.first()),
674 _ => None,
675 }
676 }
677
678 pub fn type_array_contains_null(&self) -> bool {
681 match self {
682 Schema::TypedMulti { schema_types, .. } => schema_types.contains(&SchemaType::Null),
683 _ => false,
684 }
685 }
686
687 pub fn is_nullable_any(&self) -> bool {
699 self.details().is_nullable()
700 || self.type_array_contains_null()
701 || self.is_nullable_pattern()
702 }
703
704 pub fn details(&self) -> &SchemaDetails {
706 static EMPTY_DETAILS: Lazy<SchemaDetails> = Lazy::new(SchemaDetails::default);
707 match self {
708 Schema::Typed { details, .. } => details,
709 Schema::TypedMulti { details, .. } => details,
710 Schema::Reference { .. } | Schema::RecursiveRef { .. } | Schema::DynamicRef { .. } => {
711 &EMPTY_DETAILS
712 }
713 Schema::OneOf { details, .. } => details,
714 Schema::AnyOf { details, .. } => details,
715 Schema::AllOf { details, .. } => details,
716 Schema::Untyped { details } => details,
717 }
718 }
719
720 pub fn details_mut(&mut self) -> &mut SchemaDetails {
722 match self {
723 Schema::Typed { details, .. } => details,
724 Schema::TypedMulti { details, .. } => details,
725 Schema::Reference { .. } => {
726 panic!("Cannot get mutable details for reference schema")
727 }
728 Schema::RecursiveRef { .. } => {
729 panic!("Cannot get mutable details for recursive reference schema")
730 }
731 Schema::DynamicRef { .. } => {
732 panic!("Cannot get mutable details for dynamic reference schema")
733 }
734 Schema::OneOf { details, .. } => details,
735 Schema::AnyOf { details, .. } => details,
736 Schema::AllOf { details, .. } => details,
737 Schema::Untyped { details } => details,
738 }
739 }
740
741 pub fn is_reference(&self) -> bool {
743 matches!(self, Schema::Reference { .. } | Schema::RecursiveRef { .. })
744 }
745
746 pub fn reference(&self) -> Option<&str> {
748 match self {
749 Schema::Reference { reference, .. } => Some(reference),
750 _ => None,
751 }
752 }
753
754 pub fn recursive_reference(&self) -> Option<&str> {
756 match self {
757 Schema::RecursiveRef { recursive_ref, .. } => Some(recursive_ref),
758 _ => None,
759 }
760 }
761
762 pub fn is_discriminated_union(&self) -> bool {
764 match self {
765 Schema::OneOf { discriminator, .. } => discriminator.is_some(),
766 Schema::AnyOf { discriminator, .. } => discriminator.is_some(),
767 _ => false,
768 }
769 }
770
771 pub fn discriminator(&self) -> Option<&Discriminator> {
773 match self {
774 Schema::OneOf { discriminator, .. } => discriminator.as_ref(),
775 Schema::AnyOf { discriminator, .. } => discriminator.as_ref(),
776 _ => None,
777 }
778 }
779
780 pub fn union_variants(&self) -> Option<&[Schema]> {
782 match self {
783 Schema::OneOf { one_of, .. } => Some(one_of),
784 Schema::AnyOf { any_of, .. } => Some(any_of),
785 _ => None,
786 }
787 }
788
789 pub fn is_nullable_pattern(&self) -> bool {
791 let variants = match self {
792 Schema::AnyOf { any_of, .. } => any_of,
793 Schema::OneOf { one_of, .. } => one_of,
794 _ => return false,
795 };
796 variants.len() == 2
797 && variants
798 .iter()
799 .any(|s| matches!(s.schema_type(), Some(SchemaType::Null)))
800 }
801
802 pub fn non_null_variant(&self) -> Option<&Schema> {
804 if !self.is_nullable_pattern() {
805 return None;
806 }
807 let variants = match self {
808 Schema::AnyOf { any_of, .. } => any_of,
809 Schema::OneOf { one_of, .. } => one_of,
810 _ => return None,
811 };
812 variants
813 .iter()
814 .find(|s| !matches!(s.schema_type(), Some(SchemaType::Null)))
815 }
816
817 pub fn inferred_type(&self) -> Option<SchemaType> {
819 match self {
820 Schema::Typed { schema_type, .. } => Some(schema_type.clone()),
821 Schema::TypedMulti { .. } => self.schema_type().cloned(),
822 Schema::Untyped { details } => {
823 if details.properties.is_some() {
825 Some(SchemaType::Object)
826 } else if details.items.is_some() {
827 Some(SchemaType::Array)
828 } else if details.enum_values.is_some() {
829 Some(SchemaType::String) } else {
831 None
832 }
833 }
834 _ => None,
835 }
836 }
837}
838
839impl SchemaDetails {
840 pub fn is_nullable(&self) -> bool {
842 self.nullable.unwrap_or(false)
843 }
844
845 pub fn is_string_enum(&self) -> bool {
851 self.enum_values.is_some() || self.const_string_value().is_some()
852 }
853
854 pub fn string_enum_values(&self) -> Option<Vec<String>> {
860 if let Some(values) = self.enum_values.as_ref() {
861 return Some(
866 values
867 .iter()
868 .map(|v| match v {
869 Value::String(s) => s.clone(),
870 Value::Number(n) => n.to_string(),
871 Value::Bool(b) => b.to_string(),
872 Value::Null => "null".to_string(),
873 _ => v.to_string(),
874 })
875 .collect(),
876 );
877 }
878 self.const_string_value().map(|s| vec![s])
879 }
880
881 fn const_string_value(&self) -> Option<String> {
882 self.const_value
883 .as_ref()
884 .and_then(|v| v.as_str())
885 .map(|s| s.to_string())
886 }
887
888 pub fn is_field_required(&self, field_name: &str) -> bool {
890 self.required
891 .as_ref()
892 .map(|req| req.contains(&field_name.to_string()))
893 .unwrap_or(false)
894 }
895}
896
897#[derive(Debug, Clone, Deserialize, Serialize)]
899pub struct PathItem {
900 #[serde(default)]
901 pub summary: Option<String>,
902 #[serde(default)]
903 pub description: Option<String>,
904 pub get: Option<Operation>,
905 pub put: Option<Operation>,
906 pub post: Option<Operation>,
907 pub delete: Option<Operation>,
908 pub options: Option<Operation>,
909 pub head: Option<Operation>,
910 pub patch: Option<Operation>,
911 pub trace: Option<Operation>,
912 pub query: Option<Operation>,
915 #[serde(rename = "additionalOperations", default)]
919 pub additional_operations: Option<BTreeMap<String, Operation>>,
920 pub parameters: Option<Vec<Parameter>>,
921 #[serde(default)]
922 pub servers: Option<Vec<Server>>,
923 #[serde(rename = "$ref", default)]
924 pub reference: Option<String>,
925 #[serde(flatten, default)]
926 pub extensions: Extensions,
927}
928
929impl PathItem {
930 pub fn operations(&self) -> Vec<(&str, &Operation)> {
933 let mut ops = Vec::new();
934 if let Some(ref op) = self.get {
935 ops.push(("get", op));
936 }
937 if let Some(ref op) = self.put {
938 ops.push(("put", op));
939 }
940 if let Some(ref op) = self.post {
941 ops.push(("post", op));
942 }
943 if let Some(ref op) = self.delete {
944 ops.push(("delete", op));
945 }
946 if let Some(ref op) = self.options {
947 ops.push(("options", op));
948 }
949 if let Some(ref op) = self.head {
950 ops.push(("head", op));
951 }
952 if let Some(ref op) = self.patch {
953 ops.push(("patch", op));
954 }
955 if let Some(ref op) = self.trace {
956 ops.push(("trace", op));
957 }
958 if let Some(ref op) = self.query {
959 ops.push(("query", op));
960 }
961 if let Some(map) = &self.additional_operations {
962 for (verb, op) in map {
963 ops.push((verb.as_str(), op));
964 }
965 }
966 ops
967 }
968}
969
970#[derive(Debug, Clone, Deserialize, Serialize)]
972pub struct Operation {
973 #[serde(rename = "operationId", default)]
974 pub operation_id: Option<String>,
975 #[serde(default)]
976 pub summary: Option<String>,
977 #[serde(default)]
978 pub description: Option<String>,
979 #[serde(default)]
980 pub tags: Option<Vec<String>>,
981 #[serde(default)]
982 pub deprecated: Option<bool>,
983 pub parameters: Option<Vec<Parameter>>,
984 #[serde(rename = "requestBody")]
985 pub request_body: Option<RequestBody>,
986 pub responses: Option<BTreeMap<String, Response>>,
987 #[serde(default)]
988 pub callbacks: Option<BTreeMap<String, Callback>>,
989 #[serde(default)]
990 pub security: Option<Vec<BTreeMap<String, Vec<String>>>>,
991 #[serde(default)]
992 pub servers: Option<Vec<Server>>,
993 #[serde(rename = "externalDocs", default)]
994 pub external_docs: Option<ExternalDocs>,
995 #[serde(flatten, default)]
996 pub extensions: Extensions,
997}
998
999#[derive(Debug, Clone, Deserialize, Serialize)]
1001pub struct Parameter {
1002 #[serde(default)]
1003 pub name: Option<String>,
1004 #[serde(rename = "in", default)]
1005 pub location: Option<String>,
1006 #[serde(default)]
1007 pub required: Option<bool>,
1008 #[serde(default)]
1009 pub deprecated: Option<bool>,
1010 #[serde(rename = "allowEmptyValue", default)]
1011 pub allow_empty_value: Option<bool>,
1012 #[serde(default)]
1013 pub style: Option<String>,
1014 #[serde(default)]
1015 pub explode: Option<bool>,
1016 #[serde(rename = "allowReserved", default)]
1017 pub allow_reserved: Option<bool>,
1018 #[serde(default)]
1019 pub schema: Option<Schema>,
1020 #[serde(default)]
1021 pub content: Option<BTreeMap<String, MediaType>>,
1022 #[serde(default)]
1023 pub example: Option<Value>,
1024 #[serde(default)]
1025 pub examples: Option<BTreeMap<String, Example>>,
1026 #[serde(default)]
1027 pub description: Option<String>,
1028 #[serde(rename = "$ref", default)]
1029 pub reference: Option<String>,
1030 #[serde(flatten, default)]
1031 pub extensions: Extensions,
1032}
1033
1034#[derive(Debug, Clone, Deserialize, Serialize)]
1036pub struct RequestBody {
1037 pub content: Option<BTreeMap<String, MediaType>>,
1038 #[serde(default)]
1039 pub description: Option<String>,
1040 #[serde(default)]
1041 pub required: Option<bool>,
1042 #[serde(rename = "$ref", default)]
1043 pub reference: Option<String>,
1044 #[serde(flatten, default)]
1045 pub extensions: Extensions,
1046}
1047
1048#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1054#[serde(rename_all = "snake_case")]
1055pub enum ResponseMediaKind {
1056 Json,
1057 EventStream,
1058 Text,
1059 Binary,
1060 Unsupported,
1061}
1062
1063pub fn media_type_essence(content_type: &str) -> &str {
1069 content_type
1070 .split(';')
1071 .next()
1072 .unwrap_or(content_type)
1073 .trim()
1074}
1075
1076pub fn is_json_media_type(ct: &str) -> bool {
1084 let essence = media_type_essence(ct).to_ascii_lowercase();
1085 if essence == "application/json" {
1086 return true;
1087 }
1088 if let Some(subtype) = essence.strip_prefix("application/") {
1089 return subtype.ends_with("+json");
1090 }
1091 false
1092}
1093
1094pub fn is_form_urlencoded_media_type(ct: &str) -> bool {
1097 let essence = media_type_essence(ct).to_ascii_lowercase();
1098 essence == "application/x-www-form-urlencoded"
1099}
1100
1101pub fn is_event_stream_media_type(ct: &str) -> bool {
1107 media_type_essence(ct).eq_ignore_ascii_case("text/event-stream")
1108}
1109
1110pub fn is_text_media_type(ct: &str) -> bool {
1118 let Some((top_level, subtype)) = media_type_essence(ct).split_once('/') else {
1119 return false;
1120 };
1121 if top_level.eq_ignore_ascii_case("text")
1122 && !subtype.is_empty()
1123 && !is_event_stream_media_type(ct)
1124 {
1125 return true;
1126 }
1127 top_level.eq_ignore_ascii_case("application")
1128 && (subtype.eq_ignore_ascii_case("xml")
1129 || subtype.to_ascii_lowercase().ends_with("+xml")
1130 || subtype.eq_ignore_ascii_case("jwt"))
1133}
1134
1135pub fn is_wildcard_media_type(ct: &str) -> bool {
1141 let Some((top_level, subtype)) = media_type_essence(ct).split_once('/') else {
1142 return false;
1143 };
1144 !top_level.is_empty() && subtype == "*"
1145}
1146
1147fn schema_has_binary_format(schema: Option<&Schema>) -> bool {
1148 schema.is_some_and(|schema| {
1149 schema
1150 .details()
1151 .format
1152 .as_deref()
1153 .is_some_and(|format| format.eq_ignore_ascii_case("binary"))
1154 })
1155}
1156
1157pub fn is_binary_media_type(ct: &str, schema: Option<&Schema>) -> bool {
1165 if schema_has_binary_format(schema) {
1166 return true;
1167 }
1168
1169 let essence = media_type_essence(ct);
1170 let Some((top_level, _)) = essence.split_once('/') else {
1171 return false;
1172 };
1173 if top_level.eq_ignore_ascii_case("image")
1174 || top_level.eq_ignore_ascii_case("audio")
1175 || top_level.eq_ignore_ascii_case("video")
1176 {
1177 return true;
1178 }
1179 if essence.eq_ignore_ascii_case("application/octet-stream")
1180 || essence.eq_ignore_ascii_case("application/zip")
1181 || essence.eq_ignore_ascii_case("application/pdf")
1182 {
1183 return true;
1184 }
1185
1186 !top_level.eq_ignore_ascii_case("text") && is_wildcard_media_type(ct)
1187}
1188
1189pub fn classify_response_media_type(ct: &str, schema: Option<&Schema>) -> ResponseMediaKind {
1194 if is_json_media_type(ct) {
1195 ResponseMediaKind::Json
1196 } else if is_event_stream_media_type(ct) {
1197 ResponseMediaKind::EventStream
1198 } else if schema_has_binary_format(schema) {
1199 ResponseMediaKind::Binary
1200 } else if is_text_media_type(ct) {
1201 if is_wildcard_media_type(ct) {
1202 ResponseMediaKind::Unsupported
1203 } else {
1204 ResponseMediaKind::Text
1205 }
1206 } else if is_binary_media_type(ct, schema) {
1207 ResponseMediaKind::Binary
1208 } else {
1209 ResponseMediaKind::Unsupported
1210 }
1211}
1212
1213fn find_json_content(content: &BTreeMap<String, MediaType>) -> Option<(&str, &MediaType)> {
1214 if let Some(mt) = content
1215 .get("application/json")
1216 .filter(|media_type| media_type.schema.is_some())
1217 {
1218 return Some(("application/json", mt));
1219 }
1220 content
1221 .iter()
1222 .find(|(ct, media_type)| is_json_media_type(ct) && media_type.schema.is_some())
1223 .map(|(ct, mt)| (ct.as_str(), mt))
1224 .or_else(|| {
1225 content
1226 .get("application/json")
1227 .map(|media_type| ("application/json", media_type))
1228 })
1229 .or_else(|| {
1230 content
1231 .iter()
1232 .find(|(ct, _)| is_json_media_type(ct))
1233 .map(|(ct, mt)| (ct.as_str(), mt))
1234 })
1235}
1236
1237impl RequestBody {
1238 pub fn json_schema(&self) -> Option<&Schema> {
1244 self.content
1245 .as_ref()
1246 .and_then(find_json_content)
1247 .and_then(|(_, media_type)| media_type.schema.as_ref())
1248 }
1249
1250 pub fn best_content(&self) -> Option<(&str, Option<&Schema>)> {
1252 let content = self.content.as_ref()?;
1253
1254 if let Some((ct, media_type)) = find_json_content(content) {
1255 return Some((ct, media_type.schema.as_ref()));
1256 }
1257
1258 const PRIORITY: &[&str] = &[
1259 "application/x-www-form-urlencoded",
1260 "multipart/form-data",
1261 "application/octet-stream",
1262 "text/plain",
1263 ];
1264 for preferred_essence in PRIORITY {
1265 if let Some((ct, media_type)) = content
1266 .iter()
1267 .find(|(ct, _)| media_type_essence(ct).eq_ignore_ascii_case(preferred_essence))
1268 {
1269 return Some((ct.as_str(), media_type.schema.as_ref()));
1270 }
1271 }
1272 if let Some((ct, media_type)) = content.iter().find(|(ct, _)| is_text_media_type(ct)) {
1275 return Some((ct.as_str(), media_type.schema.as_ref()));
1276 }
1277 content
1278 .iter()
1279 .find(|(ct, media_type)| {
1285 !is_wildcard_media_type(ct) && is_binary_media_type(ct, media_type.schema.as_ref())
1286 })
1287 .map(|(ct, media_type)| (ct.as_str(), media_type.schema.as_ref()))
1288 }
1289}
1290
1291#[derive(Debug, Clone, Deserialize, Serialize)]
1293pub struct Response {
1294 #[serde(default)]
1295 pub description: Option<String>,
1296 #[serde(default)]
1297 pub headers: Option<BTreeMap<String, Header>>,
1298 #[serde(default)]
1299 pub content: Option<BTreeMap<String, MediaType>>,
1300 #[serde(default)]
1301 pub links: Option<Value>,
1302 #[serde(rename = "$ref", default)]
1303 pub reference: Option<String>,
1304 #[serde(flatten, default)]
1305 pub extensions: Extensions,
1306}
1307
1308impl Response {
1309 pub fn json_schema(&self) -> Option<&Schema> {
1316 self.content
1317 .as_ref()
1318 .and_then(find_json_content)
1319 .and_then(|(_, media_type)| media_type.schema.as_ref())
1320 }
1321
1322 pub fn json_content(&self) -> Option<(&str, &Schema)> {
1324 self.content
1325 .as_ref()
1326 .and_then(find_json_content)
1327 .and_then(|(content_type, media_type)| {
1328 media_type
1329 .schema
1330 .as_ref()
1331 .map(|schema| (content_type, schema))
1332 })
1333 }
1334}
1335
1336#[derive(Debug, Clone, Deserialize, Serialize)]
1338pub struct MediaType {
1339 #[serde(default)]
1340 pub schema: Option<Schema>,
1341 #[serde(default)]
1342 pub example: Option<Value>,
1343 #[serde(default)]
1344 pub examples: Option<BTreeMap<String, Example>>,
1345 #[serde(default)]
1346 pub encoding: Option<BTreeMap<String, Encoding>>,
1347 #[serde(rename = "itemSchema", default)]
1350 pub item_schema: Option<Schema>,
1351 #[serde(rename = "prefixEncoding", default)]
1354 pub prefix_encoding: Option<Vec<Encoding>>,
1355 #[serde(rename = "itemEncoding", default)]
1358 pub item_encoding: Option<Encoding>,
1359 #[serde(rename = "$ref", default)]
1360 pub reference: Option<String>,
1361 #[serde(flatten, default)]
1362 pub extensions: Extensions,
1363}
1364
1365#[cfg(test)]
1366#[allow(clippy::unwrap_used, clippy::expect_used)]
1367mod tests {
1368 use super::*;
1369 use serde_json::json;
1370
1371 #[test]
1372 fn paths_map_skips_extension_scalars() {
1373 let spec: OpenApiSpec = serde_json::from_value(json!({
1376 "openapi": "3.0.0",
1377 "info": { "title": "lenient paths", "version": "1" },
1378 "paths": {
1379 "x-codegen-contextRoot": "/apis/registry/v2",
1380 "/items": {
1381 "get": {
1382 "operationId": "listItems",
1383 "responses": { "204": { "description": "ok" } }
1384 }
1385 }
1386 }
1387 }))
1388 .unwrap();
1389 let paths = spec.paths.unwrap();
1390 assert!(paths.contains_key("/items"));
1391 assert!(!paths.contains_key("x-codegen-contextRoot"));
1392 }
1393
1394 #[test]
1395 fn test_parse_simple_object_schema() {
1396 let schema_json = json!({
1397 "type": "object",
1398 "properties": {
1399 "name": {
1400 "type": "string",
1401 "description": "User name"
1402 },
1403 "age": {
1404 "type": "integer"
1405 }
1406 },
1407 "required": ["name"]
1408 });
1409
1410 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1411
1412 match schema {
1413 Schema::Typed {
1414 schema_type: SchemaType::Object,
1415 details,
1416 } => {
1417 assert!(details.properties.is_some());
1418 assert_eq!(details.required, Some(vec!["name".to_string()]));
1419 assert!(details.is_field_required("name"));
1420 assert!(!details.is_field_required("age"));
1421 }
1422 _ => panic!("Expected object schema"),
1423 }
1424 }
1425
1426 #[test]
1427 fn test_parse_string_enum() {
1428 let schema_json = json!({
1429 "type": "string",
1430 "enum": ["active", "inactive", "pending"],
1431 "description": "User status"
1432 });
1433
1434 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1435
1436 match schema {
1437 Schema::Typed {
1438 schema_type: SchemaType::String,
1439 details,
1440 } => {
1441 assert!(details.is_string_enum());
1442 let values = details.string_enum_values().unwrap();
1443 assert_eq!(values, vec!["active", "inactive", "pending"]);
1444 }
1445 _ => panic!("Expected string enum schema"),
1446 }
1447 }
1448
1449 #[test]
1450 fn test_parse_reference_schema() {
1451 let schema_json = json!({
1452 "$ref": "#/components/schemas/User"
1453 });
1454
1455 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1456
1457 assert!(schema.is_reference());
1458 assert_eq!(schema.reference(), Some("#/components/schemas/User"));
1459 }
1460
1461 #[test]
1462 fn test_parse_discriminated_union() {
1463 let schema_json = json!({
1464 "oneOf": [
1465 {"$ref": "#/components/schemas/Dog"},
1466 {"$ref": "#/components/schemas/Cat"}
1467 ],
1468 "discriminator": {
1469 "propertyName": "petType"
1470 }
1471 });
1472
1473 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1474
1475 assert!(schema.is_discriminated_union());
1476 let discriminator = schema.discriminator().unwrap();
1477 assert_eq!(discriminator.property_name, "petType");
1478 }
1479
1480 #[test]
1481 fn test_parse_nullable_pattern() {
1482 let schema_json = json!({
1483 "anyOf": [
1484 {"$ref": "#/components/schemas/User"},
1485 {"type": "null"}
1486 ]
1487 });
1488
1489 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1490
1491 assert!(schema.is_nullable_pattern());
1492 let non_null = schema.non_null_variant().unwrap();
1493 assert!(non_null.is_reference());
1494 }
1495
1496 #[test]
1497 fn is_json_media_type_accepts_canonical_and_structured_suffix() {
1498 assert!(is_json_media_type("application/json"));
1500 assert!(is_json_media_type("application/json; charset=utf-8"));
1502 assert!(is_json_media_type("APPLICATION/JSON"));
1503 assert!(is_json_media_type("application/vnd.api+json"));
1505 assert!(is_json_media_type("application/hal+json"));
1506 assert!(is_json_media_type("application/problem+json"));
1507 assert!(is_json_media_type("application/ld+json"));
1508 assert!(is_json_media_type(
1509 "application/vnd.api+json; charset=utf-8"
1510 ));
1511 assert!(!is_json_media_type("application/xml"));
1513 assert!(!is_json_media_type("application/x-www-form-urlencoded"));
1514 assert!(!is_json_media_type("text/plain"));
1515 assert!(!is_json_media_type("application/jsonbutnotreally"));
1516 assert!(!is_json_media_type("text/something+json"));
1518 }
1519
1520 #[test]
1521 fn response_media_helpers_normalize_parameters_and_case() {
1522 assert_eq!(
1523 media_type_essence(" Text/Plain ; charset=utf-8 "),
1524 "Text/Plain"
1525 );
1526 assert!(is_text_media_type("TEXT/HTML; charset=UTF-8"));
1527 assert!(!is_text_media_type("Text/Event-Stream; charset=utf-8"));
1528 assert!(is_wildcard_media_type("*/*; q=0.8"));
1529 assert!(is_wildcard_media_type("IMAGE/*"));
1530 assert!(is_wildcard_media_type("text/*"));
1531 assert!(!is_wildcard_media_type("image/*+json"));
1532 assert!(!is_wildcard_media_type("application/json"));
1533 }
1534
1535 #[test]
1536 fn response_media_classifier_keeps_json_sse_and_text_distinct() {
1537 for media_type in [
1538 "application/json",
1539 "APPLICATION/PROBLEM+JSON; charset=utf-8",
1540 ] {
1541 assert_eq!(
1542 classify_response_media_type(media_type, None),
1543 ResponseMediaKind::Json,
1544 "{media_type}"
1545 );
1546 }
1547
1548 assert_eq!(
1549 classify_response_media_type("Text/Event-Stream; charset=utf-8", None),
1550 ResponseMediaKind::EventStream
1551 );
1552 for media_type in ["text/plain", "TEXT/HTML; charset=UTF-8"] {
1553 assert_eq!(
1554 classify_response_media_type(media_type, None),
1555 ResponseMediaKind::Text,
1556 "{media_type}"
1557 );
1558 }
1559 assert_eq!(
1560 classify_response_media_type("text/event-streaming", None),
1561 ResponseMediaKind::Text
1562 );
1563 assert_eq!(
1564 classify_response_media_type("text/*", None),
1565 ResponseMediaKind::Unsupported,
1566 "a media range is not a valid concrete response Content-Type"
1567 );
1568 }
1569
1570 #[test]
1571 fn response_json_content_skips_schema_less_canonical_entry() {
1572 let response: Response = serde_json::from_value(json!({
1573 "description": "mixed JSON",
1574 "content": {
1575 "application/json": {},
1576 "application/vnd.example+json": {
1577 "schema": { "type": "string" }
1578 }
1579 }
1580 }))
1581 .unwrap();
1582
1583 let (media_type, schema) = response.json_content().expect("schema-bearing JSON");
1584 assert_eq!(media_type, "application/vnd.example+json");
1585 assert!(matches!(schema.schema_type(), Some(SchemaType::String)));
1586 }
1587
1588 #[test]
1589 fn response_media_classifier_recognizes_binary_formats_and_wildcards() {
1590 for media_type in [
1591 "image/png",
1592 "IMAGE/*; version=1",
1593 "audio/mpeg",
1594 "video/mp4",
1595 "application/octet-stream",
1596 "APPLICATION/ZIP; version=1",
1597 "application/*",
1598 "*/*",
1599 ] {
1600 assert_eq!(
1601 classify_response_media_type(media_type, None),
1602 ResponseMediaKind::Binary,
1603 "{media_type}"
1604 );
1605 }
1606
1607 let binary_schema: Schema = serde_json::from_value(json!({
1608 "type": "string",
1609 "format": "BINARY"
1610 }))
1611 .unwrap();
1612 assert_eq!(
1613 classify_response_media_type("application/x-custom", Some(&binary_schema)),
1614 ResponseMediaKind::Binary
1615 );
1616 assert_eq!(
1617 classify_response_media_type("text/plain", Some(&binary_schema)),
1618 ResponseMediaKind::Binary,
1619 "an explicit binary schema must prevent UTF-8 decoding"
1620 );
1621 assert!(is_binary_media_type(
1622 "application/x-custom",
1623 Some(&binary_schema)
1624 ));
1625 }
1626
1627 #[test]
1628 fn response_media_classifier_leaves_ambiguous_formats_unsupported() {
1629 let string_schema: Schema = serde_json::from_value(json!({ "type": "string" })).unwrap();
1630 for media_type in ["application/x-unknown", "not-a-media-type"] {
1631 assert_eq!(
1632 classify_response_media_type(media_type, Some(&string_schema)),
1633 ResponseMediaKind::Unsupported,
1634 "{media_type}"
1635 );
1636 }
1637 assert_eq!(
1641 classify_response_media_type("application/pdf", Some(&string_schema)),
1642 ResponseMediaKind::Binary
1643 );
1644 assert_eq!(
1645 classify_response_media_type("application/xml", Some(&string_schema)),
1646 ResponseMediaKind::Text
1647 );
1648 assert_eq!(
1649 classify_response_media_type("application/atom+xml", Some(&string_schema)),
1650 ResponseMediaKind::Text
1651 );
1652 assert_eq!(
1654 classify_response_media_type("application/jwt", Some(&string_schema)),
1655 ResponseMediaKind::Text
1656 );
1657 assert!(!is_binary_media_type("text/plain", None));
1658 }
1659
1660 #[test]
1661 fn request_body_json_schema_finds_vnd_api_plus_json() {
1662 let body_json = json!({
1665 "required": true,
1666 "content": {
1667 "application/vnd.api+json": {
1668 "schema": {"$ref": "#/components/schemas/create_api_key"}
1669 }
1670 }
1671 });
1672
1673 let body: RequestBody = serde_json::from_value(body_json).unwrap();
1674 let schema = body.json_schema().expect("expected +json schema match");
1675 assert!(schema.is_reference());
1676 }
1677
1678 #[test]
1679 fn request_body_best_content_prefers_canonical_json_over_plus_json() {
1680 let body_json = json!({
1684 "required": true,
1685 "content": {
1686 "application/json": {
1687 "schema": {"$ref": "#/components/schemas/A"}
1688 },
1689 "application/vnd.api+json": {
1690 "schema": {"$ref": "#/components/schemas/B"}
1691 }
1692 }
1693 });
1694
1695 let body: RequestBody = serde_json::from_value(body_json).unwrap();
1696 let (ct, _) = body.best_content().expect("expected best_content");
1697 assert_eq!(ct, "application/json");
1698 }
1699
1700 #[test]
1701 fn request_body_best_content_falls_back_to_plus_json() {
1702 let body_json = json!({
1705 "required": true,
1706 "content": {
1707 "application/vnd.api+json": {
1708 "schema": {"$ref": "#/components/schemas/B"}
1709 }
1710 }
1711 });
1712
1713 let body: RequestBody = serde_json::from_value(body_json).unwrap();
1714 let (ct, _) = body.best_content().expect("expected best_content");
1715 assert_eq!(ct, "application/vnd.api+json");
1716 }
1717
1718 #[test]
1719 fn request_body_best_content_does_not_select_wildcard_media_ranges() {
1720 let body: RequestBody = serde_json::from_value(json!({
1721 "required": true,
1722 "content": {
1723 "image/*": {
1724 "schema": { "type": "string", "format": "binary" }
1725 },
1726 "*/*": {
1727 "schema": { "type": "string", "format": "binary" }
1728 }
1729 }
1730 }))
1731 .unwrap();
1732
1733 assert!(
1734 body.best_content().is_none(),
1735 "request media ranges require a runtime concrete Content-Type"
1736 );
1737 }
1738
1739 #[test]
1740 fn request_body_best_content_matches_parameterized_text_plain_by_essence() {
1741 let body: RequestBody = serde_json::from_value(json!({
1742 "required": true,
1743 "content": {
1744 "Text/Plain; charset=utf-8": {
1745 "schema": { "type": "string" }
1746 }
1747 }
1748 }))
1749 .unwrap();
1750
1751 let (media_type, _) = body.best_content().expect("parameterized text body");
1752 assert_eq!(media_type, "Text/Plain; charset=utf-8");
1753 }
1754
1755 #[test]
1756 fn response_json_schema_finds_vnd_api_plus_json() {
1757 let resp_json = json!({
1760 "description": "OK",
1761 "content": {
1762 "application/vnd.api+json": {
1763 "schema": {"$ref": "#/components/schemas/api_keys"}
1764 }
1765 }
1766 });
1767
1768 let resp: Response = serde_json::from_value(resp_json).unwrap();
1769 let schema = resp.json_schema().expect("expected +json schema match");
1770 assert!(schema.is_reference());
1771 }
1772}