Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wip experiemental actix_web auto-repsonses #649

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions scripts/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ elif [[ "$crate" == "utoipa-gen" ]]; then

cargo test -p utoipa-gen --test path_derive_auto_types --features auto_types
cargo test -p utoipa-gen --test path_derive_actix --test path_parameter_derive_actix --features actix_extras
cargo test -p utoipa-gen --test path_derive_auto_types_actix --features actix_extras,auto_types
cargo test -p utoipa-gen --test path_derive_auto_types_actix --features actix_extras,auto_types,auto_into_responses

cargo test -p utoipa-gen --test path_derive_rocket --features rocket_extras

cargo test -p utoipa-gen --test path_derive_axum_test --features axum_extras
cargo test -p utoipa-gen --test path_derive_auto_types_axum --features axum_extras,auto_types
cargo test -p utoipa-gen --test path_derive_auto_types_axum --features axum_extras,auto_types,auto_into_responses
elif [[ "$crate" == "utoipa-swagger-ui" ]]; then
cargo test -p utoipa-swagger-ui --features actix-web,rocket,axum
fi
2 changes: 2 additions & 0 deletions utoipa-gen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,5 @@ smallvec = []
repr = []
indexmap = []
auto_types = []
auto_into_responses = []
actix_auto_responses = []
141 changes: 137 additions & 4 deletions utoipa-gen/src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,18 +126,35 @@ impl ToTokens for RequestBody<'_> {
})
};

let content_type = TypeTreeExt::get_default_content_type(&actual_body);
if self.ty.is("Bytes") {
let bytes_as_bytes_vec = parse_quote!(Vec<u8>);
let ty = TypeTree::from_type(&bytes_as_bytes_vec);
create_body_tokens("application/octet-stream", &ty);
} else if self.ty.is("Form") {
create_body_tokens("application/x-www-form-urlencoded", &actual_body);
create_body_tokens(content_type, &ty);
} else {
create_body_tokens(actual_body.get_default_content_type(), &actual_body);
create_body_tokens(content_type, &actual_body);
};
}
}

#[cfg(feature = "actix_auto_responses")]
trait TypeTreeExt<'t> {
fn get_default_content_type(&'t self) -> &'t str;
}

#[cfg(feature = "actix_auto_responses")]
impl<'t> TypeTreeExt<'t> for TypeTree<'t> {
fn get_default_content_type(&self) -> &str {
if self.is("Bytes") || self.is("ByteString") {
"application/octet-stream"
} else if self.is("Form") {
"application/x-www-form-urlencoded"
} else {
<Self as PathTypeTree>::get_default_content_type(self)
}
}
}

fn get_actual_body_type<'t>(ty: &'t TypeTree<'t>) -> Option<&'t TypeTree<'t>> {
ty.path
.as_deref()
Expand Down Expand Up @@ -171,6 +188,122 @@ fn get_actual_body_type<'t>(ty: &'t TypeTree<'t>) -> Option<&'t TypeTree<'t>> {
})
}

fn get_actual_type(ty: TypeTree<'_>) -> Option<(Option<TypeTree<'_>>, Option<TypeTree<'_>>)> {
ty.path.as_ref().and_then(|path| {
// TODO
path.segments
.iter()
.find_map(|segment| match &*segment.ident.to_string() {
"Json" => Some((
Some(
ty.children
.as_deref()
.expect("Json must have children")
.first()
.expect("Json must have one child")
.clone(),
),
None,
)),
"Form" => Some((
Some(
ty.children
.as_deref()
.expect("Form must have children")
.first()
.expect("Form must have one child")
.clone(),
),
None,
)),
"Option" => get_actual_type(
ty.children
.as_deref()
.expect("Option must have children")
.first()
.expect("Option must have one child")
.clone(),
),
"Bytes" | "ByteString" => Some((Some(ty.clone()), None)),
"Result" => {
let children = ty.children.as_deref().expect("Result must have children");

Some((
get_actual_type(
children
.first()
.expect("Result children must have at least one child")
.clone(),
)
.unwrap()
.0,
children
.get(1)
.and_then(|other_type| get_actual_type(other_type.clone()).unwrap().0),
))
}
_ => Some((Some(ty.clone()), None)),
})
})

// ty.path
// .as_deref()
// .expect("get_actual_type TypeTree must have syn::Path")
// .segments
// .iter()
// .find_map(|segment| match &*segment.ident.to_string() {
// "Json" => Some((
// Some(
// ty.children
// .as_deref()
// .expect("Json must have children")
// .first()
// .expect("Json must have one child")
// .clone(),
// ),
// None,
// )),
// "Form" => Some((
// Some(
// ty.children
// .as_deref()
// .expect("Form must have children")
// .first()
// .expect("Form must have one child")
// .clone(),
// ),
// None,
// )),
// "Option" => get_actual_type(
// ty.children
// .as_deref()
// .expect("Option must have children")
// .first()
// .expect("Option must have one child")
// .clone(),
// ),
// "Bytes" | "ByteString" => Some((Some(ty), None)),
// "Result" => {
// let children = ty.children.as_deref().expect("Result must have children");

// Some((
// get_actual_type(
// children
// .first()
// .expect("Result children must have at least one child")
// .clone(),
// )
// .unwrap()
// .0,
// children
// .get(1)
// .and_then(|other_type| get_actual_type(other_type.clone()).unwrap().0),
// ))
// }
// _ => Some((Some(ty), None)),
// })
}

