Skip to content

Commit 845d940

Browse files
authored
🔄 Merge pull request Ambro17#18 from Ambro17/add-max-field-arguments-rule
Add max field arguments rule
2 parents f35e460 + e534c5a commit 845d940

File tree

6 files changed

+164
-24
lines changed

6 files changed

+164
-24
lines changed

schemadiff/changes/field.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from graphql import is_non_null_type
1+
from graphql import is_non_null_type, GraphQLField
22

33
from schemadiff.changes import Change, Criticality, is_safe_type_change
44

@@ -71,13 +71,14 @@ def path(self):
7171

7272

7373
class FieldArgumentAdded(Change):
74-
def __init__(self, parent, field_name, argument_name, arg_type):
74+
def __init__(self, parent, field_name: str, field: GraphQLField, argument_name, arg_type):
7575
self.criticality = Criticality.safe('Adding an optional argument is a safe change')\
7676
if not is_non_null_type(arg_type.type)\
7777
else Criticality.breaking("Adding a required argument to an existing field is a breaking "
7878
"change because it will break existing uses of this field")
7979
self.parent = parent
8080
self.field_name = field_name
81+
self.field = field
8182
self.argument_name = argument_name
8283
self.arg_type = arg_type
8384

schemadiff/changes/object.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1+
from graphql import GraphQLField, GraphQLObjectType
2+
13
from schemadiff.changes import Change, Criticality
24

35

46
class ObjectTypeFieldAdded(Change):
57

68
criticality = Criticality.safe()
79

8-
def __init__(self, parent, field_name):
10+
def __init__(self, parent: GraphQLObjectType, field_name, field: GraphQLField):
911
self.parent = parent
1012
self.field_name = field_name
13+
self.field = field
1114
self.description = parent.fields[field_name].description
1215

1316
@property

schemadiff/diff/field.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010

1111
class Field:
1212

13-
def __init__(self, type_, name, old_field, new_field):
14-
self.type_ = type_
13+
def __init__(self, parent, name, old_field, new_field):
14+
self.parent = parent
1515
self.field_name = name
1616
self.old_field = old_field
1717
self.new_field = new_field
@@ -22,31 +22,31 @@ def diff(self):
2222
changes = []
2323

2424
if self.old_field.description != self.new_field.description:
25-
changes.append(FieldDescriptionChanged(self.type_, self.field_name, self.old_field, self.new_field))
25+
changes.append(FieldDescriptionChanged(self.parent, self.field_name, self.old_field, self.new_field))
2626

2727
if self.old_field.deprecation_reason != self.new_field.deprecation_reason:
28-
changes.append(FieldDeprecationReasonChanged(self.type_, self.field_name, self.old_field, self.new_field))
28+
changes.append(FieldDeprecationReasonChanged(self.parent, self.field_name, self.old_field, self.new_field))
2929

3030
if str(self.old_field.type) != str(self.new_field.type):
31-
changes.append(FieldTypeChanged(self.type_, self.field_name, self.old_field, self.new_field))
31+
changes.append(FieldTypeChanged(self.parent, self.field_name, self.old_field, self.new_field))
3232

3333
added = self.new_args - self.old_args
3434
removed = self.old_args - self.new_args
3535

3636
changes.extend(
37-
FieldArgumentAdded(self.type_, self.field_name, arg_name, self.new_field.args[arg_name])
37+
FieldArgumentAdded(self.parent, self.field_name, self.new_field, arg_name, self.new_field.args[arg_name])
3838
for arg_name in added
3939
)
4040
changes.extend(
41-
FieldArgumentRemoved(self.type_, self.field_name, arg_name)
41+
FieldArgumentRemoved(self.parent, self.field_name, arg_name)
4242
for arg_name in removed
4343
)
4444

4545
common_arguments = self.common_arguments()
4646
for arg_name in common_arguments:
4747
old_arg = self.old_field.args[arg_name]
4848
new_arg = self.new_field.args[arg_name]
49-
changes += Argument(self.type_, self.field_name, arg_name, old_arg, new_arg).diff() or []
49+
changes += Argument(self.parent, self.field_name, arg_name, old_arg, new_arg).diff() or []
5050

