angus-remote 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/lib/angus-remote.rb +4 -0
  2. data/lib/angus/remote/builder.rb +204 -0
  3. data/lib/angus/remote/client.rb +79 -0
  4. data/lib/angus/remote/exceptions.rb +50 -0
  5. data/lib/angus/remote/http/multipart.rb +54 -0
  6. data/lib/angus/remote/http/multipart_methods/multipart_base.rb +36 -0
  7. data/lib/angus/remote/http/multipart_methods/multipart_post.rb +11 -0
  8. data/lib/angus/remote/http/multipart_methods/multipart_put.rb +11 -0
  9. data/lib/angus/remote/http/query_params.rb +53 -0
  10. data/lib/angus/remote/message.rb +14 -0
  11. data/lib/angus/remote/proxy_client.rb +58 -0
  12. data/lib/angus/remote/proxy_client_utils.rb +70 -0
  13. data/lib/angus/remote/remote_response.rb +44 -0
  14. data/lib/angus/remote/representation.rb +18 -0
  15. data/lib/angus/remote/response/builder.rb +308 -0
  16. data/lib/angus/remote/response/hash.rb +47 -0
  17. data/lib/angus/remote/response/serializer.rb +43 -0
  18. data/lib/angus/remote/service_directory.rb +217 -0
  19. data/lib/angus/remote/utils.rb +119 -0
  20. data/lib/angus/remote/version.rb +4 -2
  21. data/lib/angus/unmarshalling.rb +33 -0
  22. data/spec/angus/remote/builder_spec.rb +105 -0
  23. data/spec/angus/remote/client_spec.rb +75 -0
  24. data/spec/angus/remote/http/multipart_methods/multipart_base_spec.rb +36 -0
  25. data/spec/angus/remote/http/multipart_spec.rb +120 -0
  26. data/spec/angus/remote/http/query_params_spec.rb +28 -0
  27. data/spec/angus/remote/proxy_client_utils_spec.rb +102 -0
  28. data/spec/angus/remote/response/builder_spec.rb +69 -0
  29. data/spec/angus/remote/service_directory_spec.rb +76 -0
  30. data/spec/angus/remote/utils_spec.rb +204 -0
  31. metadata +192 -32
  32. data/.gitignore +0 -17
  33. data/Gemfile +0 -4
  34. data/LICENSE.txt +0 -22
  35. data/README.md +0 -29
  36. data/Rakefile +0 -1
  37. data/angus-remote.gemspec +0 -23
  38. data/lib/angus/remote.rb +0 -7
