Skip to content

Commit 31ee7c6

Browse files
andimarekclaude
andcommitted
Remove clearDirectives and use first-class properties for @specifiedBy in SchemaPrinter
Remove GraphQLSchema.Builder.clearDirectives() as it allows removing spec-required built-in directives, which arguably violates the spec. All 7 built-in directives are now always present. Change SchemaPrinter to synthesize @specifiedBy from the first-class specifiedByUrl property on GraphQLScalarType, matching how @deprecated already uses the first-class deprecationReason property. This ensures both directives rely on their first-class properties as the source of truth rather than applied directives. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9f18071 commit 31ee7c6

File tree

3 files changed

+74
-23
lines changed

3 files changed

+74
-23
lines changed

src/main/java/graphql/schema/GraphQLSchema.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,6 @@ public static Builder newSchema(GraphQLSchema existingSchema) {
692692
.introspectionSchemaType(existingSchema.getIntrospectionSchemaType())
693693
.codeRegistry(existingSchema.getCodeRegistry())
694694
.clearAdditionalTypes()
695-
.clearDirectives()
696695
.additionalDirectives(new LinkedHashSet<>(existingSchema.getDirectives()))
697696
.clearSchemaDirectives()
698697
.withSchemaDirectives(schemaDirectivesArray(existingSchema))
@@ -862,12 +861,6 @@ public Builder additionalDirective(GraphQLDirective additionalDirective) {
862861
return this;
863862
}
864863

865-
public Builder clearDirectives() {
866-
this.additionalDirectives.clear();
867-
return this;
868-
}
869-
870-
871864
public Builder withSchemaDirectives(GraphQLDirective... directives) {
872865
for (GraphQLDirective directive : directives) {
873866
withSchemaDirective(directive);

src/main/java/graphql/schema/idl/SchemaPrinter.java

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
import java.util.stream.Stream;
6565

6666
import static graphql.Directives.DeprecatedDirective;
67+
import static graphql.Directives.SpecifiedByDirective;
6768
import static graphql.Scalars.GraphQLString;
6869
import static graphql.schema.visibility.DefaultGraphqlFieldVisibility.DEFAULT_FIELD_VISIBILITY;
6970
import static graphql.util.EscapeUtil.escapeJsonString;
@@ -559,7 +560,8 @@ private SchemaElementPrinter<GraphQLScalarType> scalarPrinter() {
559560
printAsAst(out, type.getDefinition(), type.getExtensionDefinitions());
560561
} else {
561562
printComments(out, type, "");
562-
out.format("scalar %s%s\n\n", type.getName(), directivesString(GraphQLScalarType.class, type));
563+
List<GraphQLAppliedDirective> directives = addOrUpdateSpecifiedByDirectiveIfNeeded(type);
564+
out.format("scalar %s%s\n\n", type.getName(), directivesString(GraphQLScalarType.class, directives));
563565
}
564566
}
565567
};
@@ -1103,6 +1105,65 @@ private String getDeprecationReason(GraphQLDirectiveContainer directiveContainer
11031105
}
11041106
}
11051107

