Skip to content
Open
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
13 changes: 13 additions & 0 deletions utoipa-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ pub struct Config<'c> {
pub aliases: HashMap<Cow<'c, str>, Cow<'c, str>>,
/// Schema collect mode for `utoipa`. By default only non inlined schemas are collected.
pub schema_collect: SchemaCollect,
/// Automatically include parameters from extractor types that implement `IntoParams`.
/// This acts as a global default; individual paths can still override via `auto_params`.
pub auto_into_params: bool,
}

/// Configures schema collect mode. By default only non explicitly inlined schemas are collected.
Expand Down Expand Up @@ -190,6 +193,16 @@ impl<'c> Config<'c> {
self
}

/// Define default behavior for automatically including `IntoParams` implementations.
///
/// When enabled, `utoipa::path` will include parameters from extractors that implement
/// `IntoParams` unless the path explicitly disables it with `auto_params = false`.
pub fn auto_into_params(mut self, enabled: bool) -> Self {
self.auto_into_params = enabled;

self
}

fn get_out_dir() -> Option<String> {
match std::env::var("OUT_DIR") {
Ok(out_dir) => Some(out_dir),
Expand Down
2 changes: 1 addition & 1 deletion utoipa-gen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ rc_schema = []
config = ["dep:utoipa-config", "dep:once_cell"]

# EXPERIEMENTAL! use with cauntion
auto_into_responses = []
auto_into_responses = ["utoipa/auto_into_responses"]

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(doc_cfg)'] }
37 changes: 32 additions & 5 deletions utoipa-gen/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub struct PathAttr<'p> {
operation_id: Option<Expr>,
tag: Option<parse_utils::LitStrOrExpr>,
tags: Vec<parse_utils::LitStrOrExpr>,
auto_params: Option<bool>,
params: Vec<Parameter<'p>>,
security: Option<Array<'p, SecurityRequirementsAttr>>,
context_path: Option<parse_utils::LitStrOrExpr>,
Expand Down Expand Up @@ -98,11 +99,21 @@ impl<'p> PathAttr<'p> {
}
}

self.params.extend(
new_params
.into_iter()
.filter(|param| !matches!(param, Parameter::IntoParamsIdent(_))),
);
let mut default_auto_params = false;
#[cfg(feature = "config")]
{
default_auto_params = crate::CONFIG.auto_into_params;
}

if self.auto_params.unwrap_or(default_auto_params) {
self.params.extend(new_params);
} else {
self.params.extend(
new_params
.into_iter()
.filter(|param| !matches!(param, Parameter::IntoParamsIdent(_))),
);
}
}
}

Expand Down Expand Up @@ -144,6 +155,9 @@ impl Parse for PathAttr<'_> {
Punctuated::<Response, Token![,]>::parse_terminated(&responses)
.map(|punctuated| punctuated.into_iter().collect::<Vec<Response>>())?;
}
"auto_params" => {
path_attr.auto_params = Some(parse_utils::parse_bool_or_true(input)?);
}
"params" => {
let params;
parenthesized!(params in input);
Expand Down Expand Up @@ -514,6 +528,18 @@ impl<'p> ToTokensDiagnostics for Path<'p> {
.flatten()
.fold(TokenStream2::new(), to_schema_references);

let into_response_schemas = self
.path_attr
.responses
.iter()
.filter_map(|response| match response {
Response::IntoResponses(path) => Some(quote_spanned! {path.span()=>
<#path as utoipa::IntoResponses>::schemas(schemas);
}),
_ => None,
})
.collect::<TokenStream2>();

let schemas = self
.path_attr
.request_body
Expand Down Expand Up @@ -611,6 +637,7 @@ impl<'p> ToTokensDiagnostics for Path<'p> {
fn schemas(schemas: &mut Vec<(String, utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>)>) {
#schemas
#response_schemas
#into_response_schemas
}
}

Expand Down
26 changes: 26 additions & 0 deletions utoipa-gen/src/path/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,32 @@ impl<'r> ResponseTuple<'r> {
}
Ok(())
}

pub(crate) fn get_component_schemas(
&self,
) -> Result<impl Iterator<Item = (bool, ComponentSchema)>, Diagnostics> {
match &self.inner {
Some(ResponseTupleInner::Value(value)) => {
Ok(ResponseComponentSchemaIter::Iter(Box::new(
value
.content
.iter()
.map(
|media_type| match media_type.schema.get_component_schema() {
Ok(component_schema) => {
Ok(Some(media_type.schema.is_inline()).zip(component_schema))
}
Err(error) => Err(error),
},
)
.collect::<Result<Vec<_>, Diagnostics>>()?
.into_iter()
.flatten(),
)))
}
_ => Ok(ResponseComponentSchemaIter::Empty),
}
}
}

#[cfg_attr(feature = "debug", derive(Debug))]
Expand Down
110 changes: 86 additions & 24 deletions utoipa-gen/src/path/response/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use syn::{
};

