3410: Add default_collection_replication to config and discovery doc.
[arvados.git] / services / api / app / controllers / arvados / v1 / schema_controller.rb
1 class Arvados::V1::SchemaController < ApplicationController
2   skip_before_filter :find_objects_for_index
3   skip_before_filter :find_object_by_uuid
4   skip_before_filter :render_404_if_no_object
5   skip_before_filter :require_auth_scope
6
7   def index
8     expires_in 24.hours, public: true
9     discovery = Rails.cache.fetch 'arvados_v1_rest_discovery' do
10       Rails.application.eager_load!
11       discovery = {
12         kind: "discovery#restDescription",
13         discoveryVersion: "v1",
14         id: "arvados:v1",
15         name: "arvados",
16         version: "v1",
17         revision: "20131114",
18         source_version: (Rails.application.config.source_version ? Rails.application.config.source_version : "No version information available") + (Rails.application.config.local_modified ? Rails.application.config.local_modified.to_s : ''),
19         generatedAt: Time.now.iso8601,
20         title: "Arvados API",
21         description: "The API to interact with Arvados.",
22         documentationLink: "http://doc.arvados.org/api/index.html",
23         defaultCollectionReplication: Rails.configuration.default_collection_replication,
24         protocol: "rest",
25         baseUrl: root_url + "arvados/v1/",
26         basePath: "/arvados/v1/",
27         rootUrl: root_url,
28         servicePath: "arvados/v1/",
29         batchPath: "batch",
30         defaultTrashLifetime: Rails.application.config.default_trash_lifetime,
31         parameters: {
32           alt: {
33             type: "string",
34             description: "Data format for the response.",
35             default: "json",
36             enum: [
37                    "json"
38                   ],
39             enumDescriptions: [
40                                "Responses with Content-Type of application/json"
41                               ],
42             location: "query"
43           },
44           fields: {
45             type: "string",
46             description: "Selector specifying which fields to include in a partial response.",
47             location: "query"
48           },
49           key: {
50             type: "string",
51             description: "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.",
52             location: "query"
53           },
54           oauth_token: {
55             type: "string",
56             description: "OAuth 2.0 token for the current user.",
57             location: "query"
58           }
59         },
60         auth: {
61           oauth2: {
62             scopes: {
63               "https://api.curoverse.com/auth/arvados" => {
64                 description: "View and manage objects"
65               },
66               "https://api.curoverse.com/auth/arvados.readonly" => {
67                 description: "View objects"
68               }
69             }
70           }
71         },
72         schemas: {},
73         resources: {}
74       }
75
76       if Rails.application.config.websocket_address
77         discovery[:websocketUrl] = Rails.application.config.websocket_address
78       elsif ENV['ARVADOS_WEBSOCKETS']
79         discovery[:websocketUrl] = (root_url.sub /^http/, 'ws') + "websocket"
80       end
81
82       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
83         begin
84           ctl_class = "Arvados::V1::#{k.to_s.pluralize}Controller".constantize
85         rescue
86           # No controller -> no discovery.
87           next
88         end
89         object_properties = {}
90         k.columns.
91           select { |col| col.name != 'id' }.
92           collect do |col|
93           if k.serialized_attributes.has_key? col.name
94             object_properties[col.name] = {
95               type: k.serialized_attributes[col.name].object_class.to_s
96             }
97           else
98             object_properties[col.name] = {
99               type: col.type
100             }
101           end
102         end
103         discovery[:schemas][k.to_s + 'List'] = {
104           id: k.to_s + 'List',
105           description: k.to_s + ' list',
106           type: "object",
107           properties: {
108             kind: {
109               type: "string",
110               description: "Object type. Always arvados##{k.to_s.camelcase(:lower)}List.",
111               default: "arvados##{k.to_s.camelcase(:lower)}List"
112             },
113             etag: {
114               type: "string",
115               description: "List version."
116             },
117             items: {
118               type: "array",
119               description: "The list of #{k.to_s.pluralize}.",
120               items: {
121                 "$ref" => k.to_s
122               }
123             },
124             next_link: {
125               type: "string",
126               description: "A link to the next page of #{k.to_s.pluralize}."
127             },
128             next_page_token: {
129               type: "string",
130               description: "The page token for the next page of #{k.to_s.pluralize}."
131             },
132             selfLink: {
133               type: "string",
134               description: "A link back to this list."
135             }
136           }
137         }
138         discovery[:schemas][k.to_s] = {
139           id: k.to_s,
140           description: k.to_s,
141           type: "object",
142           uuidPrefix: (k.respond_to?(:uuid_prefix) ? k.uuid_prefix : nil),
143           properties: {
144             uuid: {
145               type: "string",
146               description: "Object ID."
147             },
148             etag: {
149               type: "string",
150               description: "Object version."
151             }
152           }.merge(object_properties)
153         }
154         discovery[:resources][k.to_s.underscore.pluralize] = {
155           methods: {
156             get: {
157               id: "arvados.#{k.to_s.underscore.pluralize}.get",
158               path: "#{k.to_s.underscore.pluralize}/{uuid}",
159               httpMethod: "GET",
160               description: "Gets a #{k.to_s}'s metadata by UUID.",
161               parameters: {
162                 uuid: {
163                   type: "string",
164                   description: "The UUID of the #{k.to_s} in question.",
165                   required: true,
166                   location: "path"
167                 }
168               },
169               parameterOrder: [
170                                "uuid"
171                               ],
172               response: {
173                 "$ref" => k.to_s
174               },
175               scopes: [
176                        "https://api.curoverse.com/auth/arvados",
177                        "https://api.curoverse.com/auth/arvados.readonly"
178                       ]
179             },
180             list: {
181               id: "arvados.#{k.to_s.underscore.pluralize}.list",
182               path: k.to_s.underscore.pluralize,
183               httpMethod: "GET",
184               description:
185                  %|List #{k.to_s.pluralize}.
186
187                    The <code>list</code> method returns a
188                    <a href="/api/resources.html">resource list</a> of
189                    matching #{k.to_s.pluralize}. For example:
190
191                    <pre>
192                    {
193                     "kind":"arvados##{k.to_s.camelcase(:lower)}List",
194                     "etag":"",
195                     "self_link":"",
196                     "next_page_token":"",
197                     "next_link":"",
198                     "items":[
199                        ...
200                     ],
201                     "items_available":745,
202                     "_profile":{
203                      "request_time":0.157236317
204                     }
205                     </pre>|,
206               parameters: {
207                 limit: {
208                   type: "integer",
209                   description: "Maximum number of #{k.to_s.underscore.pluralize} to return.",
210                   default: "100",
211                   format: "int32",
212                   minimum: "0",
213                   location: "query",
214                 },
215                 offset: {
216                   type: "integer",
217                   description: "Number of #{k.to_s.underscore.pluralize} to skip before first returned record.",
218                   default: "0",
219                   format: "int32",
220                   minimum: "0",
221                   location: "query",
222                   },
223                 filters: {
224                   type: "array",
225                   description: "Conditions for filtering #{k.to_s.underscore.pluralize}.",
226                   location: "query"
227                 },
228                 where: {
229                   type: "object",
230                   description: "Conditions for filtering #{k.to_s.underscore.pluralize}. (Deprecated. Use filters instead.)",
231                   location: "query"
232                 },
233                 order: {
234                   type: "string",
235                   description: "Order in which to return matching #{k.to_s.underscore.pluralize}.",
236                   location: "query"
237                 },
238                 select: {
239                   type: "array",
240                   description: "Select which fields to return",
241                   location: "query"
242                 },
243                 distinct: {
244                   type: "boolean",
245                   description: "Return each distinct object",
246                   location: "query"
247                 }
248               },
249               response: {
250                 "$ref" => "#{k.to_s}List"
251               },
252               scopes: [
253                        "https://api.curoverse.com/auth/arvados",
254                        "https://api.curoverse.com/auth/arvados.readonly"
255                       ]
256             },
257             create: {
258               id: "arvados.#{k.to_s.underscore.pluralize}.create",
259               path: "#{k.to_s.underscore.pluralize}",
260               httpMethod: "POST",
261               description: "Create a new #{k.to_s}.",
262               parameters: {},
263               request: {
264                 required: true,
265                 properties: {
266                   k.to_s.underscore => {
267                     "$ref" => k.to_s
268                   }
269                 }
270               },
271               response: {
272                 "$ref" => k.to_s
273               },
274               scopes: [
275                        "https://api.curoverse.com/auth/arvados"
276                       ]
277             },
278             update: {
279               id: "arvados.#{k.to_s.underscore.pluralize}.update",
280               path: "#{k.to_s.underscore.pluralize}/{uuid}",
281               httpMethod: "PUT",
282               description: "Update attributes of an existing #{k.to_s}.",
283               parameters: {
284                 uuid: {
285                   type: "string",
286                   description: "The UUID of the #{k.to_s} in question.",
287                   required: true,
288                   location: "path"
289                 }
290               },
291               request: {
292                 required: true,
293                 properties: {
294                   k.to_s.underscore => {
295                     "$ref" => k.to_s
296                   }
297                 }
298               },
299               response: {
300                 "$ref" => k.to_s
301               },
302               scopes: [
303                        "https://api.curoverse.com/auth/arvados"
304                       ]
305             },
306             delete: {
307               id: "arvados.#{k.to_s.underscore.pluralize}.delete",
308               path: "#{k.to_s.underscore.pluralize}/{uuid}",
309               httpMethod: "DELETE",
310               description: "Delete an existing #{k.to_s}.",
311               parameters: {
312                 uuid: {
313                   type: "string",
314                   description: "The UUID of the #{k.to_s} in question.",
315                   required: true,
316                   location: "path"
317                 }
318               },
319               response: {
320                 "$ref" => k.to_s
321               },
322               scopes: [
323                        "https://api.curoverse.com/auth/arvados"
324                       ]
325             }
326           }
327         }
328         # Check for Rails routes that don't match the usual actions
329         # listed above
330         d_methods = discovery[:resources][k.to_s.underscore.pluralize][:methods]
331         Rails.application.routes.routes.each do |route|
332           action = route.defaults[:action]
333           httpMethod = ['GET', 'POST', 'PUT', 'DELETE'].map { |method|
334             method if route.verb.match(method)
335           }.compact.first
336           if httpMethod and
337               route.defaults[:controller] == 'arvados/v1/' + k.to_s.underscore.pluralize and
338               ctl_class.action_methods.include? action
339             if !d_methods[action.to_sym]
340               method = {
341                 id: "arvados.#{k.to_s.underscore.pluralize}.#{action}",
342                 path: route.path.spec.to_s.sub('/arvados/v1/','').sub('(.:format)','').sub(/:(uu)?id/,'{uuid}'),
343                 httpMethod: httpMethod,
344                 description: "#{action} #{k.to_s.underscore.pluralize}",
345                 parameters: {},
346                 response: {
347                   "$ref" => (action == 'index' ? "#{k.to_s}List" : k.to_s)
348                 },
349                 scopes: [
350                          "https://api.curoverse.com/auth/arvados"
351                         ]
352               }
353               route.segment_keys.each do |key|
354                 if key != :format
355                   key = :uuid if key == :id
356                   method[:parameters][key] = {
357                     type: "string",
358                     description: "",
359                     required: true,
360                     location: "path"
361                   }
362                 end
363               end
364             else
365               # We already built a generic method description, but we
366               # might find some more required parameters through
367               # introspection.
368               method = d_methods[action.to_sym]
369             end
370             if ctl_class.respond_to? "_#{action}_requires_parameters".to_sym
371               ctl_class.send("_#{action}_requires_parameters".to_sym).each do |k, v|
372                 if v.is_a? Hash
373                   method[:parameters][k] = v
374                 else
375                   method[:parameters][k] = {}
376                 end
377                 if !method[:parameters][k][:default].nil?
378                   # The JAVA SDK is sensitive to all values being strings
379                   method[:parameters][k][:default] = method[:parameters][k][:default].to_s
380                 end
381                 method[:parameters][k][:type] ||= 'string'
382                 method[:parameters][k][:description] ||= ''
383                 method[:parameters][k][:location] = (route.segment_keys.include?(k) ? 'path' : 'query')
384                 if method[:parameters][k][:required].nil?
385                   method[:parameters][k][:required] = v != false
386                 end
387               end
388             end
389             d_methods[action.to_sym] = method
390           end
391         end
392       end
393       discovery
394     end
395     send_json discovery
396   end
397 end