Skip to content

Commit

Permalink
Swagger ui documentation (#31)
Browse files Browse the repository at this point in the history
* Add swagger_ui module documentation
* Improve documentation -> No more failing doc tests
* Remove log dependency
  • Loading branch information
juhaku authored Feb 20, 2022
1 parent a2d8908 commit 4fbd4cb
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 72 deletions.
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ json = ["serde_json", "utoipa-gen/json"]

[dependencies]
serde = { version = "1.0", features = ["derive"] }
log = "0.4.14"
serde_json = { version = "1.0", optional = true }
rust-embed = { version = "6.3", optional = true, features = ["interpolate-folder-path"] }
actix-web = { version = "3.3", optional = true }
Expand Down
80 changes: 56 additions & 24 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,17 @@
//! ```
//!
//! Create an handler that would handle your business logic and add `path` proc attribute macro over it.
//! ```compile_fail
//! ```rust
//! mod pet_api {
//! # use utoipa::OpenApi;
//! # use utoipa::Component;
//! #
//! # #[derive(Component)]
//! # struct Pet {
//! # id: u64,
//! # name: String,
//! # age: Option<i32>,
//! # }
//! /// Get pet by id
//! ///
//! /// Get pet from database by pet id
Expand All @@ -89,39 +98,62 @@
//! ```
//!
//! Tie the component and the above api to the openapi schema with following `OpenApi` derive proc macro.
//! ```compile_fail
//! use utoipa::OpenApi;
//! use crate::Pet;
//!
//! ```rust
//! # mod pet_api {
//! # use utoipa::Component;
//! #
//! # #[derive(Component)]
//! # struct Pet {
//! # id: u64,
//! # name: String,
//! # age: Option<i32>,
//! # }
//! #
//! # /// Get pet by id
//! # ///
//! # /// Get pet from database by pet id
//! # #[utoipa::path(
//! # get,
//! # path = "/pets/{id}"
//! # responses = [
//! # (status = 200, description = "Pet found succesfully", body = Pet),
//! # (status = 404, description = "Pet was not found")
//! # ],
//! # params = [
//! # ("id" = u64, path, description = "Pet database id to get Pet for"),
//! # ]
//! # )]
//! # async fn get_pet_by_id(pet_id: u64) -> Pet {
//! # Pet {
//! # id: pet_id,
//! # age: None,
//! # name: "lightning".to_string(),
//! # }
//! # }
//! # }
//! # use utoipa::Component;
//! #
//! # #[derive(Component)]
//! # struct Pet {
//! # id: u64,
//! # name: String,
//! # age: Option<i32>,
//! # }
//! # use utoipa::OpenApi;
//! #[derive(OpenApi)]
//! #[openapi(handlers = [pet_api::get_pet_by_id], components = [Pet])]
//! struct ApiDoc;
//!
//! println!("{}", ApiDoc::openapi().to_pretty_json().unwrap());
//! ```
//!
//! If you have *swagger_ui* and the *actix-web* features enabled you can display the openapi documentation
//! as easily as follows:
//! ```compile_fail
//! HttpServer::new(move || {
//! App::new()
//! .service(
//! SwaggerUi::new("/swagger-ui/{_:.*}")
//! .with_url("/api-doc/openapi.json", ApiDoc::openapi()),
//! )
//! })
//! .bind(format!("{}:{}", Ipv4Addr::UNSPECIFIED, 8989))?
//! .run()
//! .await
//! ```
//!
//! See more details in [`swagger_ui`] module. You can also browse to
//! [examples](https://github.com/juhaku/utoipa/tree/master/examples) for more comprehensinve examples.
//! See how to serve OpenAPI doc via Swagger UI check [`swagger_ui`] module for more details.
//! You can also browse to [examples](https://github.com/juhaku/utoipa/tree/master/examples)
//! for more comprehensinve examples.

/// Openapi module contains Rust implementation of Openapi Spec V3
/// Rust implementation of Openapi Spec V3
pub mod openapi;
#[cfg(feature = "swagger_ui")]
/// Swagger UI module contains embedded Swagger UI and extensions for actix-web
pub mod swagger_ui;

pub use utoipa_gen::*;
Expand Down
133 changes: 129 additions & 4 deletions src/swagger_ui.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
//! Provides implementations to serve Swagger UI.
//!
//! While serving Swagger UI is framework independant. Uotipa implements the boiler plate for
//! few frameworks.
//!
//! **Currently supported frameworks by boiler plate implementations:**
//!
//! * **actix-web**
//!
//! # Examples
//!
//! Serve Swagger UI with api doc via actix-web. [^actix]
//! ```no_run
//! # use actix_web::{App, HttpServer};
//! # use utoipa::swagger_ui::SwaggerUi;
//! # use utoipa::OpenApi;
//! # use std::net::Ipv4Addr;
//! # #[derive(OpenApi)]
//! # #[openapi(handlers = [])]
//! # struct ApiDoc;
//! let _ = HttpServer::new(move || {
//! App::new()
//! .service(
//! SwaggerUi::new("/swagger-ui/{_:.*}")
//! .with_url("/api-doc/openapi.json", ApiDoc::openapi()),
//! )
//! })
//! .bind(format!("{}:{}", Ipv4Addr::UNSPECIFIED, 8989)).unwrap()
//! .run();
//! ```
//! [^actix]: **actix-web** feature need to be enabled.
use std::borrow::Cow;

use actix_web::{dev::HttpServiceFactory, guard::Get, web, HttpResponse, Resource, Responder};
Expand All @@ -10,6 +41,7 @@ use crate::openapi::OpenApi;
#[folder = "$UTOIPA_SWAGGER_DIR/$UTOIPA_SWAGGER_UI_VERSION/dist/"]
pub struct SwaggerUiDist;

/// Entry point for serving Swagger UI and api docs in application.
#[non_exhaustive]
#[derive(Clone)]
pub struct SwaggerUi {
Expand All @@ -18,19 +50,86 @@ pub struct SwaggerUi {
}

impl SwaggerUi {
/// Create a new [`SwaggerUi`] for given path.
///
/// Path argument will expose the Swagger UI to the user and should be something that
/// the underlying application framework / library supports.
///
/// # Examples
///
/// Exposes Swagger UI using path `/swagger-ui` using actix-web supported syntax.
///
/// ```rust
/// # use utoipa::swagger_ui::SwaggerUi;
/// let swagger = SwaggerUi::new("/swagger-ui/{_:.*}");
/// ```
pub fn new<P: Into<Cow<'static, str>>>(path: P) -> Self {
Self {
path: path.into(),
urls: Vec::new(),
}
}

/// Add api doc [`Url`] into [`SwaggerUi`].
///
/// Method takes two arguments where first one is path which exposes the [`OpenApi`] to the user.
/// Second argument is the actual Rust implementation of the OpenAPI doc which is being exposed.
///
/// # Examples
///
/// Expose manually created OpenAPI doc.
/// ```rust
/// # use utoipa::swagger_ui::SwaggerUi;
/// let swagger = SwaggerUi::new("/swagger-ui/{_:.*}")
/// .with_url("/api-doc/openapi.json", utoipa::openapi::OpenApi::new(
/// utoipa::openapi::Info::new("my application", "0.1.0"),
/// utoipa::openapi::Paths::new(),
/// ));
/// ```
///
/// Expose derived OpenAPI doc.
/// ```rust
/// # use utoipa::swagger_ui::SwaggerUi;
/// # use utoipa::OpenApi;
/// # #[derive(OpenApi)]
/// # #[openapi(handlers = [])]
/// # struct ApiDoc;
/// let swagger = SwaggerUi::new("/swagger-ui/{_:.*}")
/// .with_url("/api-doc/openapi.json", ApiDoc::openapi());
/// ```
pub fn with_url<U: Into<Url<'static>>>(mut self, url: U, openapi: OpenApi) -> Self {
self.urls.push((url.into(), openapi));

self
}

/// Add multiple [`Url`]s to Swagger UI.
///
/// Takes one [`Vec`] argument containing tuples of [`Url`] and [`OpenApi`].
///
/// Situations where this comes handy is when there is a need or wish to seprate different parts
/// of the api to separate api docs.
///
/// # Examples
///
/// Expose multiple api docs via Swagger UI.
/// ```rust
/// # use utoipa::swagger_ui::{SwaggerUi, Url};
/// # use utoipa::OpenApi;
/// # #[derive(OpenApi)]
/// # #[openapi(handlers = [])]
/// # struct ApiDoc;
/// # #[derive(OpenApi)]
/// # #[openapi(handlers = [])]
/// # struct ApiDoc2;
/// let swagger = SwaggerUi::new("/swagger-ui/{_:.*}")
/// .with_urls(
/// vec![
/// (Url::with_primary("api doc 1", "/api-doc/openapi.json", true), ApiDoc::openapi()),
/// (Url::new("api doc 2", "/api-doc/openapi2.json"), ApiDoc2::openapi())
/// ]
/// );
/// ```
pub fn with_urls(mut self, urls: Vec<(Url<'static>, OpenApi)>) -> Self {
self.urls = urls;

Expand Down Expand Up @@ -62,8 +161,6 @@ impl HttpServiceFactory for SwaggerUi {
#[cfg(feature = "actix-web")]
fn register_api_doc_url_resource(url: &(Url, OpenApi), config: &mut actix_web::dev::AppService) {
pub async fn get_api_doc(api_doc: web::Data<OpenApi>) -> impl Responder {
log::trace!("Get api doc:\n{}", api_doc.to_pretty_json().unwrap());

HttpResponse::Ok().json(api_doc.as_ref())
}

Expand All @@ -74,6 +171,7 @@ fn register_api_doc_url_resource(url: &(Url, OpenApi), config: &mut actix_web::d
HttpServiceFactory::register(url_resource, config);
}

/// Rust type for Swagger UI url configuration object.
#[non_exhaustive]
#[derive(Default, Clone)]
pub struct Url<'a> {
Expand All @@ -83,6 +181,18 @@ pub struct Url<'a> {
}

impl<'a> Url<'a> {
/// Create new [`Url`].
///
/// Name is shown in the select dropdown when there are multiple docs in Swagger UI.
///
/// Url is path which exposes the OpenAPI doc.
///
/// # Examples
///
/// ```rust
/// # use utoipa::swagger_ui::Url;
/// let url = Url::new("My Api", "/api-doc/openapi.json");
/// ```
pub fn new(name: &'a str, url: &'a str) -> Self {
Self {
name: Cow::Borrowed(name),
Expand All @@ -91,6 +201,23 @@ impl<'a> Url<'a> {
}
}

/// Create new [`Url`] with primary flag.
///
/// Primary flag allows users to override the default behaviour of the Swagger UI for selecting the primary
/// doc to display. By default when there are multiple docs in Swagger UI the first one in the list
/// will be the primary.
///
/// Name is shown in the select dropdown when there are multiple docs in Swagger UI.
///
/// Url is path which exposes the OpenAPI doc.
///
/// # Examples
///
/// Set "My Api" as primary.
/// ```rust
/// # use utoipa::swagger_ui::Url;
/// let url = Url::with_primary("My Api", "/api-doc/openapi.json", true);
/// ```
pub fn with_primary(name: &'a str, url: &'a str, primary: bool) -> Self {
Self {
name: Cow::Borrowed(name),
Expand Down Expand Up @@ -123,8 +250,6 @@ async fn serve_swagger_ui(
web::Path(mut part): web::Path<String>,
data: web::Data<Vec<Url<'_>>>,
) -> HttpResponse {
log::debug!("Get swagger resource: {}", &part);

if part.is_empty() || part == "/" {
part = "index.html".to_string()
}
Expand Down
47 changes: 4 additions & 43 deletions tests/utoipa_gen_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,48 +71,9 @@ struct ApiDoc;
#[test]
#[ignore = "this is just a test bed to run macros"]
fn derive_openapi() {
utoipa::openapi::OpenApi::new(
utoipa::openapi::Info::new("my application", "0.1.0"),
utoipa::openapi::Paths::new(),
);
println!("{}", ApiDoc::openapi().to_pretty_json().unwrap());
}

fn path() -> &'static str {
"/pets/{id}"
}

fn path_item(default_tag: Option<&str>) -> utoipa::openapi::path::Paths {
utoipa::openapi::Paths::new().append(
"/pets/{id}",
utoipa::openapi::PathItem::new(
utoipa::openapi::PathItemType::Get,
utoipa::openapi::path::Operation::new()
.with_responses(
utoipa::openapi::Responses::new()
.with_response(
"200",
utoipa::openapi::Response::new("Pet found succesfully").with_content(
"application/json",
utoipa::openapi::Content::new(
utoipa::openapi::Ref::from_component_name("Pet"),
),
),
)
.with_response("404", utoipa::openapi::Response::new("Pet was not found")),
)
.with_operation_id("get_pet_by_id")
.with_deprecated(utoipa::openapi::Deprecated::False)
.with_summary("Get pet by id")
.with_description("Get pet by id\n\nGet pet from database by pet database id\n")
.with_parameter(
utoipa::openapi::path::Parameter::new("id")
.with_in(utoipa::openapi::path::ParameterIn::Path)
.with_deprecated(utoipa::openapi::Deprecated::False)
.with_description("Pet database id to get Pet for")
.with_schema(
utoipa::openapi::Property::new(utoipa::openapi::ComponentType::Integer)
.with_format(utoipa::openapi::ComponentFormat::Int64),
)
.with_required(utoipa::openapi::Required::True),
)
.with_tag("pet_api"),
),
)
}

0 comments on commit 4fbd4cb

Please sign in to comment.