use crate::component::schema::{EnumSchema, NamedStructSchema, Root};
use crate::component::ComponentSchema;
use crate::doc_comment::CommentAttributes;
use crate::path::media_type::{DefaultSchema, MediaTypeAttr, ParsedType, Schema};
use crate::{
Expand Down Expand Up @@ -116,6 +117,40 @@ pub struct IntoResponses {

impl ToTokensDiagnostics for IntoResponses {
fn to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> {
let enum_response_tuples = match &self.data {
Data::Enum(enum_value) => Some(
enum_value
.variants
.iter()
.map(|variant| match &variant.fields {
Fields::Named(fields) => Ok(NamedStructResponse::new(
&variant.attrs,
&variant.ident,
&fields.named,
)?
.0),
Fields::Unnamed(fields) => {
let field = fields
.unnamed
.iter()
.next()
.expect("Unnamed enum variant must have 1 field");
match UnnamedStructResponse::new(
&variant.attrs,
&field.ty,
&field.attrs,
) {
Ok(response) => Ok(response.0),
Err(diagnostics) => Err(diagnostics),
}
}
Fields::Unit => Ok(UnitStructResponse::new(&variant.attrs)?.0),
})
.collect::<Result<Vec<ResponseTuple>, Diagnostics>>()?,
),
_ => None,
};

let responses = match &self.data {
Data::Struct(struct_value) => match &struct_value.fields {
Fields::Named(fields) => {
Expand Down Expand Up @@ -148,30 +183,9 @@ impl ToTokensDiagnostics for IntoResponses {
Array::from_iter(iter::once(quote!((#status, #response_tokens))))
}
},
Data::Enum(enum_value) => enum_value
.variants
.iter()
.map(|variant| match &variant.fields {
Fields::Named(fields) => Ok(NamedStructResponse::new(
&variant.attrs,
&variant.ident,
&fields.named,
)?
.0),
Fields::Unnamed(fields) => {
let field = fields
.unnamed
.iter()
.next()
.expect("Unnamed enum variant must have 1 field");
match UnnamedStructResponse::new(&variant.attrs, &field.ty, &field.attrs) {
Ok(response) => Ok(response.0),
Err(diagnostics) => Err(diagnostics),
}
}
Fields::Unit => Ok(UnitStructResponse::new(&variant.attrs)?.0),
})
.collect::<Result<Vec<ResponseTuple>, Diagnostics>>()?
Data::Enum(_) => enum_response_tuples
.as_ref()
.expect("enum response tuples must be set for enum data")
.iter()
.map(|response| {
let status = &response.status_code;
Expand All @@ -187,6 +201,50 @@ impl ToTokensDiagnostics for IntoResponses {
}
};

fn to_schema_references(
mut schemas: TokenStream,
(is_inline, component_schema): (bool, ComponentSchema),
) -> TokenStream {
for reference in component_schema.schema_references {
let name = &reference.name;
let tokens = &reference.tokens;
let references = &reference.references;

#[cfg(feature = "config")]
let should_collect_schema = (matches!(
crate::CONFIG.schema_collect,
utoipa_config::SchemaCollect::NonInlined
) && !is_inline)
|| matches!(
crate::CONFIG.schema_collect,
utoipa_config::SchemaCollect::All
);
#[cfg(not(feature = "config"))]
let should_collect_schema = !is_inline;
if should_collect_schema {
schemas.extend(quote!( schemas.push((#name, #tokens)); ));
}
schemas.extend(quote!( #references; ));
}

schemas
}

let response_schemas = enum_response_tuples
.as_ref()
.map(|responses| {
let schemas = responses
.iter()
.map(|response| response.get_component_schemas())
.collect::<Result<Vec<_>, Diagnostics>>()?
.into_iter()
.flatten()
.fold(TokenStream::new(), to_schema_references);
Ok::<_, Diagnostics>(schemas)
})
.transpose()?
.unwrap_or_else(TokenStream::new);

let ident = &self.ident;
let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();

Expand All @@ -203,6 +261,10 @@ impl ToTokensDiagnostics for IntoResponses {
.build()
.into()
}

fn schemas(schemas: &mut Vec<(String, utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>)>) {
#response_schemas
}
}
});

Expand Down
51 changes: 51 additions & 0 deletions utoipa-gen/tests/path_derive_auto_into_responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ fn path_operation_auto_types_responses() {
let value = serde_json::to_value(&doc).unwrap();
let path = value.pointer("/paths/~1item/get").unwrap();

let schema = value
.pointer("/components/schemas/Item")
.expect("Item schema should be collected from IntoResponses");
assert!(schema.is_object());

assert_json_snapshot!(&path.pointer("/responses").unwrap())
}

Expand All @@ -55,3 +60,49 @@ fn path_operation_auto_types_default_response_type() {

assert_json_snapshot!(&path.pointer("/responses").unwrap())
}

#[test]
fn path_operation_auto_types_result_responses_schema_collect() {
/// Test item to to return
#[derive(serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
struct Item<'s> {
value: &'s str,
}

#[derive(utoipa::IntoResponses)]
#[allow(unused)]
enum ItemResponse<'s> {
/// Item found
#[response(status = 200)]
Success(Item<'s>),
/// No item found
#[response(status = NOT_FOUND)]
NotFound,
}

#[derive(utoipa::IntoResponses)]
#[allow(unused)]
enum ErrorResponse {
/// Something went wrong
#[response(status = INTERNAL_SERVER_ERROR)]
InternalError,
}

#[utoipa::path(get, path = "/item")]
#[allow(unused)]
async fn get_item() -> Result<ItemResponse<'static>, ErrorResponse> {
Ok(ItemResponse::Success(Item { value: "super" }))
}

#[derive(OpenApi)]
#[openapi(paths(get_item))]
struct ApiDoc;

let doc = ApiDoc::openapi();
let value = serde_json::to_value(&doc).unwrap();

let schema = value
.pointer("/components/schemas/Item")
.expect("Item schema should be collected from Result IntoResponses");
assert!(schema.is_object());
}
Loading