File: iterable.rb

package info (click to toggle)
puppet-agent 7.23.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 19,092 kB
  • sloc: ruby: 245,074; sh: 456; makefile: 38; xml: 33
file content (365 lines) | stat: -rw-r--r-- 11,268 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
module Puppet::Pops::Types

  # Implemented by classes that can produce an iterator to iterate over their contents
  module IteratorProducer
    def iterator
      raise ArgumentError, 'iterator() is not implemented'
    end
  end

  # The runtime Iterable type for an Iterable
  module Iterable
    # Produces an `Iterable` for one of the following types with the following characterstics:
    #
    # `String`       - yields each character in the string
    # `Array`        - yields each element in the array
    # `Hash`         - yields each key/value pair as a two element array
    # `Integer`      - when positive, yields each value from zero to the given number
    # `PIntegerType` - yields each element from min to max (inclusive) provided min < max and neither is unbounded.
    # `PEnumtype`    - yields each possible value of the enum.
    # `Range`        - yields an iterator for all elements in the range provided that the range start and end
    #                  are both integers or both strings and start is less than end using natural ordering.
    # `Dir`          - yields each name in the directory
    #
    # An `ArgumentError` is raised for all other objects.
    #
    # @param my_caller [Object] The calling object to reference in errors
    # @param obj [Object] The object to produce an `Iterable` for
    # @param infer_elements [Boolean] Whether or not to recursively infer all elements of obj. Optional
    #
    # @return [Iterable,nil] The produced `Iterable`
    # @raise [ArgumentError] In case an `Iterable` cannot be produced
    # @api public
    def self.asserted_iterable(my_caller, obj, infer_elements = false)
      iter = self.on(obj, nil, infer_elements)
      raise ArgumentError, "#{my_caller.class}(): wrong argument type (#{obj.class}; is not Iterable." if iter.nil?
      iter
    end

    # Produces an `Iterable` for one of the following types with the following characteristics:
    #
    # `String`       - yields each character in the string
    # `Array`        - yields each element in the array
    # `Hash`         - yields each key/value pair as a two element array
    # `Integer`      - when positive, yields each value from zero to the given number
    # `PIntegerType` - yields each element from min to max (inclusive) provided min < max and neither is unbounded.
    # `PEnumtype`    - yields each possible value of the enum.
    # `Range`        - yields an iterator for all elements in the range provided that the range start and end
    #                  are both integers or both strings and start is less than end using natural ordering.
    # `Dir`          - yields each name in the directory
    #
    # The value `nil` is returned for all other objects.
    #
    # @param o [Object] The object to produce an `Iterable` for
    # @param element_type [PAnyType] the element type for the iterator. Optional
    # @param infer_elements [Boolean] if element_type is nil, whether or not to recursively
    #   infer types for the entire collection. Optional
    #
    # @return [Iterable,nil] The produced `Iterable` or `nil` if it couldn't be produced
    #
    # @api public
    def self.on(o, element_type = nil, infer_elements = true)
      case o
      when IteratorProducer
        o.iterator
      when Iterable
        o
      when String
        Iterator.new(PStringType.new(PIntegerType.new(1, 1)), o.each_char)
      when Array
        if o.empty?
          Iterator.new(PUnitType::DEFAULT, o.each)
        else
          if element_type.nil? && infer_elements
            tc = TypeCalculator.singleton
            element_type = PVariantType.maybe_create(o.map {|e| tc.infer_set(e) })
          end
          Iterator.new(element_type, o.each)
        end
      when Hash
        # Each element is a two element [key, value] tuple.
        if o.empty?
          HashIterator.new(PHashType::DEFAULT_KEY_PAIR_TUPLE, o.each)
        else
          if element_type.nil? && infer_elements
            tc = TypeCalculator.singleton
            element_type = PTupleType.new([
              PVariantType.maybe_create(o.keys.map {|e| tc.infer_set(e) }),
              PVariantType.maybe_create(o.values.map {|e| tc.infer_set(e) })], PHashType::KEY_PAIR_TUPLE_SIZE)
          end
          HashIterator.new(element_type, o.each_pair)
        end
      when Integer
        if o == 0
          Iterator.new(PUnitType::DEFAULT, o.times)
        elsif o > 0
          IntegerRangeIterator.new(PIntegerType.new(0, o - 1))
        else
          nil
        end
      when PIntegerType
        # a finite range will always produce at least one element since it's inclusive
        o.finite_range? ? IntegerRangeIterator.new(o) : nil
      when PEnumType
        Iterator.new(o, o.values.each)
      when PTypeAliasType
        on(o.resolved_type)
      when Range
        min = o.min
        max = o.max
        if min.is_a?(Integer) && max.is_a?(Integer) && max >= min
          IntegerRangeIterator.new(PIntegerType.new(min, max))
        elsif min.is_a?(String) && max.is_a?(String) && max >= min
          # A generalized element type where only the size is inferred is used here since inferring the full
          # range might waste a lot of memory.
          if min.length < max.length
            shortest = min
            longest = max
          else
            shortest = max
            longest = min
          end
          Iterator.new(PStringType.new(PIntegerType.new(shortest.length, longest.length)), o.each)
        else
          # Unsupported range. It's either descending or nonsensical for other reasons (float, mixed types, etc.)
          nil
        end
      else
        # Not supported. We cannot determine the element type
        nil
      end
    end

    # Answers the question if there is an end to the iteration. Puppet does not currently provide any unbounded
    # iterables.
    #
    # @return [Boolean] `true` if the iteration is unbounded
    def self.unbounded?(object)
      case object
      when Iterable
        object.unbounded?
      when String,Integer,Array,Hash,Enumerator,PIntegerType,PEnumType,Dir
        false
      else
        TypeAsserter.assert_instance_of('', PIterableType::DEFAULT, object, false)
        !object.respond_to?(:size)
      end
    end

    def each(&block)
      step(1, &block)
    end

    def element_type
      PAnyType::DEFAULT
    end

    def reverse_each(&block)
      # Default implementation cannot propagate reverse_each to a new enumerator so chained
      # calls must put reverse_each last.
      raise ArgumentError, 'reverse_each() is not implemented'
    end

    def step(step, &block)
      # Default implementation cannot propagate step to a new enumerator so chained
      # calls must put stepping last.
      raise ArgumentError, 'step() is not implemented'
    end

    def to_a
      raise Puppet::Error, 'Attempt to create an Array from an unbounded Iterable' if unbounded?
      super
    end

    def hash_style?
      false
    end

    def unbounded?
      true
    end
  end

  # @api private
  class Iterator
    # Note! We do not include Enumerable module here since that would make this class respond
    # in a bad way to all enumerable methods. We want to delegate all those calls directly to
    # the contained @enumeration
    include Iterable

    def initialize(element_type, enumeration)
      @element_type = element_type
      @enumeration = enumeration
    end

    def element_type
      @element_type
    end

    def size
      @enumeration.size
    end

    def respond_to_missing?(name, include_private)
      @enumeration.respond_to?(name, include_private)
    end

    def method_missing(name, *arguments, &block)
      @enumeration.send(name, *arguments, &block)
    end

    def next
      @enumeration.next
    end

    def map(*args, &block)
      @enumeration.map(*args, &block)
    end

    def reduce(*args, &block)
      @enumeration.reduce(*args, &block)
    end

    def all?(&block)
      @enumeration.all?(&block)
    end

    def any?(&block)
      @enumeration.any?(&block)
    end

    def step(step, &block)
      raise ArgumentError if step <= 0
      r = self
      r = r.step_iterator(step) if step > 1

      if block_given?
        begin
        if block.arity == 1
          loop { yield(r.next) }
        else
          loop { yield(*r.next) }
        end
        rescue StopIteration
        end
        self
      else
        r
      end
    end

    def reverse_each(&block)
      r = Iterator.new(@element_type, @enumeration.reverse_each)
      block_given? ? r.each(&block) : r
    end

    def step_iterator(step)
      StepIterator.new(@element_type, self, step)
    end

    def to_s
      et = element_type
      et.nil? ? 'Iterator-Value' : "Iterator[#{et.generalize}]-Value"
    end

    def unbounded?
      Iterable.unbounded?(@enumeration)
    end
  end

  # Special iterator used when iterating over hashes. Returns `true` for `#hash_style?` so that
  # it is possible to differentiate between two element arrays and key => value associations
  class HashIterator < Iterator
    def hash_style?
      true
    end
  end

  # @api private
  class StepIterator < Iterator
    include Enumerable

    def initialize(element_type, enumeration, step_size)
      super(element_type, enumeration)
      raise ArgumentError if step_size <= 0
      @step_size = step_size
    end

    def next
      result = @enumeration.next
      skip = @step_size - 1
      if skip > 0
        begin
          skip.times { @enumeration.next }
        rescue StopIteration
        end
      end
      result
    end

    def reverse_each(&block)
      r = Iterator.new(@element_type, to_a.reverse_each)
      block_given? ? r.each(&block) : r
    end

    def size
      super / @step_size
    end
  end

  # @api private
  class IntegerRangeIterator < Iterator
    include Enumerable

    def initialize(range, step = 1)
      raise ArgumentError if step == 0
      @range = range
      @step_size = step
      @current = (step < 0 ? range.to : range.from) - step
    end

    def element_type
      @range
    end

    def next
      value = @current + @step_size
      if @step_size < 0
        raise StopIteration if value < @range.from
      else
        raise StopIteration if value > @range.to
      end
      @current = value
    end

    def reverse_each(&block)
      r = IntegerRangeIterator.new(@range, -@step_size)
      block_given? ? r.each(&block) : r
    end

    def size
      (@range.to - @range.from) / @step_size.abs
    end

    def step_iterator(step)
      # The step iterator must use a range that has its logical end truncated at an even step boundary. This will
      # fulfil two objectives:
      # 1. The element_type method should not report excessive integers as possible numbers
      # 2. A reversed iterator must start at the correct number
      #
      range = @range
      step = @step_size * step
      mod = (range.to - range.from) % step
      if mod < 0
        range = PIntegerType.new(range.from - mod, range.to)
      elsif mod > 0
        range = PIntegerType.new(range.from, range.to - mod)
      end
      IntegerRangeIterator.new(range, step)
    end

    def unbounded?
      false
    end
  end
end