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 Bool(bool),
199}
200
201#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
202#[serde(rename_all = "lowercase")]
203pub enum SchemaType {
204 String,
205 Integer,
206 Number,
207 Boolean,
208 Array,
209 Object,
210 #[serde(rename = "null")]
211 Null,
212}
213
214#[derive(Debug, Clone, Default, Deserialize, Serialize)]
215pub struct SchemaDetails {
216 #[serde(skip_serializing_if = "Option::is_none")]
217 pub description: Option<String>,
218 #[serde(skip_serializing_if = "Option::is_none")]
219 pub nullable: Option<bool>,
220
221 #[serde(rename = "$recursiveAnchor", skip_serializing_if = "Option::is_none")]
223 pub recursive_anchor: Option<bool>,
224
225 #[serde(rename = "$dynamicAnchor", skip_serializing_if = "Option::is_none")]
227 pub dynamic_anchor: Option<String>,
228 #[serde(rename = "$id", skip_serializing_if = "Option::is_none")]
229 pub schema_id: Option<String>,
230
231 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
233 pub enum_values: Option<Vec<Value>>,
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub format: Option<String>,
236 #[serde(skip_serializing_if = "Option::is_none")]
237 pub default: Option<Value>,
238 #[serde(
239 rename = "const",
240 default,
241 deserialize_with = "deserialize_present_value",
242 skip_serializing_if = "Option::is_none"
243 )]
244 pub const_value: Option<Value>,
245
246 #[serde(skip_serializing_if = "Option::is_none")]
248 pub properties: Option<BTreeMap<String, Schema>>,
249 #[serde(skip_serializing_if = "Option::is_none")]
250 pub required: Option<Vec<String>>,
251 #[serde(
252 rename = "additionalProperties",
253 skip_serializing_if = "Option::is_none"
254 )]
255 pub additional_properties: Option<AdditionalProperties>,
256
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub items: Option<Items>,
260
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub minimum: Option<serde_json::Number>,
264 #[serde(skip_serializing_if = "Option::is_none")]
265 pub maximum: Option<serde_json::Number>,
266
267 #[serde(
269 rename = "minLength",
270 default,
271 deserialize_with = "deserialize_count",
272 skip_serializing_if = "Option::is_none"
273 )]
274 pub min_length: Option<u64>,
275 #[serde(
276 rename = "maxLength",
277 default,
278 deserialize_with = "deserialize_count",
279 skip_serializing_if = "Option::is_none"
280 )]
281 pub max_length: Option<u64>,
282 #[serde(skip_serializing_if = "Option::is_none")]
283 pub pattern: Option<String>,
284 #[serde(rename = "exclusiveMinimum", skip_serializing_if = "Option::is_none")]
288 pub exclusive_minimum: Option<ExclusiveBound>,
289 #[serde(rename = "exclusiveMaximum", skip_serializing_if = "Option::is_none")]
290 pub exclusive_maximum: Option<ExclusiveBound>,
291 #[serde(rename = "multipleOf", skip_serializing_if = "Option::is_none")]
292 pub multiple_of: Option<f64>,
293 #[serde(
294 rename = "minItems",
295 default,
296 deserialize_with = "deserialize_count",
297 skip_serializing_if = "Option::is_none"
298 )]
299 pub min_items: Option<u64>,
300 #[serde(
301 rename = "maxItems",
302 default,
303 deserialize_with = "deserialize_count",
304 skip_serializing_if = "Option::is_none"
305 )]
306 pub max_items: Option<u64>,
307 #[serde(rename = "uniqueItems", skip_serializing_if = "Option::is_none")]
308 pub unique_items: Option<bool>,
309 #[serde(
310 rename = "minProperties",
311 default,
312 deserialize_with = "deserialize_count",
313 skip_serializing_if = "Option::is_none"
314 )]
315 pub min_properties: Option<u64>,
316 #[serde(
317 rename = "maxProperties",
318 default,
319 deserialize_with = "deserialize_count",
320 skip_serializing_if = "Option::is_none"
321 )]
322 pub max_properties: Option<u64>,
323
324 #[serde(rename = "prefixItems", skip_serializing_if = "Option::is_none")]
326 pub prefix_items: Option<Vec<Schema>>,
327 #[serde(skip_serializing_if = "Option::is_none")]
328 pub contains: Option<Box<Schema>>,
329 #[serde(
330 rename = "minContains",
331 default,
332 deserialize_with = "deserialize_count",
333 skip_serializing_if = "Option::is_none"
334 )]
335 pub min_contains: Option<u64>,
336 #[serde(
337 rename = "maxContains",
338 default,
339 deserialize_with = "deserialize_count",
340 skip_serializing_if = "Option::is_none"
341 )]
342 pub max_contains: Option<u64>,
343
344 #[serde(rename = "patternProperties", skip_serializing_if = "Option::is_none")]
346 pub pattern_properties: Option<BTreeMap<String, Schema>>,
347 #[serde(rename = "propertyNames", skip_serializing_if = "Option::is_none")]
348 pub property_names: Option<Box<Schema>>,
349 #[serde(
350 rename = "unevaluatedProperties",
351 skip_serializing_if = "Option::is_none"
352 )]
353 pub unevaluated_properties: Option<AdditionalProperties>,
354 #[serde(rename = "unevaluatedItems", skip_serializing_if = "Option::is_none")]
355 pub unevaluated_items: Option<AdditionalProperties>,
356 #[serde(rename = "dependentRequired", skip_serializing_if = "Option::is_none")]
357 pub dependent_required: Option<BTreeMap<String, Vec<String>>>,
358 #[serde(rename = "dependentSchemas", skip_serializing_if = "Option::is_none")]
359 pub dependent_schemas: Option<BTreeMap<String, Schema>>,
360
361 #[serde(rename = "contentEncoding", skip_serializing_if = "Option::is_none")]
363 pub content_encoding: Option<String>,
364 #[serde(rename = "contentMediaType", skip_serializing_if = "Option::is_none")]
365 pub content_media_type: Option<String>,
366 #[serde(rename = "contentSchema", skip_serializing_if = "Option::is_none")]
367 pub content_schema: Option<Box<Schema>>,
368
369 #[serde(rename = "if", skip_serializing_if = "Option::is_none")]
371 pub if_schema: Option<Box<Schema>>,
372 #[serde(rename = "then", skip_serializing_if = "Option::is_none")]
373 pub then_schema: Option<Box<Schema>>,
374 #[serde(rename = "else", skip_serializing_if = "Option::is_none")]
375 pub else_schema: Option<Box<Schema>>,
376 #[serde(skip_serializing_if = "Option::is_none")]
377 pub not: Option<Box<Schema>>,
378
379 #[serde(skip_serializing_if = "Option::is_none")]
381 pub title: Option<String>,
382 #[serde(skip_serializing_if = "Option::is_none")]
383 pub deprecated: Option<bool>,
384 #[serde(rename = "readOnly", skip_serializing_if = "Option::is_none")]
385 pub read_only: Option<bool>,
386 #[serde(rename = "writeOnly", skip_serializing_if = "Option::is_none")]
387 pub write_only: Option<bool>,
388 #[serde(skip_serializing_if = "Option::is_none")]
389 pub examples: Option<Vec<Value>>,
390 #[serde(skip_serializing_if = "Option::is_none")]
391 pub example: Option<Value>,
392 #[serde(rename = "$comment", skip_serializing_if = "Option::is_none")]
394 pub comment: Option<String>,
395 #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
396 pub schema_keyword: Option<String>,
397 #[serde(rename = "$defs", skip_serializing_if = "Option::is_none")]
398 pub defs: Option<BTreeMap<String, Schema>>,
399
400 #[serde(flatten)]
403 pub extra: BTreeMap<String, Value>,
404}
405
406fn deserialize_count<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
413where
414 D: serde::Deserializer<'de>,
415{
416 use serde::de::Error;
417
418 let Some(value) = Option::<Value>::deserialize(deserializer)? else {
419 return Ok(None);
420 };
421 match &value {
422 Value::Null => Ok(None),
423 Value::Number(number) => {
424 if let Some(count) = number.as_u64() {
425 return Ok(Some(count));
426 }
427 match number.as_f64() {
430 Some(float) if float.fract() == 0.0 && float >= 0.0 && float <= u64::MAX as f64 => {
431 Ok(Some(float as u64))
432 }
433 _ => Err(D::Error::custom(format!(
434 "expected a non-negative integer, found {number}"
435 ))),
436 }
437 }
438 other => Err(D::Error::custom(format!(
439 "expected a non-negative integer, found {other}"
440 ))),
441 }
442}
443
444fn deserialize_present_value<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
445where
446 D: serde::Deserializer<'de>,
447{
448 Value::deserialize(deserializer).map(Some)
449}
450
451#[derive(Debug, Clone, Deserialize, Serialize)]
454#[serde(untagged)]
455pub enum ExclusiveBound {
456 Bool(bool),
457 Number(f64),
458}
459
460#[derive(Debug, Clone, Deserialize, Serialize)]
470#[serde(untagged)]
471pub enum Items {
472 Single(Box<Schema>),
474 Positional(Vec<Schema>),
476}
477
478#[derive(Debug, Clone, Deserialize, Serialize)]
479#[serde(untagged)]
480pub enum AdditionalProperties {
481 Boolean(bool),
482 Schema(Box<Schema>),
483}
484
485#[derive(Debug, Clone, Deserialize, Serialize)]
487pub struct Example {
488 #[serde(default)]
489 pub summary: Option<String>,
490 #[serde(default)]
491 pub description: Option<String>,
492 #[serde(default)]
494 pub value: Option<Value>,
495 #[serde(rename = "externalValue", default)]
496 pub external_value: Option<String>,
497 #[serde(rename = "dataValue", default)]
499 pub data_value: Option<Value>,
500 #[serde(rename = "serializedValue", default)]
502 pub serialized_value: Option<String>,
503 #[serde(rename = "$ref", default)]
504 pub reference: Option<String>,
505 #[serde(flatten, default)]
506 pub extensions: Extensions,
507}
508
509#[derive(Debug, Clone, Deserialize, Serialize)]
511pub struct Link {
512 #[serde(rename = "operationRef", default)]
513 pub operation_ref: Option<String>,
514 #[serde(rename = "operationId", default)]
515 pub operation_id: Option<String>,
516 #[serde(default)]
517 pub parameters: Option<BTreeMap<String, Value>>,
518 #[serde(rename = "requestBody", default)]
519 pub request_body: Option<Value>,
520 #[serde(default)]
521 pub description: Option<String>,
522 #[serde(default)]
523 pub server: Option<Server>,
524 #[serde(rename = "$ref", default)]
525 pub reference: Option<String>,
526 #[serde(flatten, default)]
527 pub extensions: Extensions,
528}
529
530#[derive(Debug, Clone, Deserialize, Serialize)]
533#[serde(transparent)]
534pub struct Callback(pub BTreeMap<String, PathItem>);
535
536#[derive(Debug, Clone, Deserialize, Serialize)]
539pub struct Encoding {
540 #[serde(rename = "contentType", default)]
541 pub content_type: Option<String>,
542 #[serde(default)]
543 pub headers: Option<BTreeMap<String, Header>>,
544 #[serde(default)]
545 pub style: Option<String>,
546 #[serde(default)]
547 pub explode: Option<bool>,
548 #[serde(rename = "allowReserved", default)]
549 pub allow_reserved: Option<bool>,
550 #[serde(rename = "itemEncoding", default)]
552 pub item_encoding: Option<Box<Encoding>>,
553 #[serde(flatten, default)]
554 pub extensions: Extensions,
555}
556
557#[derive(Debug, Clone, Deserialize, Serialize)]
561pub struct Header {
562 #[serde(default)]
563 pub description: Option<String>,
564 #[serde(default)]
565 pub required: Option<bool>,
566 #[serde(default)]
567 pub deprecated: Option<bool>,
568 #[serde(rename = "allowEmptyValue", default)]
569 pub allow_empty_value: Option<bool>,
570 #[serde(default)]
571 pub style: Option<String>,
572 #[serde(default)]
573 pub explode: Option<bool>,
574 #[serde(rename = "allowReserved", default)]
575 pub allow_reserved: Option<bool>,
576 #[serde(default)]
577 pub schema: Option<Schema>,
578 #[serde(default)]
579 pub content: Option<BTreeMap<String, MediaType>>,
580 #[serde(default)]
581 pub example: Option<Value>,
582 #[serde(default)]
583 pub examples: Option<Value>,
584 #[serde(rename = "$ref", default)]
585 pub reference: Option<String>,
586 #[serde(flatten, default)]
587 pub extensions: Extensions,
588}
589
590#[derive(Debug, Clone, Deserialize, Serialize)]
594#[serde(tag = "type")]
595pub enum SecurityScheme {
596 #[serde(rename = "apiKey")]
597 ApiKey {
598 name: String,
599 #[serde(rename = "in")]
600 location: String, #[serde(default)]
602 description: Option<String>,
603 #[serde(default)]
605 deprecated: Option<bool>,
606 #[serde(flatten, default)]
607 extensions: Extensions,
608 },
609 #[serde(rename = "http")]
610 Http {
611 scheme: String, #[serde(rename = "bearerFormat", default)]
613 bearer_format: Option<String>,
614 #[serde(default)]
615 description: Option<String>,
616 #[serde(default)]
617 deprecated: Option<bool>,
618 #[serde(flatten, default)]
619 extensions: Extensions,
620 },
621 #[serde(rename = "mutualTLS")]
622 MutualTls {
623 #[serde(default)]
624 description: Option<String>,
625 #[serde(default)]
626 deprecated: Option<bool>,
627 #[serde(flatten, default)]
628 extensions: Extensions,
629 },
630 #[serde(rename = "oauth2")]
631 OAuth2 {
632 flows: Box<OAuthFlows>,
636 #[serde(default)]
637 description: Option<String>,
638 #[serde(rename = "oauth2MetadataUrl", default)]
640 oauth2_metadata_url: Option<String>,
641 #[serde(default)]
642 deprecated: Option<bool>,
643 #[serde(flatten, default)]
644 extensions: Extensions,
645 },
646 #[serde(rename = "openIdConnect")]
647 OpenIdConnect {
648 #[serde(rename = "openIdConnectUrl")]
649 open_id_connect_url: String,
650 #[serde(default)]
651 description: Option<String>,
652 #[serde(default)]
653 deprecated: Option<bool>,
654 #[serde(flatten, default)]
655 extensions: Extensions,
656 },
657}
658
659#[derive(Debug, Clone, Deserialize, Serialize)]
660pub struct OAuthFlows {
661 #[serde(default)]
662 pub implicit: Option<OAuthFlow>,
663 #[serde(default)]
664 pub password: Option<OAuthFlow>,
665 #[serde(rename = "clientCredentials", default)]
666 pub client_credentials: Option<OAuthFlow>,
667 #[serde(rename = "authorizationCode", default)]
668 pub authorization_code: Option<OAuthFlow>,
669 #[serde(rename = "deviceAuthorization", default)]
671 pub device_authorization: Option<OAuthFlow>,
672 #[serde(flatten, default)]
673 pub extensions: Extensions,
674}
675
676#[derive(Debug, Clone, Deserialize, Serialize)]
677pub struct OAuthFlow {
678 #[serde(rename = "authorizationUrl", default)]
679 pub authorization_url: Option<String>,
680 #[serde(rename = "tokenUrl", default)]
681 pub token_url: Option<String>,
682 #[serde(rename = "refreshUrl", default)]
683 pub refresh_url: Option<String>,
684 #[serde(rename = "deviceAuthorizationUrl", default)]
686 pub device_authorization_url: Option<String>,
687 pub scopes: BTreeMap<String, String>,
688 #[serde(flatten, default)]
689 pub extensions: Extensions,
690}
691
692#[derive(Debug, Clone, Deserialize, Serialize)]
694pub struct ExternalDocs {
695 pub url: String,
696 #[serde(default)]
697 pub description: Option<String>,
698 #[serde(flatten, default)]
699 pub extensions: Extensions,
700}
701
702#[derive(Debug, Clone, Deserialize, Serialize)]
704pub struct Tag {
705 pub name: String,
706 #[serde(default)]
708 pub summary: Option<String>,
709 #[serde(default)]
710 pub description: Option<String>,
711 #[serde(default)]
713 pub parent: Option<String>,
714 #[serde(default)]
718 pub kind: Option<String>,
719 #[serde(rename = "externalDocs", default)]
720 pub external_docs: Option<ExternalDocs>,
721 #[serde(flatten, default)]
722 pub extensions: Extensions,
723}
724
725#[derive(Debug, Clone, Deserialize, Serialize)]
728pub struct Server {
729 pub url: String,
730 #[serde(default)]
732 pub name: Option<String>,
733 #[serde(default)]
734 pub description: Option<String>,
735 #[serde(default)]
736 pub variables: Option<BTreeMap<String, ServerVariable>>,
737 #[serde(flatten, default)]
738 pub extensions: Extensions,
739}
740
741#[derive(Debug, Clone, Deserialize, Serialize)]
742pub struct ServerVariable {
743 #[serde(default)]
745 pub default: Option<String>,
746 #[serde(rename = "enum", default)]
747 pub enum_values: Option<Vec<String>>,
748 #[serde(default)]
749 pub description: Option<String>,
750 #[serde(flatten, default)]
751 pub extensions: Extensions,
752}
753
754#[derive(Debug, Clone, Deserialize, Serialize)]
755pub struct Discriminator {
756 #[serde(rename = "propertyName")]
757 pub property_name: String,
758 #[serde(default)]
759 pub mapping: Option<BTreeMap<String, String>>,
760 #[serde(rename = "defaultMapping", default)]
764 pub default_mapping: Option<String>,
765 #[serde(flatten, default)]
766 pub extensions: Extensions,
767}
768
769impl Schema {
770 pub fn schema_type(&self) -> Option<&SchemaType> {
777 match self {
778 Schema::Typed { schema_type, .. } => Some(schema_type),
779 Schema::TypedMulti { schema_types, .. } => schema_types
780 .iter()
781 .find(|t| **t != SchemaType::Null)
782 .or_else(|| schema_types.first()),
783 _ => None,
784 }
785 }
786
787 pub fn declared_type(&self) -> Option<&SchemaType> {
794 match self {
795 Schema::AnyOf { schema_type, .. }
796 | Schema::AllOf { schema_type, .. }
797 | Schema::OneOf { schema_type, .. } => schema_type.as_ref(),
798 other => other.schema_type(),
799 }
800 }
801
802 pub fn non_null_schema_types(&self) -> Option<Vec<SchemaType>> {
809 match self {
810 Schema::TypedMulti { schema_types, .. } => {
811 let mut non_null = Vec::new();
812 for t in schema_types {
813 if *t != SchemaType::Null && !non_null.contains(t) {
814 non_null.push(t.clone());
815 }
816 }
817 (non_null.len() > 1).then_some(non_null)
818 }
819 _ => None,
820 }
821 }
822
823 pub fn type_array_contains_null(&self) -> bool {
826 match self {
827 Schema::TypedMulti { schema_types, .. } => schema_types.contains(&SchemaType::Null),
828 _ => false,
829 }
830 }
831
832 pub fn is_nullable_any(&self) -> bool {
844 self.reference_siblings_are_nullable()
845 || self.details().is_nullable()
846 || self.type_array_contains_null()
847 || self.has_explicit_null_variant()
848 }
849
850 fn reference_siblings_are_nullable(&self) -> bool {
855 let extra = match self {
856 Schema::Reference { extra, .. }
857 | Schema::RecursiveRef { extra, .. }
858 | Schema::DynamicRef { extra, .. } => extra,
859 _ => return false,
860 };
861 extra.get("nullable").and_then(Value::as_bool) == Some(true)
862 }
863
864 pub fn details(&self) -> &SchemaDetails {
866 static EMPTY_DETAILS: Lazy<SchemaDetails> = Lazy::new(SchemaDetails::default);
867 match self {
868 Schema::Typed { details, .. } => details,
869 Schema::TypedMulti { details, .. } => details,
870 Schema::Reference { .. }
873 | Schema::RecursiveRef { .. }
874 | Schema::DynamicRef { .. }
875 | Schema::Bool(_) => &EMPTY_DETAILS,
876 Schema::OneOf { details, .. } => details,
877 Schema::AnyOf { details, .. } => details,
878 Schema::AllOf { details, .. } => details,
879 Schema::Untyped { details } => details,
880 }
881 }
882
883 pub fn details_mut(&mut self) -> &mut SchemaDetails {
885 match self {
886 Schema::Typed { details, .. } => details,
887 Schema::TypedMulti { details, .. } => details,
888 Schema::Reference { .. } => {
889 panic!("Cannot get mutable details for reference schema")
890 }
891 Schema::RecursiveRef { .. } => {
892 panic!("Cannot get mutable details for recursive reference schema")
893 }
894 Schema::DynamicRef { .. } => {
895 panic!("Cannot get mutable details for dynamic reference schema")
896 }
897 Schema::Bool(_) => {
898 panic!("Cannot get mutable details for a boolean schema")
899 }
900 Schema::OneOf { details, .. } => details,
901 Schema::AnyOf { details, .. } => details,
902 Schema::AllOf { details, .. } => details,
903 Schema::Untyped { details } => details,
904 }
905 }
906
907 pub fn is_reference(&self) -> bool {
909 matches!(self, Schema::Reference { .. } | Schema::RecursiveRef { .. })
910 }
911
912 pub fn reference(&self) -> Option<&str> {
914 match self {
915 Schema::Reference { reference, .. } => Some(reference),
916 _ => None,
917 }
918 }
919
920 pub fn recursive_reference(&self) -> Option<&str> {
922 match self {
923 Schema::RecursiveRef { recursive_ref, .. } => Some(recursive_ref),
924 _ => None,
925 }
926 }
927
928 pub fn is_discriminated_union(&self) -> bool {
930 match self {
931 Schema::OneOf { discriminator, .. } => discriminator.is_some(),
932 Schema::AnyOf { discriminator, .. } => discriminator.is_some(),
933 _ => false,
934 }
935 }
936
937 pub fn discriminator(&self) -> Option<&Discriminator> {
939 match self {
940 Schema::OneOf { discriminator, .. } => discriminator.as_ref(),
941 Schema::AnyOf { discriminator, .. } => discriminator.as_ref(),
942 _ => None,
943 }
944 }
945
946 pub fn union_variants(&self) -> Option<&[Schema]> {
948 match self {
949 Schema::OneOf { one_of, .. } => Some(one_of),
950 Schema::AnyOf { any_of, .. } => Some(any_of),
951 _ => None,
952 }
953 }
954
955 pub fn is_nullable_pattern(&self) -> bool {
957 self.non_null_variant().is_some()
958 }
959
960 pub fn has_explicit_null_variant(&self) -> bool {
965 self.union_variants()
966 .is_some_and(|variants| variants.iter().any(Self::is_explicit_null_only))
967 }
968
969 pub fn non_null_variant(&self) -> Option<&Schema> {
972 let variants = match self {
973 Schema::AnyOf { any_of, .. } => any_of,
974 Schema::OneOf { one_of, .. } => one_of,
975 _ => return None,
976 };
977 let [first, second] = variants.as_slice() else {
978 return None;
979 };
980 match (
981 first.is_explicit_null_only(),
982 second.is_explicit_null_only(),
983 ) {
984 (true, false) => Some(second),
985 (false, true) => Some(first),
986 _ => None,
988 }
989 }
990
991 pub(crate) fn is_explicit_null_only(&self) -> bool {
999 match self {
1000 Schema::Typed { schema_type, .. } => *schema_type == SchemaType::Null,
1001 Schema::TypedMulti { schema_types, .. } => {
1002 !schema_types.is_empty()
1003 && schema_types
1004 .iter()
1005 .all(|schema_type| *schema_type == SchemaType::Null)
1006 }
1007 Schema::Untyped { details } => {
1008 details.const_value.as_ref().is_some_and(Value::is_null)
1009 || details.enum_values.as_ref().is_some_and(
1010 |values| matches!(values.as_slice(), [value] if value.is_null()),
1011 )
1012 }
1013 _ => false,
1014 }
1015 }
1016
1017 pub fn inferred_type(&self) -> Option<SchemaType> {
1019 match self {
1020 Schema::Typed { schema_type, .. } => Some(schema_type.clone()),
1021 Schema::TypedMulti { .. } => self.schema_type().cloned(),
1022 Schema::Untyped { details } => {
1023 if self.is_explicit_null_only() {
1025 Some(SchemaType::Null)
1026 } else if details.properties.is_some() {
1027 Some(SchemaType::Object)
1028 } else if details.items.is_some() || details.prefix_items.is_some() {
1029 Some(SchemaType::Array)
1030 } else if details.enum_values.is_some() {
1031 Some(SchemaType::String) } else {
1033 None
1034 }
1035 }
1036 _ => None,
1037 }
1038 }
1039}
1040
1041impl SchemaDetails {
1042 pub fn item_schema(&self) -> Option<&Schema> {
1048 match self.items.as_ref()? {
1049 Items::Single(schema) => Some(schema),
1050 Items::Positional(_) => None,
1051 }
1052 }
1053
1054 pub fn positional_items(&self) -> Option<&[Schema]> {
1057 if let Some(prefix_items) = self.prefix_items.as_deref() {
1058 return Some(prefix_items);
1059 }
1060 match self.items.as_ref()? {
1061 Items::Positional(schemas) => Some(schemas),
1062 Items::Single(_) => None,
1063 }
1064 }
1065
1066 pub fn positional_items_are_closed(&self) -> bool {
1072 let Some(positions) = self.positional_items() else {
1073 return false;
1074 };
1075 if matches!(
1078 self.items.as_ref(),
1079 Some(Items::Single(schema)) if matches!(**schema, Schema::Bool(false))
1080 ) {
1081 return true;
1082 }
1083 if self.extra.get("additionalItems") == Some(&Value::Bool(false)) {
1084 return true;
1085 }
1086 self.max_items
1087 .is_some_and(|maximum| maximum <= positions.len() as u64)
1088 }
1089
1090 pub fn positional_items_are_exact(&self) -> bool {
1094 let Some(positions) = self.positional_items() else {
1095 return false;
1096 };
1097 self.positional_items_are_closed()
1098 && self
1099 .min_items
1100 .is_some_and(|minimum| minimum >= positions.len() as u64)
1101 }
1102
1103 pub fn is_nullable(&self) -> bool {
1105 self.nullable.unwrap_or(false)
1106 }
1107
1108 pub fn is_string_enum(&self) -> bool {
1114 self.enum_values.is_some() || self.const_string_value().is_some()
1115 }
1116
1117 pub fn string_enum_values(&self) -> Option<Vec<String>> {
1123 if let Some(values) = self.enum_values.as_ref() {
1124 return Some(
1129 values
1130 .iter()
1131 .map(|v| match v {
1132 Value::String(s) => s.clone(),
1133 Value::Number(n) => n.to_string(),
1134 Value::Bool(b) => b.to_string(),
1135 Value::Null => "null".to_string(),
1136 _ => v.to_string(),
1137 })
1138 .collect(),
1139 );
1140 }
1141 self.const_string_value().map(|s| vec![s])
1142 }
1143
1144 fn const_string_value(&self) -> Option<String> {
1145 self.const_value
1146 .as_ref()
1147 .and_then(|v| v.as_str())
1148 .map(|s| s.to_string())
1149 }
1150
1151 pub fn is_field_required(&self, field_name: &str) -> bool {
1153 self.required
1154 .as_ref()
1155 .map(|req| req.contains(&field_name.to_string()))
1156 .unwrap_or(false)
1157 }
1158}
1159
1160#[derive(Debug, Clone, Deserialize, Serialize)]
1162pub struct PathItem {
1163 #[serde(default)]
1164 pub summary: Option<String>,
1165 #[serde(default)]
1166 pub description: Option<String>,
1167 pub get: Option<Operation>,
1168 pub put: Option<Operation>,
1169 pub post: Option<Operation>,
1170 pub delete: Option<Operation>,
1171 pub options: Option<Operation>,
1172 pub head: Option<Operation>,
1173 pub patch: Option<Operation>,
1174 pub trace: Option<Operation>,
1175 pub query: Option<Operation>,
1178 #[serde(rename = "additionalOperations", default)]
1182 pub additional_operations: Option<BTreeMap<String, Operation>>,
1183 pub parameters: Option<Vec<Parameter>>,
1184 #[serde(default)]
1185 pub servers: Option<Vec<Server>>,
1186 #[serde(rename = "$ref", default)]
1187 pub reference: Option<String>,
1188 #[serde(flatten, default)]
1189 pub extensions: Extensions,
1190}
1191
1192impl PathItem {
1193 pub fn operations(&self) -> Vec<(&str, &Operation)> {
1196 let mut ops = Vec::new();
1197 if let Some(ref op) = self.get {
1198 ops.push(("get", op));
1199 }
1200 if let Some(ref op) = self.put {
1201 ops.push(("put", op));
1202 }
1203 if let Some(ref op) = self.post {
1204 ops.push(("post", op));
1205 }
1206 if let Some(ref op) = self.delete {
1207 ops.push(("delete", op));
1208 }
1209 if let Some(ref op) = self.options {
1210 ops.push(("options", op));
1211 }
1212 if let Some(ref op) = self.head {
1213 ops.push(("head", op));
1214 }
1215 if let Some(ref op) = self.patch {
1216 ops.push(("patch", op));
1217 }
1218 if let Some(ref op) = self.trace {
1219 ops.push(("trace", op));
1220 }
1221 if let Some(ref op) = self.query {
1222 ops.push(("query", op));
1223 }
1224 if let Some(map) = &self.additional_operations {
1225 for (verb, op) in map {
1226 ops.push((verb.as_str(), op));
1227 }
1228 }
1229 ops
1230 }
1231}
1232
1233#[derive(Debug, Clone, Deserialize, Serialize)]
1235pub struct Operation {
1236 #[serde(rename = "operationId", default)]
1237 pub operation_id: Option<String>,
1238 #[serde(default)]
1239 pub summary: Option<String>,
1240 #[serde(default)]
1241 pub description: Option<String>,
1242 #[serde(default)]
1243 pub tags: Option<Vec<String>>,
1244 #[serde(default)]
1245 pub deprecated: Option<bool>,
1246 pub parameters: Option<Vec<Parameter>>,
1247 #[serde(rename = "requestBody")]
1248 pub request_body: Option<RequestBody>,
1249 pub responses: Option<BTreeMap<String, Response>>,
1250 #[serde(default)]
1251 pub callbacks: Option<BTreeMap<String, Callback>>,
1252 #[serde(default)]
1253 pub security: Option<Vec<BTreeMap<String, Vec<String>>>>,
1254 #[serde(default)]
1255 pub servers: Option<Vec<Server>>,
1256 #[serde(rename = "externalDocs", default)]
1257 pub external_docs: Option<ExternalDocs>,
1258 #[serde(flatten, default)]
1259 pub extensions: Extensions,
1260}
1261
1262#[derive(Debug, Clone, Deserialize, Serialize)]
1264pub struct Parameter {
1265 #[serde(default)]
1266 pub name: Option<String>,
1267 #[serde(rename = "in", default)]
1268 pub location: Option<String>,
1269 #[serde(default)]
1270 pub required: Option<bool>,
1271 #[serde(default)]
1272 pub deprecated: Option<bool>,
1273 #[serde(rename = "allowEmptyValue", default)]
1274 pub allow_empty_value: Option<bool>,
1275 #[serde(default)]
1276 pub style: Option<String>,
1277 #[serde(default)]
1278 pub explode: Option<bool>,
1279 #[serde(rename = "allowReserved", default)]
1280 pub allow_reserved: Option<bool>,
1281 #[serde(default)]
1282 pub schema: Option<Schema>,
1283 #[serde(default)]
1284 pub content: Option<BTreeMap<String, MediaType>>,
1285 #[serde(default)]
1286 pub example: Option<Value>,
1287 #[serde(default)]
1288 pub examples: Option<BTreeMap<String, Example>>,
1289 #[serde(default)]
1290 pub description: Option<String>,
1291 #[serde(rename = "$ref", default)]
1292 pub reference: Option<String>,
1293 #[serde(flatten, default)]
1294 pub extensions: Extensions,
1295}
1296
1297#[derive(Debug, Clone, Deserialize, Serialize)]
1299pub struct RequestBody {
1300 pub content: Option<BTreeMap<String, MediaType>>,
1301 #[serde(default)]
1302 pub description: Option<String>,
1303 #[serde(default)]
1304 pub required: Option<bool>,
1305 #[serde(rename = "$ref", default)]
1306 pub reference: Option<String>,
1307 #[serde(flatten, default)]
1308 pub extensions: Extensions,
1309}
1310
1311#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1317#[serde(rename_all = "snake_case")]
1318pub enum ResponseMediaKind {
1319 Json,
1320 EventStream,
1321 Text,
1322 Binary,
1323 Unsupported,
1324}
1325
1326pub fn media_type_essence(content_type: &str) -> &str {
1332 content_type
1333 .split(';')
1334 .next()
1335 .unwrap_or(content_type)
1336 .trim()
1337}
1338
1339pub fn is_json_media_type(ct: &str) -> bool {
1347 let essence = media_type_essence(ct).to_ascii_lowercase();
1348 if essence == "application/json" {
1349 return true;
1350 }
1351 if let Some(subtype) = essence.strip_prefix("application/") {
1352 return subtype.ends_with("+json");
1353 }
1354 false
1355}
1356
1357pub fn is_form_urlencoded_media_type(ct: &str) -> bool {
1360 let essence = media_type_essence(ct).to_ascii_lowercase();
1361 essence == "application/x-www-form-urlencoded"
1362}
1363
1364pub fn is_event_stream_media_type(ct: &str) -> bool {
1370 media_type_essence(ct).eq_ignore_ascii_case("text/event-stream")
1371}
1372
1373pub fn is_text_media_type(ct: &str) -> bool {
1381 let Some((top_level, subtype)) = media_type_essence(ct).split_once('/') else {
1382 return false;
1383 };
1384 if top_level.eq_ignore_ascii_case("text")
1385 && !subtype.is_empty()
1386 && !is_event_stream_media_type(ct)
1387 {
1388 return true;
1389 }
1390 top_level.eq_ignore_ascii_case("application")
1391 && (subtype.eq_ignore_ascii_case("xml")
1392 || subtype.to_ascii_lowercase().ends_with("+xml")
1393 || subtype.eq_ignore_ascii_case("jwt"))
1396}
1397
1398pub fn is_wildcard_media_type(ct: &str) -> bool {
1404 let Some((top_level, subtype)) = media_type_essence(ct).split_once('/') else {
1405 return false;
1406 };
1407 !top_level.is_empty() && subtype == "*"
1408}
1409
1410fn schema_has_binary_format(schema: Option<&Schema>) -> bool {
1411 schema.is_some_and(|schema| {
1412 schema
1413 .details()
1414 .format
1415 .as_deref()
1416 .is_some_and(|format| format.eq_ignore_ascii_case("binary"))
1417 })
1418}
1419
1420pub fn is_binary_media_type(ct: &str, schema: Option<&Schema>) -> bool {
1428 if schema_has_binary_format(schema) {
1429 return true;
1430 }
1431
1432 let essence = media_type_essence(ct);
1433 let Some((top_level, _)) = essence.split_once('/') else {
1434 return false;
1435 };
1436 if top_level.eq_ignore_ascii_case("image")
1437 || top_level.eq_ignore_ascii_case("audio")
1438 || top_level.eq_ignore_ascii_case("video")
1439 {
1440 return true;
1441 }
1442 if essence.eq_ignore_ascii_case("application/octet-stream")
1443 || essence.eq_ignore_ascii_case("application/zip")
1444 || essence.eq_ignore_ascii_case("application/pdf")
1445 {
1446 return true;
1447 }
1448
1449 !top_level.eq_ignore_ascii_case("text") && is_wildcard_media_type(ct)
1450}
1451
1452pub fn classify_response_media_type(ct: &str, schema: Option<&Schema>) -> ResponseMediaKind {
1457 if is_json_media_type(ct) {
1458 ResponseMediaKind::Json
1459 } else if is_event_stream_media_type(ct) {
1460 ResponseMediaKind::EventStream
1461 } else if schema_has_binary_format(schema) {
1462 ResponseMediaKind::Binary
1463 } else if is_text_media_type(ct) {
1464 if is_wildcard_media_type(ct) {
1465 ResponseMediaKind::Unsupported
1466 } else {
1467 ResponseMediaKind::Text
1468 }
1469 } else if is_binary_media_type(ct, schema) {
1470 ResponseMediaKind::Binary
1471 } else {
1472 ResponseMediaKind::Unsupported
1473 }
1474}
1475
1476fn find_json_content(content: &BTreeMap<String, MediaType>) -> Option<(&str, &MediaType)> {
1477 if let Some(mt) = content
1478 .get("application/json")
1479 .filter(|media_type| media_type.schema.is_some())
1480 {
1481 return Some(("application/json", mt));
1482 }
1483 content
1484 .iter()
1485 .find(|(ct, media_type)| is_json_media_type(ct) && media_type.schema.is_some())
1486 .map(|(ct, mt)| (ct.as_str(), mt))
1487 .or_else(|| {
1488 content
1489 .get("application/json")
1490 .map(|media_type| ("application/json", media_type))
1491 })
1492 .or_else(|| {
1493 content
1494 .iter()
1495 .find(|(ct, _)| is_json_media_type(ct))
1496 .map(|(ct, mt)| (ct.as_str(), mt))
1497 })
1498}
1499
1500impl RequestBody {
1501 pub fn json_schema(&self) -> Option<&Schema> {
1507 self.content
1508 .as_ref()
1509 .and_then(find_json_content)
1510 .and_then(|(_, media_type)| media_type.schema.as_ref())
1511 }
1512
1513 pub fn best_content(&self) -> Option<(&str, Option<&Schema>)> {
1515 let content = self.content.as_ref()?;
1516
1517 if let Some((ct, media_type)) = find_json_content(content) {
1518 return Some((ct, media_type.schema.as_ref()));
1519 }
1520
1521 const PRIORITY: &[&str] = &[
1522 "application/x-www-form-urlencoded",
1523 "multipart/form-data",
1524 "application/octet-stream",
1525 "text/plain",
1526 ];
1527 for preferred_essence in PRIORITY {
1528 if let Some((ct, media_type)) = content
1529 .iter()
1530 .find(|(ct, _)| media_type_essence(ct).eq_ignore_ascii_case(preferred_essence))
1531 {
1532 return Some((ct.as_str(), media_type.schema.as_ref()));
1533 }
1534 }
1535 if let Some((ct, media_type)) = content.iter().find(|(ct, _)| is_text_media_type(ct)) {
1538 return Some((ct.as_str(), media_type.schema.as_ref()));
1539 }
1540 content
1541 .iter()
1542 .find(|(ct, media_type)| {
1548 !is_wildcard_media_type(ct) && is_binary_media_type(ct, media_type.schema.as_ref())
1549 })
1550 .map(|(ct, media_type)| (ct.as_str(), media_type.schema.as_ref()))
1551 }
1552}
1553
1554#[derive(Debug, Clone, Deserialize, Serialize)]
1556pub struct Response {
1557 #[serde(default)]
1558 pub description: Option<String>,
1559 #[serde(default)]
1560 pub headers: Option<BTreeMap<String, Header>>,
1561 #[serde(default)]
1562 pub content: Option<BTreeMap<String, MediaType>>,
1563 #[serde(default)]
1564 pub links: Option<Value>,
1565 #[serde(rename = "$ref", default)]
1566 pub reference: Option<String>,
1567 #[serde(flatten, default)]
1568 pub extensions: Extensions,
1569}
1570
1571impl Response {
1572 pub fn json_schema(&self) -> Option<&Schema> {
1579 self.content
1580 .as_ref()
1581 .and_then(find_json_content)
1582 .and_then(|(_, media_type)| media_type.schema.as_ref())
1583 }
1584
1585 pub fn json_content(&self) -> Option<(&str, &Schema)> {
1587 self.content
1588 .as_ref()
1589 .and_then(find_json_content)
1590 .and_then(|(content_type, media_type)| {
1591 media_type
1592 .schema
1593 .as_ref()
1594 .map(|schema| (content_type, schema))
1595 })
1596 }
1597}
1598
1599#[derive(Debug, Clone, Deserialize, Serialize)]
1601pub struct MediaType {
1602 #[serde(default)]
1603 pub schema: Option<Schema>,
1604 #[serde(default)]
1605 pub example: Option<Value>,
1606 #[serde(default)]
1607 pub examples: Option<BTreeMap<String, Example>>,
1608 #[serde(default)]
1609 pub encoding: Option<BTreeMap<String, Encoding>>,
1610 #[serde(rename = "itemSchema", default)]
1613 pub item_schema: Option<Schema>,
1614 #[serde(rename = "prefixEncoding", default)]
1617 pub prefix_encoding: Option<Vec<Encoding>>,
1618 #[serde(rename = "itemEncoding", default)]
1621 pub item_encoding: Option<Encoding>,
1622 #[serde(rename = "$ref", default)]
1623 pub reference: Option<String>,
1624 #[serde(flatten, default)]
1625 pub extensions: Extensions,
1626}
1627
1628#[cfg(test)]
1629#[allow(clippy::unwrap_used, clippy::expect_used)]
1630mod tests {
1631 use super::*;
1632 use serde_json::json;
1633
1634 #[test]
1635 fn paths_map_skips_extension_scalars() {
1636 let spec: OpenApiSpec = serde_json::from_value(json!({
1639 "openapi": "3.0.0",
1640 "info": { "title": "lenient paths", "version": "1" },
1641 "paths": {
1642 "x-codegen-contextRoot": "/apis/registry/v2",
1643 "/items": {
1644 "get": {
1645 "operationId": "listItems",
1646 "responses": { "204": { "description": "ok" } }
1647 }
1648 }
1649 }
1650 }))
1651 .unwrap();
1652 let paths = spec.paths.unwrap();
1653 assert!(paths.contains_key("/items"));
1654 assert!(!paths.contains_key("x-codegen-contextRoot"));
1655 }
1656
1657 #[test]
1658 fn test_parse_simple_object_schema() {
1659 let schema_json = json!({
1660 "type": "object",
1661 "properties": {
1662 "name": {
1663 "type": "string",
1664 "description": "User name"
1665 },
1666 "age": {
1667 "type": "integer"
1668 }
1669 },
1670 "required": ["name"]
1671 });
1672
1673 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1674
1675 match schema {
1676 Schema::Typed {
1677 schema_type: SchemaType::Object,
1678 details,
1679 } => {
1680 assert!(details.properties.is_some());
1681 assert_eq!(details.required, Some(vec!["name".to_string()]));
1682 assert!(details.is_field_required("name"));
1683 assert!(!details.is_field_required("age"));
1684 }
1685 _ => panic!("Expected object schema"),
1686 }
1687 }
1688
1689 #[test]
1690 fn test_parse_string_enum() {
1691 let schema_json = json!({
1692 "type": "string",
1693 "enum": ["active", "inactive", "pending"],
1694 "description": "User status"
1695 });
1696
1697 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1698
1699 match schema {
1700 Schema::Typed {
1701 schema_type: SchemaType::String,
1702 details,
1703 } => {
1704 assert!(details.is_string_enum());
1705 let values = details.string_enum_values().unwrap();
1706 assert_eq!(values, vec!["active", "inactive", "pending"]);
1707 }
1708 _ => panic!("Expected string enum schema"),
1709 }
1710 }
1711
1712 #[test]
1713 fn test_parse_reference_schema() {
1714 let schema_json = json!({
1715 "$ref": "#/components/schemas/User"
1716 });
1717
1718 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1719
1720 assert!(schema.is_reference());
1721 assert_eq!(schema.reference(), Some("#/components/schemas/User"));
1722 }
1723
1724 #[test]
1725 fn test_parse_discriminated_union() {
1726 let schema_json = json!({
1727 "oneOf": [
1728 {"$ref": "#/components/schemas/Dog"},
1729 {"$ref": "#/components/schemas/Cat"}
1730 ],
1731 "discriminator": {
1732 "propertyName": "petType"
1733 }
1734 });
1735
1736 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1737
1738 assert!(schema.is_discriminated_union());
1739 let discriminator = schema.discriminator().unwrap();
1740 assert_eq!(discriminator.property_name, "petType");
1741 }
1742
1743 #[test]
1744 fn test_parse_nullable_pattern() {
1745 let schema_json = json!({
1746 "anyOf": [
1747 {"$ref": "#/components/schemas/User"},
1748 {"type": "null"}
1749 ]
1750 });
1751
1752 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1753
1754 assert!(schema.is_nullable_pattern());
1755 let non_null = schema.non_null_variant().unwrap();
1756 assert!(non_null.is_reference());
1757 }
1758
1759 #[test]
1760 fn explicit_null_only_branches_are_order_independent_nullable_patterns() {
1761 for union_keyword in ["anyOf", "oneOf"] {
1762 for null_schema in [
1763 json!({"type": "null"}),
1764 json!({"type": ["null"]}),
1765 json!({"const": null}),
1766 json!({"enum": [null]}),
1767 ] {
1768 for variants in [
1769 vec![
1770 json!({"$ref": "#/components/schemas/User"}),
1771 null_schema.clone(),
1772 ],
1773 vec![
1774 null_schema.clone(),
1775 json!({"$ref": "#/components/schemas/User"}),
1776 ],
1777 ] {
1778 let schema: Schema = serde_json::from_value(json!({
1779 (union_keyword): variants,
1780 }))
1781 .unwrap();
1782 let non_null = schema.non_null_variant().unwrap_or_else(|| {
1783 panic!("{union_keyword} must recognize {null_schema} as null-only")
1784 });
1785 assert_eq!(
1786 non_null.reference(),
1787 Some("#/components/schemas/User"),
1788 "{union_keyword} with {null_schema}"
1789 );
1790 }
1791 }
1792 }
1793 }
1794
1795 #[test]
1796 fn nullable_schemas_are_real_union_branches_even_beside_references() {
1797 for union_keyword in ["anyOf", "oneOf"] {
1798 for nullable_branch in [
1799 json!({"nullable": true}),
1800 json!({"type": "object", "nullable": true}),
1801 json!({
1802 "$ref": "#/components/schemas/Other",
1803 "nullable": true
1804 }),
1805 ] {
1806 for variants in [
1807 vec![
1808 json!({"$ref": "#/components/schemas/User"}),
1809 nullable_branch.clone(),
1810 ],
1811 vec![
1812 nullable_branch.clone(),
1813 json!({"$ref": "#/components/schemas/User"}),
1814 ],
1815 ] {
1816 let schema: Schema = serde_json::from_value(json!({
1817 (union_keyword): variants,
1818 }))
1819 .unwrap();
1820 assert!(
1821 schema.non_null_variant().is_none(),
1822 "{union_keyword} must preserve nullable branch {nullable_branch}"
1823 );
1824 }
1825 }
1826 }
1827 }
1828
1829 #[test]
1830 fn reference_sibling_nullable_annotation_is_not_lost() {
1831 for reference_keyword in ["$ref", "$recursiveRef", "$dynamicRef"] {
1832 let nullable: Schema = serde_json::from_value(json!({
1833 (reference_keyword): "#/components/schemas/Value",
1834 "nullable": true,
1835 "description": "retained sibling"
1836 }))
1837 .unwrap();
1838 assert!(
1839 nullable.is_nullable_any(),
1840 "{reference_keyword} should retain nullable:true"
1841 );
1842
1843 let non_nullable: Schema = serde_json::from_value(json!({
1844 (reference_keyword): "#/components/schemas/Value",
1845 "nullable": false
1846 }))
1847 .unwrap();
1848 assert!(
1849 !non_nullable.is_nullable_any(),
1850 "{reference_keyword} nullable:false must stay non-nullable"
1851 );
1852 }
1853 }
1854
1855 #[test]
1856 fn null_only_enum_and_const_infer_null_instead_of_string() {
1857 for source in [json!({"enum": [null]}), json!({"const": null})] {
1858 let schema: Schema = serde_json::from_value(source.clone()).unwrap();
1859 assert_eq!(schema.inferred_type(), Some(SchemaType::Null), "{source}");
1860 }
1861
1862 for source in [
1863 json!({"enum": ["null"]}),
1864 json!({"enum": ["ready", null]}),
1865 json!({"type": "string", "enum": ["ready"]}),
1866 ] {
1867 let schema: Schema = serde_json::from_value(source.clone()).unwrap();
1868 assert_ne!(schema.inferred_type(), Some(SchemaType::Null), "{source}");
1869 }
1870 }
1871
1872 #[test]
1873 fn three_branch_unions_do_not_collapse_even_with_an_explicit_null() {
1874 for union_keyword in ["anyOf", "oneOf"] {
1875 let schema: Schema = serde_json::from_value(json!({
1876 (union_keyword): [
1877 {"$ref": "#/components/schemas/User"},
1878 {"type": "null"},
1879 {"type": "array", "items": {"type": "string"}}
1880 ],
1881 }))
1882 .unwrap();
1883 assert!(
1884 schema.non_null_variant().is_none(),
1885 "{union_keyword} with three branches must retain its non-null union"
1886 );
1887 assert!(
1888 schema.is_nullable_any(),
1889 "{union_keyword} with an explicit null branch must remain nullable"
1890 );
1891 }
1892 }
1893
1894 #[test]
1895 fn is_json_media_type_accepts_canonical_and_structured_suffix() {
1896 assert!(is_json_media_type("application/json"));
1898 assert!(is_json_media_type("application/json; charset=utf-8"));
1900 assert!(is_json_media_type("APPLICATION/JSON"));
1901 assert!(is_json_media_type("application/vnd.api+json"));
1903 assert!(is_json_media_type("application/hal+json"));
1904 assert!(is_json_media_type("application/problem+json"));
1905 assert!(is_json_media_type("application/ld+json"));
1906 assert!(is_json_media_type(
1907 "application/vnd.api+json; charset=utf-8"
1908 ));
1909 assert!(!is_json_media_type("application/xml"));
1911 assert!(!is_json_media_type("application/x-www-form-urlencoded"));
1912 assert!(!is_json_media_type("text/plain"));
1913 assert!(!is_json_media_type("application/jsonbutnotreally"));
1914 assert!(!is_json_media_type("text/something+json"));
1916 }
1917
1918 #[test]
1919 fn response_media_helpers_normalize_parameters_and_case() {
1920 assert_eq!(
1921 media_type_essence(" Text/Plain ; charset=utf-8 "),
1922 "Text/Plain"
1923 );
1924 assert!(is_text_media_type("TEXT/HTML; charset=UTF-8"));
1925 assert!(!is_text_media_type("Text/Event-Stream; charset=utf-8"));
1926 assert!(is_wildcard_media_type("*/*; q=0.8"));
1927 assert!(is_wildcard_media_type("IMAGE/*"));
1928 assert!(is_wildcard_media_type("text/*"));
1929 assert!(!is_wildcard_media_type("image/*+json"));
1930 assert!(!is_wildcard_media_type("application/json"));
1931 }
1932
1933 #[test]
1934 fn response_media_classifier_keeps_json_sse_and_text_distinct() {
1935 for media_type in [
1936 "application/json",
1937 "APPLICATION/PROBLEM+JSON; charset=utf-8",
1938 ] {
1939 assert_eq!(
1940 classify_response_media_type(media_type, None),
1941 ResponseMediaKind::Json,
1942 "{media_type}"
1943 );
1944 }
1945
1946 assert_eq!(
1947 classify_response_media_type("Text/Event-Stream; charset=utf-8", None),
1948 ResponseMediaKind::EventStream
1949 );
1950 for media_type in ["text/plain", "TEXT/HTML; charset=UTF-8"] {
1951 assert_eq!(
1952 classify_response_media_type(media_type, None),
1953 ResponseMediaKind::Text,
1954 "{media_type}"
1955 );
1956 }
1957 assert_eq!(
1958 classify_response_media_type("text/event-streaming", None),
1959 ResponseMediaKind::Text
1960 );
1961 assert_eq!(
1962 classify_response_media_type("text/*", None),
1963 ResponseMediaKind::Unsupported,
1964 "a media range is not a valid concrete response Content-Type"
1965 );
1966 }
1967
1968 #[test]
1969 fn response_json_content_skips_schema_less_canonical_entry() {
1970 let response: Response = serde_json::from_value(json!({
1971 "description": "mixed JSON",
1972 "content": {
1973 "application/json": {},
1974 "application/vnd.example+json": {
1975 "schema": { "type": "string" }
1976 }
1977 }
1978 }))
1979 .unwrap();
1980
1981 let (media_type, schema) = response.json_content().expect("schema-bearing JSON");
1982 assert_eq!(media_type, "application/vnd.example+json");
1983 assert!(matches!(schema.schema_type(), Some(SchemaType::String)));
1984 }
1985
1986 #[test]
1987 fn response_media_classifier_recognizes_binary_formats_and_wildcards() {
1988 for media_type in [
1989 "image/png",
1990 "IMAGE/*; version=1",
1991 "audio/mpeg",
1992 "video/mp4",
1993 "application/octet-stream",
1994 "APPLICATION/ZIP; version=1",
1995 "application/*",
1996 "*/*",
1997 ] {
1998 assert_eq!(
1999 classify_response_media_type(media_type, None),
2000 ResponseMediaKind::Binary,
2001 "{media_type}"
2002 );
2003 }
2004
2005 let binary_schema: Schema = serde_json::from_value(json!({
2006 "type": "string",
2007 "format": "BINARY"
2008 }))
2009 .unwrap();
2010 assert_eq!(
2011 classify_response_media_type("application/x-custom", Some(&binary_schema)),
2012 ResponseMediaKind::Binary
2013 );
2014 assert_eq!(
2015 classify_response_media_type("text/plain", Some(&binary_schema)),
2016 ResponseMediaKind::Binary,
2017 "an explicit binary schema must prevent UTF-8 decoding"
2018 );
2019 assert!(is_binary_media_type(
2020 "application/x-custom",
2021 Some(&binary_schema)
2022 ));
2023 }
2024
2025 #[test]
2026 fn response_media_classifier_leaves_ambiguous_formats_unsupported() {
2027 let string_schema: Schema = serde_json::from_value(json!({ "type": "string" })).unwrap();
2028 for media_type in ["application/x-unknown", "not-a-media-type"] {
2029 assert_eq!(
2030 classify_response_media_type(media_type, Some(&string_schema)),
2031 ResponseMediaKind::Unsupported,
2032 "{media_type}"
2033 );
2034 }
2035 assert_eq!(
2039 classify_response_media_type("application/pdf", Some(&string_schema)),
2040 ResponseMediaKind::Binary
2041 );
2042 assert_eq!(
2043 classify_response_media_type("application/xml", Some(&string_schema)),
2044 ResponseMediaKind::Text
2045 );
2046 assert_eq!(
2047 classify_response_media_type("application/atom+xml", Some(&string_schema)),
2048 ResponseMediaKind::Text
2049 );
2050 assert_eq!(
2052 classify_response_media_type("application/jwt", Some(&string_schema)),
2053 ResponseMediaKind::Text
2054 );
2055 assert!(!is_binary_media_type("text/plain", None));
2056 }
2057
2058 #[test]
2059 fn request_body_json_schema_finds_vnd_api_plus_json() {
2060 let body_json = json!({
2063 "required": true,
2064 "content": {
2065 "application/vnd.api+json": {
2066 "schema": {"$ref": "#/components/schemas/create_api_key"}
2067 }
2068 }
2069 });
2070
2071 let body: RequestBody = serde_json::from_value(body_json).unwrap();
2072 let schema = body.json_schema().expect("expected +json schema match");
2073 assert!(schema.is_reference());
2074 }
2075
2076 #[test]
2077 fn request_body_best_content_prefers_canonical_json_over_plus_json() {
2078 let body_json = json!({
2082 "required": true,
2083 "content": {
2084 "application/json": {
2085 "schema": {"$ref": "#/components/schemas/A"}
2086 },
2087 "application/vnd.api+json": {
2088 "schema": {"$ref": "#/components/schemas/B"}
2089 }
2090 }
2091 });
2092
2093 let body: RequestBody = serde_json::from_value(body_json).unwrap();
2094 let (ct, _) = body.best_content().expect("expected best_content");
2095 assert_eq!(ct, "application/json");
2096 }
2097
2098 #[test]
2099 fn request_body_best_content_falls_back_to_plus_json() {
2100 let body_json = json!({
2103 "required": true,
2104 "content": {
2105 "application/vnd.api+json": {
2106 "schema": {"$ref": "#/components/schemas/B"}
2107 }
2108 }
2109 });
2110
2111 let body: RequestBody = serde_json::from_value(body_json).unwrap();
2112 let (ct, _) = body.best_content().expect("expected best_content");
2113 assert_eq!(ct, "application/vnd.api+json");
2114 }
2115
2116 #[test]
2117 fn request_body_best_content_does_not_select_wildcard_media_ranges() {
2118 let body: RequestBody = serde_json::from_value(json!({
2119 "required": true,
2120 "content": {
2121 "image/*": {
2122 "schema": { "type": "string", "format": "binary" }
2123 },
2124 "*/*": {
2125 "schema": { "type": "string", "format": "binary" }
2126 }
2127 }
2128 }))
2129 .unwrap();
2130
2131 assert!(
2132 body.best_content().is_none(),
2133 "request media ranges require a runtime concrete Content-Type"
2134 );
2135 }
2136
2137 #[test]
2138 fn request_body_best_content_matches_parameterized_text_plain_by_essence() {
2139 let body: RequestBody = serde_json::from_value(json!({
2140 "required": true,
2141 "content": {
2142 "Text/Plain; charset=utf-8": {
2143 "schema": { "type": "string" }
2144 }
2145 }
2146 }))
2147 .unwrap();
2148
2149 let (media_type, _) = body.best_content().expect("parameterized text body");
2150 assert_eq!(media_type, "Text/Plain; charset=utf-8");
2151 }
2152
2153 #[test]
2154 fn response_json_schema_finds_vnd_api_plus_json() {
2155 let resp_json = json!({
2158 "description": "OK",
2159 "content": {
2160 "application/vnd.api+json": {
2161 "schema": {"$ref": "#/components/schemas/api_keys"}
2162 }
2163 }
2164 });
2165
2166 let resp: Response = serde_json::from_value(resp_json).unwrap();
2167 let schema = resp.json_schema().expect("expected +json schema match");
2168 assert!(schema.is_reference());
2169 }
2170}