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,4 @@
1
+ require 'angus/remote/client'
2
+ require 'angus/remote/builder'
3
+ require 'angus/remote/service_directory'
4
+ require 'angus/remote/response/serializer'
@@ -0,0 +1,204 @@
1
+ require 'uri'
2
+
3
+ require_relative 'client'
4
+ require_relative 'response/builder'
5
+
6
+ module Angus
7
+ module Remote
8
+
9
+ module Builder
10
+
11
+ DEFAULT_TIMEOUT = 60
12
+
13
+ # Builds a client for a specific service.
14
+ #
15
+ # @param [String] code_name The service's name known to the service directory
16
+ # @param [Angus::SDoc::Definitions::Service] service_definition
17
+ # @param api_url Base service api url
18
+ #
19
+ # @return [Remote::Client] object that implements each method specified as operation
20
+ # in the service metadata
21
+ def self.build(code_name, service_definition, api_url)
22
+ remote_service_class = build_client_class(service_definition.name)
23
+
24
+ # TODO: define how to use the namespace in the remote client.
25
+ if service_definition.operations.is_a?(Hash)
26
+ service_definition.operations.each do |namespace, operations|
27
+ operations.each do |operation|
28
+ self.define_operation(remote_service_class, namespace, operation, code_name,
29
+ service_definition)
30
+ end
31
+ end
32
+ else
33
+ service_definition.operations.each do |operation|
34
+ self.define_operation(remote_service_class, code_name, operation, code_name,
35
+ service_definition)
36
+ end
37
+ end
38
+
39
+ service_definition.proxy_operations.each do |namespace, operations|
40
+ operations.each do |operation|
41
+ self.define_proxy_operation(remote_service_class, namespace, operation, code_name,
42
+ service_definition)
43
+ end
44
+ end
45
+
46
+ remote_service_class.new(api_url, self.default_timeout)
47
+ end
48
+
49
+ def self.define_operation(client_class, namespace, operation, service_code_name,
50
+ service_definition)
51
+ client_class.send :define_method, operation.code_name do |encode_as_json = false,
52
+ path_params = nil,
53
+ request_params = nil|
54
+
55
+ args = [encode_as_json, path_params, request_params]
56
+
57
+ request_params = Angus::Remote::Builder.extract_var_arg!(args, Hash) || {}
58
+ path_params = Angus::Remote::Builder.extract_var_arg!(args, Array) || []
59
+ encode_as_json = Angus::Remote::Builder.extract_var_arg!(args, TrueClass) || false
60
+
61
+ request_params = Angus::Remote::Builder.apply_glossary(service_definition.glossary, request_params)
62
+ request_params = Angus::Remote::Builder.escape_request_params(request_params)
63
+
64
+ response = make_request(operation.path, operation.method, encode_as_json, path_params,
65
+ request_params)
66
+
67
+ Angus::Remote::Response::Builder.build_from_remote_response(response,
68
+ service_code_name,
69
+ service_definition.version,
70
+ namespace,
71
+ operation.code_name)
72
+ end
73
+ end
74
+
75
+ def self.define_proxy_operation(client_class, namespace, operation, service_code_name,
76
+ service_definition)
77
+ client_class.send :define_method, operation.code_name do |encode_as_json = false,
78
+ path_params = nil,
79
+ request_params = nil|
80
+
81
+ service_definition = Angus::Remote::ServiceDirectory.join_proxy(
82
+ service_code_name,
83
+ service_definition.version,
84
+ operation.service_name
85
+ )
86
+
87
+ args = [encode_as_json, path_params, request_params]
88
+
89
+ request_params = Angus::Remote::Builder.extract_var_arg!(args, Hash) || {}
90
+ path_params = Angus::Remote::Builder.extract_var_arg!(args, Array) || []
91
+ encode_as_json = Angus::Remote::Builder.extract_var_arg!(args, TrueClass) || false
92
+
93
+ request_params = Angus::Remote::Builder.apply_glossary(service_definition.glossary,
94
+ request_params)
95
+
96
+ request_params = Angus::Remote::Builder.escape_request_params(request_params)
97
+
98
+ response = make_request(operation.path, operation.method, encode_as_json, path_params,
99
+ request_params)
100
+
101
+ Angus::Remote::Response::Builder.build_from_remote_response(response,
102
+ service_code_name,
103
+ service_definition.version,
104
+ namespace,
105
+ operation.code_name)
106
+ end
107
+ end
108
+
109
+ # Build a client class for the service
110
+ #
111
+ # @param [String] service_name the name of the service
112
+ # @param [String] api_url the url for consuming the service's api
113
+ #
114
+ # @return [Class] A class client, that inherits from {Angus::Remote::Client}
115
+ def self.build_client_class(service_name)
116
+ remote_service_class = Class.new(Angus::Remote::Client)
117
+
118
+ remote_service_class.class_eval <<-END
119
+ def self.name
120
+ "#<Client_#{service_name}>"
121
+ end
122
+
123
+ def self.to_s
124
+ name
125
+ end
126
+ END
127
+
128
+ remote_service_class
129
+ end
130
+
131
+
132
+ # Applies glossary to params.
133
+ #
134
+ # Converts the params that are long names to short names
135
+ #
136
+ # @param [Glossary] glossary of terms
137
+ # @param [Hash] params
138
+ #
139
+ # @return [Hash] params with long names
140
+ def self.apply_glossary(glossary, params)
141
+ terms_hash = glossary.terms_hash_with_long_names
142
+
143
+ applied_params = {}
144
+
145
+ params.each do |name, value|
146
+ if terms_hash.include?(name.to_s)
147
+ term = terms_hash[name.to_s]
148
+ applied_params[term.short_name.to_sym] = value
149
+ else
150
+ applied_params[name] = value
151
+ end
152
+ end
153
+
154
+ applied_params
155
+ end
156
+
157
+ # Extract an argument of class +klass+ from an array of +args+
158
+ #
159
+ # Returns the first value from +args+ (starting from the end of args) whose class matches +klass+
160
+ # @param args Array of arguments
161
+ # @param klass Class that should match the returned value
162
+ #
163
+ def self.extract_var_arg!(args, klass)
164
+ arg = nil
165
+ arg_found = false
166
+
167
+ i = args.length
168
+ while !arg_found && i > 0
169
+ i -= 1
170
+ arg = args[i]
171
+ arg_found = true if arg.is_a?(klass)
172
+ end
173
+
174
+ if arg_found
175
+ args.delete_at(i)
176
+ arg
177
+ end
178
+ end
179
+
180
+ def self.escape_request_params(request_params)
181
+ encoded = {}
182
+ request_params.each do |name, value|
183
+ encoded_name = URI.escape(name.to_s)
184
+ if value.is_a? Hash
185
+ value = self.escape_request_params(value)
186
+ end
187
+ encoded[encoded_name] = value
188
+ end
189
+ encoded
190
+ end
191
+
192
+
193
+ def self.default_timeout
194
+ @default_timeout || DEFAULT_TIMEOUT
195
+ end
196
+
197
+ def self.default_timeout=(default_timeout)
198
+ @default_timeout = default_timeout
199
+ end
200
+
201
+ end
202
+
203
+ end
204
+ end
@@ -0,0 +1,79 @@
1
+ require 'json'
2
+ require 'persistent_http'
3
+
4
+ require_relative 'exceptions'
5
+ require_relative 'utils'
6
+
7
+ require_relative 'response/builder'
8
+
9
+ module Angus
10
+ module Remote
11
+
12
+ # A client for service invocation
13
+ class Client
14
+ def initialize(api_url, timeout = nil)
15
+ api_url = api_url[0..-2] if api_url[-1] == '/'
16
+
17
+ @connection = PersistentHTTP.new(
18
+ :pool_size => 10,
19
+ :pool_timeout => 10,
20
+ :warn_timeout => 0.25,
21
+ :force_retry => false,
22
+ :url => api_url,
23
+
24
+ :read_timeout => timeout,
25
+ :open_timeout => timeout
26
+ )
27
+
28
+ @api_base_path = @connection.default_path
29
+ end
30
+
31
+ # Makes a request to the service
32
+ #
33
+ # @param [String] path The operation URL path. It can have place holders,
34
+ # ex: /user/:user_id/profile
35
+ # @param [String] method The http method for the request: get, post, put, delete
36
+ # @param [String] encode_as_json If true, the request params are encoded as json in the
37
+ # request body
38
+ # @param [String] path_params Params that go into the path. This is an array, the first
39
+ # element in the array goes in the first path placeholder.
40
+ # @param [String] request_params Params that go as url params or as data encoded in the body.
41
+ #
42
+ # @return [Net::HTTPResponse] The remote service response.
43
+ #
44
+ # @raise (see Utils.build_request)
45
+ # @raise [RemoteSevereError] When the remote response status code is of severe error.
46
+ # see Utils.severe_error_response?
47
+ # @raise [RemoteConnectionError] When the remote service refuses the connection.
48
+ def make_request(path, method, encode_as_json, path_params, request_params)
49
+ path = @api_base_path + Utils.build_path(path, path_params)
50
+
51
+ request = Utils.build_request(method, path, request_params, encode_as_json)
52
+
53
+ begin
54
+ response = @connection.request(request)
55
+
56
+ if Utils.severe_error_response?(response)
57
+ raise RemoteSevereError.new(get_error_messages(response.body))
58
+ end
59
+
60
+ response
61
+ rescue Errno::ECONNREFUSED, PersistentHTTP::Error
62
+ raise RemoteConnectionError.new(@api_base_path)
63
+ end
64
+ end
65
+
66
+ def to_s
67
+ "#<#{self.class}:#{object_id}>"
68
+ end
69
+
70
+ private
71
+
72
+ def get_error_messages(response_body)
73
+ json_response = JSON(response_body) rescue { 'messages' => [] }
74
+ Response::Builder::build_messages(json_response['messages'])
75
+ end
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,50 @@
1
+ module Angus
2
+ module Remote
3
+
4
+ class RemoteSevereError < Exception
5
+
6
+ attr_reader :messages
7
+
8
+ def initialize(messages)
9
+ @messages = messages
10
+ end
11
+
12
+ end
13
+
14
+ class RemoteConnectionError < Exception
15
+
16
+ def initialize(url)
17
+ @remote_url = url
18
+ end
19
+
20
+ def message
21
+ "Remote Connection Error: #@remote_url"
22
+ end
23
+
24
+ end
25
+
26
+ class MethodArgumentError < Exception
27
+
28
+ def initialize(method)
29
+ @method = method
30
+ end
31
+
32
+ def message
33
+ "Invalid http method: #@method"
34
+ end
35
+ end
36
+
37
+ class PathArgumentError < Exception
38
+
39
+ def initialize(current, expected)
40
+ @current = current
41
+ @expected = expected
42
+ end
43
+
44
+ def message
45
+ "Wrong number of arguments (#@current for #@expected)"
46
+ end
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,54 @@
1
+ require 'tempfile'
2
+ require 'net/http/post/multipart'
3
+
4
+ module Http
5
+ module Multipart
6
+
7
+ TRANSFORMABLE_TYPES = [File, Tempfile, StringIO]
8
+
9
+ QUERY_STRING_NORMALIZER = Proc.new do |params|
10
+ Multipart.flatten_params(params).map do |(k,v)|
11
+ [k, Multipart.transformable_type?(v) ? Multipart.file_to_upload_io(v) : v]
12
+ end
13
+ end
14
+
15
+ def self.file_to_upload_io(file)
16
+ if file.respond_to? :original_filename
17
+ filename = file.original_filename
18
+ else
19
+ filename = File.split(file.path).last
20
+ end
21
+ content_type = 'application/octet-stream'
22
+ UploadIO.new(file, content_type, filename)
23
+ end
24
+
25
+ def self.hash_contains_files?(hash)
26
+ hash.is_a?(Hash) && self.flatten_params(hash).select do |(k,v)|
27
+ self.transformable_type?(v) || v.is_a?(UploadIO)
28
+ end.size > 0
29
+ end
30
+
31
+ def self.transformable_type?(object)
32
+ TRANSFORMABLE_TYPES.any? { |klass| object.is_a?(klass) }
33
+ end
34
+
35
+ def self.flatten_params(params={}, prefix='')
36
+ flattened = []
37
+ params.each do |(k,v)|
38
+ if params.is_a?(Array)
39
+ v = k
40
+ k = ''
41
+ end
42
+
43
+ flattened_key = prefix == '' ? "#{k}" : "#{prefix}[#{k}]"
44
+ if v.is_a?(Hash) || v.is_a?(Array)
45
+ flattened += flatten_params(v, flattened_key)
46
+ else
47
+ flattened << [flattened_key, v]
48
+ end
49
+ end
50
+ flattened
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,36 @@
1
+ require 'net/http/post/multipart'
2
+
3
+ module Http
4
+ module MultipartMethods
5
+
6
+ module MultipartBase
7
+ DEFAULT_BOUNDARY = '-----------RubyMultipartPost'
8
+ # prevent reinitialization of headers
9
+ def initialize_http_header(initheader)
10
+ super
11
+ set_headers_for_body
12
+ end
13
+
14
+ def body=(value)
15
+ @body_parts = value.map {|(k,v)| Parts::Part.new(boundary, k, v)}
16
+ @body_parts << Parts::EpiloguePart.new(boundary)
17
+ set_headers_for_body
18
+ end
19
+
20
+ def boundary
21
+ DEFAULT_BOUNDARY
22
+ end
23
+
24
+ private
25
+
26
+ def set_headers_for_body
27
+ if @body_parts
28
+ self.set_content_type('multipart/form-data', {'boundary' => boundary})
29
+ self.content_length = @body_parts.inject(0) { |sum,i| sum + i.length }
30
+ self.body_stream = CompositeReadIO.new(*@body_parts.map { |part| part.to_io })
31
+ end
32
+ end
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'multipart_base'
2
+
3
+ module Http
4
+ module MultipartMethods
5
+
6
+ class Post < Net::HTTP::Post
7
+ include MultipartBase
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'multipart_base'
2
+
3
+ module Http
4
+ module MultipartMethods
5
+
6
+ class Put < Net::HTTP::Put
7
+ include MultipartBase
8
+ end
9
+
10
+ end
11
+ end