1use once_cell::sync::Lazy;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::collections::BTreeMap;
5
6#[derive(Debug, Clone, Deserialize, Serialize)]
7pub struct OpenApiSpec {
8 pub openapi: String,
9 pub info: Info,
10 pub paths: Option<BTreeMap<String, PathItem>>,
11 pub components: Option<Components>,
12 #[serde(flatten)]
13 pub extra: BTreeMap<String, Value>,
14}
15
16#[derive(Debug, Clone, Deserialize, Serialize)]
17pub struct Info {
18 pub title: String,
19 #[serde(default)]
20 pub version: Option<String>,
21 #[serde(flatten)]
22 pub extra: BTreeMap<String, Value>,
23}
24
25#[derive(Debug, Clone, Deserialize, Serialize)]
26pub struct Components {
27 pub schemas: Option<BTreeMap<String, Schema>>,
28 pub parameters: Option<BTreeMap<String, Parameter>>,
29 #[serde(flatten)]
30 pub extra: BTreeMap<String, Value>,
31}
32
33#[derive(Debug, Clone, Deserialize, Serialize)]
34#[serde(untagged)]
35pub enum Schema {
36 Reference {
38 #[serde(rename = "$ref")]
39 reference: String,
40 #[serde(flatten)]
41 extra: BTreeMap<String, Value>,
42 },
43 RecursiveRef {
45 #[serde(rename = "$recursiveRef")]
46 recursive_ref: String,
47 #[serde(flatten)]
48 extra: BTreeMap<String, Value>,
49 },
50 OneOf {
52 #[serde(rename = "oneOf")]
53 one_of: Vec<Schema>,
54 discriminator: Option<Discriminator>,
55 #[serde(flatten)]
56 details: SchemaDetails,
57 },
58 AnyOf {
60 #[serde(rename = "type")]
61 schema_type: Option<SchemaType>,
62 #[serde(rename = "anyOf")]
63 any_of: Vec<Schema>,
64 discriminator: Option<Discriminator>,
65 #[serde(flatten)]
66 details: SchemaDetails,
67 },
68 Typed {
70 #[serde(rename = "type")]
71 schema_type: SchemaType,
72 #[serde(flatten)]
73 details: SchemaDetails,
74 },
75 AllOf {
77 #[serde(rename = "allOf")]
78 all_of: Vec<Schema>,
79 #[serde(flatten)]
80 details: SchemaDetails,
81 },
82 Untyped {
84 #[serde(flatten)]
85 details: SchemaDetails,
86 },
87}
88
89#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
90#[serde(rename_all = "lowercase")]
91pub enum SchemaType {
92 String,
93 Integer,
94 Number,
95 Boolean,
96 Array,
97 Object,
98 #[serde(rename = "null")]
99 Null,
100}
101
102#[derive(Debug, Clone, Deserialize, Serialize)]
103pub struct SchemaDetails {
104 pub description: Option<String>,
105 pub nullable: Option<bool>,
106
107 #[serde(rename = "$recursiveAnchor")]
109 pub recursive_anchor: Option<bool>,
110
111 #[serde(rename = "enum")]
113 pub enum_values: Option<Vec<Value>>,
114 pub format: Option<String>,
115 pub default: Option<Value>,
116 #[serde(rename = "const")]
117 pub const_value: Option<Value>,
118
119 pub properties: Option<BTreeMap<String, Schema>>,
121 pub required: Option<Vec<String>>,
122 #[serde(rename = "additionalProperties")]
123 pub additional_properties: Option<AdditionalProperties>,
124
125 pub items: Option<Box<Schema>>,
127
128 pub minimum: Option<f64>,
130 pub maximum: Option<f64>,
131
132 #[serde(rename = "minLength")]
134 pub min_length: Option<u64>,
135 #[serde(rename = "maxLength")]
136 pub max_length: Option<u64>,
137 pub pattern: Option<String>,
138
139 #[serde(flatten)]
141 pub extra: BTreeMap<String, Value>,
142}
143
144#[derive(Debug, Clone, Deserialize, Serialize)]
145#[serde(untagged)]
146pub enum AdditionalProperties {
147 Boolean(bool),
148 Schema(Box<Schema>),
149}
150
151#[derive(Debug, Clone, Deserialize, Serialize)]
152pub struct Discriminator {
153 #[serde(rename = "propertyName")]
154 pub property_name: String,
155 pub mapping: Option<BTreeMap<String, String>>,
156 #[serde(flatten)]
157 pub extra: BTreeMap<String, Value>,
158}
159
160impl Schema {
161 pub fn schema_type(&self) -> Option<&SchemaType> {
163 match self {
164 Schema::Typed { schema_type, .. } => Some(schema_type),
165 _ => None,
166 }
167 }
168
169 pub fn details(&self) -> &SchemaDetails {
171 match self {
172 Schema::Typed { details, .. } => details,
173 Schema::Reference { .. } => {
174 static EMPTY_DETAILS: Lazy<SchemaDetails> = Lazy::new(|| SchemaDetails {
175 description: None,
176 nullable: None,
177 recursive_anchor: None,
178 enum_values: None,
179 format: None,
180 default: None,
181 const_value: None,
182 properties: None,
183 required: None,
184 additional_properties: None,
185 items: None,
186 minimum: None,
187 maximum: None,
188 min_length: None,
189 max_length: None,
190 pattern: None,
191 extra: BTreeMap::new(),
192 });
193 &EMPTY_DETAILS
194 }
195 Schema::RecursiveRef { .. } => {
196 static EMPTY_DETAILS_RECURSIVE: Lazy<SchemaDetails> = Lazy::new(|| SchemaDetails {
197 description: None,
198 nullable: None,
199 recursive_anchor: None,
200 enum_values: None,
201 format: None,
202 default: None,
203 const_value: None,
204 properties: None,
205 required: None,
206 additional_properties: None,
207 items: None,
208 minimum: None,
209 maximum: None,
210 min_length: None,
211 max_length: None,
212 pattern: None,
213 extra: BTreeMap::new(),
214 });
215 &EMPTY_DETAILS_RECURSIVE
216 }
217 Schema::OneOf { details, .. } => details,
218 Schema::AnyOf { details, .. } => details,
219 Schema::AllOf { details, .. } => details,
220 Schema::Untyped { details } => details,
221 }
222 }
223
224 pub fn details_mut(&mut self) -> &mut SchemaDetails {
226 match self {
227 Schema::Typed { details, .. } => details,
228 Schema::Reference { .. } => {
229 panic!("Cannot get mutable details for reference schema")
231 }
232 Schema::RecursiveRef { .. } => {
233 panic!("Cannot get mutable details for recursive reference schema")
235 }
236 Schema::OneOf { details, .. } => details,
237 Schema::AnyOf { details, .. } => details,
238 Schema::AllOf { details, .. } => details,
239 Schema::Untyped { details } => details,
240 }
241 }
242
243 pub fn is_reference(&self) -> bool {
245 matches!(self, Schema::Reference { .. } | Schema::RecursiveRef { .. })
246 }
247
248 pub fn reference(&self) -> Option<&str> {
250 match self {
251 Schema::Reference { reference, .. } => Some(reference),
252 _ => None,
253 }
254 }
255
256 pub fn recursive_reference(&self) -> Option<&str> {
258 match self {
259 Schema::RecursiveRef { recursive_ref, .. } => Some(recursive_ref),
260 _ => None,
261 }
262 }
263
264 pub fn is_discriminated_union(&self) -> bool {
266 match self {
267 Schema::OneOf { discriminator, .. } => discriminator.is_some(),
268 Schema::AnyOf { discriminator, .. } => discriminator.is_some(),
269 _ => false,
270 }
271 }
272
273 pub fn discriminator(&self) -> Option<&Discriminator> {
275 match self {
276 Schema::OneOf { discriminator, .. } => discriminator.as_ref(),
277 Schema::AnyOf { discriminator, .. } => discriminator.as_ref(),
278 _ => None,
279 }
280 }
281
282 pub fn union_variants(&self) -> Option<&[Schema]> {
284 match self {
285 Schema::OneOf { one_of, .. } => Some(one_of),
286 Schema::AnyOf { any_of, .. } => Some(any_of),
287 _ => None,
288 }
289 }
290
291 pub fn is_nullable_pattern(&self) -> bool {
293 match self {
294 Schema::AnyOf { any_of, .. } => {
295 any_of.len() == 2
296 && any_of
297 .iter()
298 .any(|s| matches!(s.schema_type(), Some(SchemaType::Null)))
299 }
300 _ => false,
301 }
302 }
303
304 pub fn non_null_variant(&self) -> Option<&Schema> {
306 if self.is_nullable_pattern() {
307 if let Schema::AnyOf { any_of, .. } = self {
308 return any_of
309 .iter()
310 .find(|s| !matches!(s.schema_type(), Some(SchemaType::Null)));
311 }
312 }
313 None
314 }
315
316 pub fn inferred_type(&self) -> Option<SchemaType> {
318 match self {
319 Schema::Typed { schema_type, .. } => Some(schema_type.clone()),
320 Schema::Untyped { details } => {
321 if details.properties.is_some() {
323 Some(SchemaType::Object)
324 } else if details.items.is_some() {
325 Some(SchemaType::Array)
326 } else if details.enum_values.is_some() {
327 Some(SchemaType::String) } else {
329 None
330 }
331 }
332 _ => None,
333 }
334 }
335}
336
337impl SchemaDetails {
338 pub fn is_nullable(&self) -> bool {
340 self.nullable.unwrap_or(false)
341 }
342
343 pub fn is_string_enum(&self) -> bool {
345 self.enum_values.is_some()
346 }
347
348 pub fn string_enum_values(&self) -> Option<Vec<String>> {
350 self.enum_values.as_ref().map(|values| {
351 values
352 .iter()
353 .filter_map(|v| v.as_str())
354 .map(|s| s.to_string())
355 .collect()
356 })
357 }
358
359 pub fn is_field_required(&self, field_name: &str) -> bool {
361 self.required
362 .as_ref()
363 .map(|req| req.contains(&field_name.to_string()))
364 .unwrap_or(false)
365 }
366}
367
368#[derive(Debug, Clone, Deserialize, Serialize)]
370pub struct PathItem {
371 #[serde(rename = "get")]
372 pub get: Option<Operation>,
373 #[serde(rename = "put")]
374 pub put: Option<Operation>,
375 #[serde(rename = "post")]
376 pub post: Option<Operation>,
377 #[serde(rename = "delete")]
378 pub delete: Option<Operation>,
379 #[serde(rename = "options")]
380 pub options: Option<Operation>,
381 #[serde(rename = "head")]
382 pub head: Option<Operation>,
383 #[serde(rename = "patch")]
384 pub patch: Option<Operation>,
385 #[serde(rename = "trace")]
386 pub trace: Option<Operation>,
387 pub parameters: Option<Vec<Parameter>>,
388 #[serde(flatten)]
389 pub extra: BTreeMap<String, Value>,
390}
391
392impl PathItem {
393 pub fn operations(&self) -> Vec<(&str, &Operation)> {
395 let mut ops = Vec::new();
396 if let Some(ref op) = self.get {
397 ops.push(("get", op));
398 }
399 if let Some(ref op) = self.put {
400 ops.push(("put", op));
401 }
402 if let Some(ref op) = self.post {
403 ops.push(("post", op));
404 }
405 if let Some(ref op) = self.delete {
406 ops.push(("delete", op));
407 }
408 if let Some(ref op) = self.options {
409 ops.push(("options", op));
410 }
411 if let Some(ref op) = self.head {
412 ops.push(("head", op));
413 }
414 if let Some(ref op) = self.patch {
415 ops.push(("patch", op));
416 }
417 if let Some(ref op) = self.trace {
418 ops.push(("trace", op));
419 }
420 ops
421 }
422}
423
424#[derive(Debug, Clone, Deserialize, Serialize)]
426pub struct Operation {
427 #[serde(rename = "operationId")]
428 pub operation_id: Option<String>,
429 pub summary: Option<String>,
430 pub description: Option<String>,
431 pub parameters: Option<Vec<Parameter>>,
432 #[serde(rename = "requestBody")]
433 pub request_body: Option<RequestBody>,
434 pub responses: Option<BTreeMap<String, Response>>,
435 #[serde(flatten)]
436 pub extra: BTreeMap<String, Value>,
437}
438
439#[derive(Debug, Clone, Deserialize, Serialize)]
441pub struct Parameter {
442 pub name: Option<String>,
443 #[serde(rename = "in")]
444 pub location: Option<String>,
445 pub required: Option<bool>,
446 pub schema: Option<Schema>,
447 pub description: Option<String>,
448 #[serde(flatten)]
449 pub extra: BTreeMap<String, Value>,
450}
451
452#[derive(Debug, Clone, Deserialize, Serialize)]
454pub struct RequestBody {
455 pub content: Option<BTreeMap<String, MediaType>>,
456 pub description: Option<String>,
457 pub required: Option<bool>,
458 #[serde(flatten)]
459 pub extra: BTreeMap<String, Value>,
460}
461
462impl RequestBody {
463 pub fn json_schema(&self) -> Option<&Schema> {
465 self.content
466 .as_ref()
467 .and_then(|content| content.get("application/json"))
468 .and_then(|media_type| media_type.schema.as_ref())
469 }
470
471 pub fn best_content(&self) -> Option<(&str, Option<&Schema>)> {
473 let content = self.content.as_ref()?;
474 const PRIORITY: &[&str] = &[
475 "application/json",
476 "application/x-www-form-urlencoded",
477 "multipart/form-data",
478 "application/octet-stream",
479 "text/plain",
480 ];
481 for ct in PRIORITY {
482 if let Some(media_type) = content.get(*ct) {
483 return Some((*ct, media_type.schema.as_ref()));
484 }
485 }
486 None
487 }
488}
489
490#[derive(Debug, Clone, Deserialize, Serialize)]
492pub struct Response {
493 pub description: Option<String>,
494 pub content: Option<BTreeMap<String, MediaType>>,
495 #[serde(flatten)]
496 pub extra: BTreeMap<String, Value>,
497}
498
499impl Response {
500 pub fn json_schema(&self) -> Option<&Schema> {
502 self.content
503 .as_ref()
504 .and_then(|content| content.get("application/json"))
505 .and_then(|media_type| media_type.schema.as_ref())
506 }
507}
508
509#[derive(Debug, Clone, Deserialize, Serialize)]
511pub struct MediaType {
512 pub schema: Option<Schema>,
513 #[serde(flatten)]
514 pub extra: BTreeMap<String, Value>,
515}
516
517#[cfg(test)]
518#[allow(clippy::unwrap_used)]
519mod tests {
520 use super::*;
521 use serde_json::json;
522
523 #[test]
524 fn test_parse_simple_object_schema() {
525 let schema_json = json!({
526 "type": "object",
527 "properties": {
528 "name": {
529 "type": "string",
530 "description": "User name"
531 },
532 "age": {
533 "type": "integer"
534 }
535 },
536 "required": ["name"]
537 });
538
539 let schema: Schema = serde_json::from_value(schema_json).unwrap();
540
541 match schema {
542 Schema::Typed {
543 schema_type: SchemaType::Object,
544 details,
545 } => {
546 assert!(details.properties.is_some());
547 assert_eq!(details.required, Some(vec!["name".to_string()]));
548 assert!(details.is_field_required("name"));
549 assert!(!details.is_field_required("age"));
550 }
551 _ => panic!("Expected object schema"),
552 }
553 }
554
555 #[test]
556 fn test_parse_string_enum() {
557 let schema_json = json!({
558 "type": "string",
559 "enum": ["active", "inactive", "pending"],
560 "description": "User status"
561 });
562
563 let schema: Schema = serde_json::from_value(schema_json).unwrap();
564
565 match schema {
566 Schema::Typed {
567 schema_type: SchemaType::String,
568 details,
569 } => {
570 assert!(details.is_string_enum());
571 let values = details.string_enum_values().unwrap();
572 assert_eq!(values, vec!["active", "inactive", "pending"]);
573 }
574 _ => panic!("Expected string enum schema"),
575 }
576 }
577
578 #[test]
579 fn test_parse_reference_schema() {
580 let schema_json = json!({
581 "$ref": "#/components/schemas/User"
582 });
583
584 let schema: Schema = serde_json::from_value(schema_json).unwrap();
585
586 assert!(schema.is_reference());
587 assert_eq!(schema.reference(), Some("#/components/schemas/User"));
588 }
589
590 #[test]
591 fn test_parse_discriminated_union() {
592 let schema_json = json!({
593 "oneOf": [
594 {"$ref": "#/components/schemas/Dog"},
595 {"$ref": "#/components/schemas/Cat"}
596 ],
597 "discriminator": {
598 "propertyName": "petType"
599 }
600 });
601
602 let schema: Schema = serde_json::from_value(schema_json).unwrap();
603
604 assert!(schema.is_discriminated_union());
605 let discriminator = schema.discriminator().unwrap();
606 assert_eq!(discriminator.property_name, "petType");
607 }
608
609 #[test]
610 fn test_parse_nullable_pattern() {
611 let schema_json = json!({
612 "anyOf": [
613 {"$ref": "#/components/schemas/User"},
614 {"type": "null"}
615 ]
616 });
617
618 let schema: Schema = serde_json::from_value(schema_json).unwrap();
619
620 assert!(schema.is_nullable_pattern());
621 let non_null = schema.non_null_variant().unwrap();
622 assert!(non_null.is_reference());
623 }
624}