diff --git a/utoipa-gen/src/ext/rocket.rs b/utoipa-gen/src/ext/rocket.rs index b397b9a8..b2989ca0 100644 --- a/utoipa-gen/src/ext/rocket.rs +++ b/utoipa-gen/src/ext/rocket.rs @@ -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, }; @@ -87,6 +89,7 @@ impl PathOperations { fn get_fn_args(fn_args: &Punctuated) -> impl Iterator + '_ { 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() { @@ -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 { diff --git a/utoipa-gen/src/path.rs b/utoipa-gen/src/path.rs index d9ab831e..859da612 100644 --- a/utoipa-gen/src/path.rs +++ b/utoipa-gen/src/path.rs @@ -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! { @@ -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! { diff --git a/utoipa-swagger-ui/Cargo.toml b/utoipa-swagger-ui/Cargo.toml index 28c16743..fe8a5b15 100644 --- a/utoipa-swagger-ui/Cargo.toml +++ b/utoipa-swagger-ui/Cargo.toml @@ -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] diff --git a/utoipa-swagger-ui/src/lib.rs b/utoipa-swagger-ui/src/lib.rs index d47ec7f8..0e7f14c6 100644 --- a/utoipa-swagger-ui/src/lib.rs +++ b/utoipa-swagger-ui/src/lib.rs @@ -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)] @@ -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. /// @@ -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 for Vec { + fn from(swagger_ui: SwaggerUi) -> Self { + let mut routes = Vec::::with_capacity(swagger_ui.urls.len() + 1); + let mut api_docs = Vec::::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>); + +#[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)] @@ -296,6 +384,15 @@ impl From for Url<'_> { } } +impl<'a> From> 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, data: web::Data>) -> HttpResponse { match serve(&*path.into_inner(), data.into_inner()) {