Skip to content

Generate a struct for each operation's variables - #149

Open
saga-dasgupta wants to merge 1 commit into
mainfrom
operation-variables
Open

saga-dasgupta wants to merge 1 commit into
mainfrom
operation-variables

Conversation

@saga-dasgupta

@saga-dasgupta saga-dasgupta commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

This also adds a test case to #148's custom_scalar_overrides, overriding an input field to a variables struct.

Typegen generates types for what an operation returns, but not for the variables it declares. A caller building those variables by hand gets no help from the compiler: renaming, adding, removing or retyping a variable in the query still compiles, and only fails once the query runs.

This generates a struct for an operation's variables, next to the operation's own struct in the query module:

query MyArgs($required: String!, $optional: Int, $withDefault: Int! = 1) {
  myArgs(required: $required, optional: $optional, withDefault: $withDefault)
}
pub struct MyArgsVariables {
    pub required: String,
    pub optional: Option<i32>,
    pub with_default: Option<i32>,
}
  • Naming: <Operation>Variables, or RootVariables for an anonymous operation, whose struct is Root. Operations that declare no variables get no struct.
  • Fields: a variable that is non-null with no default is a plain field. Any other variable is an Option, so a variable with a default can be left out.
  • Field types: the same as input object fields. The type-building code moves out of input_object_type_definition.rs into a new input_type.rs that both use. That includes borrowing: with borrow = true, the struct gets an 'a lifetime when a field borrows.
  • CodeGenerator hooks: attributes_for_variables_struct, additional_impls_for_variables_struct and attributes_for_variables_struct_field, modelled on the input object hooks. All three default to no-ops, so existing implementations don't change.
  • bluejay-typegen-macro:
    • derives Clone, PartialEq, Debug and serde's Serialize;
    • renames each field to its GraphQL variable name;
    • skips an optional variable that is None, so the query's default applies instead of an explicit null.
  • Validation: it's a typegen error when a variables struct's name clashes with the struct for a fragment or another operation in the same module, for example query My($x: Int) next to fragment MyVariables on .... Without this check, rustc would report a duplicate definition.

Compatibility

These are cases that compile on main and don't here:

  • A document with an operation that declares variables, alongside a fragment or operation whose struct already has that <Operation>Variables name. This now gets the clash error above.
  • A variable named $self, $Self, $super, $crate or $_ now panics in names::to_ident. These names can't be raw identifiers, so format_ident!("r#self") panics. Fields and input fields with those names already panic the same way on main; variables just didn't become identifiers before. I left this alone to keep the PR focused, but I can add a typegen error for it here if you'd prefer.
  • Two variables whose names map to the same field, such as $fooBar and $foo_bar, now fail with rustc's duplicate field error (E0124). Input objects with such fields already fail the same way.
  • With bluejay-typegen, a custom scalar that is a variable's type, but no input object field's, now needs Clone, PartialEq, Debug and serde's Serialize, which the variables struct derives. Input object fields already need these.

@saga-dasgupta
saga-dasgupta marked this pull request as ready for review October 6, 2026 14:32
saga-dasgupta added a commit to Shopify/shopify-function-rust that referenced this pull request Oct 6, 2026
A prepare target returns the variables for its run target's input query as
untyped `JSON`. Nothing checked that they matched what the run query declares,
so a renamed, added, or retyped variable deployed fine and failed at checkout,
with the run query resolving nothing.

bluejay now generates a struct for the variables of each operation that
declares them: `<Operation>Variables`, or `RootVariables` for an anonymous
operation, with a field per variable. A non-null variable with no default is a
plain field; any other is an `Option` whose `None` omits it. `typegen`
implements wasm_api's `Serialize` and `Deserialize` for it with the same code
as input objects, so it serializes directly.

bluejay's `typegen` also takes `custom_scalar_overrides`, so a prepare result's
`variables` field can be typed as the run query's variables struct instead of
`JsonValue`:

    #[typegen("schema.graphql", custom_scalar_overrides = {
        "CartValidationsGeneratePrepareResult.variables" => cart_validations_generate_run::InputVariables,
    })]

A prepare target that no longer matches its run query then fails to compile:

- a renamed or removed variable is an unknown field (E0560)
- an added variable is a missing field (E0063)
- a retyped variable, or a hand-written `JsonValue`, is a mismatch (E0308)

Depends on Shopify/bluejay#149 and Shopify/bluejay#148, patched in from
Shopify/bluejay#149's branch, which has both, until they ship in a release.
Base automatically changed from input-custom-scalar-overrides to main October 6, 2026 17:41
An operation that declares variables now also gets a struct next to its
own in the query module, named `<Operation>Variables`, or
`RootVariables` for an anonymous operation, with a field per variable.
A variable that is non-null with no default is a plain field. Any other
variable is an `Option`.

`CodeGenerator` gains `attributes_for_variables_struct`,
`additional_impls_for_variables_struct` and
`attributes_for_variables_struct_field`, so generators can derive or
implement serialization for it. `bluejay-typegen-macro` derives serde's
`Serialize`, and skips an optional variable that is `None` so that its
default applies.

It is an error for a variables struct name to clash with a fragment or
operation struct in the same module.
saga-dasgupta added a commit to Shopify/shopify-function-rust that referenced this pull request Oct 6, 2026
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.

1 participant