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)]
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
34#[derive(Debug, Clone, Deserialize, Serialize)]
35pub struct Info {
36 pub title: String,
37 #[serde(default)]
38 pub summary: Option<String>,
39 #[serde(default)]
40 pub description: Option<String>,
41 #[serde(rename = "termsOfService", default)]
42 pub terms_of_service: Option<String>,
43 #[serde(default)]
44 pub contact: Option<Value>,
45 #[serde(default)]
46 pub license: Option<Value>,
47 #[serde(default)]
48 pub version: Option<String>,
49 #[serde(flatten, default)]
50 pub extensions: Extensions,
51}
52
53#[derive(Debug, Clone, Deserialize, Serialize)]
54pub struct Components {
55 #[serde(default)]
56 pub schemas: Option<BTreeMap<String, Schema>>,
57 #[serde(default)]
58 pub responses: Option<BTreeMap<String, Response>>,
59 #[serde(default)]
60 pub parameters: Option<BTreeMap<String, Parameter>>,
61 #[serde(default)]
62 pub examples: Option<BTreeMap<String, Example>>,
63 #[serde(rename = "requestBodies", default)]
64 pub request_bodies: Option<BTreeMap<String, RequestBody>>,
65 #[serde(default)]
66 pub headers: Option<BTreeMap<String, Header>>,
67 #[serde(rename = "securitySchemes", default)]
68 pub security_schemes: Option<BTreeMap<String, SecurityScheme>>,
69 #[serde(default)]
70 pub links: Option<BTreeMap<String, Link>>,
71 #[serde(default)]
72 pub callbacks: Option<BTreeMap<String, Callback>>,
73 #[serde(rename = "pathItems", default)]
75 pub path_items: Option<BTreeMap<String, PathItem>>,
76 #[serde(rename = "mediaTypes", default)]
78 pub media_types: Option<BTreeMap<String, MediaType>>,
79 #[serde(flatten, default)]
80 pub extensions: Extensions,
81}
82
83#[derive(Debug, Clone, Deserialize, Serialize)]
84#[serde(untagged)]
85pub enum Schema {
86 Reference {
88 #[serde(rename = "$ref")]
89 reference: String,
90 #[serde(flatten)]
91 extra: BTreeMap<String, Value>,
92 },
93 RecursiveRef {
95 #[serde(rename = "$recursiveRef")]
96 recursive_ref: String,
97 #[serde(flatten)]
98 extra: BTreeMap<String, Value>,
99 },
100 DynamicRef {
106 #[serde(rename = "$dynamicRef")]
107 dynamic_ref: String,
108 #[serde(flatten)]
109 extra: BTreeMap<String, Value>,
110 },
111 OneOf {
113 #[serde(rename = "oneOf")]
114 one_of: Vec<Schema>,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 discriminator: Option<Discriminator>,
117 #[serde(flatten)]
118 details: SchemaDetails,
119 },
120 AnyOf {
122 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
123 schema_type: Option<SchemaType>,
124 #[serde(rename = "anyOf")]
125 any_of: Vec<Schema>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 discriminator: Option<Discriminator>,
128 #[serde(flatten)]
129 details: SchemaDetails,
130 },
131 TypedMulti {
136 #[serde(rename = "type")]
137 schema_types: Vec<SchemaType>,
138 #[serde(flatten)]
139 details: SchemaDetails,
140 },
141 Typed {
143 #[serde(rename = "type")]
144 schema_type: SchemaType,
145 #[serde(flatten)]
146 details: SchemaDetails,
147 },
148 AllOf {
150 #[serde(rename = "allOf")]
151 all_of: Vec<Schema>,
152 #[serde(flatten)]
153 details: SchemaDetails,
154 },
155 Untyped {
157 #[serde(flatten)]
158 details: SchemaDetails,
159 },
160}
161
162#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
163#[serde(rename_all = "lowercase")]
164pub enum SchemaType {
165 String,
166 Integer,
167 Number,
168 Boolean,
169 Array,
170 Object,
171 #[serde(rename = "null")]
172 Null,
173}
174
175#[derive(Debug, Clone, Default, Deserialize, Serialize)]
176pub struct SchemaDetails {
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub description: Option<String>,
179 #[serde(skip_serializing_if = "Option::is_none")]
180 pub nullable: Option<bool>,
181
182 #[serde(rename = "$recursiveAnchor", skip_serializing_if = "Option::is_none")]
184 pub recursive_anchor: Option<bool>,
185
186 #[serde(rename = "$dynamicAnchor", skip_serializing_if = "Option::is_none")]
188 pub dynamic_anchor: Option<String>,
189 #[serde(rename = "$id", skip_serializing_if = "Option::is_none")]
190 pub schema_id: Option<String>,
191
192 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
194 pub enum_values: Option<Vec<Value>>,
195 #[serde(skip_serializing_if = "Option::is_none")]
196 pub format: Option<String>,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 pub default: Option<Value>,
199 #[serde(
200 rename = "const",
201 default,
202 deserialize_with = "deserialize_present_value",
203 skip_serializing_if = "Option::is_none"
204 )]
205 pub const_value: Option<Value>,
206
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub properties: Option<BTreeMap<String, Schema>>,
210 #[serde(skip_serializing_if = "Option::is_none")]
211 pub required: Option<Vec<String>>,
212 #[serde(
213 rename = "additionalProperties",
214 skip_serializing_if = "Option::is_none"
215 )]
216 pub additional_properties: Option<AdditionalProperties>,
217
218 #[serde(skip_serializing_if = "Option::is_none")]
220 pub items: Option<Box<Schema>>,
221
222 #[serde(skip_serializing_if = "Option::is_none")]
224 pub minimum: Option<f64>,
225 #[serde(skip_serializing_if = "Option::is_none")]
226 pub maximum: Option<f64>,
227
228 #[serde(rename = "minLength", skip_serializing_if = "Option::is_none")]
230 pub min_length: Option<u64>,
231 #[serde(rename = "maxLength", skip_serializing_if = "Option::is_none")]
232 pub max_length: Option<u64>,
233 #[serde(skip_serializing_if = "Option::is_none")]
234 pub pattern: Option<String>,
235 #[serde(rename = "exclusiveMinimum", skip_serializing_if = "Option::is_none")]
239 pub exclusive_minimum: Option<ExclusiveBound>,
240 #[serde(rename = "exclusiveMaximum", skip_serializing_if = "Option::is_none")]
241 pub exclusive_maximum: Option<ExclusiveBound>,
242 #[serde(rename = "multipleOf", skip_serializing_if = "Option::is_none")]
243 pub multiple_of: Option<f64>,
244 #[serde(rename = "minItems", skip_serializing_if = "Option::is_none")]
245 pub min_items: Option<u64>,
246 #[serde(rename = "maxItems", skip_serializing_if = "Option::is_none")]
247 pub max_items: Option<u64>,
248 #[serde(rename = "uniqueItems", skip_serializing_if = "Option::is_none")]
249 pub unique_items: Option<bool>,
250 #[serde(rename = "minProperties", skip_serializing_if = "Option::is_none")]
251 pub min_properties: Option<u64>,
252 #[serde(rename = "maxProperties", skip_serializing_if = "Option::is_none")]
253 pub max_properties: Option<u64>,
254
255 #[serde(rename = "prefixItems", skip_serializing_if = "Option::is_none")]
257 pub prefix_items: Option<Vec<Schema>>,
258 #[serde(skip_serializing_if = "Option::is_none")]
259 pub contains: Option<Box<Schema>>,
260 #[serde(rename = "minContains", skip_serializing_if = "Option::is_none")]
261 pub min_contains: Option<u64>,
262 #[serde(rename = "maxContains", skip_serializing_if = "Option::is_none")]
263 pub max_contains: Option<u64>,
264
265 #[serde(rename = "patternProperties", skip_serializing_if = "Option::is_none")]
267 pub pattern_properties: Option<BTreeMap<String, Schema>>,
268 #[serde(rename = "propertyNames", skip_serializing_if = "Option::is_none")]
269 pub property_names: Option<Box<Schema>>,
270 #[serde(
271 rename = "unevaluatedProperties",
272 skip_serializing_if = "Option::is_none"
273 )]
274 pub unevaluated_properties: Option<AdditionalProperties>,
275 #[serde(rename = "unevaluatedItems", skip_serializing_if = "Option::is_none")]
276 pub unevaluated_items: Option<AdditionalProperties>,
277 #[serde(rename = "dependentRequired", skip_serializing_if = "Option::is_none")]
278 pub dependent_required: Option<BTreeMap<String, Vec<String>>>,
279 #[serde(rename = "dependentSchemas", skip_serializing_if = "Option::is_none")]
280 pub dependent_schemas: Option<BTreeMap<String, Schema>>,
281
282 #[serde(rename = "contentEncoding", skip_serializing_if = "Option::is_none")]
284 pub content_encoding: Option<String>,
285 #[serde(rename = "contentMediaType", skip_serializing_if = "Option::is_none")]
286 pub content_media_type: Option<String>,
287 #[serde(rename = "contentSchema", skip_serializing_if = "Option::is_none")]
288 pub content_schema: Option<Box<Schema>>,
289
290 #[serde(rename = "if", skip_serializing_if = "Option::is_none")]
292 pub if_schema: Option<Box<Schema>>,
293 #[serde(rename = "then", skip_serializing_if = "Option::is_none")]
294 pub then_schema: Option<Box<Schema>>,
295 #[serde(rename = "else", skip_serializing_if = "Option::is_none")]
296 pub else_schema: Option<Box<Schema>>,
297 #[serde(skip_serializing_if = "Option::is_none")]
298 pub not: Option<Box<Schema>>,
299
300 #[serde(skip_serializing_if = "Option::is_none")]
302 pub title: Option<String>,
303 #[serde(skip_serializing_if = "Option::is_none")]
304 pub deprecated: Option<bool>,
305 #[serde(rename = "readOnly", skip_serializing_if = "Option::is_none")]
306 pub read_only: Option<bool>,
307 #[serde(rename = "writeOnly", skip_serializing_if = "Option::is_none")]
308 pub write_only: Option<bool>,
309 #[serde(skip_serializing_if = "Option::is_none")]
310 pub examples: Option<Vec<Value>>,
311 #[serde(skip_serializing_if = "Option::is_none")]
312 pub example: Option<Value>,
313 #[serde(rename = "$comment", skip_serializing_if = "Option::is_none")]
315 pub comment: Option<String>,
316 #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
317 pub schema_keyword: Option<String>,
318 #[serde(rename = "$defs", skip_serializing_if = "Option::is_none")]
319 pub defs: Option<BTreeMap<String, Schema>>,
320
321 #[serde(flatten)]
324 pub extra: BTreeMap<String, Value>,
325}
326
327fn deserialize_present_value<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
328where
329 D: serde::Deserializer<'de>,
330{
331 Value::deserialize(deserializer).map(Some)
332}
333
334#[derive(Debug, Clone, Deserialize, Serialize)]
337#[serde(untagged)]
338pub enum ExclusiveBound {
339 Bool(bool),
340 Number(f64),
341}
342
343#[derive(Debug, Clone, Deserialize, Serialize)]
344#[serde(untagged)]
345pub enum AdditionalProperties {
346 Boolean(bool),
347 Schema(Box<Schema>),
348}
349
350#[derive(Debug, Clone, Deserialize, Serialize)]
352pub struct Example {
353 #[serde(default)]
354 pub summary: Option<String>,
355 #[serde(default)]
356 pub description: Option<String>,
357 #[serde(default)]
359 pub value: Option<Value>,
360 #[serde(rename = "externalValue", default)]
361 pub external_value: Option<String>,
362 #[serde(rename = "dataValue", default)]
364 pub data_value: Option<Value>,
365 #[serde(rename = "serializedValue", default)]
367 pub serialized_value: Option<String>,
368 #[serde(rename = "$ref", default)]
369 pub reference: Option<String>,
370 #[serde(flatten, default)]
371 pub extensions: Extensions,
372}
373
374#[derive(Debug, Clone, Deserialize, Serialize)]
376pub struct Link {
377 #[serde(rename = "operationRef", default)]
378 pub operation_ref: Option<String>,
379 #[serde(rename = "operationId", default)]
380 pub operation_id: Option<String>,
381 #[serde(default)]
382 pub parameters: Option<BTreeMap<String, Value>>,
383 #[serde(rename = "requestBody", default)]
384 pub request_body: Option<Value>,
385 #[serde(default)]
386 pub description: Option<String>,
387 #[serde(default)]
388 pub server: Option<Server>,
389 #[serde(rename = "$ref", default)]
390 pub reference: Option<String>,
391 #[serde(flatten, default)]
392 pub extensions: Extensions,
393}
394
395#[derive(Debug, Clone, Deserialize, Serialize)]
398#[serde(transparent)]
399pub struct Callback(pub BTreeMap<String, PathItem>);
400
401#[derive(Debug, Clone, Deserialize, Serialize)]
404pub struct Encoding {
405 #[serde(rename = "contentType", default)]
406 pub content_type: Option<String>,
407 #[serde(default)]
408 pub headers: Option<BTreeMap<String, Header>>,
409 #[serde(default)]
410 pub style: Option<String>,
411 #[serde(default)]
412 pub explode: Option<bool>,
413 #[serde(rename = "allowReserved", default)]
414 pub allow_reserved: Option<bool>,
415 #[serde(rename = "itemEncoding", default)]
417 pub item_encoding: Option<Box<Encoding>>,
418 #[serde(flatten, default)]
419 pub extensions: Extensions,
420}
421
422#[derive(Debug, Clone, Deserialize, Serialize)]
426pub struct Header {
427 #[serde(default)]
428 pub description: Option<String>,
429 #[serde(default)]
430 pub required: Option<bool>,
431 #[serde(default)]
432 pub deprecated: Option<bool>,
433 #[serde(rename = "allowEmptyValue", default)]
434 pub allow_empty_value: Option<bool>,
435 #[serde(default)]
436 pub style: Option<String>,
437 #[serde(default)]
438 pub explode: Option<bool>,
439 #[serde(rename = "allowReserved", default)]
440 pub allow_reserved: Option<bool>,
441 #[serde(default)]
442 pub schema: Option<Schema>,
443 #[serde(default)]
444 pub content: Option<BTreeMap<String, MediaType>>,
445 #[serde(default)]
446 pub example: Option<Value>,
447 #[serde(default)]
448 pub examples: Option<Value>,
449 #[serde(rename = "$ref", default)]
450 pub reference: Option<String>,
451 #[serde(flatten, default)]
452 pub extensions: Extensions,
453}
454
455#[derive(Debug, Clone, Deserialize, Serialize)]
459#[serde(tag = "type")]
460pub enum SecurityScheme {
461 #[serde(rename = "apiKey")]
462 ApiKey {
463 name: String,
464 #[serde(rename = "in")]
465 location: String, #[serde(default)]
467 description: Option<String>,
468 #[serde(default)]
470 deprecated: Option<bool>,
471 #[serde(flatten, default)]
472 extensions: Extensions,
473 },
474 #[serde(rename = "http")]
475 Http {
476 scheme: String, #[serde(rename = "bearerFormat", default)]
478 bearer_format: Option<String>,
479 #[serde(default)]
480 description: Option<String>,
481 #[serde(default)]
482 deprecated: Option<bool>,
483 #[serde(flatten, default)]
484 extensions: Extensions,
485 },
486 #[serde(rename = "mutualTLS")]
487 MutualTls {
488 #[serde(default)]
489 description: Option<String>,
490 #[serde(default)]
491 deprecated: Option<bool>,
492 #[serde(flatten, default)]
493 extensions: Extensions,
494 },
495 #[serde(rename = "oauth2")]
496 OAuth2 {
497 flows: Box<OAuthFlows>,
501 #[serde(default)]
502 description: Option<String>,
503 #[serde(rename = "oauth2MetadataUrl", default)]
505 oauth2_metadata_url: Option<String>,
506 #[serde(default)]
507 deprecated: Option<bool>,
508 #[serde(flatten, default)]
509 extensions: Extensions,
510 },
511 #[serde(rename = "openIdConnect")]
512 OpenIdConnect {
513 #[serde(rename = "openIdConnectUrl")]
514 open_id_connect_url: String,
515 #[serde(default)]
516 description: Option<String>,
517 #[serde(default)]
518 deprecated: Option<bool>,
519 #[serde(flatten, default)]
520 extensions: Extensions,
521 },
522}
523
524#[derive(Debug, Clone, Deserialize, Serialize)]
525pub struct OAuthFlows {
526 #[serde(default)]
527 pub implicit: Option<OAuthFlow>,
528 #[serde(default)]
529 pub password: Option<OAuthFlow>,
530 #[serde(rename = "clientCredentials", default)]
531 pub client_credentials: Option<OAuthFlow>,
532 #[serde(rename = "authorizationCode", default)]
533 pub authorization_code: Option<OAuthFlow>,
534 #[serde(rename = "deviceAuthorization", default)]
536 pub device_authorization: Option<OAuthFlow>,
537 #[serde(flatten, default)]
538 pub extensions: Extensions,
539}
540
541#[derive(Debug, Clone, Deserialize, Serialize)]
542pub struct OAuthFlow {
543 #[serde(rename = "authorizationUrl", default)]
544 pub authorization_url: Option<String>,
545 #[serde(rename = "tokenUrl", default)]
546 pub token_url: Option<String>,
547 #[serde(rename = "refreshUrl", default)]
548 pub refresh_url: Option<String>,
549 #[serde(rename = "deviceAuthorizationUrl", default)]
551 pub device_authorization_url: Option<String>,
552 pub scopes: BTreeMap<String, String>,
553 #[serde(flatten, default)]
554 pub extensions: Extensions,
555}
556
557#[derive(Debug, Clone, Deserialize, Serialize)]
559pub struct ExternalDocs {
560 pub url: String,
561 #[serde(default)]
562 pub description: Option<String>,
563 #[serde(flatten, default)]
564 pub extensions: Extensions,
565}
566
567#[derive(Debug, Clone, Deserialize, Serialize)]
569pub struct Tag {
570 pub name: String,
571 #[serde(default)]
573 pub summary: Option<String>,
574 #[serde(default)]
575 pub description: Option<String>,
576 #[serde(default)]
578 pub parent: Option<String>,
579 #[serde(default)]
583 pub kind: Option<String>,
584 #[serde(rename = "externalDocs", default)]
585 pub external_docs: Option<ExternalDocs>,
586 #[serde(flatten, default)]
587 pub extensions: Extensions,
588}
589
590#[derive(Debug, Clone, Deserialize, Serialize)]
593pub struct Server {
594 pub url: String,
595 #[serde(default)]
597 pub name: Option<String>,
598 #[serde(default)]
599 pub description: Option<String>,
600 #[serde(default)]
601 pub variables: Option<BTreeMap<String, ServerVariable>>,
602 #[serde(flatten, default)]
603 pub extensions: Extensions,
604}
605
606#[derive(Debug, Clone, Deserialize, Serialize)]
607pub struct ServerVariable {
608 #[serde(default)]
610 pub default: Option<String>,
611 #[serde(rename = "enum", default)]
612 pub enum_values: Option<Vec<String>>,
613 #[serde(default)]
614 pub description: Option<String>,
615 #[serde(flatten, default)]
616 pub extensions: Extensions,
617}
618
619#[derive(Debug, Clone, Deserialize, Serialize)]
620pub struct Discriminator {
621 #[serde(rename = "propertyName")]
622 pub property_name: String,
623 #[serde(default)]
624 pub mapping: Option<BTreeMap<String, String>>,
625 #[serde(rename = "defaultMapping", default)]
629 pub default_mapping: Option<String>,
630 #[serde(flatten, default)]
631 pub extensions: Extensions,
632}
633
634impl Schema {
635 pub fn schema_type(&self) -> Option<&SchemaType> {
639 match self {
640 Schema::Typed { schema_type, .. } => Some(schema_type),
641 Schema::TypedMulti { schema_types, .. } => schema_types
642 .iter()
643 .find(|t| **t != SchemaType::Null)
644 .or_else(|| schema_types.first()),
645 _ => None,
646 }
647 }
648
649 pub fn type_array_contains_null(&self) -> bool {
652 match self {
653 Schema::TypedMulti { schema_types, .. } => schema_types.contains(&SchemaType::Null),
654 _ => false,
655 }
656 }
657
658 pub fn details(&self) -> &SchemaDetails {
660 static EMPTY_DETAILS: Lazy<SchemaDetails> = Lazy::new(SchemaDetails::default);
661 match self {
662 Schema::Typed { details, .. } => details,
663 Schema::TypedMulti { details, .. } => details,
664 Schema::Reference { .. } | Schema::RecursiveRef { .. } | Schema::DynamicRef { .. } => {
665 &EMPTY_DETAILS
666 }
667 Schema::OneOf { details, .. } => details,
668 Schema::AnyOf { details, .. } => details,
669 Schema::AllOf { details, .. } => details,
670 Schema::Untyped { details } => details,
671 }
672 }
673
674 pub fn details_mut(&mut self) -> &mut SchemaDetails {
676 match self {
677 Schema::Typed { details, .. } => details,
678 Schema::TypedMulti { details, .. } => details,
679 Schema::Reference { .. } => {
680 panic!("Cannot get mutable details for reference schema")
681 }
682 Schema::RecursiveRef { .. } => {
683 panic!("Cannot get mutable details for recursive reference schema")
684 }
685 Schema::DynamicRef { .. } => {
686 panic!("Cannot get mutable details for dynamic reference schema")
687 }
688 Schema::OneOf { details, .. } => details,
689 Schema::AnyOf { details, .. } => details,
690 Schema::AllOf { details, .. } => details,
691 Schema::Untyped { details } => details,
692 }
693 }
694
695 pub fn is_reference(&self) -> bool {
697 matches!(self, Schema::Reference { .. } | Schema::RecursiveRef { .. })
698 }
699
700 pub fn reference(&self) -> Option<&str> {
702 match self {
703 Schema::Reference { reference, .. } => Some(reference),
704 _ => None,
705 }
706 }
707
708 pub fn recursive_reference(&self) -> Option<&str> {
710 match self {
711 Schema::RecursiveRef { recursive_ref, .. } => Some(recursive_ref),
712 _ => None,
713 }
714 }
715
716 pub fn is_discriminated_union(&self) -> bool {
718 match self {
719 Schema::OneOf { discriminator, .. } => discriminator.is_some(),
720 Schema::AnyOf { discriminator, .. } => discriminator.is_some(),
721 _ => false,
722 }
723 }
724
725 pub fn discriminator(&self) -> Option<&Discriminator> {
727 match self {
728 Schema::OneOf { discriminator, .. } => discriminator.as_ref(),
729 Schema::AnyOf { discriminator, .. } => discriminator.as_ref(),
730 _ => None,
731 }
732 }
733
734 pub fn union_variants(&self) -> Option<&[Schema]> {
736 match self {
737 Schema::OneOf { one_of, .. } => Some(one_of),
738 Schema::AnyOf { any_of, .. } => Some(any_of),
739 _ => None,
740 }
741 }
742
743 pub fn is_nullable_pattern(&self) -> bool {
745 let variants = match self {
746 Schema::AnyOf { any_of, .. } => any_of,
747 Schema::OneOf { one_of, .. } => one_of,
748 _ => return false,
749 };
750 variants.len() == 2
751 && variants
752 .iter()
753 .any(|s| matches!(s.schema_type(), Some(SchemaType::Null)))
754 }
755
756 pub fn non_null_variant(&self) -> Option<&Schema> {
758 if !self.is_nullable_pattern() {
759 return None;
760 }
761 let variants = match self {
762 Schema::AnyOf { any_of, .. } => any_of,
763 Schema::OneOf { one_of, .. } => one_of,
764 _ => return None,
765 };
766 variants
767 .iter()
768 .find(|s| !matches!(s.schema_type(), Some(SchemaType::Null)))
769 }
770
771 pub fn inferred_type(&self) -> Option<SchemaType> {
773 match self {
774 Schema::Typed { schema_type, .. } => Some(schema_type.clone()),
775 Schema::TypedMulti { .. } => self.schema_type().cloned(),
776 Schema::Untyped { details } => {
777 if details.properties.is_some() {
779 Some(SchemaType::Object)
780 } else if details.items.is_some() {
781 Some(SchemaType::Array)
782 } else if details.enum_values.is_some() {
783 Some(SchemaType::String) } else {
785 None
786 }
787 }
788 _ => None,
789 }
790 }
791}
792
793impl SchemaDetails {
794 pub fn is_nullable(&self) -> bool {
796 self.nullable.unwrap_or(false)
797 }
798
799 pub fn is_string_enum(&self) -> bool {
805 self.enum_values.is_some() || self.const_string_value().is_some()
806 }
807
808 pub fn string_enum_values(&self) -> Option<Vec<String>> {
814 if let Some(values) = self.enum_values.as_ref() {
815 return Some(
820 values
821 .iter()
822 .map(|v| match v {
823 Value::String(s) => s.clone(),
824 Value::Number(n) => n.to_string(),
825 Value::Bool(b) => b.to_string(),
826 Value::Null => "null".to_string(),
827 _ => v.to_string(),
828 })
829 .collect(),
830 );
831 }
832 self.const_string_value().map(|s| vec![s])
833 }
834
835 fn const_string_value(&self) -> Option<String> {
836 self.const_value
837 .as_ref()
838 .and_then(|v| v.as_str())
839 .map(|s| s.to_string())
840 }
841
842 pub fn is_field_required(&self, field_name: &str) -> bool {
844 self.required
845 .as_ref()
846 .map(|req| req.contains(&field_name.to_string()))
847 .unwrap_or(false)
848 }
849}
850
851#[derive(Debug, Clone, Deserialize, Serialize)]
853pub struct PathItem {
854 #[serde(default)]
855 pub summary: Option<String>,
856 #[serde(default)]
857 pub description: Option<String>,
858 pub get: Option<Operation>,
859 pub put: Option<Operation>,
860 pub post: Option<Operation>,
861 pub delete: Option<Operation>,
862 pub options: Option<Operation>,
863 pub head: Option<Operation>,
864 pub patch: Option<Operation>,
865 pub trace: Option<Operation>,
866 pub query: Option<Operation>,
869 #[serde(rename = "additionalOperations", default)]
873 pub additional_operations: Option<BTreeMap<String, Operation>>,
874 pub parameters: Option<Vec<Parameter>>,
875 #[serde(default)]
876 pub servers: Option<Vec<Server>>,
877 #[serde(rename = "$ref", default)]
878 pub reference: Option<String>,
879 #[serde(flatten, default)]
880 pub extensions: Extensions,
881}
882
883impl PathItem {
884 pub fn operations(&self) -> Vec<(&str, &Operation)> {
887 let mut ops = Vec::new();
888 if let Some(ref op) = self.get {
889 ops.push(("get", op));
890 }
891 if let Some(ref op) = self.put {
892 ops.push(("put", op));
893 }
894 if let Some(ref op) = self.post {
895 ops.push(("post", op));
896 }
897 if let Some(ref op) = self.delete {
898 ops.push(("delete", op));
899 }
900 if let Some(ref op) = self.options {
901 ops.push(("options", op));
902 }
903 if let Some(ref op) = self.head {
904 ops.push(("head", op));
905 }
906 if let Some(ref op) = self.patch {
907 ops.push(("patch", op));
908 }
909 if let Some(ref op) = self.trace {
910 ops.push(("trace", op));
911 }
912 if let Some(ref op) = self.query {
913 ops.push(("query", op));
914 }
915 if let Some(map) = &self.additional_operations {
916 for (verb, op) in map {
917 ops.push((verb.as_str(), op));
918 }
919 }
920 ops
921 }
922}
923
924#[derive(Debug, Clone, Deserialize, Serialize)]
926pub struct Operation {
927 #[serde(rename = "operationId", default)]
928 pub operation_id: Option<String>,
929 #[serde(default)]
930 pub summary: Option<String>,
931 #[serde(default)]
932 pub description: Option<String>,
933 #[serde(default)]
934 pub tags: Option<Vec<String>>,
935 #[serde(default)]
936 pub deprecated: Option<bool>,
937 pub parameters: Option<Vec<Parameter>>,
938 #[serde(rename = "requestBody")]
939 pub request_body: Option<RequestBody>,
940 pub responses: Option<BTreeMap<String, Response>>,
941 #[serde(default)]
942 pub callbacks: Option<BTreeMap<String, Callback>>,
943 #[serde(default)]
944 pub security: Option<Vec<BTreeMap<String, Vec<String>>>>,
945 #[serde(default)]
946 pub servers: Option<Vec<Server>>,
947 #[serde(rename = "externalDocs", default)]
948 pub external_docs: Option<ExternalDocs>,
949 #[serde(flatten, default)]
950 pub extensions: Extensions,
951}
952
953#[derive(Debug, Clone, Deserialize, Serialize)]
955pub struct Parameter {
956 #[serde(default)]
957 pub name: Option<String>,
958 #[serde(rename = "in", default)]
959 pub location: Option<String>,
960 #[serde(default)]
961 pub required: Option<bool>,
962 #[serde(default)]
963 pub deprecated: Option<bool>,
964 #[serde(rename = "allowEmptyValue", default)]
965 pub allow_empty_value: Option<bool>,
966 #[serde(default)]
967 pub style: Option<String>,
968 #[serde(default)]
969 pub explode: Option<bool>,
970 #[serde(rename = "allowReserved", default)]
971 pub allow_reserved: Option<bool>,
972 #[serde(default)]
973 pub schema: Option<Schema>,
974 #[serde(default)]
975 pub content: Option<BTreeMap<String, MediaType>>,
976 #[serde(default)]
977 pub example: Option<Value>,
978 #[serde(default)]
979 pub examples: Option<BTreeMap<String, Example>>,
980 #[serde(default)]
981 pub description: Option<String>,
982 #[serde(rename = "$ref", default)]
983 pub reference: Option<String>,
984 #[serde(flatten, default)]
985 pub extensions: Extensions,
986}
987
988#[derive(Debug, Clone, Deserialize, Serialize)]
990pub struct RequestBody {
991 pub content: Option<BTreeMap<String, MediaType>>,
992 #[serde(default)]
993 pub description: Option<String>,
994 #[serde(default)]
995 pub required: Option<bool>,
996 #[serde(rename = "$ref", default)]
997 pub reference: Option<String>,
998 #[serde(flatten, default)]
999 pub extensions: Extensions,
1000}
1001
1002pub fn is_json_media_type(ct: &str) -> bool {
1010 let essence = ct
1011 .split(';')
1012 .next()
1013 .unwrap_or(ct)
1014 .trim()
1015 .to_ascii_lowercase();
1016 if essence == "application/json" {
1017 return true;
1018 }
1019 if let Some(subtype) = essence.strip_prefix("application/") {
1020 return subtype.ends_with("+json");
1021 }
1022 false
1023}
1024
1025pub fn is_form_urlencoded_media_type(ct: &str) -> bool {
1028 let essence = ct
1029 .split(';')
1030 .next()
1031 .unwrap_or(ct)
1032 .trim()
1033 .to_ascii_lowercase();
1034 essence == "application/x-www-form-urlencoded"
1035}
1036
1037pub fn is_event_stream_media_type(ct: &str) -> bool {
1043 ct.split(';')
1044 .next()
1045 .unwrap_or(ct)
1046 .trim()
1047 .eq_ignore_ascii_case("text/event-stream")
1048}
1049
1050fn find_json_content(content: &BTreeMap<String, MediaType>) -> Option<(&str, &MediaType)> {
1051 if let Some(mt) = content.get("application/json") {
1052 return Some(("application/json", mt));
1053 }
1054 content
1055 .iter()
1056 .find(|(ct, _)| is_json_media_type(ct))
1057 .map(|(ct, mt)| (ct.as_str(), mt))
1058}
1059
1060impl RequestBody {
1061 pub fn json_schema(&self) -> Option<&Schema> {
1067 self.content
1068 .as_ref()
1069 .and_then(find_json_content)
1070 .and_then(|(_, media_type)| media_type.schema.as_ref())
1071 }
1072
1073 pub fn best_content(&self) -> Option<(&str, Option<&Schema>)> {
1075 let content = self.content.as_ref()?;
1076
1077 if let Some((ct, media_type)) = find_json_content(content) {
1078 return Some((ct, media_type.schema.as_ref()));
1079 }
1080
1081 const PRIORITY: &[&str] = &[
1082 "application/x-www-form-urlencoded",
1083 "multipart/form-data",
1084 "application/octet-stream",
1085 "text/plain",
1086 ];
1087 for ct in PRIORITY {
1088 if let Some(media_type) = content.get(*ct) {
1089 return Some((*ct, media_type.schema.as_ref()));
1090 }
1091 }
1092 None
1093 }
1094}
1095
1096#[derive(Debug, Clone, Deserialize, Serialize)]
1098pub struct Response {
1099 #[serde(default)]
1100 pub description: Option<String>,
1101 #[serde(default)]
1102 pub headers: Option<BTreeMap<String, Header>>,
1103 #[serde(default)]
1104 pub content: Option<BTreeMap<String, MediaType>>,
1105 #[serde(default)]
1106 pub links: Option<Value>,
1107 #[serde(rename = "$ref", default)]
1108 pub reference: Option<String>,
1109 #[serde(flatten, default)]
1110 pub extensions: Extensions,
1111}
1112
1113impl Response {
1114 pub fn json_schema(&self) -> Option<&Schema> {
1121 self.content
1122 .as_ref()
1123 .and_then(find_json_content)
1124 .and_then(|(_, media_type)| media_type.schema.as_ref())
1125 }
1126
1127 pub fn json_content(&self) -> Option<(&str, &Schema)> {
1129 self.content
1130 .as_ref()
1131 .and_then(find_json_content)
1132 .and_then(|(content_type, media_type)| {
1133 media_type
1134 .schema
1135 .as_ref()
1136 .map(|schema| (content_type, schema))
1137 })
1138 }
1139}
1140
1141#[derive(Debug, Clone, Deserialize, Serialize)]
1143pub struct MediaType {
1144 #[serde(default)]
1145 pub schema: Option<Schema>,
1146 #[serde(default)]
1147 pub example: Option<Value>,
1148 #[serde(default)]
1149 pub examples: Option<BTreeMap<String, Example>>,
1150 #[serde(default)]
1151 pub encoding: Option<BTreeMap<String, Encoding>>,
1152 #[serde(rename = "itemSchema", default)]
1155 pub item_schema: Option<Schema>,
1156 #[serde(rename = "prefixEncoding", default)]
1159 pub prefix_encoding: Option<Vec<Encoding>>,
1160 #[serde(rename = "itemEncoding", default)]
1163 pub item_encoding: Option<Encoding>,
1164 #[serde(rename = "$ref", default)]
1165 pub reference: Option<String>,
1166 #[serde(flatten, default)]
1167 pub extensions: Extensions,
1168}
1169
1170#[cfg(test)]
1171#[allow(clippy::unwrap_used, clippy::expect_used)]
1172mod tests {
1173 use super::*;
1174 use serde_json::json;
1175
1176 #[test]
1177 fn test_parse_simple_object_schema() {
1178 let schema_json = json!({
1179 "type": "object",
1180 "properties": {
1181 "name": {
1182 "type": "string",
1183 "description": "User name"
1184 },
1185 "age": {
1186 "type": "integer"
1187 }
1188 },
1189 "required": ["name"]
1190 });
1191
1192 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1193
1194 match schema {
1195 Schema::Typed {
1196 schema_type: SchemaType::Object,
1197 details,
1198 } => {
1199 assert!(details.properties.is_some());
1200 assert_eq!(details.required, Some(vec!["name".to_string()]));
1201 assert!(details.is_field_required("name"));
1202 assert!(!details.is_field_required("age"));
1203 }
1204 _ => panic!("Expected object schema"),
1205 }
1206 }
1207
1208 #[test]
1209 fn test_parse_string_enum() {
1210 let schema_json = json!({
1211 "type": "string",
1212 "enum": ["active", "inactive", "pending"],
1213 "description": "User status"
1214 });
1215
1216 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1217
1218 match schema {
1219 Schema::Typed {
1220 schema_type: SchemaType::String,
1221 details,
1222 } => {
1223 assert!(details.is_string_enum());
1224 let values = details.string_enum_values().unwrap();
1225 assert_eq!(values, vec!["active", "inactive", "pending"]);
1226 }
1227 _ => panic!("Expected string enum schema"),
1228 }
1229 }
1230
1231 #[test]
1232 fn test_parse_reference_schema() {
1233 let schema_json = json!({
1234 "$ref": "#/components/schemas/User"
1235 });
1236
1237 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1238
1239 assert!(schema.is_reference());
1240 assert_eq!(schema.reference(), Some("#/components/schemas/User"));
1241 }
1242
1243 #[test]
1244 fn test_parse_discriminated_union() {
1245 let schema_json = json!({
1246 "oneOf": [
1247 {"$ref": "#/components/schemas/Dog"},
1248 {"$ref": "#/components/schemas/Cat"}
1249 ],
1250 "discriminator": {
1251 "propertyName": "petType"
1252 }
1253 });
1254
1255 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1256
1257 assert!(schema.is_discriminated_union());
1258 let discriminator = schema.discriminator().unwrap();
1259 assert_eq!(discriminator.property_name, "petType");
1260 }
1261
1262 #[test]
1263 fn test_parse_nullable_pattern() {
1264 let schema_json = json!({
1265 "anyOf": [
1266 {"$ref": "#/components/schemas/User"},
1267 {"type": "null"}
1268 ]
1269 });
1270
1271 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1272
1273 assert!(schema.is_nullable_pattern());
1274 let non_null = schema.non_null_variant().unwrap();
1275 assert!(non_null.is_reference());
1276 }
1277
1278 #[test]
1279 fn is_json_media_type_accepts_canonical_and_structured_suffix() {
1280 assert!(is_json_media_type("application/json"));
1282 assert!(is_json_media_type("application/json; charset=utf-8"));
1284 assert!(is_json_media_type("APPLICATION/JSON"));
1285 assert!(is_json_media_type("application/vnd.api+json"));
1287 assert!(is_json_media_type("application/hal+json"));
1288 assert!(is_json_media_type("application/problem+json"));
1289 assert!(is_json_media_type("application/ld+json"));
1290 assert!(is_json_media_type(
1291 "application/vnd.api+json; charset=utf-8"
1292 ));
1293 assert!(!is_json_media_type("application/xml"));
1295 assert!(!is_json_media_type("application/x-www-form-urlencoded"));
1296 assert!(!is_json_media_type("text/plain"));
1297 assert!(!is_json_media_type("application/jsonbutnotreally"));
1298 assert!(!is_json_media_type("text/something+json"));
1300 }
1301
1302 #[test]
1303 fn request_body_json_schema_finds_vnd_api_plus_json() {
1304 let body_json = json!({
1307 "required": true,
1308 "content": {
1309 "application/vnd.api+json": {
1310 "schema": {"$ref": "#/components/schemas/create_api_key"}
1311 }
1312 }
1313 });
1314
1315 let body: RequestBody = serde_json::from_value(body_json).unwrap();
1316 let schema = body.json_schema().expect("expected +json schema match");
1317 assert!(schema.is_reference());
1318 }
1319
1320 #[test]
1321 fn request_body_best_content_prefers_canonical_json_over_plus_json() {
1322 let body_json = json!({
1326 "required": true,
1327 "content": {
1328 "application/json": {
1329 "schema": {"$ref": "#/components/schemas/A"}
1330 },
1331 "application/vnd.api+json": {
1332 "schema": {"$ref": "#/components/schemas/B"}
1333 }
1334 }
1335 });
1336
1337 let body: RequestBody = serde_json::from_value(body_json).unwrap();
1338 let (ct, _) = body.best_content().expect("expected best_content");
1339 assert_eq!(ct, "application/json");
1340 }
1341
1342 #[test]
1343 fn request_body_best_content_falls_back_to_plus_json() {
1344 let body_json = json!({
1347 "required": true,
1348 "content": {
1349 "application/vnd.api+json": {
1350 "schema": {"$ref": "#/components/schemas/B"}
1351 }
1352 }
1353 });
1354
1355 let body: RequestBody = serde_json::from_value(body_json).unwrap();
1356 let (ct, _) = body.best_content().expect("expected best_content");
1357 assert_eq!(ct, "application/vnd.api+json");
1358 }
1359
1360 #[test]
1361 fn response_json_schema_finds_vnd_api_plus_json() {
1362 let resp_json = json!({
1365 "description": "OK",
1366 "content": {
1367 "application/vnd.api+json": {
1368 "schema": {"$ref": "#/components/schemas/api_keys"}
1369 }
1370 }
1371 });
1372
1373 let resp: Response = serde_json::from_value(resp_json).unwrap();
1374 let schema = resp.json_schema().expect("expected +json schema match");
1375 assert!(schema.is_reference());
1376 }
1377}