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,103 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
|
3
|
+
module Flexirest
|
4
|
+
|
5
|
+
class TimeoutException < StandardError ; end
|
6
|
+
class ConnectionFailedException < StandardError ; end
|
7
|
+
|
8
|
+
class Connection
|
9
|
+
attr_accessor :session, :base_url
|
10
|
+
|
11
|
+
def initialize(base_url)
|
12
|
+
@base_url = base_url
|
13
|
+
@session = new_session
|
14
|
+
end
|
15
|
+
|
16
|
+
def reconnect
|
17
|
+
@session = new_session
|
18
|
+
end
|
19
|
+
|
20
|
+
def headers
|
21
|
+
@session.headers
|
22
|
+
end
|
23
|
+
|
24
|
+
def make_safe_request(path, &block)
|
25
|
+
block.call
|
26
|
+
rescue Faraday::Error::TimeoutError
|
27
|
+
raise Flexirest::TimeoutException.new("Timed out getting #{full_url(path)}")
|
28
|
+
rescue Faraday::Error::ConnectionFailed
|
29
|
+
begin
|
30
|
+
reconnect
|
31
|
+
block.call
|
32
|
+
rescue Faraday::Error::ConnectionFailed
|
33
|
+
raise Flexirest::ConnectionFailedException.new("Unable to connect to #{full_url(path)}")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def get(path, options={})
|
38
|
+
set_defaults(options)
|
39
|
+
make_safe_request(path) do
|
40
|
+
@session.get(path) do |req|
|
41
|
+
req.headers = req.headers.merge(options[:headers])
|
42
|
+
sign_request(req, options[:api_auth])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def put(path, data, options={})
|
48
|
+
set_defaults(options)
|
49
|
+
make_safe_request(path) do
|
50
|
+
@session.put(path) do |req|
|
51
|
+
req.headers = req.headers.merge(options[:headers])
|
52
|
+
req.body = data
|
53
|
+
sign_request(req, options[:api_auth])
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def post(path, data, options={})
|
59
|
+
set_defaults(options)
|
60
|
+
make_safe_request(path) do
|
61
|
+
@session.post(path) do |req|
|
62
|
+
req.headers = req.headers.merge(options[:headers])
|
63
|
+
req.body = data
|
64
|
+
sign_request(req, options[:api_auth])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def delete(path, options={})
|
70
|
+
set_defaults(options)
|
71
|
+
make_safe_request(path) do
|
72
|
+
@session.delete(path) do |req|
|
73
|
+
req.headers = req.headers.merge(options[:headers])
|
74
|
+
sign_request(req, options[:api_auth])
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def new_session
|
82
|
+
Faraday.new({url: @base_url}, &Flexirest::Base.faraday_config)
|
83
|
+
end
|
84
|
+
|
85
|
+
def full_url(path)
|
86
|
+
@session.build_url(path).to_s
|
87
|
+
end
|
88
|
+
|
89
|
+
def set_defaults(options)
|
90
|
+
options[:headers] ||= {}
|
91
|
+
options[:api_auth] ||= {}
|
92
|
+
return options
|
93
|
+
end
|
94
|
+
|
95
|
+
def sign_request(request, api_auth)
|
96
|
+
return if api_auth[:api_auth_access_id].nil? || api_auth[:api_auth_secret_key].nil?
|
97
|
+
ApiAuth.sign!(
|
98
|
+
request,
|
99
|
+
api_auth[:api_auth_access_id],
|
100
|
+
api_auth[:api_auth_secret_key])
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Flexirest
|
2
|
+
class ConnectionManager
|
3
|
+
def self.reset!
|
4
|
+
Thread.current[:_connections]={}
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.get_connection(base_url)
|
8
|
+
raise Exception.new("Nil base URL passed to ConnectionManager.get_connection") if base_url.nil?
|
9
|
+
Thread.current[:_connections] ||= {}
|
10
|
+
Thread.current[:_connections][base_url] ||= Connection.new(base_url)
|
11
|
+
Thread.current[:_connections][base_url]
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.find_connection_for_url(url)
|
15
|
+
Thread.current[:_connections] ||= {}
|
16
|
+
found = Thread.current[:_connections].keys.detect {|key| url[0,key.length] == key}
|
17
|
+
Thread.current[:_connections][found] if found
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.in_parallel(base_url)
|
21
|
+
begin
|
22
|
+
require 'typhoeus'
|
23
|
+
require 'typhoeus/adapters/faraday'
|
24
|
+
rescue LoadError
|
25
|
+
raise MissingOptionalLibraryError.new("To call '::Flexirest::ConnectionManager.in_parallel' you must include the gem 'Typhoeus' in your Gemfile.")
|
26
|
+
end
|
27
|
+
session = ConnectionManager.get_connection(base_url).session
|
28
|
+
session.in_parallel do
|
29
|
+
yield
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
class MissingOptionalLibraryError < StandardError ; end
|
36
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Flexirest
|
2
|
+
class HeadersList
|
3
|
+
STORE_MULTIPLE_VALUES = ["set-cookie"]
|
4
|
+
def initialize
|
5
|
+
@store = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def []=(key,value)
|
9
|
+
key = find_existing(key)
|
10
|
+
if STORE_MULTIPLE_VALUES.include?(key.downcase)
|
11
|
+
@store[key] ||= []
|
12
|
+
@store[key] << value
|
13
|
+
else
|
14
|
+
@store[key] = value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def [](key)
|
19
|
+
key = find_existing(key)
|
20
|
+
@store[key]
|
21
|
+
end
|
22
|
+
|
23
|
+
def each(split_multiple_headers = false)
|
24
|
+
@store.keys.each do |key|
|
25
|
+
value = @store[key]
|
26
|
+
if value.is_a?(Array) && split_multiple_headers
|
27
|
+
value.each do |inner_value|
|
28
|
+
yield(key, inner_value)
|
29
|
+
end
|
30
|
+
else
|
31
|
+
yield(key, value)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def find_existing(key)
|
39
|
+
key_downcase = key.downcase
|
40
|
+
@store.keys.each do |found_key|
|
41
|
+
return found_key if found_key.downcase == key_downcase
|
42
|
+
end
|
43
|
+
key
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Flexirest
|
2
|
+
class Instrumentation < ActiveSupport::LogSubscriber
|
3
|
+
def request_call(event)
|
4
|
+
self.class.time_spent += event.duration
|
5
|
+
self.class.calls_made += 1
|
6
|
+
name = '%s (%.1fms)' % [Flexirest::NAME, event.duration]
|
7
|
+
Flexirest::Logger.debug " \033[1;4;32m#{name}\033[0m #{event.payload[:name]}"
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.time_spent=(value)
|
11
|
+
@@time_spent = value
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.time_spent
|
15
|
+
@@time_spent ||= 0
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.calls_made=(value)
|
19
|
+
@@calls_made = value
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.calls_made
|
23
|
+
@@calls_made ||= 0
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.reset
|
27
|
+
@@time_spent = 0
|
28
|
+
@@calls_made = 0
|
29
|
+
end
|
30
|
+
|
31
|
+
def logger
|
32
|
+
Flexirest::Logger
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module ControllerInstrumentation
|
37
|
+
extend ActiveSupport::Concern
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
def append_info_to_payload(payload)
|
42
|
+
super
|
43
|
+
payload[:flexirest_time_spent] = Flexirest::Instrumentation.time_spent
|
44
|
+
payload[:flexirest_calls_made] = Flexirest::Instrumentation.calls_made
|
45
|
+
end
|
46
|
+
|
47
|
+
module ClassMethods
|
48
|
+
def log_process_action(payload)
|
49
|
+
messages, time_spent, calls_made = super, payload[:flexirest_time_spent], payload[:flexirest_calls_made]
|
50
|
+
messages << ("#{Flexirest::NAME}: %.1fms for %d calls" % [time_spent.to_f, calls_made]) if calls_made
|
51
|
+
Flexirest::Instrumentation.reset
|
52
|
+
messages
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
Flexirest::Instrumentation.attach_to :flexirest
|
59
|
+
|
60
|
+
ActiveSupport.on_load(:action_controller) do
|
61
|
+
include Flexirest::ControllerInstrumentation
|
62
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'active_support/hash_with_indifferent_access'
|
2
|
+
|
3
|
+
module Flexirest
|
4
|
+
class LazyAssociationLoader
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize(name, value, request, options = {})
|
8
|
+
@name = name
|
9
|
+
class_to_map = request.method[:options][:lazy][name] rescue nil
|
10
|
+
@request = class_to_map.nil? ? request : Flexirest::Request.new(class_to_map._mapped_method(:find), class_to_map.new, options)
|
11
|
+
@object = nil
|
12
|
+
@options = options
|
13
|
+
if value.is_a? Array
|
14
|
+
@subloaders = value.map {|url| LazyAssociationLoader.new(name, url, request, options)}
|
15
|
+
elsif value.is_a?(Hash) && (value.has_key?("url") || value.has_key?(:url))
|
16
|
+
@url = (value["url"] || value[:url])
|
17
|
+
elsif value.is_a?(Hash) && (value.has_key?("href") || value.has_key?(:href)) # HAL
|
18
|
+
@url = (value["href"] || value[:href])
|
19
|
+
@_hal_attributes = HashWithIndifferentAccess.new(value)
|
20
|
+
elsif value.is_a?(Hash)
|
21
|
+
mapped = {}
|
22
|
+
value.each do |k,v|
|
23
|
+
mapped[k.to_sym] = LazyAssociationLoader.new(name, v, request, options)
|
24
|
+
end
|
25
|
+
@subloaders = mapped
|
26
|
+
# Need to also ensure that the hash/wrapped object is returned when the property is accessed
|
27
|
+
elsif value.is_a? String
|
28
|
+
@url = value
|
29
|
+
else
|
30
|
+
raise InvalidLazyAssociationContentException.new("Invalid content for #{@name}, expected Array, String or Hash containing 'url' key")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def _hal_attributes(key)
|
35
|
+
@_hal_attributes[key]
|
36
|
+
end
|
37
|
+
|
38
|
+
def size
|
39
|
+
if @subloaders
|
40
|
+
@subloaders.size
|
41
|
+
else
|
42
|
+
ensure_lazy_loaded
|
43
|
+
@object.size
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def each
|
48
|
+
if @subloaders
|
49
|
+
if @subloaders.is_a? Array
|
50
|
+
@subloaders.each do |loader|
|
51
|
+
yield loader
|
52
|
+
end
|
53
|
+
elsif @subloaders.is_a? Hash
|
54
|
+
@subloaders.each do |key,value|
|
55
|
+
yield key, value
|
56
|
+
end
|
57
|
+
end
|
58
|
+
else
|
59
|
+
ensure_lazy_loaded
|
60
|
+
@object.each do |obj|
|
61
|
+
yield obj
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def keys
|
67
|
+
@subloaders.keys
|
68
|
+
end
|
69
|
+
|
70
|
+
def method_missing(name, *args)
|
71
|
+
if @subloaders.is_a? Hash
|
72
|
+
return @subloaders[name.to_sym]
|
73
|
+
end
|
74
|
+
ensure_lazy_loaded
|
75
|
+
if @object
|
76
|
+
@object.send(name, *args)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def ensure_lazy_loaded
|
83
|
+
if @object.nil?
|
84
|
+
method = MultiJson.load(MultiJson.dump(@request.method),:symbolize_keys => true)
|
85
|
+
method[:method] = :get
|
86
|
+
method[:options][:url] = @url
|
87
|
+
method[:options][:overridden_name] = @options[:overridden_name]
|
88
|
+
request = Flexirest::Request.new(method, @request.object)
|
89
|
+
request.url = request.forced_url = @url
|
90
|
+
@object = request.call
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class InvalidLazyAssociationContentException < Exception ; end
|
96
|
+
|
97
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Flexirest
|
2
|
+
class LazyLoader
|
3
|
+
def initialize(request, params = nil)
|
4
|
+
@request = request
|
5
|
+
@params = params
|
6
|
+
@result = nil
|
7
|
+
end
|
8
|
+
|
9
|
+
def method_missing(name, *args)
|
10
|
+
if @result.nil?
|
11
|
+
@result = @request.call(@params)
|
12
|
+
end
|
13
|
+
@result.send(name, *args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def respond_to?(name)
|
17
|
+
if @result.nil?
|
18
|
+
@result = @request.call(@params)
|
19
|
+
end
|
20
|
+
@result.respond_to?(name)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Flexirest
|
2
|
+
class Logger
|
3
|
+
@logfile = nil
|
4
|
+
@messages = []
|
5
|
+
|
6
|
+
def self.logfile=(value)
|
7
|
+
@logfile = value
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.messages
|
11
|
+
@messages
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.reset!
|
15
|
+
@logfile = nil
|
16
|
+
@messages = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.debug(message)
|
20
|
+
if defined?(Rails) && Rails.respond_to?(:logger)
|
21
|
+
Rails.logger.debug(message)
|
22
|
+
elsif @logfile
|
23
|
+
File.open(@logfile, "a") do |f|
|
24
|
+
f << "#{message}\n"
|
25
|
+
end
|
26
|
+
else
|
27
|
+
@messages << message
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.info(message)
|
32
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
33
|
+
Rails.logger.info(message)
|
34
|
+
elsif @logfile
|
35
|
+
File.open(@logfile, "a") do |f|
|
36
|
+
f << "#{message}\n"
|
37
|
+
end
|
38
|
+
else
|
39
|
+
@messages << message
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.warn(message)
|
44
|
+
if defined?(Rails) && Rails.respond_to?(:logger)
|
45
|
+
Rails.logger.warn(message)
|
46
|
+
elsif @logfile
|
47
|
+
File.open(@logfile, "a") do |f|
|
48
|
+
f << "#{message}\n"
|
49
|
+
end
|
50
|
+
else
|
51
|
+
@messages << message
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.error(message)
|
56
|
+
if defined?(Rails) && Rails.respond_to?(:logger)
|
57
|
+
Rails.logger.error(message)
|
58
|
+
elsif @logfile
|
59
|
+
File.open(@logfile, "a") do |f|
|
60
|
+
f << "#{message}\n"
|
61
|
+
end
|
62
|
+
else
|
63
|
+
@messages << message
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Flexirest
|
2
|
+
module Mapping
|
3
|
+
module ClassMethods
|
4
|
+
def get(name, url, options = {})
|
5
|
+
_map_call(name, url:url, method: :get, options:options)
|
6
|
+
end
|
7
|
+
|
8
|
+
def put(name, url, options = {})
|
9
|
+
_map_call(name, url:url, method: :put, options:options)
|
10
|
+
end
|
11
|
+
|
12
|
+
def post(name, url, options = {})
|
13
|
+
_map_call(name, url:url, method: :post, options:options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def delete(name, url, options = {})
|
17
|
+
_map_call(name, url:url, method: :delete, options:options)
|
18
|
+
end
|
19
|
+
|
20
|
+
def patch(name, url, options = {})
|
21
|
+
_map_call(name, url:url, method: :patch, options:options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def _map_call(name, details)
|
25
|
+
_calls[name] = {name:name}.merge(details)
|
26
|
+
_calls["lazy_#{name}".to_sym] = {name:name}.merge(details)
|
27
|
+
self.class.send(:define_method, name) do |options={}|
|
28
|
+
_call(name, options)
|
29
|
+
end
|
30
|
+
self.class.send(:define_method, "lazy_#{name}".to_sym) do |options={}|
|
31
|
+
_call("lazy_#{name}", options)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def _call(name, options)
|
36
|
+
mapped = _calls[name]
|
37
|
+
lazy_forced = false
|
38
|
+
if mapped.nil? && name.to_s[/^lazy_/]
|
39
|
+
mapped = _calls[name.to_s.gsub(/^lazy_/, '').to_sym]
|
40
|
+
lazy_forced = true
|
41
|
+
end
|
42
|
+
request = Request.new(mapped, self, options)
|
43
|
+
if lazy_load? || lazy_forced
|
44
|
+
Flexirest::LazyLoader.new(request)
|
45
|
+
else
|
46
|
+
request.call
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def _calls
|
51
|
+
@_calls
|
52
|
+
end
|
53
|
+
|
54
|
+
def _mapped_method(name)
|
55
|
+
_calls[name]
|
56
|
+
end
|
57
|
+
|
58
|
+
def inherited(subclass)
|
59
|
+
subclass.instance_variable_set(:@_calls, {})
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.included(base)
|
65
|
+
base.extend(ClassMethods)
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|