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