Skip to content

Commit 6329540

Browse files
authored
Merge pull request #1472 from CosmWasm/query-responses-generic-trait-bounds
QueryResponses: infer the JsonSchema trait bound
2 parents 4415fd1 + 7613623 commit 6329540

File tree

5 files changed

+159
-23
lines changed

5 files changed

+159
-23
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ and this project adheres to
2525
cannot properly measure different runtimes for differet Wasm opcodes.
2626
- cosmwasm-schema: schema generation is now locked to produce strictly
2727
`draft-07` schemas
28+
- cosmwasm-schema: `QueryResponses` derive now sets the `JsonSchema` trait bound
29+
on the generated `impl` block. This allows the contract dev to not add a
30+
`JsonSchema` trait bound on the type itself.
2831

2932
[#1465]: https://github.com/CosmWasm/cosmwasm/pull/1465
3033

packages/schema-derive/src/query_responses.rs

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1-
use syn::{parse_quote, Expr, ExprTuple, Ident, ItemEnum, ItemImpl, Type, Variant};
1+
mod context;
2+
3+
use syn::{parse_quote, Expr, ExprTuple, Generics, ItemEnum, ItemImpl, Type, Variant};
4+
5+
use self::context::Context;
26

37
pub fn query_responses_derive_impl(input: ItemEnum) -> ItemImpl {
4-
let is_nested = has_attr(&input, "query_responses", "nested");
8+
let ctx = context::get_context(&input);
59

6-
if is_nested {
10+
if ctx.is_nested {
711
let ident = input.ident;
812
let subquery_calls = input.variants.into_iter().map(parse_subquery);
913

1014
// Handle generics if the type has any
11-
let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
15+
let (_, type_generics, where_clause) = input.generics.split_for_impl();
16+
let impl_generics = impl_generics(&ctx, &input.generics);
1217

1318
let subquery_len = subquery_calls.len();
1419
parse_quote! {
@@ -31,7 +36,8 @@ pub fn query_responses_derive_impl(input: ItemEnum) -> ItemImpl {
3136
let mappings = mappings.map(parse_tuple);
3237

3338
// Handle generics if the type has any
34-
let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
39+
let (_, type_generics, where_clause) = input.generics.split_for_impl();
40+
let impl_generics = impl_generics(&ctx, &input.generics);
3541

3642
parse_quote! {
3743
#[automatically_derived]
@@ -47,6 +53,21 @@ pub fn query_responses_derive_impl(input: ItemEnum) -> ItemImpl {
4753
}
4854
}
4955

56+
/// Takes a list of generics from the type definition and produces a list of generics
57+
/// for the expanded `impl` block, adding trait bounds like `JsonSchema` as appropriate.
58+
fn impl_generics(ctx: &Context, generics: &Generics) -> Generics {
59+
let mut impl_generics = generics.to_owned();
60+
for param in impl_generics.type_params_mut() {
61+
if !ctx.no_bounds_for.contains(&param.ident) {
62+
param
63+
.bounds
64+
.push(parse_quote! {::cosmwasm_schema::schemars::JsonSchema})
65+
}
66+
}
67+
68+
impl_generics
69+
}
70+
5071
/// Extract the query -> response mapping out of an enum variant.
5172
fn parse_query(v: Variant) -> (String, Expr) {
5273
let query = to_snake_case(&v.ident.to_string());
@@ -80,14 +101,6 @@ fn parse_subquery(v: Variant) -> Expr {
80101
parse_quote!(<#submsg as ::cosmwasm_schema::QueryResponses>::response_schemas_impl())
81102
}
82103

83-
/// Checks whether the input has the given `#[$path($attr))]` attribute
84-
fn has_attr(input: &ItemEnum, path: &str, attr: &str) -> bool {
85-
input.attrs.iter().any(|a| {
86-
a.path.get_ident().unwrap() == path
87-
&& a.parse_args::<Ident>().ok().map_or(false, |i| i == attr)
88-
})
89-
}
90-
91104
fn parse_tuple((q, r): (String, Expr)) -> ExprTuple {
92105
parse_quote! {
93106
(#q.to_string(), #r)
@@ -202,13 +215,13 @@ mod tests {
202215
};
203216

204217
let result = query_responses_derive_impl(input);
205-
dbg!(&result);
218+
206219
assert_eq!(
207220
result,
208221
parse_quote! {
209222
#[automatically_derived]
210223
#[cfg(not(target_arch = "wasm32"))]
211-
impl<T> ::cosmwasm_schema::QueryResponses for QueryMsg<T> {
224+
impl<T: ::cosmwasm_schema::schemars::JsonSchema> ::cosmwasm_schema::QueryResponses for QueryMsg<T> {
212225
fn response_schemas_impl() -> ::std::collections::BTreeMap<String, ::cosmwasm_schema::schemars::schema::RootSchema> {
213226
::std::collections::BTreeMap::from([
214227
("foo".to_string(), ::cosmwasm_schema::schema_for!(bool)),
@@ -223,7 +236,7 @@ mod tests {
223236
parse_quote! {
224237
#[automatically_derived]
225238
#[cfg(not(target_arch = "wasm32"))]
226-
impl<T: std::fmt::Debug + SomeTrait> ::cosmwasm_schema::QueryResponses for QueryMsg<T> {
239+
impl<T: std::fmt::Debug + SomeTrait + ::cosmwasm_schema::schemars::JsonSchema> ::cosmwasm_schema::QueryResponses for QueryMsg<T> {
227240
fn response_schemas_impl() -> ::std::collections::BTreeMap<String, ::cosmwasm_schema::schemars::schema::RootSchema> {
228241
::std::collections::BTreeMap::from([
229242
("foo".to_string(), ::cosmwasm_schema::schema_for!(bool)),
@@ -239,7 +252,7 @@ mod tests {
239252
parse_quote! {
240253
#[automatically_derived]
241254
#[cfg(not(target_arch = "wasm32"))]
242-
impl<T> ::cosmwasm_schema::QueryResponses for QueryMsg<T>
255+
impl<T: ::cosmwasm_schema::schemars::JsonSchema> ::cosmwasm_schema::QueryResponses for QueryMsg<T>
243256
where T: std::fmt::Debug + SomeTrait,
244257
{
245258
fn response_schemas_impl() -> ::std::collections::BTreeMap<String, ::cosmwasm_schema::schemars::schema::RootSchema> {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
use std::collections::HashSet;
2+
3+
use syn::{Ident, ItemEnum, Meta, NestedMeta};
4+
5+
const ATTR_PATH: &str = "query_responses";
6+
7+
pub struct Context {
8+
/// If the enum we're trying to derive QueryResponses for collects other QueryMsgs,
9+
/// setting this flag will derive the implementation appropriately, collecting all
10+
/// KV pairs from the nested enums rather than expecting `#[return]` annotations.
11+
pub is_nested: bool,
12+
/// Disable infering the `JsonSchema` trait bound for chosen type parameters.
13+
pub no_bounds_for: HashSet<Ident>,
14+
}
15+
16+
pub fn get_context(input: &ItemEnum) -> Context {
17+
let params = input
18+
.attrs
19+
.iter()
20+
.filter(|attr| matches!(attr.path.get_ident(), Some(id) if *id == ATTR_PATH))
21+
.flat_map(|attr| {
22+
if let Meta::List(l) = attr.parse_meta().unwrap() {
23+
l.nested
24+
} else {
25+
panic!("{} attribute must contain a meta list", ATTR_PATH);
26+
}
27+
})
28+
.map(|nested_meta| {
29+
if let NestedMeta::Meta(m) = nested_meta {
30+
m
31+
} else {
32+
panic!("no literals allowed in QueryResponses params")
33+
}
34+
});
35+
36+
let mut ctx = Context {
37+
is_nested: false,
38+
no_bounds_for: HashSet::new(),
39+
};
40+
41+
for param in params {
42+
match param.path().get_ident().unwrap().to_string().as_str() {
43+
"no_bounds_for" => {
44+
if let Meta::List(l) = param {
45+
for item in l.nested {
46+
match item {
47+
NestedMeta::Meta(Meta::Path(p)) => {
48+
ctx.no_bounds_for.insert(p.get_ident().unwrap().clone());
49+
}
50+
_ => panic!("`no_bounds_for` only accepts a list of type params"),
51+
}
52+
}
53+
} else {
54+
panic!("expected a list for `no_bounds_for`")
55+
}
56+
}
57+
"nested" => ctx.is_nested = true,
58+
path => panic!("unrecognized QueryResponses param: {}", path),
59+
}
60+
}
61+
62+
ctx
63+
}

packages/schema/src/query_response.rs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub use cosmwasm_schema_derive::QueryResponses;
1313
///
1414
/// Using the derive macro is the preferred way of implementing this trait.
1515
///
16-
/// # Example
16+
/// # Examples
1717
/// ```
1818
/// use cosmwasm_schema::QueryResponses;
1919
/// use schemars::JsonSchema;
@@ -30,20 +30,40 @@ pub use cosmwasm_schema_derive::QueryResponses;
3030
/// #[returns(AccountInfo)]
3131
/// AccountInfo { account: String },
3232
/// }
33+
/// ```
34+
///
35+
/// You can compose multiple queries using `#[query_responses(nested)]`. This might be useful
36+
/// together with `#[serde(untagged)]`. If the `nested` flag is set, no `returns` attributes
37+
/// are necessary on the enum variants. Instead, the response types are collected from the
38+
/// nested enums.
3339
///
34-
/// // You can also compose multiple queries using #[query_responses(nested)]:
40+
/// ```
41+
/// # use cosmwasm_schema::QueryResponses;
42+
/// # use schemars::JsonSchema;
3543
/// #[derive(JsonSchema, QueryResponses)]
3644
/// #[query_responses(nested)]
3745
/// #[serde(untagged)]
38-
/// enum QueryMsg2 {
39-
/// MsgA(QueryMsg),
46+
/// enum QueryMsg {
47+
/// MsgA(QueryA),
4048
/// MsgB(QueryB),
4149
/// }
50+
///
51+
/// #[derive(JsonSchema, QueryResponses)]
52+
/// enum QueryA {
53+
/// #[returns(Vec<String>)]
54+
/// Denoms {},
55+
/// }
56+
///
4257
/// #[derive(JsonSchema, QueryResponses)]
4358
/// enum QueryB {
4459
/// #[returns(AccountInfo)]
4560
/// AccountInfo { account: String },
4661
/// }
62+
///
63+
/// # #[derive(JsonSchema)]
64+
/// # struct AccountInfo {
65+
/// # IcqHandle: String,
66+
/// # }
4767
/// ```
4868
pub trait QueryResponses: JsonSchema {
4969
fn response_schemas() -> Result<BTreeMap<String, RootSchema>, IntegrityError> {

packages/schema/tests/idl.rs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,16 @@ fn test_query_responses() {
109109

110110
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, QueryResponses)]
111111
#[serde(rename_all = "snake_case")]
112-
pub enum QueryMsgWithGenerics<T: std::fmt::Debug>
112+
pub enum QueryMsgWithGenerics<T> {
113+
#[returns(u128)]
114+
QueryData { data: T },
115+
}
116+
117+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, QueryResponses)]
118+
#[serde(rename_all = "snake_case")]
119+
pub enum QueryMsgWithGenericsAndTraitBounds<T: std::fmt::Debug>
113120
where
114-
T: JsonSchema,
121+
T: PartialEq,
115122
{
116123
#[returns(u128)]
117124
QueryData { data: T },
@@ -147,6 +154,36 @@ fn test_query_responses_generics() {
147154
api.get("responses").unwrap().get("query_data").unwrap();
148155
}
149156

157+
#[test]
158+
fn test_query_responses_generics_and_trait_bounds() {
159+
let api_str = generate_api! {
160+
instantiate: InstantiateMsg,
161+
query: QueryMsgWithGenericsAndTraitBounds<u32>,
162+
}
163+
.render()
164+
.to_string()
165+
.unwrap();
166+
167+
let api: Value = serde_json::from_str(&api_str).unwrap();
168+
let queries = api
169+
.get("query")
170+
.unwrap()
171+
.get("oneOf")
172+
.unwrap()
173+
.as_array()
174+
.unwrap();
175+
176+
// Find the "query_data" query in the queries schema
177+
assert_eq!(queries.len(), 1);
178+
assert_eq!(
179+
queries[0].get("required").unwrap().get(0).unwrap(),
180+
"query_data"
181+
);
182+
183+
// Find the "query_data" query in responses
184+
api.get("responses").unwrap().get("query_data").unwrap();
185+
}
186+
150187
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, QueryResponses)]
151188
#[serde(untagged)]
152189
#[query_responses(nested)]

0 commit comments

Comments
 (0)