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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a5c3b74a7feec8cfd51a193a182876636d04d77e
4
- data.tar.gz: d5cc9861ec2c2a607e7a726a86b5569e10c72d97
3
+ metadata.gz: ed31a2e9f930e5607d9905098c61fae665750d3a
4
+ data.tar.gz: a3cfb90ab6eb177fd2291d5bd6e5013014023f63
5
5
  SHA512:
6
- metadata.gz: 27782ad11aa2d68798ae77c346e2627606fc6e0ef9b67ad7d39904675d74a88be0f3b529c9c8418f53f77801efe045f8c57ad32046a86f50053dc923105b9e69
7
- data.tar.gz: 6f511e675e4684e6d2aec4b6a088f54478b496b36ea518df1113890e1c5ed10a021b41881b76884498e58880492d3645872d8bd2d38f0f4b9f3c7842a71703f6
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
- def initialize(*args)
14
- connect(*args) unless args.empty?
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={}, &block)
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, &block|
48
- request(verb, *args, &block)
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, &block|
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
- _http.get(
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
- _http.delete(
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
- encoded_data = _encode_data(params[:data])
35
- headers = _prepare_data_headers(params[:headers])
36
- _http.put(_path(path), encoded_data, headers)
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
- encoded_data = _encode_data(params[:data])
41
- headers = _prepare_data_headers(params[:headers])
42
- _http.post(_path(path), encoded_data, headers)
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
 
@@ -17,8 +17,8 @@ module Rester
17
17
  end
18
18
  end # Class Methods
19
19
 
20
- def connect(*args)
21
- nil.tap { @connection = Connection.new(*args) }
20
+ def connect(url)
21
+ nil.tap { @connection = Connection.new(url, timeout: timeout) }
22
22
  end
23
23
 
24
24
  def connected?
@@ -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, opts={})
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 = service.call(
49
- 'REQUEST_METHOD' => verb.to_s.upcase,
50
- 'PATH_INFO' => path,
51
- 'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
52
- 'QUERY_STRING' => query,
53
- 'rack.input' => StringIO.new(body)
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
- def connect(stub_filepath, opts={})
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 (action = stub[path][verb][context])
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 = Utils.stringify_vals(action['request'] || {})) == params
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 { |_, action|
84
- (Utils.stringify_vals(action['request'] || {})) == params
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
- @version = params[:version] || 1
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={}, &block)
28
- path = _path_with_version(path)
29
-
30
- _process_response(path, *adapter.request(verb, path, params, &block))
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 < Error; end
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 include stub_response }
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
- klass = @_validators[key].first
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
- opts = opts.dup
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) { _validate_obj(key.to_sym, default) }
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
- unless obj.is_a?(TrueClass) || obj.is_a?(FalseClass)
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
- unless obj.is_a?(type)
191
- _error!("#{key} should be #{type} but got #{obj.class}")
192
- end
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, 'rester/utils/stub_file')
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.inject({}) { |memo,(k,v)|
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
 
@@ -1,3 +1,3 @@
1
1
  module Rester
2
- VERSION = '0.4.1'
2
+ VERSION = '0.4.2'
3
3
  end
data/lib/rester.rb CHANGED
@@ -17,9 +17,8 @@ module Rester
17
17
  end
18
18
 
19
19
  def connect(service, params={})
20
- klass = Client::Adapters.list.find { |a| a.can_connect_to?(service) }
21
- fail "unable to connect to #{service.inspect}" unless klass
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.1
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-11-18 00:00:00.000000000 Z
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