5151
return changes
5252

schemadiff/diff/object_type.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ def __init__(self, old, new):
99
self.old = old
1010
self.new = new
1111

12-
self.old_fields = set(old.fields)
13-
self.new_fields = set(new.fields)
12+
self.old_field_names = set(old.fields)
13+
self.new_field_names = set(new.fields)
1414

1515
self.old_interfaces = set(old.interfaces)
1616
self.new_interfaces = set(new.interfaces)
@@ -19,9 +19,9 @@ def diff(self):
1919
changes = []
2020

2121
# Added and removed fields
22-
added = self.new_fields - self.old_fields
23-
removed = self.old_fields - self.new_fields
24-
changes.extend(ObjectTypeFieldAdded(self.new, field_name) for field_name in added)
22+
added = self.new_field_names - self.old_field_names
23+
removed = self.old_field_names - self.new_field_names
24+
changes.extend(ObjectTypeFieldAdded(self.new, field_name, self.new.fields[field_name]) for field_name in added)
2525
changes.extend(ObjectTypeFieldRemoved(self.new, field_name, self.old.fields[field_name])
2626
for field_name in removed)
2727

@@ -31,15 +31,15 @@ def diff(self):
3131
changes.extend(NewInterfaceImplemented(interface, self.new) for interface in added)
3232
changes.extend(DroppedInterfaceImplementation(interface, self.new) for interface in removed)
3333

34-
for field in self.common_fields():
35-
old_field = self.old.fields[field]
36-
new_field = self.new.fields[field]
37-
changes += Field(self.new, field, old_field, new_field).diff() or []
34+
for field_name in self.common_fields():
35+
old_field = self.old.fields[field_name]
36+
new_field = self.new.fields[field_name]
37+
changes += Field(self.new, field_name, old_field, new_field).diff() or []
3838

3939
return changes
4040

4141
def common_fields(self):
42-
return self.old_fields & self.new_fields
42+
return self.old_field_names & self.new_field_names
4343

4444
def added_interfaces(self):
4545
"""Compare interfaces equality by name. Internal diffs are solved later"""

schemadiff/validation_rules/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from graphql import GraphQLObjectType
55

66
from schemadiff.changes.enum import EnumValueAdded, EnumValueDescriptionChanged
7-
from schemadiff.changes.field import FieldDescriptionChanged
7+
from schemadiff.changes.field import FieldDescriptionChanged, FieldArgumentAdded
88
from schemadiff.changes.object import ObjectTypeFieldAdded
99
from schemadiff.changes.type import AddedType, TypeDescriptionChanged
1010

@@ -62,8 +62,6 @@ def is_valid(self) -> bool:
6262
else:
6363
return type_has_description
6464

65-
return True
66-
6765
@property
6866
def message(self):
6967
return f"{self.change.message} without a description for {self.change.path} (rule: `{self.name}`)."
@@ -144,3 +142,5 @@ def is_valid(self) -> bool:
144142
@property
145143
def message(self):
146144
return f"Description for enum value `{self.change.name}` was removed (rule: `{self.name}`)"
145+
146+

tests/test_validation_rules.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from graphql import build_schema as schema
33

44
from schemadiff.changes import Change, Criticality
5+
from schemadiff.changes.field import FieldArgumentAdded
6+
from schemadiff.changes.object import ObjectTypeFieldAdded
57
from schemadiff.validation import evaluate_rules, ValidationResult, ValidationError
68
from schemadiff.validation_rules import (
79
ValidationRule,
@@ -230,3 +232,137 @@ def test_schema_added_field_no_desc():
230232
)
231233
assert diff[0].path == 'AddedType.other'
232234

