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 let mut visit_schema = |schema: &SchemaRef| {
169 visit_schema_ref(schema, &mut check_ref);
170 };
171
172 for path_item in self.paths.values() {
174 visit_path_item(path_item, &mut visit_schema);
175 }
176
177 for path_item in self.webhooks.values() {
179 visit_path_item(path_item, &mut visit_schema);
180 }
181
182 if let Some(components) = &self.components {
184 for schema in components.schemas.values() {
185 visit_json_schema(schema, &mut check_ref);
186 }
187 }
189
190 if missing_refs.is_empty() {
191 Ok(())
192 } else {
193 missing_refs.sort();
195 missing_refs.dedup();
196 Err(missing_refs)
197 }
198 }
199}
200
201fn visit_path_item<F>(item: &PathItem, visit: &mut F)
202where
203 F: FnMut(&SchemaRef),
204{
205 if let Some(op) = &item.get {
206 visit_operation(op, visit);
207 }
208 if let Some(op) = &item.put {
209 visit_operation(op, visit);
210 }
211 if let Some(op) = &item.post {
212 visit_operation(op, visit);
213 }
214 if let Some(op) = &item.delete {
215 visit_operation(op, visit);
216 }
217 if let Some(op) = &item.options {
218 visit_operation(op, visit);
219 }
220 if let Some(op) = &item.head {
221 visit_operation(op, visit);
222 }
223 if let Some(op) = &item.patch {
224 visit_operation(op, visit);
225 }
226 if let Some(op) = &item.trace {
227 visit_operation(op, visit);
228 }
229
230 for param in &item.parameters {
231 if let Some(s) = ¶m.schema {
232 visit(s);
233 }
234 }
235}
236
237fn visit_operation<F>(op: &Operation, visit: &mut F)
238where
239 F: FnMut(&SchemaRef),
240{
241 for param in &op.parameters {
242 if let Some(s) = ¶m.schema {
243 visit(s);
244 }
245 }
246 if let Some(body) = &op.request_body {
247 for media in body.content.values() {
248 if let Some(s) = &media.schema {
249 visit(s);
250 }
251 }
252 }
253 for resp in op.responses.values() {
254 for media in resp.content.values() {
255 if let Some(s) = &media.schema {
256 visit(s);
257 }
258 }
259 for header in resp.headers.values() {
260 if let Some(s) = &header.schema {
261 visit(s);
262 }
263 }
264 }
265}
266
267fn visit_schema_ref<F>(s: &SchemaRef, check: &mut F)
268where
269 F: FnMut(&str),
270{
271 match s {
272 SchemaRef::Ref { reference } => check(reference),
273 SchemaRef::Schema(boxed) => visit_json_schema(boxed, check),
274 SchemaRef::Inline(_) => {} }
276}
277
278fn visit_json_schema<F>(s: &JsonSchema2020, check: &mut F)
279where
280 F: FnMut(&str),
281{
282 if let Some(r) = &s.reference {
283 check(r);
284 }
285 if let Some(items) = &s.items {
286 visit_json_schema(items, check);
287 }
288 if let Some(props) = &s.properties {
289 for p in props.values() {
290 visit_json_schema(p, check);
291 }
292 }
293 if let Some(crate::schema::AdditionalProperties::Schema(p)) =
294 &s.additional_properties.as_deref()
295 {
296 visit_json_schema(p, check);
297 }
298 if let Some(one_of) = &s.one_of {
299 for p in one_of {
300 visit_json_schema(p, check);
301 }
302 }
303 if let Some(any_of) = &s.any_of {
304 for p in any_of {
305 visit_json_schema(p, check);
306 }
307 }
308 if let Some(all_of) = &s.all_of {
309 for p in all_of {
310 visit_json_schema(p, check);
311 }
312 }
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize, Default)]
316#[serde(rename_all = "camelCase")]
317pub struct ApiInfo {
318 pub title: String,
319 pub version: String,
320 #[serde(skip_serializing_if = "Option::is_none")]
321 pub summary: Option<String>,
322 #[serde(skip_serializing_if = "Option::is_none")]
323 pub description: Option<String>,
324 #[serde(skip_serializing_if = "Option::is_none")]
325 pub terms_of_service: Option<String>,
326 #[serde(skip_serializing_if = "Option::is_none")]
327 pub contact: Option<Contact>,
328 #[serde(skip_serializing_if = "Option::is_none")]
329 pub license: Option<License>,
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize, Default)]
333pub struct Contact {
334 #[serde(skip_serializing_if = "Option::is_none")]
335 pub name: Option<String>,
336 #[serde(skip_serializing_if = "Option::is_none")]
337 pub url: Option<String>,
338 #[serde(skip_serializing_if = "Option::is_none")]
339 pub email: Option<String>,
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize, Default)]
343pub struct License {
344 pub name: String,
345 #[serde(skip_serializing_if = "Option::is_none")]
346 pub identifier: Option<String>,
347 #[serde(skip_serializing_if = "Option::is_none")]
348 pub url: Option<String>,
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct Server {
353 pub url: String,
354 #[serde(skip_serializing_if = "Option::is_none")]
355 pub description: Option<String>,
356 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
357 pub variables: BTreeMap<String, ServerVariable>,
358}
359
360impl Server {
361 pub fn new(url: impl Into<String>) -> Self {
362 Self {
363 url: url.into(),
364 description: None,
365 variables: BTreeMap::new(),
366 }
367 }
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
371#[serde(rename_all = "camelCase")]
372pub struct ServerVariable {
373 #[serde(rename = "enum", skip_serializing_if = "Vec::is_empty")]
374 pub enum_values: Vec<String>,
375 pub default: String,
376 #[serde(skip_serializing_if = "Option::is_none")]
377 pub description: Option<String>,
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize, Default)]
381pub struct PathItem {
382 #[serde(skip_serializing_if = "Option::is_none")]
383 pub summary: Option<String>,
384 #[serde(skip_serializing_if = "Option::is_none")]
385 pub description: Option<String>,
386 #[serde(skip_serializing_if = "Option::is_none")]
387 pub get: Option<Operation>,
388 #[serde(skip_serializing_if = "Option::is_none")]
389 pub put: Option<Operation>,
390 #[serde(skip_serializing_if = "Option::is_none")]
391 pub post: Option<Operation>,
392 #[serde(skip_serializing_if = "Option::is_none")]
393 pub delete: Option<Operation>,
394 #[serde(skip_serializing_if = "Option::is_none")]
395 pub options: Option<Operation>,
396 #[serde(skip_serializing_if = "Option::is_none")]
397 pub head: Option<Operation>,
398 #[serde(skip_serializing_if = "Option::is_none")]
399 pub patch: Option<Operation>,
400 #[serde(skip_serializing_if = "Option::is_none")]
401 pub trace: Option<Operation>,
402 #[serde(skip_serializing_if = "Vec::is_empty")]
403 pub servers: Vec<Server>,
404 #[serde(skip_serializing_if = "Vec::is_empty")]
405 pub parameters: Vec<Parameter>,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize, Default)]
409#[serde(rename_all = "camelCase")]
410pub struct Operation {
411 #[serde(skip_serializing_if = "Vec::is_empty")]
412 pub tags: Vec<String>,
413 #[serde(skip_serializing_if = "Option::is_none")]
414 pub summary: Option<String>,
415 #[serde(skip_serializing_if = "Option::is_none")]
416 pub description: Option<String>,
417 #[serde(skip_serializing_if = "Option::is_none")]
418 pub external_docs: Option<ExternalDocs>,
419 #[serde(skip_serializing_if = "Option::is_none")]
420 pub operation_id: Option<String>,
421 #[serde(skip_serializing_if = "Vec::is_empty")]
422 pub parameters: Vec<Parameter>,
423 #[serde(skip_serializing_if = "Option::is_none")]
424 pub request_body: Option<RequestBody>,
425 pub responses: BTreeMap<String, ResponseSpec>,
426 #[serde(skip_serializing_if = "Vec::is_empty")]
427 pub security: Vec<BTreeMap<String, Vec<String>>>,
428 #[serde(skip_serializing_if = "Option::is_none")]
429 pub deprecated: Option<bool>,
430}
431
432impl Operation {
433 pub fn new() -> Self {
434 Self {
435 responses: BTreeMap::from([("200".to_string(), ResponseSpec::default())]),
436 ..Default::default()
437 }
438 }
439
440 pub fn summary(mut self, s: impl Into<String>) -> Self {
441 self.summary = Some(s.into());
442 self
443 }
444
445 pub fn description(mut self, d: impl Into<String>) -> Self {
446 self.description = Some(d.into());
447 self
448 }
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize)]
452pub struct Parameter {
453 pub name: String,
454 #[serde(rename = "in")]
455 pub location: String,
456 #[serde(skip_serializing_if = "Option::is_none")]
457 pub description: Option<String>,
458 pub required: bool,
459 #[serde(skip_serializing_if = "Option::is_none")]
460 pub deprecated: Option<bool>,
461 #[serde(skip_serializing_if = "Option::is_none")]
462 pub schema: Option<SchemaRef>,
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize)]
466pub struct RequestBody {
467 #[serde(skip_serializing_if = "Option::is_none")]
468 pub description: Option<String>,
469 pub content: BTreeMap<String, MediaType>,
470 #[serde(skip_serializing_if = "Option::is_none")]
471 pub required: Option<bool>,
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize, Default)]
475pub struct ResponseSpec {
476 pub description: String,
477 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
478 pub content: BTreeMap<String, MediaType>,
479 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
480 pub headers: BTreeMap<String, Header>,
481}
482
483#[derive(Debug, Clone, Serialize, Deserialize)]
484pub struct MediaType {
485 #[serde(skip_serializing_if = "Option::is_none")]
486 pub schema: Option<SchemaRef>,
487 #[serde(skip_serializing_if = "Option::is_none")]
488 pub example: Option<serde_json::Value>,
489}
490
491#[derive(Debug, Clone, Serialize, Deserialize)]
492pub struct Header {
493 #[serde(skip_serializing_if = "Option::is_none")]
494 pub description: Option<String>,
495 #[serde(skip_serializing_if = "Option::is_none")]
496 pub schema: Option<SchemaRef>,
497}
498
499#[derive(Debug, Clone, Serialize, Deserialize, Default)]
500#[serde(rename_all = "camelCase")]
501pub struct Components {
502 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
503 pub schemas: BTreeMap<String, JsonSchema2020>,
504 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
505 pub responses: BTreeMap<String, ResponseSpec>,
506 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
507 pub parameters: BTreeMap<String, Parameter>,
508 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
509 pub examples: BTreeMap<String, serde_json::Value>,
510 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
511 pub request_bodies: BTreeMap<String, RequestBody>,
512 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
513 pub headers: BTreeMap<String, Header>,
514 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
515 pub security_schemes: BTreeMap<String, SecurityScheme>,
516 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
517 pub links: BTreeMap<String, serde_json::Value>,
518 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
519 pub callbacks: BTreeMap<String, BTreeMap<String, PathItem>>,
520}
521
522#[derive(Debug, Clone, Serialize, Deserialize)]
523#[serde(tag = "type", rename_all = "camelCase")]
524pub enum SecurityScheme {
525 ApiKey {
526 name: String,
527 #[serde(rename = "in")]
528 location: String,
529 #[serde(skip_serializing_if = "Option::is_none")]
530 description: Option<String>,
531 },
532 Http {
533 scheme: String,
534 #[serde(skip_serializing_if = "Option::is_none")]
535 bearer_format: Option<String>,
536 #[serde(skip_serializing_if = "Option::is_none")]
537 description: Option<String>,
538 },
539 Oauth2 {
540 flows: Box<OAuthFlows>,
541 #[serde(skip_serializing_if = "Option::is_none")]
542 description: Option<String>,
543 },
544 OpenIdConnect {
545 open_id_connect_url: String,
546 #[serde(skip_serializing_if = "Option::is_none")]
547 description: Option<String>,
548 },
549}
550
551#[derive(Debug, Clone, Serialize, Deserialize, Default)]
552#[serde(rename_all = "camelCase")]
553pub struct OAuthFlows {
554 #[serde(skip_serializing_if = "Option::is_none")]
555 pub implicit: Option<OAuthFlow>,
556 #[serde(skip_serializing_if = "Option::is_none")]
557 pub password: Option<OAuthFlow>,
558 #[serde(skip_serializing_if = "Option::is_none")]
559 pub client_credentials: Option<OAuthFlow>,
560 #[serde(skip_serializing_if = "Option::is_none")]
561 pub authorization_code: Option<OAuthFlow>,
562}
563
564#[derive(Debug, Clone, Serialize, Deserialize)]
565#[serde(rename_all = "camelCase")]
566pub struct OAuthFlow {
567 #[serde(skip_serializing_if = "Option::is_none")]
568 pub authorization_url: Option<String>,
569 #[serde(skip_serializing_if = "Option::is_none")]
570 pub token_url: Option<String>,
571 #[serde(skip_serializing_if = "Option::is_none")]
572 pub refresh_url: Option<String>,
573 pub scopes: BTreeMap<String, String>,
574}
575
576#[derive(Debug, Clone, Serialize, Deserialize)]
577pub struct Tag {
578 pub name: String,
579 #[serde(skip_serializing_if = "Option::is_none")]
580 pub description: Option<String>,
581 #[serde(skip_serializing_if = "Option::is_none")]
582 pub external_docs: Option<ExternalDocs>,
583}
584
585#[derive(Debug, Clone, Serialize, Deserialize)]
586pub struct ExternalDocs {
587 pub url: String,
588 #[serde(skip_serializing_if = "Option::is_none")]
589 pub description: Option<String>,
590}
591
592pub trait OperationModifier {
594 fn update_operation(op: &mut Operation);
595}
596
597pub trait ResponseModifier {
598 fn update_response(op: &mut Operation);
599}
600
601impl<T: OperationModifier> OperationModifier for Option<T> {
603 fn update_operation(op: &mut Operation) {
604 T::update_operation(op);
605 if let Some(body) = &mut op.request_body {
606 body.required = Some(false);
607 }
608 }
609}
610
611impl<T: OperationModifier, E> OperationModifier for Result<T, E> {
612 fn update_operation(op: &mut Operation) {
613 T::update_operation(op);
614 }
615}
616
617macro_rules! impl_op_modifier_for_primitives {
618 ($($ty:ty),*) => {
619 $(
620 impl OperationModifier for $ty {
621 fn update_operation(_op: &mut Operation) {}
622 }
623 )*
624 };
625}
626impl_op_modifier_for_primitives!(
627 i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool, String
628);
629
630impl ResponseModifier for () {
631 fn update_response(op: &mut Operation) {
632 op.responses.insert(
633 "200".to_string(),
634 ResponseSpec {
635 description: "Successful response".into(),
636 ..Default::default()
637 },
638 );
639 }
640}
641
642impl ResponseModifier for String {
643 fn update_response(op: &mut Operation) {
644 let mut content = BTreeMap::new();
645 content.insert(
646 "text/plain".to_string(),
647 MediaType {
648 schema: Some(SchemaRef::Inline(serde_json::json!({"type": "string"}))),
649 example: None,
650 },
651 );
652 op.responses.insert(
653 "200".to_string(),
654 ResponseSpec {
655 description: "Successful response".into(),
656 content,
657 ..Default::default()
658 },
659 );
660 }
661}
662
663impl ResponseModifier for &'static str {
664 fn update_response(op: &mut Operation) {
665 String::update_response(op);
666 }
667}
668
669impl<T: ResponseModifier> ResponseModifier for Option<T> {
670 fn update_response(op: &mut Operation) {
671 T::update_response(op);
672 }
673}
674
675impl<T: ResponseModifier, E: ResponseModifier> ResponseModifier for Result<T, E> {
676 fn update_response(op: &mut Operation) {
677 T::update_response(op);
678 E::update_response(op);
679 }
680}
681
682impl<T> ResponseModifier for http::Response<T> {
683 fn update_response(op: &mut Operation) {
684 op.responses.insert(
685 "200".to_string(),
686 ResponseSpec {
687 description: "Successful response".into(),
688 ..Default::default()
689 },
690 );
691 }
692}