libis-workflow-mongoid 2.0.2 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0a259d21bb114e3b4397ecb6c1152769d9960965
4
- data.tar.gz: c75a90afe74196dd1ab779cd37fc7012f2b04c0b
3
+ metadata.gz: ae15d66c11b105c5d81e707bad684d3b7f9805ec
4
+ data.tar.gz: 1116380cbcc99ae1bf7ba0aac642f12c633feac9
5
5
  SHA512:
6
- metadata.gz: c3318624c4f63e725150382369c9bc29fedb501996cb37113cb7d57729090d024f81bf54bf5cdcd086b47e43d0c42e350b6d66342148024b77eeaca298debdae
7
- data.tar.gz: 8f2b4c72528be421539f498166943cdcd5f228d7f6799b8889f4bbd9e58265bfe591c8f5ed16f68f55d98b19671ebabdac72f9e62646a6dc2b0a4bd0f9732d4d
6
+ metadata.gz: 082fee48cc81d1a54b0e451b93ab4704aa86aaaf8d479946bfbeeff886eabee7628b43730266c027f08b61fcfaf6eb7ec11c2bf27c60f5dfffc33f0fb79f50da
7
+ data.tar.gz: 4563f3ec708ecc6e72d05e1b7485422dae39b06080a7afa5ddba4be5df6481ea9aeac5387dc4e234ca54549fab37da23e71709008534244f4c7ae0096848abfe
data/Gemfile CHANGED
@@ -1,4 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec name: 'libis-workflow-mongoid', development_group: :test
4
-
@@ -3,6 +3,7 @@ require 'mongoid'
3
3
  require 'mongoid/document'
4
4
  require 'yaml'
5
5
  require 'libis/tools/extend/hash'
6
+ require 'map_with_indifferent_access'
6
7
 
7
8
  # require 'mongoid_indifferent_access'
8
9
  require_relative 'sequence'
@@ -15,6 +16,7 @@ module Libis
15
16
  module Base
16
17
 
17
18
  def self.included(klass)
19
+ klass.extend(ClassMethods)
18
20
  klass.class_eval do
19
21
  include ::Mongoid::Document
20
22
  include ::Mongoid::Timestamps::Created::Short
@@ -26,14 +28,50 @@ module Libis
26
28
  end
27
29
  end
28
30
 
31
+ module ClassMethods
32
+ def from_hash(hash)
33
+ self.create_from_hash(hash.cleanup, [:name])
34
+ end
35
+
36
+ def create_from_hash(hash, id_tags, &block)
37
+ hash = hash.key_strings_to_symbols
38
+ id_tags = id_tags.map(&:to_sym)
39
+ return nil unless id_tags.empty? || id_tags.any? { |k| hash.include?(k) }
40
+ tags = id_tags.inject({}) do |h, k|
41
+ v = hash.delete(k)
42
+ h[k] = v if v
43
+ h
44
+ end
45
+ item = tags.empty? ? self.new : self.find_or_initialize_by(tags)
46
+ block.call(item, hash) if block unless hash.empty?
47
+ item.assign_attributes(hash)
48
+ unless self.embedded?
49
+ item.save!
50
+ end
51
+ item
52
+ end
53
+
54
+ def indifferent_hash(field_name, method_name)
55
+ define_method method_name do
56
+ MapWithIndifferentAccess::Map.new(self.read_attribute(field_name))
57
+ end
58
+
59
+ define_method "#{method_name}=" do |value|
60
+ self.write_attribute(field_name, value)
61
+ self.send(method_name)
62
+ end
63
+ end
64
+
65
+ end
66
+
29
67
  def dup
30
68
  new_obj = self.class.new
31
69
  new_obj.copy_attributes(self)
32
70
  end
33
71
 
34
- def info
35
- result = self.attributes.reject { |k,v| v.blank? || volatile_attributes.include?(k) }
36
- result = result.to_yaml.gsub(/!ruby\/hash:BSON::Document/,'')
72
+ def to_hash
73
+ result = self.attributes.reject { |k, v| v.blank? || volatile_attributes.include?(k) }
74
+ result = result.to_yaml.gsub(/!ruby\/hash:BSON::Document/, '')
37
75
  # noinspection RubyResolve
38
76
  result = YAML.load(result)
39
77
  result.key_strings_to_symbols!(recursive: true)
@@ -48,6 +86,7 @@ module Libis
48
86
  def volatile_attributes
49
87
  %w'_id c_at'
50
88
  end
89
+
51
90
  private
52
91
 
53
92
  def copy_attributes(other)
@@ -1,5 +1,4 @@
1
- # encoding: utf-8
2
-
1
+ require 'map_with_indifferent_access'
3
2
  require 'libis/workflow/base/job'
4
3
  require 'libis/workflow/mongoid/base'
5
4
 
@@ -16,7 +15,7 @@ module Libis
16
15
 
17
16
  field :name, type: String
18
17
  field :description, type: String
19
- field :input, type: Hash, default: -> { Hash.new }
18
+ field :_input, type: Hash, default: -> { Hash.new }
20
19
  field :run_object, type: String
21
20
  field :log_to_file, type: Boolean, default: false
22
21
  field :log_each_run, type: Boolean, default: false
@@ -30,6 +29,29 @@ module Libis
30
29
 
31
30
  belongs_to :workflow, polymorphic: true
