1#![cfg_attr(feature = "docs", doc = "\n\nSee the [changelog][changelog] for a full release history.")]
7#![cfg_attr(feature = "docs", doc = "## Feature flags")]
8#![cfg_attr(feature = "docs", doc = document_features::document_features!())]
9#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
22#![cfg_attr(docsrs, feature(doc_cfg))]
23#![deny(missing_docs)]
24#![deny(unsafe_code)]
25#![deny(unreachable_pub)]
26
27use std::fmt::Formatter;
28
29use indexmap::IndexMap;
30use serde::de::{Error, Expected, Visitor};
31use serde::{Deserializer, Serializer};
32use serde_derive::{Deserialize, Serialize};
33
34pub use self::content::{Content, ContentBuilder};
35pub use self::external_docs::ExternalDocs;
36pub use self::header::{Header, HeaderBuilder};
37pub use self::info::{Contact, ContactBuilder, Info, InfoBuilder, License, LicenseBuilder};
38pub use self::path::{HttpMethod, PathItem, Paths, PathsBuilder};
39pub use self::response::{Response, ResponseBuilder, Responses, ResponsesBuilder};
40pub use self::schema::{Components, ComponentsBuilder, Discriminator, Object, Ref, Schema, Type};
41pub use self::security::SecurityRequirement;
42pub use self::server::{Server, ServerBuilder, ServerVariable, ServerVariableBuilder};
43pub use self::tag::Tag;
44
45pub mod content;
46pub mod encoding;
47pub mod example;
48pub mod extensions;
49pub mod external_docs;
50pub mod header;
51pub mod info;
52pub mod link;
53pub mod path;
54pub mod request_body;
55pub mod response;
56pub mod schema;
57pub mod security;
58pub mod server;
59pub mod tag;
60pub mod xml;
61
62#[non_exhaustive]
71#[derive(serde_derive::Serialize, serde_derive::Deserialize, Default, Clone, PartialEq, bon::Builder)]
72#[cfg_attr(feature = "debug", derive(Debug))]
73#[serde(rename_all = "camelCase")]
74#[builder(on(_, into))]
75pub struct OpenApi {
76 #[builder(default)]
78 pub openapi: OpenApiVersion,
79
80 #[builder(default)]
84 pub info: Info,
85
86 #[serde(skip_serializing_if = "Option::is_none")]
92 pub servers: Option<Vec<Server>>,
93
94 #[builder(default)]
98 pub paths: Paths,
99
100 #[serde(skip_serializing_if = "Option::is_none")]
106 pub components: Option<Components>,
107
108 #[serde(skip_serializing_if = "Option::is_none")]
114 pub security: Option<Vec<SecurityRequirement>>,
115
116 #[serde(skip_serializing_if = "Option::is_none")]
120 pub tags: Option<Vec<Tag>>,
121
122 #[serde(skip_serializing_if = "Option::is_none")]
126 pub external_docs: Option<ExternalDocs>,
127
128 #[serde(rename = "$schema", default, skip_serializing_if = "String::is_empty")]
133 #[builder(default)]
134 pub schema: String,
135
136 #[serde(skip_serializing_if = "Option::is_none", flatten)]
138 pub extensions: Option<Extensions>,
139}
140
141impl OpenApi {
142 pub fn new(info: impl Into<Info>, paths: impl Into<Paths>) -> Self {
155 Self {
156 info: info.into(),
157 paths: paths.into(),
158 ..Default::default()
159 }
160 }
161
162 pub fn to_json(&self) -> Result<String, serde_json::Error> {
164 serde_json::to_string(self)
165 }
166
167 pub fn to_pretty_json(&self) -> Result<String, serde_json::Error> {
169 serde_json::to_string_pretty(self)
170 }
171
172 #[cfg(feature = "yaml")]
174 #[cfg_attr(docsrs, doc(cfg(feature = "yaml")))]
175 pub fn to_yaml(&self) -> Result<String, serde_norway::Error> {
176 serde_norway::to_string(self)
177 }
178
179 pub fn merge_from(mut self, other: OpenApi) -> OpenApi {
184 self.merge(other);
185 self
186 }
187
188 pub fn merge(&mut self, mut other: OpenApi) {
203 if let Some(other_servers) = &mut other.servers {
204 let servers = self.servers.get_or_insert(Vec::new());
205 other_servers.retain(|server| !servers.contains(server));
206 servers.append(other_servers);
207 }
208
209 if !other.paths.paths.is_empty() {
210 self.paths.merge(other.paths);
211 };
212
213 if let Some(other_components) = &mut other.components {
214 let components = self.components.get_or_insert(Components::default());
215
216 other_components
217 .schemas
218 .retain(|name, _| !components.schemas.contains_key(name));
219 components.schemas.append(&mut other_components.schemas);
220
221 other_components
222 .responses
223 .retain(|name, _| !components.responses.contains_key(name));
224 components.responses.append(&mut other_components.responses);
225
226 other_components
227 .security_schemes
228 .retain(|name, _| !components.security_schemes.contains_key(name));
229 components.security_schemes.append(&mut other_components.security_schemes);
230 }
231
232 if let Some(other_security) = &mut other.security {
233 let security = self.security.get_or_insert(Vec::new());
234 other_security.retain(|requirement| !security.contains(requirement));
235 security.append(other_security);
236 }
237
238 if let Some(other_tags) = &mut other.tags {
239 let tags = self.tags.get_or_insert(Vec::new());
240 other_tags.retain(|tag| !tags.contains(tag));
241 tags.append(other_tags);
242 }
243 }
244
245 pub fn nest<P: Into<String>, O: Into<OpenApi>>(self, path: P, other: O) -> Self {
287 self.nest_with_path_composer(path, other, |base, path| format!("{base}{path}"))
288 }
289
290 pub fn nest_with_path_composer<P: Into<String>, O: Into<OpenApi>, F: Fn(&str, &str) -> String>(
297 mut self,
298 path: P,
299 other: O,
300 composer: F,
301 ) -> Self {
302 let path: String = path.into();
303 let mut other_api: OpenApi = other.into();
304
305 let nested_paths = other_api.paths.paths.into_iter().map(|(item_path, item)| {
306 let path = composer(&path, &item_path);
307 (path, item)
308 });
309
310 self.paths.paths.extend(nested_paths);
311
312 other_api.paths.paths = IndexMap::new();
314 self.merge_from(other_api)
315 }
316}
317
318#[derive(Serialize, Clone, PartialEq, Eq, Default)]
322#[cfg_attr(feature = "debug", derive(Debug))]
323pub enum OpenApiVersion {
324 #[serde(rename = "3.1.0")]
326 #[default]
327 Version31,
328}
329
330impl<'de> serde::Deserialize<'de> for OpenApiVersion {
331 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
332 where
333 D: Deserializer<'de>,
334 {
335 struct VersionVisitor;
336
337 impl<'v> Visitor<'v> for VersionVisitor {
338 type Value = OpenApiVersion;
339
340 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
341 formatter.write_str("a version string in 3.1.x format")
342 }
343
344 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
345 where
346 E: Error,
347 {
348 self.visit_string(v.to_string())
349 }
350
351 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
352 where
353 E: Error,
354 {
355 let version = v.split('.').flat_map(|digit| digit.parse::<i8>()).collect::<Vec<_>>();
356
357 if version.len() == 3 && version.first() == Some(&3) && version.get(1) == Some(&1) {
358 Ok(OpenApiVersion::Version31)
359 } else {
360 let expected: &dyn Expected = &"3.1.0";
361 Err(Error::invalid_value(serde::de::Unexpected::Str(&v), expected))
362 }
363 }
364 }
365
366 deserializer.deserialize_string(VersionVisitor)
367 }
368}
369
370#[derive(PartialEq, Eq, Clone, Default)]
374#[cfg_attr(feature = "debug", derive(Debug))]
375#[allow(missing_docs)]
376pub enum Deprecated {
377 True,
378 #[default]
379 False,
380}
381
382impl serde::Serialize for Deprecated {
383 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
384 where
385 S: Serializer,
386 {
387 serializer.serialize_bool(matches!(self, Self::True))
388 }
389}
390
391impl<'de> serde::Deserialize<'de> for Deprecated {
392 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
393 where
394 D: serde::Deserializer<'de>,
395 {
396 struct BoolVisitor;
397 impl<'de> Visitor<'de> for BoolVisitor {
398 type Value = Deprecated;
399
400 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
401 formatter.write_str("a bool true or false")
402 }
403
404 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
405 where
406 E: serde::de::Error,
407 {
408 match v {
409 true => Ok(Deprecated::True),
410 false => Ok(Deprecated::False),
411 }
412 }
413 }
414 deserializer.deserialize_bool(BoolVisitor)
415 }
416}
417
418#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
423#[cfg_attr(feature = "debug", derive(Debug))]
424#[serde(untagged)]
425pub enum RefOr<T> {
426 Ref(Ref),
429 T(T),
432}
433
434use crate::extensions::Extensions;
435
436#[cfg(feature = "docs")]
438#[scuffle_changelog::changelog]
439pub mod changelog {}
440
441#[cfg(test)]
442#[cfg_attr(coverage_nightly, coverage(off))]
443mod tests {
444 use insta::assert_json_snapshot;
445
446 use super::response::Response;
447 use super::*;
448 use crate::path::Operation;
449
450 #[test]
451 fn serialize_deserialize_openapi_version_success() -> Result<(), serde_json::Error> {
452 assert_eq!(serde_json::to_value(&OpenApiVersion::Version31)?, "3.1.0");
453 Ok(())
454 }
455
456 #[test]
457 fn serialize_openapi_json_minimal_success() {
458 let openapi = OpenApi::new(
459 Info::builder()
460 .title("My api")
461 .version("1.0.0")
462 .description("My api description")
463 .license(License::builder().name("MIT").url("http://mit.licence")),
464 Paths::new(),
465 );
466
467 assert_json_snapshot!(openapi);
468 }
469
470 #[test]
471 fn serialize_openapi_json_with_paths_success() {
472 let openapi = OpenApi::new(
473 Info::new("My big api", "1.1.0"),
474 Paths::builder()
475 .path(
476 "/api/v1/users",
477 PathItem::new(
478 HttpMethod::Get,
479 Operation::builder().response("200", Response::new("Get users list")),
480 ),
481 )
482 .path(
483 "/api/v1/users",
484 PathItem::new(
485 HttpMethod::Post,
486 Operation::builder().response("200", Response::new("Post new user")),
487 ),
488 )
489 .path(
490 "/api/v1/users/{id}",
491 PathItem::new(
492 HttpMethod::Get,
493 Operation::builder().response("200", Response::new("Get user by id")),
494 ),
495 ),
496 );
497
498 assert_json_snapshot!(openapi);
499 }
500
501 #[test]
502 fn merge_2_openapi_documents() {
503 let mut api_1 = OpenApi::new(
504 Info::new("Api", "v1"),
505 Paths::builder()
506 .path(
507 "/api/v1/user",
508 PathItem::new(
509 HttpMethod::Get,
510 Operation::builder().response("200", Response::new("Get user success")),
511 ),
512 )
513 .build(),
514 );
515
516 let api_2 = OpenApi::builder()
517 .info(Info::new("Api", "v2"))
518 .paths(
519 Paths::builder()
520 .path(
521 "/api/v1/user",
522 PathItem::new(
523 HttpMethod::Get,
524 Operation::builder().response("200", Response::new("This will not get added")),
525 ),
526 )
527 .path(
528 "/ap/v2/user",
529 PathItem::new(
530 HttpMethod::Get,
531 Operation::builder().response("200", Response::new("Get user success 2")),
532 ),
533 )
534 .path(
535 "/api/v2/user",
536 PathItem::new(
537 HttpMethod::Post,
538 Operation::builder().response("200", Response::new("Get user success")),
539 ),
540 )
541 .build(),
542 )
543 .components(
544 Components::builder().schema(
545 "User2",
546 Object::builder()
547 .schema_type(Type::Object)
548 .property("name", Object::builder().schema_type(Type::String)),
549 ),
550 )
551 .build();
552
553 api_1.merge(api_2);
554
555 assert_json_snapshot!(api_1, {
556 ".paths" => insta::sorted_redaction()
557 });
558 }
559
560 #[test]
561 fn merge_same_path_diff_methods() {
562 let mut api_1 = OpenApi::new(
563 Info::new("Api", "v1"),
564 Paths::builder()
565 .path(
566 "/api/v1/user",
567 PathItem::new(
568 HttpMethod::Get,
569 Operation::builder().response("200", Response::new("Get user success 1")),
570 ),
571 )
572 .extensions(Extensions::from_iter([("x-v1-api", true)]))
573 .build(),
574 );
575
576 let api_2 = OpenApi::builder()
577 .info(Info::new("Api", "v2"))
578 .paths(
579 Paths::builder()
580 .path(
581 "/api/v1/user",
582 PathItem::new(
583 HttpMethod::Get,
584 Operation::builder().response("200", Response::new("This will not get added")),
585 ),
586 )
587 .path(
588 "/api/v1/user",
589 PathItem::new(
590 HttpMethod::Post,
591 Operation::builder().response("200", Response::new("Post user success 1")),
592 ),
593 )
594 .path(
595 "/api/v2/user",
596 PathItem::new(
597 HttpMethod::Get,
598 Operation::builder().response("200", Response::new("Get user success 2")),
599 ),
600 )
601 .path(
602 "/api/v2/user",
603 PathItem::new(
604 HttpMethod::Post,
605 Operation::builder().response("200", Response::new("Post user success 2")),
606 ),
607 )
608 .extensions(Extensions::from_iter([("x-random", "Value")])),
609 )
610 .components(
611 Components::builder().schema(
612 "User2",
613 Object::builder()
614 .schema_type(Type::Object)
615 .property("name", Object::builder().schema_type(Type::String)),
616 ),
617 )
618 .build();
619
620 api_1.merge(api_2);
621
622 assert_json_snapshot!(api_1, {
623 ".paths" => insta::sorted_redaction()
624 });
625 }
626
627 #[test]
628 fn test_nest_open_apis() {
629 let api = OpenApi::builder()
630 .paths(Paths::builder().path(
631 "/api/v1/status",
632 PathItem::new(HttpMethod::Get, Operation::builder().description("Get status")),
633 ))
634 .build();
635
636 let user_api = OpenApi::builder()
637 .paths(
638 Paths::builder()
639 .path(
640 "/",
641 PathItem::new(HttpMethod::Get, Operation::builder().description("Get user details").build()),
642 )
643 .path("/foo", PathItem::new(HttpMethod::Post, Operation::builder().build())),
644 )
645 .build();
646
647 let nest_merged = api.nest("/api/v1/user", user_api);
648 let value = serde_json::to_value(nest_merged).expect("should serialize as json");
649 let paths = value.pointer("/paths").expect("paths should exits in openapi");
650
651 assert_json_snapshot!(paths);
652 }
653
654 #[test]
655 fn openapi_custom_extension() {
656 let mut api = OpenApi::builder().build();
657 let extensions = api.extensions.get_or_insert(Default::default());
658 extensions.insert(
659 String::from("x-tagGroup"),
660 String::from("anything that serializes to Json").into(),
661 );
662
663 assert_json_snapshot!(api);
664 }
665}