Repository: juhaku/utoipa
Pull request #8 (2022-01-12 23:53:39)

Feature path operation parameters
* Add path operation derive -> Support basic path operation parameters
* Add path operation parameters derive -> Support basic type of parameters in path, query, cookie, and header
* Separate tests that need actix_web and actix_extras feature flags
* Support extendability through traits
* Add actix-web extras for even better integratability
* Add tests covering path operation and parameter derive cases


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index c4dcaee..c45ddf1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,7 +11,7 @@ members = [
 [features]
 swagger_ui = ["rust-embed", "mime_guess"]
 debug = ["utoipa-gen/debug"]
-actix_gen = ["utoipa-gen/actix_gen"]
+actix_extras = ["utoipa-gen/actix_extras"]
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
@@ -23,3 +23,6 @@ rust-embed = { version = "6.2.0", optional = true }
 actix-web =  { version = "3.3.2", optional = true }
 mime_guess = { version = "2.0.3", optional = true }
 utoipa-gen = { path = "./utoipa-gen" }
+
+[dev-dependencies]
+paste = "1.0.6"
\ No newline at end of file
diff --git a/src/openapi/mod.rs b/src/openapi/mod.rs
index e6355c0..68a7186 100644
--- a/src/openapi/mod.rs
+++ b/src/openapi/mod.rs
@@ -80,6 +80,10 @@ impl OpenApi {
     pub fn to_json(&self) -> Result<String, Error> {
         serde_json::to_string(self).map_err(Error::Serde)
     }
+
+    pub fn to_pretty_json(&self) -> Result<String, Error> {
+        serde_json::to_string_pretty(self).map_err(Error::Serde)
+    }
 }
 
 #[derive(Debug, Deserialize)]
diff --git a/src/openapi/path.rs b/src/openapi/path.rs
index 620501c..a400ca2 100644
--- a/src/openapi/path.rs
+++ b/src/openapi/path.rs
@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
 use super::{
     request_body::RequestBody,
     response::{Response, Responses},
-    Deprecated, ExternalDocs, Required, Security, Server,
+    Component, Deprecated, ExternalDocs, Required, Security, Server,
 };
 
 #[non_exhaustive]
@@ -121,6 +121,7 @@ pub enum PathItemType {
     Head,
     Patch,
     Trace,
+    Connect,
 }
 
 impl Display for PathItemType {
@@ -134,6 +135,7 @@ impl Display for PathItemType {
             Self::Head => write!(f, "head"),
             Self::Patch => write!(f, "patch"),
             Self::Trace => write!(f, "trace"),
+            Self::Connect => write!(f, "connect"),
         }
     }
 }
@@ -236,7 +238,12 @@ impl Operation {
     }
 
     pub fn with_parameter(mut self, parameter: Parameter) -> Self {
-        self.parameters.as_mut().unwrap().push(parameter);
+        match self.parameters {
+            Some(ref mut parameters) => parameters.push(parameter),
+            None => {
+                self.parameters = Some(vec![parameter]);
+            }
+        }
 
         self
     }
@@ -307,6 +314,8 @@ pub struct Parameter {
     #[serde(skip_serializing_if = "Option::is_none")]
     pub deprecated: Option<Deprecated>,
     // pub allow_empty_value: bool, this is going to be removed from further open api spec releases
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub schema: Option<Component>,
 }
 
 impl Parameter {
@@ -345,6 +354,12 @@ impl Parameter {
 
         self
     }
+
+    pub fn with_schema<I: Into<Component>>(mut self, component: I) -> Self {
+        self.schema = Some(component.into());
+
+        self
+    }
 }
 
 #[derive(Deserialize, Debug, PartialEq, Clone)]
diff --git a/src/openapi/schema.rs b/src/openapi/schema.rs
index 5a1fa02..c4a9728 100644
--- a/src/openapi/schema.rs
+++ b/src/openapi/schema.rs
@@ -237,7 +237,7 @@ impl From<Array> for Component {
     }
 }
 
-trait ToArray
+pub trait ToArray
 where
     Component: From<Self>,
     Self: Sized,
diff --git a/utoipa-gen/Cargo.toml b/utoipa-gen/Cargo.toml
index c0d10d5..cab6a08 100644
--- a/utoipa-gen/Cargo.toml
+++ b/utoipa-gen/Cargo.toml
@@ -10,10 +10,12 @@ proc-macro = true
 
 [dependencies]
 proc-macro2 = "1.0.32"
-syn = "1.0.81"
+syn = { version = "1.0.81", features = ["full"] }
 quote = "1.0.10"
 proc-macro-error = "1.0.4"
+regex = { version = "1.5.4", optional = true } 
+lazy_static = { version = "1.4.0", optional = true }
 
 [features]
 debug = []
-actix_gen = []
+actix_extras = ["regex", "lazy_static"]
diff --git a/utoipa-gen/src/component_type.rs b/utoipa-gen/src/component_type.rs
index bd8cfbd..78a5e81 100644
--- a/utoipa-gen/src/component_type.rs
+++ b/utoipa-gen/src/component_type.rs
@@ -55,7 +55,7 @@ impl<'a> ToTokens for ComponentType<'a> {
 }
 
 // TODO add extendability to this so that custom types can also be tokenized?