32
31
 
32
+ def self.from_hash(hash)
33
+ self.create_from_hash(hash, [:name]) do |item, cfg|
34
+ item.workflow = Libis::Workflow::Mongoid.from_hash(name: cfg.delete(:workflow))
35
+ end
36
+ end
37
+
38
+ # def input
39
+ # MapWithIndifferentAccess::Map.new(self.read_attribute(:_input))
40
+ # end
41
+ #
42
+ # def input=(hash)
43
+ # self.write_attribute(:_input, hash)
44
+ # self.input
45
+ # end
46
+
47
+ indifferent_hash :_input, :input
48
+
49
+ def to_hash
50
+ result = super
51
+ result[:input] = result.delete(:_input)
52
+ result
53
+ end
54
+
33
55
  def logger
34
56
  return ::Libis::Workflow::Mongoid::Config.logger unless self.log_to_file
35
57
  logger = ::Logging::Repository[self.name]
@@ -19,10 +19,14 @@ module Libis
19
19
  field :start_date, type: Time, default: -> { Time.now }
20
20
  field :log_to_file, type: Boolean, default: false
21
21
  field :log_level, type: String, default: 'DEBUG'
22
+ field :log_filename, type: String
22
23
 
23
24
  set_callback(:destroy, :before) do |document|
24
25
  wd = document.work_dir
25
26
  FileUtils.rmtree wd if wd && !wd.blank? && Dir.exist?(wd)
27
+ # noinspection RubyResolve
28
+ log_file = document.log_filename
29
+ FileUtils.rm(log_file) if log_file && !log_file.blank? && File.exist?(log_file)
26
30
  end
27
31
 
28
32
  index start_date: 1
@@ -30,23 +34,24 @@ module Libis
30
34
  belongs_to :job, polymorphic: true
31
35
  embeds_one :log_config, as: :log_configurator
32
36
 
33
- def run
37
+ def run(action = :run)
34
38
  self.tasks = []
35
39
  self.items = []
36
40
  # noinspection RubySuperCallWithoutSuperclassInspection
37
- super
41
+ super action
38
42
  end
39
43
 
40
44
  def logger
41
45
  unless self.log_to_file
42
46
  return self.job.logger
43
47
  end
44
- logger = ::Logging::Repository[self.name]
48
+ logger = ::Logging::Repository.instance[self.name]
45
49
  return logger if logger
46
50
  unless ::Logging::Appenders[self.name]
51
+ self.log_filename ||= File.join(::Libis::Workflow::Mongoid::Config[:log_dir], "#{self.name}.log")
47
52
  ::Logging::Appenders::File.new(
48
53
  self.name,
49
- filename: File.join(::Libis::Workflow::Mongoid::Config[:log_dir], "#{self.name}.log"),
54
+ filename: self.log_filename,
50
55
  layout: ::Libis::Workflow::Mongoid::Config.get_log_formatter,
51
56
  level: self.log_level
52
57
  )
@@ -3,7 +3,7 @@
3
3
  module Libis
4
4
  module Workflow
5
5
  module Mongoid
6
- VERSION = '2.0.2' unless const_defined? :VERSION # the guard is against a redefinition warning that happens on Travis
6
+ VERSION = '2.0.3' unless const_defined? :VERSION # the guard is against a redefinition warning that happens on Travis
7
7
  end
8
8
  end
9
9
  end
@@ -1,4 +1,4 @@
1
- # encoding: utf-8
1
+ require 'map_with_indifferent_access'
2
2
  require 'libis-workflow'
3
3
 
4
4
  module Libis
@@ -14,9 +14,9 @@ module Libis
14
14
 
15
15
  store_in collection: 'workflow_items'
16
16
 
17
- field :options, type: Hash, default: -> { Hash.new }
18
- field :properties, type: Hash, default: -> { Hash.new }
19
- field :summary, type: Hash, default: -> { Hash.new }
17
+ field :_options, type: Hash, default: -> { Hash.new }
18
+ field :_properties, type: Hash, default: -> { Hash.new }
19
+ field :_summary, type: Hash, default: -> { Hash.new }
20
20
 
21
21
  has_many :logs, as: :logger, class_name: Libis::Workflow::Mongoid::LogEntry.to_s,
22
22
  dependent: :destroy, autosave: true, order: :_id.asc do
@@ -39,9 +39,21 @@ module Libis
39
39
  document.logs.each { |log| log.destroy! }
40
40
  end
41
41
 
42
+ indifferent_hash :_options, :options
43
+ indifferent_hash :_properties, :properties
44
+ indifferent_hash :_summary, :summary
45
+
42
46
  end
43
47
  end
44
48
 
49
+ def to_hash
50
+ result = super
51
+ result[:options] = result.delete(:_options)
52
+ result[:properties] = result.delete(:_properties)
53
+ result[:summary] = result.delete(:_summary)
54
+ result
55
+ end
56
+
45
57
  def log_history
46
58
  # noinspection RubyResolve
47
59
  self.logs.log_history.all || []
@@ -1,5 +1,4 @@
1
- # encoding: utf-8
2
-
1
+ require 'map_with_indifferent_access'
3
2
  require 'libis/workflow/base/workflow'
4
3
  require 'libis/workflow/mongoid/base'
5
4
  require 'libis/tools/config_file'
