rester 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
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