Repository navigation
Build enums used only by directives without a Java class - #852
Open
oryan-block wants to merge 2 commits into
Open
oryan-block wants to merge 2 commits into
oryan-block wants to merge 2 commits into
Conversation
An enum used only as a directive argument, such as Federation 2's @link(for: Purpose), failed the schema build with "Enum type 'Purpose' is used in a directive, but no class could be found" unless a Java enum was passed in the dictionary. That happened even when the directive was never applied, since the scanner checks the arguments of every directive definition, and createEnumObject could only build an enum from a class. Directive argument values are never passed to resolver methods, so such an enum doesn't need a class. When the scanner finds no class for it, the enum is now built with its value names as values, which is what graphql-java does without an EnumValuesProvider. Applied directives and SchemaDirectiveWiring implementations then get "SECURITY" from getValue() rather than an enum constant. Passing the enum class in the dictionary still gives enum constants. The decision is made once scanning is done, so a class found later, for example through an unused type with includeUnusedTypes, is still used. Enums that fields or arguments use keep their class as before, and an enum matched to String, Object or a Map still fails in the parser. Federation's bundled definitions still need their custom scalars, such as Import and FieldSet, passed to the parser. One case does change for resolvers. An enum that a directive uses and that is also used by an input object field without a Java property, e.g. an input bound to a Map, now builds with the value names, so the Map gets "SECURITY". Without the directive that schema still fails. A test pins this. Fixes #823 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit built an enum from its value names whenever a directive used it and the scanner found no class for it. That also covered enums whose values do reach user code without going through a resolver parameter the scanner can match: input fields with no Java property, such as an input bound to a Map, and arguments of unimplemented fields served by a missing resolver data fetcher. Those schemas fail without the directive, but declaring an unrelated directive made them build, and the Map or the data fetcher got "SECURITY" instead of an enum constant. Now an enum is only built from its value names if no field, argument or input field of a type in the schema uses it, including fields added by extensions and types found later through includeUnusedTypes. Otherwise it needs a class as before, and the parser fails with "Expected enum with name 'Purpose' but found none!". This replaces the check against the scanner's dictionary, since any enum the scanner matched to a class was matched through such a field. The test that pinned the Map-bound input case now expects the error, and new tests cover an unimplemented field's argument added by an extension and an unused type's Object property. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Fixes #823
Checklist
Description
An enum that's only used as a directive argument fails the schema build unless you register a Java enum for it. This is the schema from the issue:
It fails even when the directive is never applied. The common case is Fed2's
@link(for: link__Purpose), where people end up writing a dummylink__Purposeenum just to get the schema to build.SchemaClassScanner#handleDirectivesscans the arguments of every directive definition and sent enums throughhandleDictionaryTypes, which throws when the dictionary has no class for them.SchemaParser#createEnumObjectalso required a Java enum for every enum. But directive argument values are never passed to resolver methods, so there's nothing for the class to be matched against.Now the scanner handles directive enums in
handleDirectiveEnum. If there's a dictionary class for the enum it's used, as before. Otherwise the enum goes intounvalidatedTypesanddirectiveEnumsWithoutClass. Once scanning is done,validateAndCreateResultkeeps only the ones that no field, argument or input field of a type in the schema uses, and passes them to the parser asScannedSchemaObjects#directiveOnlyEnums. That check includes fields added byextend type/extend inputand types found throughincludeUnusedTypes.createEnumObjectbuilds those enums with their value names as values, which is what graphql-java does for enums without a class. "Expected enum with name" and "is not a Java enum" still apply to every other enum.The field-use check came out of review. My first version built any classless directive enum from its names, even when a
Map-bound input field or the argument of a field with no resolver also used it. That meant adding an unrelated directive could turn a schema that failed to build into one that built and handedStrings to user code. Now any use outside directive definitions needs a class, the same as on master.Left out:
Map-bound input case a clearer message.extend enumis still ignored, the same as for enums backed by a class.createDirectives,determineInputTypeandbuildAppliedDirectivesaren't touched, so the overlap with #843 is a few nearby lines inSchemaParser. #842 will have small textual conflicts in the scanner's field declarations and invalidateAndCreateResult. Both PRs fail on an enum used by aMap-bound input field, but #842 throws its ownSchemaClassScannerErrorbefore the parser runs. Whichever lands second has to update the expected exception inscanner requires a class for enums used by directives and by input fields without a property.There are 11 new tests in
SchemaClassScannerDirectiveTest. The build tests cover the issue's schema exactly as written, applied schema and field directives using the enum's default and an explicit value (checked throughgetValue(), a full introspection query andSchemaPrinter), the Fed2 link-v1.0 definitions with anImportscalar, and an enum class found through an unused type withincludeUnusedTypes. The guards check that an enum used by both a directive and a resolver keeps its Java constants, that aStringparameter still gives "is not a Java enum", that anObjectparameter or anAny?property on an unused type still gives "Expected enum with name", that a dictionary class is used for a directive-only enum, and that aMap-bound input field or an unimplemented field's argument inextend type Querystill needs a class. I ran the test file against master: the 4 build tests fail with the error above, and so do the 3 guards for unused types,Mapinputs and extensions, since master throws its scanner error, not the parser's. The other guards and the existing tests pass on master.clean verifypasses.Behaviour change: an enum that only directive definitions use, with no dictionary class, now builds instead of failing. Its values are the value names as
Strings, soappliedDirective.getArgument("for").getValue<...>()in aSchemaDirectiveWiringreturns"SECURITY", not an enum constant. Passing the enum in the dictionary (.dictionary(Purpose::class)) brings the constants back. These enums are now in the built schema and no longer show up ingetUnusedDefinitions(). The scanner error "Enum type '...' is used in a directive, but no class could be found for that type name" is gone. Schemas that hit it because the enum is also used by a field without a class still fail, now with the parser's "Expected enum with name 'Purpose' but found none!". Fed2 users who load federation's definitions still have to pass its custom scalars (Import,FieldSetand so on) to the parser.🤖 Generated with Claude Code