@@ -18,12 +17,23 @@ module Libis
18
17
 
19
18
  field :name, type: String
20
19
  field :description, type: String
21
- field :config, type: Hash, default: -> { Hash.new }
20
+ field :_config, type: Hash, default: -> { Hash.new }
22
21
 
23
22
  index({name: 1}, {unique: 1})
24
23
 
25
24
  has_many :jobs, as: :workflow, dependent: :destroy, autosave: true, order: :c_at.asc
26
25
 
26
+ def self.from_hash(hash)
27
+ self.create_from_hash(hash, [:name]) do |item, cfg|
28
+ if (value = item.read_attribute(:config))
29
+ item.write_attribute(:_config, value)
30
+ item.remove_attribute(:config)
31
+ end
32
+ item.configure(cfg.key_strings_to_symbols(recursive: true).merge(name: item.name))
33
+ cfg.clear
34
+ end
35
+ end
36
+
27
37
  def self.load(file_or_hash)
28
38
  config = Libis::Tools::ConfigFile.new
29
39
  config << file_or_hash
@@ -33,6 +43,18 @@ module Libis
33
43
  workflow
34
44
  end
35
45
 
46
+ indifferent_hash :_config, :config
47
+
48
+ def to_hash
49
+ result = super
50
+ result[:config] = result.delete(:_config)
51
+ result
52
+ end
53
+
54
+ def input
55
+ Libis::Tools::DeepStruct.new(super)
56
+ end
57
+
36
58
  end
37
59
  end
38
60
  end
