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,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