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 is_nullable_any(&self) -> bool {
670 self.details().is_nullable()
671 || self.type_array_contains_null()
672 || self.is_nullable_pattern()
673 }
674
675 pub fn details(&self) -> &SchemaDetails {
677 static EMPTY_DETAILS: Lazy<SchemaDetails> = Lazy::new(SchemaDetails::default);
678 match self {
679 Schema::Typed { details, .. } => details,
680 Schema::TypedMulti { details, .. } => details,
681 Schema::Reference { .. } | Schema::RecursiveRef { .. } | Schema::DynamicRef { .. } => {
682 &EMPTY_DETAILS
683 }
684 Schema::OneOf { details, .. } => details,
685 Schema::AnyOf { details, .. } => details,
686 Schema::AllOf { details, .. } => details,
687 Schema::Untyped { details } => details,
688 }
689 }
690
691 pub fn details_mut(&mut self) -> &mut SchemaDetails {
693 match self {
694 Schema::Typed { details, .. } => details,
695 Schema::TypedMulti { details, .. } => details,
696 Schema::Reference { .. } => {
697 panic!("Cannot get mutable details for reference schema")
698 }
699 Schema::RecursiveRef { .. } => {
700 panic!("Cannot get mutable details for recursive reference schema")
701 }
702 Schema::DynamicRef { .. } => {
703 panic!("Cannot get mutable details for dynamic reference schema")
704 }
705 Schema::OneOf { details, .. } => details,
706 Schema::AnyOf { details, .. } => details,
707 Schema::AllOf { details, .. } => details,
708 Schema::Untyped { details } => details,
709 }
710 }
711
712 pub fn is_reference(&self) -> bool {
714 matches!(self, Schema::Reference { .. } | Schema::RecursiveRef { .. })
715 }
716
717 pub fn reference(&self) -> Option<&str> {
719 match self {
720 Schema::Reference { reference, .. } => Some(reference),
721 _ => None,
722 }
723 }
724
725 pub fn recursive_reference(&self) -> Option<&str> {
727 match self {
728 Schema::RecursiveRef { recursive_ref, .. } => Some(recursive_ref),
729 _ => None,
730 }
731 }
732
733 pub fn is_discriminated_union(&self) -> bool {
735 match self {
736 Schema::OneOf { discriminator, .. } => discriminator.is_some(),
737 Schema::AnyOf { discriminator, .. } => discriminator.is_some(),
738 _ => false,
739 }
740 }
741
742 pub fn discriminator(&self) -> Option<&Discriminator> {
744 match self {
745 Schema::OneOf { discriminator, .. } => discriminator.as_ref(),
746 Schema::AnyOf { discriminator, .. } => discriminator.as_ref(),
747 _ => None,
748 }
749 }
750
751 pub fn union_variants(&self) -> Option<&[Schema]> {
753 match self {
754 Schema::OneOf { one_of, .. } => Some(one_of),
755 Schema::AnyOf { any_of, .. } => Some(any_of),
756 _ => None,
757 }
758 }
759
760 pub fn is_nullable_pattern(&self) -> bool {
762 let variants = match self {
763 Schema::AnyOf { any_of, .. } => any_of,
764 Schema::OneOf { one_of, .. } => one_of,
765 _ => return false,
766 };
767 variants.len() == 2
768 && variants
769 .iter()
770 .any(|s| matches!(s.schema_type(), Some(SchemaType::Null)))
771 }
772
773 pub fn non_null_variant(&self) -> Option<&Schema> {
775 if !self.is_nullable_pattern() {
776 return None;
777 }
778 let variants = match self {
779 Schema::AnyOf { any_of, .. } => any_of,
780 Schema::OneOf { one_of, .. } => one_of,
781 _ => return None,
782 };
783 variants
784 .iter()
785 .find(|s| !matches!(s.schema_type(), Some(SchemaType::Null)))
786 }
787
788 pub fn inferred_type(&self) -> Option<SchemaType> {
790 match self {
791 Schema::Typed { schema_type, .. } => Some(schema_type.clone()),
792 Schema::TypedMulti { .. } => self.schema_type().cloned(),
793 Schema::Untyped { details } => {
794 if details.properties.is_some() {
796 Some(SchemaType::Object)
797 } else if details.items.is_some() {
798 Some(SchemaType::Array)
799 } else if details.enum_values.is_some() {
800 Some(SchemaType::String) } else {
802 None
803 }
804 }
805 _ => None,
806 }
807 }
808}
809
810impl SchemaDetails {
811 pub fn is_nullable(&self) -> bool {
813 self.nullable.unwrap_or(false)
814 }
815
816 pub fn is_string_enum(&self) -> bool {
822 self.enum_values.is_some() || self.const_string_value().is_some()
823 }
824
825 pub fn string_enum_values(&self) -> Option<Vec<String>> {
831 if let Some(values) = self.enum_values.as_ref() {
832 return Some(
837 values
838 .iter()
839 .map(|v| match v {
840 Value::String(s) => s.clone(),
841 Value::Number(n) => n.to_string(),
842 Value::Bool(b) => b.to_string(),
843 Value::Null => "null".to_string(),
844 _ => v.to_string(),
845 })
846 .collect(),
847 );
848 }
849 self.const_string_value().map(|s| vec![s])
850 }
851
852 fn const_string_value(&self) -> Option<String> {
853 self.const_value
854 .as_ref()
855 .and_then(|v| v.as_str())
856 .map(|s| s.to_string())
857 }
858
859 pub fn is_field_required(&self, field_name: &str) -> bool {
861 self.required
862 .as_ref()
863 .map(|req| req.contains(&field_name.to_string()))
864 .unwrap_or(false)
865 }
866}
867
868#[derive(Debug, Clone, Deserialize, Serialize)]
870pub struct PathItem {
871 #[serde(default)]
872 pub summary: Option<String>,
873 #[serde(default)]
874 pub description: Option<String>,
875 pub get: Option<Operation>,
876 pub put: Option<Operation>,
877 pub post: Option<Operation>,
878 pub delete: Option<Operation>,
879 pub options: Option<Operation>,
880 pub head: Option<Operation>,
881 pub patch: Option<Operation>,
882 pub trace: Option<Operation>,
883 pub query: Option<Operation>,
886 #[serde(rename = "additionalOperations", default)]
890 pub additional_operations: Option<BTreeMap<String, Operation>>,
891 pub parameters: Option<Vec<Parameter>>,
892 #[serde(default)]
893 pub servers: Option<Vec<Server>>,
894 #[serde(rename = "$ref", default)]
895 pub reference: Option<String>,
896 #[serde(flatten, default)]
897 pub extensions: Extensions,
898}
899
900impl PathItem {
901 pub fn operations(&self) -> Vec<(&str, &Operation)> {
904 let mut ops = Vec::new();
905 if let Some(ref op) = self.get {
906 ops.push(("get", op));
907 }
908 if let Some(ref op) = self.put {
909 ops.push(("put", op));
910 }
911 if let Some(ref op) = self.post {
912 ops.push(("post", op));
913 }
914 if let Some(ref op) = self.delete {
915 ops.push(("delete", op));
916 }
917 if let Some(ref op) = self.options {
918 ops.push(("options", op));
919 }
920 if let Some(ref op) = self.head {
921 ops.push(("head", op));
922 }
923 if let Some(ref op) = self.patch {
924 ops.push(("patch", op));
925 }
926 if let Some(ref op) = self.trace {
927 ops.push(("trace", op));
928 }
929 if let Some(ref op) = self.query {
930 ops.push(("query", op));
931 }
932 if let Some(map) = &self.additional_operations {
933 for (verb, op) in map {
934 ops.push((verb.as_str(), op));
935 }
936 }
937 ops
938 }
939}
940
941#[derive(Debug, Clone, Deserialize, Serialize)]
943pub struct Operation {
944 #[serde(rename = "operationId", default)]
945 pub operation_id: Option<String>,
946 #[serde(default)]
947 pub summary: Option<String>,
948 #[serde(default)]
949 pub description: Option<String>,
950 #[serde(default)]
951 pub tags: Option<Vec<String>>,
952 #[serde(default)]
953 pub deprecated: Option<bool>,
954 pub parameters: Option<Vec<Parameter>>,
955 #[serde(rename = "requestBody")]
956 pub request_body: Option<RequestBody>,
957 pub responses: Option<BTreeMap<String, Response>>,
958 #[serde(default)]
959 pub callbacks: Option<BTreeMap<String, Callback>>,
960 #[serde(default)]
961 pub security: Option<Vec<BTreeMap<String, Vec<String>>>>,
962 #[serde(default)]
963 pub servers: Option<Vec<Server>>,
964 #[serde(rename = "externalDocs", default)]
965 pub external_docs: Option<ExternalDocs>,
966 #[serde(flatten, default)]
967 pub extensions: Extensions,
968}
969
970#[derive(Debug, Clone, Deserialize, Serialize)]
972pub struct Parameter {
973 #[serde(default)]
974 pub name: Option<String>,
975 #[serde(rename = "in", default)]
976 pub location: Option<String>,
977 #[serde(default)]
978 pub required: Option<bool>,
979 #[serde(default)]
980 pub deprecated: Option<bool>,
981 #[serde(rename = "allowEmptyValue", default)]
982 pub allow_empty_value: Option<bool>,
983 #[serde(default)]
984 pub style: Option<String>,
985 #[serde(default)]
986 pub explode: Option<bool>,
987 #[serde(rename = "allowReserved", default)]
988 pub allow_reserved: Option<bool>,
989 #[serde(default)]
990 pub schema: Option<Schema>,
991 #[serde(default)]
992 pub content: Option<BTreeMap<String, MediaType>>,
993 #[serde(default)]
994 pub example: Option<Value>,
995 #[serde(default)]
996 pub examples: Option<BTreeMap<String, Example>>,
997 #[serde(default)]
998 pub description: Option<String>,
999 #[serde(rename = "$ref", default)]
1000 pub reference: Option<String>,
1001 #[serde(flatten, default)]
1002 pub extensions: Extensions,
1003}
1004
1005#[derive(Debug, Clone, Deserialize, Serialize)]
1007pub struct RequestBody {
1008 pub content: Option<BTreeMap<String, MediaType>>,
1009 #[serde(default)]
1010 pub description: Option<String>,
1011 #[serde(default)]
1012 pub required: Option<bool>,
1013 #[serde(rename = "$ref", default)]
1014 pub reference: Option<String>,
1015 #[serde(flatten, default)]
1016 pub extensions: Extensions,
1017}
1018
1019pub fn is_json_media_type(ct: &str) -> bool {
1027 let essence = ct
1028 .split(';')
1029 .next()
1030 .unwrap_or(ct)
1031 .trim()
1032 .to_ascii_lowercase();
1033 if essence == "application/json" {
1034 return true;
1035 }
1036 if let Some(subtype) = essence.strip_prefix("application/") {
1037 return subtype.ends_with("+json");
1038 }
1039 false
1040}
1041
1042pub fn is_form_urlencoded_media_type(ct: &str) -> bool {
1045 let essence = ct
1046 .split(';')
1047 .next()
1048 .unwrap_or(ct)
1049 .trim()
1050 .to_ascii_lowercase();
1051 essence == "application/x-www-form-urlencoded"
1052}
1053
1054pub fn is_event_stream_media_type(ct: &str) -> bool {
1060 ct.split(';')
1061 .next()
1062 .unwrap_or(ct)
1063 .trim()
1064 .eq_ignore_ascii_case("text/event-stream")
1065}
1066
1067fn find_json_content(content: &BTreeMap<String, MediaType>) -> Option<(&str, &MediaType)> {
1068 if let Some(mt) = content.get("application/json") {
1069 return Some(("application/json", mt));
1070 }
1071 content
1072 .iter()
1073 .find(|(ct, _)| is_json_media_type(ct))
1074 .map(|(ct, mt)| (ct.as_str(), mt))
1075}
1076
1077impl RequestBody {
1078 pub fn json_schema(&self) -> Option<&Schema> {
1084 self.content
1085 .as_ref()
1086 .and_then(find_json_content)
1087 .and_then(|(_, media_type)| media_type.schema.as_ref())
1088 }
1089
1090 pub fn best_content(&self) -> Option<(&str, Option<&Schema>)> {
1092 let content = self.content.as_ref()?;
1093
1094 if let Some((ct, media_type)) = find_json_content(content) {
1095 return Some((ct, media_type.schema.as_ref()));
1096 }
1097
1098 const PRIORITY: &[&str] = &[
1099 "application/x-www-form-urlencoded",
1100 "multipart/form-data",
1101 "application/octet-stream",
1102 "text/plain",
1103 ];
1104 for ct in PRIORITY {
1105 if let Some(media_type) = content.get(*ct) {
1106 return Some((*ct, media_type.schema.as_ref()));
1107 }
1108 }
1109 None
1110 }
1111}
1112
1113#[derive(Debug, Clone, Deserialize, Serialize)]
1115pub struct Response {
1116 #[serde(default)]
1117 pub description: Option<String>,
1118 #[serde(default)]
1119 pub headers: Option<BTreeMap<String, Header>>,
1120 #[serde(default)]
1121 pub content: Option<BTreeMap<String, MediaType>>,
1122 #[serde(default)]
1123 pub links: Option<Value>,
1124 #[serde(rename = "$ref", default)]
1125 pub reference: Option<String>,
1126 #[serde(flatten, default)]
1127 pub extensions: Extensions,
1128}
1129
1130impl Response {
1131 pub fn json_schema(&self) -> Option<&Schema> {
1138 self.content
1139 .as_ref()
1140 .and_then(find_json_content)
1141 .and_then(|(_, media_type)| media_type.schema.as_ref())
1142 }
1143
1144 pub fn json_content(&self) -> Option<(&str, &Schema)> {
1146 self.content
1147 .as_ref()
1148 .and_then(find_json_content)
1149 .and_then(|(content_type, media_type)| {
1150 media_type
1151 .schema
1152 .as_ref()
1153 .map(|schema| (content_type, schema))
1154 })
1155 }
1156}
1157
1158#[derive(Debug, Clone, Deserialize, Serialize)]
1160pub struct MediaType {
1161 #[serde(default)]
1162 pub schema: Option<Schema>,
1163 #[serde(default)]
1164 pub example: Option<Value>,
1165 #[serde(default)]
1166 pub examples: Option<BTreeMap<String, Example>>,
1167 #[serde(default)]
1168 pub encoding: Option<BTreeMap<String, Encoding>>,
1169 #[serde(rename = "itemSchema", default)]
1172 pub item_schema: Option<Schema>,
1173 #[serde(rename = "prefixEncoding", default)]
1176 pub prefix_encoding: Option<Vec<Encoding>>,
1177 #[serde(rename = "itemEncoding", default)]
1180 pub item_encoding: Option<Encoding>,
1181 #[serde(rename = "$ref", default)]
1182 pub reference: Option<String>,
1183 #[serde(flatten, default)]
1184 pub extensions: Extensions,
1185}
1186
1187#[cfg(test)]
1188#[allow(clippy::unwrap_used, clippy::expect_used)]
1189mod tests {
1190 use super::*;
1191 use serde_json::json;
1192
1193 #[test]
1194 fn test_parse_simple_object_schema() {
1195 let schema_json = json!({
1196 "type": "object",
1197 "properties": {
1198 "name": {
1199 "type": "string",
1200 "description": "User name"
1201 },
1202 "age": {
1203 "type": "integer"
1204 }
1205 },
1206 "required": ["name"]
1207 });
1208
1209 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1210
1211 match schema {
1212 Schema::Typed {
1213 schema_type: SchemaType::Object,
1214 details,
1215 } => {
1216 assert!(details.properties.is_some());
1217 assert_eq!(details.required, Some(vec!["name".to_string()]));
1218 assert!(details.is_field_required("name"));
1219 assert!(!details.is_field_required("age"));
1220 }
1221 _ => panic!("Expected object schema"),
1222 }
1223 }
1224
1225 #[test]
1226 fn test_parse_string_enum() {
1227 let schema_json = json!({
1228 "type": "string",
1229 "enum": ["active", "inactive", "pending"],
1230 "description": "User status"
1231 });
1232
1233 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1234
1235 match schema {
1236 Schema::Typed {
1237 schema_type: SchemaType::String,
1238 details,
1239 } => {
1240 assert!(details.is_string_enum());
1241 let values = details.string_enum_values().unwrap();
1242 assert_eq!(values, vec!["active", "inactive", "pending"]);
1243 }
1244 _ => panic!("Expected string enum schema"),
1245 }
1246 }
1247
1248 #[test]
1249 fn test_parse_reference_schema() {
1250 let schema_json = json!({
1251 "$ref": "#/components/schemas/User"
1252 });
1253
1254 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1255
1256 assert!(schema.is_reference());
1257 assert_eq!(schema.reference(), Some("#/components/schemas/User"));
1258 }
1259
1260 #[test]
1261 fn test_parse_discriminated_union() {
1262 let schema_json = json!({
1263 "oneOf": [
1264 {"$ref": "#/components/schemas/Dog"},
1265 {"$ref": "#/components/schemas/Cat"}
1266 ],
1267 "discriminator": {
1268 "propertyName": "petType"
1269 }
1270 });
1271
1272 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1273
1274 assert!(schema.is_discriminated_union());
1275 let discriminator = schema.discriminator().unwrap();
1276 assert_eq!(discriminator.property_name, "petType");
1277 }
1278
1279 #[test]
1280 fn test_parse_nullable_pattern() {
1281 let schema_json = json!({
1282 "anyOf": [
1283 {"$ref": "#/components/schemas/User"},
1284 {"type": "null"}
1285 ]
1286 });
1287
1288 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1289
1290 assert!(schema.is_nullable_pattern());
1291 let non_null = schema.non_null_variant().unwrap();
1292 assert!(non_null.is_reference());
1293 }
1294
1295 #[test]
1296 fn is_json_media_type_accepts_canonical_and_structured_suffix() {
1297 assert!(is_json_media_type("application/json"));
1299 assert!(is_json_media_type("application/json; charset=utf-8"));
1301 assert!(is_json_media_type("APPLICATION/JSON"));
1302 assert!(is_json_media_type("application/vnd.api+json"));
1304 assert!(is_json_media_type("application/hal+json"));
1305 assert!(is_json_media_type("application/problem+json"));
1306 assert!(is_json_media_type("application/ld+json"));
1307 assert!(is_json_media_type(
1308 "application/vnd.api+json; charset=utf-8"
1309 ));
1310 assert!(!is_json_media_type("application/xml"));
1312 assert!(!is_json_media_type("application/x-www-form-urlencoded"));
1313 assert!(!is_json_media_type("text/plain"));
1314 assert!(!is_json_media_type("application/jsonbutnotreally"));
1315 assert!(!is_json_media_type("text/something+json"));
1317 }
1318
1319 #[test]
1320 fn request_body_json_schema_finds_vnd_api_plus_json() {
1321 let body_json = json!({
1324 "required": true,
1325 "content": {
1326 "application/vnd.api+json": {
1327 "schema": {"$ref": "#/components/schemas/create_api_key"}
1328 }
1329 }
1330 });
1331
1332 let body: RequestBody = serde_json::from_value(body_json).unwrap();
1333 let schema = body.json_schema().expect("expected +json schema match");
1334 assert!(schema.is_reference());
1335 }
1336
1337 #[test]
1338 fn request_body_best_content_prefers_canonical_json_over_plus_json() {
1339 let body_json = json!({
1343 "required": true,
1344 "content": {
1345 "application/json": {
1346 "schema": {"$ref": "#/components/schemas/A"}
1347 },
1348 "application/vnd.api+json": {
1349 "schema": {"$ref": "#/components/schemas/B"}
1350 }
1351 }
1352 });
1353
1354 let body: RequestBody = serde_json::from_value(body_json).unwrap();
1355 let (ct, _) = body.best_content().expect("expected best_content");
1356 assert_eq!(ct, "application/json");
1357 }
1358
1359 #[test]
1360 fn request_body_best_content_falls_back_to_plus_json() {
1361 let body_json = json!({
1364 "required": true,
1365 "content": {
1366 "application/vnd.api+json": {
1367 "schema": {"$ref": "#/components/schemas/B"}
1368 }
1369 }
1370 });
1371
1372 let body: RequestBody = serde_json::from_value(body_json).unwrap();
1373 let (ct, _) = body.best_content().expect("expected best_content");
1374 assert_eq!(ct, "application/vnd.api+json");
1375 }
1376
1377 #[test]
1378 fn response_json_schema_finds_vnd_api_plus_json() {
1379 let resp_json = json!({
1382 "description": "OK",
1383 "content": {
1384 "application/vnd.api+json": {
1385 "schema": {"$ref": "#/components/schemas/api_keys"}
1386 }
1387 }
1388 });
1389
1390 let resp: Response = serde_json::from_value(resp_json).unwrap();
1391 let schema = resp.json_schema().expect("expected +json schema match");
1392 assert!(schema.is_reference());
1393 }
1394}