@@ -0,0 +1,20 @@
1
+ require "map_with_indifferent_access/version"
2
+ require "map_with_indifferent_access/wraps_collection"
3
+ require "map_with_indifferent_access/map"
4
+ require "map_with_indifferent_access/list"
5
+ require "map_with_indifferent_access/values"
6
+ require "map_with_indifferent_access/normalization"
7
+ require 'forwardable'
8
+
9
+ module MapWithIndifferentAccess
10
+ class << self
11
+ extend Forwardable
12
+
13
+ # @!method new
14
+ # Creates and returns a new instance of {Map}.
15
+ #
16
+ # @return [Map]
17
+ # @see Map#initialize
18
+ def_delegator Map, :new
19
+ end
20
+ end
@@ -0,0 +1,867 @@
1
+ module MapWithIndifferentAccess
2
+
3
+ class List
4
+ extend Forwardable
5
+ include MapWithIndifferentAccess::WrapsCollection
6
+
7
+ def first
8
+ self.at(0)
9
+ end
10
+
11
+ def last
12
+ self.at(-1)
13
+ end
14
+
15
+ # Try to convert `from_obj` into a {List}.
16
+ #
17
+ # @return [List]
18
+ # converted object if `from_obj` is convertible.
19
+ #
20
+ # @return [nil]
21
+ # if `from_obj` cannot be converted for any reason.
22
+ def self.try_convert(from_obj)
23
+ if self === from_obj
24
+ from_obj
25
+ else
26
+ array = ::Array.try_convert( from_obj )
27
+ new( array ) if array
28
+ end
29
+ end
30
+
31
+ # Try to convert `obj`, which might be a {List} into an
32
+ # `Array`.
33
+ #
34
+ # @return [Array]
35
+ # converted object if `obj` is convertible.
36
+ #
37
+ # @return [nil]
38
+ # if `obj` cannot be converted for any reason.
39
+ def self.try_deconstruct(obj)
40
+ if self === obj
41
+ obj.inner_array
42
+ elsif obj.respond_to?(:to_ary )
43
+ a = obj.to_ary
44
+ ::Array === a ? a : nil
45
+ else
46
+ nil
47
+ end
48
+ end
49
+
50
+ # @!attribute inner_array
51
+ # @return [Array]
52
+ #
53
+ # Alias for {#inner_collection}. The encapsulated `Array`
54
+ # instance.
55
+ alias inner_array inner_collection
56
+
57
+ # @!method to_a
58
+ #
59
+ # Alias for {#inner_collection}. Returns the
60
+ # encapsulated `Array` instance.
61
+
62
+ # Use class_eval to hide the aliasing from Yard doc.
63
+ class_eval 'alias to_a inner_collection', __FILE__, __LINE__
64
+
65
+ # Initializes a new instance of {List} that encapsulates a
66
+ # new empty `Array` or the `Array` coerced from the given
67
+ # `basis`.
68
+ #
69
+ # When a {List} is given as a `basis`, this results on the
70
+ # given and new instances sharing the same {#inner_array}.
71
+ # There is no obvious reason to do that on purpose, but there
72
+ # is also no particular harm in allowing it to happen.
73
+ #
74
+ # @param [Array, List, Object] basis
75
+ # An `Array` or an object that can be implicitly coerced to
76
+ # an `Array`
77
+ def initialize(basis = [])
78
+ use_basis = basis
79
+ use_basis = basis.inner_array if self.class === basis
80
+ use_basis = ::Array.try_convert( use_basis )
81
+ raise ArgumentError, "Could not convert #{basis.inspect} into an ::Array" unless use_basis
82
+ @inner_collection = use_basis
83
+ end
84
+
85
+ # Element Assignment — Sets the element at index, or replaces
86
+ # a subarray from the start index for length elements, or
87
+ # replaces a subarray specified by the range of indices.
88
+ #
89
+ # The given object or array is internalized befor being
90
+ # ussed for assignment into the {#inner_array}.
91
+ #
92
+ # @return the given value or array.
93
+ #
94
+ # @see Values.internalize
95
+ # @see #push
96
+ # @see #unshift
97
+ #
98
+ # @overload []=(index, value)
99
+ # @param index [Fixnum]
100
+ # @param value [Object]
101
+ #
102
+ # @overload []=(start, length, array_or_value)
103
+ # @param start [Fixnum]
104
+ # @param length [Fixnum]
105
+ # @param array_or_value [Array, List Object, nil]
106
+ #
107
+ # @overload []=(range, array_or_value)
108
+ # @param range [Ramge]
109
+ # @param array_or_value [Array, List, Object, nil]
110
+ def []=(index, length_or_value, *maybe_value)
111
+ arg_count = 2 + maybe_value.length
112
+ unless (2..3) === arg_count
113
+ raise ArgumentError, "wrong number of arguments (#{arg_count} for 2..3)"
114
+ end
115
+
116
+ if maybe_value.empty?
117
+ maybe_length = []
118
+ value_or_values = length_or_value
119
+ else
120
+ maybe_length = [length_or_value]
121
+ value_or_values = maybe_value.first
122
+ end
123
+
124
+ if (
125
+ ( !maybe_length.empty? || Range === index ) &&
126
+ ( value_array = List.try_deconstruct( value_or_values ) )
127
+ )
128
+ value_array = value_array.map{ |v| Values << v }
129
+ inner_array[ index, *maybe_length ] = value_array
130
+ else
131
+ value = Values << value_or_values
132
+ inner_array[ index, *maybe_length ] = value
133
+ end
134
+ end
135
+
136
+ # @!method []
137
+ # Returns the element at index, or returns a subarray
138
+ # starting at the start index and continuing for length
139
+ # elements, or returns a subarray specified by range of
140
+ # indices.
141
+ #
142
+ # Externalizes the result before returning it.
143
+ #
144
+ # @see Values.externalize
145
+ #
146
+ # @overload [](index)
147
+ # @param index [Fixnum]
148
+ # @return [Object]
149
+ #
150
+ # @overload [](start, length)
151
+ # @param start [Fixnum]
152
+ # @param length [Fixnum]
153
+ # @return [List]
154
+ #
155
+ # @overload [](range)
156
+ # @param range [Range]
157
+ # @return [List]
158
+ #
159
+ # @overload slice(index)
160
+ # @param index [Fixnum]
161
+ # @return [Object]
162
+ #
163
+ # @overload slice(start, length)
164
+ # @param start [Fixnum]
165
+ # @param length [Fixnum]
166
+ # @return [List]
167
+ #
168
+ # @overload slice(range)
169
+ # @param range [Range]
170
+ # @return [List]
171
+
172
+ ['[]', 'slice'].each do |method_name|
173
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
174
+
175
+ def #{method_name}(index, *maybe_length)
176
+ arg_count = 1 + maybe_length.length
177
+ unless (1..2) === arg_count
178
+ raise ArgumentError, "wrong number of arguments (\#{arg_count} for 1..2)"
179
+ end
180
+
181
+ if !maybe_length.empty? || Range === index
182
+ value_array = inner_array.#{method_name}( index, *maybe_length )
183
+ value_array.map!{ |v| Values >> v }
184
+ List.new( value_array )
185
+ else
186
+ value = inner_array.#{method_name}( index )
187
+ Values >> value
188
+ end
189
+ end
190
+
191
+ EOS
192
+ end
193
+
194
+ # Returns the externalization of the element at `index`. A
195
+ # negative index counts from the end of the list. Returns
196
+ # `nil` if the index is out of range.
197
+ #
198
+ # @see #[]
199
+ def at(index)
200
+ item = inner_array.at( index )
201
+ Values >> item
202
+ end
203
+
204
+ # Append. Pushes the given object on to the end of the list.
205
+ # Returns the array itself, so several appends may be chained
206
+ # together.
207
+ #
208
+ # Internalizes the given onject before appending it to the
209
+ # target's {#inner_array}.
210
+ #
211
+ # @return [List]
212
+ # @see #push
213
+ def <<(value)
214
+ value = Values << value
215
+ inner_array << value
216
+ self
217
+ end
218
+
219
+ # Append. Pushes the given object(s) on to the end of the
220
+ # list. Returns the array itself, so several appends may be
221
+ # chained together.
222
+ #
223
+ # Internalizes each given object before appending it to the
224
+ # target's {#inner_array}.
225
+ #
226
+ # @return [List]
227
+ # @see #<<
228
+ # @see #pop
229
+ # @see Values.internalize
230
+ def push(*values)
231
+ values.map!{ |v| Values << v }
232
+ inner_array.push *values
233
+ self
234
+ end
235
+
236
+ # Prepends objects to the front of the list, moving other
237
+ # elements upwards.
238
+ #
239
+ # Internalizes each value before prepending it to the
240
+ # target's {#inner_array}.
241
+ #
242
+ # See also {#shift} for the opposite effect.
243
+ #
244
+ # @return [List]
245
+ # @see #shift
246
+ # @see Values.internalize
247
+ def unshift(*values)
248
+ values.map!{ |v| Values << v }
249
+ inner_array.unshift *values
250
+ self
251
+ end
252
+
253
+ # Inserts the given values before the element with the given
254
+ # index.
255
+ #
256
+ # Internalizes the values before inserting them into the
257
+ # target's {#inner_array}.
258
+ #
259
+ # Negative indices count backwards from the end of the array,
260
+ # where -1 is the last element. If a negative index is used,
261
+ # the given values will be inserted after that element, so
262
+ # using an index of -1 will insert the values at the end of
263
+ # the list.
264
+ #
265
+ # @return [List]
266
+ # @see Values.internalize
267
+ def insert(index, *values)
268
+ values.map!{ |v| Values << v }
269
+ inner_array.insert(index, *values)
270
+ self
271
+ end
272
+
273
+ # Appends elements of `other` (a `List` or other
274
+ # `Array`-like object) to the target `List`.
275
+ #
276
+ # @param other [List, Array, Object]
277
+ # @return [List] The target list.
278
+ #
279
+ # @see #+
280
+ def concat(other)
281
+ other = self.class.try_deconstruct(other) || other
282
+ inner_array.concat other
283
+ self
284
+ end
285
+
286
+ # Returns a {List} containing the elements in self
287
+ # corresponding to the given selector(s).
288
+ #
289
+ # The selectors may be either `Integer` indices or
290
+ # `Range`s.
291
+ #
292
+ # @return List
293
+ def values_at(*indexes)
294
+ inner_result = inner_array.values_at( *indexes )
295
+ Values >> inner_result
296
+ end
297
+
298
+ # Tries to retrieve the element at position `index`, but
299
+ # raises an `IndexError` exception or uses a default value
300
+ # when an invalid index is referenced.
301
+ #
302
+ # Returns the externalization of the retrieved value.
303
+ #
304
+ # @see MapWithIndifferentAccess::Values.externalize
305
+ #
306
+ # @overload fetch(index)
307
+ # Tries to retrieve the element at position `index`, but
308
+ # raises an `IndexError` exception if the referenced index
309
+ # lies outside of the array bounds.
310
+ #
311
+ # @raise [IndexError]
312
+ #
313
+ # @overload fetch(index, default)
314
+ # Tries to retrieve the element at position `index`, but
315
+ # uses the given default if the referenced index lies
316
+ # outside of the array bounds.
317
+ #
318
+ # @overload fetch(index)
319
+ # @yieldparam index
320
+ # Tries to retrieve the element at position `index`, but if
321
+ # the referenced index lies outside of the array bounds,
322
+ # calls the given block, and uses the block call result.
323
+ def fetch(index, *args)
324
+ item =
325
+ if block_given?
326
+ inner_array.fetch( index, *args ){ |idx| yield idx }
327
+ else
328
+ inner_array.fetch( index, *args )
329
+ end
330
+ Values >> item
331
+ end
332
+
333
+ # @!method shift(*maybe_n)
334
+ # Removes and returns the first element or first `n`
335
+ # elements of the array, shifting all of the other elements
336
+ # downward.
337
+ #
338
+ # Returns the externalization of the removed element or
339
+ # array of elements
340
+ #
341
+ # See {#unshift} for the opposite effect.
342
+ #
343
+ # @see #unshift
344
+ # @see #pop
345
+ #
346
+ # @overload shift()
347
+ # Removes the first element and returns it, shifting all
348
+ # other elements down by one. Returns nil if the array is
349
+ # empty.
350
+ #
351
+ # @return [Object, nil]
352
+ #
353
+ # @overload shift(n)
354
+ # Returns a {List} of the first `n` elements (or less) just
355
+ # like `array.slice!(0, n)` does, but also removing those
356
+ # elements from the target.
357
+ #
358
+ # @return [List]
359
+
360
+ # @!method pop(*maybe_n)
361
+ # Removes and returns the last element or last `n` elements
362
+ # of the array.
363
+ #
364
+ # Returns the externalization of the removed element or array
365
+ # of elements
366
+ #
367
+ # See {#push} for the opposite effect.
368
+ #
369
+ # @see #push
370
+ # @see #shift
371
+ #
372
+ # @overload pop()
373
+ # Removes the last element and returns it. Returns nil if
374
+ # the array is empty.
375
+ #
376
+ # @return [Object, nil]
377
+ #
378
+ # @overload pop(n)
379
+ # Returns a {MapWithIndifferentAccess::List} of the last
380
+ # `n` elements (or less) just like `array.slice!(-n, n)`
381
+ # does, but also removing those elements from the target.
382
+ #
383
+ # @param n [Fixnum]
384
+ # @return [MapWithIndifferentAccess::List]
385
+ #
386
+
387
+ %w(shift pop).each do |method_name|
388
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
389
+
390
+ def #{method_name}(*maybe_n)
391
+ arg_count = maybe_n.length
392
+ unless (0..1) === arg_count
393
+ raise ArgumentError, "wrong number of arguments (\#{arg_count} for 0..1)"
394
+ end
395
+ if maybe_n.empty?
396
+ Values >> inner_array.#{method_name}
397
+ else
398
+ inner_result = inner_array.#{method_name}( *maybe_n )
399
+ List.new( inner_result )
400
+ end
401
+ end
402
+
403
+ EOS
404
+ end
405
+
406
+ # Deletes the element at the specified `index`, returning the
407
+ # externalization of that element, or `nil` if the index is
408
+ # out of range.
409
+ #
410
+ # @param index [Fixnum]
411
+ # @return [Object, nil]
412
+ #
413
+ # @see #slice
414
+ # @see Values.externalize
415
+ def delete_at(index)
416
+ inner_result = inner_array.delete_at( index )
417
+ Values >> inner_result
418
+ end
419
+
420
+ # @!method &(other)
421
+ # @param other [List, Array, Object]
422
+ # @return [List]
423
+ #
424
+ # Set Intersection. Returns a new {List} containing
425
+ # elements common to the target {List} and `other` (a
426
+ # `List` or other `Array`-like object), excluding any
427
+ # duplicate items. The order is preserved from the
428
+ # original list.
429
+ #
430
+ # It compares elements using their `#hash` and `#eql?`
431
+ # methods for efficiency.
432
+ #
433
+ # Note that this does not recongnize items of `Map` type as
434
+ # equal just because they are equal by `#==`, which can be
435
+ # the case when they have equivalent keys that differ by
436
+ # `String`/`Symbol` type. You might therefore wish to call
437
+ # {#&} for lists that have first had their keys
438
+ # deeply-stringified or deeply-symbolized.
439
+
440
+ # @!method |(other)
441
+ # @param other [List, Array, Object]
442
+ # @return [List]
443
+ #
444
+ # Set Union. Returns a new {List} by joining the target
445
+ # `List` with `other` (a `List` or other `Array`-like
446
+ # object), excluding any duplicates and preserving the
447
+ # order from the original `List`.
448
+ #
449
+ # It compares elements using their `#hash` and `#eql?`
450
+ # methods for efficiency.
451
+ #
452
+ # Note that this does not recongnize items of `Map` type as
453
+ # equal just because they are equal by `#==`, which can be
454
+ # the case when they have equivalent keys that differ by
455
+ # `String`/`Symbol` type. You might therefore wish to call
456
+ # {#|} for lists that have first had their keys
457
+ # deeply-stringified or deeply-symbolized.
458
+
459
+ # @!method +(other)
460
+ # @param other [List, Array, Object]
461
+ # @return [List]
462
+ #
463
+ # Concatenation. Returns a new {List} built by
464
+ # concatenating `other` (a `List` or other `Array`-like
465
+ # object) to the target `List`.
466
+ #
467
+ # @see #concat
468
+
469
+ # @!method -(other)
470
+ # @param other [List, Array, Object]
471
+ # @return [List]
472
+ #
473
+ # Difference. Returns a new {List} that is a copy of the
474
+ # original, removing any items that also appear in
475
+ # `other` (a `List` or other `Array`-like object). The
476
+ # order is preserved from the original `List`.
477
+ #
478
+ # It compares elements using their `#hash` and `#eql?`
479
+ # methods for efficiency.
480
+ #
481
+ # Note that this does not recongnize items of `Map` type as
482
+ # equal just because they are equal by `#==`, which can be
483
+ # the case when they have equivalent keys that differ by
484
+ # `String`/`Symbol` type. You might therefore wish to call
485
+ # {#-} for lists that have first had their keys
486
+ # deeply-stringified or deeply-symbolized.
487
+
488
+ %w( & | + - ).each do |method_name|
489
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
490
+
491
+ def #{method_name}(other)
492
+ other = self.class.try_deconstruct( other ) || other
493
+ inner_result = inner_array.#{method_name}(other)
494
+ List.new( inner_result )
495
+ end
496
+
497
+ EOS
498
+ end
499
+
500
+ # @!method join(separator=$,)
501
+ # @param separator [String]
502
+ # @return [String]
503
+ #
504
+ # Returns a string consisting of `String`-converted item
505
+ # values from the target `List` separated by the
506
+ # `separator` string. If no `separator` or `nil` is given,
507
+ # uses the value of `$,` as the separator. Treats a `nil`
508
+ # `$,` value as a blank string.
509
+ #
510
+ # The items are not externalized before being converted to
511
+ # `String`s, so `my_map.join` is exactly equivalent to
512
+ # `my_map.inner_array.join`.
513
+ #
514
+ # @see Array#join
515
+ def_delegator :inner_array, :join
516
+
517
+ # Repetition.
518
+ #
519
+ # @overload *(n_copies)
520
+ # @return [Map]
521
+ #
522
+ # Returns a new `List` built by concatenating `n_copies`
523
+ # copies of itself together.
524
+ #
525
+ # @overload *(separator)
526
+ # @return [String]
527
+ #
528
+ # Equivalent to `target_list.join(separator)`.
529
+ def *(n_copies_or_separator)
530
+ result = inner_array * n_copies_or_separator
531
+ result = List.new( result ) if Array === result
532
+ result
533
+ end
534
+
535
+ # Deletes all items from self, the externalizations of which
536
+ # are equal to the externalization of `obj`.
537
+ #
538
+ # Returns the externalization of the last deleted item if
539
+ # applicable.
540
+ #
541
+ # @see Values.externalize
542
+ #
543
+ # @overload delete(obj)
544
+ # Returns `nil` if no matching items are found.
545
+ #
546
+ # @overload delete(obj)
547
+ # @yield
548
+ # Returns the externalization of the block result is no
549
+ # matching items are found.
550
+ def delete(obj)
551
+ obj = Values >> obj
552
+ removed_items = false
553
+ result = nil
554
+ inner_array.delete_if{ |v|
555
+ v = Values >> v
556
+ if v == obj
557
+ result = v
558
+ removed_items = true
559
+ true
560
+ end
561
+ }
562
+ if !removed_items && block_given?
563
+ result = Values >> yield( obj )
564
+ end
565
+ result
566
+ end
567
+
568
+ # Returns a new instance with duplicate items omitted.
569
+ # Items are considered equal if their `#hash` values are
570
+ # equal and comparison using `#eql?` returns `true`.
571
+ #
572
+ # If a block is given, then externalized items are passed to
573
+ # the block, and the return values from the block will be
574
+ # used for dupliacte-check comparison.
575
+ #
576
+ # Note that items externally represented as `Map`s that are
577
+ # equal according to {Map#==} will not necessarily be
578
+ # identified as duplicates since they can still differ
579
+ # according to {Map#eql} if their encapsulated `Hash`
580
+ # objects are unequal due to key `String`/`Symbol` type
581
+ # differences. You might therefore want to ensure that the
582
+ # target `List` has been deeply stringified or symbolized
583
+ # before calling {#uniq!} on it.
584
+ #
585
+ # @return [List]
586
+ #
587
+ # @see #uniq!
588
+ def uniq
589
+ result = dup
590
+ if block_given?
591
+ result.uniq!{ |item| yield( item ) }
592
+ else
593
+ result.uniq!
594
+ end
595
+ result
596
+ end
597
+
598
+ # Deletes duplicate items from the target's {#inner_array},
599
+ # leaving only unique items remaining. Items are considered
600
+ # equal if their `#hash` values are equal and comparison
601
+ # using `#eql?` returns `true`.
602
+ #
603
+ # Returns the target `List` if any duplicates were found and
604
+ # removed. Otherwise, returns `nil`.
605
+ #
606
+ # If a block is given, then externalized items are passed to
607
+ # the block, and the return values from the block will be
608
+ # used for dupliacte-check comparison.
609
+ #
610
+ # Note that items externally represented as `Map`s that are
611
+ # equal according to {Map#==} will not necessarily be
612
+ # identified as duplicates since they can still differ
613
+ # according to {Map#eql} if their encapsulated `Hash`
614
+ # objects are unequal due to key `String`/`Symbol` type
615
+ # differences. You might therefore want to ensure that the
616
+ # target `List` has been deeply stringified or symbolized
617
+ # before calling {#uniq} on it.
618
+ #
619
+ # @return [List, nil]
620
+ #
621
+ # @see #uniq
622
+ def uniq!
623
+ inner_result =
624
+ if block_given?
625
+ inner_array.uniq!{ |item|
626
+ yield( Values >> item )
627
+ }
628
+ else
629
+ inner_array.uniq!
630
+ end
631
+
632
+ inner_result && self
633
+ end
634
+
635
+ # Equality. The target is equal to the given `Array`-like
636
+ # object if both contain the same number of elements, and
637
+ # externalizations of corresponding items in itself and the
638
+ # given object are equal according to `#==`.
639
+ #
640
+ # @return [Boolean]
641
+ #
642
+ # @see Values.externalize
643
+ def ==(other)
644
+ same_class = self.class === other
645
+
646
+ return false unless same_class || other.respond_to?(:to_ary )
647
+
648
+ # Optimizations
649
+ return true if equal?( other )
650
+ return true if same_class && inner_array == other.inner_array
651
+
652
+ return false unless length == other.length
653
+ zip( other ).all? { |(v,other_v)| v == Values >> other_v }
654
+ end
655
+
656
+ # @param [List, Array, Object]
657
+ # @return [1, 0, -1, nil]
658
+ #
659
+ # Comparison. Returns an integer (-1, 0, or +1) if this
660
+ # `List` is less than, equal to, or greater than `other`, and
661
+ # `other` is a `List` or other `Array`-like object that can
662
+ # be coerced to a `List`.
663
+ #
664
+ # Each externaized item in the target `List` is compared to
665
+ # the corresponding externalized item in `other` (using the
666
+ # `<=>` operator). As soon as a comparison is non zero (i.e.
667
+ # the two corresponding elements are not equal), that result
668
+ # is returned for the whole array comparison.
669
+ #
670
+ # If all the elements are equal, then the result is based on
671
+ # a comparison of the list lengths. Thus, two `List`s are
672
+ # "equal" according to {#<=>} if, and only if, they have the
673
+ # same length and the value of each element is equal to the
674
+ # value of the corresponding element in the other list.
675
+ #
676
+ # `nil` is returned if `other` is not a `List` or `Array`like
677
+ # object or if the comparison of two elements returns `nil`.
678
+ #
679
+ # @see Array#<=>
680
+ def <=>(other)
681
+ return nil unless \
682
+ List === other || (
683
+ other.respond_to?(:to_ary ) && other.respond_to?(:length )
684
+ )
685
+ other = Values >> other
686
+ rel_order( other )
687
+ end
688
+
689
+ # Calls the given block once for each item in the target's
690
+ # {#inner_array}, passing the externalization of the item to
691
+ # the block.
692
+ #
693
+ # @see MapWithIndifferentAccess::Values.externalize
694
+ #
695
+ # @overload each
696
+ # @yieldparam item
697
+ # @return [List]
698
+ #
699
+ # @overload each
700
+ # @return [Enumerator]
701
+ def each
702
+ inner_array.each do |item|
703
+ item = Values >> item
704
+ yield item
705
+ end
706
+ end
707
+
708
+ # @!method assoc(value)
709
+ # Searches through elements of the target `List` that are
710
+ # also externally represented as `List`s, comparing the
711
+ # first item in each of those with `value` using {#==}.
712
+ #
713
+ # Returns the first item from the target that matches (is
714
+ # the first associated `List`) or nil of no match is found.
715
+ #
716
+ # @return [List, nil]
717
+ #
718
+ # @see Array#assoc
719
+ # @see #rassoc
720
+
721
+ # @!method rassoc(value)
722
+ # Searches through elements of the target `List` that are
723
+ # also externally represented as `List`s, comparing the
724
+ # second item in each of those with `value` using {#==}.
725
+ #
726
+ # Returns the first item from the target that matches (is
727
+ # the first associated `List`) or nil of no match is found.
728
+ #
729
+ # @return [List, nil]
730
+ #
731
+ # @see Array#rassoc
732
+ # @see #assoc
733
+
734
+ [ ['assoc', 0 ], ['rassoc', 1 ] ].each do |(method_name,search_col)|
735
+ class_eval <<-EOS, __FILE__, __LINE__
736
+
737
+ def #{method_name}(value)
738
+ result = nil
739
+ each do |item|
740
+ next unless List === item && item.length > #{search_col}
741
+ result = item if item[#{search_col}] == value
742
+ end
743
+ result
744
+ end
745
+
746
+ EOS
747
+ end
748
+
749
+ # Works identically to `Array#bsearch` except that
750
+ # externalized values are passed to the block, and the
751
+ # externalized result is returned.
752
+ #
753
+ # @overload bsearch
754
+ # @yieldparam x
755
+ # @return [Object, nil]
756
+ #
757
+ # @overload bsearch
758
+ # @return [Enumerator]
759
+ def bsearch
760
+ return to_enum(:bsearch) unless block_given?
761
+ inner_result = inner_array.bsearch{ |x| yield Values >> x }
762
+ Values >> inner_result
763
+ end
764
+
765
+ # @!method collect!
766
+ # Invokes the given block once for each externalized item
767
+ # from the target `List`, replacing the element with the
768
+ # internalization of the value returned by the block.
769
+ #
770
+ # If no block is given, returns an `Enumerator` instead.
771
+ #
772
+ # @yieldparam extern_item
773
+ # @return [List, Enumerable]
774
+ #
775
+ # @see Enumerable#collect
776
+ #
777
+ # @overload collect!
778
+ # @overload map!
779
+
780
+ %w(collect! map!).each do |method_name|
781
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
782
+
783
+ def #{method_name}
784
+ return to_enum( :#{method_name} ) unless block_given?
785
+
786
+ inner_array.#{method_name}{ |item|
787
+ item = Values >> item
788
+ mapped_outer = yield( item )
789
+ Values << mapped_outer
790
+ }
791
+ self
792
+ end
793
+
794
+ EOS
795
+ end
796
+
797
+ # Yields every combination of length n of elements from the
798
+ # target `List` in the form of a `List` and then returns the
799
+ # target `List` itself.
800
+ #
801
+ # Makes no guarantees about the order in which the
802
+ # combinations are yielded.
803
+ #
804
+ # If no block is given, an `Enumerator` is returned instead.
805
+ #
806
+ # @overload combination(n)
807
+ # @param n [Fixnum]
808
+ # @yieldparam combination [List]
809
+ # @return [List]
810
+ #
811
+ # @overload combination(n)
812
+ # @param n [Fixnum]
813
+ # @return [Enumerator]
814
+ def combination(n)
815
+ return to_enum( :combination, n ) unless block_given?
816
+
817
+ inner_array.combination n do |inner_combos|
818
+ yield List.new( inner_combos )
819
+ end
820
+ self
821
+ end
822
+
823
+ # Removes `nil` elements from the target `List`.
824
+ #
825
+ # Returns `nil` if no changes were made. Otherwise returns
826
+ # the `List`.
827
+ #
828
+ # @return [List, nil]
829
+ #
830
+ # @see #compact
831
+ def compact!
832
+ inner_array.compact! && self
833
+ end
834
+
835
+ # Returns a copy of the target `List` with all `nil` items
836
+ # removed.
837
+ #
838
+ # @return [List]
839
+ #
840
+ # @see #compact!
841
+ def compact
842
+ result = dup
843
+ result.compact!
844
+ result
845
+ end
846
+
847
+ protected
848
+
849
+ def rel_order(other_list)
850
+ length_rel = length <=> other_list.length
851
+ return other_list.reverse_rel_order(self) if length_rel == 1
852
+
853
+ rel = 0
854
+ zip other_list do |a,b|
855
+ rel = a <=> b
856
+ break unless rel == 0
857
+ end
858
+ rel == 0 ? length_rel : rel
859
+ end
860
+
861
+ def reverse_rel_order(other_list)
862
+ rel = rel_order(other_list)
863
+ rel.nil? ? nil : -rel
864
+ end
865
+ end
866
+
867
+ end