Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/main/java/graphql/schema/diff/DiffCategory.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,9 @@ public enum DiffCategory {
/**
* The new API has changed something compared to the old API
*/
DIFFERENT
DIFFERENT,
/**
* The new API has deprecated something or removed something deprecated from the old API
*/
DEPRECATED
}
73 changes: 62 additions & 11 deletions src/main/java/graphql/schema/diff/SchemaDiff.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import graphql.introspection.IntrospectionResultToSchema;
import graphql.language.Argument;
import graphql.language.Directive;
import graphql.language.DirectivesContainer;
import graphql.language.Document;
import graphql.language.EnumTypeDefinition;
import graphql.language.EnumValueDefinition;
Expand Down Expand Up @@ -269,6 +270,10 @@ private void checkType(DiffCtx ctx, Type oldType, Type newType) {
ctx.exitType();
}

private boolean isDeprecated(DirectivesContainer node) {
return node.getDirective("deprecated") != null;
}

private boolean isReservedType(String typeName) {
return typeName.startsWith("__");
}
Expand Down Expand Up @@ -370,12 +375,21 @@ private void checkInputFields(DiffCtx ctx, TypeDefinition old, List<InputValueDe


if (!newField.isPresent()) {
DiffCategory category;
String message;
if (isDeprecated(oldField)) {
category = DiffCategory.DEPRECATED;
message = "The new API has removed a deprecated field '%s'";
} else {
category = DiffCategory.MISSING;
message = "The new API is missing an input field '%s'";
}
ctx.report(DiffEvent.apiBreakage()
.category(DiffCategory.MISSING)
.category(category)
.typeName(old.getName())
.typeKind(getTypeKind(old))
.fieldName(oldField.getName())
.reasonMsg("The new API is missing an input field '%s'", mkDotName(old.getName(), oldField.getName()))
.reasonMsg(message, mkDotName(old.getName(), oldField.getName()))
.build());
} else {
DiffCategory category = checkTypeWithNonNullAndList(oldField.getType(), newField.get().getType());
Expand All @@ -394,7 +408,7 @@ private void checkInputFields(DiffCtx ctx, TypeDefinition old, List<InputValueDe
//
// recurse via input types
//
checkType( ctx, oldField.getType(), newField.get().getType() );
checkType(ctx, oldField.getType(), newField.get().getType());
}
}

Expand Down Expand Up @@ -427,12 +441,21 @@ private void checkEnumType(DiffCtx ctx, EnumTypeDefinition oldDef, EnumTypeDefin
Optional<EnumValueDefinition> newEnum = Optional.ofNullable(newDefinitionMap.get(enumName));

if (!newEnum.isPresent()) {
DiffCategory category;
String message;
if (isDeprecated(oldEnum)) {
category = DiffCategory.DEPRECATED;
message = "The new API has removed a deprecated enum value '%s'";
} else {
category = DiffCategory.MISSING;
message = "The new API is missing an enum value '%s'";
}
ctx.report(DiffEvent.apiBreakage()
.category(DiffCategory.MISSING)
.category(category)
.typeName(oldDef.getName())
.typeKind(getTypeKind(oldDef))
.components(oldEnum.getName())
.reasonMsg("The new API is missing an enum value '%s'", oldEnum.getName())
.reasonMsg(message, oldEnum.getName())
.build());
} else {
checkDirectives(ctx, oldDef, oldEnum.getDirectives(), newEnum.get().getDirectives());
Expand All @@ -449,6 +472,14 @@ private void checkEnumType(DiffCtx ctx, EnumTypeDefinition oldDef, EnumTypeDefin
.components(enumName)
.reasonMsg("The new API has added a new enum value '%s'", enumName)
.build());
} else if (isDeprecated(newDefinitionMap.get(enumName))) {
ctx.report(DiffEvent.apiDanger()
.category(DiffCategory.DEPRECATED)
.typeName(oldDef.getName())
.typeKind(getTypeKind(oldDef))
.components(enumName)
.reasonMsg("The new API has deprecated an enum value '%s'", enumName)
.build());
}
}
checkDirectives(ctx, oldDef, newDef);
Expand All @@ -463,18 +494,21 @@ private void checkImplements(DiffCtx ctx, ObjectTypeDefinition old, List<Type> o
Map<String, Type> newImplementsMap = sortedMap(newImplements, t -> ((TypeName) t).getName());

for (Map.Entry<String, Type> entry : oldImplementsMap.entrySet()) {
InterfaceTypeDefinition oldInterface = ctx.getOldTypeDef(entry.getValue(), InterfaceTypeDefinition.class).get();
Optional<InterfaceTypeDefinition> oldInterface = ctx.getOldTypeDef(entry.getValue(), InterfaceTypeDefinition.class);
if (!oldInterface.isPresent()) {
continue;
}
Optional<InterfaceTypeDefinition> newInterface = ctx.getNewTypeDef(newImplementsMap.get(entry.getKey()), InterfaceTypeDefinition.class);
if (!newInterface.isPresent()) {
ctx.report(DiffEvent.apiBreakage()
.category(DiffCategory.MISSING)
.typeName(old.getName())
.typeKind(getTypeKind(old))
.components(oldInterface.getName())
.reasonMsg("The new API is missing the interface named '%s'", oldInterface.getName())
.components(oldInterface.get().getName())
.reasonMsg("The new API is missing the interface named '%s'", oldInterface.get().getName())
.build());
} else {
checkInterfaceType(ctx, oldInterface, newInterface.get());
checkInterfaceType(ctx, oldInterface.get(), newInterface.get());
}
}
}
Expand Down Expand Up @@ -509,12 +543,21 @@ private void checkFieldRemovals(

FieldDefinition newField = newFields.get(fieldName);
if (newField == null) {
DiffCategory category;
String message;
if (isDeprecated(entry.getValue())) {
category = DiffCategory.DEPRECATED;
message = "The new API has removed a deprecated field '%s'";
} else {
category = DiffCategory.MISSING;
message = "The new API is missing the field '%s'";
}
ctx.report(DiffEvent.apiBreakage()
.category(DiffCategory.MISSING)
.category(category)
.typeName(oldDef.getName())
.typeKind(getTypeKind(oldDef))
.fieldName(fieldName)
.reasonMsg("The new API is missing the field '%s'", mkDotName(oldDef.getName(), fieldName))
.reasonMsg(message, mkDotName(oldDef.getName(), fieldName))
.build());
} else {
checkField(ctx, oldDef, entry.getValue(), newField);
Expand Down Expand Up @@ -547,6 +590,14 @@ private void checkFieldAdditions(
.fieldName(fieldName)
.reasonMsg("The new API adds the field '%s'", mkDotName(newDef.getName(), fieldName))
.build());
} else if (!isDeprecated(oldField) && isDeprecated(entry.getValue())) {
ctx.report(DiffEvent.apiDanger()
.category(DiffCategory.DEPRECATED)
.typeName(newDef.getName())
.typeKind(getTypeKind(newDef))
.fieldName(fieldName)
.reasonMsg("The new API deprecated a field '%s'", mkDotName(newDef.getName(), fieldName))
.build());
}
}
}
Expand Down
34 changes: 33 additions & 1 deletion src/test/groovy/graphql/schema/diff/SchemaDiffTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ class SchemaDiffTest extends Specification {

}

