graphql 1.12.3 → 1.12.8

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.

Potentially problematic release.


This version of graphql might be problematic. Click here for more details.

Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install_generator.rb +4 -1
  3. data/lib/generators/graphql/loader_generator.rb +1 -0
  4. data/lib/generators/graphql/mutation_generator.rb +1 -0
  5. data/lib/generators/graphql/relay.rb +55 -0
  6. data/lib/generators/graphql/relay_generator.rb +4 -46
  7. data/lib/generators/graphql/type_generator.rb +1 -0
  8. data/lib/graphql.rb +4 -2
  9. data/lib/graphql/backtrace/inspect_result.rb +0 -1
  10. data/lib/graphql/backtrace/table.rb +0 -1
  11. data/lib/graphql/backtrace/traced_error.rb +0 -1
  12. data/lib/graphql/backtrace/tracer.rb +4 -8
  13. data/lib/graphql/dataloader.rb +102 -92
  14. data/lib/graphql/dataloader/null_dataloader.rb +5 -5
  15. data/lib/graphql/dataloader/request.rb +1 -6
  16. data/lib/graphql/dataloader/request_all.rb +1 -4
  17. data/lib/graphql/dataloader/source.rb +20 -6
  18. data/lib/graphql/execution/errors.rb +109 -11
  19. data/lib/graphql/execution/interpreter.rb +2 -2
  20. data/lib/graphql/execution/interpreter/arguments_cache.rb +37 -14
  21. data/lib/graphql/execution/interpreter/resolve.rb +33 -25
  22. data/lib/graphql/execution/interpreter/runtime.rb +41 -78
  23. data/lib/graphql/execution/multiplex.rb +21 -22
  24. data/lib/graphql/introspection.rb +1 -1
  25. data/lib/graphql/introspection/directive_type.rb +7 -3
  26. data/lib/graphql/language.rb +1 -0
  27. data/lib/graphql/language/cache.rb +37 -0
  28. data/lib/graphql/language/parser.rb +15 -5
  29. data/lib/graphql/language/parser.y +15 -5
  30. data/lib/graphql/object_type.rb +0 -2
  31. data/lib/graphql/pagination/active_record_relation_connection.rb +7 -0
  32. data/lib/graphql/pagination/connection.rb +15 -1
  33. data/lib/graphql/pagination/connections.rb +1 -0
  34. data/lib/graphql/pagination/relation_connection.rb +12 -1
  35. data/lib/graphql/parse_error.rb +0 -1
  36. data/lib/graphql/query.rb +9 -5
  37. data/lib/graphql/query/arguments_cache.rb +0 -1
  38. data/lib/graphql/query/context.rb +1 -3
  39. data/lib/graphql/query/executor.rb +0 -1
  40. data/lib/graphql/query/null_context.rb +3 -2
  41. data/lib/graphql/query/validation_pipeline.rb +1 -1
  42. data/lib/graphql/query/variable_validation_error.rb +1 -1
  43. data/lib/graphql/railtie.rb +9 -1
  44. data/lib/graphql/relay/range_add.rb +10 -5
  45. data/lib/graphql/schema.rb +14 -27
  46. data/lib/graphql/schema/argument.rb +61 -0
  47. data/lib/graphql/schema/field.rb +10 -5
  48. data/lib/graphql/schema/field/connection_extension.rb +1 -0
  49. data/lib/graphql/schema/find_inherited_value.rb +3 -1
  50. data/lib/graphql/schema/input_object.rb +6 -2
  51. data/lib/graphql/schema/member/has_arguments.rb +43 -56
  52. data/lib/graphql/schema/member/has_fields.rb +1 -4
  53. data/lib/graphql/schema/member/instrumentation.rb +0 -1
  54. data/lib/graphql/schema/resolver.rb +28 -1
  55. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +3 -1
  56. data/lib/graphql/static_validation/rules/argument_literals_are_compatible_error.rb +6 -2
  57. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +2 -1
  58. data/lib/graphql/static_validation/rules/arguments_are_defined_error.rb +4 -2
  59. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +2 -2
  60. data/lib/graphql/subscriptions/broadcast_analyzer.rb +0 -3
  61. data/lib/graphql/subscriptions/event.rb +0 -1
  62. data/lib/graphql/subscriptions/instrumentation.rb +0 -1
  63. data/lib/graphql/subscriptions/serialize.rb +3 -1
  64. data/lib/graphql/tracing/active_support_notifications_tracing.rb +2 -1
  65. data/lib/graphql/types/relay/base_connection.rb +4 -0
  66. data/lib/graphql/types/relay/connection_behaviors.rb +38 -5
  67. data/lib/graphql/types/relay/edge_behaviors.rb +12 -1
  68. data/lib/graphql/version.rb +1 -1
  69. data/readme.md +1 -1
  70. metadata +8 -90