fn find_option_type_tree<'t>(ty: &'t TypeTree) -> Option<&'t TypeTree<'t>> {
let eq = ty.generic_type == Some(crate::component::GenericType::Option);

Expand Down
61 changes: 57 additions & 4 deletions utoipa-gen/src/ext/auto_types.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,68 @@
use std::borrow::Cow;

use syn::{ItemFn, TypePath};

pub fn parse_fn_operation_responses(fn_op: &ItemFn) -> Option<&TypePath> {
match &fn_op.sig.output {
syn::ReturnType::Type(_, item) => get_type_path(item.as_ref()),
syn::ReturnType::Default => None, // default return type () should result no responses
}
get_response_type(fn_op).and_then(get_type_path)
}

#[inline]
fn get_type_path(ty: &syn::Type) -> Option<&TypePath> {
match ty {
syn::Type::Path(ty_path) => Some(ty_path),
_ => None,
}
}

#[inline]
fn get_response_type(fn_op: &ItemFn) -> Option<&syn::Type> {
match &fn_op.sig.output {
syn::ReturnType::Type(_, item) => Some(item.as_ref()),
syn::ReturnType::Default => None, // default return type () should result no responses
}
}

#[cfg(all(feature = "actix_extras", feature = "actix_auto_responses"))]
fn to_response(
type_tree: crate::component::TypeTree<'_>,
status: crate::path::response::ResponseStatus,
) -> crate::path::response::Response {
use crate::ext::TypeTreeExt;
use crate::path::response::{Response, ResponseTuple, ResponseValue};

dbg!(&type_tree);
let type_path = TypePath {
path: type_tree
.path
.as_deref()
.expect("Response should have a type")
.clone(),
qself: None,
};
let content_type = type_tree.get_default_content_type();
let path = syn::Type::Path(type_path);
let response_value = ResponseValue::from((Cow::Owned(path), content_type));
let response: ResponseTuple = (status, response_value).into();

dbg!(&response);

Response::Tuple(response)
}

#[cfg(all(feature = "actix_extras", feature = "actix_auto_responses"))]
pub fn parse_actix_web_response(fn_op: &ItemFn) -> Vec<crate::path::response::Response<'_>> {
get_response_type(fn_op)
.map(crate::component::TypeTree::from_type)
.and_then(super::get_actual_type)
.map(|(first, second)| {
let mut responses = Vec::<crate::path::response::Response>::with_capacity(2);
if let Some(first) = first {
responses.push(to_response(first, syn::parse_quote!(200)));
};
if let Some(second) = second {
responses.push(to_response(second, syn::parse_quote!("default")));
};
responses
})
.unwrap_or_else(Vec::new)
}
9 changes: 7 additions & 2 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1296,19 +1296,24 @@ pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream {
feature = "actix_extras",
feature = "rocket_extras",
feature = "axum_extras",
feature = "auto_types"
feature = "auto_into_responses"
))]
let mut path_attribute = path_attribute;

let ast_fn = syn::parse::<ItemFn>(item).unwrap_or_abort();
let fn_name = &*ast_fn.sig.ident.to_string();

#[cfg(feature = "auto_types")]
#[cfg(feature = "auto_into_responses")]
{
if let Some(responses) = ext::auto_types::parse_fn_operation_responses(&ast_fn) {
path_attribute.responses_from_into_responses(responses);
};
}
#[cfg(feature = "actix_auto_responses")]
{
let responses = ext::auto_types::parse_actix_web_response(&ast_fn);
path_attribute.responses_from_vec(responses);
}

let mut resolved_operation = PathOperations::resolve_operation(&ast_fn);

Expand Down
9 changes: 7 additions & 2 deletions utoipa-gen/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,17 @@ impl<'p> PathAttr<'p> {
}
}

#[cfg(feature = "auto_types")]
#[cfg(feature = "auto_into_responses")]
pub fn responses_from_into_responses(&mut self, ty: &'p syn::TypePath) {
self.responses
.push(Response::IntoResponses(Cow::Borrowed(ty)))
}

#[cfg(feature = "actix_auto_responses")]
pub fn responses_from_vec(&mut self, mut responses: Vec<Response<'p>>) {
self.responses.append(&mut responses)
}

#[cfg(feature = "auto_types")]
pub fn update_request_body(&mut self, request_body: Option<crate::ext::RequestBody<'p>>) {
self.request_body = request_body.map(RequestBody::Ext);
Expand Down Expand Up @@ -603,7 +608,7 @@ pub trait PathTypeTree {

impl PathTypeTree for TypeTree<'_> {
/// Resolve default content type based on current [`Type`].
fn get_default_content_type(&self) -> &'static str {
fn get_default_content_type(&self) -> &str {
if self.is_array()
&& self
.children
Expand Down
15 changes: 14 additions & 1 deletion utoipa-gen/src/path/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,19 @@ impl<'r> From<DeriveResponsesAttributes<Option<DeriveToResponseValue>>> for Resp
}
}

impl<'t, 'c> From<(Cow<'t, syn::Type>, &'c str)> for ResponseValue<'t> {
fn from((value, content_type): (Cow<'t, syn::Type>, &'c str)) -> Self {
Self {
response_type: Some(PathType::MediaType(InlineType {
ty: value,
is_inline: false,
})),
content_type: Some(vec![content_type.to_string()]),
..Default::default()
}
}
}

#[derive(Default)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct ResponseValue<'r> {
Expand Down Expand Up @@ -561,7 +574,7 @@ impl Parse for DeriveIntoResponsesValue {

#[derive(Default)]
#[cfg_attr(feature = "debug", derive(Debug))]
struct ResponseStatus(TokenStream2);
pub struct ResponseStatus(TokenStream2);

impl Parse for ResponseStatus {
fn parse(input: ParseStream) -> syn::Result<Self> {
Expand Down
Loading