angus-remote 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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