-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Add experimental support for parsing variable definitions in fragments #1141
Conversation
src/language/parser.js
Outdated
/** | ||
* If enabled, the parser will understand and parse variable definitions | ||
* contained in a fragment definition. They'll be represented in the | ||
* `variableDefinitions` field of the FragmentDefinitionNode. |
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.
Should we make a point that this is not guaranteed to be supported in the future, and anyone that depends on this may have to migrate away from it at a later date?
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.
Agreed - highlight the fact this is experimental and may change or be removed in the future
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.
The implementation looks solid - I think making sure any reader cannot mistake the experiment is important so some suggestions inline
src/language/printer.js
Outdated
`fragment ${name} on ${typeCondition} ` + | ||
wrap('(', join(variableDefinitions, ', '), ') ') + |
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.
Add a comment here about this being experimental for those who stumble upon it
src/language/visitor.js
Outdated
FragmentDefinition: [ | ||
'name', | ||
'typeCondition', | ||
'variableDefinitions', |
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.
same
src/language/parser.js
Outdated
@@ -488,6 +501,17 @@ function parseFragment( | |||
function parseFragmentDefinition(lexer: Lexer<*>): FragmentDefinitionNode { | |||
const start = lexer.token; | |||
expectKeyword(lexer, 'fragment'); | |||
if (lexer.options.fragmentVariablesExperimental) { |
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.
Add a comment above this that includes the altered grammar with a bit of explanation
// Experimental support for defining variables within fragments changes
// the grammar of FragmentDefinition:
// - fragment FragmentName on TypeCondition VariableDefinitions? Directives? SelectionSet
src/language/parser.js
Outdated
/** | ||
* If enabled, the parser will understand and parse variable definitions | ||
* contained in a fragment definition. They'll be represented in the | ||
* `variableDefinitions` field of the FragmentDefinitionNode. |
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.
Agreed - highlight the fact this is experimental and may change or be removed in the future
src/language/parser.js
Outdated
* ... | ||
* } | ||
*/ | ||
fragmentVariablesExperimental?: boolean, |
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 option reads a bit strangely, what about experimentalFragmentVariables
?
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.
Yeah, I had fragmentVariables_experimental
before (not sure if that was any better), but the camelcase linter didn't like that. I'll take your suggestion.
src/language/ast.js
Outdated
@@ -253,6 +253,7 @@ export type FragmentDefinitionNode = { | |||
+loc?: Location, | |||
+name: NameNode, | |||
+typeCondition: NamedTypeNode, | |||
+variableDefinitions?: $ReadOnlyArray<VariableDefinitionNode>, |
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.
Add a short comment above this explaining its an experimental addition
@@ -290,6 +290,53 @@ describe('Visitor', () => { | |||
]); | |||
}); | |||
|
|||
it('visits variables defined in fragments', () => { |
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.
it('Experimental: visits variables...
Added comments emphasizing the experimental nature of this. |
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.
two more experimental-notices and it looks good to me
@@ -391,6 +391,16 @@ describe('Parser', () => { | |||
expect(result.loc).to.equal(undefined); | |||
}); | |||
|
|||
it('allows parsing fragment defined variables if enabled', () => { |
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.
nit: Experimental:
@@ -103,6 +103,22 @@ describe('Printer', () => { | |||
`); | |||
}); | |||
|
|||
it('correctly prints fragment definition with variables', () => { |
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.
nit: Experimental:
Added "Experimental" to the other test cases. |
Also - just to discuss I think we have two potential grammars for this experiment that I've seen and we should settle on one with some forethought.
I noticed in the linked issue in the graphql spec repo the examples use a variables-first form where this PR is condition first. Thoughts on one vs the other? |
Some examples to look at variables first
Some examples to look at type condition first
|
No strong opinions, but I leaned towards the latter (with |
Some argument for the other form is that the variable definitions sort of "attach" to the preceding fragment name - because of how we're used to reading Though I agree with you that when there are multiple variable definitions such that the cleanest rendering is multi-line, then having the type condition no longer on that first line is a real drawback |
Yeah I can see fair arguments for either. I'm not attached to the current approach, so happy to switch it if you'd like. |
I had the same initial reaction as @sam-swarr where "variables first" looked wrong to me because it didn't have the closing parenthesis immediately before the open curly brace. I think my reaction stemmed from my language background: I mostly used languages where return types precede the function (Java or C style) or there is no return type (JS or PHP style), and hence I expect the curly brace right after the close parenthesis. Looking at languages that declare the return type after the function (like Flow/Hack/Swift/Rust), though: // Flow
function concat(a: string, b: string): string {
return a + b;
} // Hack
public function alpha(): int {
return 1;
} // Swift
func greet(person: String, day: String) -> String {
return "Hello \(person), today is \(day)."
} It seems like having something between the arguments and the brace is becoming more common... and I do like the fact that in "variables first" the arguments attach to the fragment name, instead of attaching to the type name. I don't feel strongly either way, though if it were up to me and I had to make the call, I'd lean towards "variables first". My 2¢. |
Hmmm. Personally I think I've become convinced that, of the two to begin experimenting with, I'd try "variables first" for now. This is explicitly an experimental feature, so if we need to change it because over and over again it "keeps looking wrong" we can with a pretty trivial codemod. So I'm happiest if we pick something, but I suspect @leebyron's instincts are "more right" in that the latter, "variables last" proposal will end up feeling off in more cases compared to "variables first". |
Having slept on it and after hearing everyone's input, I'm also in the variables first camp now. Updated the commit to reflect that. |
@@ -391,6 +391,16 @@ describe('Parser', () => { | |||
expect(result.loc).to.equal(undefined); | |||
}); | |||
|
|||
it('Experimental: allows parsing fragment defined variables', () => { |
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.
Is it worth adding a test that verifies that without the experimentalFragmentVariables: true
flag, this does not parse? Makes sure that we've gated the experimental parsing correctly. (applies to each of the tests, I suspect)
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 test case already has that (see my comment below).
Looks great to me! |
expect(() => | ||
parse(source, { experimentalFragmentVariables: true }), | ||
).to.not.throw(); | ||
expect(() => parse(source)).to.throw('Syntax Error'); |
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.
@dschafer, already got it covered here!
expect(() => | ||
parse(source, { experimentalFragmentVariables: true }), | ||
).to.not.throw(); | ||
expect(() => parse(source)).to.throw('Syntax Error'); |
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.
Ah, didn't notice this test did both, you're way ahead of me!
I also have some ideas for a few experiments with GraphQL syntax both on the client-side for query transformations and on the server-side for schema stitching. Can we wrap all methods from @leebyron Would you be interested in such PR? |
@IvanGoncharov we discussed taking that exact approach before sending this PR and while there are merits to building an extendable parser, decided not to pursue it this time. I still like the idea though and would be in favor of trying it out in a PR to see what the complexity cost looks like. |
Thanks for your feedback, @dschafer - that was a really valuable additional opinion. I'm happy to see some consensus on the grammar. This PR looks great, @sam-swarr - thanks for your hard work |
This PR adds experimental support for parsing variable definitions within a fragment declaration. If the
fragmentVariablesExperimental
flag is passed into the options forparse
then the parser will acceptVariableDefinitions
placed after theTypeCondition
and before anyDirectives
. The syntax is identical to the usual query-defined variable definitions, and the parser will parse these into the optionalvariableDefinitions
field onFragmentDefinitionNode
.Context:
There's been ongoing discussion on exploring this concept: graphql/graphql-spec#204. Note that this PR only supports parsing fragment variable definitions into the AST. It does not allow for passing variable values into a fragment spread. It at least gets a foot in the door for starting to explore further down this path.
An emergent pain point with the current system (where a query must define all variables used in its dependent fragments) is that adding a new variable reference to a commonly used fragment could involve updating the declarations of the dozens of queries that depend on that fragment. With this PR, it would become easy to implement a client side transform that would allow for variable defaults defined at the fragment level to be automatically hoisted to its dependent queries. For example, with
the transform would automatically hoist the
$size
variable declaration intoquery q
without the need to explicitly define it. This can be useful iffragment a
is used by many queries.