1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use super::schema::{JsonSchema2020, SchemaTransformer};
10use super::webhooks::{Callback, Webhook};
11use crate::{Operation, PathItem};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct OpenApi31Spec {
17 pub openapi: String,
19
20 pub info: ApiInfo31,
22
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub json_schema_dialect: Option<String>,
26
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub servers: Option<Vec<Server>>,
30
31 #[serde(skip_serializing_if = "HashMap::is_empty")]
33 pub paths: HashMap<String, PathItem>,
34
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub webhooks: Option<HashMap<String, Webhook>>,
38
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub components: Option<Components31>,
42
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub security: Option<Vec<HashMap<String, Vec<String>>>>,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub tags: Option<Vec<Tag>>,
50
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub external_docs: Option<ExternalDocs>,
54}
55
56impl OpenApi31Spec {
57 pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
59 Self {
60 openapi: "3.1.0".to_string(),
61 info: ApiInfo31 {
62 title: title.into(),
63 version: version.into(),
64 summary: None,
65 description: None,
66 terms_of_service: None,
67 contact: None,
68 license: None,
69 },
70 json_schema_dialect: Some("https://json-schema.org/draft/2020-12/schema".to_string()),
71 servers: None,
72 paths: HashMap::new(),
73 webhooks: None,
74 components: None,
75 security: None,
76 tags: None,
77 external_docs: None,
78 }
79 }
80
81 pub fn description(mut self, desc: impl Into<String>) -> Self {
83 self.info.description = Some(desc.into());
84 self
85 }
86
87 pub fn summary(mut self, summary: impl Into<String>) -> Self {
89 self.info.summary = Some(summary.into());
90 self
91 }
92
93 pub fn terms_of_service(mut self, url: impl Into<String>) -> Self {
95 self.info.terms_of_service = Some(url.into());
96 self
97 }
98
99 pub fn contact(mut self, contact: Contact) -> Self {
101 self.info.contact = Some(contact);
102 self
103 }
104
105 pub fn license(mut self, license: License) -> Self {
107 self.info.license = Some(license);
108 self
109 }
110
111 pub fn server(mut self, server: Server) -> Self {
113 self.servers.get_or_insert_with(Vec::new).push(server);
114 self
115 }
116
117 pub fn path(mut self, path: &str, method: &str, operation: Operation) -> Self {
119 let item = self.paths.entry(path.to_string()).or_default();
120 match method.to_uppercase().as_str() {
121 "GET" => item.get = Some(operation),
122 "POST" => item.post = Some(operation),
123 "PUT" => item.put = Some(operation),
124 "PATCH" => item.patch = Some(operation),
125 "DELETE" => item.delete = Some(operation),
126 _ => {}
127 }
128 self
129 }
130
131 pub fn webhook(mut self, name: impl Into<String>, webhook: Webhook) -> Self {
133 self.webhooks
134 .get_or_insert_with(HashMap::new)
135 .insert(name.into(), webhook);
136 self
137 }
138
139 pub fn schema(mut self, name: impl Into<String>, schema: JsonSchema2020) -> Self {
141 let components = self.components.get_or_insert_with(Components31::default);
142 components
143 .schemas
144 .get_or_insert_with(HashMap::new)
145 .insert(name.into(), schema);
146 self
147 }
148
149 pub fn schema_from_30(mut self, name: impl Into<String>, schema: serde_json::Value) -> Self {
151 let transformed = SchemaTransformer::transform_30_to_31(schema);
152 if let Ok(schema31) = serde_json::from_value::<JsonSchema2020>(transformed) {
153 let components = self.components.get_or_insert_with(Components31::default);
154 components
155 .schemas
156 .get_or_insert_with(HashMap::new)
157 .insert(name.into(), schema31);
158 }
159 self
160 }
161
162 pub fn register<T: for<'a> utoipa::ToSchema<'a>>(mut self) -> Self {
166 let (name, schema) = T::schema();
167 if let Ok(json_schema) = serde_json::to_value(schema) {
168 let transformed = SchemaTransformer::transform_30_to_31(json_schema);
169 if let Ok(schema31) = serde_json::from_value::<JsonSchema2020>(transformed) {
170 let components = self.components.get_or_insert_with(Components31::default);
171 components
172 .schemas
173 .get_or_insert_with(HashMap::new)
174 .insert(name.to_string(), schema31);
175 }
176 }
177 self
178 }
179
180 pub fn security_scheme(mut self, name: impl Into<String>, scheme: SecurityScheme) -> Self {
182 let components = self.components.get_or_insert_with(Components31::default);
183 components
184 .security_schemes
185 .get_or_insert_with(HashMap::new)
186 .insert(name.into(), scheme);
187 self
188 }
189
190 pub fn security_requirement(mut self, name: impl Into<String>, scopes: Vec<String>) -> Self {
192 let mut req = HashMap::new();
193 req.insert(name.into(), scopes);
194 self.security.get_or_insert_with(Vec::new).push(req);
195 self
196 }
197
198 pub fn tag(mut self, tag: Tag) -> Self {
200 self.tags.get_or_insert_with(Vec::new).push(tag);
201 self
202 }
203
204 pub fn external_docs(mut self, docs: ExternalDocs) -> Self {
206 self.external_docs = Some(docs);
207 self
208 }
209
210 pub fn callback(mut self, name: impl Into<String>, callback: Callback) -> Self {
212 let components = self.components.get_or_insert_with(Components31::default);
213 components
214 .callbacks
215 .get_or_insert_with(HashMap::new)
216 .insert(name.into(), callback);
217 self
218 }
219
220 pub fn to_json(&self) -> serde_json::Value {
222 serde_json::to_value(self).unwrap_or_else(|_| serde_json::json!({}))
223 }
224
225 pub fn to_json_pretty(&self) -> String {
227 serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
228 }
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
233#[serde(rename_all = "camelCase")]
234pub struct ApiInfo31 {
235 pub title: String,
237
238 pub version: String,
240
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub summary: Option<String>,
244
245 #[serde(skip_serializing_if = "Option::is_none")]
247 pub description: Option<String>,
248
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub terms_of_service: Option<String>,
252
253 #[serde(skip_serializing_if = "Option::is_none")]
255 pub contact: Option<Contact>,
256
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub license: Option<License>,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct Contact {
265 #[serde(skip_serializing_if = "Option::is_none")]
267 pub name: Option<String>,
268
269 #[serde(skip_serializing_if = "Option::is_none")]
271 pub url: Option<String>,
272
273 #[serde(skip_serializing_if = "Option::is_none")]
275 pub email: Option<String>,
276}
277
278impl Contact {
279 pub fn new() -> Self {
281 Self {
282 name: None,
283 url: None,
284 email: None,
285 }
286 }
287
288 pub fn name(mut self, name: impl Into<String>) -> Self {
290 self.name = Some(name.into());
291 self
292 }
293
294 pub fn url(mut self, url: impl Into<String>) -> Self {
296 self.url = Some(url.into());
297 self
298 }
299
300 pub fn email(mut self, email: impl Into<String>) -> Self {
302 self.email = Some(email.into());
303 self
304 }
305}
306
307impl Default for Contact {
308 fn default() -> Self {
309 Self::new()
310 }
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct License {
316 pub name: String,
318
319 #[serde(skip_serializing_if = "Option::is_none")]
321 pub identifier: Option<String>,
322
323 #[serde(skip_serializing_if = "Option::is_none")]
325 pub url: Option<String>,
326}
327
328impl License {
329 pub fn new(name: impl Into<String>) -> Self {
331 Self {
332 name: name.into(),
333 identifier: None,
334 url: None,
335 }
336 }
337
338 pub fn spdx(name: impl Into<String>, identifier: impl Into<String>) -> Self {
340 Self {
341 name: name.into(),
342 identifier: Some(identifier.into()),
343 url: None,
344 }
345 }
346
347 pub fn url(mut self, url: impl Into<String>) -> Self {
349 self.url = Some(url.into());
350 self
351 }
352}
353
354#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct Server {
357 pub url: String,
359
360 #[serde(skip_serializing_if = "Option::is_none")]
362 pub description: Option<String>,
363
364 #[serde(skip_serializing_if = "Option::is_none")]
366 pub variables: Option<HashMap<String, ServerVariable>>,
367}
368
369impl Server {
370 pub fn new(url: impl Into<String>) -> Self {
372 Self {
373 url: url.into(),
374 description: None,
375 variables: None,
376 }
377 }
378
379 pub fn description(mut self, desc: impl Into<String>) -> Self {
381 self.description = Some(desc.into());
382 self
383 }
384
385 pub fn variable(mut self, name: impl Into<String>, var: ServerVariable) -> Self {
387 self.variables
388 .get_or_insert_with(HashMap::new)
389 .insert(name.into(), var);
390 self
391 }
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
396#[serde(rename_all = "camelCase")]
397pub struct ServerVariable {
398 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
400 pub enum_values: Option<Vec<String>>,
401
402 pub default: String,
404
405 #[serde(skip_serializing_if = "Option::is_none")]
407 pub description: Option<String>,
408}
409
410impl ServerVariable {
411 pub fn new(default: impl Into<String>) -> Self {
413 Self {
414 enum_values: None,
415 default: default.into(),
416 description: None,
417 }
418 }
419
420 pub fn enum_values(mut self, values: Vec<String>) -> Self {
422 self.enum_values = Some(values);
423 self
424 }
425
426 pub fn description(mut self, desc: impl Into<String>) -> Self {
428 self.description = Some(desc.into());
429 self
430 }
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize, Default)]
435#[serde(rename_all = "camelCase")]
436pub struct Components31 {
437 #[serde(skip_serializing_if = "Option::is_none")]
439 pub schemas: Option<HashMap<String, JsonSchema2020>>,
440
441 #[serde(skip_serializing_if = "Option::is_none")]
443 pub responses: Option<HashMap<String, serde_json::Value>>,
444
445 #[serde(skip_serializing_if = "Option::is_none")]
447 pub parameters: Option<HashMap<String, serde_json::Value>>,
448
449 #[serde(skip_serializing_if = "Option::is_none")]
451 pub examples: Option<HashMap<String, serde_json::Value>>,
452
453 #[serde(skip_serializing_if = "Option::is_none")]
455 pub request_bodies: Option<HashMap<String, serde_json::Value>>,
456
457 #[serde(skip_serializing_if = "Option::is_none")]
459 pub headers: Option<HashMap<String, serde_json::Value>>,
460
461 #[serde(skip_serializing_if = "Option::is_none")]
463 pub security_schemes: Option<HashMap<String, SecurityScheme>>,
464
465 #[serde(skip_serializing_if = "Option::is_none")]
467 pub links: Option<HashMap<String, serde_json::Value>>,
468
469 #[serde(skip_serializing_if = "Option::is_none")]
471 pub callbacks: Option<HashMap<String, Callback>>,
472
473 #[serde(skip_serializing_if = "Option::is_none")]
475 pub path_items: Option<HashMap<String, PathItem>>,
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize)]
480#[serde(rename_all = "camelCase")]
481pub struct SecurityScheme {
482 #[serde(rename = "type")]
484 pub scheme_type: String,
485
486 #[serde(skip_serializing_if = "Option::is_none")]
488 pub description: Option<String>,
489
490 #[serde(skip_serializing_if = "Option::is_none")]
492 pub name: Option<String>,
493
494 #[serde(rename = "in", skip_serializing_if = "Option::is_none")]
496 pub location: Option<String>,
497
498 #[serde(skip_serializing_if = "Option::is_none")]
500 pub scheme: Option<String>,
501
502 #[serde(skip_serializing_if = "Option::is_none")]
504 pub bearer_format: Option<String>,
505
506 #[serde(skip_serializing_if = "Option::is_none")]
508 pub flows: Option<OAuthFlows>,
509
510 #[serde(skip_serializing_if = "Option::is_none")]
512 pub open_id_connect_url: Option<String>,
513}
514
515impl SecurityScheme {
516 pub fn api_key(name: impl Into<String>, location: impl Into<String>) -> Self {
518 Self {
519 scheme_type: "apiKey".to_string(),
520 description: None,
521 name: Some(name.into()),
522 location: Some(location.into()),
523 scheme: None,
524 bearer_format: None,
525 flows: None,
526 open_id_connect_url: None,
527 }
528 }
529
530 pub fn bearer(format: impl Into<String>) -> Self {
532 Self {
533 scheme_type: "http".to_string(),
534 description: None,
535 name: None,
536 location: None,
537 scheme: Some("bearer".to_string()),
538 bearer_format: Some(format.into()),
539 flows: None,
540 open_id_connect_url: None,
541 }
542 }
543
544 pub fn basic() -> Self {
546 Self {
547 scheme_type: "http".to_string(),
548 description: None,
549 name: None,
550 location: None,
551 scheme: Some("basic".to_string()),
552 bearer_format: None,
553 flows: None,
554 open_id_connect_url: None,
555 }
556 }
557
558 pub fn oauth2(flows: OAuthFlows) -> Self {
560 Self {
561 scheme_type: "oauth2".to_string(),
562 description: None,
563 name: None,
564 location: None,
565 scheme: None,
566 bearer_format: None,
567 flows: Some(flows),
568 open_id_connect_url: None,
569 }
570 }
571
572 pub fn openid_connect(url: impl Into<String>) -> Self {
574 Self {
575 scheme_type: "openIdConnect".to_string(),
576 description: None,
577 name: None,
578 location: None,
579 scheme: None,
580 bearer_format: None,
581 flows: None,
582 open_id_connect_url: Some(url.into()),
583 }
584 }
585
586 pub fn description(mut self, desc: impl Into<String>) -> Self {
588 self.description = Some(desc.into());
589 self
590 }
591}
592
593#[derive(Debug, Clone, Serialize, Deserialize, Default)]
595#[serde(rename_all = "camelCase")]
596pub struct OAuthFlows {
597 #[serde(skip_serializing_if = "Option::is_none")]
599 pub implicit: Option<OAuthFlow>,
600
601 #[serde(skip_serializing_if = "Option::is_none")]
603 pub password: Option<OAuthFlow>,
604
605 #[serde(skip_serializing_if = "Option::is_none")]
607 pub client_credentials: Option<OAuthFlow>,
608
609 #[serde(skip_serializing_if = "Option::is_none")]
611 pub authorization_code: Option<OAuthFlow>,
612}
613
614#[derive(Debug, Clone, Serialize, Deserialize)]
616#[serde(rename_all = "camelCase")]
617pub struct OAuthFlow {
618 #[serde(skip_serializing_if = "Option::is_none")]
620 pub authorization_url: Option<String>,
621
622 #[serde(skip_serializing_if = "Option::is_none")]
624 pub token_url: Option<String>,
625
626 #[serde(skip_serializing_if = "Option::is_none")]
628 pub refresh_url: Option<String>,
629
630 pub scopes: HashMap<String, String>,
632}
633
634#[derive(Debug, Clone, Serialize, Deserialize)]
636#[serde(rename_all = "camelCase")]
637pub struct Tag {
638 pub name: String,
640
641 #[serde(skip_serializing_if = "Option::is_none")]
643 pub description: Option<String>,
644
645 #[serde(skip_serializing_if = "Option::is_none")]
647 pub external_docs: Option<ExternalDocs>,
648}
649
650impl Tag {
651 pub fn new(name: impl Into<String>) -> Self {
653 Self {
654 name: name.into(),
655 description: None,
656 external_docs: None,
657 }
658 }
659
660 pub fn description(mut self, desc: impl Into<String>) -> Self {
662 self.description = Some(desc.into());
663 self
664 }
665
666 pub fn external_docs(mut self, docs: ExternalDocs) -> Self {
668 self.external_docs = Some(docs);
669 self
670 }
671}
672
673#[derive(Debug, Clone, Serialize, Deserialize)]
675pub struct ExternalDocs {
676 pub url: String,
678
679 #[serde(skip_serializing_if = "Option::is_none")]
681 pub description: Option<String>,
682}
683
684impl ExternalDocs {
685 pub fn new(url: impl Into<String>) -> Self {
687 Self {
688 url: url.into(),
689 description: None,
690 }
691 }
692
693 pub fn description(mut self, desc: impl Into<String>) -> Self {
695 self.description = Some(desc.into());
696 self
697 }
698}
699
700#[cfg(test)]
701mod tests {
702 use super::*;
703 use crate::v31::Webhook;
704
705 #[test]
706 fn test_openapi31_spec_creation() {
707 let spec = OpenApi31Spec::new("Test API", "1.0.0")
708 .description("A test API")
709 .summary("Test API Summary");
710
711 assert_eq!(spec.openapi, "3.1.0");
712 assert_eq!(spec.info.title, "Test API");
713 assert_eq!(spec.info.version, "1.0.0");
714 assert_eq!(spec.info.summary, Some("Test API Summary".to_string()));
715 assert_eq!(
716 spec.json_schema_dialect,
717 Some("https://json-schema.org/draft/2020-12/schema".to_string())
718 );
719 }
720
721 #[test]
722 fn test_license_spdx() {
723 let license = License::spdx("MIT License", "MIT");
724 assert_eq!(license.name, "MIT License");
725 assert_eq!(license.identifier, Some("MIT".to_string()));
726 }
727
728 #[test]
729 fn test_webhook_addition() {
730 let spec = OpenApi31Spec::new("Test API", "1.0.0").webhook(
731 "orderPlaced",
732 Webhook::with_summary("Order placed notification"),
733 );
734
735 assert!(spec.webhooks.is_some());
736 assert!(spec.webhooks.as_ref().unwrap().contains_key("orderPlaced"));
737 }
738
739 #[test]
740 fn test_security_scheme_bearer() {
741 let scheme = SecurityScheme::bearer("JWT").description("JWT Bearer token authentication");
742
743 assert_eq!(scheme.scheme_type, "http");
744 assert_eq!(scheme.scheme, Some("bearer".to_string()));
745 assert_eq!(scheme.bearer_format, Some("JWT".to_string()));
746 }
747
748 #[test]
749 fn test_server_with_variables() {
750 let server = Server::new("https://{environment}.api.example.com")
751 .description("Server with environment variable")
752 .variable(
753 "environment",
754 ServerVariable::new("production")
755 .enum_values(vec![
756 "development".to_string(),
757 "staging".to_string(),
758 "production".to_string(),
759 ])
760 .description("Server environment"),
761 );
762
763 assert!(server.variables.is_some());
764 assert!(server
765 .variables
766 .as_ref()
767 .unwrap()
768 .contains_key("environment"));
769 }
770
771 #[test]
772 fn test_spec_to_json() {
773 let spec =
774 OpenApi31Spec::new("Test API", "1.0.0").server(Server::new("https://api.example.com"));
775
776 let json = spec.to_json();
777 assert_eq!(json["openapi"], "3.1.0");
778 assert_eq!(json["info"]["title"], "Test API");
779 }
780}