flexirest 1.2.0
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.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +2 -0
- data/.simplecov +4 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +37 -0
- data/CONTRIBUTING.md +62 -0
- data/Gemfile +4 -0
- data/Guardfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +846 -0
- data/Rakefile +13 -0
- data/doc/ActiveRestClient Internals.graffle +1236 -0
- data/doc/ActiveRestClient Internals.png +0 -0
- data/flexirest.gemspec +39 -0
- data/lib/flexirest.rb +25 -0
- data/lib/flexirest/base.rb +189 -0
- data/lib/flexirest/caching.rb +92 -0
- data/lib/flexirest/configuration.rb +209 -0
- data/lib/flexirest/connection.rb +103 -0
- data/lib/flexirest/connection_manager.rb +36 -0
- data/lib/flexirest/headers_list.rb +47 -0
- data/lib/flexirest/instrumentation.rb +62 -0
- data/lib/flexirest/lazy_association_loader.rb +97 -0
- data/lib/flexirest/lazy_loader.rb +23 -0
- data/lib/flexirest/logger.rb +67 -0
- data/lib/flexirest/mapping.rb +69 -0
- data/lib/flexirest/monkey_patching.rb +7 -0
- data/lib/flexirest/proxy_base.rb +193 -0
- data/lib/flexirest/recording.rb +24 -0
- data/lib/flexirest/request.rb +573 -0
- data/lib/flexirest/request_delegator.rb +44 -0
- data/lib/flexirest/request_filtering.rb +62 -0
- data/lib/flexirest/result_iterator.rb +85 -0
- data/lib/flexirest/validation.rb +60 -0
- data/lib/flexirest/version.rb +3 -0
- data/spec/lib/base_spec.rb +389 -0
- data/spec/lib/caching_spec.rb +217 -0
- data/spec/lib/configuration_spec.rb +234 -0
- data/spec/lib/connection_manager_spec.rb +43 -0
- data/spec/lib/connection_spec.rb +159 -0
- data/spec/lib/headers_list_spec.rb +61 -0
- data/spec/lib/instrumentation_spec.rb +58 -0
- data/spec/lib/lazy_association_loader_spec.rb +135 -0
- data/spec/lib/lazy_loader_spec.rb +25 -0
- data/spec/lib/logger_spec.rb +63 -0
- data/spec/lib/mapping_spec.rb +52 -0
- data/spec/lib/proxy_spec.rb +189 -0
- data/spec/lib/recording_spec.rb +34 -0
- data/spec/lib/request_filtering_spec.rb +84 -0
- data/spec/lib/request_spec.rb +711 -0
- data/spec/lib/result_iterator_spec.rb +140 -0
- data/spec/lib/validation_spec.rb +113 -0
- data/spec/lib/xml_spec.rb +74 -0
- data/spec/spec_helper.rb +88 -0
- metadata +347 -0
@@ -0,0 +1,193 @@
|
|
1
|
+
require "uri"
|
2
|
+
|
3
|
+
module Flexirest
|
4
|
+
class ProxyBase
|
5
|
+
cattr_accessor :mappings, :request, :original_handler
|
6
|
+
cattr_accessor :original_body, :original_get_params, :original_post_params, :original_url
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def get(match, &block)
|
10
|
+
add_mapping(:get, match, block)
|
11
|
+
end
|
12
|
+
|
13
|
+
def post(match, &block)
|
14
|
+
add_mapping(:post, match, block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def put(match, &block)
|
18
|
+
add_mapping(:put, match, block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def delete(match, &block)
|
22
|
+
add_mapping(:delete, match, block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_mapping(method_type, match, block)
|
26
|
+
@mappings ||= []
|
27
|
+
|
28
|
+
if match.is_a?(String) && (param_keys = match.scan(/:\w+/)) && param_keys.any?
|
29
|
+
param_keys.each do |key|
|
30
|
+
match.gsub!(key, "([^/]+)")
|
31
|
+
end
|
32
|
+
param_keys = param_keys.map {|k| k.gsub(":", "").to_sym}
|
33
|
+
match = Regexp.new(match)
|
34
|
+
end
|
35
|
+
|
36
|
+
@mappings << OpenStruct.new(http_method:method_type, match:match, block:block, param_keys:param_keys)
|
37
|
+
end
|
38
|
+
|
39
|
+
def body(value = nil)
|
40
|
+
@body = value if value
|
41
|
+
@body
|
42
|
+
end
|
43
|
+
|
44
|
+
def url(value = nil)
|
45
|
+
@url = value if value
|
46
|
+
@url
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_params(value = nil)
|
50
|
+
@get_params = value if value
|
51
|
+
@get_params
|
52
|
+
end
|
53
|
+
|
54
|
+
def post_params(value = nil)
|
55
|
+
@post_params = value if value
|
56
|
+
@post_params
|
57
|
+
end
|
58
|
+
|
59
|
+
def params(value = nil)
|
60
|
+
@params = value if value
|
61
|
+
@params
|
62
|
+
end
|
63
|
+
|
64
|
+
def passthrough
|
65
|
+
rebuild_request
|
66
|
+
@original_handler.call(@request)
|
67
|
+
end
|
68
|
+
|
69
|
+
def result_is_json_or_unspecified?(result)
|
70
|
+
result.headers['Content-Type'].include?('json')
|
71
|
+
rescue
|
72
|
+
true
|
73
|
+
end
|
74
|
+
|
75
|
+
def translate(result, options = {})
|
76
|
+
incoming_content_type = result.headers['Content-Type']
|
77
|
+
if result_is_json_or_unspecified?(result)
|
78
|
+
result.headers["content-type"] = "application/hal+json"
|
79
|
+
end
|
80
|
+
result = FaradayResponseProxy.new(OpenStruct.new(status:result.status, response_headers:result.headers, body:result.body))
|
81
|
+
if result.body.present?
|
82
|
+
if incoming_content_type && incoming_content_type["xml"]
|
83
|
+
result.body = yield Crack::XML.parse(result.body)
|
84
|
+
else
|
85
|
+
result.body = yield MultiJson.load(result.body)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
result
|
89
|
+
end
|
90
|
+
|
91
|
+
def rebuild_request
|
92
|
+
if @url != @original_url
|
93
|
+
@request.forced_url = @request.url = @url
|
94
|
+
end
|
95
|
+
if @body != @original_body
|
96
|
+
@request.body = @body
|
97
|
+
elsif @post_params != @original_post_params
|
98
|
+
@request.body = nil
|
99
|
+
@request.prepare_request_body(@post_params)
|
100
|
+
end
|
101
|
+
if @get_params != @original_get_params
|
102
|
+
@request.get_params = @get_params
|
103
|
+
@request.prepare_url
|
104
|
+
@request.append_get_parameters
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def handle(request, &block)
|
109
|
+
@request = request
|
110
|
+
@original_handler = block
|
111
|
+
|
112
|
+
@original_body = request.body
|
113
|
+
@body = @original_body.dup
|
114
|
+
|
115
|
+
@original_get_params = request.get_params
|
116
|
+
@get_params = @original_get_params.dup
|
117
|
+
|
118
|
+
@original_post_params = request.post_params
|
119
|
+
@post_params = (@original_post_params || {}).dup
|
120
|
+
|
121
|
+
@original_url = request.url
|
122
|
+
@url = @original_url.dup
|
123
|
+
|
124
|
+
if mapping = find_mapping_for_current_request
|
125
|
+
self.class_eval(&mapping.block)
|
126
|
+
else
|
127
|
+
passthrough
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def find_mapping_for_current_request
|
132
|
+
uri = URI.parse(@original_url)
|
133
|
+
@mappings ||= []
|
134
|
+
@params = {}
|
135
|
+
@mappings.each do |mapping|
|
136
|
+
match = mapping.match
|
137
|
+
if (match_data = uri.path.match(match)) && @request.http_method.to_sym == mapping.http_method
|
138
|
+
matches = match_data.to_a
|
139
|
+
matches.shift
|
140
|
+
matches.each_with_index do |value, index|
|
141
|
+
@params[mapping.param_keys[index]] = value
|
142
|
+
end
|
143
|
+
return mapping
|
144
|
+
end
|
145
|
+
end
|
146
|
+
nil
|
147
|
+
end
|
148
|
+
|
149
|
+
def render(body, status=200, content_type="application/javascript", headers={})
|
150
|
+
headers["Content-type"] = content_type
|
151
|
+
FaradayResponseProxy.new(OpenStruct.new(body:body, status:status, response_headers:headers, proxied:true))
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.inherited(base)
|
156
|
+
base.extend(ClassMethods)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# FaradayResponseProxy acts just like a Faraday Response object,
|
161
|
+
# however it always resolves the request immediately regardless of
|
162
|
+
# whether it is inside an in_parallel block or not
|
163
|
+
class FaradayResponseProxy
|
164
|
+
def initialize(response)
|
165
|
+
@response = response
|
166
|
+
end
|
167
|
+
|
168
|
+
def headers
|
169
|
+
@response.response_headers
|
170
|
+
end
|
171
|
+
|
172
|
+
def status
|
173
|
+
@response.status
|
174
|
+
end
|
175
|
+
|
176
|
+
def body
|
177
|
+
@response.body
|
178
|
+
end
|
179
|
+
|
180
|
+
def body=(value)
|
181
|
+
@response.body = value
|
182
|
+
value
|
183
|
+
end
|
184
|
+
|
185
|
+
def on_complete
|
186
|
+
yield(@response)
|
187
|
+
end
|
188
|
+
|
189
|
+
def finished?
|
190
|
+
true
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Flexirest
|
2
|
+
module Recording
|
3
|
+
module ClassMethods
|
4
|
+
@record_response = nil
|
5
|
+
|
6
|
+
def record_response(url = nil, response = nil, &block)
|
7
|
+
if url && response && @record_response
|
8
|
+
@record_response.call(url, response)
|
9
|
+
elsif block
|
10
|
+
@record_response = block
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def record_response?
|
15
|
+
!!@record_response
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.included(base)
|
20
|
+
base.extend(ClassMethods)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,573 @@
|
|
1
|
+
require "cgi"
|
2
|
+
require "multi_json"
|
3
|
+
require 'crack'
|
4
|
+
require 'crack/xml'
|
5
|
+
|
6
|
+
module Flexirest
|
7
|
+
|
8
|
+
class Request
|
9
|
+
attr_accessor :post_params, :get_params, :url, :path, :headers, :method, :object, :body, :forced_url, :original_url
|
10
|
+
|
11
|
+
def initialize(method, object, params = {})
|
12
|
+
@method = method
|
13
|
+
@method[:options] ||= {}
|
14
|
+
@method[:options][:lazy] ||= []
|
15
|
+
@method[:options][:has_one] ||= {}
|
16
|
+
@overridden_name = @method[:options][:overridden_name]
|
17
|
+
@object = object
|
18
|
+
@response_delegate = Flexirest::RequestDelegator.new(nil)
|
19
|
+
@params = params
|
20
|
+
@headers = HeadersList.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def object_is_class?
|
24
|
+
!@object.respond_to?(:dirty?)
|
25
|
+
end
|
26
|
+
|
27
|
+
def class_name
|
28
|
+
if object_is_class?
|
29
|
+
@object.name
|
30
|
+
else
|
31
|
+
@object.class.name
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def original_object_class
|
36
|
+
if object_is_class?
|
37
|
+
@object
|
38
|
+
else
|
39
|
+
@object.class
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def base_url
|
44
|
+
if object_is_class?
|
45
|
+
@object.base_url
|
46
|
+
else
|
47
|
+
@object.class.base_url
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def using_api_auth?
|
52
|
+
if object_is_class?
|
53
|
+
@object.using_api_auth?
|
54
|
+
else
|
55
|
+
@object.class.using_api_auth?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def api_auth_access_id
|
60
|
+
if object_is_class?
|
61
|
+
@object.api_auth_access_id
|
62
|
+
else
|
63
|
+
@object.class.api_auth_access_id
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def api_auth_secret_key
|
68
|
+
if object_is_class?
|
69
|
+
@object.api_auth_secret_key
|
70
|
+
else
|
71
|
+
@object.class.api_auth_secret_key
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def username
|
76
|
+
if object_is_class?
|
77
|
+
@object.username
|
78
|
+
else
|
79
|
+
@object.class.username
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def password
|
84
|
+
if object_is_class?
|
85
|
+
@object.password
|
86
|
+
else
|
87
|
+
@object.class.password
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def request_body_type
|
92
|
+
if @method[:options][:request_body_type]
|
93
|
+
@method[:options][:request_body_type]
|
94
|
+
elsif object_is_class?
|
95
|
+
@object.request_body_type
|
96
|
+
else
|
97
|
+
@object.class.request_body_type
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def verbose?
|
102
|
+
if object_is_class?
|
103
|
+
@object.verbose
|
104
|
+
else
|
105
|
+
@object.class.verbose
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def translator
|
110
|
+
if object_is_class?
|
111
|
+
@object.translator
|
112
|
+
else
|
113
|
+
@object.class.translator
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def proxy
|
118
|
+
if object_is_class?
|
119
|
+
@object.proxy
|
120
|
+
else
|
121
|
+
@object.class.proxy
|
122
|
+
end
|
123
|
+
rescue
|
124
|
+
nil
|
125
|
+
end
|
126
|
+
|
127
|
+
def http_method
|
128
|
+
@method[:method]
|
129
|
+
end
|
130
|
+
|
131
|
+
def call(explicit_parameters=nil)
|
132
|
+
@instrumentation_name = "#{class_name}##{@method[:name]}"
|
133
|
+
result = nil
|
134
|
+
cached = nil
|
135
|
+
ActiveSupport::Notifications.instrument("request_call.flexirest", :name => @instrumentation_name) do
|
136
|
+
@explicit_parameters = explicit_parameters
|
137
|
+
@body = nil
|
138
|
+
prepare_params
|
139
|
+
prepare_url
|
140
|
+
if fake = @method[:options][:fake]
|
141
|
+
if fake.respond_to?(:call)
|
142
|
+
fake = fake.call(self)
|
143
|
+
end
|
144
|
+
Flexirest::Logger.debug " \033[1;4;32m#{Flexirest::NAME}\033[0m #{@instrumentation_name} - Faked response found"
|
145
|
+
content_type = @method[:options][:fake_content_type] || "application/json"
|
146
|
+
return handle_response(OpenStruct.new(status:200, body:fake, response_headers:{"X-ARC-Faked-Response" => "true", "Content-Type" => content_type}))
|
147
|
+
end
|
148
|
+
if object_is_class?
|
149
|
+
@object.send(:_filter_request, :before, @method[:name], self)
|
150
|
+
else
|
151
|
+
@object.class.send(:_filter_request, :before, @method[:name], self)
|
152
|
+
end
|
153
|
+
append_get_parameters
|
154
|
+
prepare_request_body
|
155
|
+
self.original_url = self.url
|
156
|
+
cached = original_object_class.read_cached_response(self)
|
157
|
+
if cached
|
158
|
+
if cached.expires && cached.expires > Time.now
|
159
|
+
Flexirest::Logger.debug " \033[1;4;32m#{Flexirest::NAME}\033[0m #{@instrumentation_name} - Absolutely cached copy found"
|
160
|
+
return handle_cached_response(cached)
|
161
|
+
elsif cached.etag.to_s != "" #present? isn't working for some reason
|
162
|
+
Flexirest::Logger.debug " \033[1;4;32m#{Flexirest::NAME}\033[0m #{@instrumentation_name} - Etag cached copy found with etag #{cached.etag}"
|
163
|
+
etag = cached.etag
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
response = (
|
168
|
+
if proxy
|
169
|
+
proxy.handle(self) do |request|
|
170
|
+
request.do_request(etag)
|
171
|
+
end
|
172
|
+
else
|
173
|
+
do_request(etag)
|
174
|
+
end
|
175
|
+
)
|
176
|
+
|
177
|
+
# This block is called immediately when this request is not inside a parallel request block.
|
178
|
+
# Otherwise this callback is called after the parallel request block ends.
|
179
|
+
response.on_complete do |response_env|
|
180
|
+
if verbose?
|
181
|
+
Flexirest::Logger.debug " Response"
|
182
|
+
Flexirest::Logger.debug " << Status : #{response_env.status}"
|
183
|
+
response_env.response_headers.each do |k,v|
|
184
|
+
Flexirest::Logger.debug " << #{k} : #{v}"
|
185
|
+
end
|
186
|
+
Flexirest::Logger.debug " << Body:\n#{response_env.body}"
|
187
|
+
end
|
188
|
+
|
189
|
+
if object_is_class? && @object.record_response?
|
190
|
+
@object.record_response(self.url, response_env)
|
191
|
+
end
|
192
|
+
if object_is_class?
|
193
|
+
@object.send(:_filter_request, :after, @method[:name], response_env)
|
194
|
+
else
|
195
|
+
@object.class.send(:_filter_request, :after, @method[:name], response_env)
|
196
|
+
end
|
197
|
+
|
198
|
+
result = handle_response(response_env, cached)
|
199
|
+
@response_delegate.__setobj__(result)
|
200
|
+
original_object_class.write_cached_response(self, response_env, result)
|
201
|
+
end
|
202
|
+
|
203
|
+
# If this was not a parallel request just return the original result
|
204
|
+
return result if response.finished?
|
205
|
+
# Otherwise return the delegate which will get set later once the call back is completed
|
206
|
+
return @response_delegate
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def prepare_params
|
211
|
+
params = @params || @object._attributes rescue {}
|
212
|
+
if params.is_a?(String) || params.is_a?(Fixnum)
|
213
|
+
params = {id:params}
|
214
|
+
end
|
215
|
+
|
216
|
+
default_params = @method[:options][:defaults] || {}
|
217
|
+
|
218
|
+
if @explicit_parameters
|
219
|
+
params = @explicit_parameters
|
220
|
+
end
|
221
|
+
if http_method == :get
|
222
|
+
@get_params = default_params.merge(params || {})
|
223
|
+
@post_params = nil
|
224
|
+
else
|
225
|
+
@post_params = default_params.merge(params || {})
|
226
|
+
@get_params = {}
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def prepare_url
|
231
|
+
if @forced_url && @forced_url.present?
|
232
|
+
@url = @forced_url
|
233
|
+
else
|
234
|
+
@url = @method[:url].dup
|
235
|
+
matches = @url.scan(/(:[a-z_-]+)/)
|
236
|
+
@get_params ||= {}
|
237
|
+
@post_params ||= {}
|
238
|
+
matches.each do |token|
|
239
|
+
token = token.first[1,999]
|
240
|
+
target = @get_params.delete(token.to_sym) || @post_params.delete(token.to_sym) || @get_params.delete(token.to_s) || @post_params.delete(token.to_s) || ""
|
241
|
+
@url.gsub!(":#{token}", target.to_s)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def append_get_parameters
|
247
|
+
if @get_params.any?
|
248
|
+
@url += "?" + @get_params.to_query
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def prepare_request_body(params = nil)
|
253
|
+
if request_body_type == :form_encoded
|
254
|
+
@body ||= (params || @post_params || {}).to_query
|
255
|
+
headers["Content-Type"] ||= "application/x-www-form-urlencoded"
|
256
|
+
elsif request_body_type == :json
|
257
|
+
@body ||= (params || @post_params || {}).to_json
|
258
|
+
headers["Content-Type"] ||= "application/json; charset=utf-8"
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def do_request(etag)
|
263
|
+
http_headers = {}
|
264
|
+
http_headers["If-None-Match"] = etag if etag
|
265
|
+
http_headers["Accept"] = "application/hal+json, application/json;q=0.5"
|
266
|
+
headers.each do |key,value|
|
267
|
+
value = value.join(",") if value.is_a?(Array)
|
268
|
+
http_headers[key] = value
|
269
|
+
end
|
270
|
+
if @method[:options][:url] || @forced_url
|
271
|
+
@url = @method[:options][:url] || @method[:url]
|
272
|
+
@url = @forced_url if @forced_url
|
273
|
+
if connection = Flexirest::ConnectionManager.find_connection_for_url(@url)
|
274
|
+
@url = @url.slice(connection.base_url.length, 255)
|
275
|
+
else
|
276
|
+
parts = @url.match(%r{^(https?://[a-z\d\.:-]+?)(/.*)}).to_a
|
277
|
+
if (parts.empty?) # Not a full URL, so use hostname/protocol from existing base_url
|
278
|
+
uri = URI.parse(base_url)
|
279
|
+
@base_url = "#{uri.scheme}://#{uri.host}#{":#{uri.port}" if uri.port != 80 && uri.port != 443}"
|
280
|
+
@url = "#{base_url}#{@url}".gsub(@base_url, "")
|
281
|
+
else
|
282
|
+
_, @base_url, @url = parts
|
283
|
+
end
|
284
|
+
base_url.gsub!(%r{//(.)}, "//#{username}:#{password}@\\1") if username && !base_url[%r{//[^/]*:[^/]*@}]
|
285
|
+
connection = Flexirest::ConnectionManager.get_connection(@base_url)
|
286
|
+
end
|
287
|
+
else
|
288
|
+
parts = @url.match(%r{^(https?://[a-z\d\.:-]+?)(/.*)}).to_a
|
289
|
+
if (parts.empty?) # Not a full URL, so use hostname/protocol from existing base_url
|
290
|
+
uri = URI.parse(base_url)
|
291
|
+
@base_url = "#{uri.scheme}://#{uri.host}#{":#{uri.port}" if uri.port != 80 && uri.port != 443}"
|
292
|
+
@url = "#{base_url}#{@url}".gsub(@base_url, "")
|
293
|
+
base_url = @base_url
|
294
|
+
end
|
295
|
+
base_url.gsub!(%r{//(.)}, "//#{username}:#{password}@\\1") if username && !base_url[%r{//[^/]*:[^/]*@}]
|
296
|
+
connection = Flexirest::ConnectionManager.get_connection(base_url)
|
297
|
+
end
|
298
|
+
Flexirest::Logger.info " \033[1;4;32m#{Flexirest::NAME}\033[0m #{@instrumentation_name} - Requesting #{connection.base_url}#{@url}"
|
299
|
+
|
300
|
+
if verbose?
|
301
|
+
Flexirest::Logger.debug "Flexirest Verbose Log:"
|
302
|
+
Flexirest::Logger.debug " Request"
|
303
|
+
Flexirest::Logger.debug " >> #{http_method.upcase} #{@url} HTTP/1.1"
|
304
|
+
http_headers.each do |k,v|
|
305
|
+
Flexirest::Logger.debug " >> #{k} : #{v}"
|
306
|
+
end
|
307
|
+
Flexirest::Logger.debug " >> Body:\n#{@body}"
|
308
|
+
end
|
309
|
+
|
310
|
+
request_options = {:headers => http_headers}
|
311
|
+
if using_api_auth?
|
312
|
+
request_options[:api_auth] = {
|
313
|
+
:api_auth_access_id => api_auth_access_id,
|
314
|
+
:api_auth_secret_key => api_auth_secret_key
|
315
|
+
}
|
316
|
+
end
|
317
|
+
|
318
|
+
case http_method
|
319
|
+
when :get
|
320
|
+
response = connection.get(@url, request_options)
|
321
|
+
when :put
|
322
|
+
response = connection.put(@url, @body, request_options)
|
323
|
+
when :post
|
324
|
+
response = connection.post(@url, @body, request_options)
|
325
|
+
when :delete
|
326
|
+
response = connection.delete(@url, request_options)
|
327
|
+
else
|
328
|
+
raise InvalidRequestException.new("Invalid method #{http_method}")
|
329
|
+
end
|
330
|
+
|
331
|
+
response
|
332
|
+
end
|
333
|
+
|
334
|
+
def handle_cached_response(cached)
|
335
|
+
if cached.result.is_a? Flexirest::ResultIterator
|
336
|
+
cached.result
|
337
|
+
else
|
338
|
+
if object_is_class?
|
339
|
+
cached.result
|
340
|
+
else
|
341
|
+
@object._copy_from(cached.result)
|
342
|
+
@object
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
def handle_response(response, cached = nil)
|
348
|
+
@response = response
|
349
|
+
status = @response.status || 200
|
350
|
+
|
351
|
+
if cached && response.status == 304
|
352
|
+
Flexirest::Logger.debug " \033[1;4;32m#{Flexirest::NAME}\033[0m #{@instrumentation_name}" +
|
353
|
+
' - Etag copy is the same as the server'
|
354
|
+
return handle_cached_response(cached)
|
355
|
+
end
|
356
|
+
|
357
|
+
if (200..399).include?(status)
|
358
|
+
if @method[:options][:plain]
|
359
|
+
return @response = response.body
|
360
|
+
elsif is_json_response? || is_xml_response?
|
361
|
+
if @response.respond_to?(:proxied) && @response.proxied
|
362
|
+
Flexirest::Logger.debug " \033[1;4;32m#{Flexirest::NAME}\033[0m #{@instrumentation_name} - Response was proxied, unable to determine size"
|
363
|
+
else
|
364
|
+
Flexirest::Logger.debug " \033[1;4;32m#{Flexirest::NAME}\033[0m #{@instrumentation_name} - Response received #{@response.body.size} bytes"
|
365
|
+
end
|
366
|
+
result = generate_new_object(ignore_xml_root: @method[:options][:ignore_xml_root])
|
367
|
+
else
|
368
|
+
raise ResponseParseException.new(status:status, body:@response.body)
|
369
|
+
end
|
370
|
+
else
|
371
|
+
if is_json_response? || is_xml_response?
|
372
|
+
error_response = generate_new_object(mutable: false, ignore_xml_root: @method[:options][:ignore_xml_root])
|
373
|
+
else
|
374
|
+
error_response = @response.body
|
375
|
+
end
|
376
|
+
if status == 400
|
377
|
+
raise HTTPBadRequestClientException.new(status:status, result:error_response, url:@url)
|
378
|
+
elsif status == 401
|
379
|
+
raise HTTPUnauthorisedClientException.new(status:status, result:error_response, url:@url)
|
380
|
+
elsif status == 403
|
381
|
+
raise HTTPForbiddenClientException.new(status:status, result:error_response, url:@url)
|
382
|
+
elsif status == 404
|
383
|
+
raise HTTPNotFoundClientException.new(status:status, result:error_response, url:@url)
|
384
|
+
elsif (400..499).include? status
|
385
|
+
raise HTTPClientException.new(status:status, result:error_response, url:@url)
|
386
|
+
elsif (500..599).include? status
|
387
|
+
raise HTTPServerException.new(status:status, result:error_response, url:@url)
|
388
|
+
elsif status == 0
|
389
|
+
raise TimeoutException.new("Timed out getting #{response.url}")
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
result
|
394
|
+
end
|
395
|
+
|
396
|
+
def new_object(attributes, name = nil)
|
397
|
+
@method[:options][:has_many] ||= {}
|
398
|
+
name = name.to_sym rescue nil
|
399
|
+
if @method[:options][:has_many][name]
|
400
|
+
overridden_name = name
|
401
|
+
object = @method[:options][:has_many][name].new
|
402
|
+
elsif @method[:options][:has_one][name]
|
403
|
+
overridden_name = name
|
404
|
+
object = @method[:options][:has_one][name].new
|
405
|
+
else
|
406
|
+
object = create_object_instance
|
407
|
+
end
|
408
|
+
|
409
|
+
if hal_response? && name.nil?
|
410
|
+
attributes = handle_hal_links_embedded(object, attributes)
|
411
|
+
end
|
412
|
+
|
413
|
+
attributes.each do |k,v|
|
414
|
+
k = k.to_sym
|
415
|
+
overridden_name = select_name(k, overridden_name)
|
416
|
+
if @method[:options][:lazy].include?(k)
|
417
|
+
object._attributes[k] = Flexirest::LazyAssociationLoader.new(overridden_name, v, self, overridden_name:(overridden_name))
|
418
|
+
elsif v.is_a? Hash
|
419
|
+
object._attributes[k] = new_object(v, overridden_name )
|
420
|
+
elsif v.is_a? Array
|
421
|
+
object._attributes[k] = Flexirest::ResultIterator.new
|
422
|
+
v.each do |item|
|
423
|
+
if item.is_a? Hash
|
424
|
+
object._attributes[k] << new_object(item, overridden_name)
|
425
|
+
else
|
426
|
+
object._attributes[k] << item
|
427
|
+
end
|
428
|
+
end
|
429
|
+
else
|
430
|
+
if v.to_s[/\d{4}\-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})/]
|
431
|
+
object._attributes[k] = DateTime.parse(v)
|
432
|
+
else
|
433
|
+
object._attributes[k] = v
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
object.clean! unless object_is_class?
|
438
|
+
|
439
|
+
object
|
440
|
+
end
|
441
|
+
|
442
|
+
def hal_response?
|
443
|
+
_, content_type = @response.response_headers.detect{|k,v| k.downcase == "content-type"}
|
444
|
+
faked_response = @response.response_headers.detect{|k,v| k.downcase == "x-arc-faked-response"}
|
445
|
+
if content_type && content_type.respond_to?(:each)
|
446
|
+
content_type.each do |ct|
|
447
|
+
return true if ct[%r{application\/hal\+json}i]
|
448
|
+
return true if ct[%r{application\/json}i]
|
449
|
+
end
|
450
|
+
faked_response
|
451
|
+
elsif content_type && (content_type[%r{application\/hal\+json}i] || content_type[%r{application\/json}i]) || faked_response
|
452
|
+
true
|
453
|
+
else
|
454
|
+
false
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
def handle_hal_links_embedded(object, attributes)
|
459
|
+
attributes["_links"] = attributes[:_links] if attributes[:_links]
|
460
|
+
attributes["_embedded"] = attributes[:_embedded] if attributes[:_embedded]
|
461
|
+
if attributes["_links"]
|
462
|
+
attributes["_links"].each do |key, value|
|
463
|
+
if value.is_a?(Array)
|
464
|
+
object._attributes[key.to_sym] ||= Flexirest::ResultIterator.new
|
465
|
+
value.each do |element|
|
466
|
+
begin
|
467
|
+
embedded_version = attributes["_embedded"][key].detect{|embed| embed["_links"]["self"]["href"] == element["href"]}
|
468
|
+
object._attributes[key.to_sym] << new_object(embedded_version, key)
|
469
|
+
rescue NoMethodError
|
470
|
+
object._attributes[key.to_sym] << Flexirest::LazyAssociationLoader.new(key, element, self)
|
471
|
+
end
|
472
|
+
end
|
473
|
+
else
|
474
|
+
begin
|
475
|
+
embedded_version = attributes["_embedded"][key]
|
476
|
+
object._attributes[key.to_sym] = new_object(embedded_version, key)
|
477
|
+
rescue NoMethodError
|
478
|
+
object._attributes[key.to_sym] = Flexirest::LazyAssociationLoader.new(key, value, self)
|
479
|
+
end
|
480
|
+
end
|
481
|
+
end
|
482
|
+
attributes.delete("_links")
|
483
|
+
attributes.delete("_embedded")
|
484
|
+
end
|
485
|
+
|
486
|
+
attributes
|
487
|
+
end
|
488
|
+
|
489
|
+
private
|
490
|
+
|
491
|
+
def create_object_instance
|
492
|
+
return object_is_class? ? @object.new : @object.class.new
|
493
|
+
end
|
494
|
+
|
495
|
+
def select_name(name, parent_name)
|
496
|
+
if @method[:options][:has_many][name] || @method[:options][:has_one][name]
|
497
|
+
return name
|
498
|
+
end
|
499
|
+
|
500
|
+
parent_name || name
|
501
|
+
end
|
502
|
+
|
503
|
+
def is_json_response?
|
504
|
+
@response.response_headers['Content-Type'].nil? || @response.response_headers['Content-Type'].include?('json')
|
505
|
+
end
|
506
|
+
|
507
|
+
def is_xml_response?
|
508
|
+
@response.response_headers['Content-Type'].include?('xml')
|
509
|
+
end
|
510
|
+
|
511
|
+
def generate_new_object(options={})
|
512
|
+
if @response.body.is_a?(Array) || @response.body.is_a?(Hash)
|
513
|
+
body = @response.body
|
514
|
+
elsif is_json_response?
|
515
|
+
body = @response.body.blank? ? {} : MultiJson.load(@response.body)
|
516
|
+
elsif is_xml_response?
|
517
|
+
body = @response.body.blank? ? {} : Crack::XML.parse(@response.body)
|
518
|
+
if options[:ignore_xml_root]
|
519
|
+
body = body[options[:ignore_xml_root].to_s]
|
520
|
+
end
|
521
|
+
end
|
522
|
+
body = begin
|
523
|
+
@method[:name].nil? ? body : translator.send(@method[:name], body)
|
524
|
+
rescue NoMethodError
|
525
|
+
body
|
526
|
+
end
|
527
|
+
if body.is_a? Array
|
528
|
+
result = Flexirest::ResultIterator.new(@response)
|
529
|
+
body.each do |json_object|
|
530
|
+
result << new_object(json_object, @overridden_name)
|
531
|
+
end
|
532
|
+
else
|
533
|
+
result = new_object(body, @overridden_name)
|
534
|
+
result._status = @response.status
|
535
|
+
result._headers = @response.response_headers
|
536
|
+
result._etag = @response.response_headers['ETag']
|
537
|
+
if !object_is_class? && options[:mutable] != false
|
538
|
+
@object._copy_from(result)
|
539
|
+
@object._clean!
|
540
|
+
result = @object
|
541
|
+
end
|
542
|
+
end
|
543
|
+
result
|
544
|
+
end
|
545
|
+
end
|
546
|
+
|
547
|
+
class RequestException < StandardError ; end
|
548
|
+
|
549
|
+
class InvalidRequestException < RequestException ; end
|
550
|
+
class ResponseParseException < RequestException
|
551
|
+
attr_accessor :status, :body
|
552
|
+
def initialize(options)
|
553
|
+
@status = options[:status]
|
554
|
+
@body = options[:body]
|
555
|
+
end
|
556
|
+
end
|
557
|
+
|
558
|
+
class HTTPException < RequestException
|
559
|
+
attr_accessor :status, :result, :request_url
|
560
|
+
def initialize(options)
|
561
|
+
@status = options[:status]
|
562
|
+
@result = options[:result]
|
563
|
+
@request_url = options[:url]
|
564
|
+
end
|
565
|
+
end
|
566
|
+
class HTTPClientException < HTTPException ; end
|
567
|
+
class HTTPUnauthorisedClientException < HTTPClientException ; end
|
568
|
+
class HTTPBadRequestClientException < HTTPClientException ; end
|
569
|
+
class HTTPForbiddenClientException < HTTPClientException ; end
|
570
|
+
class HTTPNotFoundClientException < HTTPClientException ; end
|
571
|
+
class HTTPServerException < HTTPException ; end
|
572
|
+
|
573
|
+
end
|