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