Skip to content
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

Merged
merged 41 commits into from
Jul 11, 2022
Merged

Conversation

uint
Copy link
Contributor

@uint uint commented Jun 27, 2022

The goal here is to remove the following schema generation boilerplate for contract authors:

        [
            ("verifier", schema_for!(VerifierResponse)),
            ("other_balance", schema_for!(AllBalanceResponse)),
            ("recurse", schema_for!(RecurseResponse)),
            ("get_int", schema_for!(IntResponse)),
        ]
        .into_iter()
        .collect()

And replace it with attributes added to the QueryMsg itself:

#[derive(QueryResponses)]
pub enum QueryMsg {
    #[returns(VerifierResponse)]
    Verifier {},
    #[returns(AllBalanceResponse)]
    OtherBalance { address: String },
    #[returns(RecurseResponse)]
    Recurse { depth: u32, work: u32 },
    #[returns(IntResponse)]
    GetInt {},
}

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! 👌

@uint uint changed the title PoC: better UX for definit PoC: better UX for defining query response types Jun 27, 2022
@uint
Copy link
Contributor Author

uint commented Jun 27, 2022

Note: when merged to master, remember to add && (cd packages/schema-derive && cargo publish) \ to the Releasing wiki.

@uint
Copy link
Contributor Author

uint commented Jun 28, 2022

TODO:

  • doc comments
  • tests for panic msgs when a contract dev forgets to annotate a QueryMsg variant
  • a macro or builder pattern for constructing an Api object by giving it types - also an opportunity to remove more schema.rs boilerplate

Base automatically changed from idl to main June 28, 2022 10:17
@uint uint requested review from ethanfrey and hashedone June 29, 2022 06:38
@uint
Copy link
Contributor Author

uint commented Jun 29, 2022

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 QueryResponses trait implementation.

@uint uint requested a review from pyramation June 29, 2022 06:49
@uint
Copy link
Contributor Author

uint commented Jun 29, 2022

I'm on the fence about how Api should be built here. I want to make that less fragile since contract devs still have to interact with this until they start using Sylvia. Ideas:

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.

Copy link
Member

@webmaster128 webmaster128 left a 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

packages/schema/src/idl.rs Outdated Show resolved Hide resolved
packages/schema/src/query_response.rs Outdated Show resolved Hide resolved
packages/schema-derive/src/lib.rs Outdated Show resolved Hide resolved
@uint
Copy link
Contributor Author

uint commented Jun 29, 2022

Rebased

Co-authored-by: Simon Warta <2603011+webmaster128@users.noreply.github.com>

/// 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());
Copy link
Member

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?

Copy link
Contributor Author

@uint uint Jun 29, 2022

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.

Copy link
Contributor Author

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")]

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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?

Copy link
Contributor Author

@uint uint Jun 29, 2022

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.

Copy link
Contributor Author

@uint uint Jul 4, 2022

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.

Copy link
Contributor Author

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

@uint
Copy link
Contributor Author

uint commented Jul 5, 2022

The coverage report looks broken. @webmaster128 are you aware of a way to clean the cache, or do you have any other ideas?

@webmaster128
Copy link
Member

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:

Bildschirmfoto 2022-07-06 um 08 11 23

@uint
Copy link
Contributor Author

uint commented Jul 6, 2022

Looking at this diff coverage report, it seems like our coverage tool does not understand lines that are fields only

Weird!

@uint
Copy link
Contributor Author

uint commented Jul 6, 2022

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?

@uint uint changed the title PoC: better UX for defining query response types Better UX for defining query response types and schema generation Jul 6, 2022
@uint uint requested a review from webmaster128 July 6, 2022 16:26
@uint uint marked this pull request as ready for review July 6, 2022 16:26
@uint
Copy link
Contributor Author

uint commented Jul 7, 2022

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.

Copy link
Member

@webmaster128 webmaster128 left a 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?

codecov.yml Show resolved Hide resolved
packages/schema/src/idl.rs Show resolved Hide resolved
packages/schema/src/query_response.rs Show resolved Hide resolved
@uint
Copy link
Contributor Author

uint commented Jul 11, 2022

  • Is there no hackatom.json in this PR? Why is CI not complaining?

I may have forgotten to commit hackatom.json. On it.

  • 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?

Sounds like an improvement. Will do!

@uint
Copy link
Contributor Author

uint commented Jul 11, 2022

Very nice!

  • Is there no hackatom.json in this PR? Why is CI not complaining?

I may have forgotten to commit hackatom.json. On it.

Apparently it is commited (see here), but doesn't show up in this PR's diff. Odd.

@uint uint requested a review from webmaster128 July 11, 2022 11:16
.circleci/config.yml Show resolved Hide resolved
codecov.yml Outdated Show resolved Hide resolved
@webmaster128
Copy link
Member

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.

uint and others added 2 commits July 11, 2022 15:34
Co-authored-by: Simon Warta <2603011+webmaster128@users.noreply.github.com>
@uint
Copy link
Contributor Author

uint commented Jul 11, 2022

There is simply no diff to the version already available on main.

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 hackatom.json in this PR, but the format hasn't changed one iota.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants