Repository: CosmWasm/sylvia
Pull request #388 (2024-06-20 09:24:23)

feat: Attributes forwarding to message enums and fields

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/CosmWasm/sylvia/pull/388?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm) Report
Attention: Patch coverage is `75.86207%` with `7 lines` in your changes missing coverage. Please review.
> Project coverage is 85.90%. Comparing base [(`f086c11`)](https://app.codecov.io/gh/CosmWasm/sylvia/commit/f086c116f59019c103577ce7429ec332c3c689c3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm) to head [(`7a776cf`)](https://app.codecov.io/gh/CosmWasm/sylvia/commit/7a776cfbf88b1b7bb6341b56031e0931f6d2e8c0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm).

| [Files](https://app.codecov.io/gh/CosmWasm/sylvia/pull/388?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm) | Patch % | Lines |
|---|---|---|
| [sylvia-derive/src/parser/attributes/msg.rs](https://app.codecov.io/gh/CosmWasm/sylvia/pull/388?src=pr&el=tree&filepath=sylvia-derive%2Fsrc%2Fparser%2Fattributes%2Fmsg.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm#diff-c3lsdmlhLWRlcml2ZS9zcmMvcGFyc2VyL2F0dHJpYnV0ZXMvbXNnLnJz) | 70.83% | [7 Missing :warning: ](https://app.codecov.io/gh/CosmWasm/sylvia/pull/388?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm) |

<details><summary>Additional details and impacted files</summary>


```diff
@@                    Coverage Diff                    @@
##           tkulik/feat/executors     #388      +/-   ##
=========================================================
- Coverage                  86.12%   85.90%   -0.22%     
=========================================================
  Files                         39       39              
  Lines                       2782     2782              
=========================================================
- Hits                        2396     2390       -6     
- Misses                       386      392       +6     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/CosmWasm/sylvia/pull/388?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm).

kulikthebird: > Please document this attribute in doc comments of `contract` macro. You can do it on separate PR.
> 
> LGTM

I've just created a new issue: https://github.com/CosmWasm/sylvia/issues/390

Patch:
diff --git a/sylvia-derive/src/message.rs b/sylvia-derive/src/message.rs
index 365f153..cb70e54 100644
--- a/sylvia-derive/src/message.rs
+++ b/sylvia-derive/src/message.rs
@@ -2,6 +2,7 @@ use crate::associated_types::{AssociatedTypes, ItemType, EXEC_TYPE, QUERY_TYPE};
 use crate::check_generics::{CheckGenerics, GetPath};
 use crate::crate_module;
 use crate::interfaces::Interfaces;
+use crate::parser::attributes::{MsgAttrForwarding, VariantAttrForwarding};
 use crate::parser::{
     parse_associated_custom_type, ContractErrorAttr, Custom, EntryPointArgs,
     FilteredOverrideEntryPoints, MsgAttr, MsgType, OverrideEntryPoint, ParsedSylviaAttributes,
@@ -38,6 +39,7 @@ pub struct StructMessage<'a> {
     result: &'a ReturnType,
     msg_attr: MsgAttr,
     custom: &'a Custom,
+    msg_attrs_to_forward: Vec<MsgAttrForwarding>,
 }
 
 impl<'a> StructMessage<'a> {
@@ -60,6 +62,12 @@ impl<'a> StructMessage<'a> {
         let (used_generics, unused_generics) = generics_checker.used_unused();
         let wheres = filter_wheres(&source.generics.where_clause, generics, &used_generics);
 
+        let msg_attrs_to_forward = ParsedSylviaAttributes::new(source.attrs.iter())
+            .msg_attrs_forward
+            .into_iter()
+            .filter(|attr| attr.msg_type == ty)
+            .collect();
+
         Some(Self {
             contract_type,
             fields,
@@ -71,6 +79,7 @@ impl<'a> StructMessage<'a> {
             result: &method.sig.output,
             msg_attr,
             custom,
+            msg_attrs_to_forward,
         })
     }
 
@@ -111,9 +120,12 @@ impl<'a> StructMessage<'a> {
     pub fn emit(&self) -> TokenStream {
         use MsgAttr::*;
 
+        let instantiate_msg = Ident::new("InstantiateMsg", self.function_name.span());
+        let migrate_msg = Ident::new("MigrateMsg", self.function_name.span());
+
         match &self.msg_attr {
-            Instantiate { name } => self.emit_struct(name),
-            Migrate { name } => self.emit_struct(name),
+            Instantiate { .. } => self.emit_struct(&instantiate_msg),
+            Migrate { .. } => self.emit_struct(&migrate_msg),
             _ => {
                 emit_error!(Span::mixed_site(), "Invalid message type");
                 quote! {}
@@ -135,6 +147,7 @@ impl<'a> StructMessage<'a> {
             result,
             msg_attr,
             custom,
+            msg_attrs_to_forward,
         } = self;
 
         let ctx_type = msg_attr
@@ -151,10 +164,12 @@ impl<'a> StructMessage<'a> {
         let where_clause = as_where_clause(wheres);
         let generics = emit_bracketed_generics(generics);
         let unused_generics = emit_bracketed_generics(unused_generics);
+        let msg_attrs_to_forward = msg_attrs_to_forward.iter().map(|attr| &attr.attrs);
 
         quote! {
             #[allow(clippy::derive_partial_eq_without_eq)]
             #[derive(#sylvia ::serde::Serialize, #sylvia ::serde::Deserialize, Clone, Debug, PartialEq, #sylvia ::schemars::JsonSchema)]
+            #( #[ #msg_attrs_to_forward ] )*
             #[serde(rename_all="snake_case")]
             pub struct #name #generics {
                 #(pub #fields,)*
@@ -184,6 +199,7 @@ pub struct EnumMessage<'a> {
     msg_ty: MsgType,
     resp_type: Type,
     query_type: Type,
+    msg_attrs_to_forward: Vec<MsgAttrForwarding>,
 }
 
 impl<'a> EnumMessage<'a> {
@@ -209,6 +225,12 @@ impl<'a> EnumMessage<'a> {
             .or(associated_query)
             .unwrap_or_else(Custom::default_type);
 
+        let msg_attrs_to_forward = ParsedSylviaAttributes::new(source.attrs.iter())
+            .msg_attrs_forward
+            .into_iter()
+            .filter(|attr| attr.msg_type == msg_ty)
+            .collect();
+
         Self {
             source,
             variants,
@@ -216,6 +238,7 @@ impl<'a> EnumMessage<'a> {
             msg_ty,
             resp_type,
             query_type,
+            msg_attrs_to_forward,
         }
     }
 
@@ -227,6 +250,7 @@ impl<'a> EnumMessage<'a> {
             msg_ty,
             resp_type,
             query_type,
+            msg_attrs_to_forward,
         } = self;
 
         let trait_name = &source.ident;
@@ -259,10 +283,12 @@ impl<'a> EnumMessage<'a> {
         let ep_name = msg_ty.emit_ep_name();
         let messages_fn_name = Ident::new(&format!("{}_messages", ep_name), enum_name.span());
         let derive_call = msg_ty.emit_derive_call();
+        let msg_attrs_to_forward = msg_attrs_to_forward.iter().map(|attr| &attr.attrs);
 
         quote! {
             #[allow(clippy::derive_partial_eq_without_eq)]
             #derive_call
+            #( #[ #msg_attrs_to_forward ] )*
             #[serde(rename_all="snake_case")]
             pub enum #unique_enum_name #bracketed_used_generics {
                 #(#msg_variants,)*
@@ -302,6 +328,7 @@ pub struct ContractEnumMessage<'a> {
     error: &'a ContractErrorAttr,
     custom: &'a Custom,
     where_clause: &'a Option<WhereClause>,
+    msg_attrs_to_forward: Vec<MsgAttrForwarding>,
 }
 
 impl<'a> ContractEnumMessage<'a> {
@@ -314,6 +341,11 @@ impl<'a> ContractEnumMessage<'a> {
     ) -> Self {
         let where_clause = &source.generics.where_clause;
         let variants = MsgVariants::new(source.as_variants(), msg_ty, generics, where_clause);
+        let msg_attrs_to_forward = ParsedSylviaAttributes::new(source.attrs.iter())
+            .msg_attrs_forward
+            .into_iter()
+            .filter(|attr| attr.msg_type == msg_ty)
+            .collect();
 
         Self {
             variants,
@@ -322,6 +354,7 @@ impl<'a> ContractEnumMessage<'a> {
             error,
             custom,
             where_clause,
+            msg_attrs_to_forward,
         }
     }
 
@@ -335,6 +368,7 @@ impl<'a> ContractEnumMessage<'a> {
             error,
             custom,
             where_clause,
+            msg_attrs_to_forward,
             ..
         } = self;
 
@@ -369,10 +403,12 @@ impl<'a> ContractEnumMessage<'a> {
             },
             false => quote! {},
         };
+        let msg_attrs_to_forward = msg_attrs_to_forward.iter().map(|attr| &attr.attrs);
 
         quote! {
             #[allow(clippy::derive_partial_eq_without_eq)]
             #[derive(#sylvia ::serde::Serialize, #sylvia ::serde::Deserialize, Clone, Debug, PartialEq, #sylvia ::schemars::JsonSchema, #derive_query )]
+            #( #[ #msg_attrs_to_forward ] )*
             #[serde(rename_all="snake_case")]
             pub enum #enum_name #bracketed_used_generics {
                 #(#variants,)*
@@ -409,6 +445,7 @@ pub struct MsgVariant<'a> {
     /// `returns` attribute.
     return_type: Option<Type>,
     msg_type: MsgType,
+    attrs_to_forward: Vec<VariantAttrForwarding>,
 }
 
 impl<'a> MsgVariant<'a> {
@@ -417,6 +454,7 @@ impl<'a> MsgVariant<'a> {
         sig: &'a Signature,
         generics_checker: &mut CheckGenerics<Generic>,
         msg_attr: MsgAttr,
+        attrs_to_forward: Vec<VariantAttrForwarding>,
     ) -> MsgVariant<'a>
     where
         Generic: GetPath + PartialEq,
@@ -427,7 +465,7 @@ impl<'a> MsgVariant<'a> {
         let fields = process_fields(sig, generics_checker);
         let msg_type = msg_attr.msg_type();
 
-        let return_type = if let MsgAttr::Query { resp_type } = msg_attr {
+        let return_type = if let MsgAttr::Query { resp_type, .. } = msg_attr {
             match resp_type {
                 Some(resp_type) => {
                     let resp_type = parse_quote! { #resp_type };
@@ -451,6 +489,7 @@ impl<'a> MsgVariant<'a> {
             fields,
             return_type,
             msg_type,
+            attrs_to_forward,
         }
     }
 
@@ -461,13 +500,16 @@ impl<'a> MsgVariant<'a> {
             fields,
             msg_type,
             return_type,
+            attrs_to_forward,
             ..
         } = self;
         let fields = fields.iter().map(MsgField::emit);
         let returns_attribute = msg_type.emit_returns_attribute(return_type);
+        let attrs_to_forward = attrs_to_forward.iter().map(|attr| &attr.attrs);
 
         quote! {
             #returns_attribute
+            #( #[ #attrs_to_forward ] )*
             #name {
                 #(#fields,)*
             }
@@ -570,6 +612,7 @@ where
         let variants: Vec<_> = source
             .filter_map(|variant_desc| {
                 let msg_attr: MsgAttr = variant_desc.attr_msg()?;
+                let attrs_to_forward = variant_desc.attrs_to_forward();
 
                 if msg_attr.msg_type() != msg_ty {
                     return None;
@@ -579,6 +622,7 @@ where
                     variant_desc.into_sig(),
                     &mut generics_checker,
                     msg_attr,
+                    attrs_to_forward,
                 ))
             })
             .collect();
diff --git a/sylvia-derive/src/parser/attributes/attr.rs b/sylvia-derive/src/parser/attributes/attr.rs
new file mode 100644
index 0000000..e694119
--- /dev/null
+++ b/sylvia-derive/src/parser/attributes/attr.rs
@@ -0,0 +1,71 @@
+use proc_macro2::{Span, TokenStream};
+use proc_macro_error::emit_error;
+use syn::parse::{Error, Parse, ParseStream, Parser};
+use syn::spanned::Spanned;
+use syn::{Ident, MetaList, Result, Token};
+
+use super::MsgType;
+
+#[derive(Clone, Debug)]
+pub struct VariantAttrForwarding {
+    pub attrs: TokenStream,
+    pub span: Span,
+}
+
+impl VariantAttrForwarding {
+    pub fn new(attr: &MetaList) -> Self {
+        VariantAttrForwarding {
+            attrs: attr.tokens.clone(),
+            span: attr.span(),
+        }
+    }
+}
+
+#[derive(Clone, Debug)]
+pub struct MsgAttrForwarding {
+    pub msg_type: MsgType,
+    pub attrs: TokenStream,
+}
+
+impl MsgAttrForwarding {
+    pub fn new(attr: &MetaList) -> Result<Self> {
+        MsgAttrForwarding::parse
+            .parse2(attr.tokens.clone())
+            .map_err(|err| {
+                emit_error!(attr.tokens.span(), err);
+                err
+            })
+    }
+}
+
+impl Parse for MsgAttrForwarding {
+    fn parse(input: ParseStream) -> Result<Self> {
+        let error_msg =
+            "Expected attribute of the form: `#[sv::msg_attr(msg_type, attribute_to_forward)]`";
+        let msg_type: Ident = input
+            .parse()
+            .map_err(|err| Error::new(err.span(), error_msg))?;
+        let _: Token![,] = input
+            .parse()
+            .map_err(|err| Error::new(err.span(), error_msg))?;
+        let attrs: TokenStream = input
+            .parse()
+            .map_err(|err| Error::new(err.span(), error_msg))?;
+        if attrs.is_empty() {
+            return Err(Error::new(attrs.span(), error_msg));
+        }
+        let msg_type = match msg_type.to_string().as_str() {
+            "exec" => MsgType::Exec,
+            "query" => MsgType::Query,
+            "instantiate" => MsgType::Instantiate,
+            "migrate" => MsgType::Migrate,
+            "reply" => MsgType::Reply,
+            "sudo" => MsgType::Sudo,
+            _ => return Err(Error::new(
+                msg_type.span(),
+                "Invalid message type, expected one of: `exec`, `query`, `instantiate`, `migrate`, `reply` or `sudo`.",
+            ))
+        };
+        Ok(Self { msg_type, attrs })
+    }
+}
diff --git a/sylvia-derive/src/parser/attributes/custom.rs b/sylvia-derive/src/parser/attributes/custom.rs
index 3d48a92..bccd015 100644
--- a/sylvia-derive/src/parser/attributes/custom.rs
+++ b/sylvia-derive/src/parser/attributes/custom.rs
@@ -1,7 +1,6 @@
 use proc_macro_error::emit_error;
 use syn::parse::{Parse, ParseStream, Parser};
-use syn::spanned::Spanned;
-use syn::{parse_quote, Attribute, Ident, Result, Token, Type};
+use syn::{parse_quote, Error, Ident, MetaList, Result, Token, Type};
 
 use crate::crate_module;
 
@@ -12,14 +11,11 @@ pub struct Custom {
 }
 
 impl Custom {
-    pub fn new(attr: &Attribute) -> Result<Self> {
-        attr.meta
-            .require_list()
-            .and_then(|meta| Custom::parse.parse2(meta.tokens.clone()))
-            .map_err(|err| {
-                emit_error!(attr.span(), err);
-                err
-            })
+    pub fn new(attr: &MetaList) -> Result<Self> {
+        Custom::parse.parse2(attr.tokens.clone()).map_err(|err| {
+            emit_error!(err.span(), err);
+            err
+        })
     }
 
     pub fn msg_or_default(&self) -> Type {
@@ -43,15 +39,17 @@ impl Parse for Custom {
         while !input.is_empty() {
             let ty: Ident = input.parse()?;
             let _: Token![=] = input.parse()?;
-            if ty == "msg" {
-                custom.msg = Some(input.parse()?)
-            } else if ty == "query" {
-                custom.query = Some(input.parse()?)
-            } else {
-                emit_error!(ty.span(), "Invalid custom type.";
-                    note = ty.span() => "Expected `#[sv::custom(msg=SomeMsg, query=SomeQuery)]`"
-                );
-            };
+            match ty.to_string().as_str() {
+                "msg" => custom.msg = Some(input.parse()?),
+                "query" => custom.query = Some(input.parse()?),
+                _ => {
+                    return Err(Error::new(
+                        ty.span(),
+                        "Invalid custom type.\n
+  = note: Expected `#[sv::custom(msg=SomeMsg, query=SomeQuery)]`.\n",
+                    ))
+                }
+            }
             if !input.peek(Token![,]) {
                 break;
             }
diff --git a/sylvia-derive/src/parser/attributes/error.rs b/sylvia-derive/src/parser/attributes/error.rs
index 960fe36..1c3d7f1 100644
--- a/sylvia-derive/src/parser/attributes/error.rs
+++ b/sylvia-derive/src/parser/attributes/error.rs
@@ -1,7 +1,6 @@
 use proc_macro_error::emit_error;
 use syn::parse::{Parse, ParseStream, Parser};
-use syn::spanned::Spanned;
-use syn::{parse_quote, Attribute, Result, Type};
+use syn::{parse_quote, MetaList, Result, Type};
 
 use crate::crate_module;
 
@@ -20,12 +19,11 @@ impl Default for ContractErrorAttr {
 }
 
 impl ContractErrorAttr {
-    pub fn new(attr: &Attribute) -> Result<Self> {
-        attr.meta
-            .require_list()
-            .and_then(|meta| ContractErrorAttr::parse.parse2(meta.tokens.clone()))
+    pub fn new(attr: &MetaList) -> Result<Self> {
+        ContractErrorAttr::parse
+            .parse2(attr.tokens.clone())
             .map_err(|err| {
-                emit_error!(attr.span(), err);
+                emit_error!(err.span(), err);
                 err
             })
     }
diff --git a/sylvia-derive/src/parser/attributes/messages.rs b/sylvia-derive/src/parser/attributes/messages.rs
index 0b9e8df..c88831b 100644
--- a/sylvia-derive/src/parser/attributes/messages.rs
+++ b/sylvia-derive/src/parser/attributes/messages.rs
@@ -4,7 +4,7 @@ use syn::fold::Fold;
 use syn::parse::{Parse, ParseStream, Parser};
 use syn::punctuated::Punctuated;
 use syn::spanned::Spanned;
-use syn::{parenthesized, Attribute, GenericArgument, Ident, Path, Result, Token};
+use syn::{parenthesized, Error, GenericArgument, Ident, MetaList, Path, Result, Token};
 
 use proc_macro_error::emit_error;
 
@@ -20,12 +20,11 @@ pub struct ContractMessageAttr {
 }
 
 impl ContractMessageAttr {
-    pub fn new(attr: &Attribute) -> Result<Self> {
-        attr.meta
-            .require_list()
-            .and_then(|meta| ContractMessageAttr::parse.parse2(meta.tokens.clone()))
+    pub fn new(attr: &MetaList) -> Result<Self> {
+        ContractMessageAttr::parse
+            .parse2(attr.tokens.clone())
             .map_err(|err| {
-                emit_error!(attr.span(), err);
+                emit_error!(err.span(), err);
                 err
             })
     }
@@ -61,10 +60,13 @@ fn interface_has_custom(content: ParseStream) -> Result<Customs> {
         match custom.get_ident() {
             Some(ident) if ident == "msg" => customs.has_msg = true,
             Some(ident) if ident == "query" => customs.has_query = true,
-            _ => emit_error!(custom.span(),
-                    "Invalid custom attribute, expected one or both of: [`msg`, `query`]";
-                    note = "Expected attribute to be in form `#[sv::messages(interface: custom(msg, query))]`."
-            ),
+            _ => {
+                return Err(Error::new(
+                    custom.span(),
+                    "Invalid custom attribute, expected one or both of: [`msg`, `query`]\n
+  = note: Expected attribute to be in form `#[sv::messages(interface: custom(msg, query))]`.\n",
+                ))
+            }
         }
         if !custom_content.peek(Token![,]) {
             break;
@@ -103,10 +105,10 @@ impl Parse for ContractMessageAttr {
         };
         let customs = interface_has_custom(input)?;
         if !input.is_empty() {
-            emit_error!(input.span(),
-                "Unexpected tokens inside `sv::messages` attribtue.";
-                note = "Maximal supported form of attribute: `#[sv::messages(interface::path as InterfaceName: custom(msg, query))]`."
-            )
+            return Err(Error::new(input.span(),
+                "Unexpected tokens inside `sv::messages` attribtue.\n
+  = note: Maximal supported form of attribute: `#[sv::messages(interface::path as InterfaceName: custom(msg, query))]`.\n"
+            ));
         }
         Ok(Self {
             module,
diff --git a/sylvia-derive/src/parser/attributes/mod.rs b/sylvia-derive/src/parser/attributes/mod.rs
index e5f7b8c..b854623 100644
--- a/sylvia-derive/src/parser/attributes/mod.rs
+++ b/sylvia-derive/src/parser/attributes/mod.rs
@@ -1,13 +1,15 @@
 use proc_macro_error::emit_error;
 use syn::spanned::Spanned;
-use syn::{Attribute, PathSegment};
+use syn::{Attribute, MetaList, PathSegment};
 
+pub mod attr;
 pub mod custom;
 pub mod error;
 pub mod messages;
 pub mod msg;
 pub mod override_entry_point;
 
+pub use attr::{MsgAttrForwarding, VariantAttrForwarding};
 pub use custom::Custom;
 pub use error::ContractErrorAttr;
 pub use messages::{ContractMessageAttr, Customs};
@@ -23,6 +25,8 @@ pub enum SylviaAttribute {
     Messages,
     Msg,
     OverrideEntryPoint,
+    VariantAttrs,
+    MsgAttrs,
 }
 
 impl SylviaAttribute {
@@ -42,6 +46,8 @@ impl SylviaAttribute {
             "messages" => Some(Self::Messages),
             "msg" => Some(Self::Msg),
             "override_entry_point" => Some(Self::OverrideEntryPoint),
+            "attr" => Some(Self::VariantAttrs),
+            "msg_attr" => Some(Self::MsgAttrs),
             _ => None,
         }
     }
@@ -57,6 +63,8 @@ pub struct ParsedSylviaAttributes {
     pub messages_attrs: Vec<ContractMessageAttr>,
     pub msg_attr: Option<MsgAttr>,
     pub override_entry_point_attrs: Vec<OverrideEntryPoint>,
+    pub variant_attrs_forward: Vec<VariantAttrForwarding>,
+    pub msg_attrs_forward: Vec<MsgAttrForwarding>,
 }
 
 impl ParsedSylviaAttributes {
@@ -64,14 +72,31 @@ impl ParsedSylviaAttributes {
         let mut result = Self::default();
         for attr in attrs {
             let sylvia_attr = SylviaAttribute::new(attr);
-            if let Some(sylvia_attr) = sylvia_attr {
+            let attr_content = attr.meta.require_list();
+
+            if let (Some(sylvia_attr), Ok(attr)) = (sylvia_attr, &attr_content) {
                 result.match_attribute(&sylvia_attr, attr);
             }
         }
+
+        if let Some(attr) = result.variant_attrs_forward.first() {
+            if let Some(MsgAttr::Instantiate) = result.msg_attr {
+                emit_error!(
+                    attr.span, "The attribute `sv::attr` is not supported for `instantiate`";
+                    note = "Message `instantiate` is a structure, use `#[sv::msg_attr] instead`";
+                );
+            } else if let Some(MsgAttr::Migrate) = result.msg_attr {
+                emit_error!(
+                    attr.span, "The attribute `sv::attr` is not supported for `migrate`";
+                    note = "Message `migrate` is a structure, use `#[sv::msg_attr] instead`";
+                );
+            }
+        }
+
         result
     }
 
-    fn match_attribute(&mut self, attribute_type: &SylviaAttribute, attr: &Attribute) {
+    fn match_attribute(&mut self, attribute_type: &SylviaAttribute, attr: &MetaList) {
         match attribute_type {
             SylviaAttribute::Custom => {
                 if self.custom_attr.is_none() {
@@ -122,6 +147,15 @@ impl ParsedSylviaAttributes {
                     self.override_entry_point_attrs.push(override_entry_point)
                 }
             }
+            SylviaAttribute::VariantAttrs => {
+                self.variant_attrs_forward
+                    .push(VariantAttrForwarding::new(attr));
+            }
+            SylviaAttribute::MsgAttrs => {
+                if let Ok(message_attrs) = MsgAttrForwarding::new(attr) {
+                    self.msg_attrs_forward.push(message_attrs);
+                }
+            }
         }
     }
 }
diff --git a/sylvia-derive/src/parser/attributes/msg.rs b/sylvia-derive/src/parser/attributes/msg.rs
index 1ff0f00..857081e 100644
--- a/sylvia-derive/src/parser/attributes/msg.rs
+++ b/sylvia-derive/src/parser/attributes/msg.rs
@@ -1,9 +1,6 @@
-use proc_macro2::Punct;
-use syn::parse::{Error, Parse, ParseBuffer, ParseStream, Parser};
-use syn::spanned::Spanned;
-use syn::{Attribute, Ident, Result};
-
 use proc_macro_error::emit_error;
+use syn::parse::{Error, Parse, ParseStream, Parser};
+use syn::{Ident, MetaList, Result, Token};
 
 /// Type of message to be generated
 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
@@ -16,26 +13,52 @@ pub enum MsgType {
     Sudo,
 }
 
+#[derive(Default)]
+struct ArgumentParser {
+    pub resp_type: Option<Ident>,
+}
+
+impl Parse for ArgumentParser {
+    fn parse(input: ParseStream) -> Result<Self> {
+        let mut result = Self::default();
+        while input.peek2(Ident) {
+            let _: Token![,] = input.parse()?;
+            let arg_type: Ident = input.parse()?;
+            let _: Token![=] = input.parse()?;
+            match arg_type.to_string().as_str() {
+                "resp" => {
+                    let resp_type: Ident = input.parse()?;
+                    result.resp_type = Some(resp_type);
+                }
+                _ => {
+                    return Err(Error::new(
+                        input.span(),
+                        "Invalid argument type, expected `resp` or no argument.",
+                    ))
+                }
+            }
+        }
+        Ok(result)
+    }
+}
+
 /// Parsed representation of `#[sv::msg(...)]` attribute.
 #[derive(Clone)]
 pub enum MsgAttr {
     Exec,
     Query { resp_type: Option<Ident> },
-    Instantiate { name: Ident },
-    Migrate { name: Ident },
+    Instantiate,
+    Migrate,
     Reply,
     Sudo,
 }
 
 impl MsgAttr {
-    pub fn new(attr: &Attribute) -> Result<Self> {
-        attr.meta
-            .require_list()
-            .and_then(|meta| MsgAttr::parse.parse2(meta.tokens.clone()))
-            .map_err(|err| {
-                emit_error!(attr.span(), err);
-                err
-            })
+    pub fn new(attr: &MetaList) -> Result<Self> {
+        MsgAttr::parse.parse2(attr.tokens.clone()).map_err(|err| {
+            emit_error!(err.span(), err);
+            err
+        })
     }
 }
 
@@ -46,27 +69,14 @@ impl PartialEq<MsgType> for MsgAttr {
 }
 
 impl MsgAttr {
-    fn parse_query(content: &ParseBuffer) -> Result<Self> {
-        if content.peek2(Ident) {
-            let _: Punct = content.parse()?;
-            let _: Ident = content.parse()?;
-            let _: Punct = content.parse()?;
-            let resp_type: Option<Ident> = Some(content.parse()?);
-            Ok(Self::Query { resp_type })
-        } else {
-            Ok(Self::Query { resp_type: None })
-        }
-    }
-
     pub fn msg_type(&self) -> MsgType {
-        MsgType::Exec.as_accessor_name();
         match self {
-            Self::Exec => MsgType::Exec,
+            Self::Exec { .. } => MsgType::Exec,
             Self::Query { .. } => MsgType::Query,
             Self::Instantiate { .. } => MsgType::Instantiate,
             Self::Migrate { .. } => MsgType::Migrate,
-            Self::Reply => MsgType::Reply,
-            Self::Sudo => MsgType::Sudo,
+            Self::Reply { .. } => MsgType::Reply,
+            Self::Sudo { .. } => MsgType::Sudo,
         }
     }
 }
@@ -74,26 +84,20 @@ impl MsgAttr {
 impl Parse for MsgAttr {
     fn parse(input: ParseStream) -> Result<Self> {
         let ty: Ident = input.parse()?;
+        let ArgumentParser { resp_type } = ArgumentParser::parse(input)?;
 
-        if ty == "exec" {
-            Ok(Self::Exec)
-        } else if ty == "query" {
-            Self::parse_query(input)
-        } else if ty == "instantiate" {
-            let name = Ident::new("InstantiateMsg", input.span());
-            Ok(Self::Instantiate { name })
-        } else if ty == "migrate" {
-            let name = Ident::new("MigrateMsg", input.span());
-            Ok(Self::Migrate { name })
-        } else if ty == "reply" {
-            Ok(Self::Reply)
-        } else if ty == "sudo" {
-            Ok(Self::Sudo)
-        } else {
-            Err(Error::new(
+        let result = match ty.to_string().as_str() {
+            "exec" => Self::Exec,
+            "query" => Self::Query { resp_type },
+            "instantiate" => Self::Instantiate,
+            "migrate" => Self::Migrate,
+            "reply" => Self::Reply,
+            "sudo" => Self::Sudo,
+            _ => return Err(Error::new(
                 input.span(),
                 "Invalid message type, expected one of: `exec`, `query`, `instantiate`, `migrate`, `reply` or `sudo`.",
             ))
-        }
+        };
+        Ok(result)
     }
 }
diff --git a/sylvia-derive/src/parser/attributes/override_entry_point.rs b/sylvia-derive/src/parser/attributes/override_entry_point.rs
index ce40c92..a085e17 100644
--- a/sylvia-derive/src/parser/attributes/override_entry_point.rs
+++ b/sylvia-derive/src/parser/attributes/override_entry_point.rs
@@ -2,8 +2,7 @@ use proc_macro2::TokenStream;
 use proc_macro_error::emit_error;
 use quote::quote;
 use syn::parse::{Error, Parse, ParseStream, Parser};
-use syn::spanned::Spanned;
-use syn::{parenthesized, Attribute, Ident, Path, Result, Token, Type};
+use syn::{parenthesized, Ident, MetaList, Path, Result, Token, Type};
 
 use crate::crate_module;
 use crate::parser::MsgType;
@@ -16,12 +15,11 @@ pub struct OverrideEntryPoint {
 }
 
 impl OverrideEntryPoint {
-    pub fn new(attr: &Attribute) -> Result<Self> {
-        attr.meta
-            .require_list()
-            .and_then(|meta| OverrideEntryPoint::parse.parse2(meta.tokens.clone()))
+    pub fn new(attr: &MetaList) -> Result<Self> {
+        OverrideEntryPoint::parse
+            .parse2(attr.tokens.clone())
             .map_err(|err| {
-                emit_error!(attr.span(), err);
+                emit_error!(err.span(), err);
                 err
             })
     }
diff --git a/sylvia-derive/src/variant_descs.rs b/sylvia-derive/src/variant_descs.rs
index 4f0a969..de04dcc 100644
--- a/sylvia-derive/src/variant_descs.rs
+++ b/sylvia-derive/src/variant_descs.rs
@@ -1,15 +1,23 @@
+use crate::parser::attributes::VariantAttrForwarding;
 use crate::parser::{MsgAttr, ParsedSylviaAttributes};
 use syn::{Attribute, ImplItem, ItemImpl, ItemTrait, Signature, TraitItem};
 
 pub struct VariantDesc<'a> {
     msg_attr: Option<MsgAttr>,
+    attrs_to_forward: Vec<VariantAttrForwarding>,
     sig: &'a Signature,
 }
 
 impl<'a> VariantDesc<'a> {
     pub fn new(attrs: &'a [Attribute], sig: &'a Signature) -> Self {
-        let msg_attr = ParsedSylviaAttributes::new(attrs.iter()).msg_attr;
-        Self { msg_attr, sig }
+        let sylvia_params = ParsedSylviaAttributes::new(attrs.iter());
+        let attrs_to_forward = sylvia_params.variant_attrs_forward;
+        let msg_attr = sylvia_params.msg_attr;
+        Self {
+            msg_attr,
+            attrs_to_forward,
+            sig,
+        }
     }
 
     pub fn into_sig(self) -> &'a Signature {
@@ -19,6 +27,10 @@ impl<'a> VariantDesc<'a> {
     pub fn attr_msg(&self) -> Option<MsgAttr> {
         self.msg_attr.clone()
     }
+
+    pub fn attrs_to_forward(&self) -> Vec<VariantAttrForwarding> {
+        self.attrs_to_forward.clone()
+    }
 }
 
 pub type VariantDescs<'a> = Box<dyn Iterator<Item = VariantDesc<'a>> + 'a>;


Test patch:
diff --git a/sylvia/tests/messages_generation.rs b/sylvia/tests/messages_generation.rs
index dec442d..26feabb 100644
--- a/sylvia/tests/messages_generation.rs
+++ b/sylvia/tests/messages_generation.rs
@@ -1,3 +1,6 @@
+use std::fmt::Debug;
+use std::str::FromStr;
+
 use cosmwasm_std::{Addr, CustomQuery, Decimal};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -16,18 +19,24 @@ pub mod interface {
     use cosmwasm_std::{Addr, Decimal, Response, StdError};
     use sylvia::interface;
     use sylvia::types::{ExecCtx, QueryCtx, SudoCtx};
+    use thiserror::Error;
 
     use crate::QueryResult;
 
     #[interface]
+    #[sv::msg_attr(exec, derive(PartialOrd, Error))]
+    #[sv::msg_attr(query, derive(PartialOrd, Error))]
+    #[sv::msg_attr(sudo, derive(PartialOrd, Error))]
     #[sv::custom(msg=cosmwasm_std::Empty, query=cosmwasm_std::Empty)]
     pub trait Interface {
         type Error: From<StdError>;
 
         #[sv::msg(exec)]
+        #[sv::attr(error("NoArgsExecution"))]
         fn no_args_execution(&self, ctx: ExecCtx) -> Result<Response, Self::Error>;
 
         #[sv::msg(exec)]
+        #[sv::attr(error("ArgumentedExecution"))]
         fn argumented_execution(
             &self,
             ctx: ExecCtx,
@@ -37,15 +46,19 @@ pub mod interface {
         ) -> Result<Response, Self::Error>;
 
         #[sv::msg(query)]
+        #[sv::attr(error("NoArgsQuery"))]
         fn no_args_query(&self, ctx: QueryCtx) -> Result<QueryResult, Self::Error>;
 
         #[sv::msg(query)]
+        #[sv::attr(error("ArgumentedQuery"))]
         fn argumented_query(&self, ctx: QueryCtx, user: Addr) -> Result<QueryResult, Self::Error>;
 
         #[sv::msg(sudo)]
+        #[sv::attr(error("NoArgsSudo"))]
         fn no_args_sudo(&self, ctx: SudoCtx) -> Result<Response, Self::Error>;
 
         #[sv::msg(sudo)]
+        #[sv::attr(error("ArgumentedSudo"))]
         fn argumented_sudo(&self, ctx: SudoCtx, user: Addr) -> Result<Response, Self::Error>;
     }
 }
@@ -55,6 +68,7 @@ mod contract {
     use sylvia::contract;
     use sylvia::types::{ExecCtx, InstantiateCtx, MigrateCtx, QueryCtx, ReplyCtx, SudoCtx};
     use sylvia_derive::entry_points;
+    use thiserror::Error;
 
     use crate::{MyQuery, QueryResult};
 
@@ -64,6 +78,13 @@ mod contract {
     #[contract]
     #[allow(dead_code)]
     #[sv::custom(query=MyQuery)]
+    #[sv::msg_attr(exec, derive(PartialOrd, Error))]
+    #[sv::msg_attr(query, derive(PartialOrd, Error))]
+    #[sv::msg_attr(sudo, derive(PartialOrd, Error))]
+    #[sv::msg_attr(instantiate, derive(PartialOrd, Error))]
+    #[sv::msg_attr(instantiate, error("Instantiate"))]
+    #[sv::msg_attr(migrate, derive(PartialOrd, Error))]
+    #[sv::msg_attr(migrate, error("Migrate"))]
     impl Contract {
         #[allow(clippy::new_without_default)]
         #[allow(dead_code)]
@@ -82,11 +103,13 @@ mod contract {
         }
 
         #[sv::msg(exec)]
+        #[sv::attr(error("NoArgsExecution"))]
         fn no_args_execution(&self, _ctx: ExecCtx<MyQuery>) -> StdResult<Response> {
             Ok(Response::new())
         }
 
         #[sv::msg(exec)]
+        #[sv::attr(error("ArgumentedExecution"))]
         fn argumented_execution(
             &self,
             _ctx: ExecCtx<MyQuery>,
@@ -98,11 +121,13 @@ mod contract {
         }
 
         #[sv::msg(query)]
+        #[sv::attr(error("NoArgsQuery"))]
         fn no_args_query(&self, _ctx: QueryCtx<MyQuery>) -> StdResult<QueryResult> {
             Ok(QueryResult {})
         }
 
         #[sv::msg(query)]
+        #[sv::attr(error("ArgumentedQuery"))]
         fn argumented_query(&self, _ctx: QueryCtx<MyQuery>, _user: Addr) -> StdResult<QueryResult> {
             Ok(QueryResult {})
         }
@@ -113,11 +138,13 @@ mod contract {
         }
 
         #[sv::msg(sudo)]
+        #[sv::attr(error("NoArgsSudo"))]
         fn no_args_sudo(&self, _ctx: SudoCtx<MyQuery>) -> StdResult<Response> {
             Ok(Response::new())
         }
 
         #[sv::msg(sudo)]
+        #[sv::attr(error("ArgumentedSudo"))]
         fn argumented_sudo(&self, _ctx: SudoCtx<MyQuery>, _user: Addr) -> StdResult<Response> {
             Ok(Response::new())
         }
@@ -207,3 +234,112 @@ fn entry_points_generation() {
     .with_reply(entry_points::reply)
     .with_sudo(entry_points::sudo);
 }
+
+#[test]
+fn attributes_forwarding_for_message_structs_and_enums() {
+    let exec_msg_contract = contract::sv::ExecMsg::NoArgsExecution {};
+    let _partial_ord_implemented = exec_msg_contract < exec_msg_contract;
+
+    let query_msg_contract = contract::sv::QueryMsg::NoArgsQuery {};
+    let _partial_ord_implemented = query_msg_contract < query_msg_contract;
+
+    let sudo_msg_contract = contract::sv::SudoMsg::NoArgsSudo {};
+    let _partial_ord_implemented = sudo_msg_contract < sudo_msg_contract;
+
+    let instantiate_msg_contract = contract::sv::InstantiateMsg {};
+    let _partial_ord_implemented = instantiate_msg_contract < instantiate_msg_contract;
+
+    let migrate_msg_contract = contract::sv::MigrateMsg {};
+    let _partial_ord_implemented = migrate_msg_contract < migrate_msg_contract;
+
+    let exec_msg_interface = interface::sv::ExecMsg::NoArgsExecution {};
+    let _partial_ord_implemented = exec_msg_interface < exec_msg_interface;
+
+    let query_msg_interface = interface::sv::QueryMsg::NoArgsQuery {};
+    let _partial_ord_implemented = query_msg_interface < query_msg_interface;
+
+    let sudo_msg_interface = interface::sv::SudoMsg::NoArgsSudo {};
+    let _partial_ord_implemented = sudo_msg_interface < sudo_msg_interface;
+}
+
+#[test]
+fn attributes_forwarding_for_message_variants() {
+    // By adding #[sv::msg_attr(exec, derive(Error))] and #[sv::attr(error("NoArgsExecution"))]
+    // for execution message and its variants, it should be possible to call `to_string` method
+    // on each exec variant. Without this derive, the method is not implemented.
+    // Analogous for the other message enums and structs.
+
+    assert_eq!(
+        contract::sv::ExecMsg::NoArgsExecution {}.to_string(),
+        "NoArgsExecution"
+    );
+    assert_eq!(
+        contract::sv::ExecMsg::ArgumentedExecution {
+            _addr: Addr::unchecked("input"),
+            _coef: Decimal::from_str("0").unwrap(),
+            _desc: "".to_owned(),
+        }
+        .to_string(),
+        "ArgumentedExecution"
+    );
+    assert_eq!(
+        contract::sv::QueryMsg::NoArgsQuery {}.to_string(),
+        "NoArgsQuery"
+    );
+    assert_eq!(
+        contract::sv::QueryMsg::ArgumentedQuery {
+            _user: Addr::unchecked("input"),
+        }
+        .to_string(),
+        "ArgumentedQuery"
+    );
+    assert_eq!(
+        contract::sv::SudoMsg::NoArgsSudo {}.to_string(),
+        "NoArgsSudo"
+    );
+    assert_eq!(
+        contract::sv::SudoMsg::ArgumentedSudo {
+            _user: Addr::unchecked("input"),
+        }
+        .to_string(),
+        "ArgumentedSudo"
+    );
+    assert_eq!(contract::sv::InstantiateMsg {}.to_string(), "Instantiate");
+    assert_eq!(contract::sv::MigrateMsg {}.to_string(), "Migrate");
+
+    assert_eq!(
+        interface::sv::ExecMsg::NoArgsExecution {}.to_string(),
+        "NoArgsExecution"
+    );
+    assert_eq!(
+        interface::sv::ExecMsg::ArgumentedExecution {
+            addr: Addr::unchecked("input"),
+            coef: Decimal::from_str("0").unwrap(),
+            desc: "".to_owned(),
+        }
+        .to_string(),
+        "ArgumentedExecution"
+    );
+    assert_eq!(
+        interface::sv::QueryMsg::NoArgsQuery {}.to_string(),
+        "NoArgsQuery"
+    );
+    assert_eq!(
+        interface::sv::QueryMsg::ArgumentedQuery {
+            user: Addr::unchecked("input"),
+        }
+        .to_string(),
+        "ArgumentedQuery"
+    );
+    assert_eq!(
+        interface::sv::SudoMsg::NoArgsSudo {}.to_string(),
+        "NoArgsSudo"
+    );
+    assert_eq!(
+        interface::sv::SudoMsg::ArgumentedSudo {
+            user: Addr::unchecked("input"),
+        }
+        .to_string(),
+        "ArgumentedSudo"
+    );
+}
diff --git a/sylvia/tests/ui/attributes/attr_forwarding/invalid_sv_attr.rs b/sylvia/tests/ui/attributes/attr_forwarding/invalid_sv_attr.rs
new file mode 100644
index 0000000..34db261
--- /dev/null
+++ b/sylvia/tests/ui/attributes/attr_forwarding/invalid_sv_attr.rs
@@ -0,0 +1,28 @@
+#![allow(unused_imports)]
+use sylvia::cw_std::{Response, StdResult};
+use sylvia::types::{ExecCtx, InstantiateCtx};
+
+pub struct Contract;
+
+#[sylvia::contract]
+#[sv::msg_attr(exec,)]
+#[sv::msg_attr(random_msg, PartialOrd)]
+#[sv::msg_attr(exec PartialOrd)]
+#[sv::msg_attr(PartialOrd)]
+impl Contract {
+    pub const fn new() -> Self {
+        Self
+    }
+
+    #[sv::msg(instantiate)]
+    fn instantiate(&self, _ctx: InstantiateCtx) -> StdResult<Response> {
+        Ok(Response::new())
+    }
+
+    #[sv::msg(exec)]
+    fn exec(&self, _ctx: ExecCtx) -> StdResult<Response> {
+        Ok(Response::new())
+    }
+}
+
+fn main() {}
diff --git a/sylvia/tests/ui/attributes/attr_forwarding/invalid_sv_attr.stderr b/sylvia/tests/ui/attributes/attr_forwarding/invalid_sv_attr.stderr
new file mode 100644
index 0000000..6dda730
--- /dev/null
+++ b/sylvia/tests/ui/attributes/attr_forwarding/invalid_sv_attr.stderr
@@ -0,0 +1,23 @@
+error: Expected attribute of the form: `#[sv::msg_attr(msg_type, attribute_to_forward)]`
+ --> tests/ui/attributes/attr_forwarding/invalid_sv_attr.rs:8:16
+  |
+8 | #[sv::msg_attr(exec,)]
+  |                ^^^^
+
+error: Invalid message type, expected one of: `exec`, `query`, `instantiate`, `migrate`, `reply` or `sudo`.
+ --> tests/ui/attributes/attr_forwarding/invalid_sv_attr.rs:9:16
+  |
+9 | #[sv::msg_attr(random_msg, PartialOrd)]
+  |                ^^^^^^^^^^
+
+error: Expected attribute of the form: `#[sv::msg_attr(msg_type, attribute_to_forward)]`
+  --> tests/ui/attributes/attr_forwarding/invalid_sv_attr.rs:10:16
+   |
+10 | #[sv::msg_attr(exec PartialOrd)]
+   |                ^^^^
+
+error: Expected attribute of the form: `#[sv::msg_attr(msg_type, attribute_to_forward)]`
+  --> tests/ui/attributes/attr_forwarding/invalid_sv_attr.rs:11:16
+   |
+11 | #[sv::msg_attr(PartialOrd)]
+   |                ^^^^^^^^^^
diff --git a/sylvia/tests/ui/attributes/custom/invalid_custom.stderr b/sylvia/tests/ui/attributes/custom/invalid_custom.stderr
index 2f7eafc..83edaa7 100644
--- a/sylvia/tests/ui/attributes/custom/invalid_custom.stderr
+++ b/sylvia/tests/ui/attributes/custom/invalid_custom.stderr
@@ -1,14 +1,7 @@
 error: Invalid custom type.
 
-         = note: Expected `#[sv::custom(msg=SomeMsg, query=SomeQuery)]`
-
+         = note: Expected `#[sv::custom(msg=SomeMsg, query=SomeQuery)]`.
   --> tests/ui/attributes/custom/invalid_custom.rs:12:14
    |
 12 | #[sv::custom(mgs=MyMsg)]
    |              ^^^
-
-error: unexpected token
-  --> tests/ui/attributes/custom/invalid_custom.rs:12:1
-   |
-12 | #[sv::custom(mgs=MyMsg)]
-   | ^
diff --git a/sylvia/tests/ui/attributes/messages/invalid_custom.stderr b/sylvia/tests/ui/attributes/messages/invalid_custom.stderr
index 8fa4f57..3c00426 100644
--- a/sylvia/tests/ui/attributes/messages/invalid_custom.stderr
+++ b/sylvia/tests/ui/attributes/messages/invalid_custom.stderr
@@ -1,7 +1,6 @@
 error: Invalid custom attribute, expected one or both of: [`msg`, `query`]
 
          = note: Expected attribute to be in form `#[sv::messages(interface: custom(msg, query))]`.
-
   --> tests/ui/attributes/messages/invalid_custom.rs:21:34
    |
 21 | #[sv::messages(interface: custom(wrong))]
diff --git a/sylvia/tests/ui/attributes/messages/unexpected_token.stderr b/sylvia/tests/ui/attributes/messages/unexpected_token.stderr
index 6e1bcc9..e089e0b 100644
--- a/sylvia/tests/ui/attributes/messages/unexpected_token.stderr
+++ b/sylvia/tests/ui/attributes/messages/unexpected_token.stderr
@@ -1,14 +1,7 @@
 error: Unexpected tokens inside `sv::messages` attribtue.
 
          = note: Maximal supported form of attribute: `#[sv::messages(interface::path as InterfaceName: custom(msg, query))]`.
-
   --> tests/ui/attributes/messages/unexpected_token.rs:24:25
    |
 24 | #[sv::messages(interface(Empty))]
    |                         ^
-
-error: unexpected token
-  --> tests/ui/attributes/messages/unexpected_token.rs:24:1
-   |
-24 | #[sv::messages(interface(Empty))]
-   | ^