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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +2 -0
  4. data/.simplecov +4 -0
  5. data/.travis.yml +6 -0
  6. data/CHANGELOG.md +37 -0
  7. data/CONTRIBUTING.md +62 -0
  8. data/Gemfile +4 -0
  9. data/Guardfile +9 -0
  10. data/LICENSE.txt +22 -0
  11. data/README.md +846 -0
  12. data/Rakefile +13 -0
  13. data/doc/ActiveRestClient Internals.graffle +1236 -0
  14. data/doc/ActiveRestClient Internals.png +0 -0
  15. data/flexirest.gemspec +39 -0
  16. data/lib/flexirest.rb +25 -0
  17. data/lib/flexirest/base.rb +189 -0
  18. data/lib/flexirest/caching.rb +92 -0
  19. data/lib/flexirest/configuration.rb +209 -0
  20. data/lib/flexirest/connection.rb +103 -0
  21. data/lib/flexirest/connection_manager.rb +36 -0
  22. data/lib/flexirest/headers_list.rb +47 -0
  23. data/lib/flexirest/instrumentation.rb +62 -0
  24. data/lib/flexirest/lazy_association_loader.rb +97 -0
  25. data/lib/flexirest/lazy_loader.rb +23 -0
  26. data/lib/flexirest/logger.rb +67 -0
  27. data/lib/flexirest/mapping.rb +69 -0
  28. data/lib/flexirest/monkey_patching.rb +7 -0
  29. data/lib/flexirest/proxy_base.rb +193 -0
  30. data/lib/flexirest/recording.rb +24 -0
  31. data/lib/flexirest/request.rb +573 -0
  32. data/lib/flexirest/request_delegator.rb +44 -0
  33. data/lib/flexirest/request_filtering.rb +62 -0
  34. data/lib/flexirest/result_iterator.rb +85 -0
  35. data/lib/flexirest/validation.rb +60 -0
  36. data/lib/flexirest/version.rb +3 -0
  37. data/spec/lib/base_spec.rb +389 -0
  38. data/spec/lib/caching_spec.rb +217 -0
  39. data/spec/lib/configuration_spec.rb +234 -0
  40. data/spec/lib/connection_manager_spec.rb +43 -0
  41. data/spec/lib/connection_spec.rb +159 -0
  42. data/spec/lib/headers_list_spec.rb +61 -0
  43. data/spec/lib/instrumentation_spec.rb +58 -0
  44. data/spec/lib/lazy_association_loader_spec.rb +135 -0
  45. data/spec/lib/lazy_loader_spec.rb +25 -0
  46. data/spec/lib/logger_spec.rb +63 -0
  47. data/spec/lib/mapping_spec.rb +52 -0
  48. data/spec/lib/proxy_spec.rb +189 -0
  49. data/spec/lib/recording_spec.rb +34 -0
  50. data/spec/lib/request_filtering_spec.rb +84 -0
  51. data/spec/lib/request_spec.rb +711 -0
  52. data/spec/lib/result_iterator_spec.rb +140 -0
  53. data/spec/lib/validation_spec.rb +113 -0
  54. data/spec/lib/xml_spec.rb +74 -0
  55. data/spec/spec_helper.rb +88 -0
  56. 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