def "dangerous changes "() {
def "dangerous changes"() {
DiffSet diffSet = diffSet("schema_dangerous_changes.graphqls")

def diff = new SchemaDiff()
Expand Down Expand Up @@ -479,4 +479,36 @@ class SchemaDiffTest extends Specification {

}

def "field was deprecated"() {
DiffSet diffSet = diffSet("schema_deprecated_fields_new.graphqls")

def diff = new SchemaDiff()
diff.diffSchema(diffSet, chainedReporter)

expect:
reporter.dangerCount == 13
reporter.breakageCount == 0
reporter.dangers.every {
it.getCategory() == DiffCategory.DEPRECATED
}

}

def "deprecated field was removed"() {
def schemaOld = TestUtil.schemaFile("diff/" + "schema_deprecated_fields_new.graphqls", wireWithNoFetching())
def schemaNew = TestUtil.schemaFile("diff/" + "schema_deprecated_fields_removed.graphqls", wireWithNoFetching())

DiffSet diffSet = DiffSet.diffSet(schemaOld, schemaNew)

def diff = new SchemaDiff()
diff.diffSchema(diffSet, chainedReporter)

expect:
reporter.dangerCount == 0
reporter.breakageCount == 11
reporter.breakages.every {
it.getCategory() == DiffCategory.DEPRECATED
}
}

}
81 changes: 81 additions & 0 deletions src/test/resources/diff/schema_deprecated_fields_new.graphqls
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
schema {
query : Query
mutation : Mutation
}

type Query {
being(id : ID, type : String = "wizard") : Being
beings(type : String) : [Being] @deprecated

wizards : [Istari]
gods : [Ainur]
deities : [Deity] @deprecated

allCharacters : [Character!] @deprecated(reason: "no longer supported")

customScalar : CustomScalar @deprecated(reason: "because")
}

type Mutation {
being(questor : Questor) : Query
sword(bearer : Questor, name : String, alloy : String, temperament : Temperament) : String
}

input Questor {
beingID : ID
queryTarget : String
nestedInput : NestedInput
}

input NestedInput {
nestedInput: String
}

scalar CustomScalar


interface Being {
id : ID
name : String
nameInQuenyan : String @deprecated
invitedBy(id : ID) : Being
}


type Ainur implements Being {
id : ID
name : String
nameInQuenyan : String @deprecated
invitedBy(id : ID) : Being
loves : String
}


type Istari implements Being {
id : ID
name : String
nameInQuenyan : String @deprecated
invitedBy(id : ID) : Being
colour : String @deprecated
temperament : Temperament!

}

type Deity implements Being {
id : ID
name : String
nameInQuenyan : String @deprecated
invitedBy(id : ID) : Being
outlook : String @deprecated
}

union Character = Ainur | Istari | Deity

enum Temperament {
Hero
Duplicitous @deprecated
Evil
}



68 changes: 68 additions & 0 deletions src/test/resources/diff/schema_deprecated_fields_removed.graphqls
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
schema {
query : Query
mutation : Mutation
}

type Query {
being(id : ID, type : String = "wizard") : Being

wizards : [Istari]
gods : [Ainur]
}

type Mutation {
being(questor : Questor) : Query
sword(bearer : Questor, name : String, alloy : String, temperament : Temperament) : String
}

input Questor {
beingID : ID
queryTarget : String
nestedInput : NestedInput
}

input NestedInput {
nestedInput: String
}

scalar CustomScalar


interface Being {
id : ID
name : String
invitedBy(id : ID) : Being
}


type Ainur implements Being {
id : ID
name : String
invitedBy(id : ID) : Being
loves : String
}


type Istari implements Being {
id : ID
name : String
invitedBy(id : ID) : Being
temperament : Temperament!

}

type Deity implements Being {
id : ID
name : String
invitedBy(id : ID) : Being
}

union Character = Ainur | Istari | Deity

enum Temperament {
Hero
Evil
}