235+
236+
def test_cant_create_mutation_with_more_than_10_arguments():
237+
schema_restrictions = ['field-has-too-many-arguments']
238+
239+
class FieldHasTooManyArguments(ValidationRule):
240+
"""Restrict adding fields with too many top level arguments"""
241+
242+
name = "field-has-too-many-arguments"
243+
limit = 10
244+
245+
def is_valid(self) -> bool:
246+
if not isinstance(self.change, (ObjectTypeFieldAdded, FieldArgumentAdded)):
247+
return True
248+
249+
if len(self.args) > self.limit:
250+
return False
251+
else:
252+
return True
253+
254+
@property
255+
def args(self):
256+
return self.change.field.args or {}
257+
258+
@property
259+
def message(self):
260+
return f"Field `{self.change.parent.name}.{self.change.field_name}` has too many arguments " \
261+
f"({len(self.args)}>{self.limit}). Rule: {self.name}"
262+
263+
old_schema = schema("""
264+
schema {
265+
mutation: Mutation
266+
}
267+
268+
type Mutation {
269+
field: Int
270+
}
271+
""")
272+
273+
new_schema = schema("""
274+
schema {
275+
mutation: Mutation
276+
}
277+
278+
type Mutation {
279+
field: Int
280+
mutation_with_too_many_args(
281+
a1: Int, a2: Int, a3: Int, a4: Int, a5: Int, a6: Int, a7: Int, a8: Int, a9: Int, a10: Int, a11: Int
282+
): Int
283+
}
284+
""")
285+
286+
diff = Schema(old_schema, new_schema).diff()
287+
# Type Int was also added but its ignored because its a primitive.
288+
assert diff and len(diff) == 1
289+
error_msg = (
290+
'Field `Mutation.mutation_with_too_many_args` has too many arguments (11>10). '
291+
'Rule: field-has-too-many-arguments'
292+
)
293+
assert evaluate_rules(diff, schema_restrictions) == ValidationResult(
294+
ok=False,
295+
errors=[ValidationError(error_msg)]
296+
)
297+
assert diff[0].path == 'Mutation.mutation_with_too_many_args'
298+
299+
300+
def test_cant_add_arguments_to_mutation_if_exceeds_10_args():
301+
schema_restrictions = ['field-has-too-many-arguments']
302+
303+
class FieldHasTooManyArguments(ValidationRule):
304+
"""Restrict adding fields with too many top level arguments"""
305+
306+
name = "field-has-too-many-arguments"
307+
limit = 10
308+
309+
def is_valid(self) -> bool:
310+
if not isinstance(self.change, (ObjectTypeFieldAdded, FieldArgumentAdded)):
311+
return True
312+
313+
if len(self.args) > self.limit:
314+
return False
315+
else:
316+
return True
317+
318+
@property
319+
def args(self):
320+
return self.change.field.args or {}
321+
322+
@property
323+
def message(self):
324+
return f"Field `{self.change.parent.name}.{self.change.field_name}` has too many arguments " \
325+
f"({len(self.args)}>{self.limit}). Rule: {self.name}"
326+
327+
old_schema = schema("""
328+
schema {
329+
mutation: Mutation
330+
}
331+
332+
type Mutation {
333+
field: Int
334+
mutation_with_too_many_args(
335+
a1: Int, a2: Int, a3: Int, a4: Int, a5: Int, a6: Int, a7: Int, a8: Int, a9: Int, a10: Int
336+
): Int
337+
}
338+
""")
339+
340+
new_schema = schema("""
341+
schema {
342+
mutation: Mutation
343+
}
344+
345+
type Mutation {
346+
field: Int
347+
mutation_with_too_many_args(
348+
a1: Int, a2: Int, a3: Int, a4: Int, a5: Int, a6: Int, a7: Int, a8: Int, a9: Int, a10: Int, a11: Int
349+
): Int
350+
}
351+
""")
352+
353+
diff = Schema(old_schema, new_schema).diff()
354+
# Type Int was also added but its ignored because its a primitive.
355+
assert diff and len(diff) == 1
356+
error_msg = (
357+
'Field `Mutation.mutation_with_too_many_args` has too many arguments (11>10). '
358+
'Rule: field-has-too-many-arguments'
359+
)
360+
assert evaluate_rules(diff, schema_restrictions) == ValidationResult(
361+
ok=False,
362+
errors=[ValidationError(error_msg)]
363+
)
364+
assert diff[0].path == 'Mutation.mutation_with_too_many_args'
365+
366+
367+
def test_mutation_input_fields_cant_share_prefix():
368+
pass

0 commit comments

Comments
 (0)