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 discriminator: Option<Discriminator>,
116 #[serde(flatten)]
117 details: SchemaDetails,
118 },
119 AnyOf {
121 #[serde(rename = "type")]
122 schema_type: Option<SchemaType>,
123 #[serde(rename = "anyOf")]
124 any_of: Vec<Schema>,
125 discriminator: Option<Discriminator>,
126 #[serde(flatten)]
127 details: SchemaDetails,
128 },
129 TypedMulti {
134 #[serde(rename = "type")]
135 schema_types: Vec<SchemaType>,
136 #[serde(flatten)]
137 details: SchemaDetails,
138 },
139 Typed {
141 #[serde(rename = "type")]
142 schema_type: SchemaType,
143 #[serde(flatten)]
144 details: SchemaDetails,
145 },
146 AllOf {
148 #[serde(rename = "allOf")]
149 all_of: Vec<Schema>,
150 #[serde(flatten)]
151 details: SchemaDetails,
152 },
153 Untyped {
155 #[serde(flatten)]
156 details: SchemaDetails,
157 },
158}
159
160#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
161#[serde(rename_all = "lowercase")]
162pub enum SchemaType {
163 String,
164 Integer,
165 Number,
166 Boolean,
167 Array,
168 Object,
169 #[serde(rename = "null")]
170 Null,
171}
172
173#[derive(Debug, Clone, Default, Deserialize, Serialize)]
174pub struct SchemaDetails {
175 pub description: Option<String>,
176 pub nullable: Option<bool>,
177
178 #[serde(rename = "$recursiveAnchor")]
180 pub recursive_anchor: Option<bool>,
181
182 #[serde(rename = "$dynamicAnchor")]
184 pub dynamic_anchor: Option<String>,
185 #[serde(rename = "$id")]
186 pub schema_id: Option<String>,
187
188 #[serde(rename = "enum")]
190 pub enum_values: Option<Vec<Value>>,
191 pub format: Option<String>,
192 pub default: Option<Value>,
193 #[serde(rename = "const")]
194 pub const_value: Option<Value>,
195
196 pub properties: Option<BTreeMap<String, Schema>>,
198 pub required: Option<Vec<String>>,
199 #[serde(rename = "additionalProperties")]
200 pub additional_properties: Option<AdditionalProperties>,
201
202 pub items: Option<Box<Schema>>,
204
205 pub minimum: Option<f64>,
207 pub maximum: Option<f64>,
208
209 #[serde(rename = "minLength")]
211 pub min_length: Option<u64>,
212 #[serde(rename = "maxLength")]
213 pub max_length: Option<u64>,
214 pub pattern: Option<String>,
215 #[serde(rename = "exclusiveMinimum")]
219 pub exclusive_minimum: Option<ExclusiveBound>,
220 #[serde(rename = "exclusiveMaximum")]
221 pub exclusive_maximum: Option<ExclusiveBound>,
222 #[serde(rename = "multipleOf")]
223 pub multiple_of: Option<f64>,
224 #[serde(rename = "minItems")]
225 pub min_items: Option<u64>,
226 #[serde(rename = "maxItems")]
227 pub max_items: Option<u64>,
228 #[serde(rename = "uniqueItems")]
229 pub unique_items: Option<bool>,
230 #[serde(rename = "minProperties")]
231 pub min_properties: Option<u64>,
232 #[serde(rename = "maxProperties")]
233 pub max_properties: Option<u64>,
234
235 #[serde(rename = "prefixItems")]
237 pub prefix_items: Option<Vec<Schema>>,
238 pub contains: Option<Box<Schema>>,
239 #[serde(rename = "minContains")]
240 pub min_contains: Option<u64>,
241 #[serde(rename = "maxContains")]
242 pub max_contains: Option<u64>,
243
244 #[serde(rename = "patternProperties")]
246 pub pattern_properties: Option<BTreeMap<String, Schema>>,
247 #[serde(rename = "propertyNames")]
248 pub property_names: Option<Box<Schema>>,
249 #[serde(rename = "unevaluatedProperties")]
250 pub unevaluated_properties: Option<AdditionalProperties>,
251 #[serde(rename = "unevaluatedItems")]
252 pub unevaluated_items: Option<AdditionalProperties>,
253 #[serde(rename = "dependentRequired")]
254 pub dependent_required: Option<BTreeMap<String, Vec<String>>>,
255 #[serde(rename = "dependentSchemas")]
256 pub dependent_schemas: Option<BTreeMap<String, Schema>>,
257
258 #[serde(rename = "contentEncoding")]
260 pub content_encoding: Option<String>,
261 #[serde(rename = "contentMediaType")]
262 pub content_media_type: Option<String>,
263 #[serde(rename = "contentSchema")]
264 pub content_schema: Option<Box<Schema>>,
265
266 #[serde(rename = "if")]
268 pub if_schema: Option<Box<Schema>>,
269 #[serde(rename = "then")]
270 pub then_schema: Option<Box<Schema>>,
271 #[serde(rename = "else")]
272 pub else_schema: Option<Box<Schema>>,
273 pub not: Option<Box<Schema>>,
274
275 pub title: Option<String>,
277 pub deprecated: Option<bool>,
278 #[serde(rename = "readOnly")]
279 pub read_only: Option<bool>,
280 #[serde(rename = "writeOnly")]
281 pub write_only: Option<bool>,
282 pub examples: Option<Vec<Value>>,
283 pub example: Option<Value>,
284 #[serde(rename = "$comment")]
286 pub comment: Option<String>,
287 #[serde(rename = "$schema")]
288 pub schema_keyword: Option<String>,
289 #[serde(rename = "$defs")]
290 pub defs: Option<BTreeMap<String, Schema>>,
291
292 #[serde(flatten)]
295 pub extra: BTreeMap<String, Value>,
296}
297
298#[derive(Debug, Clone, Deserialize, Serialize)]
301#[serde(untagged)]
302pub enum ExclusiveBound {
303 Bool(bool),
304 Number(f64),
305}
306
307#[derive(Debug, Clone, Deserialize, Serialize)]
308#[serde(untagged)]
309pub enum AdditionalProperties {
310 Boolean(bool),
311 Schema(Box<Schema>),
312}
313
314#[derive(Debug, Clone, Deserialize, Serialize)]
316pub struct Example {
317 #[serde(default)]
318 pub summary: Option<String>,
319 #[serde(default)]
320 pub description: Option<String>,
321 #[serde(default)]
323 pub value: Option<Value>,
324 #[serde(rename = "externalValue", default)]
325 pub external_value: Option<String>,
326 #[serde(rename = "dataValue", default)]
328 pub data_value: Option<Value>,
329 #[serde(rename = "serializedValue", default)]
331 pub serialized_value: Option<String>,
332 #[serde(rename = "$ref", default)]
333 pub reference: Option<String>,
334 #[serde(flatten, default)]
335 pub extensions: Extensions,
336}
337
338#[derive(Debug, Clone, Deserialize, Serialize)]
340pub struct Link {
341 #[serde(rename = "operationRef", default)]
342 pub operation_ref: Option<String>,
343 #[serde(rename = "operationId", default)]
344 pub operation_id: Option<String>,
345 #[serde(default)]
346 pub parameters: Option<BTreeMap<String, Value>>,
347 #[serde(rename = "requestBody", default)]
348 pub request_body: Option<Value>,
349 #[serde(default)]
350 pub description: Option<String>,
351 #[serde(default)]
352 pub server: Option<Server>,
353 #[serde(rename = "$ref", default)]
354 pub reference: Option<String>,
355 #[serde(flatten, default)]
356 pub extensions: Extensions,
357}
358
359#[derive(Debug, Clone, Deserialize, Serialize)]
362#[serde(transparent)]
363pub struct Callback(pub BTreeMap<String, PathItem>);
364
365#[derive(Debug, Clone, Deserialize, Serialize)]
368pub struct Encoding {
369 #[serde(rename = "contentType", default)]
370 pub content_type: Option<String>,
371 #[serde(default)]
372 pub headers: Option<BTreeMap<String, Header>>,
373 #[serde(default)]
374 pub style: Option<String>,
375 #[serde(default)]
376 pub explode: Option<bool>,
377 #[serde(rename = "allowReserved", default)]
378 pub allow_reserved: Option<bool>,
379 #[serde(rename = "itemEncoding", default)]
381 pub item_encoding: Option<Box<Encoding>>,
382 #[serde(flatten, default)]
383 pub extensions: Extensions,
384}
385
386#[derive(Debug, Clone, Deserialize, Serialize)]
390pub struct Header {
391 #[serde(default)]
392 pub description: Option<String>,
393 #[serde(default)]
394 pub required: Option<bool>,
395 #[serde(default)]
396 pub deprecated: Option<bool>,
397 #[serde(rename = "allowEmptyValue", default)]
398 pub allow_empty_value: Option<bool>,
399 #[serde(default)]
400 pub style: Option<String>,
401 #[serde(default)]
402 pub explode: Option<bool>,
403 #[serde(rename = "allowReserved", default)]
404 pub allow_reserved: Option<bool>,
405 #[serde(default)]
406 pub schema: Option<Schema>,
407 #[serde(default)]
408 pub content: Option<BTreeMap<String, MediaType>>,
409 #[serde(default)]
410 pub example: Option<Value>,
411 #[serde(default)]
412 pub examples: Option<Value>,
413 #[serde(rename = "$ref", default)]
414 pub reference: Option<String>,
415 #[serde(flatten, default)]
416 pub extensions: Extensions,
417}
418
419#[derive(Debug, Clone, Deserialize, Serialize)]
423#[serde(tag = "type")]
424pub enum SecurityScheme {
425 #[serde(rename = "apiKey")]
426 ApiKey {
427 name: String,
428 #[serde(rename = "in")]
429 location: String, #[serde(default)]
431 description: Option<String>,
432 #[serde(default)]
434 deprecated: Option<bool>,
435 #[serde(flatten, default)]
436 extensions: Extensions,
437 },
438 #[serde(rename = "http")]
439 Http {
440 scheme: String, #[serde(rename = "bearerFormat", default)]
442 bearer_format: Option<String>,
443 #[serde(default)]
444 description: Option<String>,
445 #[serde(default)]
446 deprecated: Option<bool>,
447 #[serde(flatten, default)]
448 extensions: Extensions,
449 },
450 #[serde(rename = "mutualTLS")]
451 MutualTls {
452 #[serde(default)]
453 description: Option<String>,
454 #[serde(default)]
455 deprecated: Option<bool>,
456 #[serde(flatten, default)]
457 extensions: Extensions,
458 },
459 #[serde(rename = "oauth2")]
460 OAuth2 {
461 flows: Box<OAuthFlows>,
465 #[serde(default)]
466 description: Option<String>,
467 #[serde(rename = "oauth2MetadataUrl", default)]
469 oauth2_metadata_url: Option<String>,
470 #[serde(default)]
471 deprecated: Option<bool>,
472 #[serde(flatten, default)]
473 extensions: Extensions,
474 },
475 #[serde(rename = "openIdConnect")]
476 OpenIdConnect {
477 #[serde(rename = "openIdConnectUrl")]
478 open_id_connect_url: String,
479 #[serde(default)]
480 description: Option<String>,
481 #[serde(default)]
482 deprecated: Option<bool>,
483 #[serde(flatten, default)]
484 extensions: Extensions,
485 },
486}
487
488#[derive(Debug, Clone, Deserialize, Serialize)]
489pub struct OAuthFlows {
490 #[serde(default)]
491 pub implicit: Option<OAuthFlow>,
492 #[serde(default)]
493 pub password: Option<OAuthFlow>,
494 #[serde(rename = "clientCredentials", default)]
495 pub client_credentials: Option<OAuthFlow>,
496 #[serde(rename = "authorizationCode", default)]
497 pub authorization_code: Option<OAuthFlow>,
498 #[serde(rename = "deviceAuthorization", default)]
500 pub device_authorization: Option<OAuthFlow>,
501 #[serde(flatten, default)]
502 pub extensions: Extensions,
503}
504
505#[derive(Debug, Clone, Deserialize, Serialize)]
506pub struct OAuthFlow {
507 #[serde(rename = "authorizationUrl", default)]
508 pub authorization_url: Option<String>,
509 #[serde(rename = "tokenUrl", default)]
510 pub token_url: Option<String>,
511 #[serde(rename = "refreshUrl", default)]
512 pub refresh_url: Option<String>,
513 #[serde(rename = "deviceAuthorizationUrl", default)]
515 pub device_authorization_url: Option<String>,
516 pub scopes: BTreeMap<String, String>,
517 #[serde(flatten, default)]
518 pub extensions: Extensions,
519}
520
521#[derive(Debug, Clone, Deserialize, Serialize)]
523pub struct ExternalDocs {
524 pub url: String,
525 #[serde(default)]
526 pub description: Option<String>,
527 #[serde(flatten, default)]
528 pub extensions: Extensions,
529}
530
531#[derive(Debug, Clone, Deserialize, Serialize)]
533pub struct Tag {
534 pub name: String,
535 #[serde(default)]
537 pub summary: Option<String>,
538 #[serde(default)]
539 pub description: Option<String>,
540 #[serde(default)]
542 pub parent: Option<String>,
543 #[serde(default)]
547 pub kind: Option<String>,
548 #[serde(rename = "externalDocs", default)]
549 pub external_docs: Option<ExternalDocs>,
550 #[serde(flatten, default)]
551 pub extensions: Extensions,
552}
553
554#[derive(Debug, Clone, Deserialize, Serialize)]
557pub struct Server {
558 pub url: String,
559 #[serde(default)]
561 pub name: Option<String>,
562 #[serde(default)]
563 pub description: Option<String>,
564 #[serde(default)]
565 pub variables: Option<BTreeMap<String, ServerVariable>>,
566 #[serde(flatten, default)]
567 pub extensions: Extensions,
568}
569
570#[derive(Debug, Clone, Deserialize, Serialize)]
571pub struct ServerVariable {
572 #[serde(default)]
574 pub default: Option<String>,
575 #[serde(rename = "enum", default)]
576 pub enum_values: Option<Vec<String>>,
577 #[serde(default)]
578 pub description: Option<String>,
579 #[serde(flatten, default)]
580 pub extensions: Extensions,
581}
582
583#[derive(Debug, Clone, Deserialize, Serialize)]
584pub struct Discriminator {
585 #[serde(rename = "propertyName")]
586 pub property_name: String,
587 #[serde(default)]
588 pub mapping: Option<BTreeMap<String, String>>,
589 #[serde(rename = "defaultMapping", default)]
593 pub default_mapping: Option<String>,
594 #[serde(flatten, default)]
595 pub extensions: Extensions,
596}
597
598impl Schema {
599 pub fn schema_type(&self) -> Option<&SchemaType> {
603 match self {
604 Schema::Typed { schema_type, .. } => Some(schema_type),
605 Schema::TypedMulti { schema_types, .. } => schema_types
606 .iter()
607 .find(|t| **t != SchemaType::Null)
608 .or_else(|| schema_types.first()),
609 _ => None,
610 }
611 }
612
613 pub fn type_array_contains_null(&self) -> bool {
616 match self {
617 Schema::TypedMulti { schema_types, .. } => schema_types.contains(&SchemaType::Null),
618 _ => false,
619 }
620 }
621
622 pub fn details(&self) -> &SchemaDetails {
624 static EMPTY_DETAILS: Lazy<SchemaDetails> = Lazy::new(SchemaDetails::default);
625 match self {
626 Schema::Typed { details, .. } => details,
627 Schema::TypedMulti { details, .. } => details,
628 Schema::Reference { .. } | Schema::RecursiveRef { .. } | Schema::DynamicRef { .. } => {
629 &EMPTY_DETAILS
630 }
631 Schema::OneOf { details, .. } => details,
632 Schema::AnyOf { details, .. } => details,
633 Schema::AllOf { details, .. } => details,
634 Schema::Untyped { details } => details,
635 }
636 }
637
638 pub fn details_mut(&mut self) -> &mut SchemaDetails {
640 match self {
641 Schema::Typed { details, .. } => details,
642 Schema::TypedMulti { details, .. } => details,
643 Schema::Reference { .. } => {
644 panic!("Cannot get mutable details for reference schema")
645 }
646 Schema::RecursiveRef { .. } => {
647 panic!("Cannot get mutable details for recursive reference schema")
648 }
649 Schema::DynamicRef { .. } => {
650 panic!("Cannot get mutable details for dynamic reference schema")
651 }
652 Schema::OneOf { details, .. } => details,
653 Schema::AnyOf { details, .. } => details,
654 Schema::AllOf { details, .. } => details,
655 Schema::Untyped { details } => details,
656 }
657 }
658
659 pub fn is_reference(&self) -> bool {
661 matches!(self, Schema::Reference { .. } | Schema::RecursiveRef { .. })
662 }
663
664 pub fn reference(&self) -> Option<&str> {
666 match self {
667 Schema::Reference { reference, .. } => Some(reference),
668 _ => None,
669 }
670 }
671
672 pub fn recursive_reference(&self) -> Option<&str> {
674 match self {
675 Schema::RecursiveRef { recursive_ref, .. } => Some(recursive_ref),
676 _ => None,
677 }
678 }
679
680 pub fn is_discriminated_union(&self) -> bool {
682 match self {
683 Schema::OneOf { discriminator, .. } => discriminator.is_some(),
684 Schema::AnyOf { discriminator, .. } => discriminator.is_some(),
685 _ => false,
686 }
687 }
688
689 pub fn discriminator(&self) -> Option<&Discriminator> {
691 match self {
692 Schema::OneOf { discriminator, .. } => discriminator.as_ref(),
693 Schema::AnyOf { discriminator, .. } => discriminator.as_ref(),
694 _ => None,
695 }
696 }
697
698 pub fn union_variants(&self) -> Option<&[Schema]> {
700 match self {
701 Schema::OneOf { one_of, .. } => Some(one_of),
702 Schema::AnyOf { any_of, .. } => Some(any_of),
703 _ => None,
704 }
705 }
706
707 pub fn is_nullable_pattern(&self) -> bool {
709 let variants = match self {
710 Schema::AnyOf { any_of, .. } => any_of,
711 Schema::OneOf { one_of, .. } => one_of,
712 _ => return false,
713 };
714 variants.len() == 2
715 && variants
716 .iter()
717 .any(|s| matches!(s.schema_type(), Some(SchemaType::Null)))
718 }
719
720 pub fn non_null_variant(&self) -> Option<&Schema> {
722 if !self.is_nullable_pattern() {
723 return None;
724 }
725 let variants = match self {
726 Schema::AnyOf { any_of, .. } => any_of,
727 Schema::OneOf { one_of, .. } => one_of,
728 _ => return None,
729 };
730 variants
731 .iter()
732 .find(|s| !matches!(s.schema_type(), Some(SchemaType::Null)))
733 }
734
735 pub fn inferred_type(&self) -> Option<SchemaType> {
737 match self {
738 Schema::Typed { schema_type, .. } => Some(schema_type.clone()),
739 Schema::TypedMulti { .. } => self.schema_type().cloned(),
740 Schema::Untyped { details } => {
741 if details.properties.is_some() {
743 Some(SchemaType::Object)
744 } else if details.items.is_some() {
745 Some(SchemaType::Array)
746 } else if details.enum_values.is_some() {
747 Some(SchemaType::String) } else {
749 None
750 }
751 }
752 _ => None,
753 }
754 }
755}
756
757impl SchemaDetails {
758 pub fn is_nullable(&self) -> bool {
760 self.nullable.unwrap_or(false)
761 }
762
763 pub fn is_string_enum(&self) -> bool {
769 self.enum_values.is_some() || self.const_string_value().is_some()
770 }
771
772 pub fn string_enum_values(&self) -> Option<Vec<String>> {
778 if let Some(values) = self.enum_values.as_ref() {
779 return Some(
784 values
785 .iter()
786 .map(|v| match v {
787 Value::String(s) => s.clone(),
788 Value::Number(n) => n.to_string(),
789 Value::Bool(b) => b.to_string(),
790 Value::Null => "null".to_string(),
791 _ => v.to_string(),
792 })
793 .collect(),
794 );
795 }
796 self.const_string_value().map(|s| vec![s])
797 }
798
799 fn const_string_value(&self) -> Option<String> {
800 self.const_value
801 .as_ref()
802 .and_then(|v| v.as_str())
803 .map(|s| s.to_string())
804 }
805
806 pub fn is_field_required(&self, field_name: &str) -> bool {
808 self.required
809 .as_ref()
810 .map(|req| req.contains(&field_name.to_string()))
811 .unwrap_or(false)
812 }
813}
814
815#[derive(Debug, Clone, Deserialize, Serialize)]
817pub struct PathItem {
818 #[serde(default)]
819 pub summary: Option<String>,
820 #[serde(default)]
821 pub description: Option<String>,
822 pub get: Option<Operation>,
823 pub put: Option<Operation>,
824 pub post: Option<Operation>,
825 pub delete: Option<Operation>,
826 pub options: Option<Operation>,
827 pub head: Option<Operation>,
828 pub patch: Option<Operation>,
829 pub trace: Option<Operation>,
830 pub query: Option<Operation>,
833 #[serde(rename = "additionalOperations", default)]
837 pub additional_operations: Option<BTreeMap<String, Operation>>,
838 pub parameters: Option<Vec<Parameter>>,
839 #[serde(default)]
840 pub servers: Option<Vec<Server>>,
841 #[serde(rename = "$ref", default)]
842 pub reference: Option<String>,
843 #[serde(flatten, default)]
844 pub extensions: Extensions,
845}
846
847impl PathItem {
848 pub fn operations(&self) -> Vec<(&str, &Operation)> {
851 let mut ops = Vec::new();
852 if let Some(ref op) = self.get {
853 ops.push(("get", op));
854 }
855 if let Some(ref op) = self.put {
856 ops.push(("put", op));
857 }
858 if let Some(ref op) = self.post {
859 ops.push(("post", op));
860 }
861 if let Some(ref op) = self.delete {
862 ops.push(("delete", op));
863 }
864 if let Some(ref op) = self.options {
865 ops.push(("options", op));
866 }
867 if let Some(ref op) = self.head {
868 ops.push(("head", op));
869 }
870 if let Some(ref op) = self.patch {
871 ops.push(("patch", op));
872 }
873 if let Some(ref op) = self.trace {
874 ops.push(("trace", op));
875 }
876 if let Some(ref op) = self.query {
877 ops.push(("query", op));
878 }
879 if let Some(map) = &self.additional_operations {
880 for (verb, op) in map {
881 ops.push((verb.as_str(), op));
882 }
883 }
884 ops
885 }
886}
887
888#[derive(Debug, Clone, Deserialize, Serialize)]
890pub struct Operation {
891 #[serde(rename = "operationId", default)]
892 pub operation_id: Option<String>,
893 #[serde(default)]
894 pub summary: Option<String>,
895 #[serde(default)]
896 pub description: Option<String>,
897 #[serde(default)]
898 pub tags: Option<Vec<String>>,
899 #[serde(default)]
900 pub deprecated: Option<bool>,
901 pub parameters: Option<Vec<Parameter>>,
902 #[serde(rename = "requestBody")]
903 pub request_body: Option<RequestBody>,
904 pub responses: Option<BTreeMap<String, Response>>,
905 #[serde(default)]
906 pub callbacks: Option<BTreeMap<String, Callback>>,
907 #[serde(default)]
908 pub security: Option<Vec<BTreeMap<String, Vec<String>>>>,
909 #[serde(default)]
910 pub servers: Option<Vec<Server>>,
911 #[serde(rename = "externalDocs", default)]
912 pub external_docs: Option<ExternalDocs>,
913 #[serde(flatten, default)]
914 pub extensions: Extensions,
915}
916
917#[derive(Debug, Clone, Deserialize, Serialize)]
919pub struct Parameter {
920 #[serde(default)]
921 pub name: Option<String>,
922 #[serde(rename = "in", default)]
923 pub location: Option<String>,
924 #[serde(default)]
925 pub required: Option<bool>,
926 #[serde(default)]
927 pub deprecated: Option<bool>,
928 #[serde(rename = "allowEmptyValue", default)]
929 pub allow_empty_value: Option<bool>,
930 #[serde(default)]
931 pub style: Option<String>,
932 #[serde(default)]
933 pub explode: Option<bool>,
934 #[serde(rename = "allowReserved", default)]
935 pub allow_reserved: Option<bool>,
936 #[serde(default)]
937 pub schema: Option<Schema>,
938 #[serde(default)]
939 pub content: Option<BTreeMap<String, MediaType>>,
940 #[serde(default)]
941 pub example: Option<Value>,
942 #[serde(default)]
943 pub examples: Option<BTreeMap<String, Example>>,
944 #[serde(default)]
945 pub description: Option<String>,
946 #[serde(rename = "$ref", default)]
947 pub reference: Option<String>,
948 #[serde(flatten, default)]
949 pub extensions: Extensions,
950}
951
952#[derive(Debug, Clone, Deserialize, Serialize)]
954pub struct RequestBody {
955 pub content: Option<BTreeMap<String, MediaType>>,
956 #[serde(default)]
957 pub description: Option<String>,
958 #[serde(default)]
959 pub required: Option<bool>,
960 #[serde(rename = "$ref", default)]
961 pub reference: Option<String>,
962 #[serde(flatten, default)]
963 pub extensions: Extensions,
964}
965
966pub fn is_json_media_type(ct: &str) -> bool {
974 let essence = ct
975 .split(';')
976 .next()
977 .unwrap_or(ct)
978 .trim()
979 .to_ascii_lowercase();
980 if essence == "application/json" {
981 return true;
982 }
983 if let Some(subtype) = essence.strip_prefix("application/") {
984 return subtype.ends_with("+json");
985 }
986 false
987}
988
989pub fn is_form_urlencoded_media_type(ct: &str) -> bool {
992 let essence = ct
993 .split(';')
994 .next()
995 .unwrap_or(ct)
996 .trim()
997 .to_ascii_lowercase();
998 essence == "application/x-www-form-urlencoded"
999}
1000
1001fn find_json_content(content: &BTreeMap<String, MediaType>) -> Option<(&str, &MediaType)> {
1002 if let Some(mt) = content.get("application/json") {
1003 return Some(("application/json", mt));
1004 }
1005 content
1006 .iter()
1007 .find(|(ct, _)| is_json_media_type(ct))
1008 .map(|(ct, mt)| (ct.as_str(), mt))
1009}
1010
1011impl RequestBody {
1012 pub fn json_schema(&self) -> Option<&Schema> {
1018 self.content
1019 .as_ref()
1020 .and_then(find_json_content)
1021 .and_then(|(_, media_type)| media_type.schema.as_ref())
1022 }
1023
1024 pub fn best_content(&self) -> Option<(&str, Option<&Schema>)> {
1026 let content = self.content.as_ref()?;
1027
1028 if let Some((ct, media_type)) = find_json_content(content) {
1029 return Some((ct, media_type.schema.as_ref()));
1030 }
1031
1032 const PRIORITY: &[&str] = &[
1033 "application/x-www-form-urlencoded",
1034 "multipart/form-data",
1035 "application/octet-stream",
1036 "text/plain",
1037 ];
1038 for ct in PRIORITY {
1039 if let Some(media_type) = content.get(*ct) {
1040 return Some((*ct, media_type.schema.as_ref()));
1041 }
1042 }
1043 None
1044 }
1045}
1046
1047#[derive(Debug, Clone, Deserialize, Serialize)]
1049pub struct Response {
1050 #[serde(default)]
1051 pub description: Option<String>,
1052 #[serde(default)]
1053 pub headers: Option<BTreeMap<String, Header>>,
1054 #[serde(default)]
1055 pub content: Option<BTreeMap<String, MediaType>>,
1056 #[serde(default)]
1057 pub links: Option<Value>,
1058 #[serde(rename = "$ref", default)]
1059 pub reference: Option<String>,
1060 #[serde(flatten, default)]
1061 pub extensions: Extensions,
1062}
1063
1064impl Response {
1065 pub fn json_schema(&self) -> Option<&Schema> {
1072 self.content
1073 .as_ref()
1074 .and_then(find_json_content)
1075 .and_then(|(_, media_type)| media_type.schema.as_ref())
1076 }
1077}
1078
1079#[derive(Debug, Clone, Deserialize, Serialize)]
1081pub struct MediaType {
1082 #[serde(default)]
1083 pub schema: Option<Schema>,
1084 #[serde(default)]
1085 pub example: Option<Value>,
1086 #[serde(default)]
1087 pub examples: Option<BTreeMap<String, Example>>,
1088 #[serde(default)]
1089 pub encoding: Option<BTreeMap<String, Encoding>>,
1090 #[serde(rename = "itemSchema", default)]
1093 pub item_schema: Option<Schema>,
1094 #[serde(rename = "prefixEncoding", default)]
1097 pub prefix_encoding: Option<Vec<Encoding>>,
1098 #[serde(rename = "itemEncoding", default)]
1101 pub item_encoding: Option<Encoding>,
1102 #[serde(rename = "$ref", default)]
1103 pub reference: Option<String>,
1104 #[serde(flatten, default)]
1105 pub extensions: Extensions,
1106}
1107
1108#[cfg(test)]
1109#[allow(clippy::unwrap_used, clippy::expect_used)]
1110mod tests {
1111 use super::*;
1112 use serde_json::json;
1113
1114 #[test]
1115 fn test_parse_simple_object_schema() {
1116 let schema_json = json!({
1117 "type": "object",
1118 "properties": {
1119 "name": {
1120 "type": "string",
1121 "description": "User name"
1122 },
1123 "age": {
1124 "type": "integer"
1125 }
1126 },
1127 "required": ["name"]
1128 });
1129
1130 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1131
1132 match schema {
1133 Schema::Typed {
1134 schema_type: SchemaType::Object,
1135 details,
1136 } => {
1137 assert!(details.properties.is_some());
1138 assert_eq!(details.required, Some(vec!["name".to_string()]));
1139 assert!(details.is_field_required("name"));
1140 assert!(!details.is_field_required("age"));
1141 }
1142 _ => panic!("Expected object schema"),
1143 }
1144 }
1145
1146 #[test]
1147 fn test_parse_string_enum() {
1148 let schema_json = json!({
1149 "type": "string",
1150 "enum": ["active", "inactive", "pending"],
1151 "description": "User status"
1152 });
1153
1154 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1155
1156 match schema {
1157 Schema::Typed {
1158 schema_type: SchemaType::String,
1159 details,
1160 } => {
1161 assert!(details.is_string_enum());
1162 let values = details.string_enum_values().unwrap();
1163 assert_eq!(values, vec!["active", "inactive", "pending"]);
1164 }
1165 _ => panic!("Expected string enum schema"),
1166 }
1167 }
1168
1169 #[test]
1170 fn test_parse_reference_schema() {
1171 let schema_json = json!({
1172 "$ref": "#/components/schemas/User"
1173 });
1174
1175 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1176
1177 assert!(schema.is_reference());
1178 assert_eq!(schema.reference(), Some("#/components/schemas/User"));
1179 }
1180
1181 #[test]
1182 fn test_parse_discriminated_union() {
1183 let schema_json = json!({
1184 "oneOf": [
1185 {"$ref": "#/components/schemas/Dog"},
1186 {"$ref": "#/components/schemas/Cat"}
1187 ],
1188 "discriminator": {
1189 "propertyName": "petType"
1190 }
1191 });
1192
1193 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1194
1195 assert!(schema.is_discriminated_union());
1196 let discriminator = schema.discriminator().unwrap();
1197 assert_eq!(discriminator.property_name, "petType");
1198 }
1199
1200 #[test]
1201 fn test_parse_nullable_pattern() {
1202 let schema_json = json!({
1203 "anyOf": [
1204 {"$ref": "#/components/schemas/User"},
1205 {"type": "null"}
1206 ]
1207 });
1208
1209 let schema: Schema = serde_json::from_value(schema_json).unwrap();
1210
1211 assert!(schema.is_nullable_pattern());
1212 let non_null = schema.non_null_variant().unwrap();
1213 assert!(non_null.is_reference());
1214 }
1215
1216 #[test]
1217 fn is_json_media_type_accepts_canonical_and_structured_suffix() {
1218 assert!(is_json_media_type("application/json"));
1220 assert!(is_json_media_type("application/json; charset=utf-8"));
1222 assert!(is_json_media_type("APPLICATION/JSON"));
1223 assert!(is_json_media_type("application/vnd.api+json"));
1225 assert!(is_json_media_type("application/hal+json"));
1226 assert!(is_json_media_type("application/problem+json"));
1227 assert!(is_json_media_type("application/ld+json"));
1228 assert!(is_json_media_type(
1229 "application/vnd.api+json; charset=utf-8"
1230 ));
1231 assert!(!is_json_media_type("application/xml"));
1233 assert!(!is_json_media_type("application/x-www-form-urlencoded"));
1234 assert!(!is_json_media_type("text/plain"));
1235 assert!(!is_json_media_type("application/jsonbutnotreally"));
1236 assert!(!is_json_media_type("text/something+json"));
1238 }
1239
1240 #[test]
1241 fn request_body_json_schema_finds_vnd_api_plus_json() {
1242 let body_json = json!({
1245 "required": true,
1246 "content": {
1247 "application/vnd.api+json": {
1248 "schema": {"$ref": "#/components/schemas/create_api_key"}
1249 }
1250 }
1251 });
1252
1253 let body: RequestBody = serde_json::from_value(body_json).unwrap();
1254 let schema = body.json_schema().expect("expected +json schema match");
1255 assert!(schema.is_reference());
1256 }
1257
1258 #[test]
1259 fn request_body_best_content_prefers_canonical_json_over_plus_json() {
1260 let body_json = json!({
1264 "required": true,
1265 "content": {
1266 "application/json": {
1267 "schema": {"$ref": "#/components/schemas/A"}
1268 },
1269 "application/vnd.api+json": {
1270 "schema": {"$ref": "#/components/schemas/B"}
1271 }
1272 }
1273 });
1274
1275 let body: RequestBody = serde_json::from_value(body_json).unwrap();
1276 let (ct, _) = body.best_content().expect("expected best_content");
1277 assert_eq!(ct, "application/json");
1278 }
1279
1280 #[test]
1281 fn request_body_best_content_falls_back_to_plus_json() {
1282 let body_json = json!({
1285 "required": true,
1286 "content": {
1287 "application/vnd.api+json": {
1288 "schema": {"$ref": "#/components/schemas/B"}
1289 }
1290 }
1291 });
1292
1293 let body: RequestBody = serde_json::from_value(body_json).unwrap();
1294 let (ct, _) = body.best_content().expect("expected best_content");
1295 assert_eq!(ct, "application/vnd.api+json");
1296 }
1297
1298 #[test]
1299 fn response_json_schema_finds_vnd_api_plus_json() {
1300 let resp_json = json!({
1303 "description": "OK",
1304 "content": {
1305 "application/vnd.api+json": {
1306 "schema": {"$ref": "#/components/schemas/api_keys"}
1307 }
1308 }
1309 });
1310
1311 let resp: Response = serde_json::from_value(resp_json).unwrap();
1312 let schema = resp.json_schema().expect("expected +json schema match");
1313 assert!(schema.is_reference());
1314 }
1315}