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 || match self {
848 Schema::AnyOf { any_of, .. } => any_of
851 .iter()
852 .any(|branch| branch.is_explicit_null_only() || branch.is_nullable_any()),
853 Schema::OneOf { one_of, .. } => {
855 one_of
856 .iter()
857 .filter(|branch| branch.is_explicit_null_only() || branch.is_nullable_any())
858 .count()
859 == 1
860 }
861 _ => false,
862 }
863 }
864
865 fn reference_siblings_are_nullable(&self) -> bool {
870 let extra = match self {
871 Schema::Reference { extra, .. }
872 | Schema::RecursiveRef { extra, .. }
873 | Schema::DynamicRef { extra, .. } => extra,
874 _ => return false,
875 };
876 extra.get("nullable").and_then(Value::as_bool) == Some(true)
877 }
878
879 pub fn details(&self) -> &SchemaDetails {
881 static EMPTY_DETAILS: Lazy<SchemaDetails> = Lazy::new(SchemaDetails::default);
882 match self {
883 Schema::Typed { details, .. } => details,
884 Schema::TypedMulti { details, .. } => details,
885 Schema::Reference { .. }
888 | Schema::RecursiveRef { .. }
889 | Schema::DynamicRef { .. }
890 | Schema::Bool(_) => &EMPTY_DETAILS,
891 Schema::OneOf { details, .. } => details,
892 Schema::AnyOf { details, .. } => details,
893 Schema::AllOf { details, .. } => details,
894 Schema::Untyped { details } => details,
895 }
896 }
897
898 pub fn details_mut(&mut self) -> &mut SchemaDetails {
900 match self {
901 Schema::Typed { details, .. } => details,
902 Schema::TypedMulti { details, .. } => details,
903 Schema::Reference { .. } => {
904 panic!("Cannot get mutable details for reference schema")
905 }
906 Schema::RecursiveRef { .. } => {
907 panic!("Cannot get mutable details for recursive reference schema")
908 }
909 Schema::DynamicRef { .. } => {
910 panic!("Cannot get mutable details for dynamic reference schema")
911 }
912 Schema::Bool(_) => {
913 panic!("Cannot get mutable details for a boolean schema")
914 }
915 Schema::OneOf { details, .. } => details,
916 Schema::AnyOf { details, .. } => details,
917 Schema::AllOf { details, .. } => details,
918 Schema::Untyped { details } => details,
919 }
920 }
921
922 pub fn is_reference(&self) -> bool {
924 matches!(self, Schema::Reference { .. } | Schema::RecursiveRef { .. })
925 }
926
927 pub fn reference(&self) -> Option<&str> {
929 match self {
930 Schema::Reference { reference, .. } => Some(reference),
931 _ => None,
932 }
933 }
934
935 pub fn recursive_reference(&self) -> Option<&str> {
937 match self {
938 Schema::RecursiveRef { recursive_ref, .. } => Some(recursive_ref),
939 _ => None,
940 }
941 }
942
943 pub fn is_discriminated_union(&self) -> bool {
945 match self {
946 Schema::OneOf { discriminator, .. } => discriminator.is_some(),
947 Schema::AnyOf { discriminator, .. } => discriminator.is_some(),
948 _ => false,
949 }
950 }
951
952 pub fn discriminator(&self) -> Option<&Discriminator> {
954 match self {
955 Schema::OneOf { discriminator, .. } => discriminator.as_ref(),
956 Schema::AnyOf { discriminator, .. } => discriminator.as_ref(),
957 _ => None,
958 }
959 }
960
961 pub fn union_variants(&self) -> Option<&[Schema]> {
963 match self {
964 Schema::OneOf { one_of, .. } => Some(one_of),
965 Schema::AnyOf { any_of, .. } => Some(any_of),
966 _ => None,
967 }
968 }
969
970 pub fn is_nullable_pattern(&self) -> bool {
972 self.non_null_variant().is_some()
973 }
974
975 pub fn has_explicit_null_variant(&self) -> bool {
980 self.union_variants()
981 .is_some_and(|variants| variants.iter().any(Self::is_explicit_null_only))
982 }
983
984 pub fn non_null_variant(&self) -> Option<&Schema> {
987 let variants = match self {
988 Schema::AnyOf { any_of, .. } => any_of,
989 Schema::OneOf { one_of, .. } => one_of,
990 _ => return None,
991 };
992 let [first, second] = variants.as_slice() else {
993 return None;
994 };
995 match (
996 first.is_explicit_null_only(),
997 second.is_explicit_null_only(),
998 ) {
999 (true, false) => Some(second),
1000 (false, true) => Some(first),
1001 _ => None,
1003 }
1004 }
1005
1006 pub(crate) fn is_explicit_null_only(&self) -> bool {
1014 match self {
1015 Schema::Typed { schema_type, .. } => *schema_type == SchemaType::Null,
1016 Schema::TypedMulti { schema_types, .. } => {
1017 !schema_types.is_empty()
1018 && schema_types
1019 .iter()
1020 .all(|schema_type| *schema_type == SchemaType::Null)
1021 }
1022 Schema::Untyped { details } => {
1023 details.const_value.as_ref().is_some_and(Value::is_null)
1024 || details.enum_values.as_ref().is_some_and(
1025 |values| matches!(values.as_slice(), [value] if value.is_null()),
1026 )
1027 }
1028 _ => false,
1029 }
1030 }
1031
1032 pub fn inferred_type(&self) -> Option<SchemaType> {
1034 match self {
1035 Schema::Typed { schema_type, .. } => Some(schema_type.clone()),
1036 Schema::TypedMulti { .. } => self.schema_type().cloned(),
1037 Schema::Untyped { details } => {
1038 if self.is_explicit_null_only() {
1040 Some(SchemaType::Null)
1041 } else if details.properties.is_some() {
1042 Some(SchemaType::Object)
1043 } else if details.items.is_some() || details.prefix_items.is_some() {
1044 Some(SchemaType::Array)
1045 } else if details.enum_values.is_some() {
1046 Some(SchemaType::String) } else {
1048 None
1049 }
1050 }
1051 _ => None,
1052 }
1053 }
1054}
1055
1056impl SchemaDetails {
1057 pub fn item_schema(&self) -> Option<&Schema> {
1063 match self.items.as_ref()? {
1064 Items::Single(schema) => Some(schema),
1065 Items::Positional(_) => None,
1066 }
1067 }
1068
1069 pub fn positional_items(&self) -> Option<&[Schema]> {
1072 if let Some(prefix_items) = self.prefix_items.as_deref() {
1073 return Some(prefix_items);
1074 }
1075 match self.items.as_ref()? {
1076 Items::Positional(schemas) => Some(schemas),
1077 Items::Single(_) => None,
1078 }
1079 }
1080
1081 pub fn positional_items_are_closed(&self) -> bool {
1087 let Some(positions) = self.positional_items() else {
1088 return false;
1089 };
1090 if matches!(
1093 self.items.as_ref(),
1094 Some(Items::Single(schema)) if matches!(**schema, Schema::Bool(false))
1095 ) {
1096 return true;
1097 }
1098 if self.extra.get("additionalItems") == Some(&Value::Bool(false)) {
1099 return true;
1100 }
1101 self.max_items
1102 .is_some_and(|maximum| maximum <= positions.len() as u64)
1103 }
1104
1105 pub fn positional_items_are_exact(&self) -> bool {
1109 let Some(positions) = self.positional_items() else {
1110 return false;
1111 };
1112 self.positional_items_are_closed()
1113 && self
1114 .min_items
1115 .is_some_and(|minimum| minimum >= positions.len() as u64)
1116 }
1117
1118 pub fn is_nullable(&self) -> bool {
1120 self.nullable.unwrap_or(false)
1121 }
1122
1123 pub fn is_string_enum(&self) -> bool {
1129 self.enum_values.is_some() || self.const_string_value().is_some()
1130 }
1131
1132 pub fn string_enum_values(&self) -> Option<Vec<String>> {
1138 if let Some(values) = self.enum_values.as_ref() {
1139 return Some(
1144 values
1145 .iter()
1146 .map(|v| match v {
1147 Value::String(s) => s.clone(),
1148 Value::Number(n) => n.to_string(),
1149 Value::Bool(b) => b.to_string(),
1150 Value::Null => "null".to_string(),
1151 _ => v.to_string(),
1152 })
1153 .collect(),
1154 );
1155 }
1156 self.const_string_value().map(|s| vec![s])
1157 }
1158
1159 fn const_string_value(&self) -> Option<String> {
1160 self.const_value
1161 .as_ref()
1162 .and_then(|v| v.as_str())
1163 .map(|s| s.to_string())
1164 }
1165
1166 pub fn is_field_required(&self, field_name: &str) -> bool {
1168 self.required
1169 .as_ref()
1170 .map(|req| req.contains(&field_name.to_string()))
1171 .unwrap_or(false)
1172 }
1173}
1174
1175#[derive(Debug, Clone, Deserialize, Serialize)]
1177pub struct PathItem {
1178 #[serde(default)]
1179 pub summary: Option<String>,
1180 #[serde(default)]
1181 pub description: Option<String>,
1182 pub get: Option<Operation>,
1183 pub put: Option<Operation>,
1184 pub post: Option<Operation>,
1185 pub delete: Option<Operation>,
1186 pub options: Option<Operation>,
1187 pub head: Option<Operation>,
1188 pub patch: Option<Operation>,
1189 pub trace: Option<Operation>,
1190 pub query: Option<Operation>,
1193 #[serde(rename = "additionalOperations", default)]
1197 pub additional_operations: Option<BTreeMap<String, Operation>>,
1198 pub parameters: Option<Vec<Parameter>>,
1199 #[serde(default)]
1200 pub servers: Option<Vec<Server>>,
1201 #[serde(rename = "$ref", default)]
1202 pub reference: Option<String>,
1203 #[serde(flatten, default)]
1204 pub extensions: Extensions,
1205}
1206
1207impl PathItem {
1208 pub fn operations(&self) -> Vec<(&str, &Operation)> {
1211 let mut ops = Vec::new();
1212 if let Some(ref op) = self.get {
1213 ops.push(("get", op));
1214 }
1215 if let Some(ref op) = self.put {
1216 ops.push(("put", op));
1217 }
1218 if let Some(ref op) = self.post {
1219 ops.push(("post", op));
1220 }
1221 if let Some(ref op) = self.delete {
1222 ops.push(("delete", op));
1223 }
1224 if let Some(ref op) = self.options {
1225 ops.push(("options", op));
1226 }
1227 if let Some(ref op) = self.head {
1228 ops.push(("head", op));
1229 }
1230 if let Some(ref op) = self.patch {
1231 ops.push(("patch", op));
1232 }
1233 if let Some(ref op) = self.trace {
1234 ops.push(("trace", op));
1235 }
1236 if let Some(ref op) = self.query {
1237 ops.push(("query", op));
1238 }
1239 if let Some(map) = &self.additional_operations {
1240 for (verb, op) in map {
1241 ops.push((verb.as_str(), op));
1242 }
1243 }
1244 ops
1245 }
1246}
1247
1248#[derive(Debug, Clone, Deserialize, Serialize)]
1250pub struct Operation {
1251 #[serde(rename = "operationId", default)]
1252 pub operation_id: Option<String>,
1253 #[serde(default)]
1254 pub summary: Option<String>,
1255 #[serde(default)]
1256 pub description: Option<String>,
1257 #[serde(default)]
1258 pub tags: Option<Vec<String>>,
1259 #[serde(default)]
1260 pub deprecated: Option<bool>,
1261 pub parameters: Option<Vec<Parameter>>,
1262 #[serde(rename = "requestBody")]
1263 pub request_body: Option<RequestBody>,
1264 pub responses: Option<BTreeMap<String, Response>>,
1265 #[serde(default)]
1266 pub callbacks: Option<BTreeMap<String, Callback>>,
1267 #[serde(default)]
1268 pub security: Option<Vec<BTreeMap<String, Vec<String>>>>,
1269 #[serde(default)]
1270 pub servers: Option<Vec<Server>>,
1271 #[serde(rename = "externalDocs", default)]
1272 pub external_docs: Option<ExternalDocs>,
1273 #[serde(flatten, default)]
1274 pub extensions: Extensions,
1275}
1276
1277#[derive(Debug, Clone, Deserialize, Serialize)]
1279pub struct Parameter {
1280 #[serde(default)]
1281 pub name: Option<String>,
1282 #[serde(rename = "in", default)]
1283 pub location: Option<String>,
1284 #[serde(default)]
1285 pub required: Option<bool>,
1286 #[serde(default)]
1287 pub deprecated: Option<bool>,
1288 #[serde(rename = "allowEmptyValue", default)]
1289 pub allow_empty_value: Option<bool>,
1290 #[serde(default)]
1291 pub style: Option<String>,
1292 #[serde(default)]
1293 pub explode: Option<bool>,
1294 #[serde(rename = "allowReserved", default)]
1295 pub allow_reserved: Option<bool>,
1296 #[serde(default)]
1297 pub schema: Option<Schema>,
1298 #[serde(default)]
1299 pub content: Option<BTreeMap<String, MediaType>>,
1300 #[serde(default)]
1301 pub example: Option<Value>,
1302 #[serde(default)]
1303 pub examples: Option<BTreeMap<String, Example>>,
1304 #[serde(default)]
1305 pub description: Option<String>,
1306 #[serde(rename = "$ref", default)]
1307 pub reference: Option<String>,
1308 #[serde(flatten, default)]
1309 pub extensions: Extensions,
1310}
1311
1312#[derive(Debug, Clone, Deserialize, Serialize)]
1314pub struct RequestBody {
1315 pub content: Option<BTreeMap<String, MediaType>>,
1316 #[serde(default)]
1317 pub description: Option<String>,
1318 #[serde(default)]
1319 pub required: Option<bool>,
1320 #[serde(rename = "$ref", default)]
1321 pub reference: Option<String>,
1322 #[serde(flatten, default)]
1323 pub extensions: Extensions,
1324}
1325
1326#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1332#[serde(rename_all = "snake_case")]
1333pub enum ResponseMediaKind {
1334 Json,
1335 EventStream,
1336 Text,
1337 Binary,
1338 Unsupported,
1339}
1340
1341pub fn media_type_essence(content_type: &str) -> &str {
1347 content_type
1348 .split(';')
1349 .next()
1350 .unwrap_or(content_type)
1351 .trim()
1352}
1353
1354pub fn is_json_media_type(ct: &str) -> bool {
1362 let essence = media_type_essence(ct).to_ascii_lowercase();
1363 if essence == "application/json" {
1364 return true;
1365 }
1366 if let Some(subtype) = essence.strip_prefix("application/") {
1367 return subtype.ends_with("+json");
1368 }
1369 false
1370}
1371
1372pub fn is_form_urlencoded_media_type(ct: &str) -> bool {
1375 let essence = media_type_essence(ct).to_ascii_lowercase();
1376 essence == "application/x-www-form-urlencoded"
1377}
1378
1379pub fn is_event_stream_media_type(ct: &str) -> bool {
1385 media_type_essence(ct).eq_ignore_ascii_case("text/event-stream")
1386}
1387
1388pub fn is_text_media_type(ct: &str) -> bool {
1396 let Some((top_level, subtype)) = media_type_essence(ct).split_once('/') else {
1397 return false;
1398 };
1399 if top_level.eq_ignore_ascii_case("text")
1400 && !subtype.is_empty()
1401 && !is_event_stream_media_type(ct)
1402 {
1403 return true;
1404 }
1405 top_level.eq_ignore_ascii_case("application")
1406 && (subtype.eq_ignore_ascii_case("xml")
1407 || subtype.to_ascii_lowercase().ends_with("+xml")
1408 || subtype.eq_ignore_ascii_case("jwt"))
1411}
1412
1413pub fn is_wildcard_media_type(ct: &str) -> bool {
1419 let Some((top_level, subtype)) = media_type_essence(ct).split_once('/') else {
1420 return false;
1421 };
1422 !top_level.is_empty() && subtype == "*"
1423}
1424
1425fn schema_has_binary_format(schema: Option<&Schema>) -> bool {
1426 schema.is_some_and(|schema| {
1427 schema
1428 .details()
1429 .format
1430 .as_deref()
1431 .is_some_and(|format| format.eq_ignore_ascii_case("binary"))
1432 })
1433}
1434
1435pub fn is_binary_media_type(ct: &str, schema: Option<&Schema>) -> bool {
1443 if schema_has_binary_format(schema) {
1444 return true;
1445 }
1446
1447 let essence = media_type_essence(ct);
1448 let Some((top_level, _)) = essence.split_once('/') else {
1449 return false;
1450 };
1451 if top_level.eq_ignore_ascii_case("image")
1452 || top_level.eq_ignore_ascii_case("audio")
1453 || top_level.eq_ignore_ascii_case("video")
1454 {
1455 return true;
1456 }
1457 if essence.eq_ignore_ascii_case("application/octet-stream")
1458 || essence.eq_ignore_ascii_case("application/zip")
1459 || essence.eq_ignore_ascii_case("application/pdf")
1460 {
1461 return true;
1462 }
1463
1464 !top_level.eq_ignore_ascii_case("text") && is_wildcard_media_type(ct)
1465}
1466
1467pub fn classify_response_media_type(ct: &str, schema: Option<&Schema>) -> ResponseMediaKind {
1472 if is_json_media_type(ct) {
1473 ResponseMediaKind::Json
1474 } else if is_event_stream_media_type(ct) {
1475 ResponseMediaKind::EventStream
1476 } else if schema_has_binary_format(schema) {
1477 ResponseMediaKind::Binary
1478 } else if is_text_media_type(ct) {
1479 if is_wildcard_media_type(ct) {
1480 ResponseMediaKind::Unsupported
1481 } else {
1482 ResponseMediaKind::Text
1483 }
1484 } else if is_binary_media_type(ct, schema) {
1485 ResponseMediaKind::Binary
1486 } else {
1487 ResponseMediaKind::Unsupported
1488 }
1489}
1490
1491fn find_json_content(content: &BTreeMap<String, MediaType>) -> Option<(&str, &MediaType)> {
1492 if let Some(mt) = content
1493 .get("application/json")
1494 .filter(|media_type| media_type.schema.is_some())
1495 {
1496 return Some(("application/json", mt));
1497 }
1498 content
1499 .iter()
1500 .find(|(ct, media_type)| is_json_media_type(ct) && media_type.schema.is_some())
1501 .map(|(ct, mt)| (ct.as_str(), mt))
1502 .or_else(|| {
1503 content
1504 .get("application/json")
1505 .map(|media_type| ("application/json", media_type))
1506 })
1507 .or_else(|| {
1508 content
1509 .iter()
1510 .find(|(ct, _)| is_json_media_type(ct))
1511 .map(|(ct, mt)| (ct.as_str(), mt))
1512 })
1513}
1514
1515impl RequestBody {
1516 pub fn json_schema(&self) -> Option<&Schema> {
1522 self.content
1523 .as_ref()
1524 .and_then(find_json_content)
1525 .and_then(|(_, media_type)| media_type.schema.as_ref())
1526 }
1527
1528 pub fn best_content(&self) -> Option<(&str, Option<&Schema>)> {
1530 let content = self.content.as_ref()?;
1531
1532 if let Some((ct, media_type)) = find_json_content(content) {
1533 return Some((ct, media_type.schema.as_ref()));
1534 }
1535
1536 const PRIORITY: &[&str] = &[
1537 "application/x-www-form-urlencoded",
1538 "multipart/form-data",
1539 "application/octet-stream",
1540 "text/plain",
1541 ];
1542 for preferred_essence in PRIORITY {
1543 if let Some((ct, media_type)) = content
1544 .iter()
1545 .find(|(ct, _)| media_type_essence(ct).eq_ignore_ascii_case(preferred_essence))
1546 {
1547 return Some((ct.as_str(), media_type.schema.as_ref()));
1548 }
1549 }
1550 if let Some((ct, media_type)) = content.iter().find(|(ct, _)| is_text_media_type(ct)) {
1553 return Some((ct.as_str(), media_type.schema.as_ref()));
1554 }
1555 content
1556 .iter()
1557 .find(|(ct, media_type)| {
1563 !is_wildcard_media_type(ct) && is_binary_media_type(ct, media_type.schema.as_ref())
1564 })
1565 .map(|(ct, media_type)| (ct.as_str(), media_type.schema.as_ref()))
1566 }
1567}
1568
1569#[derive(Debug, Clone, Deserialize, Serialize)]
1571pub struct Response {
1572 #[serde(default)]
1573 pub description: Option<String>,
1574 #[serde(default)]
1575 pub headers: Option<BTreeMap<String, Header>>,
1576 #[serde(default)]
1577 pub content: Option<BTreeMap<String, MediaType>>,
1578 #[serde(default)]
1579 pub links: Option<Value>,
1580 #[serde(rename = "$ref", default)]
1581 pub reference: Option<String>,
1582 #[serde(flatten, default)]
1583 pub extensions: Extensions,
1584}
1585
1586impl Response {
1587 pub fn json_schema(&self) -> Option<&Schema> {
1594 self.content
1595 .as_ref()
1596 .and_then(find_json_content)
1597 .and_then(|(_, media_type)| media_type.schema.as_ref())
1598 }
1599
1600 pub fn json_content(&self) -> Option<(&str, &Schema)> {
1602 self.content
1603 .as_ref()
1604 .and_then(find_json_content)
1605 .and_then(|(content_type, media_type)| {
1606 media_type
1607 .schema
1608 .as_ref()
1609 .map(|schema| (content_type, schema))
1610 })
1611 }
1612}
1613
1614#[derive(Debug, Clone, Deserialize, Serialize)]
1616pub struct MediaType {
1617 #[serde(default)]
1618 pub schema: Option<Schema>,
1619 #[serde(default)]
1620 pub example: Option<Value>,
1621 #[serde(default)]
1622 pub examples: Option<BTreeMap<String, Example>>,
1623 #[serde(default)]
1624 pub encoding: Option<BTreeMap<String, Encoding>>,
1625 #[serde(rename = "itemSchema", default)]
1628 pub item_schema: Option<Schema>,
1629 #[serde(rename = "prefixEncoding", default)]
1632 pub prefix_encoding: Option<Vec<Encoding>>,
1633 #[serde(rename = "itemEncoding", default)]
1636 pub item_encoding: Option<Encoding>,
1637 #[serde(rename = "$ref", default)]
1638 pub reference: Option<String>,
1639 #[serde(flatten, default)]
1640 pub extensions: Extensions,
1641}
1642
1643#[cfg(test)]
1644#[allow(clippy::unwrap_used, clippy::expect_used)]
1645mod tests {
1646 use super::*;
1647 use serde_json::json;
1648
1649 #[test]
1650 fn paths_map_skips_extension_scalars() {
1651 let spec: OpenApiSpec = serde_json::from_value(json!({
1654 "openapi": "3.0.0",
1655 "info": { "title": "lenient paths", "version": "1" },
1656 "paths": {
1657 "x-codegen-contextRoot": "/apis/registry/v2",
1658 "/items": {
1659 "get": {
1660 "operationId": "listItems",
1661 "responses": { "204": { "description": "ok" } }
1662 }
1663 }
1664 }
1665 }))
1666 .unwrap();
1667 let paths = spec.paths.unwrap();
1668 assert!(paths.contains_key("/items"));
1669 assert!(!paths.contains_key("x-codegen-contextRoot"));
1670 }
1671
1672 #[test]
1673 fn test_parse_simple_object_schema() {
1674 let schema_json = json!({
1675 "type": "object",
1676 "properties": {
1677 "name": {
1678 "type": "string",
1679 "description": "User name"
1680 },
1681 "age": {
1682 "type": "integer"
1683 }
1684 },
1685 "required": ["name"]
1686 });
1687
1688 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1689
1690 match schema {
1691 Schema::Typed {
1692 schema_type: SchemaType::Object,
1693 details,
1694 } => {
1695 assert!(details.properties.is_some());
1696 assert_eq!(details.required, Some(vec!["name".to_string()]));
1697 assert!(details.is_field_required("name"));
1698 assert!(!details.is_field_required("age"));
1699 }
1700 _ => panic!("Expected object schema"),
1701 }
1702 }
1703
1704 #[test]
1705 fn test_parse_string_enum() {
1706 let schema_json = json!({
1707 "type": "string",
1708 "enum": ["active", "inactive", "pending"],
1709 "description": "User status"
1710 });
1711
1712 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1713
1714 match schema {
1715 Schema::Typed {
1716 schema_type: SchemaType::String,
1717 details,
1718 } => {
1719 assert!(details.is_string_enum());
1720 let values = details.string_enum_values().unwrap();
1721 assert_eq!(values, vec!["active", "inactive", "pending"]);
1722 }
1723 _ => panic!("Expected string enum schema"),
1724 }
1725 }
1726
1727 #[test]
1728 fn test_parse_reference_schema() {
1729 let schema_json = json!({
1730 "$ref": "#/components/schemas/User"
1731 });
1732
1733 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1734
1735 assert!(schema.is_reference());
1736 assert_eq!(schema.reference(), Some("#/components/schemas/User"));
1737 }
1738
1739 #[test]
1740 fn test_parse_discriminated_union() {
1741 let schema_json = json!({
1742 "oneOf": [
1743 {"$ref": "#/components/schemas/Dog"},
1744 {"$ref": "#/components/schemas/Cat"}
1745 ],
1746 "discriminator": {
1747 "propertyName": "petType"
1748 }
1749 });
1750
1751 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1752
1753 assert!(schema.is_discriminated_union());
1754 let discriminator = schema.discriminator().unwrap();
1755 assert_eq!(discriminator.property_name, "petType");
1756 }
1757
1758 #[test]
1759 fn test_parse_nullable_pattern() {
1760 let schema_json = json!({
1761 "anyOf": [
1762 {"$ref": "#/components/schemas/User"},
1763 {"type": "null"}
1764 ]
1765 });
1766
1767 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1768
1769 assert!(schema.is_nullable_pattern());
1770 let non_null = schema.non_null_variant().unwrap();
1771 assert!(non_null.is_reference());
1772 }
1773
1774 #[test]
1775 fn explicit_null_only_branches_are_order_independent_nullable_patterns() {
1776 for union_keyword in ["anyOf", "oneOf"] {
1777 for null_schema in [
1778 json!({"type": "null"}),
1779 json!({"type": ["null"]}),
1780 json!({"const": null}),
1781 json!({"enum": [null]}),
1782 ] {
1783 for variants in [
1784 vec![
1785 json!({"$ref": "#/components/schemas/User"}),
1786 null_schema.clone(),
1787 ],
1788 vec![
1789 null_schema.clone(),
1790 json!({"$ref": "#/components/schemas/User"}),
1791 ],
1792 ] {
1793 let schema: Schema = serde_json::from_value(json!({
1794 (union_keyword): variants,
1795 }))
1796 .unwrap();
1797 let non_null = schema.non_null_variant().unwrap_or_else(|| {
1798 panic!("{union_keyword} must recognize {null_schema} as null-only")
1799 });
1800 assert_eq!(
1801 non_null.reference(),
1802 Some("#/components/schemas/User"),
1803 "{union_keyword} with {null_schema}"
1804 );
1805 }
1806 }
1807 }
1808 }
1809
1810 #[test]
1811 fn nullable_schemas_are_real_union_branches_even_beside_references() {
1812 for union_keyword in ["anyOf", "oneOf"] {
1813 for nullable_branch in [
1814 json!({"nullable": true}),
1815 json!({"type": "object", "nullable": true}),
1816 json!({
1817 "$ref": "#/components/schemas/Other",
1818 "nullable": true
1819 }),
1820 ] {
1821 for variants in [
1822 vec![
1823 json!({"$ref": "#/components/schemas/User"}),
1824 nullable_branch.clone(),
1825 ],
1826 vec![
1827 nullable_branch.clone(),
1828 json!({"$ref": "#/components/schemas/User"}),
1829 ],
1830 ] {
1831 let schema: Schema = serde_json::from_value(json!({
1832 (union_keyword): variants,
1833 }))
1834 .unwrap();
1835 assert!(
1836 schema.non_null_variant().is_none(),
1837 "{union_keyword} must preserve nullable branch {nullable_branch}"
1838 );
1839 }
1840 }
1841 }
1842 }
1843
1844 #[test]
1845 fn reference_sibling_nullable_annotation_is_not_lost() {
1846 for reference_keyword in ["$ref", "$recursiveRef", "$dynamicRef"] {
1847 let nullable: Schema = serde_json::from_value(json!({
1848 (reference_keyword): "#/components/schemas/Value",
1849 "nullable": true,
1850 "description": "retained sibling"
1851 }))
1852 .unwrap();
1853 assert!(
1854 nullable.is_nullable_any(),
1855 "{reference_keyword} should retain nullable:true"
1856 );
1857
1858 let non_nullable: Schema = serde_json::from_value(json!({
1859 (reference_keyword): "#/components/schemas/Value",
1860 "nullable": false
1861 }))
1862 .unwrap();
1863 assert!(
1864 !non_nullable.is_nullable_any(),
1865 "{reference_keyword} nullable:false must stay non-nullable"
1866 );
1867 }
1868 }
1869
1870 #[test]
1871 fn nullable_union_branches_preserve_union_nullability() {
1872 for (schema, expected) in [
1873 (
1874 json!({"anyOf": [{"type": "object", "nullable": true}, {"type": "string", "nullable": true}]}),
1875 true,
1876 ),
1877 (
1878 json!({"oneOf": [{"type": "object", "nullable": true}, {"type": "string"}]}),
1879 true,
1880 ),
1881 (
1882 json!({"oneOf": [{"type": "object", "nullable": true}, {"type": "string", "nullable": true}]}),
1883 false,
1884 ),
1885 ] {
1886 let schema: Schema = serde_json::from_value(schema.clone()).unwrap();
1887 assert_eq!(schema.is_nullable_any(), expected, "{schema:?}");
1888 }
1889 }
1890
1891 #[test]
1892 fn null_only_enum_and_const_infer_null_instead_of_string() {
1893 for source in [json!({"enum": [null]}), json!({"const": null})] {
1894 let schema: Schema = serde_json::from_value(source.clone()).unwrap();
1895 assert_eq!(schema.inferred_type(), Some(SchemaType::Null), "{source}");
1896 }
1897
1898 for source in [
1899 json!({"enum": ["null"]}),
1900 json!({"enum": ["ready", null]}),
1901 json!({"type": "string", "enum": ["ready"]}),
1902 ] {
1903 let schema: Schema = serde_json::from_value(source.clone()).unwrap();
1904 assert_ne!(schema.inferred_type(), Some(SchemaType::Null), "{source}");
1905 }
1906 }
1907
1908 #[test]
1909 fn three_branch_unions_do_not_collapse_even_with_an_explicit_null() {
1910 for union_keyword in ["anyOf", "oneOf"] {
1911 let schema: Schema = serde_json::from_value(json!({
1912 (union_keyword): [
1913 {"$ref": "#/components/schemas/User"},
1914 {"type": "null"},
1915 {"type": "array", "items": {"type": "string"}}
1916 ],
1917 }))
1918 .unwrap();
1919 assert!(
1920 schema.non_null_variant().is_none(),
1921 "{union_keyword} with three branches must retain its non-null union"
1922 );
1923 assert!(
1924 schema.is_nullable_any(),
1925 "{union_keyword} with an explicit null branch must remain nullable"
1926 );
1927 }
1928 }
1929
1930 #[test]
1931 fn is_json_media_type_accepts_canonical_and_structured_suffix() {
1932 assert!(is_json_media_type("application/json"));
1934 assert!(is_json_media_type("application/json; charset=utf-8"));
1936 assert!(is_json_media_type("APPLICATION/JSON"));
1937 assert!(is_json_media_type("application/vnd.api+json"));
1939 assert!(is_json_media_type("application/hal+json"));
1940 assert!(is_json_media_type("application/problem+json"));
1941 assert!(is_json_media_type("application/ld+json"));
1942 assert!(is_json_media_type(
1943 "application/vnd.api+json; charset=utf-8"
1944 ));
1945 assert!(!is_json_media_type("application/xml"));
1947 assert!(!is_json_media_type("application/x-www-form-urlencoded"));
1948 assert!(!is_json_media_type("text/plain"));
1949 assert!(!is_json_media_type("application/jsonbutnotreally"));
1950 assert!(!is_json_media_type("text/something+json"));
1952 }
1953
1954 #[test]
1955 fn response_media_helpers_normalize_parameters_and_case() {
1956 assert_eq!(
1957 media_type_essence(" Text/Plain ; charset=utf-8 "),
1958 "Text/Plain"
1959 );
1960 assert!(is_text_media_type("TEXT/HTML; charset=UTF-8"));
1961 assert!(!is_text_media_type("Text/Event-Stream; charset=utf-8"));
1962 assert!(is_wildcard_media_type("*/*; q=0.8"));
1963 assert!(is_wildcard_media_type("IMAGE/*"));
1964 assert!(is_wildcard_media_type("text/*"));
1965 assert!(!is_wildcard_media_type("image/*+json"));
1966 assert!(!is_wildcard_media_type("application/json"));
1967 }
1968
1969 #[test]
1970 fn response_media_classifier_keeps_json_sse_and_text_distinct() {
1971 for media_type in [
1972 "application/json",
1973 "APPLICATION/PROBLEM+JSON; charset=utf-8",
1974 ] {
1975 assert_eq!(
1976 classify_response_media_type(media_type, None),
1977 ResponseMediaKind::Json,
1978 "{media_type}"
1979 );
1980 }
1981
1982 assert_eq!(
1983 classify_response_media_type("Text/Event-Stream; charset=utf-8", None),
1984 ResponseMediaKind::EventStream
1985 );
1986 for media_type in ["text/plain", "TEXT/HTML; charset=UTF-8"] {
1987 assert_eq!(
1988 classify_response_media_type(media_type, None),
1989 ResponseMediaKind::Text,
1990 "{media_type}"
1991 );
1992 }
1993 assert_eq!(
1994 classify_response_media_type("text/event-streaming", None),
1995 ResponseMediaKind::Text
1996 );
1997 assert_eq!(
1998 classify_response_media_type("text/*", None),
1999 ResponseMediaKind::Unsupported,
2000 "a media range is not a valid concrete response Content-Type"
2001 );
2002 }
2003
2004 #[test]
2005 fn response_json_content_skips_schema_less_canonical_entry() {
2006 let response: Response = serde_json::from_value(json!({
2007 "description": "mixed JSON",
2008 "content": {
2009 "application/json": {},
2010 "application/vnd.example+json": {
2011 "schema": { "type": "string" }
2012 }
2013 }
2014 }))
2015 .unwrap();
2016
2017 let (media_type, schema) = response.json_content().expect("schema-bearing JSON");
2018 assert_eq!(media_type, "application/vnd.example+json");
2019 assert!(matches!(schema.schema_type(), Some(SchemaType::String)));
2020 }
2021
2022 #[test]
2023 fn response_media_classifier_recognizes_binary_formats_and_wildcards() {
2024 for media_type in [
2025 "image/png",
2026 "IMAGE/*; version=1",
2027 "audio/mpeg",
2028 "video/mp4",
2029 "application/octet-stream",
2030 "APPLICATION/ZIP; version=1",
2031 "application/*",
2032 "*/*",
2033 ] {
2034 assert_eq!(
2035 classify_response_media_type(media_type, None),
2036 ResponseMediaKind::Binary,
2037 "{media_type}"
2038 );
2039 }
2040
2041 let binary_schema: Schema = serde_json::from_value(json!({
2042 "type": "string",
2043 "format": "BINARY"
2044 }))
2045 .unwrap();
2046 assert_eq!(
2047 classify_response_media_type("application/x-custom", Some(&binary_schema)),
2048 ResponseMediaKind::Binary
2049 );
2050 assert_eq!(
2051 classify_response_media_type("text/plain", Some(&binary_schema)),
2052 ResponseMediaKind::Binary,
2053 "an explicit binary schema must prevent UTF-8 decoding"
2054 );
2055 assert!(is_binary_media_type(
2056 "application/x-custom",
2057 Some(&binary_schema)
2058 ));
2059 }
2060
2061 #[test]
2062 fn response_media_classifier_leaves_ambiguous_formats_unsupported() {
2063 let string_schema: Schema = serde_json::from_value(json!({ "type": "string" })).unwrap();
2064 for media_type in ["application/x-unknown", "not-a-media-type"] {
2065 assert_eq!(
2066 classify_response_media_type(media_type, Some(&string_schema)),
2067 ResponseMediaKind::Unsupported,
2068 "{media_type}"
2069 );
2070 }
2071 assert_eq!(
2075 classify_response_media_type("application/pdf", Some(&string_schema)),
2076 ResponseMediaKind::Binary
2077 );
2078 assert_eq!(
2079 classify_response_media_type("application/xml", Some(&string_schema)),
2080 ResponseMediaKind::Text
2081 );
2082 assert_eq!(
2083 classify_response_media_type("application/atom+xml", Some(&string_schema)),
2084 ResponseMediaKind::Text
2085 );
2086 assert_eq!(
2088 classify_response_media_type("application/jwt", Some(&string_schema)),
2089 ResponseMediaKind::Text
2090 );
2091 assert!(!is_binary_media_type("text/plain", None));
2092 }
2093
2094 #[test]
2095 fn request_body_json_schema_finds_vnd_api_plus_json() {
2096 let body_json = json!({
2099 "required": true,
2100 "content": {
2101 "application/vnd.api+json": {
2102 "schema": {"$ref": "#/components/schemas/create_api_key"}
2103 }
2104 }
2105 });
2106
2107 let body: RequestBody = serde_json::from_value(body_json).unwrap();
2108 let schema = body.json_schema().expect("expected +json schema match");
2109 assert!(schema.is_reference());
2110 }
2111
2112 #[test]
2113 fn request_body_best_content_prefers_canonical_json_over_plus_json() {
2114 let body_json = json!({
2118 "required": true,
2119 "content": {
2120 "application/json": {
2121 "schema": {"$ref": "#/components/schemas/A"}
2122 },
2123 "application/vnd.api+json": {
2124 "schema": {"$ref": "#/components/schemas/B"}
2125 }
2126 }
2127 });
2128
2129 let body: RequestBody = serde_json::from_value(body_json).unwrap();
2130 let (ct, _) = body.best_content().expect("expected best_content");
2131 assert_eq!(ct, "application/json");
2132 }
2133
2134 #[test]
2135 fn request_body_best_content_falls_back_to_plus_json() {
2136 let body_json = json!({
2139 "required": true,
2140 "content": {
2141 "application/vnd.api+json": {
2142 "schema": {"$ref": "#/components/schemas/B"}
2143 }
2144 }
2145 });
2146
2147 let body: RequestBody = serde_json::from_value(body_json).unwrap();
2148 let (ct, _) = body.best_content().expect("expected best_content");
2149 assert_eq!(ct, "application/vnd.api+json");
2150 }
2151
2152 #[test]
2153 fn request_body_best_content_does_not_select_wildcard_media_ranges() {
2154 let body: RequestBody = serde_json::from_value(json!({
2155 "required": true,
2156 "content": {
2157 "image/*": {
2158 "schema": { "type": "string", "format": "binary" }
2159 },
2160 "*/*": {
2161 "schema": { "type": "string", "format": "binary" }
2162 }
2163 }
2164 }))
2165 .unwrap();
2166
2167 assert!(
2168 body.best_content().is_none(),
2169 "request media ranges require a runtime concrete Content-Type"
2170 );
2171 }
2172
2173 #[test]
2174 fn request_body_best_content_matches_parameterized_text_plain_by_essence() {
2175 let body: RequestBody = serde_json::from_value(json!({
2176 "required": true,
2177 "content": {
2178 "Text/Plain; charset=utf-8": {
2179 "schema": { "type": "string" }
2180 }
2181 }
2182 }))
2183 .unwrap();
2184
2185 let (media_type, _) = body.best_content().expect("parameterized text body");
2186 assert_eq!(media_type, "Text/Plain; charset=utf-8");
2187 }
2188
2189 #[test]
2190 fn response_json_schema_finds_vnd_api_plus_json() {
2191 let resp_json = json!({
2194 "description": "OK",
2195 "content": {
2196 "application/vnd.api+json": {
2197 "schema": {"$ref": "#/components/schemas/api_keys"}
2198 }
2199 }
2200 });
2201
2202 let resp: Response = serde_json::from_value(resp_json).unwrap();
2203 let schema = resp.json_schema().expect("expected +json schema match");
2204 assert!(schema.is_reference());
2205 }
2206}