1108+
private boolean isSpecifiedByDirectiveAllowed() {
1109+
return options.getIncludeDirective().test(SpecifiedByDirective.getName());
1110+
}
1111+
1112+
private boolean isSpecifiedByDirective(GraphQLAppliedDirective directive) {
1113+
return directive.getName().equals(SpecifiedByDirective.getName());
1114+
}
1115+
1116+
private boolean hasSpecifiedByDirective(List<GraphQLAppliedDirective> directives) {
1117+
return directives.stream().anyMatch(this::isSpecifiedByDirective);
1118+
}
1119+
1120+
private List<GraphQLAppliedDirective> addOrUpdateSpecifiedByDirectiveIfNeeded(GraphQLScalarType scalarType) {
1121+
List<GraphQLAppliedDirective> directives = DirectivesUtil.toAppliedDirectives(scalarType);
1122+
String url = scalarType.getSpecifiedByUrl();
1123+
1124+
if (url == null) {
1125+
// first-class property is not set - remove any @specifiedBy applied directive
1126+
return directives.stream()
1127+
.filter(d -> !isSpecifiedByDirective(d))
1128+
.collect(toList());
1129+
}
1130+
if (!hasSpecifiedByDirective(directives) && isSpecifiedByDirectiveAllowed()) {
1131+
directives = new ArrayList<>(directives);
1132+
directives.add(createSpecifiedByDirective(url));
1133+
} else if (hasSpecifiedByDirective(directives) && isSpecifiedByDirectiveAllowed()) {
1134+
// Update URL in case modified by schema transform
1135+
directives = updateSpecifiedByDirective(directives, url);
1136+
}
1137+
return directives;
1138+
}
1139+
1140+
private GraphQLAppliedDirective createSpecifiedByDirective(String url) {
1141+
GraphQLAppliedDirectiveArgument arg = GraphQLAppliedDirectiveArgument.newArgument()
1142+
.name("url")
1143+
.valueProgrammatic(url)
1144+
.type(GraphQLString)
1145+
.build();
1146+
return GraphQLAppliedDirective.newDirective()
1147+
.name("specifiedBy")
1148+
.argument(arg)
1149+
.build();
1150+
}
1151+
1152+
private List<GraphQLAppliedDirective> updateSpecifiedByDirective(List<GraphQLAppliedDirective> directives, String url) {
1153+
GraphQLAppliedDirectiveArgument newArg = GraphQLAppliedDirectiveArgument.newArgument()
1154+
.name("url")
1155+
.valueProgrammatic(url)
1156+
.type(GraphQLString)
1157+
.build();
1158+
1159+
return directives.stream().map(d -> {
1160+
if (isSpecifiedByDirective(d)) {
1161+
return d.transform(builder -> builder.argument(newArg));
1162+
}
1163+
return d;
1164+
}).collect(toList());
1165+
}
1166+
11061167
private String directiveDefinition(GraphQLDirective directive) {
11071168
StringBuilder sb = new StringBuilder();
11081169

src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -148,28 +148,25 @@ class GraphQLSchemaTest extends Specification {
148148
((Directive) newSchema.extensionDefinitions.first().getDirectives().first()).name == "pizza"
149149
}
150150

151-
def "clear directives works as expected"() {
151+
def "all built-in directives are always present"() {
152152
setup:
153153
def schemaBuilder = basicSchemaBuilder()
154154

155-
when: "no additional directives have been specified"
155+
when: "a schema is built"
156156
def schema = schemaBuilder.build()
157-
then:
157+
then: "all 7 built-in directives are present"
158158
schema.directives.size() == 7
159-
160-
when: "clear directives is called"
161-
schema = schemaBuilder.clearDirectives().build()
162-
then:
163-
schema.directives.size() == 5 // @deprecated and @specifiedBy and @oneOf et al is ALWAYS added if missing
164-
165-
when: "clear directives is called with more directives"
166-
schema = schemaBuilder.clearDirectives().additionalDirective(Directives.SkipDirective).build()
167-
then:
168-
schema.directives.size() == 6
159+
schema.getDirective("include") != null
160+
schema.getDirective("skip") != null
161+
schema.getDirective("deprecated") != null
162+
schema.getDirective("specifiedBy") != null
163+
schema.getDirective("oneOf") != null
164+
schema.getDirective("defer") != null
165+
schema.getDirective("experimental_disableErrorPropagation") != null
169166

170167
when: "the schema is transformed, things are copied"
171-
schema = schema.transform({ builder -> builder.additionalDirective(Directives.IncludeDirective) })
172-
then:
168+
schema = schema.transform({ builder -> builder })
169+
then: "all 7 built-in directives are still present"
173170
schema.directives.size() == 7
174171
}
175172

0 commit comments

Comments
 (0)