Repository: juhaku/utoipa
Pull request #10 (2022-01-16 01:35:11)

Feature responses
* Add parsing cabability for path responses and response headers
* Add test for supported scenarios


Patch:
diff --git a/src/openapi/content.rs b/src/openapi/content.rs
new file mode 100644
index 0000000..e7b7e94
--- /dev/null
+++ b/src/openapi/content.rs
@@ -0,0 +1,17 @@
+use serde::{Deserialize, Serialize};
+
+use super::Component;
+
+#[derive(Serialize, Deserialize, Default)]
+#[non_exhaustive]
+pub struct Content {
+    pub schema: Component,
+}
+
+impl Content {
+    pub fn new<I: Into<Component>>(schema: I) -> Self {
+        Self {
+            schema: schema.into(),
+        }
+    }
+}
diff --git a/src/openapi/header.rs b/src/openapi/header.rs
new file mode 100644
index 0000000..6fe673f
--- /dev/null
+++ b/src/openapi/header.rs
@@ -0,0 +1,36 @@
+use serde::{Deserialize, Serialize};
+
+use super::{Component, ComponentType, Property};
+
+#[non_exhaustive]
+#[derive(Serialize, Deserialize)]
+pub struct Header {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub description: Option<String>,
+
+    pub schema: Component,
+}
+
+impl Header {
+    pub fn new<C: Into<Component>>(component: C) -> Self {
+        Self {
+            schema: component.into(),
+            ..Default::default()
+        }
+    }
+
+    pub fn with_description<S: AsRef<str>>(mut self, description: S) -> Self {
+        self.description = Some(description.as_ref().to_string());
+
+        self
+    }
+}
+
+impl Default for Header {
+    fn default() -> Self {
+        Self {
+            description: Default::default(),
+            schema: Property::new(ComponentType::String).into(),
+        }
+    }
+}
diff --git a/src/openapi/mod.rs b/src/openapi/mod.rs
index 68a7186..6ef4016 100644
--- a/src/openapi/mod.rs
+++ b/src/openapi/mod.rs
@@ -6,10 +6,13 @@ use crate::error::Error;
 
 pub use self::{
     contact::Contact,
+    content::Content,
     external_docs::ExternalDocs,
+    header::Header,
     info::Info,
     licence::Licence,
     path::{PathItem, PathItemType, Paths},
+    response::{Response, Responses},
     schema::{Array, Component, ComponentFormat, ComponentType, Object, Property, Ref, Schema},
     security::Security,
     server::Server,
@@ -17,7 +20,9 @@ pub use self::{
 };
 
 pub mod contact;
+pub mod content;
 pub mod external_docs;
+pub mod header;
 pub mod info;
 pub mod licence;
 pub mod path;
diff --git a/src/openapi/request_body.rs b/src/openapi/request_body.rs
index 9c4a604..020ccc7 100644
--- a/src/openapi/request_body.rs
+++ b/src/openapi/request_body.rs
@@ -2,7 +2,7 @@ use std::collections::HashMap;
 
 use serde::{Deserialize, Serialize};
 
-use super::{Component, Required};
+use super::{Component, Content, Required};
 
 #[non_exhaustive]
 #[derive(Serialize, Deserialize, Default)]
@@ -34,24 +34,16 @@ impl RequestBody {
         self
     }
 
-    pub fn with_content<S: AsRef<str>>(mut self, content_type: S, content: Content) -> Self {
-        self.content
-            .insert(content_type.as_ref().to_string(), content);
+    pub fn with_content<S: AsRef<str>, C: Into<Component>>(
+        mut self,
+        content_type: S,
+        component: C,
+    ) -> Self {
+        self.content.insert(
+            content_type.as_ref().to_string(),
+            Content::new(component.into()),
+        );
 
         self
     }
 }
-
-#[derive(Serialize, Deserialize, Default)]
-#[non_exhaustive]
-pub struct Content {
-    pub schema: Component,
-}
-
-impl Content {
-    pub fn new<I: Into<Component>>(schema: I) -> Self {
-        Self {
-            schema: schema.into(),
-        }
-    }
-}
diff --git a/src/openapi/response.rs b/src/openapi/response.rs
index bdd2a50..918c160 100644
--- a/src/openapi/response.rs
+++ b/src/openapi/response.rs
@@ -1,13 +1,15 @@
-use std::collections::HashMap;
+use std::collections::{BTreeMap, HashMap};
 
 use serde::{Deserialize, Serialize};
 
+use super::{header::Header, Component, Content};
+
 #[non_exhaustive]
 #[derive(Serialize, Deserialize, Default)]
 #[serde(rename_all = "camelCase")]
 pub struct Responses {
     #[serde(flatten)]
-    pub inner: HashMap<String, Response>,
+    pub inner: BTreeMap<String, Response>,
 }
 
 impl Responses {
@@ -26,14 +28,39 @@ impl Responses {
 #[derive(Serialize, Deserialize, Default)]
 #[serde(rename_all = "camelCase")]
 pub struct Response {
-    // TODO add missing fields
     pub description: String,
+
+    #[serde(skip_serializing_if = "HashMap::is_empty")]
+    pub headers: HashMap<String, Header>,
+
+    #[serde(skip_serializing_if = "HashMap::is_empty")]
+    pub content: HashMap<String, Content>,
 }
 
 impl Response {
     pub fn new<S: AsRef<str>>(description: S) -> Self {
         Self {
             description: description.as_ref().to_string(),
+            ..Default::default()
         }
     }
+
+    pub fn with_content<C: Into<Component>, S: AsRef<str>>(
+        mut self,
+        content_type: S,
+        component: C,
+    ) -> Self {
+        self.content.insert(
+            content_type.as_ref().to_string(),
+            Content::new(component.into()),
+        );
+
+        self
+    }
+
+    pub fn with_header<S: AsRef<str>>(mut self, name: S, header: Header) -> Self {
+        self.headers.insert(name.as_ref().to_string(), header);
+
+        self
+    }
 }
diff --git a/utoipa-gen/src/component.rs b/utoipa-gen/src/component.rs
index 1f6119d..51284a4 100644
--- a/utoipa-gen/src/component.rs
+++ b/utoipa-gen/src/component.rs
@@ -1,16 +1,17 @@
 use std::{ops::Deref, rc::Rc};
 
-use proc_macro2::{Group, Ident, Punct, TokenStream as TokenStream2};
+use proc_macro2::{Ident, TokenStream as TokenStream2};
 use proc_macro_error::{abort, abort_call_site, emit_error};
-use quote::{quote, ToTokens, TokenStreamExt};
+use quote::{quote, ToTokens};
 use syn::{
-    punctuated::Punctuated, Attribute, Fields, FieldsNamed, FieldsUnnamed, GenericArgument,
-    PathArguments, PathSegment, Type, TypePath, Variant,
+    Attribute, Fields, FieldsNamed, FieldsUnnamed, GenericArgument, PathArguments, PathSegment,
+    Type, TypePath, Variant,
 };
 
 use crate::{
     attribute::{parse_component_attribute, AttributeType, CommentAttributes, ComponentAttribute},
     component_type::{ComponentFormat, ComponentType},
+    ValueArray,
 };
 
 pub(crate) fn impl_component(data: syn::Data, attrs: Vec<syn::Attribute>) -> TokenStream2 {
@@ -184,7 +185,7 @@ impl<'a> ComponentVariant<'a> {
             .iter()
             .filter(|variant| matches!(variant.fields, Fields::Unit))
             .map(|variant| variant.ident.to_string())
-            .collect::<EnumValues>();
+            .collect::<ValueArray<String>>();
 
         tokens.extend(quote! {
             utoipa::openapi::Property::new(ComponentType::String)
@@ -212,37 +213,6 @@ impl<'a> ComponentVariant<'a> {
     }
 }
 
-/// Tokenizes slice reference (`&[...]`) correctly to OpenAPI JSON.
-struct EnumValues(Vec<String>);
-
-impl FromIterator<String> for EnumValues {
-    fn from_iter<T: IntoIterator<Item = String>>(iter: T) -> Self {
-        Self {
-            0: iter.into_iter().collect::<Vec<_>>(),
-        }
-    }
-}
-
-impl ToTokens for EnumValues {
-    fn to_tokens(&self, tokens: &mut TokenStream2) {
-        tokens.append(Punct::new('&', proc_macro2::Spacing::Joint));
-        let items = self
-            .0
-            .iter()
-            .fold(Punctuated::new(), |mut punctuated, item| {
-                punctuated.push_value(item);
-                punctuated.push_punct(Punct::new(',', proc_macro2::Spacing::Alone));
-
-                punctuated
-            });
-
-        tokens.append(Group::new(
-            proc_macro2::Delimiter::Bracket,
-            items.to_token_stream(),
-        ));
-    }
-}
-
 fn append_attributes<I: Iterator<Item = AttributeType>>(
     token_stream: &mut TokenStream2,
     component_attribute: I,
diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs
index fe20ed3..180e401 100644
--- a/utoipa-gen/src/lib.rs
+++ b/utoipa-gen/src/lib.rs
@@ -8,9 +8,9 @@ use ext::actix::update_parameters_from_arguments;
 
 use ext::{ArgumentResolver, PathOperationResolver, PathOperations, PathResolver};
 use proc_macro::TokenStream;
-use quote::{format_ident, quote, quote_spanned, ToTokens};
+use quote::{format_ident, quote, quote_spanned, ToTokens, TokenStreamExt};
 
-use proc_macro2::{Ident, TokenStream as TokenStream2};
+use proc_macro2::{Group, Ident, Punct, TokenStream as TokenStream2};
 use syn::{
     bracketed,
     parse::{Parse, ParseStream},
@@ -25,7 +25,9 @@ mod component_type;
 mod ext;
 mod info;
 mod path;
+mod property;
 mod request_body;
+mod response;
 
 use proc_macro_error::*;
 
@@ -334,6 +336,44 @@ fn impl_paths<I: IntoIterator<Item = ExprPath>>(
     )
 }
 
+/// Tokenizes slice or Vec of tokenizable items as slice reference (`&[...]`) correctly to OpenAPI JSON.
+struct ValueArray<V>(Vec<V>)
+where
+    V: Sized + ToTokens;
+
+impl<V> FromIterator<V> for ValueArray<V>
+where
+    V: Sized + ToTokens,
+{
+    fn from_iter<T: IntoIterator<Item = V>>(iter: T) -> Self {
+        Self {
+            0: iter.into_iter().collect::<Vec<_>>(),
+        }
+    }
+}
+
+impl<T> ToTokens for ValueArray<T>
+where
+    T: Sized + ToTokens,
+{
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        tokens.append(Punct::new('&', proc_macro2::Spacing::Joint));
+
+        tokens.append(Group::new(
+            proc_macro2::Delimiter::Bracket,
+            self.0
+                .iter()
+                .fold(Punctuated::new(), |mut punctuated, item| {
+                    punctuated.push_value(item);
+                    punctuated.push_punct(Punct::new(',', proc_macro2::Spacing::Alone));
+
+                    punctuated
+                })
+                .to_token_stream(),
+        ));
+    }
+}
+
 enum Deprecated {
     True,
     False,
diff --git a/utoipa-gen/src/path.rs b/utoipa-gen/src/path.rs
index 80cad06..1e86d11 100644
--- a/utoipa-gen/src/path.rs
+++ b/utoipa-gen/src/path.rs
@@ -8,82 +8,61 @@ use syn::{
     parse::{Parse, ParseStream},
     parse2,
     punctuated::Punctuated,
-    token::{Bracket, Token},
-    LitInt, LitStr, Token,
+    token::Bracket,
+    LitBool, LitInt, LitStr, Token,
 };
 
 use crate::{
     component_type::{ComponentFormat, ComponentType},
     request_body::RequestBodyAttr,
+    response::{Response, Responses},
     Deprecated, Required,
 };
 
 const PATH_STRUCT_PREFIX: &str = "__path_";
 
-// #[utoipa::path(delete,
-//    operation_id = "custom_operation_id",
-//    path = "/custom/path/{id}/{digest}",
-//    tag = "groupping_tag"
-//    request_body = [Foo]
-//    responses = [
-//     (status = 200, description = "delete foo entity successful",
-//          body = String, content_type = "text/plain"),
-//     (status = 500, description = "internal server error",
-//          body = String, content_type = "text/plain")
-//     (400, "my bad error", u64),
-//     (404, "vault not found"),
-//     (status = 500, description = "internal server error", body = String, content_type = "text/plain")
-//    ],
-//    params = [
-//      ("myval" = String, description = "this is description"),
-//      ("myval", description = "this is description"),
-//      ("myval" = String, path, required, deprecated, description = "this is description"),
-//    ]
-// )]
-
-// #[utoipa::response(
-//      status = 200,
-//      description = "success response",
-//      body = String,
-//      content_type = "text/plain"
-// )]
-// #[utoipa::response(
-//      status = 400,
-//      description = "this is bad request",
-//      body = String,
-//      content_type = "application/json"
-// )]
-// #[utoipa::response(
-//      status = 500,
-//      description = "internal server error",
-//      body = Error,
-//      content_type = "text/plain"
-// )]
-// #[utoipa::response(
-//      status = 404,
-//      description = "item not found",
-//      body = i32 // because body type is primitive the content_type is not necessary
-// )]
-// implementation should make assumptions based on response body type. If response body type is primitive type
-// content_type is set to text/pain by default
-
-/// PathAttr is parsed #[path(...)] proc macro and its attributes.
+/// PathAttr is parsed `#[utoipa::path(...)]` proc macro and its attributes.
 /// Parsed attributes can be used to override or append OpenAPI Path
 /// options.
+///
+/// # Example
+/// ```text
+/// #[utoipa::path(delete,
+///    operation_id = "custom_operation_id",
+///    path = "/custom/path/{id}/{digest}",
+///    tag = "groupping_tag"
+///    request_body = [Foo]
+///    responses = [
+///         (status = 200, description = "success update Foos", body = [Foo], content_type = "application/json",
+///             headers = [
+///                 ("fooo-bar" = String, description = "custom header value")
+///             ]
+///         ),
+///         (status = 500, description = "internal server error", body = String, content_type = "text/plain",
+///             headers = [
+///                 ("fooo-bar" = String, description = "custom header value")
+///             ]
+///         ),
+///    ],
+///    params = [
+///      ("id" = u64, description = "Id of Foo"),
+///      ("digest", description = "Foos message digest of last updated"),
+///      ("x-csrf-token", header, required, deprecated),
+///    ]
+/// )]
+/// ```
 #[derive(Default)]
 #[cfg_attr(feature = "debug", derive(Debug))]
 pub struct PathAttr {
     path_operation: Option<PathOperation>,
     request_body: Option<RequestBodyAttr>,
-    responses: Vec<PathResponse>,
+    responses: Vec<Response>,
     path: Option<String>,
     operation_id: Option<String>,
     tag: Option<String>,
     pub params: Option<Vec<Parameter>>,
 }
 
-/// Parse implementation for PathAttr will parse arguments
-/// exhaustively.
 impl Parse for PathAttr {
     fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
         let mut path_attr = PathAttr::default();
@@ -139,8 +118,8 @@ impl Parse for PathAttr {
                         .expect_or_abort("expected responses to be group separated by comma (,)");
 
                     path_attr.responses = groups
-                        .iter()
-                        .map(|group| parse2::<PathResponse>(group.stream()).unwrap_or_abort())
+                        .into_iter()
+                        .map(|group| syn::parse2::<Response>(group.stream()).unwrap_or_abort())
                         .collect::<Vec<_>>();
                 }
                 "params" => {
@@ -262,52 +241,6 @@ impl ToTokens for PathOperation {
     }
 }
 
-/// Parsed representation of response argument within `#[path(...)]` macro attribute.
-/// Response is typically formed from group such like (200, "success", String) where
-///   * 200 number represents http status code
-///   * "success" stands for response description included in documentation
-///   * String represents type of response body
-#[derive(Default)]
-#[cfg_attr(feature = "debug", derive(Debug))]
-struct PathResponse {
-    status_code: i32,
-    message: String,
-    response_type: Option<Ident>,
-}
-
-impl Parse for PathResponse {
-    fn parse(input: ParseStream) -> syn::Result<Self> {
-        let mut response = PathResponse::default();
-
-        loop {
-            let next_type = input.lookahead1();
-            if next_type.peek(LitInt) {
-                response.status_code = input
-                    .parse::<LitInt>()
-                    .unwrap()
-                    .base10_parse()
-                    .unwrap_or_abort();
-            } else if next_type.peek(LitStr) {
-                response.message = input.parse::<LitStr>().unwrap().value();
-            } else if next_type.peek(syn::Ident) {
-                response.response_type = Some(input.parse::<syn::Ident>().unwrap());
-            } else {
-                return Err(next_type.error());
-            }
-
-            if input.peek(Token![,]) {
-                input.parse::<Token![,]>().unwrap();
-            }
-
-            if input.is_empty() {
-                break;
-            }
-        }
-
-        Ok(response)
-    }
-}
-
 /// Parameter of request suchs as in path, header, query or cookie
 ///
 /// For example path `/users/{id}` the path parameter is used to define
@@ -376,6 +309,18 @@ impl Parse for Parameter {
             input.parse::<Token![,]>().unwrap();
         }
 
+        let parse_bool = |input: ParseStream| {
+            // support assign form as: required = bool
+            if input.peek(Token![=]) && input.peek2(LitBool) {
+                input.parse::<Token![=]>().unwrap();
+
+                input.parse::<LitBool>().unwrap().value()
+            } else {
+                // quick form as: required
+                true
+            }
+        };
+
         loop {
             let ident = input.parse::<syn::Ident>().unwrap();
             let name = &*ident.to_string();
@@ -386,8 +331,8 @@ impl Parse for Parameter {
                         parameter.required = true; // all path parameters are required by default
                     }
                 }
-                "required" => parameter.required = true,
-                "deprecated" => parameter.deprecated = true,
+                "required" => parameter.required = parse_bool(input),
+                "deprecated" => parameter.deprecated = parse_bool(input),
                 "description" => {
                     if input.peek(Token![=]) {
                         input.parse::<Token![=]>().unwrap();
@@ -584,6 +529,7 @@ impl ToTokens for Path {
             description: self.doc_comments.as_ref(),
             parameters: self.path_attr.params.as_ref(),
             request_body: self.path_attr.request_body.as_ref(),
+            responses: self.path_attr.responses.as_ref(),
         };
 
         tokens.extend(quote! {
@@ -620,6 +566,7 @@ struct Operation<'a> {
     deprecated: &'a Option<bool>,
     parameters: Option<&'a Vec<Parameter>>,
     request_body: Option<&'a RequestBodyAttr>,
+    responses: &'a Vec<Response>,
 }
 
 impl ToTokens for Operation<'_> {
@@ -632,12 +579,9 @@ impl ToTokens for Operation<'_> {
             })
         }
 
-        // impl dummy responses
+        let responses = Responses(self.responses);
         tokens.extend(quote! {
-            .with_response(
-                "200", // TODO resolve this status
-                utoipa::openapi::response::Response::new("this is response message")
-            )
+            .with_responses(#responses)
         });
         //         // .with_security()
         let path_struct = format_ident!("{}{}", PATH_STRUCT_PREFIX, self.fn_name);
diff --git a/utoipa-gen/src/property.rs b/utoipa-gen/src/property.rs
new file mode 100644
index 0000000..ae66256
--- /dev/null
+++ b/utoipa-gen/src/property.rs
@@ -0,0 +1,54 @@
+use proc_macro2::Ident;
+use quote::{quote, ToTokens};
+
+use crate::component_type::{ComponentFormat, ComponentType};
+
+/// Tokenizable object property. It is used as a object property for components or as property
+/// of request or response body or response header.
+pub(crate) struct Property<'a> {
+    pub(crate) is_array: bool,
+    pub(crate) component_type: ComponentType<'a>,
+}
+
+impl<'a> Property<'a> {
+    pub fn new(is_array: bool, ident: &'a Ident) -> Self {
+        Self {
+            is_array,
+            component_type: ComponentType(ident),
+        }
+    }
+}
+
+impl ToTokens for Property<'_> {
+    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
+        if self.component_type.is_primitive() {
+            let component_type = &self.component_type;
+            let mut component = quote! {
+                utoipa::openapi::Property::new(
+                    #component_type
+                )
+            };
+
+            let format = ComponentFormat(self.component_type.0);
+            if format.is_known_format() {
+                component.extend(quote! {
+                    .with_format(#format)
+                })
+            }
+
+            tokens.extend(component);
+        } else {
+            let name = &*self.component_type.0.to_string();
+
+            tokens.extend(quote! {
+                utoipa::openapi::Ref::from_component_name(#name)
+            })
+        };
+
+        if self.is_array {
+            tokens.extend(quote! {
+                .to_array()
+            });
+        }
+    }
+}
diff --git a/utoipa-gen/src/request_body.rs b/utoipa-gen/src/request_body.rs
index c0452b8..3a9bb23 100644
--- a/utoipa-gen/src/request_body.rs
+++ b/utoipa-gen/src/request_body.rs
@@ -7,10 +7,7 @@ use syn::{
     LitBool, LitStr, Token,
 };
 
-use crate::{
-    component_type::{ComponentFormat, ComponentType},
-    MediaType, Required,
-};
+use crate::{property::Property, MediaType, Required};
 
 /// Parsed information related to requst body of path.
 ///
@@ -128,42 +125,11 @@ impl Parse for RequestBodyAttr {
 
 impl ToTokens for RequestBodyAttr {
     fn to_tokens(&self, tokens: &mut TokenStream2) {
-        // TODO refactor component type & format to its own type
-        let body_type = self.content.ty.as_ref().unwrap();
-        let component_type = ComponentType(body_type);
-
-        let mut component = if component_type.is_primitive() {
-            let mut component = quote! {
-                utoipa::openapi::Property::new(
-                    #component_type
-                )
-            };
-
-            let format = ComponentFormat(body_type);
-            if format.is_known_format() {
-                component.extend(quote! {
-                    .with_format(#format)
-                })
-            }
-
-            component
-        } else {
-            let name = &*body_type.to_string();
-
-            quote! {
-                utoipa::openapi::Ref::from_component_name(#name)
-            }
-        };
-
-        if self.content.is_array {
-            component.extend(quote! {
-                .to_array()
-            });
-        }
+        let property = Property::new(self.content.is_array, self.content.ty.as_ref().unwrap());
 
         let content_type = if let Some(ref content_type) = self.content_type {
             content_type
-        } else if component_type.is_primitive() {
+        } else if property.component_type.is_primitive() {
             "text/plain"
         } else {
             "application/json"
@@ -171,7 +137,7 @@ impl ToTokens for RequestBodyAttr {
 
         tokens.extend(quote! {
             utoipa::openapi::request_body::RequestBody::new()
-                .with_content(#content_type, utoipa::openapi::request_body::Content::new(#component))
+                .with_content(#content_type, #property)
         });
 
         if let Some(required) = self.required {
diff --git a/utoipa-gen/src/response.rs b/utoipa-gen/src/response.rs
new file mode 100644
index 0000000..c347e4d
--- /dev/null
+++ b/utoipa-gen/src/response.rs
@@ -0,0 +1,332 @@
+use proc_macro2::{Group, Ident, TokenStream as TokenStream2};
+use proc_macro_error::ResultExt;
+use quote::{quote, ToTokens};
+use syn::{
+    bracketed,
+    parse::{Parse, ParseStream},
+    punctuated::Punctuated,
+    token::Comma,
+    LitInt, LitStr, Token,
+};
+
+use crate::{property::Property, MediaType};
+
+/// Parsed representation of response attributes from `#[utoipa::path]` attribute.
+///
+/// Configuration options:
+///   * **status** Http status code of the response e.g. `200`
+///   * **description** Description of the response
+///   * **body** Optional response body type. Can be primitive, struct or enum type and slice types are supported
+///     by wrapping the type with brackets e.g. `[Foo]`
+///   * **content_type** Optional content type of the response e.g. `"text/plain"`
+///   * **headers** Optional response headers. See [`Header`] for detailed description and usage
+///
+/// Only status and description are mandatory for describing response. Responses which does not
+/// define `body = type` are treated as they would not return any response back. Content type of
+/// responses will be if not provided determined automatically suggesting that any primitive type such as
+/// integer, string or boolean are treated as `"text/plain"` and struct types are treated as `"application/json"`.
+///
+/// # Examples
+///
+/// Minimal example example providing responses.
+/// ```text
+/// #[utoipa::path(
+///     ...
+///     responses = [
+///         (status = 200, description = "success response"),
+///     ]
+/// )]
+/// ```
+///
+/// Example with all supported configuration.
+/// ```text
+/// #[utoipa::path(
+///     ...
+///     responses = [
+///         (status = 200, description = "success response", body = [Foo], content_type = "text/xml",
+///             headers = [
+///                 ("xrfs-token" = String, description = "New csrf token sent back in response header")
+///             ]
+///         ),
+///     ]
+/// )]
+/// ```
+///
+/// Example with multiple responses.
+/// ```text
+/// #[utoipa::path(
+///     ...
+///     responses = [
+///         (status = 200, description = "success response", body = [Foo]),
+///         (status = 401, description = "unauthorized to access", body = UnautorizedError),
+///         (status = 404, description = "foo not found", body = NotFoundError),
+///     ]
+/// )]
+/// ```
+#[derive(Default)]
+#[cfg_attr(feature = "debug", derive(Debug))]
+pub struct Response {
+    status_code: i32,
+    description: String,
+    response_type: Option<MediaType>,
+    content_type: Option<String>,
+    headers: Vec<Header>,
+}
+
+impl Parse for Response {
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let mut response = Response::default();
+
+        loop {
+            let ident = input
+                .parse::<Ident>()
+                .expect_or_abort("unparseable response expected to find Ident");
+            let name = &*ident.to_string();
+
+            match name {
+                "status" => {
+                    response.status_code = parse_next(&input, || {
+                        input
+                            .parse::<LitInt>()
+                            .unwrap()
+                            .base10_parse()
+                            .unwrap_or_abort()
+                    });
+                }
+                "description" => {
+                    response.description =
+                        parse_next(&input, || input.parse::<LitStr>().unwrap_or_abort().value());
+                }
+                "body" => {
+                    response.response_type = Some(parse_next(&input, || {
+                        input.parse::<MediaType>().unwrap_or_abort()
+                    }));
+                }
+                "content_type" => {
+                    response.content_type = Some(parse_next(&input, || {
+                        input.parse::<LitStr>().unwrap_or_abort().value()
+                    }));
+                }
+                "headers" => {
+                    let groups = parse_next(&input, || {
+                        let content;
+                        bracketed!(content in input);
+                        Punctuated::<Group, Comma>::parse_terminated(&content)
+                    })
+                    .expect_or_abort("expected headers in brackets [..]");
+
+                    response.headers = groups
+                        .into_iter()
+                        .map(|group| syn::parse2::<Header>(group.stream()).unwrap_or_abort())
+                        .collect::<Vec<_>>();
+                }
+                _ => {
+                    let error_msg = format!(
+                        "unexpected attribute: {}, 
+                    expected values: status, description, body, content_type, headers",
+                        &name
+                    );
+                    return Err(input.error(error_msg));
+                }
+            }
+
+            if input.peek(Token![,]) {
+                input.parse::<Token![,]>().unwrap_or_abort();
+            }
+            if input.is_empty() {
+                break;
+            }
+        }
+
+        Ok(response)
+    }
+}
+
+#[inline]
+fn parse_next<T: Sized>(input: &ParseStream, next: impl FnOnce() -> T) -> T {
+    input
+        .parse::<Token![=]>()
+        .expect_or_abort("expected euqals sign token (=)");
+    next()
+}
+
+pub struct Responses<'a>(pub &'a [Response]);
+
+impl ToTokens for Responses<'_> {
+    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
+        tokens.extend(quote! { utoipa::openapi::Responses::new() });
+
+        self.0.iter().for_each(|response| {
+            let status = response.status_code.to_string();
+            let description = &response.description;
+
+            let mut response_tokens = quote! {
+                utoipa::openapi::Response::new(#description)
+            };
+
+            if let Some(ref response_body_type) = response.response_type {
+                let body_type = response_body_type.ty.as_ref().unwrap();
+
+                let component = Property::new(response_body_type.is_array, body_type);
+                let content_type = if let Some(ref content_type) = response.content_type {
+                    content_type
+                } else if component.component_type.is_primitive() {
+                    "text/plain"
+                } else {
+                    "application/json"
+                };
+
+                response_tokens.extend(quote! {
+                    .with_content(#content_type, #component)
+                })
+            }
+
+            response.headers.iter().for_each(|header| {
+                let name = &header.name;
+                let header_tokens = new_header_tokens(header);
+
+                response_tokens.extend(quote! {
+                    .with_header(#name, #header_tokens)
+                })
+            });
+
+            tokens.extend(quote! {
+                .with_response(#status, #response_tokens)
+            });
+        })
+    }
+}
+
+#[inline]
+fn new_header_tokens(header: &Header) -> TokenStream2 {
+    let mut header_tokens = if let Some(ref header_type) = header.media_type {
+        // header property with custom type
+        let header_type = Property::new(header_type.is_array, header_type.ty.as_ref().unwrap());
+
+        quote! {
+            utoipa::openapi::Header::new(#header_type)
+        }
+    } else {
+        // default header (string type)
+        quote! {
+            utoipa::openapi::Header::default()
+        }
+    };
+
+    if let Some(ref description) = header.description {
+        header_tokens.extend(quote! {
+            .with_description(#description)
+        })
+    }
+
+    header_tokens
+}
+
+/// Parsed representation of response header defined in `#[utoipa::path(..)]` attribute.
+///
+/// Supported configuration format is `("x-my-header-name" = type, description = "optional description of header")`.
+/// The `= type` and the `descripiton = ".."` are optional configurations thus so the same configuration
+/// could be written as follows: `("x-my-header-name")`.
+///
+/// The `type` can be any typical type supported as a header argument such as `String, i32, u64, bool` etc.
+/// and if not provided it will default to `String`.
+///
+/// # Examples
+///
+/// Example of 200 success response which does return nothing back in response body, but returns a
+/// new csrf token in response headers.
+/// ```text
+/// #[utoipa::path(
+///     ...
+///     responses = [
+///         (status = 200, description = "success response",
+///             headers = [
+///                 ("xrfs-token" = String, description = "New csrf token sent back in response header")
+///             ]
+///         ),
+///     ]
+/// )]
+/// ```
+///
+/// Example with default values.
+/// ```text
+/// #[utoipa::path(
+///     ...
+///     responses = [
+///         (status = 200, description = "success response",
+///             headers = [
+///                 ("xrfs-token")
+///             ]
+///         ),
+///     ]
+/// )]
+/// ```
+///
+/// Example with multiplea headers with default values.
+/// ```text
+/// #[utoipa::path(
+///     ...
+///     responses = [
+///         (status = 200, description = "success response",
+///             headers = [
+///                 ("xrfs-token"),
+///                 ("another-header"),
+///             ]
+///         ),
+///     ]
+/// )]
+/// ```
+#[derive(Default)]
+#[cfg_attr(feature = "debug", derive(Debug))]
+struct Header {
+    name: String,
+    media_type: Option<MediaType>,
+    description: Option<String>,
+}
+
+impl Parse for Header {
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let mut header = Header {
+            name: input
+                .parse::<LitStr>()
+                .expect_or_abort("unexpected attribute for Header name, expected LitStr")
+                .value(),
+            ..Default::default()
+        };
+
+        if input.peek(Token![=]) {
+            input.parse::<Token![=]>().unwrap_or_abort();
+
+            header.media_type = Some(input.parse::<MediaType>().unwrap_or_abort());
+        }
+
+        if input.peek(Token![,]) {
+            input.parse::<Token![,]>().unwrap_or_abort();
+        }
+
+        if input.peek(syn::Ident) {
+            let description = input
+                .parse::<Ident>()
+                .expect_or_abort("unexpected attribute for Header description, expected Ident");
+
+            if description == "description" {
+                if input.peek(Token![=]) {
+                    input.parse::<Token![=]>().unwrap_or_abort();
+                }
+
+                let description = input.parse::<LitStr>().unwrap_or_abort().value();
+                header.description = Some(description);
+            } else {
+                return Err(syn::Error::new(
+                    description.span(),
+                    format!(
+                        "unexpected attribute: {}, expected: description",
+                        description
+                    ),
+                ));
+            }
+        }
+
+        Ok(header)
+    }
+}


Test patch:
diff --git a/tests/path_derive.rs b/tests/path_derive.rs
index 0e4d946..0532bc0 100644
--- a/tests/path_derive.rs
+++ b/tests/path_derive.rs
@@ -27,15 +27,15 @@ macro_rules! test_api_fn {
         mod $module {
             $( $(#[$meta])* )*
             #[utoipa::path(
-                                                                    $operation,
-                                                                   $( operation_id = $operation_id, )*
-                                                                    path = $path,
-                                                                    responses = [
-                                                                        (200, "success", String),
-                                                                    ],
-                                                                   $( params = $params, )*
-                                                                   $( tag = $tag, )*
-                                                                )]
+                $operation,
+                $( operation_id = $operation_id, )*
+                path = $path,
+                responses = [
+                    (status = 200, description = "success response")
+                ],
+                $( params = $params, )*
+                $( tag = $tag, )*
+            )]
             #[allow(unused)]
             async fn $name() -> String {
                 "foo".to_string()
@@ -153,7 +153,7 @@ fn derive_path_with_defaults_success() {
     get,
     path = "/foo/{id}",
     responses = [
-        (200, "success", String)
+        (status = 200, description = "success response")
     ],
     params = [
         ("id" = u64, description = "Foo database id"),
diff --git a/tests/path_derive_actix.rs b/tests/path_derive_actix.rs
index eb0c6bc..3a38b83 100644
--- a/tests/path_derive_actix.rs
+++ b/tests/path_derive_actix.rs
@@ -14,7 +14,7 @@ mod mod_derive_path_actix {
     /// Get foo by id long description
     #[utoipa::path(
         responses = [
-            (200, "success", String),
+            (status = 200, description = "success response")
         ],
         params = [
             ("id", description = "Foo id"),
@@ -57,7 +57,7 @@ mod mod_derive_path_unnamed_regex_actix {
     /// Get foo by id long description
     #[utoipa::path(
         responses = [
-            (200, "success", String),
+            (status = 200, description = "success"),
         ],
         params = [
             ("arg0", description = "Foo path unnamed regex tail")
@@ -100,7 +100,7 @@ mod mod_derive_path_named_regex_actix {
     /// Get foo by id long description
     #[utoipa::path(
         responses = [
-            (200, "success", String),
+            (status = 200, description = "success response")
         ],
         params = [
             ("tail", description = "Foo path named regex tail")
@@ -143,7 +143,7 @@ macro_rules! test_derive_path_operations {
 
             #[utoipa::path(
                 responses = [
-                    (200, "success", String),
+                    (status = 200, description = "success response")
                 ]
             )]
             #[$operation("/foo")]
diff --git a/tests/path_parameter_derive_actix.rs b/tests/path_parameter_derive_actix.rs
index 25874aa..9dca2fb 100644
--- a/tests/path_parameter_derive_actix.rs
+++ b/tests/path_parameter_derive_actix.rs
@@ -15,7 +15,7 @@ mod derive_params_multiple_actix {
         get,
         path = "/foo/{id}/{digest}",
         responses = [
-            (200, "success", String),
+            (status = 200, description = "success response")
         ],
         params = [
             ("id", description = "Foo id"),
@@ -68,7 +68,7 @@ mod derive_parameters_multiple_no_matching_names_actix {
         get,
         path = "/foo/{id}/{digest}",
         responses = [
-            (200, "success", String),
+            (status = 200, description = "success response")
         ],
         params = [
             ("id" = i32, description = "Foo id"),
@@ -119,7 +119,7 @@ mod derive_params_from_method_args_actix {
         get,
         path = "/foo/{id}/{digest}",
         responses = [
-            (200, "success", String),
+            (status = 200, description = "success response")
         ],
     )]
     #[allow(unused)]
diff --git a/tests/path_parameter_derive_test.rs b/tests/path_parameter_derive_test.rs
index 0253360..19e3e16 100644
--- a/tests/path_parameter_derive_test.rs
+++ b/tests/path_parameter_derive_test.rs
@@ -14,7 +14,7 @@ mod derive_params_all_options {
         get,
         path = "/foo/{id}",
         responses = [
-            (200, "success", String),
+            (status = 200, description = "success"),
         ],
         params = [
             ("id" = i32, path, required, deprecated, description = "Search foos by ids"),
@@ -55,7 +55,7 @@ mod derive_params_minimal {
         get,
         path = "/foo/{id}",
         responses = [
-            (200, "success", String),
+            (status = 200, description = "success"),
         ],
         params = [
             ("id" = i32, description = "Search foos by ids"),
@@ -96,7 +96,7 @@ mod derive_params_multiple {
         get,
         path = "/foo/{id}/{digest}",
         responses = [
-            (200, "success", String),
+            (status = 200, description = "success"),
         ],
         params = [
             ("id" = i32, description = "Foo id"),
@@ -146,7 +146,7 @@ mod mod_derive_parameters_all_types {
         get,
         path = "/foo/{id}",
         responses = [
-            (200, "success", String),
+            (status = 200, description = "success"),
         ],
         params = [
             ("id" = i32, path, description = "Foo id"),
@@ -222,7 +222,7 @@ mod derive_params_without_args {
         get,
         path = "/foo/{id}",
         responses = [
-            (200, "success", String),
+            (status = 200, description = "success"),
         ],
         params = [
             ("id" = i32, path, description = "Foo id"),
diff --git a/tests/path_response_derive_test.rs b/tests/path_response_derive_test.rs
new file mode 100644
index 0000000..e1e7046
--- /dev/null
+++ b/tests/path_response_derive_test.rs
@@ -0,0 +1,175 @@
+mod common;
+
+macro_rules! test_fn {
+    ( module: $name:ident, responses: $responses:expr ) => {
+        #[allow(unused)]
+        mod $name {
+            #[utoipa::path(get,path = "/foo",responses = $responses)]
+            fn get_foo() {}
+        }
+    };
+}
+
+macro_rules! api_doc {
+    ( module: $module:expr ) => {{
+        use utoipa::OpenApi;
+        #[derive(OpenApi, Default)]
+        #[openapi(handler_files = [], handlers = [$module::get_foo])]
+        struct ApiDoc;
+
+        let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap();
+        common::get_json_path(&doc, "paths./foo.get").clone()
+    }};
+}
+
+test_fn! {
+    module: simple_success_response,
+    responses: [
+        (status = 200, description = "success")
+    ]
+}
+
+#[test]
+fn derive_path_with_simple_success_response() {
+    let doc = api_doc!(module: simple_success_response);
+
+    assert_value! {doc=>
+        "responses.200.description" = r#""success""#, "Response description"
+        "responses.200.content" = r#"null"#, "Response content"
+        "responses.200.headers" = r#"null"#, "Response headers"
+    }
+}
+
+test_fn! {
+    module: multiple_simple_responses,
+    responses: [
+        (status = 200, description = "success"),
+        (status = 401, description = "unauthorized"),
+        (status = 404, description = "not found"),
+        (status = 500, description = "server error")
+    ]
+}
+
+#[test]
+fn derive_path_with_multiple_simple_responses() {
+    let doc = api_doc!(module: multiple_simple_responses);
+
+    assert_value! {doc=>
+        "responses.200.description" = r#""success""#, "Response description"
+        "responses.200.content" = r#"null"#, "Response content"
+        "responses.200.headers" = r#"null"#, "Response headers"
+        "responses.401.description" = r#""unauthorized""#, "Response description"
+        "responses.401.content" = r#"null"#, "Response content"
+        "responses.401.headers" = r#"null"#, "Response headers"
+        "responses.404.description" = r#""not found""#, "Response description"
+        "responses.404.content" = r#"null"#, "Response content"
+        "responses.404.headers" = r#"null"#, "Response headers"
+        "responses.500.description" = r#""server error""#, "Response description"
+        "responses.500.content" = r#"null"#, "Response content"
+        "responses.500.headers" = r#"null"#, "Response headers"
+    }
+}
+
+macro_rules! test_response_types {
+    ( $( $name:ident=> $(body: $expected:expr,)? $( $content_type:literal, )? $( headers: $headers:expr, )?
+        assert: $( $path:literal = $expection:literal, $comment:literal )* )* ) => {
+        $(
+            paste::paste! {
+                test_fn! {
+                    module: [<mod_ $name>],
+                    responses: [
+                        (status = 200, description = "success",
+                            $(body = $expected ,)*
+                            $(content_type = $content_type,)*
+                            $(headers = $headers)*),
+                    ]
+                }
+            }
+
+            #[test]
+            fn $name() {
+                paste::paste! {
+                    let doc = api_doc!(module: [<mod_ $name>]);
+                }
+
+                assert_value! {doc=>
+                    "responses.200.description" = r#""success""#, "Response description"
+                    $($path = $expection, $comment)*
+                }
+            }
+        )*
+    };
+}
+
+#[allow(unused)]
+struct Foo {
+    name: String,
+}
+
+test_response_types! {
+primitive_string_body => body: String, assert:
+    "responses.200.content.text/plain.schema.type" = r#""string""#, "Response content type"
+    "responses.200.headers" = r###"null"###, "Response headers"
+primitive_string_sclice_body => body: [String], assert:
+    "responses.200.content.text/plain.schema.items.type" = r#""string""#, "Response content items type"
+    "responses.200.content.text/plain.schema.type" = r#""array""#, "Response content type"
+    "responses.200.headers" = r###"null"###, "Response headers"
+primitive_integer_slice_body => body: [i32], assert:
+    "responses.200.content.text/plain.schema.items.type" = r#""integer""#, "Response content items type"
+    "responses.200.content.text/plain.schema.items.format" = r#""int32""#, "Response content items format"
+    "responses.200.content.text/plain.schema.type" = r#""array""#, "Response content type"
+    "responses.200.headers" = r###"null"###, "Response headers"
+primitive_integer_body => body: i64, assert:
+    "responses.200.content.text/plain.schema.type" = r#""integer""#, "Response content type"
+    "responses.200.content.text/plain.schema.format" = r#""int64""#, "Response content format"
+    "responses.200.headers" = r###"null"###, "Response headers"
+primitive_big_integer_body => body: u128, assert:
+    "responses.200.content.text/plain.schema.type" = r#""integer""#, "Response content type"
+    "responses.200.content.text/plain.schema.format" = r#"null"#, "Response content format"
+    "responses.200.headers" = r###"null"###, "Response headers"
+primitive_bool_body => body: bool, assert:
+    "responses.200.content.text/plain.schema.type" = r#""boolean""#, "Response content type"
+    "responses.200.headers" = r###"null"###, "Response headers"
+object_body => body: Foo, assert:
+    "responses.200.content.application/json.schema.$ref" = r###""#/components/schemas/Foo""###, "Response content type"
+    "responses.200.headers" = r###"null"###, "Response headers"
+object_slice_body => body: [Foo], assert:
+    "responses.200.content.application/json.schema.type" = r###""array""###, "Response content type"
+    "responses.200.content.application/json.schema.items.$ref" = r###""#/components/schemas/Foo""###, "Response content items type"
+    "responses.200.headers" = r###"null"###, "Response headers"
+object_body_override_content_type_to_xml => body: Foo, "text/xml", assert:
+    "responses.200.content.application/json.schema.$ref" = r###"null"###, "Response content type"
+    "responses.200.content.text/xml.schema.$ref" = r###""#/components/schemas/Foo""###, "Response content type"
+    "responses.200.headers" = r###"null"###, "Response headers"
+object_body_with_simple_header => body: Foo, headers: [
+    ("xsrf-token")
+], assert:
+    "responses.200.content.application/json.schema.$ref" = r###""#/components/schemas/Foo""###, "Response content type"
+    "responses.200.headers.xsrf-token.schema.type" = r###""string""###, "xsrf-token header type"
+    "responses.200.headers.xsrf-token.description" = r###"null"###, "xsrf-token header description"
+object_body_with_multiple_headers => body: Foo, headers: [
+    ("xsrf-token"),
+    ("another-header")
+], assert:
+    "responses.200.content.application/json.schema.$ref" = r###""#/components/schemas/Foo""###, "Response content type"
+    "responses.200.headers.xsrf-token.schema.type" = r###""string""###, "xsrf-token header type"
+    "responses.200.headers.xsrf-token.description" = r###"null"###, "xsrf-token header description"
+    "responses.200.headers.another-header.schema.type" = r###""string""###, "another-header header type"
+    "responses.200.headers.another-header.description" = r###"null"###, "another-header header description"
+object_body_with_header_with_type => body: Foo, headers: [
+    ("random-digits" = [u64]),
+], assert:
+    "responses.200.content.application/json.schema.$ref" = r###""#/components/schemas/Foo""###, "Response content type"
+    "responses.200.headers.random-digits.schema.type" = r###""array""###, "random-digits header type"
+    "responses.200.headers.random-digits.description" = r###"null"###, "random-digits header description"
+    "responses.200.headers.random-digits.schema.items.type" = r###""integer""###, "random-digits header items type"
+    "responses.200.headers.random-digits.schema.items.format" = r###""int64""###, "random-digits header items format"
+response_no_body_with_complex_header_with_description => headers: [
+    ("random-digits" = [u64], description = "Random digits response header"),
+], assert:
+    "responses.200.content" = r###"null"###, "Response content type"
+    "responses.200.headers.random-digits.description" = r###""Random digits response header""###, "random-digits header description"
+    "responses.200.headers.random-digits.schema.type" = r###""array""###, "random-digits header type"
+    "responses.200.headers.random-digits.schema.items.type" = r###""integer""###, "random-digits header items type"
+    "responses.200.headers.random-digits.schema.items.format" = r###""int64""###, "random-digits header items format"
+}
diff --git a/tests/request_body_derive_test.rs b/tests/request_body_derive_test.rs
index 714921b..660c962 100644
--- a/tests/request_body_derive_test.rs
+++ b/tests/request_body_derive_test.rs
@@ -11,13 +11,13 @@ macro_rules! test_fn {
                 name: String,
             }
             #[utoipa::path(
-                                                                post,
-                                                                path = "/foo",
-                                                                request_body = $body,
-                                                                responses = [
-                                                                    (200, "success", String),
-                                                                ]
-                                                            )]
+                                                post,
+                                                path = "/foo",
+                                                request_body = $body,
+                                                responses = [
+                                                    (status = 200, description = "success response")
+                                                ]
+                                            )]
             fn post_foo() {}
         }
     };
diff --git a/tests/utoipa_gen_test.rs b/tests/utoipa_gen_test.rs
index 9409c4a..dc9ea38 100644
--- a/tests/utoipa_gen_test.rs
+++ b/tests/utoipa_gen_test.rs
@@ -19,10 +19,10 @@ struct Foo {
 #[utoipa::path(
     request_body = (content = Foo, required, description = "foobar", content_type = "text/xml"), 
     responses = [
-        (200, "success", String),
-        (400, "my bad error", u64),
-        (404, "vault not found"),
-        (500, "internal server error")
+        (status = 200, description = "success response", body = [Foo], headers = [("xrfs-token" = u64)])
+        // (400, "my bad error", u64),
+        // (404, "vault not found"),
+        // (500, "internal server error")
     ],
      params = [
         ("ids" = [i32], query, description = "Search foos by ids"),