ecoportal-api-v2 1.1.7 → 2.0.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.
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