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.
- data/lib/angus-remote.rb +4 -0
- data/lib/angus/remote/builder.rb +204 -0
- data/lib/angus/remote/client.rb +79 -0
- data/lib/angus/remote/exceptions.rb +50 -0
- data/lib/angus/remote/http/multipart.rb +54 -0
- data/lib/angus/remote/http/multipart_methods/multipart_base.rb +36 -0
- data/lib/angus/remote/http/multipart_methods/multipart_post.rb +11 -0
- data/lib/angus/remote/http/multipart_methods/multipart_put.rb +11 -0
- data/lib/angus/remote/http/query_params.rb +53 -0
- data/lib/angus/remote/message.rb +14 -0
- data/lib/angus/remote/proxy_client.rb +58 -0
- data/lib/angus/remote/proxy_client_utils.rb +70 -0
- data/lib/angus/remote/remote_response.rb +44 -0
- data/lib/angus/remote/representation.rb +18 -0
- data/lib/angus/remote/response/builder.rb +308 -0
- data/lib/angus/remote/response/hash.rb +47 -0
- data/lib/angus/remote/response/serializer.rb +43 -0
- data/lib/angus/remote/service_directory.rb +217 -0
- data/lib/angus/remote/utils.rb +119 -0
- data/lib/angus/remote/version.rb +4 -2
- data/lib/angus/unmarshalling.rb +33 -0
- data/spec/angus/remote/builder_spec.rb +105 -0
- data/spec/angus/remote/client_spec.rb +75 -0
- data/spec/angus/remote/http/multipart_methods/multipart_base_spec.rb +36 -0
- data/spec/angus/remote/http/multipart_spec.rb +120 -0
- data/spec/angus/remote/http/query_params_spec.rb +28 -0
- data/spec/angus/remote/proxy_client_utils_spec.rb +102 -0
- data/spec/angus/remote/response/builder_spec.rb +69 -0
- data/spec/angus/remote/service_directory_spec.rb +76 -0
- data/spec/angus/remote/utils_spec.rb +204 -0
- metadata +192 -32
- data/.gitignore +0 -17
- data/Gemfile +0 -4
- data/LICENSE.txt +0 -22
- data/README.md +0 -29
- data/Rakefile +0 -1
- data/angus-remote.gemspec +0 -23
- data/lib/angus/remote.rb +0 -7
data/lib/angus-remote.rb
ADDED
@@ -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
|