Skip to content

Improving applied directive & directive builders #3825

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

Merged
merged 10 commits into from
Mar 20, 2025
Merged

Conversation

dondonz
Copy link
Member

@dondonz dondonz commented Feb 16, 2025

Opening this PR for discussion.

For directive containers, @gnawf reported that the withAppliedDirectives builder surprisingly also cleared all of the existing directives, given that there is already a replaceAppliedDirectives builder. This led to a bug, and it was quickly fixed by using the singular withAppliedDirective builder, which doesn't delete any prior applied directives.

I agree that it is surprising, I would expect with implies that applied directives are only added without deleting existing applied directives. It seems like the previous code, before the transition to Applied Directives, also had this behaviour. I'm open here, maybe this is intended behaviour and I don't know the story behind it.

I think it would be better to have two sorts of builders, one that only adds applied directives, and another sort that can clear and replace all the existing applied directives, rather than the current state where two differently named methods are doing the same thing (replacement).

To make this change clearer I would like to introduce a new method addAppliedDirectives to supercede the withAppliedDirectives methods.

I guess this issue wasn't reported earlier because it is a less common use case. If a person wants to retain the legacy behaviour, they can switch to using replaceAppliedDirectives instead.

Although this is small, this is a breaking change so I would like to discuss this before making changes.

@dondonz dondonz added the breaking change requires a new major version to be relased label Feb 16, 2025
@dondonz dondonz added this to the 23.x breaking changes milestone Feb 16, 2025
@@ -7,7 +7,6 @@

import static graphql.Assert.assertNotNull;

@SuppressWarnings("unchecked")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IDEA told me it's no longer needed, I do what the computer says


public B addAppliedDirective(GraphQLAppliedDirective.Builder builder) {
return addAppliedDirective(builder.build());
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it makes more sense for all the additive builders to start with "add", I have also created the "add" version for the singular method, but I think the prior implementation was correct

addAppliedDirective(directive);
}
return (B) this;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This no longer clears existing directives

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a copy and paste bug

we have

public B replaceAppliedDirectives(List<GraphQLAppliedDirective> directives) {
        assertNotNull(directives, () -> "directive can't be null");
        this.appliedDirectives.clear();
        this.appliedDirectives.addAll(directives);
        return (B) this;
    }

    public B withAppliedDirectives(GraphQLAppliedDirective... directives) {
        assertNotNull(directives, () -> "directives can't be null");
        this.appliedDirectives.clear();       /// COPY PASTE BUG HERE
        for (GraphQLAppliedDirective directive : directives) {
            withAppliedDirective(directive);
        }
        return (B) this;
    }

I think it was never meant to clear it because as you say we have non clearing singular version

    public B withAppliedDirective(GraphQLAppliedDirective directive) {
        assertNotNull(directive, () -> "directive can't be null");
        this.appliedDirectives.add(directive);
        return (B) this;
    }

I agree that addXXX might be more clearer but honestly I would change the bheavior of the original withXXX rather than add a new method. This is a breaking change after all

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmmmmmm or maybe not

    public B withDirectives(GraphQLDirective... directives) {
        assertNotNull(directives, () -> "directives can't be null");
        this.directives.clear();
        for (GraphQLDirective directive : directives) {
            withDirective(directive);
        }
        return (B) this;
    }

from another class does the same thing

copy and paste copy and paste bug??

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see other "withXXX plural" and they dont do the clear behavior

        public Builder withSchemaDirectives(GraphQLDirective... directives) {
            for (GraphQLDirective directive : directives) {
                withSchemaDirective(directive);
            }
            return this;
        }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right we have a combo of a copy paste of possibly a bug and then a typo that led to unusual behaviour

My preference is to change withXXX rather than introducing new addXXX methods, because most people are used to builders having "with", but wasn't sure how cautious to be

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

y preference is to change withXXX rather than introducing new addXXX methods,
I concur. Lets do that

Copy link
Contributor

github-actions bot commented Feb 16, 2025

Test Results

  310 files    310 suites   46s ⏱️
3 565 tests 3 559 ✅ 6 💤 0 ❌
3 654 runs  3 648 ✅ 6 💤 0 ❌

Results for commit 12eaa86.

♻️ This comment has been updated with latest results.