-/// Tokenizes OpenAPI datatype format correctly by given Rust type.
+/// Tokenizes OpenAPI data type format correctly by given Rust type.
 pub(crate) struct ComponentFormat<T: Display>(pub(crate) T);
 
 impl<T: Display> ComponentFormat<T> {
diff --git a/utoipa-gen/src/ext/actix.rs b/utoipa-gen/src/ext/actix.rs
new file mode 100644
index 0000000..2b8aee2
--- /dev/null
+++ b/utoipa-gen/src/ext/actix.rs
@@ -0,0 +1,262 @@
+use lazy_static::lazy_static;
+use proc_macro2::Ident;
+use proc_macro_error::abort_call_site;
+use regex::{Captures, Regex};
+use syn::{
+    punctuated::Punctuated, token::Comma, Attribute, FnArg, GenericArgument, ItemFn, LitStr, Pat,
+    PatType, PathArguments, PathSegment, Type, TypePath,
+};
+
+use super::{
+    Argument, ArgumentIn, ArgumentResolver, PathOperationResolver, PathOperations, PathResolver,
+};
+use crate::path::{Parameter, ParameterIn};
+
+#[cfg(feature = "actix_extras")]
+impl ArgumentResolver for PathOperations {
+    fn resolve_path_arguments(fn_args: &Punctuated<FnArg, Comma>) -> Option<Vec<Argument<'_>>> {
+        if let Some((pat_type, path_segment)) = Self::find_path_pat_type_and_segment(fn_args) {
+            let names = Self::get_argument_names(pat_type);
+            let types = Self::get_argument_types(path_segment);
+
+            if names.len() != types.len() {
+                Some(
+                    types
+                        .into_iter()
+                        .map(|ty| Argument {
+                            argument_in: ArgumentIn::Path,
+                            ident: ty,
+                            name: None,
+                        })
+                        .collect::<Vec<_>>(),
+                )
+            } else {
+                Some(
+                    names
+                        .into_iter()
+                        .zip(types.into_iter())
+                        .map(|(name, ty)| Argument {
+                            argument_in: ArgumentIn::Path,
+                            ident: ty,
+                            name: Some(name.to_string()),
+                        })
+                        .collect::<Vec<_>>(),
+                )
+            }
+        } else {
+            None
+        }
+    }
+}
+
+#[cfg(feature = "actix_extras")]
+impl PathOperations {
+    fn get_type_path(ty: &Type) -> &TypePath {
+        match ty {
+            Type::Path(path) => path,
+            _ => abort_call_site!("unexpected type, expected Type::Path"), // should not get here by any means with current types
+        }
+    }
+
+    fn get_argument_names(pat_type: &PatType) -> Vec<&Ident> {
+        match pat_type.pat.as_ref() {
+            Pat::Ident(pat_ident) => {
+                vec![&pat_ident.ident]
+            }
+            Pat::TupleStruct(tuple) => tuple
+                .pat
+                .elems
+                .iter()
+                .flat_map(|pat| match pat {
+                    Pat::Ident(pat_ident) => vec![&pat_ident.ident],
+                    Pat::Tuple(tuple) => tuple
+                        .elems
+                        .iter()
+                        .map(|pat| match pat {
+                            Pat::Ident(pat_ident) => &pat_ident.ident,
+                            _ => abort_call_site!(
+                                "unexpected pat ident in Pat::Tuple expected Pat::Ident"
+                            ),
+                        })
+                        .collect(),
+                    _ => abort_call_site!("unexpected pat type expected Pat::Ident"),
+                })
+                .collect::<Vec<_>>(),
+            _ => abort_call_site!("unexpected pat type expected Pat::Ident or Pat::Tuple"),
+        }
+    }
+
+    fn get_argument_types(path_segment: &PathSegment) -> Vec<&Ident> {
+        match &path_segment.arguments {
+            PathArguments::AngleBracketed(angle_bracketed) => angle_bracketed
+                .args
+                .iter()
+                .map(|arg| match arg {
+                    GenericArgument::Type(ty) => match ty {
+                        Type::Path(path) => vec![path],
+                        Type::Tuple(tuple) => tuple.elems.iter().map(Self::get_type_path).collect(),
+                        _ => {
+                            abort_call_site!("unexpected type, expected Type::Path or Type::Tuple")
+                        } // should not get here by any means with current types
+                    },
+                    _ => {
+                        abort_call_site!(
+                            "unexpected generic argument, expected GenericArgument::Type"
+                        )
+                    }
+                })
+                .flatten()
+                .map(|type_path| type_path.path.get_ident())
+                .flatten()
+                .collect::<Vec<_>>(),
+            _ => {
+                abort_call_site!("unexpected argument type, expected Path<...> with angle brakets")
+            }
+        }
+    }
+
+    fn find_path_pat_type_and_segment(
+        fn_args: &Punctuated<FnArg, Comma>,
+    ) -> Option<(&PatType, &PathSegment)> {
+        fn_args.iter().find_map(|arg| {
+            match arg {
+                FnArg::Typed(pat_type) => {
+                    let segment = Self::get_type_path(pat_type.ty.as_ref())
+                        .path
+                        .segments
+                        .iter()
+                        .find_map(|segment| {
+                            if &*segment.ident.to_string() == "Path" {
+                                Some(segment)
+                            } else {
+                                None
+                            }
+                        });
+
+                    segment.map(|segment| (pat_type, segment))
+                }
+                _ => abort_call_site!("unexpected fn argument type, expected FnArg::Typed(...)"), // should not get here
+            }
+        })
+    }
+}
+
+#[cfg(feature = "actix_extras")]
+impl PathOperationResolver for PathOperations {
+    fn resolve_attribute(item_fn: &ItemFn) -> Option<&Attribute> {
+        item_fn.attrs.iter().find_map(|attribute| {
+            if is_valid_request_type(
+                &attribute
+                    .path
+                    .get_ident()
+                    .map(ToString::to_string)
+                    .unwrap_or_default(),
+            ) {
+                Some(attribute)
+            } else {
+                None
+            }
+        })
+    }
+}
+
+#[cfg(feature = "actix_extras")]
+impl PathResolver for PathOperations {
+    fn resolve_path(operation_attribute: &Option<&Attribute>) -> Option<String> {
+        operation_attribute.map(|attribute| {
+            let lit = attribute.parse_args::<LitStr>().unwrap();
+            format_path(&lit.value())
+        })
+    }
+}
+
+fn format_path(path: &str) -> String {
+    lazy_static! {
+        static ref RE: Regex = Regex::new(r"\{[a-zA-Z0-9_]*:[^{}]*}").unwrap();
+    }
+
+    RE.replace_all(path, |captures: &Captures| {
+        let mut capture = captures.get(0).unwrap().as_str().to_string();
+
+        if capture.contains("_:") {
+            // replace unnamed capture with generic 'arg0' name
+            "{arg0}".to_string()
+        } else if capture.contains(':') {
+            //  replace colon (:) separated regexp with empty string
+            let colon = capture.find(':').unwrap();
+            let end = capture.len() - 1;
+            capture.replace_range(colon..end, "");
+
+            capture
+        } else {
+            // otherwise return the capture itself
+            capture
+        }
+    })
+    .to_string()
+}
+
+fn is_valid_request_type(s: &str) -> bool {
+    match s {
+        "get" | "post" | "put" | "delete" | "head" | "connect" | "options" | "trace" | "patch" => {
+            true
+        }
+        _ => false,
+    }
+}
+
+#[inline]
+#[cfg(feature = "actix_extras")]
+pub fn update_parameters_from_arguments(
+    arguments: Option<Vec<Argument>>,
+    parameters: &mut Option<Vec<Parameter>>,
+) {
+    if let Some(arguments) = arguments {
+        let new_parameter = |argument: &Argument| {
+            Parameter::new(
+                &argument.name.as_ref().unwrap(),
+                argument.ident,
+                if argument.argument_in == ArgumentIn::Path {
+                    ParameterIn::Path
+                } else {
+                    ParameterIn::Query
+                },
+            )
+        };
+
+        if let Some(ref mut parameters) = parameters {
+            parameters.iter_mut().for_each(|parameter| {
+                if let Some(argument) = arguments
+                    .iter()
+                    .find(|argument| argument.name.as_ref() == Some(&parameter.name))
+                {
+                    parameter.update_parameter_type(argument.ident)
+                }
+            });
+
+            // add argument to the parameters if argument has a name and it does not exists in parameters
+            arguments
+                .iter()
+                .filter(|argument| argument.has_name())
+                .for_each(|argument| {
+                    // cannot use filter() for mutli borrow situation. :(
+                    if !parameters
+                        .iter()
+                        .any(|parameter| Some(&parameter.name) == argument.name.as_ref())
+                    {
+                        // if parameters does not contain argument
+                        parameters.push(new_parameter(argument))
+                    }
+                });
+        } else {
+            // no parameters at all, add arguments to the parameters if argument has a name
+            let mut params = Vec::with_capacity(arguments.len());
+            arguments
+                .iter()
+                .filter(|argument| argument.has_name())
+                .map(new_parameter)
+                .for_each(|parameter| params.push(parameter));
+            *parameters = Some(params);
+        }
+    }
+}
diff --git a/utoipa-gen/src/ext/mod.rs b/utoipa-gen/src/ext/mod.rs
new file mode 100644
index 0000000..390d598
--- /dev/null
+++ b/utoipa-gen/src/ext/mod.rs
@@ -0,0 +1,51 @@
+use proc_macro2::Ident;
+use syn::{punctuated::Punctuated, token::Comma, Attribute, FnArg, ItemFn};
+
+#[cfg(feature = "actix_extras")]
+pub mod actix;
+
+#[cfg_attr(feature = "debug", derive(Debug))]
+pub struct Argument<'a> {
+    pub name: Option<String>,
+    pub argument_in: ArgumentIn,
+    pub ident: &'a Ident,
+}
+
+impl Argument<'_> {
+    pub fn has_name(&self) -> bool {
+        self.name.is_some()
+    }
+}
+
+#[cfg_attr(feature = "debug", derive(Debug))]
+#[derive(PartialEq)]
+pub enum ArgumentIn {
+    Path,
+}
+
+pub trait ArgumentResolver {
+    fn resolve_path_arguments(_: &Punctuated<FnArg, Comma>) -> Option<Vec<Argument<'_>>> {
+        None
+    }
+}
+
+pub trait PathResolver {
+    fn resolve_path(_: &Option<&Attribute>) -> Option<String> {
+        None
+    }
+}
+
+pub trait PathOperationResolver {
+    fn resolve_attribute(_: &ItemFn) -> Option<&Attribute> {
+        None
+    }
+}
+
+pub struct PathOperations;
+
+#[cfg(not(feature = "actix_extras"))]
+impl ArgumentResolver for PathOperations {}
+#[cfg(not(feature = "actix_extras"))]
+impl PathResolver for PathOperations {}
+#[cfg(not(feature = "actix_extras"))]
+impl PathOperationResolver for PathOperations {}
diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs
index e76397f..48234a9 100644
--- a/utoipa-gen/src/lib.rs
+++ b/utoipa-gen/src/lib.rs
@@ -3,6 +3,10 @@
 #![warn(missing_docs)]
 #![warn(rustdoc::broken_intra_doc_links)]
 
+#[cfg(feature = "actix_extras")]
+use ext::actix::update_parameters_from_arguments;
+
+use ext::{ArgumentResolver, PathOperationResolver, PathOperations, PathResolver};
 use proc_macro::TokenStream;
 use quote::{format_ident, quote, quote_spanned};
 
@@ -15,9 +19,9 @@ use syn::{
 mod attribute;
 mod component;
 mod component_type;
+mod ext;
 mod info;
 mod path;
-mod paths;
 
 use proc_macro_error::*;
 
@@ -50,63 +54,24 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
     component.into()
 }
 
