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 = "type", skip_serializing_if = "Option::is_none")]
143 schema_type: Option<SchemaType>,
144 #[serde(rename = "oneOf")]
145 one_of: Vec<Schema>,
146 #[serde(skip_serializing_if = "Option::is_none")]
147 discriminator: Option<Discriminator>,
148 #[serde(flatten)]
149 details: SchemaDetails,
150 },
151 AnyOf {
153 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
154 schema_type: Option<SchemaType>,
155 #[serde(rename = "anyOf")]
156 any_of: Vec<Schema>,
157 #[serde(skip_serializing_if = "Option::is_none")]
158 discriminator: Option<Discriminator>,
159 #[serde(flatten)]
160 details: SchemaDetails,
161 },
162 AllOf {
165 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
166 schema_type: Option<SchemaType>,
167 #[serde(rename = "allOf")]
168 all_of: Vec<Schema>,
169 #[serde(flatten)]
170 details: SchemaDetails,
171 },
172 TypedMulti {
177 #[serde(rename = "type")]
178 schema_types: Vec<SchemaType>,
179 #[serde(flatten)]
180 details: SchemaDetails,
181 },
182 Typed {
184 #[serde(rename = "type")]
185 schema_type: SchemaType,
186 #[serde(flatten)]
187 details: SchemaDetails,
188 },
189 Untyped {
191 #[serde(flatten)]
192 details: SchemaDetails,
193 },
194}
195
196#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
197#[serde(rename_all = "lowercase")]
198pub enum SchemaType {
199 String,
200 Integer,
201 Number,
202 Boolean,
203 Array,
204 Object,
205 #[serde(rename = "null")]
206 Null,
207}
208
209#[derive(Debug, Clone, Default, Deserialize, Serialize)]
210pub struct SchemaDetails {
211 #[serde(skip_serializing_if = "Option::is_none")]
212 pub description: Option<String>,
213 #[serde(skip_serializing_if = "Option::is_none")]
214 pub nullable: Option<bool>,
215
216 #[serde(rename = "$recursiveAnchor", skip_serializing_if = "Option::is_none")]
218 pub recursive_anchor: Option<bool>,
219
220 #[serde(rename = "$dynamicAnchor", skip_serializing_if = "Option::is_none")]
222 pub dynamic_anchor: Option<String>,
223 #[serde(rename = "$id", skip_serializing_if = "Option::is_none")]
224 pub schema_id: Option<String>,
225
226 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
228 pub enum_values: Option<Vec<Value>>,
229 #[serde(skip_serializing_if = "Option::is_none")]
230 pub format: Option<String>,
231 #[serde(skip_serializing_if = "Option::is_none")]
232 pub default: Option<Value>,
233 #[serde(
234 rename = "const",
235 default,
236 deserialize_with = "deserialize_present_value",
237 skip_serializing_if = "Option::is_none"
238 )]
239 pub const_value: Option<Value>,
240
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub properties: Option<BTreeMap<String, Schema>>,
244 #[serde(skip_serializing_if = "Option::is_none")]
245 pub required: Option<Vec<String>>,
246 #[serde(
247 rename = "additionalProperties",
248 skip_serializing_if = "Option::is_none"
249 )]
250 pub additional_properties: Option<AdditionalProperties>,
251
252 #[serde(skip_serializing_if = "Option::is_none")]
254 pub items: Option<Items>,
255
256 #[serde(skip_serializing_if = "Option::is_none")]
258 pub minimum: Option<f64>,
259 #[serde(skip_serializing_if = "Option::is_none")]
260 pub maximum: Option<f64>,
261
262 #[serde(rename = "minLength", skip_serializing_if = "Option::is_none")]
264 pub min_length: Option<u64>,
265 #[serde(rename = "maxLength", skip_serializing_if = "Option::is_none")]
266 pub max_length: Option<u64>,
267 #[serde(skip_serializing_if = "Option::is_none")]
268 pub pattern: Option<String>,
269 #[serde(rename = "exclusiveMinimum", skip_serializing_if = "Option::is_none")]
273 pub exclusive_minimum: Option<ExclusiveBound>,
274 #[serde(rename = "exclusiveMaximum", skip_serializing_if = "Option::is_none")]
275 pub exclusive_maximum: Option<ExclusiveBound>,
276 #[serde(rename = "multipleOf", skip_serializing_if = "Option::is_none")]
277 pub multiple_of: Option<f64>,
278 #[serde(rename = "minItems", skip_serializing_if = "Option::is_none")]
279 pub min_items: Option<u64>,
280 #[serde(rename = "maxItems", skip_serializing_if = "Option::is_none")]
281 pub max_items: Option<u64>,
282 #[serde(rename = "uniqueItems", skip_serializing_if = "Option::is_none")]
283 pub unique_items: Option<bool>,
284 #[serde(rename = "minProperties", skip_serializing_if = "Option::is_none")]
285 pub min_properties: Option<u64>,
286 #[serde(rename = "maxProperties", skip_serializing_if = "Option::is_none")]
287 pub max_properties: Option<u64>,
288
289 #[serde(rename = "prefixItems", skip_serializing_if = "Option::is_none")]
291 pub prefix_items: Option<Vec<Schema>>,
292 #[serde(skip_serializing_if = "Option::is_none")]
293 pub contains: Option<Box<Schema>>,
294 #[serde(rename = "minContains", skip_serializing_if = "Option::is_none")]
295 pub min_contains: Option<u64>,
296 #[serde(rename = "maxContains", skip_serializing_if = "Option::is_none")]
297 pub max_contains: Option<u64>,
298
299 #[serde(rename = "patternProperties", skip_serializing_if = "Option::is_none")]
301 pub pattern_properties: Option<BTreeMap<String, Schema>>,
302 #[serde(rename = "propertyNames", skip_serializing_if = "Option::is_none")]
303 pub property_names: Option<Box<Schema>>,
304 #[serde(
305 rename = "unevaluatedProperties",
306 skip_serializing_if = "Option::is_none"
307 )]
308 pub unevaluated_properties: Option<AdditionalProperties>,
309 #[serde(rename = "unevaluatedItems", skip_serializing_if = "Option::is_none")]
310 pub unevaluated_items: Option<AdditionalProperties>,
311 #[serde(rename = "dependentRequired", skip_serializing_if = "Option::is_none")]
312 pub dependent_required: Option<BTreeMap<String, Vec<String>>>,
313 #[serde(rename = "dependentSchemas", skip_serializing_if = "Option::is_none")]
314 pub dependent_schemas: Option<BTreeMap<String, Schema>>,
315
316 #[serde(rename = "contentEncoding", skip_serializing_if = "Option::is_none")]
318 pub content_encoding: Option<String>,
319 #[serde(rename = "contentMediaType", skip_serializing_if = "Option::is_none")]
320 pub content_media_type: Option<String>,
321 #[serde(rename = "contentSchema", skip_serializing_if = "Option::is_none")]
322 pub content_schema: Option<Box<Schema>>,
323
324 #[serde(rename = "if", skip_serializing_if = "Option::is_none")]
326 pub if_schema: Option<Box<Schema>>,
327 #[serde(rename = "then", skip_serializing_if = "Option::is_none")]
328 pub then_schema: Option<Box<Schema>>,
329 #[serde(rename = "else", skip_serializing_if = "Option::is_none")]
330 pub else_schema: Option<Box<Schema>>,
331 #[serde(skip_serializing_if = "Option::is_none")]
332 pub not: Option<Box<Schema>>,
333
334 #[serde(skip_serializing_if = "Option::is_none")]
336 pub title: Option<String>,
337 #[serde(skip_serializing_if = "Option::is_none")]
338 pub deprecated: Option<bool>,
339 #[serde(rename = "readOnly", skip_serializing_if = "Option::is_none")]
340 pub read_only: Option<bool>,
341 #[serde(rename = "writeOnly", skip_serializing_if = "Option::is_none")]
342 pub write_only: Option<bool>,
343 #[serde(skip_serializing_if = "Option::is_none")]
344 pub examples: Option<Vec<Value>>,
345 #[serde(skip_serializing_if = "Option::is_none")]
346 pub example: Option<Value>,
347 #[serde(rename = "$comment", skip_serializing_if = "Option::is_none")]
349 pub comment: Option<String>,
350 #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
351 pub schema_keyword: Option<String>,
352 #[serde(rename = "$defs", skip_serializing_if = "Option::is_none")]
353 pub defs: Option<BTreeMap<String, Schema>>,
354
355 #[serde(flatten)]
358 pub extra: BTreeMap<String, Value>,
359}
360
361fn deserialize_present_value<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
362where
363 D: serde::Deserializer<'de>,
364{
365 Value::deserialize(deserializer).map(Some)
366}
367
368#[derive(Debug, Clone, Deserialize, Serialize)]
371#[serde(untagged)]
372pub enum ExclusiveBound {
373 Bool(bool),
374 Number(f64),
375}
376
377#[derive(Debug, Clone, Deserialize, Serialize)]
387#[serde(untagged)]
388pub enum Items {
389 Single(Box<Schema>),
391 Positional(Vec<Schema>),
393 Bool(bool),
397}
398
399#[derive(Debug, Clone, Deserialize, Serialize)]
400#[serde(untagged)]
401pub enum AdditionalProperties {
402 Boolean(bool),
403 Schema(Box<Schema>),
404}
405
406#[derive(Debug, Clone, Deserialize, Serialize)]
408pub struct Example {
409 #[serde(default)]
410 pub summary: Option<String>,
411 #[serde(default)]
412 pub description: Option<String>,
413 #[serde(default)]
415 pub value: Option<Value>,
416 #[serde(rename = "externalValue", default)]
417 pub external_value: Option<String>,
418 #[serde(rename = "dataValue", default)]
420 pub data_value: Option<Value>,
421 #[serde(rename = "serializedValue", default)]
423 pub serialized_value: Option<String>,
424 #[serde(rename = "$ref", default)]
425 pub reference: Option<String>,
426 #[serde(flatten, default)]
427 pub extensions: Extensions,
428}
429
430#[derive(Debug, Clone, Deserialize, Serialize)]
432pub struct Link {
433 #[serde(rename = "operationRef", default)]
434 pub operation_ref: Option<String>,
435 #[serde(rename = "operationId", default)]
436 pub operation_id: Option<String>,
437 #[serde(default)]
438 pub parameters: Option<BTreeMap<String, Value>>,
439 #[serde(rename = "requestBody", default)]
440 pub request_body: Option<Value>,
441 #[serde(default)]
442 pub description: Option<String>,
443 #[serde(default)]
444 pub server: Option<Server>,
445 #[serde(rename = "$ref", default)]
446 pub reference: Option<String>,
447 #[serde(flatten, default)]
448 pub extensions: Extensions,
449}
450
451#[derive(Debug, Clone, Deserialize, Serialize)]
454#[serde(transparent)]
455pub struct Callback(pub BTreeMap<String, PathItem>);
456
457#[derive(Debug, Clone, Deserialize, Serialize)]
460pub struct Encoding {
461 #[serde(rename = "contentType", default)]
462 pub content_type: Option<String>,
463 #[serde(default)]
464 pub headers: Option<BTreeMap<String, Header>>,
465 #[serde(default)]
466 pub style: Option<String>,
467 #[serde(default)]
468 pub explode: Option<bool>,
469 #[serde(rename = "allowReserved", default)]
470 pub allow_reserved: Option<bool>,
471 #[serde(rename = "itemEncoding", default)]
473 pub item_encoding: Option<Box<Encoding>>,
474 #[serde(flatten, default)]
475 pub extensions: Extensions,
476}
477
478#[derive(Debug, Clone, Deserialize, Serialize)]
482pub struct Header {
483 #[serde(default)]
484 pub description: Option<String>,
485 #[serde(default)]
486 pub required: Option<bool>,
487 #[serde(default)]
488 pub deprecated: Option<bool>,
489 #[serde(rename = "allowEmptyValue", default)]
490 pub allow_empty_value: Option<bool>,
491 #[serde(default)]
492 pub style: Option<String>,
493 #[serde(default)]
494 pub explode: Option<bool>,
495 #[serde(rename = "allowReserved", default)]
496 pub allow_reserved: Option<bool>,
497 #[serde(default)]
498 pub schema: Option<Schema>,
499 #[serde(default)]
500 pub content: Option<BTreeMap<String, MediaType>>,
501 #[serde(default)]
502 pub example: Option<Value>,
503 #[serde(default)]
504 pub examples: Option<Value>,
505 #[serde(rename = "$ref", default)]
506 pub reference: Option<String>,
507 #[serde(flatten, default)]
508 pub extensions: Extensions,
509}
510
511#[derive(Debug, Clone, Deserialize, Serialize)]
515#[serde(tag = "type")]
516pub enum SecurityScheme {
517 #[serde(rename = "apiKey")]
518 ApiKey {
519 name: String,
520 #[serde(rename = "in")]
521 location: String, #[serde(default)]
523 description: Option<String>,
524 #[serde(default)]
526 deprecated: Option<bool>,
527 #[serde(flatten, default)]
528 extensions: Extensions,
529 },
530 #[serde(rename = "http")]
531 Http {
532 scheme: String, #[serde(rename = "bearerFormat", default)]
534 bearer_format: Option<String>,
535 #[serde(default)]
536 description: Option<String>,
537 #[serde(default)]
538 deprecated: Option<bool>,
539 #[serde(flatten, default)]
540 extensions: Extensions,
541 },
542 #[serde(rename = "mutualTLS")]
543 MutualTls {
544 #[serde(default)]
545 description: Option<String>,
546 #[serde(default)]
547 deprecated: Option<bool>,
548 #[serde(flatten, default)]
549 extensions: Extensions,
550 },
551 #[serde(rename = "oauth2")]
552 OAuth2 {
553 flows: Box<OAuthFlows>,
557 #[serde(default)]
558 description: Option<String>,
559 #[serde(rename = "oauth2MetadataUrl", default)]
561 oauth2_metadata_url: Option<String>,
562 #[serde(default)]
563 deprecated: Option<bool>,
564 #[serde(flatten, default)]
565 extensions: Extensions,
566 },
567 #[serde(rename = "openIdConnect")]
568 OpenIdConnect {
569 #[serde(rename = "openIdConnectUrl")]
570 open_id_connect_url: String,
571 #[serde(default)]
572 description: Option<String>,
573 #[serde(default)]
574 deprecated: Option<bool>,
575 #[serde(flatten, default)]
576 extensions: Extensions,
577 },
578}
579
580#[derive(Debug, Clone, Deserialize, Serialize)]
581pub struct OAuthFlows {
582 #[serde(default)]
583 pub implicit: Option<OAuthFlow>,
584 #[serde(default)]
585 pub password: Option<OAuthFlow>,
586 #[serde(rename = "clientCredentials", default)]
587 pub client_credentials: Option<OAuthFlow>,
588 #[serde(rename = "authorizationCode", default)]
589 pub authorization_code: Option<OAuthFlow>,
590 #[serde(rename = "deviceAuthorization", default)]
592 pub device_authorization: Option<OAuthFlow>,
593 #[serde(flatten, default)]
594 pub extensions: Extensions,
595}
596
597#[derive(Debug, Clone, Deserialize, Serialize)]
598pub struct OAuthFlow {
599 #[serde(rename = "authorizationUrl", default)]
600 pub authorization_url: Option<String>,
601 #[serde(rename = "tokenUrl", default)]
602 pub token_url: Option<String>,
603 #[serde(rename = "refreshUrl", default)]
604 pub refresh_url: Option<String>,
605 #[serde(rename = "deviceAuthorizationUrl", default)]
607 pub device_authorization_url: Option<String>,
608 pub scopes: BTreeMap<String, String>,
609 #[serde(flatten, default)]
610 pub extensions: Extensions,
611}
612
613#[derive(Debug, Clone, Deserialize, Serialize)]
615pub struct ExternalDocs {
616 pub url: String,
617 #[serde(default)]
618 pub description: Option<String>,
619 #[serde(flatten, default)]
620 pub extensions: Extensions,
621}
622
623#[derive(Debug, Clone, Deserialize, Serialize)]
625pub struct Tag {
626 pub name: String,
627 #[serde(default)]
629 pub summary: Option<String>,
630 #[serde(default)]
631 pub description: Option<String>,
632 #[serde(default)]
634 pub parent: Option<String>,
635 #[serde(default)]
639 pub kind: Option<String>,
640 #[serde(rename = "externalDocs", default)]
641 pub external_docs: Option<ExternalDocs>,
642 #[serde(flatten, default)]
643 pub extensions: Extensions,
644}
645
646#[derive(Debug, Clone, Deserialize, Serialize)]
649pub struct Server {
650 pub url: String,
651 #[serde(default)]
653 pub name: Option<String>,
654 #[serde(default)]
655 pub description: Option<String>,
656 #[serde(default)]
657 pub variables: Option<BTreeMap<String, ServerVariable>>,
658 #[serde(flatten, default)]
659 pub extensions: Extensions,
660}
661
662#[derive(Debug, Clone, Deserialize, Serialize)]
663pub struct ServerVariable {
664 #[serde(default)]
666 pub default: Option<String>,
667 #[serde(rename = "enum", default)]
668 pub enum_values: Option<Vec<String>>,
669 #[serde(default)]
670 pub description: Option<String>,
671 #[serde(flatten, default)]
672 pub extensions: Extensions,
673}
674
675#[derive(Debug, Clone, Deserialize, Serialize)]
676pub struct Discriminator {
677 #[serde(rename = "propertyName")]
678 pub property_name: String,
679 #[serde(default)]
680 pub mapping: Option<BTreeMap<String, String>>,
681 #[serde(rename = "defaultMapping", default)]
685 pub default_mapping: Option<String>,
686 #[serde(flatten, default)]
687 pub extensions: Extensions,
688}
689
690impl Schema {
691 pub fn schema_type(&self) -> Option<&SchemaType> {
698 match self {
699 Schema::Typed { schema_type, .. } => Some(schema_type),
700 Schema::TypedMulti { schema_types, .. } => schema_types
701 .iter()
702 .find(|t| **t != SchemaType::Null)
703 .or_else(|| schema_types.first()),
704 _ => None,
705 }
706 }
707
708 pub fn declared_type(&self) -> Option<&SchemaType> {
715 match self {
716 Schema::AnyOf { schema_type, .. }
717 | Schema::AllOf { schema_type, .. }
718 | Schema::OneOf { schema_type, .. } => schema_type.as_ref(),
719 other => other.schema_type(),
720 }
721 }
722
723 pub fn non_null_schema_types(&self) -> Option<Vec<SchemaType>> {
730 match self {
731 Schema::TypedMulti { schema_types, .. } => {
732 let mut non_null = Vec::new();
733 for t in schema_types {
734 if *t != SchemaType::Null && !non_null.contains(t) {
735 non_null.push(t.clone());
736 }
737 }
738 (non_null.len() > 1).then_some(non_null)
739 }
740 _ => None,
741 }
742 }
743
744 pub fn type_array_contains_null(&self) -> bool {
747 match self {
748 Schema::TypedMulti { schema_types, .. } => schema_types.contains(&SchemaType::Null),
749 _ => false,
750 }
751 }
752
753 pub fn is_nullable_any(&self) -> bool {
765 self.details().is_nullable()
766 || self.type_array_contains_null()
767 || self.is_nullable_pattern()
768 }
769
770 pub fn details(&self) -> &SchemaDetails {
772 static EMPTY_DETAILS: Lazy<SchemaDetails> = Lazy::new(SchemaDetails::default);
773 match self {
774 Schema::Typed { details, .. } => details,
775 Schema::TypedMulti { details, .. } => details,
776 Schema::Reference { .. } | Schema::RecursiveRef { .. } | Schema::DynamicRef { .. } => {
777 &EMPTY_DETAILS
778 }
779 Schema::OneOf { details, .. } => details,
780 Schema::AnyOf { details, .. } => details,
781 Schema::AllOf { details, .. } => details,
782 Schema::Untyped { details } => details,
783 }
784 }
785
786 pub fn details_mut(&mut self) -> &mut SchemaDetails {
788 match self {
789 Schema::Typed { details, .. } => details,
790 Schema::TypedMulti { details, .. } => details,
791 Schema::Reference { .. } => {
792 panic!("Cannot get mutable details for reference schema")
793 }
794 Schema::RecursiveRef { .. } => {
795 panic!("Cannot get mutable details for recursive reference schema")
796 }
797 Schema::DynamicRef { .. } => {
798 panic!("Cannot get mutable details for dynamic reference schema")
799 }
800 Schema::OneOf { details, .. } => details,
801 Schema::AnyOf { details, .. } => details,
802 Schema::AllOf { details, .. } => details,
803 Schema::Untyped { details } => details,
804 }
805 }
806
807 pub fn is_reference(&self) -> bool {
809 matches!(self, Schema::Reference { .. } | Schema::RecursiveRef { .. })
810 }
811
812 pub fn reference(&self) -> Option<&str> {
814 match self {
815 Schema::Reference { reference, .. } => Some(reference),
816 _ => None,
817 }
818 }
819
820 pub fn recursive_reference(&self) -> Option<&str> {
822 match self {
823 Schema::RecursiveRef { recursive_ref, .. } => Some(recursive_ref),
824 _ => None,
825 }
826 }
827
828 pub fn is_discriminated_union(&self) -> bool {
830 match self {
831 Schema::OneOf { discriminator, .. } => discriminator.is_some(),
832 Schema::AnyOf { discriminator, .. } => discriminator.is_some(),
833 _ => false,
834 }
835 }
836
837 pub fn discriminator(&self) -> Option<&Discriminator> {
839 match self {
840 Schema::OneOf { discriminator, .. } => discriminator.as_ref(),
841 Schema::AnyOf { discriminator, .. } => discriminator.as_ref(),
842 _ => None,
843 }
844 }
845
846 pub fn union_variants(&self) -> Option<&[Schema]> {
848 match self {
849 Schema::OneOf { one_of, .. } => Some(one_of),
850 Schema::AnyOf { any_of, .. } => Some(any_of),
851 _ => None,
852 }
853 }
854
855 pub fn is_nullable_pattern(&self) -> bool {
857 self.non_null_variant().is_some()
858 }
859
860 pub fn non_null_variant(&self) -> Option<&Schema> {
863 let variants = match self {
864 Schema::AnyOf { any_of, .. } => any_of,
865 Schema::OneOf { one_of, .. } => one_of,
866 _ => return None,
867 };
868 let [first, second] = variants.as_slice() else {
869 return None;
870 };
871 match (
872 Self::is_null_marker_beside(first, second),
873 Self::is_null_marker_beside(second, first),
874 ) {
875 (true, false) => Some(second),
876 (false, true) => Some(first),
877 _ => None,
879 }
880 }
881
882 fn is_null_marker_beside(candidate: &Schema, sibling: &Schema) -> bool {
898 matches!(candidate.schema_type(), Some(SchemaType::Null))
899 || (candidate.is_empty_nullable_object() && sibling.reference().is_some())
900 }
901
902 fn is_empty_nullable_object(&self) -> bool {
904 let details = self.details();
905 if !details.is_nullable() {
906 return false;
907 }
908 if !matches!(
909 self.schema_type(),
910 Some(SchemaType::Object) | Some(SchemaType::Null) | None
911 ) {
912 return false;
913 }
914 self.reference().is_none()
915 && details.properties.as_ref().is_none_or(BTreeMap::is_empty)
916 && details.additional_properties.is_none()
917 && details.enum_values.is_none()
918 && details.const_value.is_none()
919 && details.pattern_properties.is_none()
920 && details.items.is_none()
921 && details.prefix_items.is_none()
922 && self.all_of_len() == 0
923 }
924
925 fn all_of_len(&self) -> usize {
927 match self {
928 Schema::AllOf { all_of, .. } => all_of.len(),
929 _ => 0,
930 }
931 }
932
933 pub fn inferred_type(&self) -> Option<SchemaType> {
935 match self {
936 Schema::Typed { schema_type, .. } => Some(schema_type.clone()),
937 Schema::TypedMulti { .. } => self.schema_type().cloned(),
938 Schema::Untyped { details } => {
939 if details.properties.is_some() {
941 Some(SchemaType::Object)
942 } else if details.items.is_some() || details.prefix_items.is_some() {
943 Some(SchemaType::Array)
944 } else if details.enum_values.is_some() {
945 Some(SchemaType::String) } else {
947 None
948 }
949 }
950 _ => None,
951 }
952 }
953}
954
955impl SchemaDetails {
956 pub fn item_schema(&self) -> Option<&Schema> {
962 match self.items.as_ref()? {
963 Items::Single(schema) => Some(schema),
964 Items::Positional(_) | Items::Bool(_) => None,
965 }
966 }
967
968 pub fn positional_items(&self) -> Option<&[Schema]> {
971 if let Some(prefix_items) = self.prefix_items.as_deref() {
972 return Some(prefix_items);
973 }
974 match self.items.as_ref()? {
975 Items::Positional(schemas) => Some(schemas),
976 Items::Single(_) | Items::Bool(_) => None,
977 }
978 }
979
980 pub fn positional_items_are_closed(&self) -> bool {
986 let Some(positions) = self.positional_items() else {
987 return false;
988 };
989 if matches!(self.items, Some(Items::Bool(false))) {
990 return true;
991 }
992 if self.extra.get("additionalItems") == Some(&Value::Bool(false)) {
993 return true;
994 }
995 self.max_items
996 .is_some_and(|maximum| maximum <= positions.len() as u64)
997 }
998
999 pub fn positional_items_are_exact(&self) -> bool {
1003 let Some(positions) = self.positional_items() else {
1004 return false;
1005 };
1006 self.positional_items_are_closed()
1007 && self
1008 .min_items
1009 .is_some_and(|minimum| minimum >= positions.len() as u64)
1010 }
1011
1012 pub fn is_nullable(&self) -> bool {
1014 self.nullable.unwrap_or(false)
1015 }
1016
1017 pub fn is_string_enum(&self) -> bool {
1023 self.enum_values.is_some() || self.const_string_value().is_some()
1024 }
1025
1026 pub fn string_enum_values(&self) -> Option<Vec<String>> {
1032 if let Some(values) = self.enum_values.as_ref() {
1033 return Some(
1038 values
1039 .iter()
1040 .map(|v| match v {
1041 Value::String(s) => s.clone(),
1042 Value::Number(n) => n.to_string(),
1043 Value::Bool(b) => b.to_string(),
1044 Value::Null => "null".to_string(),
1045 _ => v.to_string(),
1046 })
1047 .collect(),
1048 );
1049 }
1050 self.const_string_value().map(|s| vec![s])
1051 }
1052
1053 fn const_string_value(&self) -> Option<String> {
1054 self.const_value
1055 .as_ref()
1056 .and_then(|v| v.as_str())
1057 .map(|s| s.to_string())
1058 }
1059
1060 pub fn is_field_required(&self, field_name: &str) -> bool {
1062 self.required
1063 .as_ref()
1064 .map(|req| req.contains(&field_name.to_string()))
1065 .unwrap_or(false)
1066 }
1067}
1068
1069#[derive(Debug, Clone, Deserialize, Serialize)]
1071pub struct PathItem {
1072 #[serde(default)]
1073 pub summary: Option<String>,
1074 #[serde(default)]
1075 pub description: Option<String>,
1076 pub get: Option<Operation>,
1077 pub put: Option<Operation>,
1078 pub post: Option<Operation>,
1079 pub delete: Option<Operation>,
1080 pub options: Option<Operation>,
1081 pub head: Option<Operation>,
1082 pub patch: Option<Operation>,
1083 pub trace: Option<Operation>,
1084 pub query: Option<Operation>,
1087 #[serde(rename = "additionalOperations", default)]
1091 pub additional_operations: Option<BTreeMap<String, Operation>>,
1092 pub parameters: Option<Vec<Parameter>>,
1093 #[serde(default)]
1094 pub servers: Option<Vec<Server>>,
1095 #[serde(rename = "$ref", default)]
1096 pub reference: Option<String>,
1097 #[serde(flatten, default)]
1098 pub extensions: Extensions,
1099}
1100
1101impl PathItem {
1102 pub fn operations(&self) -> Vec<(&str, &Operation)> {
1105 let mut ops = Vec::new();
1106 if let Some(ref op) = self.get {
1107 ops.push(("get", op));
1108 }
1109 if let Some(ref op) = self.put {
1110 ops.push(("put", op));
1111 }
1112 if let Some(ref op) = self.post {
1113 ops.push(("post", op));
1114 }
1115 if let Some(ref op) = self.delete {
1116 ops.push(("delete", op));
1117 }
1118 if let Some(ref op) = self.options {
1119 ops.push(("options", op));
1120 }
1121 if let Some(ref op) = self.head {
1122 ops.push(("head", op));
1123 }
1124 if let Some(ref op) = self.patch {
1125 ops.push(("patch", op));
1126 }
1127 if let Some(ref op) = self.trace {
1128 ops.push(("trace", op));
1129 }
1130 if let Some(ref op) = self.query {
1131 ops.push(("query", op));
1132 }
1133 if let Some(map) = &self.additional_operations {
1134 for (verb, op) in map {
1135 ops.push((verb.as_str(), op));
1136 }
1137 }
1138 ops
1139 }
1140}
1141
1142#[derive(Debug, Clone, Deserialize, Serialize)]
1144pub struct Operation {
1145 #[serde(rename = "operationId", default)]
1146 pub operation_id: Option<String>,
1147 #[serde(default)]
1148 pub summary: Option<String>,
1149 #[serde(default)]
1150 pub description: Option<String>,
1151 #[serde(default)]
1152 pub tags: Option<Vec<String>>,
1153 #[serde(default)]
1154 pub deprecated: Option<bool>,
1155 pub parameters: Option<Vec<Parameter>>,
1156 #[serde(rename = "requestBody")]
1157 pub request_body: Option<RequestBody>,
1158 pub responses: Option<BTreeMap<String, Response>>,
1159 #[serde(default)]
1160 pub callbacks: Option<BTreeMap<String, Callback>>,
1161 #[serde(default)]
1162 pub security: Option<Vec<BTreeMap<String, Vec<String>>>>,
1163 #[serde(default)]
1164 pub servers: Option<Vec<Server>>,
1165 #[serde(rename = "externalDocs", default)]
1166 pub external_docs: Option<ExternalDocs>,
1167 #[serde(flatten, default)]
1168 pub extensions: Extensions,
1169}
1170
1171#[derive(Debug, Clone, Deserialize, Serialize)]
1173pub struct Parameter {
1174 #[serde(default)]
1175 pub name: Option<String>,
1176 #[serde(rename = "in", default)]
1177 pub location: Option<String>,
1178 #[serde(default)]
1179 pub required: Option<bool>,
1180 #[serde(default)]
1181 pub deprecated: Option<bool>,
1182 #[serde(rename = "allowEmptyValue", default)]
1183 pub allow_empty_value: Option<bool>,
1184 #[serde(default)]
1185 pub style: Option<String>,
1186 #[serde(default)]
1187 pub explode: Option<bool>,
1188 #[serde(rename = "allowReserved", default)]
1189 pub allow_reserved: Option<bool>,
1190 #[serde(default)]
1191 pub schema: Option<Schema>,
1192 #[serde(default)]
1193 pub content: Option<BTreeMap<String, MediaType>>,
1194 #[serde(default)]
1195 pub example: Option<Value>,
1196 #[serde(default)]
1197 pub examples: Option<BTreeMap<String, Example>>,
1198 #[serde(default)]
1199 pub description: Option<String>,
1200 #[serde(rename = "$ref", default)]
1201 pub reference: Option<String>,
1202 #[serde(flatten, default)]
1203 pub extensions: Extensions,
1204}
1205
1206#[derive(Debug, Clone, Deserialize, Serialize)]
1208pub struct RequestBody {
1209 pub content: Option<BTreeMap<String, MediaType>>,
1210 #[serde(default)]
1211 pub description: Option<String>,
1212 #[serde(default)]
1213 pub required: Option<bool>,
1214 #[serde(rename = "$ref", default)]
1215 pub reference: Option<String>,
1216 #[serde(flatten, default)]
1217 pub extensions: Extensions,
1218}
1219
1220#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1226#[serde(rename_all = "snake_case")]
1227pub enum ResponseMediaKind {
1228 Json,
1229 EventStream,
1230 Text,
1231 Binary,
1232 Unsupported,
1233}
1234
1235pub fn media_type_essence(content_type: &str) -> &str {
1241 content_type
1242 .split(';')
1243 .next()
1244 .unwrap_or(content_type)
1245 .trim()
1246}
1247
1248pub fn is_json_media_type(ct: &str) -> bool {
1256 let essence = media_type_essence(ct).to_ascii_lowercase();
1257 if essence == "application/json" {
1258 return true;
1259 }
1260 if let Some(subtype) = essence.strip_prefix("application/") {
1261 return subtype.ends_with("+json");
1262 }
1263 false
1264}
1265
1266pub fn is_form_urlencoded_media_type(ct: &str) -> bool {
1269 let essence = media_type_essence(ct).to_ascii_lowercase();
1270 essence == "application/x-www-form-urlencoded"
1271}
1272
1273pub fn is_event_stream_media_type(ct: &str) -> bool {
1279 media_type_essence(ct).eq_ignore_ascii_case("text/event-stream")
1280}
1281
1282pub fn is_text_media_type(ct: &str) -> bool {
1290 let Some((top_level, subtype)) = media_type_essence(ct).split_once('/') else {
1291 return false;
1292 };
1293 if top_level.eq_ignore_ascii_case("text")
1294 && !subtype.is_empty()
1295 && !is_event_stream_media_type(ct)
1296 {
1297 return true;
1298 }
1299 top_level.eq_ignore_ascii_case("application")
1300 && (subtype.eq_ignore_ascii_case("xml")
1301 || subtype.to_ascii_lowercase().ends_with("+xml")
1302 || subtype.eq_ignore_ascii_case("jwt"))
1305}
1306
1307pub fn is_wildcard_media_type(ct: &str) -> bool {
1313 let Some((top_level, subtype)) = media_type_essence(ct).split_once('/') else {
1314 return false;
1315 };
1316 !top_level.is_empty() && subtype == "*"
1317}
1318
1319fn schema_has_binary_format(schema: Option<&Schema>) -> bool {
1320 schema.is_some_and(|schema| {
1321 schema
1322 .details()
1323 .format
1324 .as_deref()
1325 .is_some_and(|format| format.eq_ignore_ascii_case("binary"))
1326 })
1327}
1328
1329pub fn is_binary_media_type(ct: &str, schema: Option<&Schema>) -> bool {
1337 if schema_has_binary_format(schema) {
1338 return true;
1339 }
1340
1341 let essence = media_type_essence(ct);
1342 let Some((top_level, _)) = essence.split_once('/') else {
1343 return false;
1344 };
1345 if top_level.eq_ignore_ascii_case("image")
1346 || top_level.eq_ignore_ascii_case("audio")
1347 || top_level.eq_ignore_ascii_case("video")
1348 {
1349 return true;
1350 }
1351 if essence.eq_ignore_ascii_case("application/octet-stream")
1352 || essence.eq_ignore_ascii_case("application/zip")
1353 || essence.eq_ignore_ascii_case("application/pdf")
1354 {
1355 return true;
1356 }
1357
1358 !top_level.eq_ignore_ascii_case("text") && is_wildcard_media_type(ct)
1359}
1360
1361pub fn classify_response_media_type(ct: &str, schema: Option<&Schema>) -> ResponseMediaKind {
1366 if is_json_media_type(ct) {
1367 ResponseMediaKind::Json
1368 } else if is_event_stream_media_type(ct) {
1369 ResponseMediaKind::EventStream
1370 } else if schema_has_binary_format(schema) {
1371 ResponseMediaKind::Binary
1372 } else if is_text_media_type(ct) {
1373 if is_wildcard_media_type(ct) {
1374 ResponseMediaKind::Unsupported
1375 } else {
1376 ResponseMediaKind::Text
1377 }
1378 } else if is_binary_media_type(ct, schema) {
1379 ResponseMediaKind::Binary
1380 } else {
1381 ResponseMediaKind::Unsupported
1382 }
1383}
1384
1385fn find_json_content(content: &BTreeMap<String, MediaType>) -> Option<(&str, &MediaType)> {
1386 if let Some(mt) = content
1387 .get("application/json")
1388 .filter(|media_type| media_type.schema.is_some())
1389 {
1390 return Some(("application/json", mt));
1391 }
1392 content
1393 .iter()
1394 .find(|(ct, media_type)| is_json_media_type(ct) && media_type.schema.is_some())
1395 .map(|(ct, mt)| (ct.as_str(), mt))
1396 .or_else(|| {
1397 content
1398 .get("application/json")
1399 .map(|media_type| ("application/json", media_type))
1400 })
1401 .or_else(|| {
1402 content
1403 .iter()
1404 .find(|(ct, _)| is_json_media_type(ct))
1405 .map(|(ct, mt)| (ct.as_str(), mt))
1406 })
1407}
1408
1409impl RequestBody {
1410 pub fn json_schema(&self) -> Option<&Schema> {
1416 self.content
1417 .as_ref()
1418 .and_then(find_json_content)
1419 .and_then(|(_, media_type)| media_type.schema.as_ref())
1420 }
1421
1422 pub fn best_content(&self) -> Option<(&str, Option<&Schema>)> {
1424 let content = self.content.as_ref()?;
1425
1426 if let Some((ct, media_type)) = find_json_content(content) {
1427 return Some((ct, media_type.schema.as_ref()));
1428 }
1429
1430 const PRIORITY: &[&str] = &[
1431 "application/x-www-form-urlencoded",
1432 "multipart/form-data",
1433 "application/octet-stream",
1434 "text/plain",
1435 ];
1436 for preferred_essence in PRIORITY {
1437 if let Some((ct, media_type)) = content
1438 .iter()
1439 .find(|(ct, _)| media_type_essence(ct).eq_ignore_ascii_case(preferred_essence))
1440 {
1441 return Some((ct.as_str(), media_type.schema.as_ref()));
1442 }
1443 }
1444 if let Some((ct, media_type)) = content.iter().find(|(ct, _)| is_text_media_type(ct)) {
1447 return Some((ct.as_str(), media_type.schema.as_ref()));
1448 }
1449 content
1450 .iter()
1451 .find(|(ct, media_type)| {
1457 !is_wildcard_media_type(ct) && is_binary_media_type(ct, media_type.schema.as_ref())
1458 })
1459 .map(|(ct, media_type)| (ct.as_str(), media_type.schema.as_ref()))
1460 }
1461}
1462
1463#[derive(Debug, Clone, Deserialize, Serialize)]
1465pub struct Response {
1466 #[serde(default)]
1467 pub description: Option<String>,
1468 #[serde(default)]
1469 pub headers: Option<BTreeMap<String, Header>>,
1470 #[serde(default)]
1471 pub content: Option<BTreeMap<String, MediaType>>,
1472 #[serde(default)]
1473 pub links: Option<Value>,
1474 #[serde(rename = "$ref", default)]
1475 pub reference: Option<String>,
1476 #[serde(flatten, default)]
1477 pub extensions: Extensions,
1478}
1479
1480impl Response {
1481 pub fn json_schema(&self) -> Option<&Schema> {
1488 self.content
1489 .as_ref()
1490 .and_then(find_json_content)
1491 .and_then(|(_, media_type)| media_type.schema.as_ref())
1492 }
1493
1494 pub fn json_content(&self) -> Option<(&str, &Schema)> {
1496 self.content
1497 .as_ref()
1498 .and_then(find_json_content)
1499 .and_then(|(content_type, media_type)| {
1500 media_type
1501 .schema
1502 .as_ref()
1503 .map(|schema| (content_type, schema))
1504 })
1505 }
1506}
1507
1508#[derive(Debug, Clone, Deserialize, Serialize)]
1510pub struct MediaType {
1511 #[serde(default)]
1512 pub schema: Option<Schema>,
1513 #[serde(default)]
1514 pub example: Option<Value>,
1515 #[serde(default)]
1516 pub examples: Option<BTreeMap<String, Example>>,
1517 #[serde(default)]
1518 pub encoding: Option<BTreeMap<String, Encoding>>,
1519 #[serde(rename = "itemSchema", default)]
1522 pub item_schema: Option<Schema>,
1523 #[serde(rename = "prefixEncoding", default)]
1526 pub prefix_encoding: Option<Vec<Encoding>>,
1527 #[serde(rename = "itemEncoding", default)]
1530 pub item_encoding: Option<Encoding>,
1531 #[serde(rename = "$ref", default)]
1532 pub reference: Option<String>,
1533 #[serde(flatten, default)]
1534 pub extensions: Extensions,
1535}
1536
1537#[cfg(test)]
1538#[allow(clippy::unwrap_used, clippy::expect_used)]
1539mod tests {
1540 use super::*;
1541 use serde_json::json;
1542
1543 #[test]
1544 fn paths_map_skips_extension_scalars() {
1545 let spec: OpenApiSpec = serde_json::from_value(json!({
1548 "openapi": "3.0.0",
1549 "info": { "title": "lenient paths", "version": "1" },
1550 "paths": {
1551 "x-codegen-contextRoot": "/apis/registry/v2",
1552 "/items": {
1553 "get": {
1554 "operationId": "listItems",
1555 "responses": { "204": { "description": "ok" } }
1556 }
1557 }
1558 }
1559 }))
1560 .unwrap();
1561 let paths = spec.paths.unwrap();
1562 assert!(paths.contains_key("/items"));
1563 assert!(!paths.contains_key("x-codegen-contextRoot"));
1564 }
1565
1566 #[test]
1567 fn test_parse_simple_object_schema() {
1568 let schema_json = json!({
1569 "type": "object",
1570 "properties": {
1571 "name": {
1572 "type": "string",
1573 "description": "User name"
1574 },
1575 "age": {
1576 "type": "integer"
1577 }
1578 },
1579 "required": ["name"]
1580 });
1581
1582 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1583
1584 match schema {
1585 Schema::Typed {
1586 schema_type: SchemaType::Object,
1587 details,
1588 } => {
1589 assert!(details.properties.is_some());
1590 assert_eq!(details.required, Some(vec!["name".to_string()]));
1591 assert!(details.is_field_required("name"));
1592 assert!(!details.is_field_required("age"));
1593 }
1594 _ => panic!("Expected object schema"),
1595 }
1596 }
1597
1598 #[test]
1599 fn test_parse_string_enum() {
1600 let schema_json = json!({
1601 "type": "string",
1602 "enum": ["active", "inactive", "pending"],
1603 "description": "User status"
1604 });
1605
1606 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1607
1608 match schema {
1609 Schema::Typed {
1610 schema_type: SchemaType::String,
1611 details,
1612 } => {
1613 assert!(details.is_string_enum());
1614 let values = details.string_enum_values().unwrap();
1615 assert_eq!(values, vec!["active", "inactive", "pending"]);
1616 }
1617 _ => panic!("Expected string enum schema"),
1618 }
1619 }
1620
1621 #[test]
1622 fn test_parse_reference_schema() {
1623 let schema_json = json!({
1624 "$ref": "#/components/schemas/User"
1625 });
1626
1627 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1628
1629 assert!(schema.is_reference());
1630 assert_eq!(schema.reference(), Some("#/components/schemas/User"));
1631 }
1632
1633 #[test]
1634 fn test_parse_discriminated_union() {
1635 let schema_json = json!({
1636 "oneOf": [
1637 {"$ref": "#/components/schemas/Dog"},
1638 {"$ref": "#/components/schemas/Cat"}
1639 ],
1640 "discriminator": {
1641 "propertyName": "petType"
1642 }
1643 });
1644
1645 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1646
1647 assert!(schema.is_discriminated_union());
1648 let discriminator = schema.discriminator().unwrap();
1649 assert_eq!(discriminator.property_name, "petType");
1650 }
1651
1652 #[test]
1653 fn test_parse_nullable_pattern() {
1654 let schema_json = json!({
1655 "anyOf": [
1656 {"$ref": "#/components/schemas/User"},
1657 {"type": "null"}
1658 ]
1659 });
1660
1661 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1662
1663 assert!(schema.is_nullable_pattern());
1664 let non_null = schema.non_null_variant().unwrap();
1665 assert!(non_null.is_reference());
1666 }
1667
1668 #[test]
1669 fn is_json_media_type_accepts_canonical_and_structured_suffix() {
1670 assert!(is_json_media_type("application/json"));
1672 assert!(is_json_media_type("application/json; charset=utf-8"));
1674 assert!(is_json_media_type("APPLICATION/JSON"));
1675 assert!(is_json_media_type("application/vnd.api+json"));
1677 assert!(is_json_media_type("application/hal+json"));
1678 assert!(is_json_media_type("application/problem+json"));
1679 assert!(is_json_media_type("application/ld+json"));
1680 assert!(is_json_media_type(
1681 "application/vnd.api+json; charset=utf-8"
1682 ));
1683 assert!(!is_json_media_type("application/xml"));
1685 assert!(!is_json_media_type("application/x-www-form-urlencoded"));
1686 assert!(!is_json_media_type("text/plain"));
1687 assert!(!is_json_media_type("application/jsonbutnotreally"));
1688 assert!(!is_json_media_type("text/something+json"));
1690 }
1691
1692 #[test]
1693 fn response_media_helpers_normalize_parameters_and_case() {
1694 assert_eq!(
1695 media_type_essence(" Text/Plain ; charset=utf-8 "),
1696 "Text/Plain"
1697 );
1698 assert!(is_text_media_type("TEXT/HTML; charset=UTF-8"));
1699 assert!(!is_text_media_type("Text/Event-Stream; charset=utf-8"));
1700 assert!(is_wildcard_media_type("*/*; q=0.8"));
1701 assert!(is_wildcard_media_type("IMAGE/*"));
1702 assert!(is_wildcard_media_type("text/*"));
1703 assert!(!is_wildcard_media_type("image/*+json"));
1704 assert!(!is_wildcard_media_type("application/json"));
1705 }
1706
1707 #[test]
1708 fn response_media_classifier_keeps_json_sse_and_text_distinct() {
1709 for media_type in [
1710 "application/json",
1711 "APPLICATION/PROBLEM+JSON; charset=utf-8",
1712 ] {
1713 assert_eq!(
1714 classify_response_media_type(media_type, None),
1715 ResponseMediaKind::Json,
1716 "{media_type}"
1717 );
1718 }
1719
1720 assert_eq!(
1721 classify_response_media_type("Text/Event-Stream; charset=utf-8", None),
1722 ResponseMediaKind::EventStream
1723 );
1724 for media_type in ["text/plain", "TEXT/HTML; charset=UTF-8"] {
1725 assert_eq!(
1726 classify_response_media_type(media_type, None),
1727 ResponseMediaKind::Text,
1728 "{media_type}"
1729 );
1730 }
1731 assert_eq!(
1732 classify_response_media_type("text/event-streaming", None),
1733 ResponseMediaKind::Text
1734 );
1735 assert_eq!(
1736 classify_response_media_type("text/*", None),
1737 ResponseMediaKind::Unsupported,
1738 "a media range is not a valid concrete response Content-Type"
1739 );
1740 }
1741
1742 #[test]
1743 fn response_json_content_skips_schema_less_canonical_entry() {
1744 let response: Response = serde_json::from_value(json!({
1745 "description": "mixed JSON",
1746 "content": {
1747 "application/json": {},
1748 "application/vnd.example+json": {
1749 "schema": { "type": "string" }
1750 }
1751 }
1752 }))
1753 .unwrap();
1754
1755 let (media_type, schema) = response.json_content().expect("schema-bearing JSON");
1756 assert_eq!(media_type, "application/vnd.example+json");
1757 assert!(matches!(schema.schema_type(), Some(SchemaType::String)));
1758 }
1759
1760 #[test]
1761 fn response_media_classifier_recognizes_binary_formats_and_wildcards() {
1762 for media_type in [
1763 "image/png",
1764 "IMAGE/*; version=1",
1765 "audio/mpeg",
1766 "video/mp4",
1767 "application/octet-stream",
1768 "APPLICATION/ZIP; version=1",
1769 "application/*",
1770 "*/*",
1771 ] {
1772 assert_eq!(
1773 classify_response_media_type(media_type, None),
1774 ResponseMediaKind::Binary,
1775 "{media_type}"
1776 );
1777 }
1778
1779 let binary_schema: Schema = serde_json::from_value(json!({
1780 "type": "string",
1781 "format": "BINARY"
1782 }))
1783 .unwrap();
1784 assert_eq!(
1785 classify_response_media_type("application/x-custom", Some(&binary_schema)),
1786 ResponseMediaKind::Binary
1787 );
1788 assert_eq!(
1789 classify_response_media_type("text/plain", Some(&binary_schema)),
1790 ResponseMediaKind::Binary,
1791 "an explicit binary schema must prevent UTF-8 decoding"
1792 );
1793 assert!(is_binary_media_type(
1794 "application/x-custom",
1795 Some(&binary_schema)
1796 ));
1797 }
1798
1799 #[test]
1800 fn response_media_classifier_leaves_ambiguous_formats_unsupported() {
1801 let string_schema: Schema = serde_json::from_value(json!({ "type": "string" })).unwrap();
1802 for media_type in ["application/x-unknown", "not-a-media-type"] {
1803 assert_eq!(
1804 classify_response_media_type(media_type, Some(&string_schema)),
1805 ResponseMediaKind::Unsupported,
1806 "{media_type}"
1807 );
1808 }
1809 assert_eq!(
1813 classify_response_media_type("application/pdf", Some(&string_schema)),
1814 ResponseMediaKind::Binary
1815 );
1816 assert_eq!(
1817 classify_response_media_type("application/xml", Some(&string_schema)),
1818 ResponseMediaKind::Text
1819 );
1820 assert_eq!(
1821 classify_response_media_type("application/atom+xml", Some(&string_schema)),
1822 ResponseMediaKind::Text
1823 );
1824 assert_eq!(
1826 classify_response_media_type("application/jwt", Some(&string_schema)),
1827 ResponseMediaKind::Text
1828 );
1829 assert!(!is_binary_media_type("text/plain", None));
1830 }
1831
1832 #[test]
1833 fn request_body_json_schema_finds_vnd_api_plus_json() {
1834 let body_json = json!({
1837 "required": true,
1838 "content": {
1839 "application/vnd.api+json": {
1840 "schema": {"$ref": "#/components/schemas/create_api_key"}
1841 }
1842 }
1843 });
1844
1845 let body: RequestBody = serde_json::from_value(body_json).unwrap();
1846 let schema = body.json_schema().expect("expected +json schema match");
1847 assert!(schema.is_reference());
1848 }
1849
1850 #[test]
1851 fn request_body_best_content_prefers_canonical_json_over_plus_json() {
1852 let body_json = json!({
1856 "required": true,
1857 "content": {
1858 "application/json": {
1859 "schema": {"$ref": "#/components/schemas/A"}
1860 },
1861 "application/vnd.api+json": {
1862 "schema": {"$ref": "#/components/schemas/B"}
1863 }
1864 }
1865 });
1866
1867 let body: RequestBody = serde_json::from_value(body_json).unwrap();
1868 let (ct, _) = body.best_content().expect("expected best_content");
1869 assert_eq!(ct, "application/json");
1870 }
1871
1872 #[test]
1873 fn request_body_best_content_falls_back_to_plus_json() {
1874 let body_json = json!({
1877 "required": true,
1878 "content": {
1879 "application/vnd.api+json": {
1880 "schema": {"$ref": "#/components/schemas/B"}
1881 }
1882 }
1883 });
1884
1885 let body: RequestBody = serde_json::from_value(body_json).unwrap();
1886 let (ct, _) = body.best_content().expect("expected best_content");
1887 assert_eq!(ct, "application/vnd.api+json");
1888 }
1889
1890 #[test]
1891 fn request_body_best_content_does_not_select_wildcard_media_ranges() {
1892 let body: RequestBody = serde_json::from_value(json!({
1893 "required": true,
1894 "content": {
1895 "image/*": {
1896 "schema": { "type": "string", "format": "binary" }
1897 },
1898 "*/*": {
1899 "schema": { "type": "string", "format": "binary" }
1900 }
1901 }
1902 }))
1903 .unwrap();
1904
1905 assert!(
1906 body.best_content().is_none(),
1907 "request media ranges require a runtime concrete Content-Type"
1908 );
1909 }
1910
1911 #[test]
1912 fn request_body_best_content_matches_parameterized_text_plain_by_essence() {
1913 let body: RequestBody = serde_json::from_value(json!({
1914 "required": true,
1915 "content": {
1916 "Text/Plain; charset=utf-8": {
1917 "schema": { "type": "string" }
1918 }
1919 }
1920 }))
1921 .unwrap();
1922
1923 let (media_type, _) = body.best_content().expect("parameterized text body");
1924 assert_eq!(media_type, "Text/Plain; charset=utf-8");
1925 }
1926
1927 #[test]
1928 fn response_json_schema_finds_vnd_api_plus_json() {
1929 let resp_json = json!({
1932 "description": "OK",
1933 "content": {
1934 "application/vnd.api+json": {
1935 "schema": {"$ref": "#/components/schemas/api_keys"}
1936 }
1937 }
1938 });
1939
1940 let resp: Response = serde_json::from_value(resp_json).unwrap();
1941 let schema = resp.json_schema().expect("expected +json schema match");
1942 assert!(schema.is_reference());
1943 }
1944}