rester 0.4.1 → 0.4.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.
- checksums.yaml +4 -4
- data/lib/rester/client/adapters/adapter.rb +9 -7
- data/lib/rester/client/adapters/http_adapter/connection.rb +28 -9
- data/lib/rester/client/adapters/http_adapter.rb +2 -2
- data/lib/rester/client/adapters/local_adapter.rb +13 -8
- data/lib/rester/client/adapters/stub_adapter.rb +10 -6
- data/lib/rester/client/adapters.rb +25 -0
- data/lib/rester/client.rb +51 -5
- data/lib/rester/errors.rb +11 -1
- data/lib/rester/rspec.rb +21 -1
- data/lib/rester/service/resource/params.rb +48 -11
- data/lib/rester/utils/circuit_breaker.rb +137 -0
- data/lib/rester/utils/rspec.rb +49 -0
- data/lib/rester/utils/stub_file.rb +17 -0
- data/lib/rester/utils.rb +6 -3
- data/lib/rester/version.rb +1 -1
- data/lib/rester.rb +2 -3
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed31a2e9f930e5607d9905098c61fae665750d3a
|
4
|
+
data.tar.gz: a3cfb90ab6eb177fd2291d5bd6e5013014023f63
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fb965a25b5f15759c4998897c831a4959d512d6b89225d8228879ed2e8185cb99397617417ee54b7c8e2b69444d5f8af1ca9ba8a903e7fd39b70d2ea407ccb58
|
7
|
+
data.tar.gz: a9fc47ffea8f0a68e66f8577e82e1733d06639c6ba7a8275121ddd92deee1b131ee42ca1192cb492e65b86fd0d7e3530c58abb8b2db190e330697011b333caa7
|
@@ -1,7 +1,6 @@
|
|
1
1
|
module Rester
|
2
2
|
module Client::Adapters
|
3
3
|
class Adapter
|
4
|
-
|
5
4
|
class << self
|
6
5
|
##
|
7
6
|
# Returns whether or not the Adapter can connect to the service
|
@@ -10,8 +9,11 @@ module Rester
|
|
10
9
|
end
|
11
10
|
end # Class Methods
|
12
11
|
|
13
|
-
|
14
|
-
|
12
|
+
attr_reader :timeout
|
13
|
+
|
14
|
+
def initialize(service=nil, opts={})
|
15
|
+
@timeout = opts[:timeout]
|
16
|
+
connect(service) if service
|
15
17
|
end
|
16
18
|
|
17
19
|
##
|
@@ -34,7 +36,7 @@ module Rester
|
|
34
36
|
raise NotImplementedError
|
35
37
|
end
|
36
38
|
|
37
|
-
def request(verb, path, params={}
|
39
|
+
def request(verb, path, params={})
|
38
40
|
params ||= {}
|
39
41
|
_validate_verb(verb)
|
40
42
|
params = _validate_params(params)
|
@@ -44,14 +46,14 @@ module Rester
|
|
44
46
|
[:get, :post, :put, :delete].each do |verb|
|
45
47
|
##
|
46
48
|
# Define helper methods: get, post, put, delete
|
47
|
-
define_method(verb) { |*args
|
48
|
-
request(verb, *args
|
49
|
+
define_method(verb) { |*args|
|
50
|
+
request(verb, *args)
|
49
51
|
}
|
50
52
|
|
51
53
|
##
|
52
54
|
# Define implementation methods: get!, post!, put!, delete!
|
53
55
|
# These methods should be overridden by the specific adapter.
|
54
|
-
define_method("#{verb}!") { |*args
|
56
|
+
define_method("#{verb}!") { |*args|
|
55
57
|
raise NotImplementedError
|
56
58
|
}
|
57
59
|
end
|
@@ -10,40 +10,57 @@ module Rester
|
|
10
10
|
}.freeze
|
11
11
|
|
12
12
|
attr_reader :url
|
13
|
+
attr_reader :timeout
|
13
14
|
|
14
|
-
def initialize(url)
|
15
|
+
def initialize(url, opts={})
|
15
16
|
@url = url.is_a?(String) ? URI(url) : url
|
16
17
|
@url.path = @url.path[0..-2] if @url.path[-1] == '/'
|
18
|
+
@timeout = opts[:timeout]
|
17
19
|
end
|
18
20
|
|
19
21
|
def get(path, params={})
|
20
|
-
|
22
|
+
_request(
|
23
|
+
:get,
|
21
24
|
_path(path, params[:query]),
|
22
25
|
_prepare_headers(params[:headers])
|
23
26
|
)
|
24
27
|
end
|
25
28
|
|
26
29
|
def delete(path, params={})
|
27
|
-
|
30
|
+
_request(
|
31
|
+
:delete,
|
28
32
|
_path(path, params[:query]),
|
29
33
|
_prepare_headers(params[:headers])
|
30
34
|
)
|
31
35
|
end
|
32
36
|
|
33
37
|
def put(path, params={})
|
34
|
-
|
35
|
-
|
36
|
-
|
38
|
+
_request(
|
39
|
+
:put,
|
40
|
+
_path(path),
|
41
|
+
_prepare_data_headers(params[:headers]),
|
42
|
+
_encode_data(params[:data])
|
43
|
+
)
|
37
44
|
end
|
38
45
|
|
39
46
|
def post(path, params={})
|
40
|
-
|
41
|
-
|
42
|
-
|
47
|
+
_request(
|
48
|
+
:post,
|
49
|
+
_path(path),
|
50
|
+
_prepare_data_headers(params[:headers]),
|
51
|
+
_encode_data(params[:data])
|
52
|
+
)
|
43
53
|
end
|
44
54
|
|
45
55
|
private
|
46
56
|
|
57
|
+
def _request(verb, path, headers={}, data='')
|
58
|
+
data = nil if [:get, :delete].include?(verb)
|
59
|
+
_http.public_send(verb, *[path, data, headers].compact)
|
60
|
+
rescue Net::ReadTimeout, Net::OpenTimeout
|
61
|
+
fail Errors::TimeoutError
|
62
|
+
end
|
63
|
+
|
47
64
|
def _path(path, query=nil)
|
48
65
|
u = url.dup
|
49
66
|
u.path = Utils.join_paths(u.path, path)
|
@@ -68,6 +85,8 @@ module Rester
|
|
68
85
|
s.set_default_paths
|
69
86
|
}
|
70
87
|
end
|
88
|
+
|
89
|
+
http.open_timeout = http.read_timeout = timeout
|
71
90
|
}
|
72
91
|
end
|
73
92
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'stringio'
|
2
2
|
require 'rack'
|
3
|
+
require 'timeout'
|
3
4
|
|
4
5
|
module Rester
|
5
6
|
module Client::Adapters
|
@@ -15,7 +16,7 @@ module Rester
|
|
15
16
|
end
|
16
17
|
end # Class Methods
|
17
18
|
|
18
|
-
def connect(service
|
19
|
+
def connect(service)
|
19
20
|
nil.tap { @service = service }
|
20
21
|
end
|
21
22
|
|
@@ -45,13 +46,15 @@ module Rester
|
|
45
46
|
body = URI.encode_www_form(opts[:data] || {})
|
46
47
|
query = URI.encode_www_form(opts[:query] || {})
|
47
48
|
|
48
|
-
response =
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
49
|
+
response = Timeout::timeout(timeout) do
|
50
|
+
service.call(
|
51
|
+
'REQUEST_METHOD' => verb.to_s.upcase,
|
52
|
+
'PATH_INFO' => path,
|
53
|
+
'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
|
54
|
+
'QUERY_STRING' => query,
|
55
|
+
'rack.input' => StringIO.new(body)
|
56
|
+
)
|
57
|
+
end
|
55
58
|
|
56
59
|
body = response.last
|
57
60
|
body = body.body if body.respond_to?(:body)
|
@@ -62,6 +65,8 @@ module Rester
|
|
62
65
|
response.first, # The status code
|
63
66
|
body # The response body.
|
64
67
|
]
|
68
|
+
rescue Timeout::Error
|
69
|
+
fail Errors::TimeoutError
|
65
70
|
end
|
66
71
|
end # LocalAdapter
|
67
72
|
end # Client::Adapters
|
@@ -8,6 +8,9 @@ module Rester
|
|
8
8
|
# An adapter to be used to stub the responses needed from specified service
|
9
9
|
# requests via a yaml file. This will be used in spec tests to perform
|
10
10
|
# "contractual testing"
|
11
|
+
#
|
12
|
+
# Note, this does not implement the "timeout" feature defined by the adapter
|
13
|
+
# interface.
|
11
14
|
class StubAdapter < Adapter
|
12
15
|
attr_reader :stub
|
13
16
|
|
@@ -17,7 +20,9 @@ module Rester
|
|
17
20
|
end
|
18
21
|
end # Class Methods
|
19
22
|
|
20
|
-
|
23
|
+
##
|
24
|
+
# Connects to the StubFile.
|
25
|
+
def connect(stub_filepath)
|
21
26
|
@stub = Utils::StubFile.new(stub_filepath)
|
22
27
|
end
|
23
28
|
|
@@ -60,13 +65,13 @@ module Rester
|
|
60
65
|
|
61
66
|
context = @_context || _find_context_by_params(path, verb, params)
|
62
67
|
|
63
|
-
unless (
|
68
|
+
unless (spec = stub[path][verb][context])
|
64
69
|
fail Errors::StubError,
|
65
70
|
"#{verb} #{path} with context '#{context}' not found"
|
66
71
|
end
|
67
72
|
|
68
73
|
# Verify body, if there is one
|
69
|
-
unless (request =
|
74
|
+
unless (request = spec['request']) == params
|
70
75
|
fail Errors::StubError,
|
71
76
|
"#{verb} #{path} with context '#{context}' params don't match stub. Expected: #{request} Got: #{params}"
|
72
77
|
end
|
@@ -80,9 +85,8 @@ module Rester
|
|
80
85
|
# Find the first request object with the same params as what's passed in.
|
81
86
|
# Useful for testing without having to set the context.
|
82
87
|
def _find_context_by_params(path, verb, params)
|
83
|
-
(stub[path][verb].find { |_,
|
84
|
-
|
85
|
-
} || []).first
|
88
|
+
(stub[path][verb].find { |_, spec| spec['request'] == params } || []
|
89
|
+
).first
|
86
90
|
end
|
87
91
|
end # StubAdapter
|
88
92
|
end # Client::Adapters
|
@@ -6,11 +6,36 @@ module Rester
|
|
6
6
|
autoload(:LocalAdapter, 'rester/client/adapters/local_adapter')
|
7
7
|
autoload(:StubAdapter, 'rester/client/adapters/stub_adapter')
|
8
8
|
|
9
|
+
##
|
10
|
+
# Default connection options.
|
11
|
+
DEFAULT_OPTS = {
|
12
|
+
timeout: 10 # time in seconds (may be float)
|
13
|
+
}.freeze
|
14
|
+
|
9
15
|
class << self
|
16
|
+
##
|
17
|
+
# Returns a list of available adapter classes.
|
10
18
|
def list
|
11
19
|
constants.map { |c| const_get(c) }
|
12
20
|
.select { |c| c.is_a?(Class) && c < Adapter }
|
13
21
|
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Returns an instance of the appropriate adapter that is connected to
|
25
|
+
# the service.
|
26
|
+
def connect(service, opts={})
|
27
|
+
klass = list.find { |a| a.can_connect_to?(service) }
|
28
|
+
fail "unable to connect to #{service.inspect}" unless klass
|
29
|
+
klass.new(service, opts)
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Given a hash, extracts the options that are part of the adapter
|
34
|
+
# interface.
|
35
|
+
def extract_opts(opts={})
|
36
|
+
sel = proc { |k, _| DEFAULT_OPTS.keys.include?(k) }
|
37
|
+
DEFAULT_OPTS.merge(opts.select(&sel).tap { opts.delete_if(&sel) })
|
38
|
+
end
|
14
39
|
end
|
15
40
|
end
|
16
41
|
end
|
data/lib/rester/client.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'json'
|
2
2
|
require 'active_support/inflector'
|
3
|
+
require 'logger'
|
3
4
|
|
4
5
|
module Rester
|
5
6
|
class Client
|
@@ -9,11 +10,19 @@ module Rester
|
|
9
10
|
|
10
11
|
attr_reader :adapter
|
11
12
|
attr_reader :version
|
13
|
+
attr_reader :error_threshold
|
14
|
+
attr_reader :retry_period
|
15
|
+
attr_reader :logger
|
12
16
|
|
13
17
|
def initialize(adapter, params={})
|
14
18
|
self.adapter = adapter
|
15
|
-
|
19
|
+
self.version = params[:version]
|
20
|
+
@error_threshold = (params[:error_threshold] || 3).to_i
|
21
|
+
@retry_period = (params[:retry_period] || 1).to_f
|
22
|
+
@logger = params[:logger] || Logger.new(STDOUT)
|
23
|
+
|
16
24
|
@_resource = Resource.new(self)
|
25
|
+
_init_request_breaker
|
17
26
|
end
|
18
27
|
|
19
28
|
def connect(*args)
|
@@ -24,10 +33,13 @@ module Rester
|
|
24
33
|
adapter.connected? && adapter.get('/ping').first == 200
|
25
34
|
end
|
26
35
|
|
27
|
-
def request(verb, path, params={}
|
28
|
-
path
|
29
|
-
|
30
|
-
|
36
|
+
def request(verb, path, params={})
|
37
|
+
@_request_breaker.call(verb, path, params)
|
38
|
+
rescue Utils::CircuitBreaker::CircuitOpenError
|
39
|
+
# Translate this error so it's easier handle for clients.
|
40
|
+
# Also, at some point we may want to extract CircuitBreaker into its own
|
41
|
+
# gem, and this will make that easier.
|
42
|
+
raise Errors::CircuitOpenError
|
31
43
|
end
|
32
44
|
|
33
45
|
##
|
@@ -42,6 +54,12 @@ module Rester
|
|
42
54
|
@adapter = adapter
|
43
55
|
end
|
44
56
|
|
57
|
+
def version=(version)
|
58
|
+
unless (@version = (version || 1).to_i) > 0
|
59
|
+
fail ArgumentError, 'version must be > 0'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
45
63
|
private
|
46
64
|
|
47
65
|
##
|
@@ -50,6 +68,34 @@ module Rester
|
|
50
68
|
@_resource.send(:method_missing, meth, *args, &block)
|
51
69
|
end
|
52
70
|
|
71
|
+
##
|
72
|
+
# Sets up the circuit breaker for making requests to the service.
|
73
|
+
#
|
74
|
+
# Any exception raised by the `_request` method will count as a failure for
|
75
|
+
# the circuit breaker. Once the threshold for errors has been reached, the
|
76
|
+
# circuit opens and all subsequent requests will raise a CircuitOpenError.
|
77
|
+
#
|
78
|
+
# When the circuit is opened or closed, a message is sent to the logger for
|
79
|
+
# the client.
|
80
|
+
def _init_request_breaker
|
81
|
+
@_request_breaker = Utils::CircuitBreaker.new(
|
82
|
+
threshold: error_threshold, retry_period: retry_period
|
83
|
+
) { |*args| _request(*args) }
|
84
|
+
|
85
|
+
@_request_breaker.on_open do
|
86
|
+
logger.error("circuit opened")
|
87
|
+
end
|
88
|
+
|
89
|
+
@_request_breaker.on_close do
|
90
|
+
logger.info("circuit closed")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def _request(verb, path, params)
|
95
|
+
path = _path_with_version(path)
|
96
|
+
_process_response(path, *adapter.request(verb, path, params))
|
97
|
+
end
|
98
|
+
|
53
99
|
def _path_with_version(path)
|
54
100
|
Utils.join_paths("/v#{version}", path)
|
55
101
|
end
|
data/lib/rester/errors.rb
CHANGED
@@ -12,7 +12,17 @@ module Rester
|
|
12
12
|
|
13
13
|
class Error < StandardError; end
|
14
14
|
class MethodError < Error; end
|
15
|
-
class MethodDefinitionError <
|
15
|
+
class MethodDefinitionError < MethodError; end
|
16
|
+
|
17
|
+
################
|
18
|
+
# Adapter Errors
|
19
|
+
class AdapterError < Error; end
|
20
|
+
class TimeoutError < AdapterError; end
|
21
|
+
|
22
|
+
###############
|
23
|
+
# Client Errors
|
24
|
+
class ClientError < Error; end
|
25
|
+
class CircuitOpenError < ClientError; end
|
16
26
|
|
17
27
|
#############
|
18
28
|
# Stub Errors
|
data/lib/rester/rspec.rb
CHANGED
@@ -1,5 +1,25 @@
|
|
1
1
|
require 'json'
|
2
2
|
|
3
|
+
##
|
4
|
+
# include_stub_response custom matcher which checks inclusion on nested arrays
|
5
|
+
# and objects as opposed to the top level object which RSpec's include only
|
6
|
+
# checks
|
7
|
+
RSpec::Matchers.define :include_stub_response do |stub|
|
8
|
+
failure = nil
|
9
|
+
|
10
|
+
match { |actual|
|
11
|
+
begin
|
12
|
+
Rester::Utils::RSpec.assert_deep_include(actual, stub || stub_response)
|
13
|
+
true
|
14
|
+
rescue Rester::Errors::StubError => e
|
15
|
+
failure = e
|
16
|
+
false
|
17
|
+
end
|
18
|
+
}
|
19
|
+
|
20
|
+
failure_message { |actual| failure }
|
21
|
+
end
|
22
|
+
|
3
23
|
RSpec.configure do |config|
|
4
24
|
config.before :all, rester: // do |ex|
|
5
25
|
# Load the stub file
|
@@ -96,7 +116,7 @@ RSpec.configure do |config|
|
|
96
116
|
|
97
117
|
missing_contexts.each { |missing_context, _|
|
98
118
|
context_group = _find_or_create_child(verb_group, missing_context)
|
99
|
-
context_group.it { is_expected.to
|
119
|
+
context_group.it { is_expected.to include_stub_response }
|
100
120
|
}
|
101
121
|
}
|
102
122
|
}
|
@@ -4,6 +4,12 @@ module Rester
|
|
4
4
|
DEFAULT_OPTS = { strict: true }.freeze
|
5
5
|
BASIC_TYPES = [String, Symbol, Float, Integer].freeze
|
6
6
|
|
7
|
+
DEFAULT_TYPE_MATCHERS = {
|
8
|
+
Integer => /\A\d+\z/,
|
9
|
+
Float => /\A\d+(\.\d+)?\z/,
|
10
|
+
:boolean => /\A(true|false)\z/i
|
11
|
+
}.freeze
|
12
|
+
|
7
13
|
attr_reader :options
|
8
14
|
|
9
15
|
def initialize(opts={}, &block)
|
@@ -53,7 +59,10 @@ module Rester
|
|
53
59
|
end
|
54
60
|
|
55
61
|
def validate!(key, value)
|
56
|
-
|
62
|
+
_error!("expected string value for #{key}") unless value.is_a?(String)
|
63
|
+
|
64
|
+
klass, opts = @_validators[key]
|
65
|
+
_validate_match(key, value, opts[:match]) if opts[:match]
|
57
66
|
|
58
67
|
_parse_with_class(klass, value).tap do |obj|
|
59
68
|
_validate_obj(key, obj)
|
@@ -121,7 +130,8 @@ module Rester
|
|
121
130
|
def _add_validator(name, klass, opts)
|
122
131
|
fail 'must specify param name' unless name
|
123
132
|
fail 'validation options must be a Hash' unless opts.is_a?(Hash)
|
124
|
-
|
133
|
+
default_opts = { match: DEFAULT_TYPE_MATCHERS[klass] }
|
134
|
+
opts = default_opts.merge(opts)
|
125
135
|
|
126
136
|
@_required_fields << name.to_sym if opts.delete(:required)
|
127
137
|
default = opts.delete(:default)
|
@@ -137,8 +147,23 @@ module Rester
|
|
137
147
|
nil
|
138
148
|
end
|
139
149
|
|
150
|
+
##
|
151
|
+
# Validates a default value specified in the params block. Raises
|
152
|
+
# validation error if necessary.
|
140
153
|
def _validate_default(key, default)
|
141
|
-
error = catch(:error)
|
154
|
+
error = catch(:error) do
|
155
|
+
type = @_validators[key].first
|
156
|
+
|
157
|
+
unless _valid_type?(default, type)
|
158
|
+
# The .camelcase here is for when type = 'boolean'
|
159
|
+
_error!("default for #{key} should be of "\
|
160
|
+
"type #{type.to_s.camelcase}")
|
161
|
+
end
|
162
|
+
|
163
|
+
validate!(key.to_sym, default.to_s)
|
164
|
+
nil
|
165
|
+
end
|
166
|
+
|
142
167
|
raise error if error
|
143
168
|
end
|
144
169
|
|
@@ -172,24 +197,36 @@ module Rester
|
|
172
197
|
case opt
|
173
198
|
when :within
|
174
199
|
_validate_within(key, obj, value)
|
200
|
+
when :match
|
201
|
+
# Nop - This is evaluated before the incoming string is parsed.
|
175
202
|
else
|
176
203
|
_validate_method(key, obj, opt, value) unless obj.nil?
|
177
204
|
end
|
178
205
|
end
|
179
|
-
|
180
|
-
nil
|
181
206
|
end
|
182
207
|
|
183
208
|
def _validate_type(key, obj, type)
|
209
|
+
unless _valid_type?(obj, type)
|
210
|
+
# The .camelcase here is for when type = 'boolean'
|
211
|
+
_error!("#{key} should be #{type.to_s.camelcase} but "\
|
212
|
+
"got #{obj.class}")
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def _valid_type?(obj, type)
|
184
217
|
case type
|
185
218
|
when :boolean
|
186
|
-
|
187
|
-
_error!("#{key} should be Boolean but got #{obj.class}")
|
188
|
-
end
|
219
|
+
obj.is_a?(TrueClass) || obj.is_a?(FalseClass)
|
189
220
|
else
|
190
|
-
|
191
|
-
|
192
|
-
|
221
|
+
obj.is_a?(type)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
##
|
226
|
+
# To be called *before* the incoming string is parsed into the object.
|
227
|
+
def _validate_match(key, str, matcher)
|
228
|
+
unless matcher.match(str)
|
229
|
+
_error!("#{key} does not match #{matcher.inspect}")
|
193
230
|
end
|
194
231
|
end
|
195
232
|
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module Rester
|
2
|
+
module Utils
|
3
|
+
class CircuitBreaker
|
4
|
+
class Error < StandardError; end
|
5
|
+
class CircuitOpenError < Error; end
|
6
|
+
|
7
|
+
attr_reader :threshold
|
8
|
+
attr_reader :retry_period
|
9
|
+
attr_reader :block
|
10
|
+
|
11
|
+
attr_reader :failure_count
|
12
|
+
attr_reader :last_failed_at
|
13
|
+
|
14
|
+
def initialize(opts={}, &block)
|
15
|
+
@_synchronizer = Mutex.new
|
16
|
+
@_retry_lock = Mutex.new
|
17
|
+
self.threshold = opts[:threshold]
|
18
|
+
self.retry_period = opts[:retry_period]
|
19
|
+
@block = block
|
20
|
+
reset
|
21
|
+
end
|
22
|
+
|
23
|
+
def on_open(&block)
|
24
|
+
_callbacks[:open] = block
|
25
|
+
end
|
26
|
+
|
27
|
+
def on_close(&block)
|
28
|
+
_callbacks[:close] = block
|
29
|
+
end
|
30
|
+
|
31
|
+
def closed?
|
32
|
+
!reached_threshold?
|
33
|
+
end
|
34
|
+
|
35
|
+
def half_open?
|
36
|
+
!closed? && retry_period_passed?
|
37
|
+
end
|
38
|
+
|
39
|
+
def open?
|
40
|
+
!closed? && !half_open?
|
41
|
+
end
|
42
|
+
|
43
|
+
def reached_threshold?
|
44
|
+
failure_count >= threshold
|
45
|
+
end
|
46
|
+
|
47
|
+
def retry_period_passed?
|
48
|
+
lf_at = last_failed_at
|
49
|
+
!lf_at || (Time.now - lf_at) > retry_period
|
50
|
+
end
|
51
|
+
|
52
|
+
def call(*args)
|
53
|
+
if closed?
|
54
|
+
_call(*args)
|
55
|
+
elsif half_open? && @_retry_lock.try_lock
|
56
|
+
# Ensure only one thread can retry.
|
57
|
+
begin
|
58
|
+
_call(*args)
|
59
|
+
ensure
|
60
|
+
@_retry_lock.unlock
|
61
|
+
end
|
62
|
+
else
|
63
|
+
fail CircuitOpenError
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def reset
|
68
|
+
_synchronize do
|
69
|
+
@failure_count = 0
|
70
|
+
@last_failed_at = nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
protected
|
75
|
+
|
76
|
+
def threshold=(threshold)
|
77
|
+
unless (@threshold = (threshold || 3).to_i) > 0
|
78
|
+
fail ArgumentError, 'threshold must be > 0'
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def retry_period=(retry_period)
|
83
|
+
unless (@retry_period = (retry_period || 1).to_f) > 0
|
84
|
+
fail ArgumentError, 'retry_period must be > 0'
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def _call(*args)
|
91
|
+
begin
|
92
|
+
block.call(*args).tap { _record_success }
|
93
|
+
rescue
|
94
|
+
_record_failure
|
95
|
+
raise
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def _callbacks
|
100
|
+
@__callbacks ||= {}
|
101
|
+
end
|
102
|
+
|
103
|
+
def _call_on(type)
|
104
|
+
(cb = _callbacks[type]) && cb.call
|
105
|
+
end
|
106
|
+
|
107
|
+
def _synchronize(&block)
|
108
|
+
@_synchronizer.synchronize(&block)
|
109
|
+
end
|
110
|
+
|
111
|
+
def _record_success
|
112
|
+
if @failure_count > 0
|
113
|
+
_synchronize do
|
114
|
+
# If the threshold had been reached, we're now closing the circuit.
|
115
|
+
_call_on(:close) if @failure_count == threshold
|
116
|
+
@failure_count = 0
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def _record_failure
|
122
|
+
if @failure_count < threshold
|
123
|
+
_synchronize do
|
124
|
+
if @failure_count < threshold
|
125
|
+
@failure_count += 1
|
126
|
+
|
127
|
+
# If the threshold has now been reached, we're opening the circuit.
|
128
|
+
_call_on(:open) if @failure_count == threshold
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
@last_failed_at = Time.now
|
134
|
+
end
|
135
|
+
end # CircuitBreaker
|
136
|
+
end # Utils
|
137
|
+
end # Rester
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Rester
|
2
|
+
module Utils
|
3
|
+
module RSpec
|
4
|
+
class << self
|
5
|
+
def assert_deep_include(response, stub, accessors=[])
|
6
|
+
case stub
|
7
|
+
when Hash
|
8
|
+
_type_error(response, stub, accessors) unless response.is_a?(Hash)
|
9
|
+
stub.all? { |k,v| assert_deep_include(response[k], v, accessors + [k]) }
|
10
|
+
when Array
|
11
|
+
unless response.is_a?(Array)
|
12
|
+
_type_error(response, stub, accessors)
|
13
|
+
end
|
14
|
+
|
15
|
+
unless response.length == stub.length
|
16
|
+
_length_error(response, stub, accessors)
|
17
|
+
end
|
18
|
+
|
19
|
+
stub.each_with_index.all? { |e,i|
|
20
|
+
assert_deep_include(response[i], e, accessors + [i])
|
21
|
+
}
|
22
|
+
else
|
23
|
+
_match_error(response, stub, accessors) unless stub == response
|
24
|
+
true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def _match_error(response, stub, accessors=[])
|
29
|
+
accessors_str = _pretty_print_accessors(accessors)
|
30
|
+
fail Errors::StubError, "Stub#{accessors_str}=#{stub.inspect} doesn't match Response#{accessors_str}=#{response.inspect}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def _length_error(response, stub, accessors=[])
|
34
|
+
accessors_str = _pretty_print_accessors(accessors)
|
35
|
+
fail Errors::StubError, "Stub#{accessors_str} length: #{stub.length} doesn't match Response#{accessors_str} length: #{response.length}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def _type_error(response, stub, accessors=[])
|
39
|
+
accessors_str = _pretty_print_accessors(accessors)
|
40
|
+
fail Errors::StubError, "Stub#{accessors_str} type: #{stub.class} doesn't match Response#{accessors_str} type: #{response.class}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def _pretty_print_accessors(accessors=[])
|
44
|
+
accessors.map { |a| "[#{a.inspect}]" }.join
|
45
|
+
end
|
46
|
+
end # Class Methods
|
47
|
+
end # RSpec
|
48
|
+
end # Utils
|
49
|
+
end # Rester
|
@@ -48,6 +48,23 @@ module Rester
|
|
48
48
|
# StubAdapter (i.e., removes tags from "response" key and puts them in
|
49
49
|
# a "response_tags" key).
|
50
50
|
def _update_context(path, verb, context, spec)
|
51
|
+
_update_request(path, verb, context, spec)
|
52
|
+
_update_response(path, verb, context, spec)
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Converts all the values in the request hash to strings, which mimics
|
57
|
+
# how the data will be received on the service side.
|
58
|
+
def _update_request(path, verb, context, spec)
|
59
|
+
spec['request'] = Utils.stringify_vals(spec['request'] || {})
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Parses response tags (e.g., response[successful=true]).
|
64
|
+
#
|
65
|
+
# Currently supported tags:
|
66
|
+
# successful must be 'true' or 'false'
|
67
|
+
def _update_response(path, verb, context, spec)
|
51
68
|
responses = spec.select { |k,_|
|
52
69
|
k =~ /\Aresponse(\[(\w+) *= *(\w+)(, *(\w+) *= *(\w+))*\])?\z/
|
53
70
|
}
|
data/lib/rester/utils.rb
CHANGED
@@ -2,7 +2,9 @@ require 'date'
|
|
2
2
|
|
3
3
|
module Rester
|
4
4
|
module Utils
|
5
|
-
autoload(:StubFile,
|
5
|
+
autoload(:StubFile, 'rester/utils/stub_file')
|
6
|
+
autoload(:RSpec, 'rester/utils/rspec')
|
7
|
+
autoload(:CircuitBreaker, 'rester/utils/circuit_breaker')
|
6
8
|
|
7
9
|
class << self
|
8
10
|
##
|
@@ -49,14 +51,15 @@ module Rester
|
|
49
51
|
end
|
50
52
|
|
51
53
|
def stringify_vals(hash={})
|
52
|
-
hash.
|
54
|
+
hash.each_with_object({}) { |(k,v), memo|
|
53
55
|
case v
|
54
56
|
when Hash
|
55
57
|
memo[k] = stringify_vals(v)
|
58
|
+
when NilClass
|
59
|
+
memo[k] = 'null'
|
56
60
|
else
|
57
61
|
memo[k] = v.to_s
|
58
62
|
end
|
59
|
-
memo
|
60
63
|
}
|
61
64
|
end
|
62
65
|
|
data/lib/rester/version.rb
CHANGED
data/lib/rester.rb
CHANGED
@@ -17,9 +17,8 @@ module Rester
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def connect(service, params={})
|
20
|
-
|
21
|
-
|
22
|
-
adapter = klass.new(service)
|
20
|
+
adapter_opts = Client::Adapters.extract_opts(params)
|
21
|
+
adapter = Client::Adapters.connect(service, adapter_opts)
|
23
22
|
Client.new(adapter, params)
|
24
23
|
end
|
25
24
|
end # Class Methods
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rester
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Robert Honer
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-
|
12
|
+
date: 2015-12-04 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rack
|
@@ -145,6 +145,8 @@ files:
|
|
145
145
|
- lib/rester/service/resource.rb
|
146
146
|
- lib/rester/service/resource/params.rb
|
147
147
|
- lib/rester/utils.rb
|
148
|
+
- lib/rester/utils/circuit_breaker.rb
|
149
|
+
- lib/rester/utils/rspec.rb
|
148
150
|
- lib/rester/utils/stub_file.rb
|
149
151
|
- lib/rester/version.rb
|
150
152
|
homepage: http://github.com/payout/rester
|