@@ -56,17 +56,18 @@ module GraphQL
56
56
  # Root .authorized? returned false.
57
57
  write_in_response(path, nil)
58
58
  else
59
- # Prepare this runtime state to be encapsulated in a Fiber
60
- @progress_path = path
61
- @progress_scoped_context = context.scoped_context
62
- @progress_object = object_proxy
63
- @progress_object_type = root_type
64
- @progress_index = nil
65
- @progress_is_eager_selection = root_op_type == "mutation"
66
- @progress_selections = gather_selections(object_proxy, root_type, root_operation.selections)
67
-
59
+ gathered_selections = gather_selections(object_proxy, root_type, root_operation.selections)
68
60
  # Make the first fiber which will begin execution
69
- enqueue_selections_fiber
61
+ @dataloader.append_job {
62
+ evaluate_selections(
63
+ path,
64
+ context.scoped_context,
65
+ object_proxy,
66
+ root_type,
67
+ root_op_type == "mutation",
68
+ gathered_selections,
69
+ )
70
+ }
70
71
  end
71
72
  delete_interpreter_context(:current_path)
72
73
  delete_interpreter_context(:current_field)
@@ -75,32 +76,6 @@ module GraphQL
75
76
  nil
76
77
  end
77
78
 
78
- # Use `@dataloader` to enqueue a fiber that will pick up from the current point.
79
- # @return [void]
80
- def enqueue_selections_fiber
81
- # Read these into local variables so that later assignments don't affect the block below.
82
- path = @progress_path
83
- scoped_context = @progress_scoped_context
84
- owner_object = @progress_object
85
- owner_type = @progress_object_type
86
- idx = @progress_index
87
- is_eager_selection = @progress_is_eager_selection
88
- gathered_selections = @progress_selections
89
-
90
- @dataloader.enqueue {
91
- evaluate_selections(
92
- path,
93
- scoped_context,
94
- owner_object,
95
- owner_type,
96
- is_eager_selection: is_eager_selection,
97
- after: idx,
98
- gathered_selections: gathered_selections,
99
- )
100
- }
101
- nil
102
- end
103
-
104
79
  def gather_selections(owner_object, owner_type, selections, selections_by_name = {})
105
80
  selections.each do |node|
106
81
  # Skip gathering this if the directive says so
@@ -159,39 +134,17 @@ module GraphQL
159
134
  NO_ARGS = {}.freeze
160
135
 
161
136
  # @return [void]
162
- def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection:, gathered_selections:, after:)
137
+ def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection, gathered_selections)
163
138
  set_all_interpreter_context(owner_object, nil, nil, path)
164
139
 
165
- @progress_path = path
166
- @progress_scoped_context = scoped_context
167
- @progress_object = owner_object
168
- @progress_object_type = owner_type
169
- @progress_index = nil
170
- @progress_is_eager_selection = is_eager_selection
171
- @progress_selections = gathered_selections
172
-
173
- # Track `idx` manually to avoid an allocation on this hot path
174
- idx = 0
175
140
  gathered_selections.each do |result_name, field_ast_nodes_or_ast_node|
