-
Notifications
You must be signed in to change notification settings - Fork 335
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Better UX for defining query response types and schema generation #1339
Conversation
Note: when merged to master, remember to add |
TODO:
|
Tagging @hashedone since this might be relevant to Sylvia in the future. I know Sylvia will replace the derive macro here, but I imagine we will keep the rest and get Sylvia to generate the |
I'm on the fence about how A (sort of) builder pattern: // examples/schema.rs
fn main() {
// ...
let api = Api::builder()
.name(env!("CARGO_PKG_NAME"))
.version(env!("CARGO_PKG_VERSION"))
.instantiate::<InstantiateMsg>() // InstantiateMsg has to be JsonSchema
.query::<QueryMsg>() // QueryMsg has to be JsonSchema and QueryResponses
.sudo::<SudoMsg>()
.build();
} Function-like macro: // examples/schema.rs
fn main() {
// ...
generate_api! {
instantiate = InstantiateMsg,
query = QueryMsg,
sudo = SudoMsg,
};
} I'm leaning toward the macro. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very very nice stuff
Rebased |
Co-authored-by: Simon Warta <[email protected]>
packages/schema-derive/src/lib.rs
Outdated
|
||
/// Extract the query -> response mapping out of an enum variant. | ||
fn parse_query(v: Variant) -> TokenStream { | ||
let query = to_snake_case(&v.ident.to_string()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assumes the user also sets rename_all = "snake_case"
. Is there a way for us to enforce that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our derive macro could panic if the #[serde(rename_all = "snake_case")]
attribute is missing, I suppose. But we'd have to write (or borrow) code to parse them the same way serde parses them. I don't know if I like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For one thing, serde allows them to be chained like this:
#[serde(deny_unknown_fields, rename_all = "snake_case")]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll take a look at it either way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Panic sounds good. The other option is to let QueryResponses
do the serde configuration. Then users can remove the line and get the right (tm) thing from us.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the problem is a derive macro cannot rewrite the enum or add attributes to it. It can only add extra code (a trait implementation usually).
We'd need to use an attribute macro and that feels more heavy-handed. Attribute macros consume the input and allow us to output a modified version of it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay. But that means we can add extra code as part of response_schemas()
that does a bit of serialization and check if snake case was used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh. Maybe you're onto something. I think if we add a trait bound like this:
trait QueryResponses: JsonSchema {
// ...
}
then response_schemas()
should be able to inspect the schema with something like this?
let schema = schema_for!(Self);
And maybe checking the schema for the presence of all the expected (snake_case) variants is a better integrity check than checking if a serde attribute is present? It sounds pretty good.
By the way, the parsing of serde attributes might not actually be hard. Serde exposes some internals for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, so.
- The derive macro checking for serde attributes is a no go - it can be done in theory, but requires
serde_derive_internals
, which then causes dependency resolution problems in contracts (another dep tries to pull an earlier version of it). It's probably best to stay away from that can of worms and just not risk dependency problems for the future there. - I'm going for the runtime integrity check described above since it just seems like a damn good idea to have. That throws an error during schema generation though (rather than a panicking macro during contract development), so pretty late.
- If we add this, we're golden.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
> cargo schema
--- snip ---
thread 'main' panicked at '{}: InconsistentQueries { query_msg: ["get-int", "other-balance", "recurse", "verifier"], responses: ["get_int", "other_balance", "recurse", "verifier"] }', examples/schema.rs:41:49
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
This reverts commit 7faefd3.
The coverage report looks broken. @webmaster128 are you aware of a way to clean the cache, or do you have any other ideas? |
Looking at this diff coverage report, it seems like our coverage tool does not understand lines that are fields only: |
Weird! |
So, I'm also trying to reduce schema generation boilerplate to this: use cosmwasm_schema::generate_api;
use hackatom::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, SudoMsg};
fn main() {
generate_api! {
instantiate: InstantiateMsg,
query: QueryMsg,
execute: ExecuteMsg,
sudo: SudoMsg,
migrate: MigrateMsg,
}
} For contract devs, there's now no "open this file handle, write to that directory" boilerplate to maintain. Or fumbling with filenames and risking a typo. Just give it the types it needs and be done. How's that look? |
I think this is ready for review whenever someone has time. I know it's a fair bit; no rush. I'm going to implement #1307 (comment) in the next PR. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice!
- Is there no
hackatom.json
in this PR? Why is CI not complaining? - I'm a bit unsure regarding naming. One "generate" macro create a struct, one "generate" writes a file to disk. What about
generate_*
/write_*
to differentiate the two?
I may have forgotten to commit
Sounds like an improvement. Will do! |
Apparently it is commited (see here), but doesn't show up in this PR's diff. Odd. |
There is simply no diff to the version already available on main. |
Co-authored-by: Simon Warta <[email protected]>
Ahh, right. I forgot it was there already. If there's no diff, that's what I aimed for. So answering your question, there is |
The goal here is to remove the following schema generation boilerplate for contract authors:
And replace it with attributes added to the
QueryMsg
itself:The drawback right now is that
cosmwasm_schema
is imported as a regular (not dev) dependency in the contract.Does this bloat the wasm blob? EDIT: nope, the size of the artifact is the same either way! 👌