graphql-schema_comparator 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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