176
- prev_idx = idx
177
- idx += 1
178
- # TODO: this is how a `progress` resumes where this left off.
179
- # Is there a better way to seek in the hash?
180
- # I think we could also use the array of keys; it supports seeking just fine.
181
- if after && prev_idx <= after
182
- next
183
- end
184
- @progress_index = prev_idx
185
- # This is how the current runtime gives itself to `dataloader`
186
- # so that the dataloader can enqueue another fiber to resume if needed.
187
- @dataloader.current_runtime = self
188
- evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_selection)
189
- # The dataloader knows if ^^ that selection halted and later selections were executed in another fiber.
190
- # If that's the case, then don't continue execution here.
191
- if @dataloader.yielded?(path)
192
- break
193
- end
141
+ @dataloader.append_job {
142
+ evaluate_selection(
143
+ path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_selection
144
+ )
145
+ }
194
146
  end
147
+
195
148
  nil
196
149
  end
197
150
 
@@ -243,13 +196,21 @@ module GraphQL
243
196
  object = authorized_new(field_defn.owner, object, context, next_path)
244
197
  end
245
198
 
246
- begin
247
- kwarg_arguments = arguments(object, field_defn, ast_node)
248
- rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => e
249
- continue_value(next_path, e, owner_type, field_defn, return_type.non_null?, ast_node)
250
- return
199
+ total_args_count = field_defn.arguments.size
200
+ if total_args_count == 0
201
+ kwarg_arguments = GraphQL::Execution::Interpreter::Arguments::EMPTY
202
+ evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field)
203
+ else
204
+ # TODO remove all arguments(...) usages?
205
+ @query.arguments_cache.dataload_for(ast_node, field_defn, object) do |resolved_arguments|
206
+ evaluate_selection_with_args(resolved_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field)
207
+ end
251
208
  end
209
+ end
252
210
 
211
+ def evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field) # rubocop:disable Metrics/ParameterLists
212
+ context.scoped_context = scoped_context
213
+ return_type = field_defn.type
253
214
  after_lazy(kwarg_arguments, owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments) do |resolved_arguments|
254
215
  if resolved_arguments.is_a?(GraphQL::ExecutionError) || resolved_arguments.is_a?(GraphQL::UnauthorizedError)
255
216
  continue_value(next_path, resolved_arguments, owner_type, field_defn, return_type.non_null?, ast_node)
@@ -316,10 +277,7 @@ module GraphQL
316
277
  end
317
278
  after_lazy(app_result, owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments) do |inner_result|
318
279
  continue_value = continue_value(next_path, inner_result, owner_type, field_defn, return_type.non_null?, ast_node)
319
- if RawValue === continue_value
320
- # Write raw value directly to the response without resolving nested objects
321
- write_in_response(next_path, continue_value.resolve)
322
- elsif HALT != continue_value
280
+ if HALT != continue_value
323
281
  continue_field(next_path, continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, kwarg_arguments)
324
282
  end
325
283
  end
@@ -329,10 +287,11 @@ module GraphQL
329
287
  # all of its child fields before moving on to the next root mutation field.
330
288
  # (Subselections of this mutation will still be resolved level-by-level.)
331
289
  if is_eager_field
332
- Interpreter::Resolve.resolve_all([field_result])
290
+ Interpreter::Resolve.resolve_all([field_result], @dataloader)
291
+ else
292
+ # Return this from `after_lazy` because it might be another lazy that needs to be resolved
293
+ field_result
333
294
  end
334
-
335
- nil
336
295
  end
337
296
  end
338
297
 
@@ -370,6 +329,10 @@ module GraphQL
370
329
  continue_value(path, next_value, parent_type, field, is_non_null, ast_node)
371
330
  elsif GraphQL::Execution::Execute::SKIP == value
372
331
  HALT
332
+ elsif value.is_a?(GraphQL::Execution::Interpreter::RawValue)
333
+ # Write raw value directly to the response without resolving nested objects
334
+ write_in_response(path, value.resolve)
335
+ HALT
373
336
  else
374
337
  value
375
338
  end
@@ -419,7 +382,7 @@ module GraphQL
419
382
  response_hash = {}
420
383
  write_in_response(path, response_hash)
