15793: Reuse result set even when each() is the only method called.
[arvados.git] / apps / workbench / app / models / arvados_resource_list.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 class ArvadosResourceList
6   include ArvadosApiClientHelper
7   include Enumerable
8
9   attr_reader :resource_class
10
11   def initialize resource_class=nil
12     @resource_class = resource_class
13     @fetch_multiple_pages = true
14     @arvados_api_token = Thread.current[:arvados_api_token]
15     @reader_tokens = Thread.current[:reader_tokens]
16     @results = nil
17     @count = nil
18     @offset = 0
19     @cond = nil
20     @eager = nil
21     @select = nil
22     @orderby_spec = nil
23     @filters = nil
24     @distinct = nil
25     @include_trash = nil
26     @limit = nil
27   end
28
29   def eager(bool=true)
30     @eager = bool
31     self
32   end
33
34   def distinct(bool=true)
35     @distinct = bool
36     self
37   end
38
39   def include_trash(option=nil)
40     @include_trash = option
41     self
42   end
43
44   def recursive(option=nil)
45     @recursive = option
46     self
47   end
48
49   def limit(max_results)
50     if not max_results.nil? and not max_results.is_a? Integer
51       raise ArgumentError("argument to limit() must be an Integer or nil")
52     end
53     @limit = max_results
54     self
55   end
56
57   def offset(skip)
58     @offset = skip
59     self
60   end
61
62   def order(orderby_spec)
63     @orderby_spec = orderby_spec
64     self
65   end
66
67   def select(columns=nil)
68     # If no column arguments were given, invoke Enumerable#select.
69     if columns.nil?
70       super()
71     else
72       @select ||= []
73       @select += columns
74       self
75     end
76   end
77
78   def filter _filters
79     @filters ||= []
80     @filters += _filters
81     self
82   end
83
84   def where(cond)
85     @cond = cond.dup
86     @cond.keys.each do |uuid_key|
87       if @cond[uuid_key] and (@cond[uuid_key].is_a? Array or
88                              @cond[uuid_key].is_a? ArvadosBase)
89         # Coerce cond[uuid_key] to an array of uuid strings.  This
90         # allows caller the convenience of passing an array of real
91         # objects and uuids in cond[uuid_key].
92         if !@cond[uuid_key].is_a? Array
93           @cond[uuid_key] = [@cond[uuid_key]]
94         end
95         @cond[uuid_key] = @cond[uuid_key].collect do |item|
96           if item.is_a? ArvadosBase
97             item.uuid
98           else
99             item
100           end
101         end
102       end
103     end
104     @cond.keys.select { |x| x.match(/_kind$/) }.each do |kind_key|
105       if @cond[kind_key].is_a? Class
106         @cond = @cond.merge({ kind_key => 'arvados#' + arvados_api_client.class_kind(@cond[kind_key]) })
107       end
108     end
109     self
110   end
111
112   # with_count sets the 'count' parameter to 'exact' or 'none' -- see
113   # https://doc.arvados.org/api/methods.html#index
114   def with_count(count_param='exact')
115     @count = count_param
116     self
117   end
118
119   def fetch_multiple_pages(f)
120     @fetch_multiple_pages = f
121     self
122   end
123
124   def results
125     if !@results
126       @results = []
127       self.each_page do |r|
128         @results.concat r
129       end
130     end
131     @results
132   end
133
134   def results=(r)
135     @results = r
136     @items_available = r.items_available if r.respond_to? :items_available
137     @result_limit = r.limit if r.respond_to? :limit
138     @result_offset = r.offset if r.respond_to? :offset
139     @results
140   end
141
142   def to_ary
143     results
144   end
145
146   def each(&block)
147     if not @results.nil?
148       @results.each(&block)
149     else
150       results = []
151       self.each_page do |items|
152         items.each do |i|
153           results << i
154           block.call i
155         end
156       end
157       # Cache results only if all were retrieved (block didn't raise
158       # an exception).
159       @results = results
160     end
161     self
162   end
163
164   def first
165     results.first
166   end
167
168   def last
169     results.last
170   end
171
172   def [](*x)
173     results.send('[]', *x)
174   end
175
176   def |(x)
177     if x.is_a? Hash
178       self.to_hash | x
179     else
180       results | x.to_ary
181     end
182   end
183
184   def to_hash
185     Hash[self.collect { |x| [x.uuid, x] }]
186   end
187
188   def empty?
189     self.first.nil?
190   end
191
192   def items_available
193     results
194     @items_available
195   end
196
197   def result_limit
198     results
199     @result_limit
200   end
201
202   def result_offset
203     results
204     @result_offset
205   end
206
207   # Obsolete method retained during api transition.
208   def links_for item_or_uuid, link_class=false
209     []
210   end
211
212   protected
213
214   def each_page
215     api_params = {
216       _method: 'GET'
217     }
218     api_params[:count] = @count if @count
219     api_params[:where] = @cond if @cond
220     api_params[:eager] = '1' if @eager
221     api_params[:select] = @select if @select
222     api_params[:order] = @orderby_spec if @orderby_spec
223     api_params[:filters] = @filters if @filters
224     api_params[:distinct] = @distinct if @distinct
225     api_params[:include_trash] = @include_trash if @include_trash
226     if @fetch_multiple_pages
227       # Default limit to (effectively) api server's MAX_LIMIT
228       api_params[:limit] = 2**(0.size*8 - 1) - 1
229     end
230
231     item_count = 0
232     offset = @offset || 0
233     @result_limit = nil
234     @result_offset = nil
235
236     begin
237       api_params[:offset] = offset
238       api_params[:limit] = (@limit - item_count) if @limit
239
240       res = arvados_api_client.api(@resource_class, '', api_params,
241                                    arvados_api_token: @arvados_api_token,
242                                    reader_tokens: @reader_tokens)
243       items = arvados_api_client.unpack_api_response res
244
245       @items_available = items.items_available if items.respond_to?(:items_available)
246       @result_limit = items.limit if (@fetch_multiple_pages == false) and items.respond_to?(:limit)
247       @result_offset = items.offset if (@fetch_multiple_pages == false) and items.respond_to?(:offset)
248
249       break if items.nil? or not items.any?
250
251       item_count += items.size
252       if items.respond_to?(:offset)
253         offset = items.offset + items.size
254       else
255         offset = item_count
256       end
257
258       yield items
259
260       break if @limit and item_count >= @limit
261       break if items.respond_to? :items_available and offset >= items.items_available
262     end while @fetch_multiple_pages
263     self
264   end
265
266 end