ecoportal-api-v2 1.1.7 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.markdownlint.json +4 -0
  3. data/.rubocop.yml +54 -15
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +485 -373
  6. data/ecoportal-api-v2.gemspec +13 -12
  7. data/lib/ecoportal/api/common/concerns/benchmarkable.rb +47 -34
  8. data/lib/ecoportal/api/common/concerns/threadable.rb +41 -0
  9. data/lib/ecoportal/api/common/concerns.rb +1 -0
  10. data/lib/ecoportal/api/common/content/array_model.rb +85 -79
  11. data/lib/ecoportal/api/common/content/class_helpers.rb +34 -31
  12. data/lib/ecoportal/api/common/content/collection_model.rb +77 -65
  13. data/lib/ecoportal/api/common/content/double_model.rb +105 -87
  14. data/lib/ecoportal/api/common/content/wrapped_response.rb +11 -11
  15. data/lib/ecoportal/api/v2/page/component/reference_field.rb +17 -13
  16. data/lib/ecoportal/api/v2/page/component.rb +67 -68
  17. data/lib/ecoportal/api/v2/page/components.rb +9 -9
  18. data/lib/ecoportal/api/v2/page/force.rb +6 -7
  19. data/lib/ecoportal/api/v2/page/stages.rb +5 -6
  20. data/lib/ecoportal/api/v2/page.rb +35 -33
  21. data/lib/ecoportal/api/v2/pages/page_stage.rb +22 -20
  22. data/lib/ecoportal/api/v2/pages.rb +18 -14
  23. data/lib/ecoportal/api/v2/people.rb +2 -3
  24. data/lib/ecoportal/api/v2/registers.rb +28 -13
  25. data/lib/ecoportal/api/v2/s3/data.rb +27 -0
  26. data/lib/ecoportal/api/v2/s3/files/batch_upload.rb +110 -0
  27. data/lib/ecoportal/api/v2/s3/files/poll.rb +82 -0
  28. data/lib/ecoportal/api/v2/s3/files/poll_status.rb +52 -0
  29. data/lib/ecoportal/api/v2/s3/files.rb +132 -0
  30. data/lib/ecoportal/api/v2/s3/upload.rb +154 -0
  31. data/lib/ecoportal/api/v2/s3.rb +66 -0
  32. data/lib/ecoportal/api/v2.rb +10 -3
  33. data/lib/ecoportal/api/v2_version.rb +1 -1
  34. metadata +53 -54