421
384
  gathered_selections = gather_selections(continue_value, current_type, next_selections)
422
- evaluate_selections(path, context.scoped_context, continue_value, current_type, is_eager_selection: false, gathered_selections: gathered_selections, after: nil)
385
+ evaluate_selections(path, context.scoped_context, continue_value, current_type, false, gathered_selections)
423
386
  response_hash
424
387
  end
425
388
  end
@@ -35,7 +35,7 @@ module GraphQL
35
35
  @queries = queries
36
36
  @queries.each { |q| q.multiplex = self }
37
37
  @context = context
38
- @context[:dataloader] = @dataloader = @schema.dataloader_class.new(context)
38
+ @context[:dataloader] = @dataloader = @schema.dataloader_class.new
39
39
  @tracers = schema.tracers + (context[:tracers] || [])
40
40
  # Support `context: {backtrace: true}`
41
41
  if context[:backtrace] && !@tracers.include?(GraphQL::Backtrace::Tracer)
@@ -74,6 +74,24 @@ module GraphQL
74
74
  end
75
75
  end
76
76
 
77
+ # @param query [GraphQL::Query]
78
+ def begin_query(results, idx, query, multiplex)
79
+ operation = query.selected_operation
80
+ result = if operation.nil? || !query.valid? || query.context.errors.any?
81
+ NO_OPERATION
82
+ else
83
+ begin
84
+ # These were checked to be the same in `#supports_multiplexing?`
85
+ query.schema.query_execution_strategy.begin_query(query, multiplex)
86
+ rescue GraphQL::ExecutionError => err
87
+ query.context.errors << err
88
+ NO_OPERATION
89
+ end
90
+ end
91
+ results[idx] = result
92
+ nil
93
+ end
94
+
77
95
  private
78
96
 
79
97
  def run_as_multiplex(multiplex)
@@ -83,15 +101,13 @@ module GraphQL
83
101
  # Do as much eager evaluation of the query as possible
84
102
  results = []
85
103
  queries.each_with_index do |query, idx|
86
- multiplex.dataloader.enqueue {
87
- results[idx] = begin_query(query, multiplex)
88
- }
104
+ multiplex.dataloader.append_job { begin_query(results, idx, query, multiplex) }
89
105
  end
90
106
 
91
107
  multiplex.dataloader.run
92
108
 
93
109
  # Then, work through lazy results in a breadth-first way
