Repository: CosmWasm/sylvia
Pull request #70 (2022-11-04 15:29:28)

Allow nested return types in Queries
F.e. Option<QueryResponse> is now supported for Query return type

codecov[bot]: # [Codecov](https://codecov.io/gh/CosmWasm/sylvia/pull/70?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm) Report
> Merging [#70](https://codecov.io/gh/CosmWasm/sylvia/pull/70?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm) (389101c) into [main](https://codecov.io/gh/CosmWasm/sylvia/commit/27fe3cc626a87a52fcbee0fdf622bc4144854b6e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm) (27fe3cc) will **increase** coverage by `0.28%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##             main      #70      +/-   ##
==========================================
+ Coverage   67.79%   68.08%   +0.28%     
==========================================
  Files          21       21              
  Lines        1003     1012       +9     
==========================================
+ Hits          680      689       +9     
  Misses        323      323              
```


| [Impacted Files](https://codecov.io/gh/CosmWasm/sylvia/pull/70?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm) | Coverage Δ | |
|---|---|---|
| [sylvia-derive/src/message.rs](https://codecov.io/gh/CosmWasm/sylvia/pull/70/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm#diff-c3lsdmlhLWRlcml2ZS9zcmMvbWVzc2FnZS5ycw==) | `50.92% <100.00%> (+1.04%)` | :arrow_up: |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm)


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 63f6d84..779f1a4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -43,7 +43,7 @@ jobs:
       - name: Install Rust
         uses: actions-rs/toolchain@v1
         with:
-          toolchain: 1.63.0
+          toolchain: 1.65.0
           target: wasm32-unknown-unknown
           profile: minimal
           override: true
diff --git a/Cargo.lock b/Cargo.lock
index 0f02f60..f7763dd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -883,6 +883,7 @@ dependencies = [
  "serde",
  "serde-cw-value",
  "sylvia-derive",
+ "thiserror",
 ]
 
 [[package]]
diff --git a/sylvia-derive/src/lib.rs b/sylvia-derive/src/lib.rs
index 7c587a5..7d8e7f9 100644
--- a/sylvia-derive/src/lib.rs
+++ b/sylvia-derive/src/lib.rs
@@ -115,7 +115,16 @@ pub(crate) fn crate_module() -> Path {
 ///   * `exec` - this is execute message variant
 ///   * `query` - this is query message variant
 ///
-/// For now, `#[msg(...)]` attribute doesn't support any additional data on `#[interface]`
+/// In case of query it is possible to pass second argument which is it's `ResponseType`.
+/// This is required in case of aliased results wrapping their `ResponseType`.
+/// Example for member query
+///
+/// ```ignore
+///     #[msg(query, resp=MemberQueryResponse)]
+///     fn member(&self, ctx: (Deps, Env), addr: String, at_height: Option<u64>) -> Result<MemberQueryResponse, Error>;
+/// ```
+///
+/// For now `#[msg(...)]` attribute doesn't support anymore data on `#[interface]`
 /// elements, but it may be extended in future.
 #[cfg(not(tarpaulin_include))]
 #[proc_macro_error]
@@ -197,7 +206,19 @@ fn interface_impl(attr: TokenStream2, item: TokenStream2) -> TokenStream2 {
 /// `#[msg(msg_type, ...)`. Msg attribute takes as its first argument type of message it is
 /// supposed to handle:
 /// * `instantiate` - this is instantiation message handler. There should be always exactly one
+/// * `exec` - this is execute message variant
+/// * `query` - this is query message variant
+/// * `reply` - this is reply message variant
+/// * `migrate` - this is migrate message variant
 /// handler for this kind of message.
+/// In case of query it is possible to pass second argument which is it's `ResponseType`.
+/// This is required in case of aliased results wrapping their `ResponseType`.
+/// Example for member query
+///
+/// ```ignore
+///     #[msg(query, resp=MemberQueryResponse)]
+///     fn member(&self, ctx: (Deps, Env), addr: String, at_height: Option<u64>) -> Result<MemberQueryResponse, Error>
+/// ```
 #[cfg(not(tarpaulin_include))]
 #[proc_macro_error]
 #[proc_macro_attribute]
diff --git a/sylvia-derive/src/message.rs b/sylvia-derive/src/message.rs
index 733f4c2..439f898 100644
--- a/sylvia-derive/src/message.rs
+++ b/sylvia-derive/src/message.rs
@@ -14,7 +14,8 @@ use syn::spanned::Spanned;
 use syn::visit::Visit;
 use syn::{
     parse_quote, FnArg, GenericArgument, GenericParam, Ident, ImplItem, ItemImpl, ItemTrait, Pat,
-    PatType, PathArguments, ReturnType, Signature, TraitItem, Type, WhereClause, WherePredicate,
+    PatType, PathArguments, PathSegment, ReturnType, Signature, TraitItem, Type, WhereClause,
+    WherePredicate,
 };
 
 fn filter_wheres<'a>(
@@ -249,7 +250,12 @@ impl<'a> EnumMessage<'a> {
                     };
 
                     if attr == ty {
-                        Some(MsgVariant::new(&method.sig, &mut generics_checker, name))
+                        Some(MsgVariant::new(
+                            &method.sig,
+                            &mut generics_checker,
+                            name,
+                            attr,
+                        ))
                     } else {
                         None
                     }
@@ -400,7 +406,12 @@ impl<'a> ContractEnumMessage<'a> {
                     };
 
                     if attr == ty {
-                        Some(MsgVariant::new(&method.sig, &mut generics_checker, name))
+                        Some(MsgVariant::new(
+                            &method.sig,
+                            &mut generics_checker,
+                            name,
+                            attr,
+                        ))
                     } else {
                         None
                     }
@@ -489,7 +500,7 @@ pub struct MsgVariant<'a> {
     // With https://github.com/rust-lang/rust/issues/63063 this could be just an iterator over
     // `MsgField<'a>`
     fields: Vec<MsgField<'a>>,
-    return_type: String,
+    return_type: TokenStream,
     message_type: &'a Ident,
 }
 
@@ -499,6 +510,7 @@ impl<'a> MsgVariant<'a> {
         sig: &'a Signature,
         generics_checker: &mut CheckGenerics,
         message_type: &'a Ident,
+        msg_attr: MsgAttr,
     ) -> MsgVariant<'a> {
         let function_name = &sig.ident;
 
@@ -507,7 +519,18 @@ impl<'a> MsgVariant<'a> {
             function_name.span(),
         );
         let fields = process_fields(sig, generics_checker);
-        let return_type = MsgVariant::extract_return_type(&sig.output);
+
+        let return_type = if let MsgAttr::Query { resp_type } = msg_attr {
+            match resp_type {
+                Some(resp_type) => quote! {#resp_type},
+                None => {
+                    let return_type = MsgVariant::extract_return_type(&sig.output);
+                    quote! {#return_type}
+                }
+            }
+        } else {
+            quote! {}
+        };
 
         Self {
             name,
@@ -518,54 +541,46 @@ impl<'a> MsgVariant<'a> {
         }
     }
 
-    fn extract_return_type(ret_type: &ReturnType) -> String {
-        let ty = if let ReturnType::Type(_, ty) = ret_type {
-            ty
-        } else {
+    fn extract_return_type(ret_type: &ReturnType) -> &PathSegment {
+        let ReturnType::Type(_, ty) = ret_type  else {
             unreachable!()
         };
-        let type_path = if let Type::Path(type_path) = ty.as_ref() {
-            type_path
-        } else {
+
+        let Type::Path(type_path) = ty.as_ref()  else {
             unreachable!()
         };
         let segments = &type_path.path.segments;
-        assert!(!segments.is_empty());
-        let args = if let PathArguments::AngleBracketed(arg) = &segments[0].arguments {
-            &arg.args
-        } else {
+        assert_eq!(segments.len(), 1);
+        let segment = &segments[0];
+
+        // In case of aliased result user need to define the return type by hand
+        if segment.ident != "Result" && segment.ident != "StdResult" {
+            emit_error!(
+                segment.span(),
+                "Neither Result nor StdResult found in return type. \
+                    You might be using aliased return type. \
+                    Please use #[msg(return_type=<your_return_type>)]"
+            );
+        }
+        let PathArguments::AngleBracketed(args) = &segments[0].arguments  else{
             unreachable!()
         };
+        let args = &args.args;
         assert!(!args.is_empty());
-        let type_path = if let GenericArgument::Type(Type::Path(type_path)) = &args[0] {
-            type_path
-        } else {
+        let GenericArgument::Type(Type::Path(type_path)) = &args[0] else{
             unreachable!()
         };
         let segments = &type_path.path.segments;
-        assert!(segments.len() == 1);
-        let ident = segments[0].ident.to_string();
-        match &segments[0].arguments {
-            PathArguments::AngleBracketed(arg) => {
-                let segments = if let GenericArgument::Type(Type::Path(type_path)) = &arg.args[0] {
-                    &type_path.path.segments
-                } else {
-                    unreachable!()
-                };
-                assert!(segments.len() == 1);
-                let generics = segments[0].ident.to_string();
-                format!("{}<{}>", ident, generics)
-            }
+        assert_eq!(segments.len(), 1);
 
-            _ => ident,
-        }
+        &segments[0]
     }
 
     /// Emits message variant
     pub fn emit(&self) -> TokenStream {
         let Self { name, fields, .. } = self;
         let fields = fields.iter().map(MsgField::emit);
-        let return_type = Ident::new(&self.return_type, Span::mixed_site());
+        let return_type = &self.return_type;
 
         if self.message_type == "QueryMsg" {
             quote! {
diff --git a/sylvia-derive/src/parser.rs b/sylvia-derive/src/parser.rs
index 6f8c4e2..b82ed19 100644
--- a/sylvia-derive/src/parser.rs
+++ b/sylvia-derive/src/parser.rs
@@ -1,4 +1,4 @@
-use proc_macro2::TokenStream;
+use proc_macro2::{Punct, TokenStream};
 use quote::quote;
 use syn::parse::{Error, Nothing, Parse, ParseBuffer, ParseStream};
 use syn::{parenthesized, parse_quote, Ident, Path, Result, Token, Type};
@@ -96,7 +96,7 @@ pub enum MsgType {
 /// `#[msg(...)]` attribute for `interface` macro
 pub enum MsgAttr {
     Exec,
-    Query,
+    Query { resp_type: Option<Ident> },
     Instantiate { name: Ident },
     Reply,
     Migrate,
@@ -158,12 +158,24 @@ impl MsgAttr {
         Ok(Self::Instantiate { name })
     }
 
+    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 {
         use MsgAttr::*;
 
         match self {
             Exec => MsgType::Exec,
-            Query => MsgType::Query,
+            Query { .. } => MsgType::Query,
             Instantiate { .. } => MsgType::Instantiate,
             Reply => MsgType::Reply,
             Migrate => MsgType::Migrate,
@@ -180,7 +192,7 @@ impl Parse for MsgAttr {
         if ty == "exec" {
             Ok(Self::Exec)
         } else if ty == "query" {
-            Ok(Self::Query)
+            Self::parse_query(content)
         } else if ty == "instantiate" {
             Self::parse_instantiate(content)
         } else if ty == "reply" {
diff --git a/sylvia/Cargo.toml b/sylvia/Cargo.toml
index 4543c29..2c6af74 100644
--- a/sylvia/Cargo.toml
+++ b/sylvia/Cargo.toml
@@ -23,3 +23,4 @@ konst = "0.2"
 anyhow = "1"
 cw-storage-plus = "0.16.0"
 cw-multi-test = "0.16.0"
+thiserror = "1.0.37"


Test patch:
diff --git a/sylvia/tests/query_returns.rs b/sylvia/tests/query_returns.rs
new file mode 100644
index 0000000..3b2c06a
--- /dev/null
+++ b/sylvia/tests/query_returns.rs
@@ -0,0 +1,55 @@
+use cosmwasm_std::{Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult};
+
+use sylvia::{contract, interface};
+use thiserror::Error;
+
+#[derive(Error, Debug, PartialEq)]
+pub enum ContractError {
+    #[error("{0}")]
+    Std(#[from] StdError),
+}
+
+#[derive(
+    serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq, schemars::JsonSchema,
+)]
+pub struct QueryResponse;
+
+type QueryResult<E> = Result<QueryResponse, E>;
+
+#[interface(module=msg)]
+pub trait Interface {
+    type Error: From<StdError>;
+
+    #[msg(query, resp=QueryResponse)]
+    fn query(&self, ctx: (Deps, Env)) -> QueryResult<Self::Error>;
+}
+
+pub struct SomeContract {}
+
+#[contract]
+impl SomeContract {
+    #[msg(instantiate)]
+    pub fn instantiate(&self, _ctx: (DepsMut, Env, MessageInfo)) -> StdResult<Response> {
+        Ok(Response::new())
+    }
+
+    #[msg(query, resp=QueryResponse)]
+    fn contract_query(&self, _ctx: (Deps, Env)) -> QueryResult<ContractError> {
+        Ok(QueryResponse {})
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn generate_interface_query() {
+        let _ = msg::InterfaceQueryMsg::Query {};
+    }
+
+    #[test]
+    fn generate_contract_query() {
+        let _ = QueryMsg::ContractQuery {};
+    }
+}
diff --git a/sylvia/tests/separate_module.rs b/sylvia/tests/separate_module.rs
index d07260b..139a7b9 100644
--- a/sylvia/tests/separate_module.rs
+++ b/sylvia/tests/separate_module.rs
@@ -27,7 +27,11 @@ pub trait Interface {
     fn no_args_query(&self, ctx: (Deps, Env)) -> Result<QueryResult, Self::Error>;
 
     #[msg(query)]
-    fn argumented_query(&self, ctx: (Deps, Env), user: Addr) -> Result<QueryResult, Self::Error>;
+    fn argumented_query(
+        &self,
+        ctx: (Deps, Env),
+        user: Addr,
+    ) -> Result<Option<QueryResult>, Self::Error>;
 }
 
 #[test]