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