94
- multiplex.dataloader.enqueue {
110
+ multiplex.dataloader.append_job {
95
111
  multiplex.schema.query_execution_strategy.finish_multiplex(results, multiplex)
96
112
  }
97
113
  multiplex.dataloader.run
@@ -112,23 +128,6 @@ module GraphQL
112
128
  raise
113
129
  end
114
130
 
115
- # @param query [GraphQL::Query]
116
- # @return [Hash] The initial result (may not be finished if there are lazy values)
117
- def begin_query(query, multiplex)
118
- operation = query.selected_operation
119
- if operation.nil? || !query.valid? || query.context.errors.any?
120
- NO_OPERATION
121
- else
122
- begin
123
- # These were checked to be the same in `#supports_multiplexing?`
124
- query.schema.query_execution_strategy.begin_query(query, multiplex)
125
- rescue GraphQL::ExecutionError => err
126
- query.context.errors << err
127
- NO_OPERATION
128
- end
129
- end
130
- end
131
-
132
131
  # @param data_result [Hash] The result for the "data" key, if any
133
132
  # @param query [GraphQL::Query] The query which was run
134
133
  # @return [Hash] final result of this query, including all values and errors
@@ -17,7 +17,7 @@ query IntrospectionQuery {
17
17
  name
18
18
  description
19
19
  locations
20
- args {
20
+ args#{include_deprecated_args ? '(includeDeprecated: true)' : ''} {
21
21
  ...InputValue
22
22
  }
23
23
  }
@@ -12,13 +12,17 @@ module GraphQL
12
12
  field :name, String, null: false, method: :graphql_name
13
13
  field :description, String, null: true
14
14
  field :locations, [GraphQL::Schema::LateBoundType.new("__DirectiveLocation")], null: false
15
- field :args, [GraphQL::Schema::LateBoundType.new("__InputValue")], null: false
15
+ field :args, [GraphQL::Schema::LateBoundType.new("__InputValue")], null: false do
16
+ argument :include_deprecated, Boolean, required: false, default_value: false
17
+ end
16
18
  field :on_operation, Boolean, null: false, deprecation_reason: "Use `locations`.", method: :on_operation?
17
19
  field :on_fragment, Boolean, null: false, deprecation_reason: "Use `locations`.", method: :on_fragment?
18
20
  field :on_field, Boolean, null: false, deprecation_reason: "Use `locations`.", method: :on_field?
19
21
 
20
- def args
21
- @context.warden.arguments(@object)
22
+ def args(include_deprecated:)
23
+ args = @context.warden.arguments(@object)
24
+ args = args.reject(&:deprecation_reason) unless include_deprecated
25
+ args
22
26
  end
23
27
  end
24
28
  end
@@ -6,6 +6,7 @@ require "graphql/language/document_from_schema_definition"
6
6
  require "graphql/language/generation"
7
7
  require "graphql/language/lexer"
8
8
  require "graphql/language/nodes"
9
+ require "graphql/language/cache"
9
10
  require "graphql/language/parser"
10
11
  require "graphql/language/token"
11
12
  require "graphql/language/visitor"
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql/version'
4
+ require 'digest/sha2'
5
+
6
+ module GraphQL
7
+ module Language
8
+ class Cache
9
+ def initialize(path)
10
+ @path = path
11
+ end
12
+
13
+ DIGEST = Digest::SHA256.new << GraphQL::VERSION
14
+ def fetch(filename)
15
+ hash = DIGEST.dup << filename
16
+ begin
17
+ hash << File.mtime(filename).to_i.to_s
18
+ rescue SystemCallError
19
+ return yield
20
+ end
21
+ cache_path = @path.join(hash.to_s)
22
+
23
+ if cache_path.exist?
24
+ Marshal.load(cache_path.read)
25
+ else
26
+ payload = yield
27
+ tmp_path = "#{cache_path}.#{rand}"
28
+
29
+ @path.mkpath
30
+ File.binwrite(tmp_path, Marshal.dump(payload))
31
+ File.rename(tmp_path, cache_path.to_s)
32
+ payload
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -41,12 +41,22 @@ def parse_document
41
41
  end
42
42
  end
43
43
 
44
- def self.parse(query_string, filename: nil, tracer: GraphQL::Tracing::NullTracer)
45
- self.new(query_string, filename: filename, tracer: tracer).parse_document
46
- end
44
+ class << self
45
+ attr_accessor :cache
46
+
47
+ def parse(query_string, filename: nil, tracer: GraphQL::Tracing::NullTracer)
48
+ new(query_string, filename: filename, tracer: tracer).parse_document
49
+ end
47
50
 
48
- def self.parse_file(filename, tracer: GraphQL::Tracing::NullTracer)
49
- self.parse(File.read(filename), filename: filename, tracer: tracer)
51
+ def parse_file(filename, tracer: GraphQL::Tracing::NullTracer)
52
+ if cache
53
+ cache.fetch(filename) do
54
+ parse(File.read(filename), filename: filename, tracer: tracer)
55
+ end
56
+ else
57
+ parse(File.read(filename), filename: filename, tracer: tracer)
58
+ end
59
+ end
50
60
  end
51
61
 
52
62
  private
@@ -462,12 +462,22 @@ def parse_document
462
462
  end
463
463
  end
464
464
 
465
- def self.parse(query_string, filename: nil, tracer: GraphQL::Tracing::NullTracer)
466
- self.new(query_string, filename: filename, tracer: tracer).parse_document
467
- end
465
+ class << self
466
+ attr_accessor :cache
467
+
468
+ def parse(query_string, filename: nil, tracer: GraphQL::Tracing::NullTracer)
469
+ new(query_string, filename: filename, tracer: tracer).parse_document
470
+ end
468
471
 
469
- def self.parse_file(filename, tracer: GraphQL::Tracing::NullTracer)
470
- self.parse(File.read(filename), filename: filename, tracer: tracer)
472
+ def parse_file(filename, tracer: GraphQL::Tracing::NullTracer)
473
+ if cache
474
+ cache.fetch(filename) do
475
+ parse(File.read(filename), filename: filename, tracer: tracer)
476
+ end
477
+ else
478
+ parse(File.read(filename), filename: filename, tracer: tracer)
479
+ end
480
+ end
471
481
  end
472
482
 
473
483
  private
@@ -121,8 +121,6 @@ module GraphQL
121
121
  iface = GraphQL::BaseType.resolve_related_type(type_membership.abstract_type)
122
122
  if iface.is_a?(GraphQL::InterfaceType)
123
123
  @clean_inherited_fields.merge!(iface.fields)
124
- else
125
- pp iface
126
124
  end
127
125
  end
128
126
  @clean_inherited_fields
@@ -5,6 +5,13 @@ module GraphQL
5
5
  module Pagination
6
6
  # Customizes `RelationConnection` to work with `ActiveRecord::Relation`s.
7
7
  class ActiveRecordRelationConnection < Pagination::RelationConnection
8
+ private
9
+
10
+ def relation_larger_than(relation, size)
11
+ initial_offset = relation.offset_value || 0
12
+ relation.offset(initial_offset + size).exists?
13
+ end
14
+
8
15
  def relation_count(relation)
9
16
  int_or_hash = if relation.respond_to?(:unscope)
10
17
  relation.unscope(:order).count(:all)
@@ -45,6 +45,9 @@ module GraphQL
45
45
  end
46
46
  end
47
47
 
48
+ # @return [Hash<Symbol => Object>] The field arguments from the field that returned this connection
49
+ attr_accessor :arguments
50
+
48
51
  # @param items [Object] some unpaginated collection item, like an `Array` or `ActiveRecord::Relation`
49
52
  # @param context [Query::Context]
50
53
  # @param parent [Object] The object this collection belongs to
@@ -52,8 +55,9 @@ module GraphQL
52
55
  # @param after [String, nil] A cursor for pagination, if the client provided one
53
56
  # @param last [Integer, nil] Limit parameter from the client, if provided
54
57
  # @param before [String, nil] A cursor for pagination, if the client provided one.
58
+ # @param arguments [Hash] The arguments to the field that returned the collection wrapped by this connection
55
59
  # @param max_page_size [Integer, nil] A configured value to cap the result size. Applied as `first` if neither first or last are given.
56
- def initialize(items, parent: nil, field: nil, context: nil, first: nil, after: nil, max_page_size: :not_given, last: nil, before: nil, edge_class: nil)
60
+ def initialize(items, parent: nil, field: nil, context: nil, first: nil, after: nil, max_page_size: :not_given, last: nil, before: nil, edge_class: nil, arguments: nil)
57
61
  @items = items
58
62
  @parent = parent
59
63
  @context = context
@@ -62,6 +66,7 @@ module GraphQL
62
66
  @after_value = after
63
67
  @last_value = last
64
68
  @before_value = before
69
+ @arguments = arguments
65
70
  @edge_class = edge_class || self.class::Edge
66
71
  # This is only true if the object was _initialized_ with an override
67
72
  # or if one is assigned later.
@@ -105,6 +110,15 @@ module GraphQL
105
110
  end
106
111
  end
107
112
 
113
+ # This is called by `Relay::RangeAdd` -- it can be overridden
114
+ # when `item` needs some modifications based on this connection's state.
115
+ #
116
+ # @param item [Object] An item newly added to `items`
117
+ # @return [Edge]
118
+ def range_add_edge(item)
119
+ edge_class.new(item, self)
120
+ end
121
+
108
122
  attr_writer :last
109
123
  # @return [Integer, nil] A clamped `last` value. (The underlying instance variable doesn't have limits on it)
110
124
  def last