Reorganized some of the code and removed unnecessary stuff.
[arvados.git] / lib / google / api_client / discovery.rb
1 # Copyright 2010 Google Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #      http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 require 'json'
16 require 'addressable/uri'
17 require 'addressable/template'
18 require 'extlib/inflection'
19
20 module Google #:nodoc:
21   class APIClient #:nodoc:
22     class ValidationError < StandardError
23     end
24
25     class Service
26       def initialize(service_name, service_version, service_description)
27         @name = service_name
28         @version = service_version
29         @description = service_description
30         metaclass = (class <<self; self; end)
31         self.resources.each do |resource|
32           method_name = Extlib::Inflection.underscore(resource.name).to_sym
33           if !self.respond_to?(method_name)
34             metaclass.send(:define_method, method_name) { resource }
35           end
36         end
37         self.methods.each do |method|
38           method_name = Extlib::Inflection.underscore(method.name).to_sym
39           if !self.respond_to?(method_name)
40             metaclass.send(:define_method, method_name) { method }
41           end
42         end
43       end
44
45       attr_reader :name, :version, :description
46
47       def base
48         return @base ||= Addressable::URI.parse(self.description['baseUrl'])
49       end
50
51       def resources
52         return @resources ||= (
53           (self.description['resources'] || []).inject([]) do |accu, (k, v)|
54             accu << ::Google::APIClient::Resource.new(self.base, k, v)
55             accu
56           end
57         )
58       end
59
60       def methods
61         return @methods ||= (
62           (self.description['methods'] || []).inject([]) do |accu, (k, v)|
63             accu << ::Google::APIClient::Method.new(self.base, k, v)
64             accu
65           end
66         )
67       end
68
69       def to_h
70         return @hash ||= (begin
71           methods_hash = {}
72           self.methods.each do |method|
73             methods_hash[method.rpc_name] = method
74           end
75           self.resources.each do |resource|
76             methods_hash.merge!(resource.to_h)
77           end
78           methods_hash
79         end)
80       end
81
82       ##
83       # Returns a <code>String</code> representation of the service's state.
84       #
85       # @return [String] The service's state, as a <code>String</code>.
86       def inspect
87         sprintf(
88           "#<%s:%#0x NAME:%s>", self.class.to_s, self.object_id, self.name
89         )
90       end
91     end
92
93     class Resource
94       def initialize(base, resource_name, resource_description)
95         @base = base
96         @name = resource_name
97         @description = resource_description
98         metaclass = (class <<self; self; end)
99         self.resources.each do |resource|
100           method_name = Extlib::Inflection.underscore(resource.name).to_sym
101           if !self.respond_to?(method_name)
102             metaclass.send(:define_method, method_name) { resource }
103           end
104         end
105         self.methods.each do |method|
106           method_name = Extlib::Inflection.underscore(method.name).to_sym
107           if !self.respond_to?(method_name)
108             metaclass.send(:define_method, method_name) { method }
109           end
110         end
111       end
112
113       attr_reader :name, :description, :base
114
115       def resources
116         return @resources ||= (
117           (self.description['resources'] || []).inject([]) do |accu, (k, v)|
118             accu << ::Google::APIClient::Resource.new(self.base, k, v)
119             accu
120           end
121         )
122       end
123
124       def methods
125         return @methods ||= (
126           (self.description['methods'] || []).inject([]) do |accu, (k, v)|
127             accu << ::Google::APIClient::Method.new(self.base, k, v)
128             accu
129           end
130         )
131       end
132
133       def to_h
134         return @hash ||= (begin
135           methods_hash = {}
136           self.methods.each do |method|
137             methods_hash[method.rpc_name] = method
138           end
139           self.resources.each do |resource|
140             methods_hash.merge!(resource.to_h)
141           end
142           methods_hash
143         end)
144       end
145
146       ##
147       # Returns a <code>String</code> representation of the resource's state.
148       #
149       # @return [String] The resource's state, as a <code>String</code>.
150       def inspect
151         sprintf(
152           "#<%s:%#0x NAME:%s>", self.class.to_s, self.object_id, self.name
153         )
154       end
155     end
156
157     class Method
158       def initialize(base, method_name, method_description)
159         @base = base
160         @name = method_name
161         @description = method_description
162       end
163
164       attr_reader :name, :description, :base
165
166       def rpc_name
167         return self.description['rpcName']
168       end
169
170       def uri_template
171         return @uri_template ||=
172           Addressable::Template.new(base + self.description['pathUrl'])
173       end
174
175       def normalize_parameters(parameters={})
176         # Convert keys to Strings when appropriate
177         if parameters.kind_of?(Hash) || parameters.kind_of?(Array)
178           parameters = parameters.inject({}) do |accu, (k, v)|
179             k = k.to_s if k.kind_of?(Symbol)
180             k = k.to_str if k.respond_to?(:to_str)
181             unless k.kind_of?(String)
182               raise TypeError, "Expected String, got #{k.class}."
183             end
184             accu[k] = v
185             accu
186           end
187         else
188           raise TypeError,
189             "Expected Hash or Array, got #{parameters.class}."
190         end
191         return parameters
192       end
193
194       def generate_uri(parameters={})
195         parameters = self.normalize_parameters(parameters)
196         self.validate_parameters(parameters)
197         template_variables = self.uri_template.variables
198         uri = self.uri_template.expand(parameters)
199         query_parameters = parameters.reject do |k, v|
200           template_variables.include?(k)
201         end
202         if query_parameters.size > 0
203           uri.query_values = (uri.query_values || {}).merge(query_parameters)
204         end
205         # Normalization is necessary because of undesirable percent-escaping
206         # during URI template expansion
207         return uri.normalize
208       end
209
210       def generate_request(parameters={}, body='', headers=[])
211         method = self.description['httpMethod'] || 'GET'
212         uri = self.generate_uri(parameters)
213         return [method, uri.to_str, headers, [body]]
214       end
215
216       def validate_parameters(parameters={})
217         parameters = self.normalize_parameters(parameters)
218         parameter_description = self.description['parameters'] || {}
219         required_variables = Hash[parameter_description.select do |k, v|
220           v['required']
221         end].keys
222         missing_variables = required_variables - parameters.keys
223         if missing_variables.size > 0
224           raise ArgumentError,
225             "Missing required parameters: #{missing_variables.join(', ')}."
226         end
227         parameters.each do |k, v|
228           if parameter_description[k]
229             pattern = parameter_description[k]['pattern']
230             if pattern
231               regexp = Regexp.new("^#{pattern}$")
232               if v !~ regexp
233                 raise ArgumentError,
234                   "Parameter '#{k}' has an invalid value: #{v}. " +
235                   "Must match: /^#{pattern}$/."
236               end
237             end
238           end
239         end
240         return nil
241       end
242
243       ##
244       # Returns a <code>String</code> representation of the method's state.
245       #
246       # @return [String] The method's state, as a <code>String</code>.
247       def inspect
248         sprintf(
249           "#<%s:%#0x NAME:%s>", self.class.to_s, self.object_id, self.rpc_name
250         )
251       end
252     end
253   end
254 end