@@ -0,0 +1,53 @@
1
+ module Http
2
+ module QueryParams
3
+
4
+ # @return <String> This hash as a query string
5
+ #
6
+ # @example
7
+ # { :name => "Bob",
8
+ # :address => {
9
+ # :street => '111 Ruby Ave.',
10
+ # :city => 'Ruby Central',
11
+ # :phones => ['111-111-1111', '222-222-2222']
12
+ # }
13
+ # }.to_params
14
+ # #=> "name=Bob&address[city]=Ruby Central&address[phones][]=111-111-1111&address[phones][]=222-222-2222&address[street]=111 Ruby Ave."
15
+ def self.to_params(hash)
16
+ params = hash.map { |k,v| normalize_param(k,v) }.join
17
+ params.chop! # trailing &
18
+ params
19
+ end
20
+
21
+ # @param key<Object> The key for the param.
22
+ # @param value<Object> The value for the param.
23
+ #
24
+ # @return <String> This key value pair as a param
25
+ #
26
+ # @example normalize_param(:name, "Bob Jones") #=> "name=Bob%20Jones&"
27
+ def self.normalize_param(key, value)
28
+ param = ''
29
+ stack = []
30
+
31
+ if value.is_a?(Array)
32
+ param << value.map { |element| normalize_param("#{key}[]", element) }.join
33
+ elsif value.is_a?(Hash)
34
+ stack << [key,value]
35
+ else
36
+ param << "#{key}=#{URI.encode(value.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))}&"
37
+ end
38
+
39
+ stack.each do |parent, hash|
40
+ hash.each do |k, v|
41
+ if v.is_a?(Hash)
42
+ stack << ["#{parent}[#{k}]", v]
43
+ else
44
+ param << normalize_param("#{parent}[#{k}]", v)
45
+ end
46
+ end
47
+ end
48
+
49
+ param
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,14 @@
1
+ module Angus
2
+ module Remote
3
+
4
+ # A message returned by the service's response
5
+ class Message
6
+
7
+ attr_accessor :description
8
+ attr_accessor :key
9
+ attr_accessor :level
10
+
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,58 @@
1
+ require 'json'
2
+ require 'persistent_http'
3
+
4
+ require_relative 'exceptions'
5
+ require_relative 'proxy_client_utils'
6
+
7
+ module Angus
8
+ module Remote
9
+
10
+ # A client for service invocation when proxing requests.
11
+ class ProxyClient
12
+
13
+ DEFAULT_TIMEOUT = 10
14
+
15
+ def initialize(url, timeout = DEFAULT_TIMEOUT)
16
+ url = url[0..-2] if url[-1] == '/'
17
+
18
+ @connection = PersistentHTTP.new(
19
+ :pool_size => 4,
20
+ :pool_timeout => 10,
21
+ :warn_timeout => 0.25,
22
+ :force_retry => false,
23
+ :url => url,
24
+
25
+ :read_timeout => timeout,
26
+ :open_timeout => timeout
27
+ )
28
+
29
+ @api_base_path = @connection.default_path
30
+ end
31
+
32
+ # Makes a request to the service
33
+ #
34
+ def make_request(method, path, query, headers = {}, body = nil)
35
+ full_path = @api_base_path + path
36
+
37
+ request = ProxyClientUtils.build_request(method, full_path, query, headers, body)
38
+
39
+ begin
40
+ response = @connection.request(request)
41
+
42
+ from_headers = ProxyClientUtils.normalize_headers(
43
+ ProxyClientUtils.filter_response_headers(response.to_hash)
44
+ )
45
+
46
+ [response.code.to_i, from_headers, [response.body]]
47
+ rescue Errno::ECONNREFUSED
48
+ raise RemoteConnectionError.new(self.class.base_uri)
49
+ end
50
+ end
51
+
52
+ def to_s
53
+ "#<#{self.class}:#{object_id}>"
54
+ end
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,70 @@
1
+ require 'uri'
2
+ require 'json'
3
+
4
+ require_relative 'exceptions'
5
+ require_relative 'http/query_params'
6
+
7
+ module Angus
8
+ module Remote
9
+
10
+ module ProxyClientUtils
11
+
12
+ ALLOWED_RESPONSE_HEADERS = ['content-type']
13
+
14
+ def self.build_request(method, path, query, headers = {}, body = nil)
15
+ uri = URI(path)
16
+ uri.query = query
17
+
18
+ full_uri = uri.to_s
19
+
20
+ request = case method.to_s.downcase
21
+ when 'get'
22
+ Net::HTTP::Get.new(full_uri)
23
+ when 'post'
24
+ Net::HTTP::Post.new(full_uri)
25
+ when 'put'
26
+ Net::HTTP::Put.new(full_uri)
27
+ when 'delete'
28
+ Net::HTTP::Delete.new(full_uri)
29
+ else
30
+ raise MethodArgumentError.new(method)
31
+ end
32
+
33
+ headers.each do |k, v|
34
+ request[k] = v
35
+ end
36
+
37
+ request.body = body
38
+
39
+ request
40
+ end
41
+
42
+ def self.filter_response_headers(headers)
43
+ headers.select { |h, v| ALLOWED_RESPONSE_HEADERS.include?(h) }
44
+ end
45
+
46
+ # Converts any header value that is an array to its first value.
47
+ #
48
+ # @param [Hash] header
49
+ #
50
+ # @return [Hash]
51
+ #
52
+ # @example
53
+ # normalize_headers({'content-type'=>['application/json;charset=utf-8']})
54
+ #
55
+ # -> {'content-type'=>'application/json;charset=utf-8'}
56
+ def self.normalize_headers(headers)
57
+ normalized = headers.map do |h, v|
58
+ if v.is_a?(Array)
59
+ [h, v.first]
60
+ else
61
+ [h, v]
62
+ end
63
+ end
64
+
65
+ Hash[normalized]
66
+ end
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,44 @@
1
+ require_relative 'response/hash'
2
+
3
+ module Angus
4
+ module Remote
5
+
6
+ # A service's response
7
+ #
8
+ # Acts as an array to store information at HTTP level, like the status_code
9
+ class RemoteResponse
10
+ include Angus::Remote::Response::Hash
11
+
12
+ attr_accessor :status
13
+ attr_accessor :status_code
14
+ attr_accessor :messages
15
+
16
+ def initialize
17
+ @http_response_info = {}
18
+ end
19
+
20
+ def []=(key, value)
21
+ @http_response_info[key] = value
22
+ end
23
+
24
+ def [](key)
25
+ @http_response_info[key]
26
+ end
27
+
28
+ def to_s
29
+ "#<#{self.class}:#{object_id}>"
30
+ end
31
+
32
+ def to_hash
33
+ {
34
+ :http_status_code => @http_response_info[:status_code],
35
+ :body => @http_response_info[:body],
36
+ :service_name => @http_response_info[:service_name],
37
+ :operation_name => @http_response_info[:operation_name],
38
+ }
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,18 @@
1
+ require_relative 'response/hash'
2
+
3
+ module Angus
4
+ module Remote
5
+
6
+ class Representation
7
+ include Angus::Remote::Response::Hash
8
+
9
+ attr_accessor :elements
10
+
11
+ def initialize
12
+ @http_response_info = {}
13
+ @elements = {}
14
+ end
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,308 @@
1
+ require 'bigdecimal'
2
+ require 'date'
3
+ require 'json'
4
+
5
+ require_relative '../message'
6
+ require_relative '../remote_response'
7
+
8
+ require_relative 'hash'
9
+
10
+ require_relative '../service_directory'
11
+
12
+
13
+ # TODO: move to another gem and possibly change its name
14
+ require_relative '../../unmarshalling'
15
+
16
+ module Angus
17
+ module Remote
18
+ module Response
19
+
20
+ module Builder
21
+
22
+ # Builds a Response
23
+ #
24
+ # The r parameter should contain in the body, encoded as json, the values / objects
25
+ # specified in the operation response metadata
26
+ #
27
+ # @param [Integer] status_code HTTP status_code
28
+ # @param body [String] HTTP body
29
+ # @param service_name [String] Name of the service that the response belongs to
30
+ # @param version [String] Version of the service that the response belongs to
31
+ # @param operation_namespace [String] Namespace of the operation that the response belongs to
32
+ # @param operation_name [String] Name of the operation that the response belongs to
33
+ #
34
+ # @return A Response object that responds to the methods:
35
+ # - status
36
+ # - messages
37
+ #
38
+ # Also, provides one method for each value / object / array returned
39
+ def self.build(status_code, body, service_code_name, version, operation_namespace,
40
+ operation_code_name)
41
+ service_definition = Angus::Remote::ServiceDirectory.service_definition(
42
+ service_code_name, version
43
+ )
44
+
45
+ representations = service_definition.representations
46
+ glossary = service_definition.glossary
47
+
48
+ operation_definition = service_definition.operation_definition(operation_namespace,
49
+ operation_code_name)
50
+
51
+ json_response = JSON(body)
52
+
53
+ response_class = build_response_class(operation_definition.name)
54
+
55
+ response = response_class.new
56
+
57
+ # TODO use constants
58
+ response[:status_code] = status_code
59
+ response[:body] = body
60
+ response[:service_code_name] = service_code_name
61
+ response[:service_version] = version
62
+ response[:operation_namespace] = operation_namespace
63
+ response[:operation_code_name] = operation_code_name
64
+
65
+ response.status = json_response['status']
66
+ response.messages = self.build_messages(json_response['messages'])
67
+
68
+ representations_hash = self.representations_hash(representations)
69
+ glossary_terms_hash = glossary.terms_hash
70
+
71
+ operation_definition.response_elements.each do |element|
72
+ self.build_response_method(json_response, response_class, response,
73
+ representations_hash, glossary_terms_hash, element)
74
+ end
75
+
76
+ response
77
+ end
78
+
79
+ # Builds the methods for each value / object / array
80
+ #
81
+ # The r parameter should contain in the body, encoded as json, the values / objects
82
+ # specified in the operation response metadata
83
+ def self.build_response_method(json_response, response_class, response,
84
+ representations_hash, glossary_terms_hash, element)
85
+ if (json_response.has_key?(element.name))
86
+ hash_value = json_response[element.name]
87
+ elsif (element.required == false)
88
+ hash_value = element.default
89
+ else
90
+ return
91
+ end
92
+
93
+ object_value = nil
94
+
95
+ if element.type && representations_hash.include?(element.type)
96
+ object_value = self.build_from_representation(hash_value, element.type,
97
+ representations_hash, glossary_terms_hash)
98
+ elsif element.elements_type
99
+ object_value = self.build_collection_from_representation(hash_value,
100
+ element.elements_type,
101
+ representations_hash,
102
+ glossary_terms_hash)
103
+ elsif element.type && element.type.to_sym == :variable
104
+ object_value = self.build_from_variable_fields(hash_value)
105
+ elsif element.type
106
+ begin
107
+ object_value = Angus::Unmarshalling.unmarshal_scalar(hash_value,
108
+ element.type.to_sym)
109
+ rescue ArgumentError
110
+ object_value = nil
111
+ end
112
+ end
113
+
114
+ # Don't apply the glossary to response elements
115
+ response.elements[element.name] = object_value
116
+
117
+ # Instead, apply the glossary to method names
118
+ getter_method_name = self.apply_glossary(element.name, glossary_terms_hash)
119
+
120
+ response_class.send :define_method, getter_method_name do
121
+ object_value
122
+ end
123
+ end
124
+
125
+ # Builds a Response based on a service's response
126
+ #
127
+ # The remote_response parameter should contain in the body,
128
+ # encoded as json, the values / objects
129
+ # specified in the operation response metadata.
130
+ #
131
+ # @param [Http] remote_response HTTP response object, must respond to methods :body and :code
132
+ # @param [String] service_name Name of the invoked service
133
+ # @param version [String] Version of the invoked service
134
+ # @param operation_namespace [String] Namespace of the invoked operation
135
+ # @param operation_name [String] Name of the invoked operation
136
+ #
137
+ # @return (see #build)
138
+ def self.build_from_remote_response(remote_response, service_code_name, version,
139
+ operation_namespace, operation_code_name)
140
+
141
+ status_code = remote_response.code
142
+ body = remote_response.body
143
+
144
+ self.build(status_code, body, service_code_name, version, operation_namespace,
145
+ operation_code_name)
146
+ end
147
+
148
+ # Searches for a short name in the glossary and returns the corresponding long name
149
+ #
150
+ # If name is not a short name, returns the name
151
+ #
152
+ # @param [String] name
153
+ # @param [Hash<String, GlossaryTerm>] glossary_terms_hash, where keys are short names
154
+ #
155
+ # @return long name
156
+ def self.apply_glossary(name, glossary_terms_hash)
157
+ if glossary_terms_hash.include?(name)
158
+ name = glossary_terms_hash[name].long_name
159
+ end
160
+
161
+ return name
162
+ end
163
+
164
+ # Build a response class for the operation
165
+ #
166
+ # @param [String] operation_name the name of the operation
167
+ #
168
+ # @return [Class] A class client, that inherits from {Angus::Remote::Response}
169
+ def self.build_response_class(operation_name)
170
+ response_class = Class.new(Angus::Remote::RemoteResponse)
171
+ response_class.class_eval <<-END
172
+ def self.name
173
+ "#<Response_#{operation_name.gsub(' ', '_')}>"
174
+ end
175
+
176
+ def self.to_s
177
+ name
178
+ end
179
+ END
180
+
181
+ response_class
182
+ end
183
+
184
+ # Receives an array of messages and returns an array of Message objects
185
+ def self.build_messages(messages)
186
+ messages ||= []
187
+
188
+ messages.map do |m|
189
+ message = Angus::Remote::Message.new
190
+ message.description = m['dsc']
191
+ message.key = m['key']
192
+ message.level = m['level']
193
+
194
+ message
195
+ end
196
+ end
197
+
198
+ # Receives a hash, a type and an array of representations and
199
+ # build an object that has one method for each attribute of the type.
200
+ def self.build_from_representation(hash_value, type, representations, glossary_terms_hash)
201
+ return nil if hash_value.nil?
202
+
203
+ representation_class = Class.new do
204
+ include Angus::Remote::Response::Hash
205
+ end
206
+
207
+ representation_object = representation_class.new
208
+
209
+ if representations.include?(type)
210
+ representation = representations[type]
211
+ return nil if representation.nil?
212
+ representation.fields.each do |field|
213
+ field_raw_value = hash_value[field.name]
214
+ field_value = nil
215
+ unless field_raw_value.nil? && field.required == false
216
+ if field.type && representations.include?(field.type)
217
+ field_value = self.build_from_representation(field_raw_value, field.type,
218
+ representations,
219
+ glossary_terms_hash)
220
+ elsif field.elements_type
221
+ field_value = self.build_collection_from_representation(field_raw_value,
222
+ field.elements_type,
223
+ representations,
224
+ glossary_terms_hash)
225
+ elsif field.type && field.type.to_sym == :variable
226
+ field_value = self.build_from_variable_fields(field_raw_value)
227
+ elsif field.type
228
+ field_value = Angus::Unmarshalling.unmarshal_scalar(field_raw_value,
229
+ field.type.to_sym)
230
+ end
231
+ end
232
+
233
+ # Don't apply the glossary to response elements
234
+ representation_object.elements[field.name] = field_value
235
+
236
+ # Instead, apply the glossary to method names
237
+ getter_method_name = self.apply_glossary(field.name, glossary_terms_hash)
238
+
239
+ representation_class.send :define_method, field.name.to_sym do
240
+ field_value
241
+ end
242
+ end
243
+ else
244
+ if type.to_sym == :variable
245
+ representation_object = self.build_from_variable_fields(hash_value)
246
+ else
247
+ begin
248
+ representation_object = Angus::Unmarshalling.unmarshal_scalar(hash_value,
249
+ type.to_sym)
250
+ rescue ArgumentError
251
+ representation_object = nil
252
+ end
253
+ end
254
+ end
255
+
256
+
257
+ return representation_object
258
+ end
259
+
260
+ # Builds an array of objects that corresponds to the received type
261
+ def self.build_collection_from_representation(value_array, type, representations,
262
+ glossary_terms_hash)
263
+ collection = []
264
+
265
+ value_array.each do |raw_value|
266
+ collection << build_from_representation(raw_value, type, representations,
267
+ glossary_terms_hash)
268
+ end
269
+
270
+ collection
271
+ end
272
+
273
+ # Builds an object from variable fields
274
+ def self.build_from_variable_fields(variable_fields_hash)
275
+
276
+ return nil if variable_fields_hash.nil?
277
+
278
+ representation_class = Class.new do
279
+ include Angus::Remote::Response::Hash
280
+ end
281
+
282
+ representation_object = representation_class.new
283
+
284
+ variable_fields_hash.each do |key_name, field_value|
285
+ representation_object.elements[key_name] = field_value
286
+ representation_class.send :define_method, key_name.to_sym do
287
+ field_value
288
+ end
289
+ end
290
+
291
+ representation_object
292
+
293
+ end
294
+
295
+ # Receives an array of representations and returns a hash of representations where
296
+ # the keys are the representation's names and the values are the representations
297
+ def self.representations_hash(representations)
298
+ hash = {}
299
+ representations.each do |representation|
300
+ hash[representation.name] = representation
301
+ end
302
+ hash
303
+ end
304
+ end
305
+
306
+ end
307
+ end
308
+ end