-// #[proc_macro_error]
-// #[proc_macro_attribute]
-// pub fn api_operation(attr: TokenStream, item: TokenStream) -> TokenStream {
-//     println!("Attr: {:#?}", &attr);
-//     // let input = syn::parse_macro_input!(attr as PathAttr);
-
-//     item
-// }
-
 #[proc_macro_error]
 #[proc_macro_attribute]
 /// Path attribute macro
 pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream {
-    let path_attribute = syn::parse_macro_input!(attr as PathAttr);
-
-    // println!("parsed path attribute: {:#?}", &path_attribute);
-
+    let mut path_attribute = syn::parse_macro_input!(attr as PathAttr);
     let ast_fn = syn::parse::<syn::ItemFn>(item).unwrap_or_abort();
-
-    // println!("item attrs: {:#?}", &ast_fn.attrs);
-    // println!("item block: {:#?}", &block);
-    // println!("item sig: {:#?}", &sig);
-    // println!("item vis: {:#?}", &vis);
-
     let fn_name = &*ast_fn.sig.ident.to_string();
 
-    let operation_attribute = &ast_fn.attrs.iter().find_map(|attribute| {
-        if is_valid_request_type(
-            &attribute
-                .path
-                .get_ident()
-                .map(ToString::to_string)
-                .unwrap_or_default(),
-        ) {
-            Some(attribute)
-        } else {
-            None
-        }
-    });
+    let arguments = PathOperations::resolve_path_arguments(&ast_fn.sig.inputs);
 
-    #[cfg(feature = "actix_gen")]
-    let path_provider = || {
-        operation_attribute.as_ref().map(|attribute| {
-            let lit = attribute.parse_args::<LitStr>().unwrap();
-            lit.value() // TODO format path according OpenAPI specs
-        })
-    };
-
-    #[cfg(not(feature = "actix_gen"))]
-    let path_provider = || None::<String>;
-
-    // TODO validate that path is provided one way or the other
+    #[cfg(feature = "actix_extras")]
+    update_parameters_from_arguments(arguments, &mut path_attribute.params);
 
-    // println!("path provider: {:#?}", path_provider());
+    let operation_attribute = &PathOperations::resolve_attribute(&ast_fn);
+    let path_provider = || PathOperations::resolve_path(operation_attribute);
 
     let path = Path::new(path_attribute, fn_name)
-        .with_path_operation(operation_attribute.as_ref().map(|attribute| {
+        .with_path_operation(operation_attribute.map(|attribute| {
             let ident = attribute.path.get_ident().unwrap();
             PathOperation::from_ident(ident)
         }))
@@ -122,6 +87,7 @@ pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream {
         }));
 
     quote! {
+        use utoipa::openapi::schema::ToArray;
         #path
         #ast_fn
     }
@@ -156,6 +122,7 @@ pub fn openapi(input: TokenStream) -> TokenStream {
         .flat_map(|args| match args {
             OpenApiArgs::Components(components) => components,
             _ => unreachable!(),
+            // TODO enabed if argument resolving is enabled
         })
         .collect::<Vec<_>>();
 
@@ -194,6 +161,7 @@ pub fn openapi(input: TokenStream) -> TokenStream {
     }
 
     quote.extend(quote! {
+        use utoipa::openapi::schema::ToArray;
         impl utoipa::OpenApi for #ident {
             fn openapi() -> utoipa::openapi::OpenApi {
                 utoipa::openapi::OpenApi::new(#info, #path_items)
@@ -218,7 +186,7 @@ fn parse_openapi_attributes(attributes: &[Attribute]) -> Option<Vec<OpenApiArgs>
         .next()
         .map(|attribute| {
             if !attribute.path.is_ident("openapi") {
-                abort_call_site!("Expected #[openapi(...)], but was: {:?}", attribute);
+                abort_call_site!("Expected #[openapi(...)]");
             } else {
                 attribute
             }
@@ -303,15 +271,6 @@ impl Parse for OpenApiArgs {
     }
 }
 
-fn is_valid_request_type(s: &str) -> bool {
-    match s {
-        "get" | "post" | "put" | "delete" | "head" | "connect" | "options" | "trace" | "patch" => {
-            true
-        }
-        _ => false,
-    }
-}
-
 fn impl_paths<I: IntoIterator<Item = ExprPath>>(
     handler_paths: I,
     quote: &mut TokenStream2,
diff --git a/utoipa-gen/src/path.rs b/utoipa-gen/src/path.rs
index d13076f..b8fe965 100644
--- a/utoipa-gen/src/path.rs
+++ b/utoipa-gen/src/path.rs
@@ -1,6 +1,6 @@
 use std::{io::Error, str::FromStr};
 
-use proc_macro2::{Group, Ident, TokenStream as TokenStream2};
+use proc_macro2::{Group, Ident, Span, TokenStream as TokenStream2};
 use proc_macro_error::{abort_call_site, OptionExt, ResultExt};
 use quote::{format_ident, quote, ToTokens};
 use syn::{
@@ -8,21 +8,59 @@ use syn::{
     parse::{Parse, ParseStream},
     parse2,
     punctuated::Punctuated,
-    token, LitInt, LitStr, Token,
+    token::Bracket,
+    LitInt, LitStr, Token,
 };
 
+use crate::component_type::{ComponentFormat, ComponentType};
+
 const PATH_STRUCT_PREFIX: &str = "__path_";
 
-// #[api_operation(delete,
+// #[utoipa::path(delete,
 //    operation_id = "custom_operation_id",
-//    path = "custom_path",
+//    path = "/custom/path/{id}/{digest}",
 //    tag = "groupping_tag"
 //    responses = [
-//     (200, "success", String),
+//     (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"),
-//     (500, "internal server error")
-// ])]
+//     (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.
 /// Parsed attributes can be used to override or append OpenAPI Path
@@ -35,6 +73,7 @@ pub struct PathAttr {
     path: Option<String>,
     operation_id: Option<String>,
     tag: Option<String>,
+    pub params: Option<Vec<Parameter>>,
 }
 
 /// Parse implementation for PathAttr will parse arguments
@@ -44,7 +83,9 @@ impl Parse for PathAttr {
         let mut path_attr = PathAttr::default();
 
         loop {
-            let ident = input.parse::<Ident>().unwrap();
+            let ident = input
+                .parse::<Ident>()
+                .expect_or_abort("failed to parse first ident");
             let ident_name = &*ident.to_string();
 
             let parse_lit_str = |input: &ParseStream, error_message: &str| -> String {
@@ -58,6 +99,17 @@ impl Parse for PathAttr {
                     .value()
             };
 
+            let parse_groups = |input: &ParseStream| {
+                if input.peek(Token![=]) {
+                    input.parse::<Token![=]>().unwrap();
+                }
+
+                let content;
+                bracketed!(content in input);
+
+                Punctuated::<Group, Token![,]>::parse_terminated(&content)
+            };
+
             match ident_name {
                 "operation_id" => {
                     path_attr.operation_id = Some(parse_lit_str(
@@ -70,13 +122,7 @@ impl Parse for PathAttr {
                         Some(parse_lit_str(&input, "expected literal string for path"));
                 }
                 "responses" => {
-                    if input.peek(Token![=]) {
-                        input.parse::<Token![=]>().unwrap();
-                    }
-
-                    let content;
-                    bracketed!(content in input);
-                    let groups = Punctuated::<Group, Token![,]>::parse_terminated(&content)
+                    let groups = parse_groups(&input)
                         .expect_or_abort("expected responses to be group separated by comma (,)");
 
                     path_attr.responses = groups
@@ -84,6 +130,16 @@ impl Parse for PathAttr {
                         .map(|group| parse2::<PathResponse>(group.stream()).unwrap_or_abort())
                         .collect::<Vec<_>>();
                 }
+                "params" => {
+                    let groups = parse_groups(&input)
+                        .expect_or_abort("expected parameters to be group separated by comma (,)");
+                    path_attr.params = Some(
+                        groups
+                            .iter()
+                            .map(|group| parse2::<Parameter>(group.stream()).unwrap_or_abort())
+                            .collect::<Vec<Parameter>>(),
+                    )
+                }
                 "tag" => {
                     path_attr.tag = Some(parse_lit_str(&input, "expected literal string for tag"));
                 }
@@ -138,6 +194,7 @@ pub enum PathOperation {
     Head,
     Patch,
     Trace,
+    Connect,
 }
 
 impl PathOperation {
@@ -165,9 +222,10 @@ impl FromStr for PathOperation {
             "head" => Ok(Self::Head),
             "patch" => Ok(Self::Patch),
             "trace" => Ok(Self::Trace),
+            "connect" => Ok(Self::Connect),
             _ => Err(Error::new(
                 std::io::ErrorKind::Other,
-                "invalid PathOperation expected one of: [get, post, put, delete, options, head, patch, trace]",
+                "invalid PathOperation expected one of: get, post, put, delete, options, head, patch, trace, connect",
             )),
         }
     }
@@ -184,6 +242,7 @@ impl ToTokens for PathOperation {
             Self::Head => quote! { utoipa::openapi::PathItemType::Head },
             Self::Patch => quote! { utoipa::openapi::PathItemType::Patch },
             Self::Trace => quote! { utoipa::openapi::PathItemType::Trace },
+            Self::Connect => quote! { utoipa::openapi::PathItemType::Connect },
         };
 
         tokens.extend(path_item_type);
@@ -236,6 +295,197 @@ impl Parse for PathResponse {
     }
 }
 
+/// Parameter of request suchs as in path, header, query or cookie
+///
+/// For example path `/users/{id}` the path parameter is used to define
+/// type, format and other details of the `{id}` parameter within the path
+///
+/// Parse is executed for following formats:
+///
+/// * ("id" = String, path, required, deprecated, description = "Users database id"),
+/// * ("id", path, required, deprecated, description = "Users database id"),
+///
+/// The `= String` type statement is optional if automatic resolvation is supported.
+#[derive(Default)]
+#[cfg_attr(feature = "debug", derive(Debug))]
+pub struct Parameter {
+    pub name: String,
+    parameter_in: ParameterIn,
+    required: bool,
+    deprecated: bool,
+    pub parameter_type: Option<Ident>,
+    is_array: bool,
+    description: Option<String>,
+}
+
+impl Parameter {
+    pub fn new<S: AsRef<str>>(name: S, parameter_type: &Ident, parameter_in: ParameterIn) -> Self {
+        let required = parameter_in == ParameterIn::Path;
+
+        Self {
+            name: name.as_ref().to_string(),
+            parameter_type: Some(parameter_type.clone()),
+            parameter_in,
+            required,
+            ..Default::default()
+        }
+    }
+
+    pub fn update_parameter_type(&mut self, ident: &Ident) {
+        self.parameter_type = Some(ident.clone());
+    }
+}
+
+impl Parse for Parameter {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        let mut parameter = Parameter::default();
+
+        if input.peek(LitStr) {
+            // parse name
+            let name = input.parse::<LitStr>().unwrap().value();
+            parameter.name = name;
+            if input.peek(Token![=]) && input.peek2(syn::Ident) {
+                // parse type for name if provided
+                input.parse::<Token![=]>().unwrap();
+                parameter.parameter_type = Some(input.parse::<syn::Ident>().unwrap());
+            } else if input.peek(Token![=]) && input.peek2(Bracket) {
+                // parse group as array
+                input.parse::<Token![=]>().unwrap();
+                parameter.is_array = true;
+                let group_content;
+                bracketed!(group_content in input);
+                parameter.parameter_type = Some(group_content.parse::<Ident>().unwrap());
+            }
+        } else {
+            return Err(input.error("expected first element to be LitStr parameter name"));
+        }
+        if input.peek(Token![,]) {
+            input.parse::<Token![,]>().unwrap();
+        }
+
+        loop {
+            let ident = input.parse::<syn::Ident>().unwrap();
+            let name = &*ident.to_string();
+            match name {
+                "path" | "query" | "header" | "cookie" => {
+                    parameter.parameter_in = name.parse::<ParameterIn>().unwrap_or_abort();
+                    if parameter.parameter_in == ParameterIn::Path {
+                        parameter.required = true; // all path parameters are required by default
+                    }
+                }
+                "required" => parameter.required = true,
+                "deprecated" => parameter.deprecated = true,
+                "description" => {
+                    if input.peek(Token![=]) {
+                        input.parse::<Token![=]>().unwrap();
+                    }
+
+                    parameter.description = Some(
+                        input
+                            .parse::<LitStr>()
+                            .expect_or_abort("expected description value as LitStr")
+                            .value(),
+                    )
+                }
+                _ => return Err(input.error(&format!("unexpected element: {}", name))),
+            }
+
+            if input.peek(Token![,]) {
+                input.parse::<Token![,]>().unwrap();
+            }
+            if input.is_empty() {
+                break;
+            }
+        }
+        Ok(parameter)
+    }
+}
+impl ToTokens for Parameter {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        let name = &*self.name;
+        tokens.extend(quote! { utoipa::openapi::path::Parameter::new(#name) });
+        let parameter_in = &self.parameter_in;
+        tokens.extend(quote! { .with_in(#parameter_in) });
+
+        let required: Required = self.required.into();
+        tokens.extend(quote! { .with_required(#required) });
+
+        let deprecated: Deprecated = self.deprecated.into();
+        tokens.extend(quote! { .with_deprecated(#deprecated) });
+
+        if let Some(ref description) = self.description {
+            tokens.extend(quote! { .with_description(#description) });
+        }
+
+        if let Some(ref parameter_type) = self.parameter_type {
+            // TODO unify this property logic with the one in component.rs
+            let component_type = ComponentType(parameter_type);
+            let mut property = quote! {
+                utoipa::openapi::Property::new(
+                    #component_type
+                )
+            };
+            let format = ComponentFormat(parameter_type);
+            if format.is_known_format() {
+                property.extend(quote! {
+                    .with_format(#format)
+                })
+            }
+            if self.is_array {
+                property.extend(quote! { .to_array() });
+            }
+
+            tokens.extend(quote! { .with_schema(#property) });
+        }
+    }
+}
+
+#[cfg_attr(feature = "debug", derive(Debug))]
+#[derive(PartialEq)]
+pub enum ParameterIn {
+    Query,
+    Path,
+    Header,
+    Cookie,
+}
+
+impl Default for ParameterIn {
+    fn default() -> Self {
+        Self::Path
+    }
+}
+
+impl FromStr for ParameterIn {
+    type Err = syn::Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s {
+            "path" => Ok(Self::Path),
+            "query" => Ok(Self::Query),
+            "header" => Ok(Self::Header),
+            "cookie" => Ok(Self::Cookie),
+            _ => Err(syn::Error::new(
+                Span::call_site(),
+                &format!(
+                    "unexpected str: {}, expected one of: path, query, header, cookie",
+                    s
+                ),
+            )),
+        }
+    }
+}
+
+impl ToTokens for ParameterIn {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        tokens.extend(match self {
+            Self::Path => quote! { utoipa::openapi::path::ParameterIn::Path },
+            Self::Query => quote! { utoipa::openapi::path::ParameterIn::Query },
+            Self::Header => quote! { utoipa::openapi::path::ParameterIn::Header },
+            Self::Cookie => quote! { utoipa::openapi::path::ParameterIn::Cookie },
+        })
+    }
+}
+
 pub struct Path {
     path_attr: PathAttr,
     fn_name: String,
@@ -319,6 +569,7 @@ impl ToTokens for Path {
                 .as_ref()
                 .and_then(|comments| comments.iter().next()),
             description: self.doc_comments.as_ref(),
+            parameters: self.path_attr.params.as_ref(),
         };
 
         tokens.extend(quote! {
@@ -331,7 +582,7 @@ impl ToTokens for Path {
                 }
             }
 
-            impl utoipa::Path for  #path_struct {
+            impl utoipa::Path for #path_struct {
                 fn path() -> &'static str {
                     #path
                 }
@@ -353,6 +604,7 @@ struct Operation<'a> {
     summary: Option<&'a String>,
     description: Option<&'a Vec<String>>,
     deprecated: &'a Option<bool>,
+    parameters: Option<&'a Vec<Parameter>>,
 }
 
 impl ToTokens for Operation<'_> {
@@ -366,17 +618,13 @@ impl ToTokens for Operation<'_> {
                 utoipa::openapi::response::Response::new("this is response message")
             )
         });
-        //         // .with_parameters()
         //         // .with_request_body()
-        //         // .with_description()
-        //         // .with_summary()
-        //         // .with_deprecated()
         //         // .with_security()
         let path_struct = format_ident!("{}{}", PATH_STRUCT_PREFIX, self.fn_name);
         let operation_id = self.operation_id;
         tokens.extend(quote! {
             .with_tag(
-                vec![<#path_struct as utoipa::Tag>::tag(),
+                [<#path_struct as utoipa::Tag>::tag(),
                     <#path_struct as utoipa::DefaultTag>::tag()
                 ]
                 .into_iter().find(|s| !s.is_empty()).unwrap_or_else(|| "crate")
@@ -386,10 +634,15 @@ impl ToTokens for Operation<'_> {
             )
         });
 
-        let deprecated = get_deprecated_token_stream(self.deprecated);
+        let deprecated = self
+            .deprecated
+            .map(Into::<Deprecated>::into)
+            .or(Some(Deprecated::False))
+            .unwrap();
         tokens.extend(quote! {
            .with_deprecated(#deprecated)
         });
+
         if let Some(summary) = self.summary {
             tokens.extend(quote! {
                 .with_summary(#summary)
@@ -407,27 +660,59 @@ impl ToTokens for Operation<'_> {
                 .with_description(#description)
             })
         }
+
+        if let Some(parameters) = self.parameters {
+            parameters
+                .iter()
+                .for_each(|parameter| tokens.extend(quote! { .with_parameter(#parameter) }));
+        }
     }
 }
 
-fn get_deprecated_token_stream(deprecated: &Option<bool>) -> TokenStream2 {
-    let get_deprecated_false = || {
-        quote! {
-            utoipa::openapi::Deprecated::False
+pub enum Deprecated {
+    True,
+    False,
+}
+
+impl From<bool> for Deprecated {
+    fn from(bool: bool) -> Self {
+        if bool {
+            Self::True
+        } else {
+            Self::False
         }
-    };
-
-    deprecated
-        .as_ref()
-        .map(|deprecated| {
-            println!("is deprecated: {:?}", deprecated);
-            if *deprecated {
-                quote! {
-                    utoipa::openapi::Deprecated::True
-                }
-            } else {
-                get_deprecated_false()
-            }
+    }
+}
+
+impl ToTokens for Deprecated {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        tokens.extend(match self {
+            Self::False => quote! { utoipa::openapi::Deprecated::False },
+            Self::True => quote! { utoipa::openapi::Deprecated::True },
+        })
+    }
+}
+
+pub enum Required {
+    True,
+    False,
+}
+
+impl From<bool> for Required {
+    fn from(bool: bool) -> Self {
+        if bool {
+            Self::True
+        } else {
+            Self::False
+        }
+    }
+}
+
+impl ToTokens for Required {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        tokens.extend(match self {
+            Self::False => quote! { utoipa::openapi::Required::False },
+            Self::True => quote! { utoipa::openapi::Required::True },
         })
-        .unwrap_or_else(get_deprecated_false)
+    }
 }
diff --git a/utoipa-gen/src/paths.rs b/utoipa-gen/src/paths.rs
deleted file mode 100644
index bc02aac..0000000
--- a/utoipa-gen/src/paths.rs
+++ /dev/null
@@ -1,308 +0,0 @@
-use std::{env, fs, str::FromStr};
-
-use proc_macro2::{Group, Ident, Punct, TokenStream as TokenStream2};
-use proc_macro_error::{abort, abort_call_site, ResultExt};
-use quote::quote;
-use syn::{
-    parse::Parse, punctuated::Punctuated, Attribute, Error, ItemFn, LitInt, LitStr, MetaNameValue,
-    Token,
-};
-
-const API_OPERATION_IDENT: &str = "api_operation";
-
-#[derive(Debug, Default, Clone)]
-struct Path {
-    comments: Vec<String>,
-    operation: Operation,
-    responses: Vec<ApiOperationResponse>,
-    operation_id: String,
-    // TODO missing request path, request body, request params, response body
-}
-
-impl Path {
-    fn with_comment<S: Into<String>>(mut self, comment: S) -> Self {
-        self.comments.push(comment.into());
-
-        self
-    }
-
-    fn with_responses(self, responses: Vec<ApiOperationResponse>) -> Self {
-        Self { responses, ..self }
-    }
-
-    fn with_operation(self, operation: Operation) -> Self {
-        Self { operation, ..self }
-    }
-
-    fn with_operation_id<S: Into<String>>(self, operation_id: S) -> Self {
-        Self {
-            operation_id: operation_id.into(),
-            ..self
-        }
-    }
-}
-
-#[derive(Debug, Clone)]
-enum Operation {
-    Get,
-    Delete,
-    Put,
-    Post,
-    Head,
-    Patch,
-}
-
-impl Default for Operation {
-    fn default() -> Self {
-        Self::Get
-    }
-}
-
-impl FromStr for Operation {
-    type Err = syn::Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        match s {
-            "get" => Ok(Self::Get),
-            "delete" => Ok(Self::Delete),
-            "put" => Ok(Self::Put),
-            "post" => Ok(Self::Post),
-            "head" => Ok(Self::Head),
-            "patch" => Ok(Self::Patch),
-            _ => Err(Error::new(
-                proc_macro2::Span::call_site(),
-                format!(
-                    "invalid operation: {}, expected one of: {}",
-                    s, "get,delete,put,post,head,patch"
-                ),
-            )),
-        }
-    }
-}
-
-#[derive(Debug, Default, Clone)]
-struct ApiOperationResponse {
-    code: i32,
-    description: String,
-    response_item: Option<syn::Ident>,
-}
-
-impl Parse for ApiOperationResponse {
-    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
-        let code = input.parse::<LitInt>()?;
-        println!("parsed response code: {:?}", code);
-
-        input.parse::<Punct>()?;
-
-        let description = input.parse::<LitStr>()?;
-        println!("parsed response description: {:?}", description);
-
-        if input.is_empty() {
-            Ok(ApiOperationResponse {
-                code: code.base10_parse()?,
-                description: description.value(),
-                response_item: None,
-            })
-        } else {
-            input.parse::<Punct>()?;
-
-            let response_type = input.parse::<Ident>()?;
-            println!("parsed response type: {:?}", response_type);
-
-            Ok(ApiOperationResponse {
-                code: code.base10_parse()?,
-                description: description.value(),
-                response_item: Some(response_type),
-            })
-        }
-    }
-}
-
-enum ApiOperationItem {
-    Operation(Operation),
-    Responses(Vec<ApiOperationResponse>),
-}
-
-impl Parse for ApiOperationItem {
-    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
-        let ident = input.parse::<syn::Ident>()?;
-
-        let name = &*ident.to_string();
-
-        println!("ident: {:#?}", ident);
-
-        match name {
-            operation if input.peek(syn::Token![,]) && input.peek2(syn::Ident) => {
-                // input.parse::<Token![,]>()?;
-                // input.parse::<syn::Ident>()?;
-
-                // input.parse::<Token![,]>()?; // parse next comma (,)
-
-                Ok(ApiOperationItem::Operation(operation.parse()?))
-            }
-            "responses" if input.peek(syn::Token![=]) => {
-                input.parse::<Token![=]>()?;
-
-                let content;
-                syn::bracketed!(content in input);
-                let response_groups = Punctuated::<Group, Token![,]>::parse_terminated(&content)?;
-
-                Ok(ApiOperationItem::Responses(
-                    response_groups
-                        .iter()
-                        .map(|group| {
-                            syn::parse2::<ApiOperationResponse>(group.stream()).unwrap_or_else(
-                                |error| {
-                                    abort!(
-                                        group,
-                                        "parse responses api operation error: {:?}",
-                                        error
-                                    )
-                                },
-                            )
-                        })
-                        .collect::<Vec<_>>(),
-                ))
-            }
-            _ => abort!(input.span(), "unexpected attribute value: {}", name),
-        }
-    }
-}
-
-pub fn impl_paths(files: &[String]) -> TokenStream2 {
-    parse_files(files);
-
-    let paths = quote! {
-        utoipa::openapi::Paths::new()
-    };
-
-    paths
-}
-
-fn parse_files(files: &[String]) {
-    files.iter().for_each(parse_file)
-}
-
-fn parse_file<S: AsRef<str>>(file: S) {
-    let current_dir = env::current_dir()
-        .unwrap_or_else(|error| abort_call_site!("could not define current dir: {}", error));
-
-    let path = current_dir.join(file.as_ref());
-
-    let content = fs::read_to_string(&path.as_path()).unwrap_or_else(|error| {
-        abort_call_site!("read file from path: {:?}, error: {}", path, error)
-    });
-
-    let file_content = syn::parse_file(&content).unwrap();
-
-    // file_content.items.iter().for_each(|item| match item {
-    //     syn::Item::Fn(func) => {
-    //         if let Some(span) = has_attribute(&func.attrs, API_OPERATION_IDENT) {
-    //             abort!(span, "missing api_operation");
-    //         } else {
-    //             abort_call_site!("Missing attribute open_apifffff");
-    //         }
-    //     }
-    //     _ => (),
-    // });
-
-    file_content.items.iter().for_each(|item| {
-        // TODO
-        match item {
-            syn::Item::Fn(func) if has_attribute(&func.attrs, API_OPERATION_IDENT) => {
-                process_function(func);
-            }
-            _ => (),
-        }
-    });
-    // println!("{:#?}", &file_content);
-}
-
-fn has_attribute<S: AsRef<str>>(attributes: &[Attribute], name: S) -> bool {
-    attributes.iter().any(|attribute| {
-        let ident = &attribute
-            .path
-            .segments
-            .first()
-            .unwrap_or_else(|| {
-                abort!(
-                    attribute.path,
-                    "incorrect path, expected to have a one segment"
-                )
-            })
-            .ident;
-
-        *ident == name.as_ref()
-    })
-}
-
-fn process_function(function: &ItemFn) {
-    // TODO this should return the resolved path token stream
-
-    let fn_name = function.sig.ident.to_string();
-    println!("fn: {:#?}", function);
-
-    // println!("{:?}", function.sig.ident);
-
-    // emit_error!(function.sig.ident.span(), "missing foo bar");
-
-    function
-        .attrs
-        .iter()
-        .fold(Path::default(), |api_operation, attribute| {
-            let attribute_ident = &attribute
-                .path
-                .segments
-                .first()
-                .unwrap_or_else(|| {
-                    abort!(
-                        attribute.path,
-                        "incorrect path, expected to have a one segment"
-                    )
-                })
-                .ident;
-
-            let name = attribute_ident.to_string();
-
-            match &*name {
-                "doc" => api_operation.with_comment(parse_doc_attribute(attribute)), // TODO parse doc
-                API_OPERATION_IDENT => {
-                    // println!("attribute: {:#?}", attribute);
-                    parse_api_operation_attribute(attribute).into_iter().fold(
-                        api_operation,
-                        |op, item| match item {
-                            ApiOperationItem::Operation(operation) => op.with_operation(operation),
-                            ApiOperationItem::Responses(responses) => op.with_responses(responses),
-                        },
-                    )
-                } // TODO parse api_operation
-                _rest => api_operation, // TODO do custom manipulation based on enabled library
-            }
-        })
-        .with_operation_id(fn_name);
-}
-
-fn parse_doc_attribute(attribute: &Attribute) -> String {
-    let meta = attribute.parse_meta().unwrap_or_else(|error| {
-        abort!(
-            attribute,
-            "parse attribute meta: {:?} failed: {}",
-            attribute,
-            error
-        )
-    });
-
-    match meta {
-        syn::Meta::NameValue(MetaNameValue { lit, .. }) => match lit {
-            syn::Lit::Str(lit_str) => lit_str.value(),
-            _ => abort!(lit, "unexpecte lit type: {:?}", lit),
-        },
-        _ => abort!(meta, "unexpected meta type: {:?}", meta),
-    }
-}
-
-fn parse_api_operation_attribute(attribute: &Attribute) -> Punctuated<ApiOperationItem, Token![,]> {
-    attribute
-        .parse_args_with(Punctuated::<ApiOperationItem, Token![,]>::parse_terminated)
-        .unwrap_or_abort()
-}


Test patch:
diff --git a/tests/common.rs b/tests/common.rs
index 7d85a06..b6077cd 100644
--- a/tests/common.rs
+++ b/tests/common.rs
@@ -2,10 +2,52 @@ use serde_json::Value;
 
 pub fn get_json_path<'a>(value: &'a Value, path: &str) -> &'a Value {
     path.split('.').into_iter().fold(value, |acc, fragment| {
-        acc.get(fragment).unwrap_or(&serde_json::value::Value::Null)
+        let value = if fragment.starts_with('[') && fragment.ends_with(']') {
+            let index = fragment
+                .replace('[', "")
+                .replace(']', "")
+                .parse::<usize>()
+                .unwrap();
+            acc.get(index)
+        } else {
+            acc.get(fragment)
+        };
+        value.unwrap_or(&serde_json::value::Value::Null)
     })
 }
 
 pub fn value_as_string(value: Option<&'_ Value>) -> String {
     value.unwrap_or(&Value::Null).to_string()
 }
+
+pub fn assert_json_array_len(value: &Value, len: usize) {
+    match value {
+        Value::Array(array) => assert_eq!(
+            len,
+            array.len(),
+            "wrong amount of parameters {} != {}",
+            len,
+            array.len()
+        ),
+        _ => unreachable!(),
+    }
+}
+
+#[macro_export]
+macro_rules! assert_value {
+    ($value:expr=> $( $path:literal = $expected:literal, $error:literal)* ) => {{
+        $(
+            let actual = crate::common::value_as_string(Some(crate::common::get_json_path(&$value, $path)));
+            assert_eq!(actual, $expected, "{}: {} expected to be: {} but was: {}", $error, $path, $expected, actual);
+         )*
+    }};
+
+    ($value:expr=> $( $path:literal = $expected:expr, $error:literal)*) => {
+        {
+            $(
+                let actual = crate::common::get_json_path(&$value, $path);
+                assert!(actual == &$expected, "{}: {} expected to be: {:?} but was: {:?}", $error, $path, $expected, actual);
+             )*
+        }
+    }
+}
diff --git a/tests/component_derive_test.rs b/tests/component_derive_test.rs
index 4fa8be8..a90cf65 100644
--- a/tests/component_derive_test.rs
+++ b/tests/component_derive_test.rs
@@ -3,7 +3,7 @@ use std::collections::HashMap;
 use serde_json::Value;
 use utoipa::{Component, OpenApi};
 
-use crate::common::{get_json_path, value_as_string};
+use crate::common::get_json_path;
 
 mod common;
 
@@ -38,24 +38,6 @@ macro_rules! api_doc {
     }};
 }
 
-macro_rules! assert_value {
-    ($value:expr=> $( $path:literal = $expected:literal, $error:literal)* ) => {{
-        $(
-            let actual = value_as_string(Some(get_json_path(&$value, $path)));
-            assert_eq!(actual, $expected, "{}: {} expected to be: {} but was: {}", $error, $path, $expected, actual);
-         )*
-    }};
-
-    ($value:expr=> $( $path:literal = $expected:expr, $error:literal)*) => {
-        {
-            $(
-                let actual = get_json_path(&$value, $path);
-                assert!(actual == &$expected, "{}: {} expected to be: {:?} but was: {:?}", $error, $path, $expected, actual);
-             )*
-        }
-    }
-}
-
 #[test]
 fn derive_enum_with_additional_properties_success() {
     let mode = api_doc! {
diff --git a/tests/path_derive.rs b/tests/path_derive.rs
new file mode 100644
index 0000000..0e4d946
--- /dev/null
+++ b/tests/path_derive.rs
@@ -0,0 +1,200 @@
+use paste::paste;
+
+mod common;
+
+macro_rules! test_api_fn_doc {
+    ( $handler:path, operation: $operation:expr, path: $path:literal ) => {{
+        use utoipa::OpenApi;
+        #[derive(OpenApi, Default)]
+        #[openapi(handler_files = [], handlers = [$handler])]
+        struct ApiDoc;
+
+        let doc = &serde_json::to_value(ApiDoc::openapi()).unwrap();
+        let operation =
+            common::get_json_path(doc, &format!("paths.{}.{}", $path, stringify!($operation)));
+        operation.clone()
+    }};
+}
+
+macro_rules! test_api_fn {
+    (name: $name:ident, module: $module:ident,
+        operation: $operation:ident,
+        path: $path:expr
+        $(, params: $params:expr )?
+        $(, operation_id: $operation_id:expr )?
+        $(, tag: $tag:expr )?
+        $(; $( #[$meta:meta] )* )? ) => {
+        mod $module {
+            $( $(#[$meta])* )*
+            #[utoipa::path(
+                                                                    $operation,
+                                                                   $( operation_id = $operation_id, )*
+                                                                    path = $path,
+                                                                    responses = [
+                                                                        (200, "success", String),
+                                                                    ],
+                                                                   $( params = $params, )*
+                                                                   $( tag = $tag, )*
+                                                                )]
+            #[allow(unused)]
+            async fn $name() -> String {
+                "foo".to_string()
+            }
+        }
+    };
+}
+macro_rules! test_path_operation {
+    ( $($name:ident: $operation:ident)* ) => {
+       $(paste! {
+            test_api_fn! {
+                name: test_operation,
+                module: [<mod_ $name>],
+                operation: $operation,
+                path: "/foo"
+            }
+        }
+        #[test]
+        fn $name() {
+            paste!{
+                use utoipa::OpenApi;
+                #[derive(OpenApi, Default)]
+                #[openapi(handler_files = [], handlers = [
+                    [<mod_ $name>]::test_operation
+                ])]
+                struct ApiDoc;
+            }
+
+            let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
+            let operation_value = common::get_json_path(&doc, &format!("paths./foo.{}", stringify!($operation)));
+            assert!(operation_value != &serde_json::Value::Null,
+                "expected to find operation with: {}", &format!("paths./foo.{}", stringify!($operation)));
+        })*
+    };
+}
+
+test_path_operation! {
+    derive_path_post: post
+    derive_path_get: get
+    derive_path_delete: delete
+    derive_path_put: put
+    derive_path_options: options
+    derive_path_head: head
+    derive_path_patch: patch
+    derive_path_trace: trace
+    derive_path_connect: connect
+}
+
+test_api_fn! {
+    name: test_operation2,
+    module: derive_path_with_all_info,
+    operation: post,
+    path: "/foo/bar/{id}",
+    params: [("id", description = "Foo bar id")],
+    operation_id: "foo_bar_id",
+    tag: "custom_tag";
+    /// This is test operation description
+    ///
+    /// Additional info in long description
+    #[deprecated]
+}
+
+#[test]
+fn derive_path_with_all_info_success() {
+    let operation = test_api_fn_doc! {
+        derive_path_with_all_info::test_operation2,
+        operation: post,
+        path: "/foo/bar/{id}"
+    };
+
+    common::assert_json_array_len(common::get_json_path(&operation, "parameters"), 1);
+    assert_value! {operation=>
+       "deprecated" = r#"true"#, "Api fn deprecated status"
+       "description" = r#""This is test operation description\n\nAdditional info in long description\n""#, "Api fn description"
+       "summary" = r#""This is test operation description""#, "Api fn summary"
+       "operationId" = r#""foo_bar_id""#, "Api fn operation_id"
+       "tags.[0]" = r#""custom_tag""#, "Api fn tag"
+
+       "parameters.[0].deprecated" = r#"false"#, "Path parameter deprecated"
+       "parameters.[0].description" = r#""Foo bar id""#, "Path parameter description"
+       "parameters.[0].in" = r#""path""#, "Path parameter in"
+       "parameters.[0].name" = r#""id""#, "Path parameter name"
+       "parameters.[0].required" = r#"true"#, "Path parameter required"
+    }
+}
+
+test_api_fn! {
+    name: test_operation3,
+    module: derive_path_with_defaults,
+    operation: post,
+    path: "/foo/bar";
+}
+
+#[test]
+fn derive_path_with_defaults_success() {
+    let operation = test_api_fn_doc! {
+        derive_path_with_defaults::test_operation3,
+        operation: post,
+        path: "/foo/bar"
+    };
+
+    assert_value! {operation=>
+       "deprecated" = r#"false"#, "Api fn deprecated status"
+       "description" = r#""""#, "Api fn description"
+       "operationId" = r#""test_operation3""#, "Api fn operation_id"
+       "tags.[0]" = r#""derive_path_with_defaults""#, "Api fn tag"
+       "parameters" = r#"null"#, "Api parameters"
+    }
+}
+
+/// This is test operation
+///
+/// This is long description for test operation
+#[utoipa::path(
+    get,
+    path = "/foo/{id}",
+    responses = [
+        (200, "success", String)
+    ],
+    params = [
+        ("id" = u64, description = "Foo database id"),
+        ("since" = String, query, description = "Datetime since foo is updated")
+    ]
+)]
+#[allow(unused)]
+async fn get_foos_by_id_since() -> String {
+    "".to_string()
+}
+
+#[test]
+fn derive_path_with_extra_attributes_without_nested_module() {
+    let operation = test_api_fn_doc! {
+        get_foos_by_id_since,
+        operation: get,
+        path: "/foo/{id}"
+    };
+
+    common::assert_json_array_len(common::get_json_path(&operation, "parameters"), 2);
+    assert_value! {operation=>
+        "deprecated" = r#"false"#, "Api operation deprecated"
+        "description" = r#""This is test operation\n\nThis is long description for test operation\n""#, "Api operation description"
+        "operationId" = r#""get_foos_by_id_since""#, "Api operation operation_id"
+        "summary" = r#""This is test operation""#, "Api operation summary"
+        "tags.[0]" = r#""crate""#, "Api operation tag"
+
+        "parameters.[0].deprecated" = r#"false"#, "Parameter 0 deprecated"
+        "parameters.[0].description" = r#""Foo database id""#, "Parameter 0 description"
+        "parameters.[0].in" = r#""path""#, "Parameter 0 in"
+        "parameters.[0].name" = r#""id""#, "Parameter 0 name"
+        "parameters.[0].required" = r#"true"#, "Parameter 0 required"
+        "parameters.[0].schema.format" = r#""int64""#, "Parameter 0 schema format"
+        "parameters.[0].schema.type" = r#""integer""#, "Parameter 0 schema type"
+
+        "parameters.[1].deprecated" = r#"false"#, "Parameter 1 deprecated"
+        "parameters.[1].description" = r#""Datetime since foo is updated""#, "Parameter 1 description"
+        "parameters.[1].in" = r#""query""#, "Parameter 1 in"
+        "parameters.[1].name" = r#""since""#, "Parameter 1 name"
+        "parameters.[1].required" = r#"false"#, "Parameter 1 required"
+        "parameters.[1].schema.format" = r#"null"#, "Parameter 1 schema format"
+        "parameters.[1].schema.type" = r#""string""#, "Parameter 1 schema type"
+    }
+}
diff --git a/tests/path_derive_actix.rs b/tests/path_derive_actix.rs
new file mode 100644
index 0000000..eb0c6bc
--- /dev/null
+++ b/tests/path_derive_actix.rs
@@ -0,0 +1,183 @@
+#![cfg(feature = "actix_extras")]
+
+use serde_json::Value;
+use utoipa::OpenApi;
+
+mod common;
+
+mod mod_derive_path_actix {
+    use actix_web::{get, web, HttpResponse, Responder};
+    use serde_json::json;
+
+    /// Get foo by id
+    ///
+    /// Get foo by id long description
+    #[utoipa::path(
+        responses = [
+            (200, "success", String),
+        ],
+        params = [
+            ("id", description = "Foo id"),
+        ]
+    )]
+    #[get("/foo/{id}")]
+    #[allow(unused)]
+    async fn get_foo_by_id(web::Path(id): web::Path<i32>) -> impl Responder {
+        HttpResponse::Ok().json(json!({ "foo": format!("{:?}", &id) }))
+    }
+}
+
+#[test]
+fn derive_path_one_value_actix_success() {
+    #[derive(OpenApi, Default)]
+    #[openapi(handler_files = [], handlers = [mod_derive_path_actix::get_foo_by_id])]
+    struct ApiDoc;
+
+    let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
+    let parameters = common::get_json_path(&doc, "paths./foo/{id}.get.parameters");
+
+    common::assert_json_array_len(parameters, 1);
+    assert_value! {parameters=>
+        "[0].in" = r#""path""#, "Parameter in"
+        "[0].name" = r#""id""#, "Parameter name"
+        "[0].description" = r#""Foo id""#, "Parameter description"
+        "[0].required" = r#"true"#, "Parameter required"
+        "[0].deprecated" = r#"false"#, "Parameter deprecated"
+        "[0].schema.type" = r#""integer""#, "Parameter schema type"
+        "[0].schema.format" = r#""int32""#, "Parameter schema format"
+    };
+}
+
+mod mod_derive_path_unnamed_regex_actix {
+    use actix_web::{get, web, HttpResponse, Responder};
+    use serde_json::json;
+
+    /// Get foo by id
+    ///
+    /// Get foo by id long description
+    #[utoipa::path(
+        responses = [
+            (200, "success", String),
+        ],
+        params = [
+            ("arg0", description = "Foo path unnamed regex tail")
+        ]
+    )]
+    #[get("/foo/{_:.*}")]
+    #[allow(unused)]
+    async fn get_foo_by_id(arg0: web::Path<String>) -> impl Responder {
+        HttpResponse::Ok().json(json!({ "foo": &format!("{:?}", arg0.into_inner()) }))
+    }
+}
+
+#[test]
+fn derive_path_with_unnamed_regex_actix_success() {
+    #[derive(OpenApi, Default)]
+    #[openapi(handler_files = [], handlers = [mod_derive_path_unnamed_regex_actix::get_foo_by_id])]
+    struct ApiDoc;
+
+    let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
+    let parameters = common::get_json_path(&doc, "paths./foo/{arg0}.get.parameters");
+
+    common::assert_json_array_len(parameters, 1);
+    assert_value! {parameters=>
+        "[0].in" = r#""path""#, "Parameter in"
+        "[0].name" = r#""arg0""#, "Parameter name"
+        "[0].description" = r#""Foo path unnamed regex tail""#, "Parameter description"
+        "[0].required" = r#"true"#, "Parameter required"
+        "[0].deprecated" = r#"false"#, "Parameter deprecated"
+        "[0].schema.type" = r#""string""#, "Parameter schema type"
+        "[0].schema.format" = r#"null"#, "Parameter schema format"
+    };
+}
+
+mod mod_derive_path_named_regex_actix {
+    use actix_web::{get, web, HttpResponse, Responder};
+    use serde_json::json;
+
+    /// Get foo by id
+    ///
+    /// Get foo by id long description
+    #[utoipa::path(
+        responses = [
+            (200, "success", String),
+        ],
+        params = [
+            ("tail", description = "Foo path named regex tail")
+        ]
+    )]
+    #[get("/foo/{tail:.*}")]
+    #[allow(unused)]
+    async fn get_foo_by_id(tail: web::Path<String>) -> impl Responder {
+        HttpResponse::Ok().json(json!({ "foo": &format!("{:?}", tail.into_inner()) }))
+    }
+}
+
+#[test]
+fn derive_path_with_named_regex_actix_success() {
+    #[derive(OpenApi, Default)]
+    #[openapi(handler_files = [], handlers = [mod_derive_path_named_regex_actix::get_foo_by_id])]
+    struct ApiDoc;
+
+    let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
+    let parameters = common::get_json_path(&doc, "paths./foo/{tail}.get.parameters");
+
+    common::assert_json_array_len(parameters, 1);
+    assert_value! {parameters=>
+        "[0].in" = r#""path""#, "Parameter in"
+        "[0].name" = r#""tail""#, "Parameter name"
+        "[0].description" = r#""Foo path named regex tail""#, "Parameter description"
+        "[0].required" = r#"true"#, "Parameter required"
+        "[0].deprecated" = r#"false"#, "Parameter deprecated"
+        "[0].schema.type" = r#""string""#, "Parameter schema type"
+        "[0].schema.format" = r#"null"#, "Parameter schema format"
+    };
+}
+
+macro_rules! test_derive_path_operations {
+    ( $( $name:ident, $mod:ident: $operation:ident)* ) => {
+        $(
+           mod $mod {
+            use actix_web::{$operation, HttpResponse, Responder};
+            use serde_json::json;
+
+            #[utoipa::path(
+                responses = [
+                    (200, "success", String),
+                ]
+            )]
+            #[$operation("/foo")]
+            #[allow(unused)]
+            async fn test_operation() -> impl Responder {
+                HttpResponse::Ok().json(json!({ "foo": "".to_string() }))
+            }
+        }
+
+        #[test]
+        fn $name() {
+            #[derive(OpenApi, Default)]
+            #[openapi(handler_files = [], handlers = [$mod::test_operation])]
+            struct ApiDoc;
+
+            let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
+
+            let op_str = stringify!($operation);
+            let path = format!("paths./foo.{}", op_str);
+            let value = common::get_json_path(&doc, &path);
+            assert!(value != &Value::Null, "expected to find operation with: {}", path);
+        }
+        )*
+    };
+}
+
+test_derive_path_operations! {
+    derive_path_operation_post, mod_test_post: post
+    derive_path_operation_get, mod_test_get: get
+    derive_path_operation_delete, mod_test_delete: delete
+    derive_path_operation_put, mod_test_put: put
+    derive_path_operation_head, mod_test_head: head
+    derive_path_operation_connect, mod_test_connect: connect
+    derive_path_operation_options, mod_test_options: options
+    derive_path_operation_trace, mod_test_trace: trace
+    derive_path_operation_patch, mod_test_patch: patch
+}
diff --git a/tests/path_parameter_derive_actix.rs b/tests/path_parameter_derive_actix.rs
new file mode 100644
index 0000000..25874aa
--- /dev/null
+++ b/tests/path_parameter_derive_actix.rs
@@ -0,0 +1,158 @@
+#![cfg(feature = "actix_extras")]
+
+use utoipa::OpenApi;
+
+mod common;
+
+mod derive_params_multiple_actix {
+    use actix_web::{web, HttpResponse, Responder};
+    use serde_json::json;
+
+    /// Get foo by id
+    ///
+    /// Get foo by id long description
+    #[utoipa::path(
+        get,
+        path = "/foo/{id}/{digest}",
+        responses = [
+            (200, "success", String),
+        ],
+        params = [
+            ("id", description = "Foo id"),
+            ("digest", description = "Digest of foo"),
+        ]
+    )]
+    #[allow(unused)]
+    async fn get_foo_by_id(web::Path((id, digest)): web::Path<(i32, String)>) -> impl Responder {
+        HttpResponse::Ok().json(json!({ "foo": format!("{:?}{:?}", &id, &digest) }))
+    }
+}
+
+#[test]
+fn derive_path_parameter_multiple_with_matching_names_and_types_actix_success() {
+    #[derive(OpenApi, Default)]
+    #[openapi(handler_files = [], handlers = [derive_params_multiple_actix::get_foo_by_id])]
+    struct ApiDoc;
+
+    let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
+    let parameters = common::get_json_path(&doc, "paths./foo/{id}/{digest}.get.parameters");
+
+    common::assert_json_array_len(parameters, 2);
+    assert_value! {parameters=>
+        "[0].in" = r#""path""#, "Parameter in"
+        "[0].name" = r#""id""#, "Parameter name"
+        "[0].description" = r#""Foo id""#, "Parameter description"
+        "[0].required" = r#"true"#, "Parameter required"
+        "[0].deprecated" = r#"false"#, "Parameter deprecated"
+        "[0].schema.type" = r#""integer""#, "Parameter schema type"
+        "[0].schema.format" = r#""int32""#, "Parameter schema format"
+
+        "[1].in" = r#""path""#, "Parameter in"
+        "[1].name" = r#""digest""#, "Parameter name"
+        "[1].description" = r#""Digest of foo""#, "Parameter description"
+        "[1].required" = r#"true"#, "Parameter required"
+        "[1].deprecated" = r#"false"#, "Parameter deprecated"
+        "[1].schema.type" = r#""string""#, "Parameter schema type"
+        "[1].schema.format" = r#"null"#, "Parameter schema format"
+    };
+}
+
+mod derive_parameters_multiple_no_matching_names_actix {
+    use actix_web::{web, HttpResponse, Responder};
+    use serde_json::json;
+
+    /// Get foo by id
+    ///
+    /// Get foo by id long description
+    #[utoipa::path(
+        get,
+        path = "/foo/{id}/{digest}",
+        responses = [
+            (200, "success", String),
+        ],
+        params = [
+            ("id" = i32, description = "Foo id"),
+            ("digest" = String, description = "Digest of foo"),
+        ]
+    )]
+    #[allow(unused)]
+    async fn get_foo_by_id(info: web::Path<(i32, String)>) -> impl Responder {
+        // is no matching names since the parameter name does not match to amount of types
+        HttpResponse::Ok().json(json!({ "foo": format!("{:?}{:?}", &info.0, &info.1) }))
+    }
+}
+
+#[test]
+fn derive_path_parameter_multiple_no_matching_names_acitx_success() {
+    #[derive(OpenApi, Default)]
+    #[openapi(handler_files = [], handlers = [derive_parameters_multiple_no_matching_names_actix::get_foo_by_id])]
+    struct ApiDoc;
+
+    let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
+    let parameters = common::get_json_path(&doc, "paths./foo/{id}/{digest}.get.parameters");
+
+    common::assert_json_array_len(parameters, 2);
+    assert_value! {parameters=>
+        "[0].in" = r#""path""#, "Parameter in"
+        "[0].name" = r#""id""#, "Parameter name"
+        "[0].description" = r#""Foo id""#, "Parameter description"
+        "[0].required" = r#"true"#, "Parameter required"
+        "[0].deprecated" = r#"false"#, "Parameter deprecated"
+        "[0].schema.type" = r#""integer""#, "Parameter schema type"
+        "[0].schema.format" = r#""int32""#, "Parameter schema format"
+
+        "[1].in" = r#""path""#, "Parameter in"
+        "[1].name" = r#""digest""#, "Parameter name"
+        "[1].description" = r#""Digest of foo""#, "Parameter description"
+        "[1].required" = r#"true"#, "Parameter required"
+        "[1].deprecated" = r#"false"#, "Parameter deprecated"
+        "[1].schema.type" = r#""string""#, "Parameter schema type"
+        "[1].schema.format" = r#"null"#, "Parameter schema format"
+    };
+}
+
+mod derive_params_from_method_args_actix {
+    use actix_web::{web, HttpResponse, Responder};
+    use serde_json::json;
+
+    #[utoipa::path(
+        get,
+        path = "/foo/{id}/{digest}",
+        responses = [
+            (200, "success", String),
+        ],
+    )]
+    #[allow(unused)]
+    async fn get_foo_by_id(web::Path((id, digest)): web::Path<(i32, String)>) -> impl Responder {
+        HttpResponse::Ok().json(json!({ "foo": format!("{:?}{:?}", &id, &digest) }))
+    }
+}
+
+#[test]
+fn derive_params_from_method_args_actix_success() {
+    #[derive(OpenApi, Default)]
+    #[openapi(handler_files = [], handlers = [derive_params_from_method_args_actix::get_foo_by_id])]
+    struct ApiDoc;
+
+    let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
+    let parameters = common::get_json_path(&doc, "paths./foo/{id}/{digest}.get.parameters");
+
+    common::assert_json_array_len(parameters, 2);
+    assert_value! {parameters=>
+        "[0].in" = r#""path""#, "Parameter in"
+        "[0].name" = r#""id""#, "Parameter name"
+        "[0].description" = r#"null"#, "Parameter description"
+        "[0].required" = r#"true"#, "Parameter required"
+        "[0].deprecated" = r#"false"#, "Parameter deprecated"
+        "[0].schema.type" = r#""integer""#, "Parameter schema type"
+        "[0].schema.format" = r#""int32""#, "Parameter schema format"
+
+        "[1].in" = r#""path""#, "Parameter in"
+        "[1].name" = r#""digest""#, "Parameter name"
+        "[1].description" = r#"null"#, "Parameter description"
+        "[1].required" = r#"true"#, "Parameter required"
+        "[1].deprecated" = r#"false"#, "Parameter deprecated"
+        "[1].schema.type" = r#""string""#, "Parameter schema type"
+        "[1].schema.format" = r#"null"#, "Parameter schema format"
+    };
+}
diff --git a/tests/path_parameter_derive_test.rs b/tests/path_parameter_derive_test.rs
new file mode 100644
index 0000000..0253360
--- /dev/null
+++ b/tests/path_parameter_derive_test.rs
@@ -0,0 +1,256 @@
+#![cfg(not(feature = "actix_extras"))]
+use std::println;
+
+use serde_json::Value;
+use utoipa::OpenApi;
+
+mod common;
+
+mod derive_params_all_options {
+    /// Get foo by id
+    ///
+    /// Get foo by id long description
+    #[utoipa::path(
+        get,
+        path = "/foo/{id}",
+        responses = [
+            (200, "success", String),
+        ],
+        params = [
+            ("id" = i32, path, required, deprecated, description = "Search foos by ids"),
+        ]
+    )]
+    #[allow(unused)]
+    async fn get_foo_by_id(id: i32) -> i32 {
+        id
+    }
+}
+
+#[test]
+fn derive_path_parameters_with_all_options_success() {
+    #[derive(OpenApi, Default)]
+    #[openapi(handler_files = [], handlers = [derive_params_all_options::get_foo_by_id])]
+    struct ApiDoc;
+
+    let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
+    let parameters = common::get_json_path(&doc, "paths./foo/{id}.get.parameters");
+
+    common::assert_json_array_len(parameters, 1);
+    assert_value! {parameters=>
+        "[0].in" = r#""path""#, "Parameter in"
+        "[0].name" = r#""id""#, "Parameter name"
+        "[0].description" = r#""Search foos by ids""#, "Parameter description"
+        "[0].required" = r#"true"#, "Parameter required"
+        "[0].deprecated" = r#"true"#, "Parameter deprecated"
+        "[0].schema.type" = r#""integer""#, "Parameter schema type"
+        "[0].schema.format" = r#""int32""#, "Parameter schema format"
+    };
+}
+
+mod derive_params_minimal {
+    /// Get foo by id
+    ///
+    /// Get foo by id long description
+    #[utoipa::path(
+        get,
+        path = "/foo/{id}",
+        responses = [
+            (200, "success", String),
+        ],
+        params = [
+            ("id" = i32, description = "Search foos by ids"),
+        ]
+    )]
+    #[allow(unused)]
+    async fn get_foo_by_id(id: i32) -> i32 {
+        id
+    }
+}
+
+#[test]
+fn derive_path_parameters_minimal_success() {
+    #[derive(OpenApi, Default)]
+    #[openapi(handler_files = [], handlers = [derive_params_minimal::get_foo_by_id])]
+    struct ApiDoc;
+
+    let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
+    let parameters = common::get_json_path(&doc, "paths./foo/{id}.get.parameters");
+
+    common::assert_json_array_len(parameters, 1);
+    assert_value! {parameters=>
+        "[0].in" = r#""path""#, "Parameter in"
+        "[0].name" = r#""id""#, "Parameter name"
+        "[0].description" = r#""Search foos by ids""#, "Parameter description"
+        "[0].required" = r#"true"#, "Parameter required"
+        "[0].deprecated" = r#"false"#, "Parameter deprecated"
+        "[0].schema.type" = r#""integer""#, "Parameter schema type"
+        "[0].schema.format" = r#""int32""#, "Parameter schema format"
+    };
+}
+
+mod derive_params_multiple {
+    /// Get foo by id
+    ///
+    /// Get foo by id long description
+    #[utoipa::path(
+        get,
+        path = "/foo/{id}/{digest}",
+        responses = [
+            (200, "success", String),
+        ],
+        params = [
+            ("id" = i32, description = "Foo id"),
+            ("digest" = String, description = "Digest of foo"),
+        ]
+    )]
+    #[allow(unused)]
+    async fn get_foo_by_id(id: i32, digest: String) -> String {
+        format!("{:?}{:?}", &id, &digest)
+    }
+}
+
+#[test]
+fn derive_path_parameter_multiple_success() {
+    #[derive(OpenApi, Default)]
+    #[openapi(handler_files = [], handlers = [derive_params_multiple::get_foo_by_id])]
+    struct ApiDoc;
+
+    let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
+    let parameters = common::get_json_path(&doc, "paths./foo/{id}/{digest}.get.parameters");
+
+    common::assert_json_array_len(parameters, 2);
+    assert_value! {parameters=>
+        "[0].in" = r#""path""#, "Parameter in"
+        "[0].name" = r#""id""#, "Parameter name"
+        "[0].description" = r#""Foo id""#, "Parameter description"
+        "[0].required" = r#"true"#, "Parameter required"
+        "[0].deprecated" = r#"false"#, "Parameter deprecated"
+        "[0].schema.type" = r#""integer""#, "Parameter schema type"
+        "[0].schema.format" = r#""int32""#, "Parameter schema format"
+
+        "[1].in" = r#""path""#, "Parameter in"
+        "[1].name" = r#""digest""#, "Parameter name"
+        "[1].description" = r#""Digest of foo""#, "Parameter description"
+        "[1].required" = r#"true"#, "Parameter required"
+        "[1].deprecated" = r#"false"#, "Parameter deprecated"
+        "[1].schema.type" = r#""string""#, "Parameter schema type"
+        "[1].schema.format" = r#"null"#, "Parameter schema format"
+    };
+}
+
+mod mod_derive_parameters_all_types {
+    /// Get foo by id
+    ///
+    /// Get foo by id long description
+    #[utoipa::path(
+        get,
+        path = "/foo/{id}",
+        responses = [
+            (200, "success", String),
+        ],
+        params = [
+            ("id" = i32, path, description = "Foo id"),
+            ("since" = String, deprecated, required, query, description = "Datetime since"),
+            ("numbers" = [u64], query, description = "Foo numbers list"),
+            ("token" = String, header, deprecated, required, description = "Token of foo"),
+            ("cookieval" = String, cookie, deprecated, required, description = "Foo cookie"),
+        ]
+    )]
+    #[allow(unused)]
+    async fn get_foo_by_id(id: i32) -> i32 {
+        id
+    }
+}
+
+#[test]
+fn derive_parameters_with_all_types() {
+    #[derive(OpenApi, Default)]
+    #[openapi(handler_files = [], handlers = [mod_derive_parameters_all_types::get_foo_by_id])]
+    struct ApiDoc;
+
+    let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
+    let parameters = common::get_json_path(&doc, "paths./foo/{id}.get.parameters");
+
+    common::assert_json_array_len(parameters, 5);
+    assert_value! {parameters=>
+        "[0].in" = r#""path""#, "Parameter in"
+        "[0].name" = r#""id""#, "Parameter name"
+        "[0].description" = r#""Foo id""#, "Parameter description"
+        "[0].required" = r#"true"#, "Parameter required"
+        "[0].deprecated" = r#"false"#, "Parameter deprecated"
+        "[0].schema.type" = r#""integer""#, "Parameter schema type"
+        "[0].schema.format" = r#""int32""#, "Parameter schema format"
+
+        "[1].in" = r#""query""#, "Parameter in"
+        "[1].name" = r#""since""#, "Parameter name"
+        "[1].description" = r#""Datetime since""#, "Parameter description"
+        "[1].required" = r#"true"#, "Parameter required"
+        "[1].deprecated" = r#"true"#, "Parameter deprecated"
+        "[1].schema.type" = r#""string""#, "Parameter schema type"
+        "[1].schema.format" = r#"null"#, "Parameter schema format"
+
+        "[2].in" = r#""query""#, "Parameter in"
+        "[2].name" = r#""numbers""#, "Parameter name"
+        "[2].description" = r#""Foo numbers list""#, "Parameter description"
+        "[2].required" = r#"false"#, "Parameter required"
+        "[2].deprecated" = r#"false"#, "Parameter deprecated"
+        "[2].schema.type" = r#""array""#, "Parameter schema type"
+        "[2].schema.format" = r#"null"#, "Parameter schema format"
+        "[2].schema.items.type" = r#""integer""#, "Parameter schema items type"
+        "[2].schema.items.format" = r#""int64""#, "Parameter schema items format"
+
+        "[3].in" = r#""header""#, "Parameter in"
+        "[3].name" = r#""token""#, "Parameter name"
+        "[3].description" = r#""Token of foo""#, "Parameter description"
+        "[3].required" = r#"true"#, "Parameter required"
+        "[3].deprecated" = r#"true"#, "Parameter deprecated"
+        "[3].schema.type" = r#""string""#, "Parameter schema type"
+        "[3].schema.format" = r#"null"#, "Parameter schema format"
+
+        "[4].in" = r#""cookie""#, "Parameter in"
+        "[4].name" = r#""cookieval""#, "Parameter name"
+        "[4].description" = r#""Foo cookie""#, "Parameter description"
+        "[4].required" = r#"true"#, "Parameter required"
+        "[4].deprecated" = r#"true"#, "Parameter deprecated"
+        "[4].schema.type" = r#""string""#, "Parameter schema type"
+        "[4].schema.format" = r#"null"#, "Parameter schema format"
+    };
+}
+
+mod derive_params_without_args {
+    #[utoipa::path(
+        get,
+        path = "/foo/{id}",
+        responses = [
+            (200, "success", String),
+        ],
+        params = [
+            ("id" = i32, path, description = "Foo id"),
+        ]
+    )]
+    #[allow(unused)]
+    async fn get_foo_by_id() -> String {
+        "".to_string()
+    }
+}
+
+#[test]
+fn derive_params_without_fn_args() {
+    #[derive(OpenApi, Default)]
+    #[openapi(handler_files = [], handlers = [derive_params_without_args::get_foo_by_id])]
+    struct ApiDoc;
+
+    let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
+    let parameters = common::get_json_path(&doc, "paths./foo/{id}.get.parameters");
+
+    common::assert_json_array_len(parameters, 1);
+    assert_value! {parameters=>
+        "[0].in" = r#""path""#, "Parameter in"
+        "[0].name" = r#""id""#, "Parameter name"
+        "[0].description" = r#""Foo id""#, "Parameter description"
+        "[0].required" = r#"true"#, "Parameter required"
+        "[0].deprecated" = r#"false"#, "Parameter deprecated"
+        "[0].schema.type" = r#""integer""#, "Parameter schema type"
+        "[0].schema.format" = r#""int32""#, "Parameter schema format"
+    };
+}
diff --git a/tests/utoipa_gen_test.rs b/tests/utoipa_gen_test.rs
index ac8e0ff..2d0280a 100644
--- a/tests/utoipa_gen_test.rs
+++ b/tests/utoipa_gen_test.rs
@@ -1,7 +1,14 @@
-use actix_web::{delete, HttpResponse, Responder};
+#![cfg(feature = "actix_extras")]
+use actix_web::{get, web, HttpResponse, Responder};
+use serde::Deserialize;
 use serde_json::json;
 // use utoipa::openapi_spec;
-use utoipa::{path, OpenApi};
+use utoipa::OpenApi;
+
+#[derive(Deserialize)]
+struct Foo {
+    ids: Vec<i32>,
+}
 
 // mod api {
 //     use super::*;
@@ -9,20 +16,28 @@ use utoipa::{path, OpenApi};
 /// Delete foo entity
 ///
 /// Delete foo entity by what
-#[crate::path(responses = [
-    (200, "success", String),
-    (400, "my bad error", u64),
-    (404, "vault not found"),
-    (500, "internal server error")
-])]
-#[delete("/foo")]
+#[utoipa::path(
+    responses = [
+        (200, "success", String),
+        (400, "my bad error", u64),
+        (404, "vault not found"),
+        (500, "internal server error")
+    ],
+     params = [
+        ("ids" = [i32], query, description = "Search foos by ids"),
+   ]
+)]
+#[get("/foo/{_:.*}")]
 // #[deprecated = "this is deprecated"]
-async fn foo_delete() -> impl Responder {
-    HttpResponse::Ok().json(json!({"ok": "OK"}))
+// web::Path(id): web::Path<i32>
+async fn foo_delete(web::Query(foo): web::Query<Foo>) -> impl Responder {
+    let ids = foo.ids;
+    HttpResponse::Ok().json(json!({ "searched": ids }))
 }
 // }
 
 #[test]
+#[ignore = "this is just a test bed to run macros"]
 fn derive_openapi() {
     // use crate::api::__path_foo_delete;
     #[derive(OpenApi, Default)]