graphql-schema_comparator 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e193c10ab21868ab4bf6fd6f3159b4d3e4a67525
4
- data.tar.gz: 9603fd58643f3a1dabecb23ff543a646657b1f3a
3
+ metadata.gz: 2bd04f7cdf16f7bc67fb74e219478457b180e022
4
+ data.tar.gz: eb021f9745da0a4fed5fb2cedfb5a848ca1685b0
5
5
  SHA512:
6
- metadata.gz: bced670ccc797b2222231e60d930d167e1856d820722c0c4946c45fe2d84c96fcdd50f9e91089a83a82adc9f9dee212aa6636c810a48e57745ece7b7a1d48c50
7
- data.tar.gz: ea0916f65eb4de48038077dff40add62c7fa9d5581293d1618c4b045086545b465e13aecfb0c0390f1147496d47bcee7c5ef10c09ec0c5f5cf2f4503bb04bbb8
6
+ metadata.gz: 7a6d826fd10f0bb42eebcc9546c77d40b32242d5c6888181e5dab77b7c1f9d4ce8d4be192ef750209d98d7230ee81aa0121645f6c1482b6fc1d80e9d5287f02e
7
+ data.tar.gz: f43508482f55b5b88b1627b5b9d12ce6800512a2386cb66785348f01951d25384d1142d62370dc7d339530d0593998200005ced1a4ae01402a24940676bbc62d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 (Dec 2 2017)
4
+
5
+ ## New Features
6
+
7
+ - `AbstractChange#criticality` now returns a criticality object which
8
+ has a level (non_breaking, dangerous, breaking) and a reason
9
+
10
+ - Schema::ComparatorResult maintains a list of `#dangerous_changes`
11
+
12
+ - New Methods: Change.non_breaking? Change.dangerous?
13
+
14
+ - New CLI `schema_comparator` which includes `dangerous_changes`
15
+
16
+ ## Breaking Changes
17
+
18
+ - Some changes have been recategorized as dangerous
19
+ - Some type changes now return breaking or non-breaking depending on the type kind
20
+
3
21
  ## 0.4.0 (Nov 27 2017)
4
22
 
