Skip to content

Commit

Permalink
Add rocket swagger-ui integration (#89)
Browse files Browse the repository at this point in the history
* Add utoipa-swagger-ui integration with rocket framework
  • Loading branch information
juhaku authored Apr 16, 2022
1 parent 1beb20e commit 23e0387
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 8 deletions.
29 changes: 29 additions & 0 deletions utoipa-gen/src/ext/rocket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ use proc_macro_error::{abort, abort_call_site};
use regex::{Captures, Regex};
use syn::{
parse::Parse, punctuated::Punctuated, token::Comma, FnArg, LitStr, PatIdent, Token, Type,
TypePath,
};

use crate::{
component_type::ComponentType,
ext::{ArgValue, ArgumentIn, ResolvedArg},
path::PathOperation,
};
Expand Down Expand Up @@ -87,6 +89,7 @@ impl PathOperations {
fn get_fn_args(fn_args: &Punctuated<FnArg, Comma>) -> impl Iterator<Item = Arg> + '_ {
let mut ordered_args = fn_args
.into_iter()
.filter(Self::is_supported_type)
.map(|arg| match arg {
FnArg::Typed(pat_type) => {
let ident = match pat_type.pat.as_ref() {
Expand Down Expand Up @@ -152,6 +155,32 @@ impl PathOperations {
),
}
}

fn get_type_path(ty: &Type) -> &TypePath {
match ty {
Type::Path(path) => path,
Type::Reference(reference) => Self::get_type_path(reference.elem.as_ref()),
_ => abort_call_site!("unexpected type, expected one of: Type::Path, Type::Reference"),
}
}

fn is_supported_type(arg: &&FnArg) -> bool {
match arg {
FnArg::Typed(pat_type) => {
let path = Self::get_type_path(pat_type.ty.as_ref());
let segment = &path.path.segments.first().unwrap();

let mut is_supported = ComponentType(&segment.ident).is_primitive();

if !is_supported {
is_supported = matches!(&*segment.ident.to_string(), "Vec" | "Option")
}

is_supported
}
_ => abort_call_site!("unexpected FnArg, expected FnArg::Typed"),
}
}
}

impl PathOperationResolver for PathOperations {
Expand Down
8 changes: 4 additions & 4 deletions utoipa-gen/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,11 +331,11 @@ impl ToTokens for Path<'_> {
.as_ref()
.or(self.path_operation.as_ref())
.unwrap_or_else(|| {
#[cfg(feature = "actix_extras")]
#[cfg(any(feature = "actix_extras", feature = "rocket_extras"))]
let help =
Some("Did you forget to define operation path attribute macro e.g #[get(...)]");

#[cfg(not(feature = "actix_extras"))]
#[cfg(not(any(feature = "actix_extras", feature = "rocket_extras")))]
let help = None::<&str>;

abort! {
Expand All @@ -351,11 +351,11 @@ impl ToTokens for Path<'_> {
.as_ref()
.or(self.path.as_ref())
.unwrap_or_else(|| {
#[cfg(feature = "actix_extras")]
#[cfg(any(feature = "actix_extras", feature = "rocket_extras"))]
let help =
Some("Did you forget to define operation path attribute macro e.g #[get(...)]");

#[cfg(not(feature = "actix_extras"))]
#[cfg(not(any(feature = "actix_extras", feature = "rocket_extras")))]
let help = None::<&str>;

abort! {
Expand Down
1 change: 1 addition & 0 deletions utoipa-swagger-ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ debug = []
rust-embed = { version = "6.3", features = ["interpolate-folder-path"] }
mime_guess = { version = "2.0" }
actix-web = { version = "4", optional = true }
rocket = { version = "0.5.0-rc.1", features = ["json"], optional = true }
utoipa = { version = "0.1", path = "..", default-features = false, features = [] }

[package.metadata.docs.rs]
Expand Down
105 changes: 101 additions & 4 deletions utoipa-swagger-ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,27 @@
//! .run();
//! ```
//! [^actix]: **actix-web** feature need to be enabled.
use std::{borrow::Cow, error::Error, sync::Arc};
use std::{borrow::Cow, error::Error, io::Cursor, sync::Arc};

#[cfg(feature = "actix-web")]
use actix_web::{
dev::HttpServiceFactory, guard::Get, web, web::Data, HttpResponse, Resource, Responder,
};

#[cfg(feature = "rocket")]
use rocket::{
http::{Header, Status},
response::{
status::{self, NotFound},
Responder,
},
route::{Handler, Outcome},
serde::json::Json,
Data, Request, Response, Route,
};

use rust_embed::RustEmbed;
#[cfg(feature = "actix-web")]
#[cfg(any(feature = "actix-web", feature = "rocket"))]
use utoipa::openapi::OpenApi;

#[derive(RustEmbed)]
Expand All @@ -78,13 +90,13 @@ struct SwaggerUiDist;
/// [^actix]: **actix-web** feature need to be enabled.
#[non_exhaustive]
#[derive(Clone)]
#[cfg(feature = "actix-web")]
#[cfg(any(feature = "actix-web", feature = "rocket"))]
pub struct SwaggerUi {
path: Cow<'static, str>,
urls: Vec<(Url<'static>, OpenApi)>,
}

#[cfg(feature = "actix-web")]
#[cfg(any(feature = "actix-web", feature = "rocket"))]
impl SwaggerUi {
/// Create a new [`SwaggerUi`] for given path.
///
Expand Down Expand Up @@ -210,6 +222,82 @@ fn register_api_doc_url_resource(url: &str, api: OpenApi, config: &mut actix_web
HttpServiceFactory::register(url_resource, config);
}

#[cfg(feature = "rocket")]
impl From<SwaggerUi> for Vec<Route> {
fn from(swagger_ui: SwaggerUi) -> Self {
let mut routes = Vec::<Route>::with_capacity(swagger_ui.urls.len() + 1);
let mut api_docs = Vec::<Route>::with_capacity(swagger_ui.urls.len());

let urls = swagger_ui.urls.into_iter().map(|(url, openapi)| {
api_docs.push(Route::new(
rocket::http::Method::Get,
url.url.as_ref(),
ServeApiDoc(openapi),
));
url
});

routes.push(Route::new(
rocket::http::Method::Get,
swagger_ui.path.as_ref(),
ServeSwagger(swagger_ui.path.clone(), Arc::new(Config::new(urls))),
));
routes.extend(api_docs);

routes
}
}

#[cfg(feature = "rocket")]
#[derive(Clone)]
struct ServeApiDoc(utoipa::openapi::OpenApi);

#[cfg(feature = "rocket")]
#[rocket::async_trait]
impl Handler for ServeApiDoc {
async fn handle<'r>(&self, request: &'r Request<'_>, _: Data<'r>) -> Outcome<'r> {
Outcome::from(request, Json(self.0.clone()))
}
}

#[cfg(feature = "rocket")]
#[derive(Clone)]
struct ServeSwagger(Cow<'static, str>, Arc<Config<'static>>);

#[cfg(feature = "rocket")]
#[rocket::async_trait]
impl Handler for ServeSwagger {
async fn handle<'r>(&self, request: &'r Request<'_>, _: Data<'r>) -> Outcome<'r> {
let mut path = self.0.as_ref();
if let Some(index) = self.0.find('<') {
path = &path[..index];
}

match serve(&request.uri().path().as_str()[path.len()..], self.1.clone()) {
Ok(swagger_file) => swagger_file
.map(|file| Outcome::from(request, file))
.unwrap_or_else(|| Outcome::from(request, NotFound("Swagger UI file not found"))),
Err(error) => Outcome::from(
request,
status::Custom(Status::InternalServerError, error.to_string()),
),
}
}
}

#[cfg(feature = "rocket")]
impl<'r, 'o: 'r> Responder<'r, 'o> for SwaggerFile<'o> {
fn respond_to(self, _: &'r Request<'_>) -> rocket::response::Result<'o> {
rocket::response::Result::Ok(
Response::build()
.header(Header::new("Content-Type", self.content_type))
.sized_body(self.bytes.len(), Cursor::new(self.bytes.to_vec()))
.status(Status::Ok)
.finalize(),
)
}
}

/// Rust type for Swagger UI url configuration object.
#[non_exhaustive]
#[derive(Default, Clone, Debug)]
Expand Down Expand Up @@ -296,6 +384,15 @@ impl From<String> for Url<'_> {
}
}

impl<'a> From<Cow<'static, str>> for Url<'a> {
fn from(url: Cow<'static, str>) -> Self {
Self {
url,
..Default::default()
}
}
}

#[cfg(feature = "actix-web")]
async fn serve_swagger_ui(path: web::Path<String>, data: web::Data<Config<'_>>) -> HttpResponse {
match serve(&*path.into_inner(), data.into_inner()) {
Expand Down

0 comments on commit 23e0387

Please sign in to comment.