1use serde::{Deserialize, Serialize};
4use std::collections::{BTreeMap, HashSet};
5
6use crate::schema::JsonSchema2020;
7pub use crate::schema::SchemaRef;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct OpenApiSpec {
13 pub openapi: String,
15
16 pub info: ApiInfo,
18
19 #[serde(skip_serializing_if = "Option::is_none")]
21 pub json_schema_dialect: Option<String>,
22
23 #[serde(skip_serializing_if = "Vec::is_empty")]
25 pub servers: Vec<Server>,
26
27 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
29 pub paths: BTreeMap<String, PathItem>,
30
31 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
33 pub webhooks: BTreeMap<String, PathItem>,
34
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub components: Option<Components>,
38
39 #[serde(skip_serializing_if = "Vec::is_empty")]
41 pub security: Vec<BTreeMap<String, Vec<String>>>,
42
43 #[serde(skip_serializing_if = "Vec::is_empty")]
45 pub tags: Vec<Tag>,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub external_docs: Option<ExternalDocs>,
50}
51
52impl OpenApiSpec {
53 pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
54 Self {
55 openapi: "3.1.0".to_string(),
56 info: ApiInfo {
57 title: title.into(),
58 version: version.into(),
59 ..Default::default()
60 },
61 json_schema_dialect: Some("https://json-schema.org/draft/2020-12/schema".to_string()),
62 servers: Vec::new(),
63 paths: BTreeMap::new(),
64 webhooks: BTreeMap::new(),
65 components: None,
66 security: Vec::new(),
67 tags: Vec::new(),
68 external_docs: None,
69 }
70 }
71
72 pub fn description(mut self, desc: impl Into<String>) -> Self {
73 self.info.description = Some(desc.into());
74 self
75 }
76
77 pub fn summary(mut self, summary: impl Into<String>) -> Self {
78 self.info.summary = Some(summary.into());
79 self
80 }
81
82 pub fn path(mut self, path: &str, method: &str, operation: Operation) -> Self {
83 let item = self.paths.entry(path.to_string()).or_default();
84 match method.to_uppercase().as_str() {
85 "GET" => item.get = Some(operation),
86 "POST" => item.post = Some(operation),
87 "PUT" => item.put = Some(operation),
88 "PATCH" => item.patch = Some(operation),
89 "DELETE" => item.delete = Some(operation),
90 "HEAD" => item.head = Some(operation),
91 "OPTIONS" => item.options = Some(operation),
92 "TRACE" => item.trace = Some(operation),
93 _ => {}
94 }
95 self
96 }
97
98 pub fn register<T: crate::schema::RustApiSchema>(mut self) -> Self {
100 self.register_in_place::<T>();
101 self
102 }
103
104 pub fn register_in_place<T: crate::schema::RustApiSchema>(&mut self) {
106 let mut ctx = crate::schema::SchemaCtx::new();
107
108 if let Some(c) = &self.components {
110 ctx.components = c.schemas.clone();
111 }
112
113 let _ = T::schema(&mut ctx);
115
116 let components = self.components.get_or_insert_with(Components::default);
118 for (name, schema) in ctx.components {
119 if let Some(existing) = components.schemas.get(&name) {
120 if existing != &schema {
121 panic!("Schema collision detected for component '{}'. Existing schema differs from new schema. This usually means two different types are mapped to the same component name. Please implement `RustApiSchema::name()` or alias the type.", name);
122 }
123 } else {
124 components.schemas.insert(name, schema);
125 }
126 }
127 }
128
129 pub fn server(mut self, server: Server) -> Self {
130 self.servers.push(server);
131 self
132 }
133
134 pub fn security_scheme(mut self, name: impl Into<String>, scheme: SecurityScheme) -> Self {
135 let components = self.components.get_or_insert_with(Components::default);
136 components
137 .security_schemes
138 .entry(name.into())
139 .or_insert(scheme);
140 self
141 }
142
143 pub fn to_json(&self) -> serde_json::Value {
144 serde_json::to_value(self).unwrap_or(serde_json::Value::Null)
145 }
146
147 pub fn validate_integrity(&self) -> Result<(), Vec<String>> {
150 let mut defined_schemas = HashSet::new();
151 if let Some(components) = &self.components {
152 for key in components.schemas.keys() {
153 defined_schemas.insert(format!("#/components/schemas/{}", key));
154 }
155 }
156
157 let mut missing_refs = Vec::new();
158
159 let mut check_ref = |r: &str| {
161 if r.starts_with("#/components/schemas/") && !defined_schemas.contains(r) {
162 missing_refs.push(r.to_string());
163 }
164 };
166
167 for path_item in self.paths.values() {
169 visit_path_item(path_item, &mut |s| visit_schema_ref(s, &mut check_ref));
170 }
171
172 for path_item in self.webhooks.values() {
174 visit_path_item(path_item, &mut |s| visit_schema_ref(s, &mut check_ref));
175 }
176
177 if let Some(components) = &self.components {
179 for schema in components.schemas.values() {
180 visit_json_schema(schema, &mut check_ref);
181 }
182 for resp in components.responses.values() {
183 visit_response(resp, &mut |s| visit_schema_ref(s, &mut check_ref));
184 }
185 for param in components.parameters.values() {
186 visit_parameter(param, &mut |s| visit_schema_ref(s, &mut check_ref));
187 }
188 for body in components.request_bodies.values() {
189 visit_request_body(body, &mut |s| visit_schema_ref(s, &mut check_ref));
190 }
191 for header in components.headers.values() {
192 visit_header(header, &mut |s| visit_schema_ref(s, &mut check_ref));
193 }
194 for callback_map in components.callbacks.values() {
195 for item in callback_map.values() {
196 visit_path_item(item, &mut |s| visit_schema_ref(s, &mut check_ref));
197 }
198 }
199 }
200
201 if missing_refs.is_empty() {
202 Ok(())
203 } else {
204 missing_refs.sort();
206 missing_refs.dedup();
207 Err(missing_refs)
208 }
209 }
210}
211
212fn visit_path_item<F>(item: &PathItem, visit: &mut F)
213where
214 F: FnMut(&SchemaRef),
215{
216 if let Some(op) = &item.get {
217 visit_operation(op, visit);
218 }
219 if let Some(op) = &item.put {
220 visit_operation(op, visit);
221 }
222 if let Some(op) = &item.post {
223 visit_operation(op, visit);
224 }
225 if let Some(op) = &item.delete {
226 visit_operation(op, visit);
227 }
228 if let Some(op) = &item.options {
229 visit_operation(op, visit);
230 }
231 if let Some(op) = &item.head {
232 visit_operation(op, visit);
233 }
234 if let Some(op) = &item.patch {
235 visit_operation(op, visit);
236 }
237 if let Some(op) = &item.trace {
238 visit_operation(op, visit);
239 }
240
241 for param in &item.parameters {
242 visit_parameter(param, visit);
243 }
244}
245
246fn visit_operation<F>(op: &Operation, visit: &mut F)
247where
248 F: FnMut(&SchemaRef),
249{
250 for param in &op.parameters {
251 visit_parameter(param, visit);
252 }
253 if let Some(body) = &op.request_body {
254 visit_request_body(body, visit);
255 }
256 for resp in op.responses.values() {
257 visit_response(resp, visit);
258 }
259}
260
261fn visit_parameter<F>(param: &Parameter, visit: &mut F)
262where
263 F: FnMut(&SchemaRef),
264{
265 if let Some(s) = ¶m.schema {
266 visit(s);
267 }
268}
269
270fn visit_response<F>(resp: &ResponseSpec, visit: &mut F)
271where
272 F: FnMut(&SchemaRef),
273{
274 for media in resp.content.values() {
275 visit_media_type(media, visit);
276 }
277 for header in resp.headers.values() {
278 visit_header(header, visit);
279 }
280}
281
282fn visit_request_body<F>(body: &RequestBody, visit: &mut F)
283where
284 F: FnMut(&SchemaRef),
285{
286 for media in body.content.values() {
287 visit_media_type(media, visit);
288 }
289}
290
291fn visit_header<F>(header: &Header, visit: &mut F)
292where
293 F: FnMut(&SchemaRef),
294{
295 if let Some(s) = &header.schema {
296 visit(s);
297 }
298}
299
300fn visit_media_type<F>(media: &MediaType, visit: &mut F)
301where
302 F: FnMut(&SchemaRef),
303{
304 if let Some(s) = &media.schema {
305 visit(s);
306 }
307}
308
309fn visit_schema_ref<F>(s: &SchemaRef, check: &mut F)
310where
311 F: FnMut(&str),
312{
313 match s {
314 SchemaRef::Ref { reference } => check(reference),
315 SchemaRef::Schema(boxed) => visit_json_schema(boxed, check),
316 SchemaRef::Inline(_) => {} }
318}
319
320fn visit_json_schema<F>(s: &JsonSchema2020, check: &mut F)
321where
322 F: FnMut(&str),
323{
324 if let Some(r) = &s.reference {
325 check(r);
326 }
327 if let Some(items) = &s.items {
328 visit_json_schema(items, check);
329 }
330 if let Some(props) = &s.properties {
331 for p in props.values() {
332 visit_json_schema(p, check);
333 }
334 }
335 if let Some(crate::schema::AdditionalProperties::Schema(p)) =
336 &s.additional_properties.as_deref()
337 {
338 visit_json_schema(p, check);
339 }
340 if let Some(one_of) = &s.one_of {
341 for p in one_of {
342 visit_json_schema(p, check);
343 }
344 }
345 if let Some(any_of) = &s.any_of {
346 for p in any_of {
347 visit_json_schema(p, check);
348 }
349 }
350 if let Some(all_of) = &s.all_of {
351 for p in all_of {
352 visit_json_schema(p, check);
353 }
354 }
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize, Default)]
358#[serde(rename_all = "camelCase")]
359pub struct ApiInfo {
360 pub title: String,
361 pub version: String,
362 #[serde(skip_serializing_if = "Option::is_none")]
363 pub summary: Option<String>,
364 #[serde(skip_serializing_if = "Option::is_none")]
365 pub description: Option<String>,
366 #[serde(skip_serializing_if = "Option::is_none")]
367 pub terms_of_service: Option<String>,
368 #[serde(skip_serializing_if = "Option::is_none")]
369 pub contact: Option<Contact>,
370 #[serde(skip_serializing_if = "Option::is_none")]
371 pub license: Option<License>,
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize, Default)]
375pub struct Contact {
376 #[serde(skip_serializing_if = "Option::is_none")]
377 pub name: Option<String>,
378 #[serde(skip_serializing_if = "Option::is_none")]
379 pub url: Option<String>,
380 #[serde(skip_serializing_if = "Option::is_none")]
381 pub email: Option<String>,
382}
383
384#[derive(Debug, Clone, Serialize, Deserialize, Default)]
385pub struct License {
386 pub name: String,
387 #[serde(skip_serializing_if = "Option::is_none")]
388 pub identifier: Option<String>,
389 #[serde(skip_serializing_if = "Option::is_none")]
390 pub url: Option<String>,
391}
392
393#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct Server {
395 pub url: String,
396 #[serde(skip_serializing_if = "Option::is_none")]
397 pub description: Option<String>,
398 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
399 pub variables: BTreeMap<String, ServerVariable>,
400}
401
402impl Server {
403 pub fn new(url: impl Into<String>) -> Self {
404 Self {
405 url: url.into(),
406 description: None,
407 variables: BTreeMap::new(),
408 }
409 }
410}
411
412#[derive(Debug, Clone, Serialize, Deserialize)]
413#[serde(rename_all = "camelCase")]
414pub struct ServerVariable {
415 #[serde(rename = "enum", skip_serializing_if = "Vec::is_empty")]
416 pub enum_values: Vec<String>,
417 pub default: String,
418 #[serde(skip_serializing_if = "Option::is_none")]
419 pub description: Option<String>,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize, Default)]
423pub struct PathItem {
424 #[serde(skip_serializing_if = "Option::is_none")]
425 pub summary: Option<String>,
426 #[serde(skip_serializing_if = "Option::is_none")]
427 pub description: Option<String>,
428 #[serde(skip_serializing_if = "Option::is_none")]
429 pub get: Option<Operation>,
430 #[serde(skip_serializing_if = "Option::is_none")]
431 pub put: Option<Operation>,
432 #[serde(skip_serializing_if = "Option::is_none")]
433 pub post: Option<Operation>,
434 #[serde(skip_serializing_if = "Option::is_none")]
435 pub delete: Option<Operation>,
436 #[serde(skip_serializing_if = "Option::is_none")]
437 pub options: Option<Operation>,
438 #[serde(skip_serializing_if = "Option::is_none")]
439 pub head: Option<Operation>,
440 #[serde(skip_serializing_if = "Option::is_none")]
441 pub patch: Option<Operation>,
442 #[serde(skip_serializing_if = "Option::is_none")]
443 pub trace: Option<Operation>,
444 #[serde(skip_serializing_if = "Vec::is_empty")]
445 pub servers: Vec<Server>,
446 #[serde(skip_serializing_if = "Vec::is_empty")]
447 pub parameters: Vec<Parameter>,
448}
449
450#[derive(Debug, Clone, Serialize, Deserialize, Default)]
451#[serde(rename_all = "camelCase")]
452pub struct Operation {
453 #[serde(skip_serializing_if = "Vec::is_empty")]
454 pub tags: Vec<String>,
455 #[serde(skip_serializing_if = "Option::is_none")]
456 pub summary: Option<String>,
457 #[serde(skip_serializing_if = "Option::is_none")]
458 pub description: Option<String>,
459 #[serde(skip_serializing_if = "Option::is_none")]
460 pub external_docs: Option<ExternalDocs>,
461 #[serde(skip_serializing_if = "Option::is_none")]
462 pub operation_id: Option<String>,
463 #[serde(skip_serializing_if = "Vec::is_empty")]
464 pub parameters: Vec<Parameter>,
465 #[serde(skip_serializing_if = "Option::is_none")]
466 pub request_body: Option<RequestBody>,
467 pub responses: BTreeMap<String, ResponseSpec>,
468 #[serde(skip_serializing_if = "Vec::is_empty")]
469 pub security: Vec<BTreeMap<String, Vec<String>>>,
470 #[serde(skip_serializing_if = "Option::is_none")]
471 pub deprecated: Option<bool>,
472}
473
474impl Operation {
475 pub fn new() -> Self {
476 Self {
477 responses: BTreeMap::from([("200".to_string(), ResponseSpec::default())]),
478 ..Default::default()
479 }
480 }
481
482 pub fn summary(mut self, s: impl Into<String>) -> Self {
483 self.summary = Some(s.into());
484 self
485 }
486
487 pub fn description(mut self, d: impl Into<String>) -> Self {
488 self.description = Some(d.into());
489 self
490 }
491}
492
493#[derive(Debug, Clone, Serialize, Deserialize)]
494pub struct Parameter {
495 pub name: String,
496 #[serde(rename = "in")]
497 pub location: String,
498 #[serde(skip_serializing_if = "Option::is_none")]
499 pub description: Option<String>,
500 pub required: bool,
501 #[serde(skip_serializing_if = "Option::is_none")]
502 pub deprecated: Option<bool>,
503 #[serde(skip_serializing_if = "Option::is_none")]
504 pub schema: Option<SchemaRef>,
505}
506
507#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct RequestBody {
509 #[serde(skip_serializing_if = "Option::is_none")]
510 pub description: Option<String>,
511 pub content: BTreeMap<String, MediaType>,
512 #[serde(skip_serializing_if = "Option::is_none")]
513 pub required: Option<bool>,
514}
515
516#[derive(Debug, Clone, Serialize, Deserialize, Default)]
517pub struct ResponseSpec {
518 pub description: String,
519 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
520 pub content: BTreeMap<String, MediaType>,
521 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
522 pub headers: BTreeMap<String, Header>,
523}
524
525#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct MediaType {
527 #[serde(skip_serializing_if = "Option::is_none")]
528 pub schema: Option<SchemaRef>,
529 #[serde(skip_serializing_if = "Option::is_none")]
530 pub example: Option<serde_json::Value>,
531}
532
533#[derive(Debug, Clone, Serialize, Deserialize)]
534pub struct Header {
535 #[serde(skip_serializing_if = "Option::is_none")]
536 pub description: Option<String>,
537 #[serde(skip_serializing_if = "Option::is_none")]
538 pub schema: Option<SchemaRef>,
539}
540
541#[derive(Debug, Clone, Serialize, Deserialize, Default)]
542#[serde(rename_all = "camelCase")]
543pub struct Components {
544 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
545 pub schemas: BTreeMap<String, JsonSchema2020>,
546 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
547 pub responses: BTreeMap<String, ResponseSpec>,
548 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
549 pub parameters: BTreeMap<String, Parameter>,
550 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
551 pub examples: BTreeMap<String, serde_json::Value>,
552 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
553 pub request_bodies: BTreeMap<String, RequestBody>,
554 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
555 pub headers: BTreeMap<String, Header>,
556 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
557 pub security_schemes: BTreeMap<String, SecurityScheme>,
558 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
559 pub links: BTreeMap<String, serde_json::Value>,
560 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
561 pub callbacks: BTreeMap<String, BTreeMap<String, PathItem>>,
562}
563
564#[derive(Debug, Clone, Serialize, Deserialize)]
565#[serde(tag = "type", rename_all = "camelCase")]
566pub enum SecurityScheme {
567 ApiKey {
568 name: String,
569 #[serde(rename = "in")]
570 location: String,
571 #[serde(skip_serializing_if = "Option::is_none")]
572 description: Option<String>,
573 },
574 Http {
575 scheme: String,
576 #[serde(skip_serializing_if = "Option::is_none")]
577 bearer_format: Option<String>,
578 #[serde(skip_serializing_if = "Option::is_none")]
579 description: Option<String>,
580 },
581 Oauth2 {
582 flows: Box<OAuthFlows>,
583 #[serde(skip_serializing_if = "Option::is_none")]
584 description: Option<String>,
585 },
586 OpenIdConnect {
587 open_id_connect_url: String,
588 #[serde(skip_serializing_if = "Option::is_none")]
589 description: Option<String>,
590 },
591}
592
593#[derive(Debug, Clone, Serialize, Deserialize, Default)]
594#[serde(rename_all = "camelCase")]
595pub struct OAuthFlows {
596 #[serde(skip_serializing_if = "Option::is_none")]
597 pub implicit: Option<OAuthFlow>,
598 #[serde(skip_serializing_if = "Option::is_none")]
599 pub password: Option<OAuthFlow>,
600 #[serde(skip_serializing_if = "Option::is_none")]
601 pub client_credentials: Option<OAuthFlow>,
602 #[serde(skip_serializing_if = "Option::is_none")]
603 pub authorization_code: Option<OAuthFlow>,
604}
605
606#[derive(Debug, Clone, Serialize, Deserialize)]
607#[serde(rename_all = "camelCase")]
608pub struct OAuthFlow {
609 #[serde(skip_serializing_if = "Option::is_none")]
610 pub authorization_url: Option<String>,
611 #[serde(skip_serializing_if = "Option::is_none")]
612 pub token_url: Option<String>,
613 #[serde(skip_serializing_if = "Option::is_none")]
614 pub refresh_url: Option<String>,
615 pub scopes: BTreeMap<String, String>,
616}
617
618#[derive(Debug, Clone, Serialize, Deserialize)]
619pub struct Tag {
620 pub name: String,
621 #[serde(skip_serializing_if = "Option::is_none")]
622 pub description: Option<String>,
623 #[serde(skip_serializing_if = "Option::is_none")]
624 pub external_docs: Option<ExternalDocs>,
625}
626
627#[derive(Debug, Clone, Serialize, Deserialize)]
628pub struct ExternalDocs {
629 pub url: String,
630 #[serde(skip_serializing_if = "Option::is_none")]
631 pub description: Option<String>,
632}
633
634pub trait OperationModifier {
636 fn update_operation(op: &mut Operation);
637}
638
639pub trait ResponseModifier {
640 fn update_response(op: &mut Operation);
641}
642
643impl<T: OperationModifier> OperationModifier for Option<T> {
645 fn update_operation(op: &mut Operation) {
646 T::update_operation(op);
647 if let Some(body) = &mut op.request_body {
648 body.required = Some(false);
649 }
650 }
651}
652
653impl<T: OperationModifier, E> OperationModifier for Result<T, E> {
654 fn update_operation(op: &mut Operation) {
655 T::update_operation(op);
656 }
657}
658
659macro_rules! impl_op_modifier_for_primitives {
660 ($($ty:ty),*) => {
661 $(
662 impl OperationModifier for $ty {
663 fn update_operation(_op: &mut Operation) {}
664 }
665 )*
666 };
667}
668impl_op_modifier_for_primitives!(
669 i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool, String
670);
671
672impl ResponseModifier for () {
673 fn update_response(op: &mut Operation) {
674 op.responses.insert(
675 "200".to_string(),
676 ResponseSpec {
677 description: "Successful response".into(),
678 ..Default::default()
679 },
680 );
681 }
682}
683
684impl ResponseModifier for String {
685 fn update_response(op: &mut Operation) {
686 let mut content = BTreeMap::new();
687 content.insert(
688 "text/plain".to_string(),
689 MediaType {
690 schema: Some(SchemaRef::Inline(serde_json::json!({"type": "string"}))),
691 example: None,
692 },
693 );
694 op.responses.insert(
695 "200".to_string(),
696 ResponseSpec {
697 description: "Successful response".into(),
698 content,
699 ..Default::default()
700 },
701 );
702 }
703}
704
705impl ResponseModifier for &'static str {
706 fn update_response(op: &mut Operation) {
707 String::update_response(op);
708 }
709}
710
711impl<T: ResponseModifier> ResponseModifier for Option<T> {
712 fn update_response(op: &mut Operation) {
713 T::update_response(op);
714 }
715}
716
717impl<T: ResponseModifier, E: ResponseModifier> ResponseModifier for Result<T, E> {
718 fn update_response(op: &mut Operation) {
719 T::update_response(op);
720 E::update_response(op);
721 }
722}
723
724impl<T> ResponseModifier for http::Response<T> {
725 fn update_response(op: &mut Operation) {
726 op.responses.insert(
727 "200".to_string(),
728 ResponseSpec {
729 description: "Successful response".into(),
730 ..Default::default()
731 },
732 );
733 }
734}