5
23
  ## Breaking Changes
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Build Status](https://travis-ci.org/xuorig/graphql-schema_comparator.svg?branch=master)](https://travis-ci.org/xuorig/graphql-schema_comparator)
4
4
 
5
5
  `GraphQL::SchemaComparator` is a GraphQL Schema comparator. What does that mean? `GraphQL::SchemaComparator` takes
6
- two GraphQL schemas and outputs a list of changes that happened between the two versions. This is useful for many things:
6
+ two GraphQL schemas and outputs a list of changes between versions. This is useful for many things:
7
7
 
8
8
  - Breaking Change detection
9
9
  - Applying custom rules to schema changes
@@ -28,26 +28,40 @@ Or install it yourself as:
28
28
  ## CLI
29
29
 
30
30
  `GraphQL::SchemaComparator` comes with a handy CLI to help compare two schemas using
31
- the commandline.
31
+ the command line.
32
32
 
33
33
  After a `gem install graphql-schema_comparator`, use the CLI this way:
34
34
 
35
35
  ```
36
36
  Commands:
37
- graphql-schema compare OLD_SCHEMA NEW_SCHEMA # Compares OLD_SCHEMA with NEW_SCHEMA and returns a list of changes
38
- graphql-schema help [COMMAND] # Describe available commands or one specific command
37
+ schema_comparator compare OLD_SCHEMA NEW_SCHEMA # Compares OLD_SCHEMA with NEW_SCHEMA and returns a list of changes
38
+ schema_comparator help [COMMAND] # Describe available commands or one specific command
39
39
  ```
40
40
 
41
41
  Where OLD_SCHEMA and NEW_SCHEMA can be a string containing a schema IDL or a filename where that IDL is located.
42
42
 
43
43
  ### Example
44
44
 
45
- ![comparator result](http://i.imgur.com/FnItukM.png)
45
+ ```
46
+ $ ./bin/schema_comparator compare "type Query { a: A } type A { a: String } enum B { A_VALUE }" "type Query { a: A } type A { b: String } enum B { A_VALUE ANOTHER_VALUE }"
47
+ ⏳ Checking for changes...
48
+ 🎉 Done! Result:
49
+
50
+ Detected the following changes between schemas:
51
+
52
+ 🛑 Field `a` was removed from object type `A`
53
+ ⚠️ Enum value `ANOTHER_VALUE` was added to enum `B`
54
+ ✅ Field `b` was added to object type `A`
55
+ ```
46
56
 
47
57
  ## Usage
48
58
 
49
59
  `GraphQL::SchemaComparator`, provides a simple api for Ruby applications to use.
50
60
 
61
+ ## Docs
62
+
63
+ http://www.rubydoc.info/github/xuorig/graphql-schema_comparator/master/GraphQL/SchemaComparator
64
+
51
65
  ### GraphQL::SchemaComparator.compare
52
66
 
53
67
  The compare method takes two arguments, `old_schema` and `new_schema`, the two schemas to compare.
@@ -61,31 +75,22 @@ access information on the changes between the two schemas.
61
75
  - `result.identical?` returns true if the two schemas were identical
62
76
  - `result.breaking_changes` returns the list of breaking changes found between schemas.
63
77
  - `result.non_breaking_changes` returns the list of non-breaking changes found between schemas.
78
+ - `result.dangerous_changes` returns the list of dangerous changes found between schemas.
64
79
  - `result.changes` returns the full list of change objects.
65
80
 
66
81
  ### Change Objects
67
82
 
68
- Change objects are considered any objects that respond to `message` and `breaking` and they
69
- are all namespaced under the `Changes` module.
83
+ `GraphQL::SchemaComparator` returns a list of change objects. These change objects
84
+ all inherit from `Changes::AbstractChange`
70
85
 
71
86
  Possible changes are all found in [changes.rb](lib/graphql/schema_comparator/changes.rb).
72
87
 
73
- ## TODO
74
-
75
- - [ ] Handle changes in schema directives
76
- - [ ] Test each differ
77
-
78
- ## Development
79
-
80
- After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
81
-
82
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
83
-
84
- ## Contributing
85
-
86
- Bug reports and pull requests are welcome on GitHub at https://github.com/xuorig/graphql-schema_comparator. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
88
+ ### Change Criticality
87
89
 
90
+ Each change object has a ``#criticality` method which returns a `Changes::Criticality` object.
91
+ This objects defines how dangerous a change is to a schema.
88
92
 
89
- ## License
93
+ The different levels of criticality (non_breaking, dangerous, breaking) are explained here:
94
+ https://github.com/xuorig/graphql-schema_comparator/blob/master/lib/graphql/schema_comparator/changes/criticality.rb#L6-L19
90
95
 
91
96
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/bin/graphql-schema CHANGED
@@ -8,6 +8,8 @@ class GraphQLSchema < Thor
8
8
  desc "compare OLD_SCHEMA NEW_SCHEMA", "Compares OLD_SCHEMA with NEW_SCHEMA and returns a list of changes"
9
9
 
10
10
  def compare(old_schema, new_schema)
11
+ say "[Warning] graphql-schema is deprecated. Please use `schema_comparator` instead", :yellow
12
+
11
13
  parsed_old = parse_schema(old_schema)
12
14
  parsed_new = parse_schema(new_schema)
13
15
 
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "thor"
5
+ require "graphql/schema_comparator"
6
+
7
+ class GraphQLSchema < Thor
8
+ desc "compare OLD_SCHEMA NEW_SCHEMA", "Compares OLD_SCHEMA with NEW_SCHEMA and returns a list of changes"
9
+
10
+ def compare(old_schema, new_schema)
11
+ parsed_old = parse_schema(old_schema)
12
+ parsed_new = parse_schema(new_schema)
13
+
14
+ say "⏳ Checking for changes..."
15
+ result = GraphQL::SchemaComparator.compare(parsed_old, parsed_new)
16
+
17
+ say "🎉 Done! Result:"
18
+ say "\n"
19
+
20
+ if result.identical?
21
+ say "✅ Schemas are identical"
22
+ else
23
+ print_changes(result)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def print_changes(result)
30
+ say "Detected the following changes between schemas:"
31
+ say "\n"
32
+
33
+ result.changes.each do |change|
34
+ if change.breaking?
35
+ say "🛑 #{change.message}", :red
36
+ elsif change.dangerous?
37
+ say "⚠️ #{change.message}", :yellow
38
+ else
39
+ say "✅ #{change.message}", :green
40
+ end
41
+ end
42
+ end
43
+
44
+ def parse_schema(schema)
45
+ if File.file?(schema)
46
+ File.read(schema)
47
+ elsif schema.is_a?(String)
48
+ schema
49
+ else
50
+ raise ArgumentError, "Invalid argument #{schema}. Must be an IDL string or file containing the schema IDL."
51
+ end
52
+ end
53
+ end
54
+
55
+ GraphQLSchema.start(ARGV)
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  f.match(%r{^(test|spec|features)/})
19
19
  end
20
20
  spec.bindir = "bin"
21
- spec.executables = ["graphql-schema"]
21
+ spec.executables = ["graphql-schema", "schema_comparator"]
22
22
  spec.require_paths = ["lib"]
23
23
 
24
24
  spec.add_dependency "graphql", "~> 1.6"
@@ -1,9 +1,10 @@
1
1
  require "graphql"
2
2
 
3
3
  require "graphql/schema_comparator/version"
4
- require "graphql/schema_comparator/changes"
5
4
  require "graphql/schema_comparator/result"
6
5
 
6
+ require 'graphql/schema_comparator/changes'
7
+
7
8
  require "graphql/schema_comparator/diff/schema"
8
9
  require "graphql/schema_comparator/diff/argument"
9
10
  require "graphql/schema_comparator/diff/directive"
@@ -18,6 +19,11 @@ require "graphql/schema_comparator/diff/union"
18
19
 
19
20
  module GraphQL
20
21
  module SchemaComparator
22
+ # Compares and returns changes for two versions of a schema
23
+ #
24
+ # @param old_schema [GraphQL::Schema, String]
25
+ # @param new_schema [GraphQL::Schema, String]
26
+ # @return [GraphQL::SchemaComparator::Result] the result of the comparison
21
27
  def self.compare(old_schema, new_schema)
22
28
  parsed_old = parse_schema(old_schema)
23
29
  parsed_new = parse_schema(new_schema)
@@ -1,41 +1,46 @@
1
+ require 'graphql/schema_comparator/changes/criticality'
2
+ require 'graphql/schema_comparator/changes/safe_type_change'
3
+
1
4
  module GraphQL
2
5
  module SchemaComparator
3
6
  module Changes
4
- module SafeTypeChange
5
- def safe_change?(old_type, new_type)
6
- if !old_type.kind.wraps? && !new_type.kind.wraps?
7
- old_type == new_type
8
- elsif old_type.kind.list? && new_type.kind.list?
9
- safe_change?(old_type.of_type, new_type.of_type)
10
- elsif old_type.kind.non_null?
11
- of_type = new_type.kind.non_null? ? new_type.of_type : new_type
12
- safe_change?(old_type.of_type, of_type)
13
- else
14
- false
15
- end
16
- end
17
- end
18
-
7
+ # Base class for change objects
19
8
  class AbstractChange
9
+ # A message describing the change that happened between the two version
10
+ # @return [String] The change message
20
11
  def message
21
12
  raise NotImplementedError
22
13
  end
23
14
 
15
+ # @return [Boolean] If the change is breaking or not
24
16
  def breaking?
17
+ criticality.breaking?
18
+ end
19
+
20
+ # @return [Boolean] If the change is dangerous or not
21
+ def dangerous?
22
+ criticality.dangerous?
23
+ end
24
+
25
+ # @return [Boolean] If the change is non breaking
26
+ def non_breaking?
27
+ criticality.non_breaking?
28
+ end
29
+
30
+ # @return [GraphQL::SchemaComparator::Changes::Criticality] The criticality of this change
31
+ def criticality
25
32
  raise NotImplementedError
26
33
  end
27
34
  end
28
35
 
36
+ # Mostly breaking changes
37
+
29
38
  class TypeRemoved < AbstractChange
30
- attr_reader :removed_type
39
+ attr_reader :removed_type, :criticality
31
40
 
32
41
  def initialize(removed_type)
33
42
  @removed_type = removed_type
34
- @breaking = true
35
- end
36
-
37
- def breaking?
38
- !!@breaking
43
+ @criticality = Changes::Criticality.breaking
39
44
  end
40
45
 
41
46
  def message
@@ -44,296 +49,499 @@ module GraphQL
44
49
  end
45
50
 
46
51
  class DirectiveRemoved < AbstractChange
47
- attr_reader :directive
52
+ attr_reader :directive, :criticality
48
53
 
49
54
  def initialize(directive)
50
55
  @directive = directive
51
- @breaking = true
56
+ @criticality = Changes::Criticality.breaking
52
57
  end
53
58
 
54
59
  def message
55
60
  "`#{directive.name}` was removed"
56
61
  end
57
-
58
- def breaking?
59
- !!@breaking
60
- end
61
62
  end
62
63
 
63
64
  class TypeKindChanged < AbstractChange
64
- attr_reader :old_type, :new_type
65
+ attr_reader :old_type, :new_type, :criticality
65
66
 
66
67
  def initialize(old_type, new_type)
67
68
  @old_type = old_type
68
69
  @new_type = new_type
69
- @breaking = true
70
+ @criticality = Changes::Criticality.breaking
70
71
  end
71
72
 
72
73
  def message
73
74
  "`#{old_type.name}` kind changed from `#{old_type.kind}` to `#{new_type.kind}`"
74
75
  end
75
-
76
- def breaking?
77
- !!@breaking
78
- end
79
76
  end
80
77
 
81
78
  class EnumValueRemoved < AbstractChange
82
- attr_reader :enum_value, :enum_type
79
+ attr_reader :enum_value, :enum_type, :criticality
83
80
 
84
81
  def initialize(enum_type, enum_value)
85
82
  @enum_value = enum_value
86
83
  @enum_type = enum_type
87
- @breaking = true
84
+ @criticality = Changes::Criticality.breaking
88
85
  end
89
86
 
90
87
  def message
91
88
  "Enum value `#{enum_value.name}` was removed from enum `#{enum_type.name}`"
92
89
  end
93
-
94
- def breaking?
95
- !!@breaking
96
- end
97
90
  end
98
91
 
99
92
  class UnionMemberRemoved < AbstractChange
100
- attr_reader :union_type, :union_member
93
+ attr_reader :union_type, :union_member, :criticality
101
94
 
102
95
  def initialize(union_type, union_member)
103
96
  @union_member = union_member
104
97
  @union_type = union_type
105
- @breaking = true
98
+ @criticality = Changes::Criticality.breaking
106
99
  end
107
100
 
108
101
  def message
109
102
  "Union member `#{union_member.name}` was removed from Union type `#{union_type.name}`"
110
103
  end
111
-
112
- def breaking?
113
- !!@breaking
114
- end
115
104
  end
116
105
 
117
106
  class InputFieldRemoved < AbstractChange
118
- attr_reader :input_object_type, :field
107
+ attr_reader :input_object_type, :field, :criticality
119
108
 
120
109
  def initialize(input_object_type, field)
121
110
  @input_object_type = input_object_type
122
111
  @field = field
123
- @breaking = true
112
+ @criticality = Changes::Criticality.breaking
124
113
  end
125
114
 
126
115
  def message
127
116
  "Input field `#{field.name}` was removed from input object type `#{input_object_type.name}`"
128
117
  end
129
-
130
- def breaking?
131
- !!@breaking
132
- end
133
118
  end
134
119
 
135
120
  class FieldArgumentRemoved < AbstractChange
136
- attr_reader :object_type, :field, :argument
121
+ attr_reader :object_type, :field, :argument, :criticality
137
122
 
138
123
  def initialize(object_type, field, argument)
139
124
  @object_type = object_type
140
125
  @field = field
141
126
  @argument = argument
142
- @breaking = true
127
+ @criticality = Changes::Criticality.breaking
143
128
  end
144
129
 
145
130
  def message
146
131
  "Argument `#{argument.name}: #{argument.type}` was removed from field `#{object_type.name}.#{field.name}`"
147
132
  end
148
-
149
- def breaking?
150
- !!@breaking
151
- end
152
133
  end
153
134
 
154
135
  class DirectiveArgumentRemoved < AbstractChange
155
- attr_reader :directive, :argument
136
+ attr_reader :directive, :argument, :criticality
156
137
 
157
138
  def initialize(directive, argument)
158
139
  @directive = directive
159
140
  @argument = argument
160
- @breaking = true
141
+ @criticality = Changes::Criticality.breaking
161
142
  end
162
143
 
163
144
  def message
164
145
  "Argument `#{argument.name}` was removed from directive `#{directive.name}`"
165
146
  end
166
-
167
- def breaking?
168
- !!@breaking
169
- end
170
147
  end
171
148
 
172
149
  class SchemaQueryTypeChanged < AbstractChange
173
- attr_reader :old_schema, :new_schema
150
+ attr_reader :old_schema, :new_schema, :criticality
174
151
 
175
152
  def initialize(old_schema, new_schema)
176
153
  @old_schema = old_schema
177
154
  @new_schema = new_schema
178
- @breaking = true
155
+ @criticality = Changes::Criticality.breaking
179
156
  end
180
157
 
181
158
  def message
182
159
  "Schema query root has changed from `#{old_schema.query.name}` to `#{new_schema.query.name}`"
183
160
  end
184
-
185
- def breaking?
186
- !!@breaking
187
- end
188
161
  end
189
162
 
190
163
  class FieldRemoved < AbstractChange
191
- attr_reader :object_type, :field
164
+ attr_reader :object_type, :field, :criticality
192
165
 
193
166
  def initialize(object_type, field)
194
167
  @object_type = object_type
195
168
  @field = field
196
- @breaking = true
169
+ @criticality = Changes::Criticality.breaking
197
170
  end
198
171
 
199
172
  def message
200
173
  "Field `#{field.name}` was removed from object type `#{object_type.name}`"
201
174
  end
202
-
203
- def breaking?
204
- !!@breaking
205
- end
206
175
  end
207
176
 
208
177
  class DirectiveLocationRemoved < AbstractChange
209
- attr_reader :directive, :location
178
+ attr_reader :directive, :location, :criticality
210
179
 
211
180
  def initialize(directive, location)
212
181
  @directive = directive
213
182
  @location = location
214
- @breaking = true
183
+ @criticality = Changes::Criticality.breaking
215
184
  end
216
185
 
217
186
  def message
218
187
  "Location `#{location}` was removed from directive `#{directive.name}`"
219
188
  end
220
-
221
- def breaking?
222
- !!@breaking
223
- end
224
189
  end
225
190
 
226
191
  class ObjectTypeInterfaceRemoved < AbstractChange
227
- attr_reader :interface, :object_type
192
+ attr_reader :interface, :object_type, :criticality
228
193
 
229
194
  def initialize(interface, object_type)
230
195
  @interface = interface
231
196
  @object_type = object_type
232
- @breaking = true
197
+ @criticality = Changes::Criticality.breaking
233
198
  end
234
199
 
235
200
  def message
236
201
  "`#{object_type.name}` object type no longer implements `#{interface.name}` interface"
237
202
  end
203
+ end
238
204
 
239
- def breaking?
240
- !!@breaking
205
+ class FieldTypeChanged < AbstractChange
206
+ include SafeTypeChange
207
+
208
+ attr_reader :type, :old_field, :new_field
209
+
210
+ def initialize(type, old_field, new_field)
211
+ @type = type
212
+ @old_field = old_field
213
+ @new_field = new_field
214
+ end
215
+
216
+ def message
217
+ "Field `#{type}.#{old_field.name}` changed type from `#{old_field.type}` to `#{new_field.type}`"
218
+ end
219
+
220
+ def criticality
221
+ if safe_change_for_field?(old_field.type, new_field.type)
222
+ Changes::Criticality.non_breaking
223
+ else
224
+ Changes::Criticality.breaking
225
+ end
241
226
  end
242
227
  end
243
228
 
244
- class TypeAdded < AbstractChange
245
- attr_reader :type
229
+ class InputFieldTypeChanged < AbstractChange
230
+ include SafeTypeChange
246
231
 
247
- def initialize(type)
248
- @type = type
249
- @breaking = false
232
+ attr_reader :input_type, :old_input_field, :new_input_field, :criticality
233
+
234
+ def initialize(input_type, old_input_field, new_input_field)
235
+ if safe_change_for_input_value?(old_input_field.type, new_input_field.type)
236
+ @criticality = Changes::Criticality.non_breaking(
237
+ reason: "Changing an input field from non-null to null is considered non-breaking"
238
+ )
239
+ else
240
+ @criticality = Changes::Criticality.breaking
241
+ end
242
+
243
+ @input_type = input_type
244
+ @old_input_field = old_input_field
245
+ @new_input_field = new_input_field
250
246
  end
251
247
 
252
248
  def message
253
- "Type `#{type.name}` was added"
249
+ "Input field `#{input_type}.#{old_input_field.name}` changed type from `#{old_input_field.type}` to `#{new_input_field.type}`"
254
250
  end
251
+ end
255
252
 
256
- def breaking?
257
- !!@breaking
253
+ class FieldArgumentTypeChanged < AbstractChange
254
+ include SafeTypeChange
255
+
256
+ attr_reader :type, :field, :old_argument, :new_argument, :criticality
257
+
258
+ def initialize(type, field, old_argument, new_argument)
259
+ if safe_change_for_input_value?(old_argument.type, new_argument.type)
260
+ @criticality = Changes::Criticality.non_breaking(
261
+ reason: "Changing an input field from non-null to null is considered non-breaking"
262
+ )
263
+ else
264
+ @criticality = Changes::Criticality.breaking
265
+ end
266
+
267
+ @type = type
268
+ @field = field
269
+ @old_argument = old_argument
270
+ @new_argument = new_argument
271
+ end
272
+
273
+ def message
274
+ "Type for argument `#{new_argument.name}` on field `#{type.name}.#{field.name}` changed"\
275
+ " from `#{old_argument.type}` to `#{new_argument.type}`"
258
276
  end
259
277
  end
260
278
 
261
- class DirectiveAdded < AbstractChange
262
- attr_reader :directive
279
+ class DirectiveArgumentTypeChanged < AbstractChange
280
+ include SafeTypeChange
281
+
282
+ attr_reader :directive, :old_argument, :new_argument, :criticality
283
+
284
+ def initialize(directive, old_argument, new_argument)
285
+ if safe_change_for_input_value?(old_argument.type, new_argument.type)
286
+ @criticality = Changes::Criticality.non_breaking(
287
+ reason: "Changing an input field from non-null to null is considered non-breaking"
288
+ )
289
+ else
290
+ @criticality = Changes::Criticality.breaking
291
+ end
263
292
 
264
- def initialize(directive)
265
293
  @directive = directive
266
- @breaking = false
294
+ @old_argument = old_argument
295
+ @new_argument = new_argument
267
296
  end
268
297
 
269
298
  def message
270
- "Directive `#{directive.name}` was added"
299
+ "Type for argument `#{new_argument.name}` on directive `#{directive.name}` changed"\
300
+ " from `#{old_argument.type}` to `#{new_argument.type}`"
271
301
  end
302
+ end
272
303
 
273
- def breaking?
274
- !!@breaking
304
+ class SchemaMutationTypeChanged < AbstractChange
305
+ attr_reader :old_schema, :new_schema, :criticality
306
+
307
+ def initialize(old_schema, new_schema)
308
+ @old_schema = old_schema
309
+ @new_schema = new_schema
310
+ @criticality = Changes::Criticality.breaking
311
+ end
312
+
313
+ def message
314
+ "Schema mutation root has changed from `#{old_schema.mutation}` to `#{new_schema.mutation}`"
275
315
  end
276
316
  end
277
317
 
278
- class TypeDescriptionChanged < AbstractChange
279
- attr_reader :old_type, :new_type
318
+ class SchemaSubscriptionTypeChanged < AbstractChange
319
+ attr_reader :old_schema, :new_schema, :criticality
280
320
 
281
- def initialize(old_type, new_type)
282
- @old_type = old_type
283
- @new_type = new_type
284
- @breaking = false
321
+ def initialize(old_schema, new_schema)
322
+ @old_schema = old_schema
323
+ @new_schema = new_schema
324
+ @criticality = Changes::Criticality.breaking
285
325
  end
286
326
 
287
327
  def message
288
- "Description `#{old_type.description}` on type `#{old_type.name}` has changed to `#{new_type.description}`"
328
+ "Schema subscription type has changed from `#{old_schema.subscription}` to `#{new_schema.subscription}`"
289
329
  end
330
+ end
290
331
 
291
- def breaking?
292
- !!@breaking
332
+ # Dangerous Changes
333
+
334
+ class FieldArgumentDefaultChanged < AbstractChange
335
+ attr_reader :type, :field, :old_argument, :new_argument, :criticality
336
+
337
+ def initialize(type, field, old_argument, new_argument)
338
+ @type = type
339
+ @field = field
340
+ @old_argument = old_argument
341
+ @new_argument = new_argument
342
+ @criticality = Changes::Criticality.dangerous(
343
+ reason: "Changing the default value for an argument may change the runtime " \
344
+ "behaviour of a field if it was never provided."
345
+ )
346
+ end
347
+
348
+ def message
349
+ "Default value for argument `#{new_argument.name}` on field `#{type.name}.#{field.name}` changed"\
350
+ " from `#{old_argument.default_value}` to `#{new_argument.default_value}`"
351
+ end
352
+ end
353
+
354
+ class InputFieldDefaultChanged < AbstractChange
355
+ attr_reader :input_type, :old_field, :new_field, :criticality
356
+
357
+ def initialize(input_type, old_field, new_field)
358
+ @criticality = Changes::Criticality.dangerous(
359
+ reason: "Changing the default value for an argument may change the runtime " \
360
+ "behaviour of a field if it was never provided."
361
+ )
362
+ @input_type = input_type
363
+ @old_field = old_field
364
+ @new_field = new_field
365
+ end
366
+
367
+ def message
368
+ "Input field `#{input_type.name}.#{old_field.name}` default changed"\
369
+ " from `#{old_field.default_value}` to `#{new_field.default_value}`"
370
+ end
371
+ end
372
+
373
+ class DirectiveArgumentDefaultChanged < AbstractChange
374
+ attr_reader :directive, :old_argument, :new_argument, :criticality
375
+
376
+ def initialize(directive, old_argument, new_argument)
377
+ @criticality = Changes::Criticality.dangerous(
378
+ reason: "Changing the default value for an argument may change the runtime " \
379
+ "behaviour of a field if it was never provided."
380
+ )
381
+ @directive = directive
382
+ @old_argument = old_argument
383
+ @new_argument = new_argument
384
+ end
385
+
386
+ def message
387
+ "Default value for argument `#{new_argument.name}` on directive `#{directive.name}` changed"\
388
+ " from `#{old_argument.default_value}` to `#{new_argument.default_value}`"
293
389
  end
294
390
  end
295
391
 
296
392
  class EnumValueAdded < AbstractChange
297
- attr_reader :enum_type, :enum_value
393
+ attr_reader :enum_type, :enum_value, :criticality
298
394
 
299
395
  def initialize(enum_type, enum_value)
300
396
  @enum_type = enum_type
301
397
  @enum_value = enum_value
302
- @breaking = false
398
+ @criticality = Changes::Criticality.dangerous(
399
+ reason: "Adding an enum value may break existing clients that were not " \
400
+ "programming defensively against an added case when querying an enum."
401
+ )
402
+ end
403
+
404
+ def message
405
+ "Enum value `#{enum_value.name}` was added to enum `#{enum_type.name}`"
406
+ end
407
+ end
408
+
409
+ class UnionMemberAdded < AbstractChange
410
+ attr_reader :union_type, :union_member, :criticality
411
+
412
+ def initialize(union_type, union_member)
413
+ @union_member = union_member
414
+ @union_type = union_type
415
+ @criticality = Changes::Criticality.dangerous(
416
+ reason: "Adding a possible type to Unions may break existing clients " \
417
+ "that were not programming defensively against a new possible type."
418
+ )
419
+ end
420
+
421
+ def message
422
+ "Union member `#{union_member.name}` was added to Union type `#{union_type.name}`"
423
+ end
424
+ end
425
+
426
+ class ObjectTypeInterfaceAdded < AbstractChange
427
+ attr_reader :interface, :object_type, :criticality
428
+
429
+ def initialize(interface, object_type)
430
+ @criticality = Changes::Criticality.dangerous(
431
+ reason: "Adding an interface to an object type may break existing clients " \
432
+ "that were not programming defensively against a new possible type."
433
+ )
434
+ @interface = interface
435
+ @object_type = object_type
436
+ end
437
+
438
+ def message
439
+ "`#{object_type.name}` object implements `#{interface.name}` interface"
440
+ end
441
+ end
442
+
443
+ # Mostly Non-Breaking Changes
444
+
445
+ class InputFieldAdded < AbstractChange
446
+ attr_reader :input_object_type, :field, :criticality
447
+
448
+ def initialize(input_object_type, field)
449
+ @criticality = if field.type.non_null?
450
+ Changes::Criticality.breaking
451
+ else
452
+ Changes::Criticality.non_breaking
453
+ end
454
+
455
+ @input_object_type = input_object_type
456
+ @field = field
457
+ end
458
+
459
+ def message
460
+ "Input field `#{field.name}` was added to input object type `#{input_object_type.name}`"
461
+ end
462
+ end
463
+
464
+ class FieldArgumentAdded < AbstractChange
465
+ attr_reader :type, :field, :argument, :criticality
466
+
467
+ def initialize(type, field, argument)
468
+ @criticality = if argument.type.non_null?
469
+ Changes::Criticality.breaking
470
+ else
471
+ Changes::Criticality.non_breaking
472
+ end
473
+
474
+ @type = type
475
+ @field = field
476
+ @argument = argument
477
+ end
478
+
479
+ def message
480
+ "Argument `#{argument.name}: #{argument.type}` added to field `#{type.name}.#{field.name}`"
481
+ end
482
+ end
483
+
484
+ class TypeAdded < AbstractChange
485
+ attr_reader :type, :criticality
486
+
487
+ def initialize(type)
488
+ @type = type
489
+ @criticality = Changes::Criticality.non_breaking
490
+ end
491
+
492
+ def message
493
+ "Type `#{type.name}` was added"
494
+ end
495
+ end
496
+
497
+ class DirectiveAdded < AbstractChange
498
+ attr_reader :directive, :criticality
499
+
500
+ def initialize(directive)
501
+ @directive = directive
502
+ @criticality = Changes::Criticality.non_breaking
503
+ end
504
+
505
+ def message
506
+ "Directive `#{directive.name}` was added"
507
+ end
508
+ end
509
+
510
+ class TypeDescriptionChanged < AbstractChange
511
+ attr_reader :old_type, :new_type, :criticality
512
+
513
+ def initialize(old_type, new_type)
514
+ @old_type = old_type
515
+ @new_type = new_type
516
+ @criticality = Changes::Criticality.non_breaking
303
517
  end
304
518
 
305
519
  def message
306
- "Enum value `#{enum_value.name}` was added to enum `#{enum_type.name}`"
307
- end
308
-
309
- def breaking?
310
- !!@breaking
520
+ "Description `#{old_type.description}` on type `#{old_type.name}` has changed to `#{new_type.description}`"
311
521
  end
312
522
  end
313
523
 
314
524
  class EnumValueDescriptionChanged < AbstractChange
315
- attr_reader :enum, :old_enum_value, :new_enum_value
525
+ attr_reader :enum, :old_enum_value, :new_enum_value, :criticality
316
526
 
317
527
  def initialize(enum, old_enum_value, new_enum_value)
318
528
  @enum = enum
319
529
  @old_enum_value = old_enum_value
320
530
  @new_enum_value = new_enum_value
531
+ @criticality = Changes::Criticality.non_breaking
321
532
  end
322
533
 
323
534
  def message
324
535
  "Description for enum value `#{enum.name}.#{new_enum_value.name}` changed from " \
325
536
  "`#{old_enum_value.description}` to `#{new_enum_value.description}`"
326
537
  end
327
-
328
- def breaking?
329
- false
330
- end
331
538
  end
332
539
 
333
540
  class EnumValueDeprecated < AbstractChange
334
- attr_reader :enum, :old_enum_value, :new_enum_value
541
+ attr_reader :enum, :old_enum_value, :new_enum_value, :criticality
335
542
 
336
543
  def initialize(enum, old_enum_value, new_enum_value)
544
+ @criticality = Changes::Criticality.non_breaking
337
545
  @enum = enum
338
546
  @old_enum_value = old_enum_value
339
547
  @new_enum_value = new_enum_value
@@ -348,263 +556,131 @@ module GraphQL
348
556
  " `#{new_enum_value.deprecation_reason}`"
349
557
  end
350
558
  end
351
-
352
- def breaking?
353
- false
354
- end
355
- end
356
-
357
- class UnionMemberAdded < AbstractChange
358
- attr_reader :union_type, :union_member
359
-
360
- def initialize(union_type, union_member)
361
- @union_member = union_member
362
- @union_type = union_type
363
- @breaking = false
364
- end
365
-
366
- def message
367
- "Union member `#{union_member.name}` was added to Union type `#{union_type.name}`"
368
- end
369
-
370
- def breaking?
371
- !!@breaking
372
- end
373
559
  end
374
560
 
375
561
  class InputFieldDescriptionChanged < AbstractChange
376
- attr_reader :input_type, :old_field, :new_field
562
+ attr_reader :input_type, :old_field, :new_field, :criticality
377
563
 
378
564
  def initialize(input_type, old_field, new_field)
565
+ @criticality = Changes::Criticality.non_breaking
379
566
  @input_type = input_type
380
567
  @old_field = old_field
381
568
  @new_field = new_field
382
- @breaking = false
383
569
  end
384
570
 
385
571
  def message
386
572
  "Input field `#{input_type.name}.#{old_field.name}` description changed"\
387
573
  " from `#{old_field.description}` to `#{new_field.description}`"
388
574
  end
389
-
390
- def breaking?
391
- !!@breaking
392
- end
393
575
  end
394
576
 
395
577
  class DirectiveDescriptionChanged < AbstractChange
396
- attr_reader :old_directive, :new_directive
578
+ attr_reader :old_directive, :new_directive, :criticality
397
579
 
398
580
  def initialize(old_directive, new_directive)
581
+ @criticality = Changes::Criticality.non_breaking
399
582
  @old_directive = old_directive
400
583
  @new_directive = new_directive
401
- @breaking = false
402
584
  end
403
585
 
404
586
  def message
405
587
  "Directive `#{new_directive.name}` description changed"\
406
588
  " from `#{old_directive.description}` to `#{new_directive.description}`"
407
589
  end
408
-
409
- def breaking?
410
- !!@breaking
411
- end
412
590
  end
413
591
 
414
592
  class FieldDescriptionChanged < AbstractChange
415
- attr_reader :type, :old_field, :new_field
593
+ attr_reader :type, :old_field, :new_field, :criticality
416
594
 
417
595
  def initialize(type, old_field, new_field)
596
+ @criticality = Changes::Criticality.non_breaking
418
597
  @type = type
419
598
  @old_field = old_field
420
599
  @new_field = new_field
421
- @breaking = false
422
600
  end
423
601
 
424
602
  def message
425
603
  "Field `#{type.name}.#{old_field.name}` description changed"\
426
604
  " from `#{old_field.description}` to `#{new_field.description}`"
427
605
  end
428
-
429
- def breaking?
430
- !!@breaking
431
- end
432
606
  end
433
607
 
434
608
  class FieldArgumentDescriptionChanged < AbstractChange
435
- attr_reader :type, :field, :old_argument, :new_argument
609
+ attr_reader :type, :field, :old_argument, :new_argument, :criticality
436
610
 
437
611
  def initialize(type, field, old_argument, new_argument)
612
+ @criticality = Changes::Criticality.non_breaking
438
613
  @type = type
439
614
  @field = field
440
615
  @old_argument = old_argument
441
616
  @new_argument = new_argument
442
- @breaking = false
443
617
  end
444
618
 
445
619
  def message
446
620
  "Description for argument `#{new_argument.name}` on field `#{type.name}.#{field.name}` changed"\
447
621
  " from `#{old_argument.description}` to `#{new_argument.description}`"
448
622
  end
449
-
450
- def breaking?
451
- !!@breaking
452
- end
453
623
  end
454
624
 
455
625
  class DirectiveArgumentDescriptionChanged < AbstractChange
456
- attr_reader :directive, :old_argument, :new_argument
626
+ attr_reader :directive, :old_argument, :new_argument, :criticality
457
627
 
458
628
  def initialize(directive, old_argument, new_argument)
629
+ @criticality = Changes::Criticality.non_breaking
459
630
  @directive = directive
460
631
  @old_argument = old_argument
461
632
  @new_argument = new_argument
462
- @breaking = false
463
633
  end
464
634
 
465
635
  def message
466
636
  "Description for argument `#{new_argument.name}` on directive `#{directive.name}` changed"\
467
637
  " from `#{old_argument.description}` to `#{new_argument.description}`"
468
638
  end
469
-
470
- def breaking?
471
- !!@breaking
472
- end
473
639
  end
474
640
 
475
641
  class FieldDeprecationChanged < AbstractChange
476
- attr_reader :type, :old_field, :new_field
642
+ attr_reader :type, :old_field, :new_field, :criticality
477
643
 
478
644
  def initialize(type, old_field, new_field)
645
+ @criticality = Changes::Criticality.non_breaking
479
646
  @type = type
480
647
  @old_field = old_field
481
648
  @new_field = new_field
482
- @breaking = false
483
649
  end
484
650
 
485
651
  def message
486
652
  "Deprecation reason on field `#{type.name}.#{new_field.name}` has changed "\
487
653
  "from `#{old_field.deprecation_reason}` to `#{new_field.deprecation_reason}`"
488
654
  end
489
-
490
- def breaking?
491
- !!@breaking
492
- end
493
- end
494
-
495
- class InputFieldDefaultChanged < AbstractChange
496
- attr_reader :input_type, :old_field, :new_field
497
-
498
- def initialize(input_type, old_field, new_field)
499
- @input_type = input_type
500
- @old_field = old_field
501
- @new_field = new_field
502
- @breaking = false
503
- end
504
-
505
- def message
506
- "Input field `#{input_type.name}.#{old_field.name}` default changed"\
507
- " from `#{old_field.default_value}` to `#{new_field.default_value}`"
508
- end
509
-
510
- def breaking?
511
- !!@breaking
512
- end
513
- end
514
-
515
- class FieldArgumentDefaultChanged < AbstractChange
516
- attr_reader :type, :field, :old_argument, :new_argument
517
-
518
- def initialize(type, field, old_argument, new_argument)
519
- @type = type
520
- @field = field
521
- @old_argument = old_argument
522
- @new_argument = new_argument
523
- @breaking = false
524
- end
525
-
526
- def message
527
- "Default value for argument `#{new_argument.name}` on field `#{type.name}.#{field.name}` changed"\
528
- " from `#{old_argument.default_value}` to `#{new_argument.default_value}`"
529
- end
530
-
531
- def breaking?
532
- !!@breaking
533
- end
534
- end
535
-
536
- class DirectiveArgumentDefaultChanged < AbstractChange
537
- attr_reader :directive, :old_argument, :new_argument
538
-
539
- def initialize(directive, old_argument, new_argument)
540
- @directive = directive
541
- @old_argument = old_argument
542
- @new_argument = new_argument
543
- @breaking = false
544
- end
545
-
546
- def message
547
- "Default value for argument `#{new_argument.name}` on directive `#{directive.name}` changed"\
548
- " from `#{old_argument.default_value}` to `#{new_argument.default_value}`"
549
- end
550
-
551
- def breaking?
552
- !!@breaking
553
- end
554
- end
555
-
556
- class ObjectTypeInterfaceAdded < AbstractChange
557
- attr_reader :interface, :object_type
558
-
559
- def initialize(interface, object_type)
560
- @interface = interface
561
- @object_type = object_type
562
- @breaking = false
563
- end
564
-
565
- def message
566
- "`#{object_type.name}` object implements `#{interface.name}` interface"
567
- end
568
-
569
- def breaking?
570
- !!@breaking
571
- end
572
655
  end
573
656
 
574
657
  class FieldAdded < AbstractChange
575
- attr_reader :object_type, :field
658
+ attr_reader :object_type, :field, :criticality
576
659
 
577
660
  def initialize(object_type, field)
661
+ @criticality = Changes::Criticality.non_breaking
578
662
  @object_type = object_type
579
663
  @field = field
580
- @breaking = false
581
664
  end
582
665
 
583
666
  def message
584
667
  "Field `#{field.name}` was added to object type `#{object_type.name}`"
585
668
  end
586
669
 
587
- def breaking?
588
- !!@breaking
589
- end
590
670
  end
591
671
 
592
672
  class DirectiveLocationAdded < AbstractChange
593
- attr_reader :directive, :location
673
+ attr_reader :directive, :location, :criticality
594
674
 
595
675
  def initialize(directive, location)
676
+ @criticality = Changes::Criticality.non_breaking
596
677
  @directive = directive
597
678
  @location = location
598
- @breaking = false
599
679
  end
600
680
 
601
681
  def message
602
682
  "Location `#{location}` was added to directive `#{directive.name}`"
603
683
  end
604
-
605
- def breaking?
606
- !!@breaking
607
- end
608
684
  end
609
685
 
610
686
  # TODO
@@ -751,177 +827,22 @@ module GraphQL
751
827
  end
752
828
  end
753
829
 
754
- class InputFieldAdded < AbstractChange
755
- attr_reader :input_object_type, :field
756
-
757
- def initialize(input_object_type, field)
758
- @input_object_type = input_object_type
759
- @field = field
760
- @breaking = field.type.kind.non_null? ? true : false
761
- end
762
-
763
- def message
764
- "Input field `#{field.name}` was added to input object type `#{input_object_type.name}`"
765
- end
766
-
767
- def breaking?
768
- !!@breaking
769
- end
770
- end
771
-
772
- class FieldArgumentAdded < AbstractChange
773
- attr_reader :type, :field, :argument
774
-
775
- def initialize(type, field, argument)
776
- @type = type
777
- @field = field
778
- @argument = argument
779
- # TODO: should at least have a warning that it may still be breaking
780
- @breaking = argument.type.kind.non_null? ? true : false
781
- end
782
-
783
- def message
784
- "Argument `#{argument.name}: #{argument.type}` added to field `#{type.name}.#{field.name}`"
785
- end
786
-
787
- def breaking?
788
- !!@breaking
789
- end
790
- end
791
-
792
830
  class DirectiveArgumentAdded < AbstractChange
793
- attr_reader :directive, :argument
831
+ attr_reader :directive, :argument, :criticality
794
832
 
795
833
  def initialize(directive, argument)
834
+ @criticality = if argument.type.non_null?
835
+ Changes::Criticality.breaking
836
+ else
837
+ Changes::Criticality.non_breaking
838
+ end
796
839
  @directive = directive
797
840
  @argument = argument
798
- @breaking = false
799
841
  end
800
842
 
801
843
  def message
802
844
  "Argument `#{argument.name}` was added to directive `#{directive.name}`"
803
845
  end
804
-
805
- def breaking?
806
- !!@breaking
807
- end
808
- end
809
-
810
- class InputFieldTypeChanged < AbstractChange
811
- include SafeTypeChange
812
-
813
- attr_reader :input_type, :old_input_field, :new_input_field
814
-
815
- def initialize(input_type, old_input_field, new_input_field)
816
- @input_type = input_type
817
- @old_input_field = old_input_field
818
- @new_input_field = new_input_field
819
- @breaking = !safe_change?(old_input_field.type, new_input_field.type)
820
- end
821
-
822
- def message
823
- "Input field `#{input_type}.#{old_input_field.name}` changed type from #{old_input_field.type} to #{new_input_field.type}"
824
- end
825
-
826
- def breaking?
827
- !!@breaking
828
- end
829
- end
830
-
831
- class FieldArgumentTypeChanged < AbstractChange
832
- include SafeTypeChange
833
-
834
- attr_reader :type, :field, :old_argument, :new_argument
835
-
836
- def initialize(type, field, old_argument, new_argument)
837
- @type = type
838
- @field = field
839
- @old_argument = old_argument
840
- @new_argument = new_argument
841
- @breaking = !safe_change?(old_argument.type, new_argument.type)
842
- end
843
-
844
- def message
845
- "Type for argument `#{new_argument.name}` on field `#{type.name}.#{field.name}` changed"\
846
- " from `#{old_argument.type}` to `#{new_argument.type}`"
847
- end
848
-
849
- def breaking?
850
- !!@breaking
851
- end
852
- end
853
-
854
- class DirectiveArgumentTypeChanged < AbstractChange
855
- attr_reader :directive, :old_argument, :new_argument
856
-
857
- def initialize(directive, old_argument, new_argument)
858
- @directive = directive
859
- @old_argument = old_argument
860
- @new_argument = new_argument
861
- @breaking = false
862
- end
863
-
864
- def message
865
- "Type for argument `#{new_argument.name}` on directive `#{directive.name}` changed"\
866
- " from `#{old_argument.type}` to `#{new_argument.type}`"
867
- end
868
-
869
- def breaking?
870
- !!@breaking
871
- end
872
- end
873
-
874
- class FieldTypeChanged < AbstractChange
875
- attr_reader :type, :old_field, :new_field
876
-
877
- def initialize(type, old_field, new_field)
878
- @type = type
879
- @old_field = old_field
880
- @new_field = new_field
881
- @breaking = true
882
- end
883
-
884
- def message
885
- "Field `#{type}.#{old_field.name}` changed type from `#{old_field.type}` to `#{new_field.type}`"
886
- end
887
-
888
- def breaking?
889
- !!@breaking
890
- end
891
- end
892
-
893
- class SchemaMutationTypeChanged < AbstractChange
894
- attr_reader :old_schema, :new_schema
895
-
896
- def initialize(old_schema, new_schema)
897
- @old_schema = old_schema
898
- @new_schema = new_schema
899
- @breaking = true
900
- end
901
-
902
- def message
903
- "Schema mutation root has changed from `#{old_schema.mutation}` to `#{new_schema.mutation}`"
904
- end
905
-
906
- def breaking?
907
- !!@breaking
908
- end
909
- end
910
-
911
- class SchemaSubscriptionTypeChanged < AbstractChange
912
- def initialize(old_schema, new_schema)
913
- @old_schema = old_schema
914
- @new_schema = new_schema
915
- @breaking = true
916
- end
917
-
918
- def message
919
- "Schema subscription type has changed from `#{old_schema.subscription}` to `#{new_schema.subscription}`"
920
- end
921
-
922
- def breaking?
923
- !!@breaking
924
- end
925
846
  end
926
847
  end
927
848
  end