@@ -1,5 +1,4 @@
1
-
2
- lib = File.expand_path("../lib", __FILE__)
1
+ lib = File.expand_path('lib', __dir__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
3
  require "ecoportal/api/v2_version"
5
4
 
@@ -9,25 +8,27 @@ Gem::Specification.new do |spec|
9
8
  spec.authors = ["Oscar Segura"]
10
9
  spec.email = ["rien@ecoportal.co.nz", "oscar@ecoportal.co.nz", "bozydar@ecoportal.co.nz"]
11
10
 
12
- spec.summary = %q{A collection of helpers for interacting with the ecoPortal MS's V2 API}
11
+ spec.summary = "A collection of helpers for interacting with the ecoPortal MS's V2 API"
13
12
  spec.homepage = "https://www.ecoportal.com"
14
13
  spec.licenses = %w[MIT]
15
14
 
16
- spec.required_ruby_version = '>= 2.7.2'
15
+ spec.metadata['rubygems_mfa_required'] = 'true'
16
+
17
+ spec.required_ruby_version = '>= 3.2.2'
17
18
 
18
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
20
  f.match(%r{^(test|spec|features)/})
20
21
  end
21
22
  spec.bindir = "exe"
22
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
24
  spec.require_paths = ["lib"]
24
25
 
25
- spec.add_development_dependency "bundler", ">= 2.4.12", "< 3"
26
- spec.add_development_dependency "rspec", ">= 3.12.0", "< 4"
27
- spec.add_development_dependency "rake", ">= 13.0.3", "< 14"
28
- spec.add_development_dependency "yard", ">= 0.9.34", "< 1"
29
- spec.add_development_dependency "redcarpet", ">= 3.6.0", "< 4"
30
- spec.add_development_dependency "pry" , ">= 0.14"
26
+ spec.add_development_dependency "pry" , '>= 0.14'
27
+ spec.add_development_dependency "rake", '>= 13.0.3', "< 14"
28
+ spec.add_development_dependency "redcarpet", '>= 3.6.0', "< 4"
29
+ spec.add_development_dependency "rspec", '>= 3.12.0', "< 4"
30
+ spec.add_development_dependency "yard", '~> 0.9.34'
31
31
 
32
- spec.add_dependency 'ecoportal-api', '>= 0.9.6', '< 0.10'
32
+ spec.add_dependency 'ecoportal-api', '~> 0.10'
33
+ spec.add_dependency 'mime-types', '~> 3.5', '>= 3.5.2'
33
34
  end
@@ -14,59 +14,63 @@ module Ecoportal
14
14
  @benchmark_enabled
15
15
  end
16
16
 
17
- def benchmarking(ref = nil, print: false)
17
+ def benchmarking(ref = nil, units: 1, print: false, &block)
18
18
  return yield unless benchmark_enabled?
19
- benchmark_mem(ref, print: false) do
20
- benchmark_time(ref, print: false) do
21
- yield
22
- end
19
+ benchmark_mem(ref, units: units, print: false) do
20
+ benchmark_time(ref, units: units, print: false, &block)
23
21
  end.tap do
24
22
  puts "\n#{bench_summary_ref(ref)}" if print
25
23
  end
26
24
  end
27
25
 
28
- def benchmark_mem(ref = nil, print: false)
26
+ def benchmark_mem(ref = nil, units: 1, print: false)
29
27
  return yield unless benchmark_enabled?
28
+
30
29
  memory_before = memory_usage
31
30
  yield.tap do
32
31
  memory_after = memory_usage
33
32
  mb_footprint = ((memory_after - memory_before) / 1024.0).round(2)
34
- bench_add_mem(ref, mb_footprint)
33
+ bench_add_mem(ref, mb_footprint, units: units)
35
34
  puts bench_str_mem(mb, ref: ref, reffix: true) if print
36
35
  end
37
36
  end
38
37
 
39
- def benchmark_time(ref = nil, print: false)
38
+ def benchmark_time(ref = nil, units: 1, print: false)
40
39
  return yield unless benchmark_enabled?
40
+
41
41
  result = nil
42
42
  time = benchmark.realtime do
43
43
  result = yield
44
44
  end.round(2)
45
+
45
46
  result.tap do
46
- bench_add_time(ref, time)
47
+ bench_add_time(ref, time, units: units)
47
48
  puts bench_str_time(time, ref: ref, reffix: true) if print
48
49
  end
49
50
  end
50
51
 
51
52
  def benchmark_summary(ref = nil)
52
- return '' unless benchmark_enabled?
53
+ return '' unless benchmark_enabled?
53
54
  return bench_summary_ref(ref) unless ref == :all
54
55
 
55
- bench.keys.each_with_object([]) do |ref, out|
56
- out << bench_summary_ref(ref)
56
+ bench.keys.each_with_object([]) do |rf, out|
57
+ out << bench_summary_ref(rf)
57
58
  end.join("\n")
58
59
  end
59
60
 
60
61
  def bench_summary_ref(ref = nil)
61
62
  ref_lines = bench_summary_ref_lines(ref)
62
- "- #{ref}\n" + " • " + ref_lines.join("\n • ")
63
+ msg = "- #{ref}\n"
64
+ msg << " • "
65
+ msg << ref_lines.join("\n • ")
66
+ msg
63
67
  end
64
68
 
65
69
  def bench_summary_ref_lines(ref = nil)
66
70
  ref_bench = bench_get(ref)
67
71
  [
68
- bench_str_time(*ref_bench[:time].values_at(:avg, :cnt), ref: ref),
69
- bench_str_mem(*ref_bench[:mem].values_at(:avg, :cnt), ref: ref)
72
+ bench_str_time(*ref_bench[:time].values_at(:avg, :cnt, :last), ref: ref),
73
+ bench_str_mem(*ref_bench[:mem].values_at(:avg, :cnt, :last), ref: ref)
70
74
  ]
71
75
  end
72
76
 
@@ -79,12 +83,12 @@ module Ecoportal
79
83
  @bench ||= {}
80
84
  end
81
85
 
82
- def bench_add_mem(ref, mem)
83
- bench_data_push(bench_get(ref)[:mem], mem)
86
+ def bench_add_mem(ref, mem, units: 1)
87
+ bench_data_push(bench_get(ref)[:mem], mem, units: units)
84
88
  end
85
89
 
86
- def bench_add_time(ref, time)
87
- bench_data_push(bench_get(ref)[:time], time)
90
+ def bench_add_time(ref, time, units: 1)
91
+ bench_data_push(bench_get(ref)[:time], time, units: units)
88
92
  end
89
93
 
90
94
  def bench_get(ref)
@@ -96,50 +100,58 @@ module Ecoportal
96
100
 
97
101
  def bench_data
98
102
  {
99
- avg: nil,
100
- cnt: 0
103
+ avg: nil,
104
+ cnt: 0,
105
+ last: nil
101
106
  }
102
107
  end
103
108
 
104
- def bench_data_push(data, value)
105
- total = value + ( (data[:avg] || 0) * data[:cnt])
106
- data[:cnt] += 1
109
+ def bench_data_push(data, value, units: 1)
110
+ total = value + ((data[:avg] || 0) * data[:cnt])
111
+ data[:last] = value.round(3)
112
+ data[:cnt] += units
107
113
  data[:avg] = (total / data[:cnt]).round(3)
108
114
  data
109
115
  end
110
116
 
111
- def bench_str_mem(mem, count = nil, ref: nil, reffix: false)
112
- ref = reffix ? ref : nil
117
+ def bench_str_mem(mem, count = nil, last = nil, ref: nil, reffix: false)
118
+ ref = nil unless reffix
113
119
  msg = [ref, 'Memory'].compact.join(' -- ')
114
- cnt = count ? " (cnt: #{count})" : ''
120
+ cnt = count ? " (cnt: #{count}; lst: #{last} MB)" : ''
115
121
  "#{msg}: #{mem} MB#{cnt}"
116
122
  end
117
123
 
118
124
  def active_support_duration?
119
125
  return false unless Kernel.const_defined?(:ActiveSupport)
126
+
120
127
  ActiveSupport.const_defined?(:Duration, false)
121
128
  end
122
129
 
123
- def bench_str_time(time, count = nil, ref: nil, reffix: false)
124
- ref = reffix ? ref : nil
130
+ def bench_str_time(time, count = nil, last = nil, ref: nil, reffix: false)
131
+ ref = nil unless reffix
125
132
  msg = [ref, 'Time'].compact.join(' -- ')
126
133
  total = (time * count).round(2)
127
134
  str_desc = ''
135
+
128
136
  if active_support_duration? && total >= 60
129
137
  duration = ActiveSupport::Duration.build(total.round)
130
- str_desc = ": #{duration_to_s(duration)}"
138
+ str_desc = " > #{duration_to_s(duration)}"
131
139
  end
132
- cnt = count ? " (cnt: #{count}; sum: #{total} s#{str_desc})" : ''
140
+
141
+ cnt = count ? " (cnt: #{count}; lst: #{last} s ; sum: #{total} s#{str_desc})" : ''
133
142
  "#{msg}: #{time} s#{cnt}"
134
143
  end
135
144
 
136
145
  def duration_to_s(value)
137
- return "" unless active_support_duration?
138
- return "" if value.nil?
146
+ return '' unless active_support_duration?
147
+ return '' if value.nil?
148
+
149
+ msg = "Expecint ActiveSupport::Duration. Given: #{value.class}"
150
+ raise ArgumentError, msg unless value.is_a?(ActiveSupport::Duration)
139
151
 
140
- raise ArgumentError, "Expecint ActiveSupport::Duration. Given: #{value.class}" unless value.is_a?(ActiveSupport::Duration)
141
152
  parts = value.parts.map {|pair| pair.reverse.join(" ")}
142
153
  return parts.first if parts.length == 1
154
+
143
155
  [parts[..-2].join(", "), parts.last].join(" and ")
144
156
  end
145
157
 
@@ -148,6 +160,7 @@ module Ecoportal
148
160
  if Gem.win_platform?
149
161
  wmem = `wmic process where processid=#{Process.pid} get WorkingSetSize | findstr "[0-9]"`
150
162
  return 0 unless wmem
163
+
151
164
  wmem.lines.first.chop.strip.to_i / 1024.0
152
165
  else
153
166
  `ps -o rss= -p #{Process.pid}`.to_i
@@ -0,0 +1,41 @@
1
+ module Ecoportal
2
+ module API
3
+ module Common
4
+ module Concerns
5
+ module Threadable
6
+ private
7
+
8
+ def new_thread(threads, max: 20, &block)
9
+ if max == 1
10
+ yield
11
+ return []
12
+ end
13
+
14
+ threads.shift.join if threads.count >= max
15
+ threads << Thread.new(&block)
16
+ end
17
+
18
+ def mutex(key = :general)
19
+ require 'thread'
20
+ key = key.to_sym
21
+ @mutex ||= {}
22
+ @mutex[key] ||= Mutex.new
23
+ end
24
+
25
+ def with_preserved_thread_globals(abort: false, report: true)
26
+ mutex(:thread_globals).synchronize do
27
+ aux_thr_aoe = Thread.abort_on_exception
28
+ aux_thr_roe = Thread.report_on_exception
29
+ Thread.abort_on_exception = abort
30
+ Thread.report_on_exception = report
31
+ yield
32
+ ensure
33
+ Thread.abort_on_exception = aux_thr_aoe
34
+ Thread.report_on_exception = aux_thr_roe
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -7,4 +7,5 @@ module Ecoportal
7
7
  end
8
8
  end
9
9
 
10
+ require 'ecoportal/api/common/concerns/threadable'
10
11
  require 'ecoportal/api/common/concerns/benchmarkable'
@@ -7,13 +7,9 @@ module Ecoportal
7
7
  # - Its purpose is to handle an Array of basic objects (i.e. `Date`, `String`, `Number`)
8
8
  class ArrayModel < Content::DoubleModel
9
9
  class TypeMismatchedComparison < StandardError
10
- def initialize (this: nil, that: msg = "Trying to compare objects with different behavior.")
11
- if this
12
- msg += " From object with 'order_matters: #{this.order_matters?}' and 'uniq: #{this.uniq?}'."
13
- end
14
- if that
15
- msg += " To object where 'order_matters: #{that.order_matters?}' and 'uniq: #{that.uniq?}'."
16
- end
10
+ def initialize(this: nil, that: msg = "Trying to compare objects with different behavior.")
11
+ msg << " From object with 'order_matters: #{this.order_matters?}' and 'uniq: #{this.uniq?}'." if this
12
+ msg << " To object where 'order_matters: #{that.order_matters?}' and 'uniq: #{that.uniq?}'." if that
17
13
  super(msg)
18
14
  end
19
15
  end
@@ -26,8 +22,9 @@ module Ecoportal
26
22
  # @param a [ArrayModel]
27
23
  # @param b [ArrayModel]
28
24
  # @return [Boolean] `true` if both elements have same behaviour
29
- def same_type?(a, b)
30
- raise "To use this comparison both objects should be `ArrayModel`" unless a.is_a?(ArrayModel) && b.is_a?(ArrayModel)
25
+ def same_type?(a, b) # rubocop:disable Naming/MethodParameterName
26
+ msg = "To use this comparison both objects should be `ArrayModel`"
27
+ raise msg unless a.is_a?(ArrayModel) && b.is_a?(ArrayModel)
31
28
  (a.order_matters? == b.order_matters?) && (a.uniq? == b.uniq?)
32
29
  end
33
30
  end
@@ -38,15 +35,29 @@ module Ecoportal
38
35
  super(doc, parent: parent, key: key, read_only: read_only)
39
36
  end
40
37
 
41
- def order_matters?; self.class.order_matters; end
42
- def uniq?; self.class.uniq; end
38
+ def order_matters?
39
+ self.class.order_matters
40
+ end
41
+
42
+ def uniq?
43
+ self.class.uniq
44
+ end
43
45
 
44
- def length; count; end
45
- def empty?; count == 0; end
46
- def present?; count > 0; end
46
+ def length
47
+ count
48
+ end
49
+
50
+ def empty?
51
+ count&.zero?
52
+ end
53
+
54
+ def present?
55
+ count&.positive?
56
+ end
47
57
 
48
58
  def each(&block)
49
59
  return to_enum(:each) unless block
60
+
50
61
  _items.each(&block)
51
62
  end
52
63
 
@@ -66,10 +77,10 @@ module Ecoportal
66
77
  _items.slice(0..-1)
67
78
  end
68
79
 
69
- # @param value [Object, Array<Object>, ArrayModel] the value(s) of the new object
80
+ # @param other [Object, Array<Object>, ArrayModel] the value(s) of the new object
70
81
  # @return [ArrayModel] a new object with the current class
71
- def new_from(value)
72
- self.class.new(into_a(value))
82
+ def new_from(other)
83
+ self.class.new(into_a(other))
73
84
  end
74
85
 
75
86
  # @return [ArrayModel] a copy of the current object
@@ -93,28 +104,28 @@ module Ecoportal
93
104
  # @param pos [Integer] the position of the element
94
105
  # @param value [String, Date, Number] the element
95
106
  # @return [Date, String, Number]
96
- def []=(post, value)
107
+ def []=(pos, value)
97
108
  _items[pos] = value
98
109
  on_change
99
110
  self[pos]
100
111
  end
101
112
 
102
113
  # Compares with an `Array` or another `ArrayModel`
103
- # @param a [ArrayModel, Array]
104
- def ==(a)
105
- return true if self.equal?(a)
106
- return false unless (a.class == self.class) || a.is_a?(Array)
107
- case a
114
+ # @param other [ArrayModel, Array]
115
+ def ==(other)
116
+ return true if equal?(other)
117
+ return false unless (other.class == self.class) || other.is_a?(Array)
118
+
119
+ case other
108
120
  when Array
109
- self == new_from(a)
121
+ self == new_from(other)
110
122
  when ArrayModel
111
- return true if
112
- raise TypeMismatchedComparison.new(this: self, that: a) unless self.class.same_type?(self, a)
123
+ raise TypeMismatchedComparison.new(this: self, that: other) unless self.class.same_type?(self, other)
113
124
 
114
- if self.order_matters?
115
- _items == a.to_a
125
+ if order_matters?
126
+ _items == other.to_a
116
127
  else
117
- (_items - a.to_a).empty? && (a.to_a - _items).empty?
128
+ (_items - other.to_a).empty? && (other.to_a - _items).empty?
118
129
  end
119
130
  end
120
131
  end
@@ -154,11 +165,10 @@ module Ecoportal
154
165
  end
155
166
 
156
167
  # Resets the `Array` by keeping its reference and adds the value(s)
157
- # @param value [Object, Array<Object>, ArrayModel] the value(s) to be added
158
- # @param values [Array]
159
- def <(values)
168
+ # @param other [Object, Array<Object>, ArrayModel] the value(s) to be added
169
+ def <(other)
160
170
  _items.clear
161
- self << values
171
+ self << other
162
172
  end
163
173
 
164
174
  # Clears the `Array` keeping its reference
@@ -169,25 +179,25 @@ module Ecoportal
169
179
  end
170
180
 
171
181
  # Concat to new
172
- def +(value)
173
- new_from(self.to_a + into_a(value))
182
+ def +(other)
183
+ new_from(to_a + into_a(other))
174
184
  end
175
185
 
176
186
  # Join
177
- # @param value [Object, Array<Object>, ArrayModel] the value(s) to be joined
187
+ # @param other [Object, Array<Object>, ArrayModel] the value(s) to be joined
178
188
  # @return [ArrayModel] a new object instance with the intersection done
179
- def |(value)
180
- new = new_from(value) - self
181
- new_from(to_a + new.to_a)
189
+ def |(other)
190
+ oth = new_from(other) - self
191
+ new_from(to_a + oth.to_a)
182
192
  end
183
193
 
184
194
  # Intersect
185
- # @param value [Object, Array<Object>, ArrayModel] the value(s) to be deleted
195
+ # @param other [Object, Array<Object>, ArrayModel] the value(s) to be deleted
186
196
  # @return [ArrayModel] a new object instance with the intersection done
187
- def &(value)
188
- self.dup.tap do |out|
189
- self.dup.tap do |delta|
190
- delta.delete!(*into_a(value))
197
+ def &(other)
198
+ dup.tap do |out|
199
+ dup.tap do |delta|
200
+ delta.delete!(*into_a(other))
191
201
  out.delete!(*into_a(delta))
192
202
  end
193
203
  end
@@ -196,9 +206,9 @@ module Ecoportal
196
206
  # Subtract
197
207
  # @param value [Object, Array<Object>, ArrayModel] the value(s) to be deleted
198
208
  # @return [ArrayModel] a **copy** of the object with the elements subtracted
199
- def -(value)
200
- self.dup.tap do |copy|
201
- copy.delete!(*into_a(value))
209
+ def -(other)
210
+ dup.tap do |copy|
211
+ copy.delete!(*into_a(other))
202
212
  end
203
213
  end
204
214
 
@@ -206,7 +216,7 @@ module Ecoportal
206
216
  def delete!(*values)
207
217
  values.map do |v|
208
218
  deletion!(v)
209
- end.tap do |r|
219
+ end.tap do |_r|
210
220
  on_change
211
221
  end
212
222
  end
@@ -216,43 +226,41 @@ module Ecoportal
216
226
  # @param val1 [Object] the first value to swap
217
227
  # @param val2 [Object] the second value to swap
218
228
  # @return [Integer] the new of `value1`, `nil` if it wasn't moved
219
- def swap(value1, value2)
220
- index(value2).tap do |dest|
221
- if dest && pos = index(value1)
222
- _items[dest] = value1
223
- _items[pos] = value2
229
+ def swap(val_1, val_2)
230
+ index(val_2).tap do |dest|
231
+ if dest && (pos = index(val_1))
232
+ _items[dest] = val_1
233
+ _items[pos] = val_2
224
234
  end
225
235
  end
226
236
  end
227
237
 
228
238
  def insert_one(value, pos: NOT_USED, before: NOT_USED, after: NOT_USED)
229
- i = index(value)
230
- return i if (i && uniq?)
231
- pos = case
232
- when used_param?(pos) && pos
233
- pos
234
- when used_param?(before) && before
235
- index(before)
236
- when used_param?(after) && after
237
- if i = index(after) then i + 1 end
238
- end
239
+ idx = index(value)
240
+ return idx if idx && uniq?
241
+
242
+ pos =
243
+ if used_param?(pos) && pos
244
+ pos
245
+ elsif used_param?(before) && before
246
+ index(before)
247
+ elsif used_param?(after) && after
248
+ if (idx = index(after)) then idx + 1 end
249
+ end
239
250
 
251
+ # use last position as default
240
252
  pos ||= length
241
- pos.tap do |i|
253
+ pos.tap do |_i|
242
254
  _items.insert(pos, value)
243
255
  on_change
244
256
  end
245
257
  end
246
258
 
247
259
  # TODO
248
- def move(value, pos: NOT_USED, before: NOT_USED, after: NOT_USED)
249
- if i = index(value)
250
- unless i == pos
251
-
252
- on_change
253
- end
254
- pos
255
- end
260
+ def move(value, pos: NOT_USED, _before: NOT_USED, _after: NOT_USED)
261
+ return unless (idx = index(value))
262
+ on_change unless idx == pos
263
+ pos
256
264
  end
257
265
 
258
266
  protected
@@ -266,17 +274,15 @@ module Ecoportal
266
274
  def into_a(value)
267
275
  raise "Can't convert to 'Array' a 'Hash', as is a key_value pair Enumerable" if value.is_a?(Hash)
268
276
  return value.to_a.slice(0..-1) if value.is_a?(Enumerable)
277
+
269
278
  [].push(value).compact
270
279
  end
271
280
 
272
281
  def deletion!(value)
273
- if !uniq?
274
- if i = _items.index(value)
275
- _items.slice!(i)
276
- end
277
- else
278
- _items.delete(value)
279
- end
282
+ return _items.delete(value) if uniq?
283
+ return unless (idx = _items.index(value))
284
+
285
+ _items.slice!(idx)
280
286
  end
281
287
  end
282
288
  end