* @deprecated - use {@link #replaceAppliedDirectives(List)} to clear and replace directives,
* or {@link #addAppliedDirectives(GraphQLAppliedDirective...)} to add directives without clearing the existing ones
*/
@Deprecated(since = "2025-02-16", forRemoval = true)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forRemoval is set to true because this behaviour is identical in replaceAppliedDirectives

* @deprecated - use {@link #replaceAppliedDirectives(List)} to clear and replace directives,
* or {@link #addAppliedDirectives(GraphQLAppliedDirective...)} to add directives without clearing the existing ones
*/
@Deprecated(since = "2025-02-16", forRemoval = true)
public B withAppliedDirectives(GraphQLAppliedDirective... directives) {
assertNotNull(directives, () -> "directives can't be null");
this.appliedDirectives.clear();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the unusual line - directives are cleared

*
* @deprecated - use {@link #addAppliedDirective(GraphQLAppliedDirective.Builder)} instead
*/
@Deprecated(since = "2025-02-16", forRemoval = true)
public B withAppliedDirective(GraphQLAppliedDirective.Builder builder) {
return withAppliedDirectives(builder.build());
Copy link
Member Author

@dondonz dondonz Feb 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may have been an unintentional typo during the major migration to Applied Directives.

The previous singular withAppliedDirective method that takes a GraphQLAppliedDirective does not clear any prior directives, but this withAppliedDirective with a builder parameter will take clear existing directives because it calls the plural withAppliedDirectives method

The new 'addAppliedDirective' method corresponding to this one (with a builder parameter) does not clear directives

@@ -100,7 +100,7 @@ class ValuesResolverTest extends Specification {
.type(GraphQLString)
def inputType = newInputObject()
.name("Person")
.withAppliedDirective(Directives.OneOfDirective.toAppliedDirective())
.addAppliedDirective(Directives.OneOfDirective.toAppliedDirective())
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated tests to use the new "add" versions of builders, and I added a few legacy tests for coverage. We can delete the legacy tests when we removed the deprecated methods

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scroll down to AppliedDirectivesAreValid test file to see the test that demonstrates the change in behaviour

hasError(schemaProblem, "The directive 'directiveA' on the 'GraphQLFieldDefinition' called 'fieldD' is a non repeatable directive but has been applied 2 times");
hasError(schemaProblem, "The directive 'directiveA' on the 'GraphQLFieldDefinition' called 'fieldA' is a non repeatable directive but has been applied 2 times");
hasError(schemaProblem, "The directive 'directiveA' on the 'GraphQLEnumValueDefinition' called 'enumA' is a non repeatable directive but has been applied 2 times");
hasError(schemaProblem, "The directive 'directiveA' on the 'GraphQLInputObjectField' called 'inputFieldA' is a non repeatable directive but has been applied 2 times");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IDEA told me to delete the semi colons, I do what the computer says

hasError(schemaProblem, "The directive 'directiveA' on the 'GraphQLInputObjectField' called 'inputFieldA' is a non repeatable directive but has been applied 2 times")
}

def "add applied directive builders do not clear any existing applied directives"(){
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are a pair of tests to demonstrate the change of "clearing" behaviour more clearly

Copy link
Member Author

@dondonz dondonz Feb 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There isn't already a directive container test file, adding here instead for now

@dondonz dondonz changed the title WIP Improving applied directive builders Improving applied directive builders Feb 16, 2025
Copy link
Member

@bbakerman bbakerman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We spoke last week about this removing the add method and changing the with methods

Marking this as "request changes" to indicate that

public B withAppliedDirectives(GraphQLAppliedDirective... directives) {
assertNotNull(directives, () -> "directives can't be null");
this.appliedDirectives.clear();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the unintended "clear" behaviour

public B withAppliedDirective(GraphQLAppliedDirective.Builder builder) {
return withAppliedDirectives(builder.build());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to have been a typo

@@ -2,7 +2,6 @@ package graphql.execution

import graphql.Directives
import graphql.ErrorType
import graphql.ExecutionInput
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not used

@@ -67,7 +78,6 @@ public B replaceDirectives(List<GraphQLDirective> directives) {
@Deprecated(since = "2022-02-24")
public B withDirectives(GraphQLDirective... directives) {
assertNotNull(directives, () -> "directives can't be null");
this.directives.clear();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is deprecated, but let's also remove the unintended clear line from here

@dondonz
Copy link
Member Author

dondonz commented Mar 16, 2025

I've changed the existing with builders instead of adding a new set of add builders

hasError(schemaProblem, "The directive 'directiveA' on the 'GraphQLInputObjectField' called 'inputFieldA' is a non repeatable directive but has been applied 2 times")
}

def "applied directive builders do not clear any existing applied directives"() {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new test to lock in the new behaviour

@bbakerman bbakerman merged commit 86724a4 into master Mar 20, 2025
2 checks passed
@dondonz dondonz changed the title Improving applied directive builders Improving applied directive & directive builders Apr 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking change requires a new major version to be relased
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants