rester 0.2.4 → 0.3.0

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: ece68b86784e1eb6be50d4f6a18a5faba22277b0
4
- data.tar.gz: 87fe10dabac3bd9bcc68027c8d1d32f16a90f543
3
+ metadata.gz: 781dcd45b4ea9347717fba375241286931b28444
4
+ data.tar.gz: 3319242e32b49c30316711918748809e736880fa
5
5
  SHA512:
6
- metadata.gz: d0937a90725c306e6ca57c8673985c743df41fb63340377615d6ff53983fec13a24deea7e43ff6d861170ae6736863fa1c23aeec68e6cce265adf491758fd150
7
- data.tar.gz: 8a660df186e630cd6473f2c0a3e073261a894008cf20053ae6bc89f79b38077eb3d64401f9e715eb16e75c0d238afea5f6d5d04c758728cc5cf3991b41591478
6
+ metadata.gz: 6d81ef7d638acb74488d729ccb22e41d749b6aa55afc00b8236d8ca84e1fd80e2798ca7e300a3c8a89e6af73a7bd9eacc913fa113d0b9f8331bd2031c271ab69
7
+ data.tar.gz: d2d00b634e8e052a15d0b819f813b417184722234253e5cc92cd8bd69b3f196a1d75d1fd5a26eb5e186c81930a3b134573f3a225cd2708ec0a3ede31fde44fde
@@ -1,6 +1,15 @@
1
1
  module Rester
2
2
  module Client::Adapters
3
3
  class Adapter
4
+
5
+ class << self
6
+ ##
7
+ # Returns whether or not the Adapter can connect to the service
8
+ def can_connect_to?(service)
9
+ raise NotImplementedError
10
+ end
11
+ end # Class Methods
12
+
4
13
  def initialize(*args)
5
14
  connect(*args) unless args.empty?
6
15
  end
@@ -5,6 +5,18 @@ module Rester
5
5
 
6
6
  attr_reader :connection
7
7
 
8
+ class << self
9
+ def can_connect_to?(service)
10
+ if service.is_a?(URI)
11
+ uri = service
12
+ elsif service.is_a?(String) && URI::regexp.match(service)
13
+ uri = URI(service)
14
+ end
15
+
16
+ !!uri && ['http', 'https'].include?(uri.scheme)
17
+ end
18
+ end # Class Methods
19
+
8
20
  def connect(*args)
9
21
  nil.tap { @connection = Connection.new(*args) }
10
22
  end
@@ -7,11 +7,15 @@ module Rester
7
7
  # An adapter for "connecting" to a service internally, without needing to
8
8
  # interface over a HTTP connection.
9
9
  class LocalAdapter < Adapter
10
- attr_reader :version
11
10
  attr_reader :service
12
11
 
12
+ class << self
13
+ def can_connect_to?(service)
14
+ service.is_a?(Class) && service < Service
15
+ end
16
+ end # Class Methods
17
+
13
18
  def connect(service, opts={})
14
- @version = opts[:version] || 1
15
19
  nil.tap { @service = service }
16
20
  end
17
21
 
@@ -43,7 +47,7 @@ module Rester
43
47
 
44
48
  response = service.call(
45
49
  'REQUEST_METHOD' => verb.to_s.upcase,
46
- 'PATH_INFO' => Utils.join_paths("/v#{version}", path),
50
+ 'PATH_INFO' => path,
47
51
  'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
48
52
  'QUERY_STRING' => query,
49
53
  'rack.input' => StringIO.new(body)
@@ -0,0 +1,90 @@
1
+ require 'yaml'
2
+ require 'json'
3
+ require 'pathname'
4
+
5
+ module Rester
6
+ module Client::Adapters
7
+ ##
8
+ # An adapter to be used to stub the responses needed from specified service
9
+ # requests via a yaml file. This will be used in spec tests to perform
10
+ # "contractual testing"
11
+ class StubAdapter < Adapter
12
+ attr_reader :stub
13
+
14
+ class << self
15
+ def can_connect_to?(service)
16
+ service.is_a?(String) && Pathname(service).file?
17
+ end
18
+ end # Class Methods
19
+
20
+ def connect(stub_filepath, opts={})
21
+ @stub = YAML.load_file(stub_filepath)
22
+ end
23
+
24
+ def connected?
25
+ !!stub
26
+ end
27
+
28
+ def get!(path, params={})
29
+ _request('GET', path, params)
30
+ end
31
+
32
+ def post!(path, params={})
33
+ _request('POST', path, params)
34
+ end
35
+
36
+ def put!(path, params={})
37
+ _request('PUT', path, params)
38
+ end
39
+
40
+ def delete!(path, params={})
41
+ _request('DELETE', path, params)
42
+ end
43
+
44
+ def context
45
+ @_context
46
+ end
47
+
48
+ def context=(context)
49
+ @_context = context
50
+ end
51
+
52
+ def with_context(context, &block)
53
+ self.context = context
54
+ yield block
55
+ self.context = nil
56
+ end
57
+
58
+ private
59
+
60
+ def _request(verb, path, params)
61
+ _validate_request(verb, path, params)
62
+
63
+ # At this point, the 'request' is valid by matching a corresponding
64
+ # request in the stub yaml file. Grab the response from the file and
65
+ # reset the context
66
+ response = stub[path][verb][context]['response']
67
+ context = nil
68
+ [response['code'], response['body'].to_json]
69
+ end
70
+
71
+ def _validate_request(verb, path, params)
72
+ fail Errors::StubError, "#{path} not found" unless stub[path]
73
+ fail Errors::StubError, "#{verb} #{path} not found" unless stub[path][verb]
74
+
75
+ unless (action = stub[path][verb][context])
76
+ fail Errors::StubError,
77
+ "#{verb} #{path} with context '#{context}' not found"
78
+ end
79
+
80
+ # Verify body, if there is one
81
+ if (request = action['request'])
82
+ unless Utils.symbolize_keys(request) == params
83
+ fail Errors::StubError,
84
+ "#{verb} #{path} with context '#{context}' params don't match stub"
85
+ end
86
+ end
87
+ end
88
+ end # StubAdapter
89
+ end # Client::Adapters
90
+ end # Rester
@@ -4,6 +4,14 @@ module Rester
4
4
  autoload(:Adapter, 'rester/client/adapters/adapter')
5
5
  autoload(:HttpAdapter, 'rester/client/adapters/http_adapter')
6
6
  autoload(:LocalAdapter, 'rester/client/adapters/local_adapter')
7
+ autoload(:StubAdapter, 'rester/client/adapters/stub_adapter')
8
+
9
+ class << self
10
+ def list
11
+ constants.map { |c| const_get(c) }
12
+ .select { |c| c.is_a?(Class) && c < Adapter }
13
+ end
14
+ end
7
15
  end
8
16
  end
9
17
  end
@@ -25,16 +25,11 @@ module Rester
25
25
 
26
26
  def method_missing(meth, *args, &block)
27
27
  meth = meth.to_s
28
-
29
- unless args.length == 1
30
- raise ArgumentError, "wrong number of arguments (#{args.length} for 1)"
31
- end
32
-
33
28
  arg = args.first
34
29
 
35
30
  case arg
36
- when Hash
37
- _handle_search_or_create(meth, arg)
31
+ when Hash, NilClass
32
+ _handle_search_or_create(meth, arg || {})
38
33
  when String, Symbol
39
34
  Resource.new(client, _path(meth, arg))
40
35
  else
@@ -54,6 +49,6 @@ module Rester
54
49
  verb, name = Utils.extract_method_verb(name)
55
50
  _request(verb, name, params)
56
51
  end
57
- end # Object
52
+ end # Resource
58
53
  end # Client
59
54
  end # Rester
@@ -0,0 +1,28 @@
1
+ module Rester
2
+ class Client
3
+ class Response < Hash
4
+ def initialize(status, hash={})
5
+ @_status = status
6
+ merge!(hash)
7
+ _deep_freeze
8
+ end
9
+
10
+ def successful?
11
+ @_status && @_status.between?(200, 299)
12
+ end
13
+
14
+ private
15
+
16
+ def _deep_freeze(value=self)
17
+ value.freeze
18
+
19
+ case value
20
+ when Hash
21
+ value.values.each { |v| _deep_freeze(v) }
22
+ when Array
23
+ value.each { |v| _deep_freeze(v) }
24
+ end
25
+ end
26
+ end # Response
27
+ end # Client
28
+ end # Rester
data/lib/rester/client.rb CHANGED
@@ -5,17 +5,14 @@ module Rester
5
5
  class Client
6
6
  autoload(:Adapters, 'rester/client/adapters')
7
7
  autoload(:Resource, 'rester/client/resource')
8
+ autoload(:Response, 'rester/client/response')
8
9
 
9
10
  attr_reader :adapter
11
+ attr_reader :version
10
12
 
11
- def initialize(*args)
12
- case args.first
13
- when Adapters::Adapter
14
- self.adapter = args.first
15
- else
16
- self.adapter = Adapters::HttpAdapter.new(*args)
17
- end
18
-
13
+ def initialize(adapter, params={})
14
+ self.adapter = adapter
15
+ @version = params[:version] || 1
19
16
  @_resource = Resource.new(self)
20
17
  end
21
18
 
@@ -24,13 +21,21 @@ module Rester
24
21
  end
25
22
 
26
23
  def connected?
27
- adapter.connected? && adapter.get('status').first == 200
24
+ adapter.connected? && adapter.get('/ping').first == 200
28
25
  end
29
26
 
30
27
  def request(verb, path, params={}, &block)
28
+ path = _path_with_version(path)
29
+
31
30
  _process_response(path, *adapter.request(verb, path, params, &block))
32
31
  end
33
32
 
33
+ ##
34
+ # This is only implemented by the StubAdapter.
35
+ def with_context(*args, &block)
36
+ adapter.with_context(*args, &block)
37
+ end
38
+
34
39
  protected
35
40
 
36
41
  def adapter=(adapter)
@@ -45,16 +50,27 @@ module Rester
45
50
  @_resource.send(:method_missing, meth, *args, &block)
46
51
  end
47
52
 
53
+ def _path_with_version(path)
54
+ Utils.join_paths("/v#{version}", path)
55
+ end
56
+
48
57
  def _process_response(path, status, body)
49
- if status.between?(200, 299)
50
- _parse_json(body)
51
- elsif status == 400
52
- raise Errors::RequestError, _parse_json(body)[:message]
53
- elsif status == 404
54
- raise Errors::NotFoundError, "/#{path}"
55
- else
56
- raise Errors::ServerError, _parse_json(body)[:message]
58
+ response = Response.new(status, _parse_json(body))
59
+
60
+ unless [200, 201, 400].include?(status)
61
+ case status
62
+ when 401
63
+ fail Errors::AuthenticationError
64
+ when 403
65
+ fail Errors::ForbiddenError
66
+ when 404
67
+ fail Errors::NotFoundError, path
68
+ else
69
+ fail Errors::ServerError, response[:message]
70
+ end
57
71
  end
72
+
73
+ response
58
74
  end
59
75
 
60
76
  def _parse_json(data)
data/lib/rester/errors.rb CHANGED
@@ -14,6 +14,10 @@ module Rester
14
14
  class MethodError < Error; end
15
15
  class MethodDefinitionError < Error; end
16
16
 
17
+ #############
18
+ # Stub Errors
19
+ class StubError < Error; end
20
+
17
21
  #############
18
22
  # Http Errors
19
23
  class HttpError < Error; end
@@ -23,11 +23,13 @@ module Rester
23
23
  def _error_to_response(error)
24
24
  code = _error_to_http_code(error)
25
25
 
26
- unless code == 404
27
- body_h = { message: error.message }
26
+ unless [401, 403, 404].include?(code)
27
+ body_h = {
28
+ message: error.message,
29
+ error: _error_name(error)
30
+ }
28
31
 
29
32
  if code == 500
30
- body_h[:error] = error.class.name
31
33
  body_h[:backtrace] = error.backtrace
32
34
  end
33
35
  end
@@ -52,6 +54,10 @@ module Rester
52
54
  500
53
55
  end
54
56
  end
57
+
58
+ def _error_name(exception)
59
+ Utils.underscore(exception.class.name.split('::').last.sub('Error', ''))
60
+ end
55
61
  end # ErrorHandling
56
62
  end # Middleware
57
63
  end # Rester
@@ -2,14 +2,14 @@ module Rester
2
2
  module Middleware
3
3
  ##
4
4
  # Provides a basic status check. Used by the Client#connected? method.
5
- class StatusCheck < Base
5
+ class Ping < Base
6
6
  def call(env)
7
- if %r{\A/v[\d+]/status\z}.match(env['REQUEST_PATH'])
7
+ if %r{\A/ping\z}.match(env['REQUEST_PATH'])
8
8
  [200, {}, []]
9
9
  else
10
10
  super
11
11
  end
12
12
  end
13
- end # StatusCheck
13
+ end # Ping
14
14
  end # Middleware
15
15
  end # Rester
@@ -2,6 +2,6 @@ module Rester
2
2
  module Middleware
3
3
  autoload(:Base, 'rester/middleware/base')
4
4
  autoload(:ErrorHandling, 'rester/middleware/error_handling')
5
- autoload(:StatusCheck, 'rester/middleware/status_check')
5
+ autoload(:Ping, 'rester/middleware/ping')
6
6
  end
7
7
  end
@@ -0,0 +1,152 @@
1
+ require 'json'
2
+
3
+ RSpec.configure do |config|
4
+ config.before :all, rester: // do |ex|
5
+ # Load the stub file
6
+ @rester_stub_filepath = ex.class.metadata[:rester]
7
+ @rester_stub = YAML.load_file(@rester_stub_filepath)
8
+
9
+ # Hook up the LocalAdapter with the Service being tested
10
+ unless (klass = ex.class.described_class) < Rester::Service
11
+ raise "invalid service to test"
12
+ end
13
+ @rester_adapter = Rester::Client::Adapters::LocalAdapter.new(klass, {})
14
+
15
+ _validate_test_coverage(ex)
16
+ end
17
+
18
+ config.before :each, rester: // do |ex|
19
+ # Gather the request args from the spec descriptions
20
+ #
21
+ # For example:
22
+ #
23
+ # describe '/v1/tests' do
24
+ # context 'GET' do
25
+ # context 'With some context' do
26
+ # end
27
+ # end
28
+ # end
29
+ #
30
+ # would produce:
31
+ #
32
+ # request_args = ['With some context', 'GET', '/v1/tests']
33
+ #
34
+ request_args = ex.example_group.parent_groups.map { |a|
35
+ a.description unless a.metadata[:description] == a.described_class.to_s
36
+ }.compact
37
+
38
+ context = request_args[0]
39
+ verb = request_args[1]
40
+ path = request_args[2]
41
+
42
+ begin
43
+ params = @rester_stub[path][verb][context]['request']
44
+ response = @rester_stub[path][verb][context]['response']
45
+ rescue NoMethodError
46
+ fail Rester::Errors::StubError,
47
+ "Could not find path: #{path.inspect} verb: #{verb.inspect} context: #{context.inspect} in #{@rester_stub_filepath}"
48
+ end
49
+
50
+ ex.example_group.let(:subject) {
51
+ @rester_adapter.request(verb.downcase.to_sym, path, params)
52
+ }
53
+
54
+ ex.example_group.let(:stub_response) {
55
+ [response['code'], response['body'].to_json]
56
+ }
57
+ end
58
+
59
+ config.after :each, rester: // do |ex|
60
+ expect(subject).to eq stub_response
61
+ end
62
+
63
+ ##
64
+ # Check to see if each stub example has a corresponding test written for it
65
+ def _validate_test_coverage(ex)
66
+ rester_service_tests = _rester_service_tests(ex)
67
+ missing_tests = _missing_stub_tests(rester_service_tests)
68
+
69
+ # Loop through each missing stub test and create a corresponding RSpect test
70
+ # to display the missing tests as a failure to the user
71
+ missing_tests.each { |missing_path, missing_verbs|
72
+ path_group = _find_or_create_child(ex.class, missing_path)
73
+
74
+ missing_verbs.each { |missing_verb, missing_contexts|
75
+ verb_group = _find_or_create_child(path_group, missing_verb)
76
+
77
+ missing_contexts.each { |missing_context, _|
78
+ context_group = _find_or_create_child(verb_group, missing_context)
79
+ context_group.it { is_expected.to eq stub_response }
80
+ }
81
+ }
82
+ }
83
+ end
84
+
85
+ def _rester_service_tests(parent_example_group)
86
+ service_tests = {}
87
+ parent_example_group.class.children.each { |path_group|
88
+ path = path_group.description
89
+ service_tests[path] ||= {}
90
+
91
+ path_group.children.each { |verb_group|
92
+ verb = verb_group.description
93
+ service_tests[path][verb] ||= {}
94
+
95
+ verb_group.children.each { |context_group|
96
+ context = context_group.description
97
+ service_tests[path][verb][context] = context_group.examples.count > 0
98
+ }
99
+ }
100
+ }
101
+
102
+ service_tests
103
+ end
104
+
105
+ ##
106
+ # Takes a hash produced by _rester_service_tests.
107
+ # Returns a hash of only the missing stub tests:
108
+ #
109
+ # {
110
+ # "/v1/tests/abc123/mounted_objects" => {
111
+ # "POST" => {
112
+ # "With some context" => false
113
+ # }
114
+ # },
115
+ # "/v1/stuff" => {
116
+ # "GET" => {
117
+ # "Doing that" => false
118
+ # }
119
+ # }
120
+ # }
121
+ def _missing_stub_tests(tests)
122
+ @rester_stub.reject { |k, _|
123
+ ['version', 'consumer', 'producer'].include?(k)
124
+ }.map { |path, verbs|
125
+ [
126
+ path,
127
+ verbs.map { |verb, contexts|
128
+ [
129
+ verb,
130
+ contexts.map { |context, _|
131
+ [
132
+ context,
133
+ !!(tests[path] && tests[path][verb] && tests[path][verb][context])
134
+ ]
135
+ }.to_h.reject { |_, v| v }
136
+ ]
137
+ }.to_h.reject { |_, v| v.empty? }
138
+ ]
139
+ }.to_h.reject { |_, v| v.empty? }
140
+ end
141
+
142
+ def _find_child_with_description(group, description)
143
+ group.children.find { |child_group|
144
+ child_group.description == description
145
+ }
146
+ end
147
+
148
+ def _find_or_create_child(group, description)
149
+ child = _find_child_with_description(group, description)
150
+ child || group.describe(description)
151
+ end
152
+ end
@@ -1,17 +1,20 @@
1
1
  module Rester
2
- class Service::Object
3
- class Validator
2
+ class Service::Resource
3
+ class Params
4
4
  BASIC_TYPES = [String, Symbol, Float, Integer].freeze
5
5
 
6
6
  attr_reader :options
7
7
 
8
- def initialize(opts={})
8
+ def initialize(opts={}, &block)
9
9
  @options = opts.dup.freeze
10
10
  @_required_fields = []
11
+ @_defaults = {}
11
12
  @_all_fields = []
12
13
 
13
14
  # Default "validator" is to just treat the param as a string.
14
15
  @_validators = Hash.new([String, {}])
16
+
17
+ instance_eval(&block) if block_given?
15
18
  end
16
19
 
17
20
  ##
@@ -24,17 +27,16 @@ module Rester
24
27
  def freeze
25
28
  @_validators.freeze
26
29
  @_required_fields.freeze
30
+ @_defaults.freeze
27
31
  @_all_fields.freeze
28
- end
29
-
30
- def required_params
31
- @_required_fields.dup
32
+ super
32
33
  end
33
34
 
34
35
  def validate(params)
35
36
  param_keys = params.keys.map(&:to_sym)
37
+ default_keys = @_defaults.keys
36
38
 
37
- unless (missing = @_required_fields - param_keys).empty?
39
+ unless (missing = @_required_fields - param_keys - default_keys).empty?
38
40
  _error!("missing params: #{missing.join(', ')}")
39
41
  end
40
42
 
@@ -42,30 +44,26 @@ module Rester
42
44
  _error!("unexpected params: #{unexpected.join(', ')}")
43
45
  end
44
46
 
45
- params.map do |key, value|
47
+ validated_params = params.map do |key, value|
46
48
  [key.to_sym, validate!(key.to_sym, value)]
47
49
  end.to_h
50
+
51
+ @_defaults.merge(validated_params)
48
52
  end
49
53
 
50
54
  def validate!(key, value)
51
- klass, opts = @_validators[key]
55
+ klass = @_validators[key].first
52
56
 
53
57
  _parse_with_class(klass, value).tap do |obj|
54
- if obj.nil? && @_required_fields.include?(key)
55
- _error!("#{key} cannot be null")
56
- end
57
-
58
- opts.each do |opt, value|
59
- case opt
60
- when :within
61
- _validate_within(key, obj, value)
62
- else
63
- _validate_method(key, obj, opt, value) unless obj.nil?
64
- end
65
- end
58
+ _validate_obj(key, obj)
66
59
  end
67
60
  end
68
61
 
62
+ def use(params)
63
+ _merge_params(params)
64
+ nil
65
+ end
66
+
69
67
  ##
70
68
  # The basic data types all have helper methods named after them in Kernel.
71
69
  # This allows you to do things like String(1234) to get '1234'. It's the
@@ -87,6 +85,24 @@ module Rester
87
85
  _add_validator(name, :boolean, opts)
88
86
  end
89
87
 
88
+ protected
89
+
90
+ def required_params
91
+ @_required_fields.dup
92
+ end
93
+
94
+ def defaults
95
+ @_defaults.dup
96
+ end
97
+
98
+ def all_fields
99
+ @_all_fields.dup
100
+ end
101
+
102
+ def validators
103
+ @_validators.dup
104
+ end
105
+
90
106
  private
91
107
 
92
108
  def method_missing(meth, *args)
@@ -96,6 +112,8 @@ module Rester
96
112
  name = args.shift
97
113
  opts = args.shift || {}
98
114
  _add_validator(name, self.class.const_get(meth), opts)
115
+ else
116
+ super
99
117
  end
100
118
  end
101
119
 
@@ -103,12 +121,26 @@ module Rester
103
121
  fail 'must specify param name' unless name
104
122
  fail 'validation options must be a Hash' unless opts.is_a?(Hash)
105
123
  opts = opts.dup
124
+
106
125
  @_required_fields << name.to_sym if opts.delete(:required)
126
+ default = opts.delete(:default)
127
+
107
128
  @_all_fields << name.to_sym
108
129
  @_validators[name.to_sym] = [klass, opts]
130
+
131
+ if default
132
+ _validate_default(name.to_sym, default)
133
+ @_defaults[name.to_sym] = default
134
+ end
135
+
109
136
  nil
110
137
  end
111
138
 
139
+ def _validate_default(key, default)
140
+ error = catch(:error) { _validate_obj(key.to_sym, default) }
141
+ raise error if error
142
+ end
143
+
112
144
  def _parse_with_class(klass, value)
113
145
  return nil if value == 'null'
114
146
 
@@ -127,6 +159,39 @@ module Rester
127
159
  end
128
160
  end
129
161
 
162
+ def _validate_obj(key, obj)
163
+ if obj.nil? && @_required_fields.include?(key)
164
+ _error!("#{key} cannot be null")
165
+ end
166
+
167
+ klass, opts = @_validators[key]
168
+ _validate_type(key, obj, klass) if obj
169
+
170
+ opts.each do |opt, value|
171
+ case opt
172
+ when :within
173
+ _validate_within(key, obj, value)
174
+ else
175
+ _validate_method(key, obj, opt, value) unless obj.nil?
176
+ end
177
+ end
178
+
179
+ nil
180
+ end
181
+
182
+ def _validate_type(key, obj, type)
183
+ case type
184
+ when :boolean
185
+ unless obj.is_a?(TrueClass) || obj.is_a?(FalseClass)
186
+ _error!("#{key} should be Boolean but got #{obj.class}")
187
+ end
188
+ else
189
+ unless obj.is_a?(type)
190
+ _error!("#{key} should be #{type} but got #{obj.class}")
191
+ end
192
+ end
193
+ end
194
+
130
195
  def _validate_within(key, obj, value)
131
196
  unless value.include?(obj)
132
197
  _error!("#{key} not within #{value.inspect}")
@@ -146,6 +211,13 @@ module Rester
146
211
  def _error!(message)
147
212
  Errors.throw_error!(Errors::ValidationError, message)
148
213
  end
214
+
215
+ def _merge_params(params)
216
+ @_validators = @_validators.merge!(params.validators)
217
+ @_defaults = @_defaults.merge!(params.defaults)
218
+ @_required_fields |= params.required_params
219
+ @_all_fields |= params.all_fields
220
+ end
149
221
  end
150
222
  end
151
223
  end
@@ -0,0 +1,127 @@
1
+ require 'rack'
2
+ require 'active_support/inflector'
3
+
4
+ module Rester
5
+ class Service
6
+ class Resource
7
+ autoload(:Params, 'rester/service/resource/params')
8
+
9
+ REQUEST_METHOD_TO_IDENTIFIED_METHOD = {
10
+ 'GET' => :get,
11
+ 'PUT' => :update,
12
+ 'DELETE' => :delete
13
+ }.freeze
14
+
15
+ REQUEST_METHOD_TO_UNIDENTIFIED_METHOD = {
16
+ 'GET' => :search,
17
+ 'POST' => :create
18
+ }.freeze
19
+
20
+ RESOURCE_METHODS = [:search, :create, :get, :update, :delete].freeze
21
+
22
+ ########################################################################
23
+ # DSL
24
+ ########################################################################
25
+ class << self
26
+ ##
27
+ # Specify the name of your identifier (Default: 'id')
28
+ def id(name)
29
+ @id_name = name.to_sym
30
+ end
31
+
32
+ ##
33
+ # Mount another Service Resource
34
+ def mount(klass)
35
+ raise "Only other Service Resources can be mounted." unless klass < Resource
36
+ start = self.name.split('::')[0..-2].join('::').length + 2
37
+ mounts[klass.name[start..-1].pluralize.underscore] = klass
38
+ end
39
+
40
+ def params(opts={}, &block)
41
+ @_next_params = Params.new(opts, &block)
42
+ end
43
+ end # DSL
44
+
45
+ ########################################################################
46
+ # Class Methods
47
+ ########################################################################
48
+ class << self
49
+ def id_name
50
+ @id_name ||= :id
51
+ end
52
+
53
+ def id_param
54
+ "#{self.name.split('::').last.underscore}_#{id_name}"
55
+ end
56
+
57
+ def mounts
58
+ (@__mounts ||= {})
59
+ end
60
+
61
+ def method_params
62
+ @_method_params ||= {}
63
+ end
64
+
65
+ def method_added(method_name)
66
+ if RESOURCE_METHODS.include?(method_name.to_sym)
67
+ method_params[method_name.to_sym] = (@_next_params || Params.new).freeze
68
+ end
69
+ @_next_params = nil
70
+ end
71
+ end # Class Methods
72
+
73
+ def id_param
74
+ self.class.id_param
75
+ end
76
+
77
+ ##
78
+ # Given an HTTP request method, calls the appropriate calls the
79
+ # appropriate instance method. `id_provided` specifies whether on not the
80
+ # ID for the object is included in the params hash. This will be used when
81
+ # determining which instance method to call. For example, if the request
82
+ # method is GET: the ID being specified will call the `get` method and if
83
+ # it's not specified then it will call the `search` method.
84
+ def process(request_method, id_provided, params={})
85
+ meth = (id_provided ? REQUEST_METHOD_TO_IDENTIFIED_METHOD
86
+ : REQUEST_METHOD_TO_UNIDENTIFIED_METHOD)[request_method]
87
+
88
+ _process(meth, params).to_h
89
+ end
90
+
91
+ def mounts
92
+ self.class.mounts
93
+ end
94
+
95
+ def method_params_for(method_name)
96
+ self.class.method_params[method_name.to_sym]
97
+ end
98
+
99
+ def error!(message=nil)
100
+ Errors.throw_error!(Errors::RequestError, message)
101
+ end
102
+
103
+ private
104
+
105
+ ##
106
+ # Calls the specified method, passing the params if the method accepts
107
+ # an argument. Allows for the arity of the method to be 0, 1 or -1.
108
+ def _process(meth, params)
109
+ if meth && respond_to?(meth)
110
+ params = method_params_for(meth).validate(params)
111
+ meth = method(meth)
112
+
113
+ case meth.arity.abs
114
+ when 1
115
+ meth.call(params)
116
+ when 0
117
+ meth.call
118
+ else
119
+ fail MethodDefinitionError, "#{meth} must take 0 or 1 argument"
120
+ end
121
+ else
122
+ fail Errors::NotFoundError, meth
123
+ end
124
+ end
125
+ end # Resource
126
+ end # Service
127
+ end # Rester
@@ -5,7 +5,7 @@ require 'active_support/inflector'
5
5
  module Rester
6
6
  class Service
7
7
  autoload(:Request, 'rester/service/request')
8
- autoload(:Object, 'rester/service/object')
8
+ autoload(:Resource, 'rester/service/resource')
9
9
 
10
10
  ##
11
11
  # The base set of middleware to use for every service.
@@ -13,7 +13,7 @@ module Rester
13
13
  BASE_MIDDLEWARE = [
14
14
  Rack::Head,
15
15
  Middleware::ErrorHandling,
16
- Middleware::StatusCheck
16
+ Middleware::Ping
17
17
  ].freeze
18
18
 
19
19
  ########################################################################
@@ -68,15 +68,15 @@ module Rester
68
68
  const_get(version.to_s.upcase)
69
69
  end
70
70
 
71
- def objects(version_module)
72
- (@__objects ||= {})[version_module] ||= _load_objects(version_module)
71
+ def resources(version_module)
72
+ (@__resources ||= {})[version_module] ||= _load_resources(version_module)
73
73
  end
74
74
 
75
- def _load_objects(version_module)
75
+ def _load_resources(version_module)
76
76
  version_module.constants.map { |c|
77
77
  version_module.const_get(c)
78
78
  }.select { |c|
79
- c.is_a?(Class) && c < Service::Object
79
+ c.is_a?(Class) && c < Service::Resource
80
80
  }
81
81
  end
82
82
  end # Class methods
@@ -119,7 +119,7 @@ module Rester
119
119
  end
120
120
 
121
121
  ##
122
- # Validates the request, calls the appropriate Service::Object method and
122
+ # Validates the request, calls the appropriate Service::Resource method and
123
123
  # returns a valid Rack response.
124
124
  def _process_request(request)
125
125
  _error!(Errors::NotFoundError) unless request.valid?
@@ -138,36 +138,35 @@ module Rester
138
138
  end
139
139
 
140
140
  ##
141
- # Calls the appropriate method on the appropriate Service::Object for the
141
+ # Calls the appropriate method on the appropriate Service::Resource for the
142
142
  # request.
143
143
  def _call_method(request)
144
144
  params = request.params
145
145
  retval = nil
146
146
 
147
147
  name, id, *object_chain = request.object_chain
148
- obj = _load_object(request, name)
148
+ obj = _load_resource(request, name)
149
149
 
150
150
  loop {
151
- obj = obj.new(id) if id
151
+ params.merge!(obj.id_param => id) if id
152
152
 
153
153
  if object_chain.empty?
154
- retval = obj.process(request.request_method, params)
154
+ retval = obj.process(request.request_method, !!id, params)
155
155
  break
156
156
  end
157
157
 
158
- params.merge!(obj.id_param => obj.id)
159
158
  name, id, *object_chain = object_chain
160
- obj = obj.mounts[name] or raise Errors::NotFoundError
159
+ obj = obj.mounts[name].new or fail Errors::NotFoundError
161
160
  }
162
161
 
163
162
  retval
164
163
  end
165
164
 
166
165
  ##
167
- # Loads the appropriate Service::Object for the request. This will return
166
+ # Loads the appropriate Service::Resource for the request. This will return
168
167
  # the class, not an instance.
169
- def _load_object(request, name)
170
- _version_module(request).const_get(name.camelcase.singularize)
168
+ def _load_resource(request, name)
169
+ _version_module(request).const_get(name.camelcase.singularize).new
171
170
  rescue NameError
172
171
  _error!(Errors::NotFoundError)
173
172
  end
@@ -179,11 +178,9 @@ module Rester
179
178
  end
180
179
 
181
180
  ##
182
- # Prepares the retval from a Service::Object method to be returned to the
181
+ # Prepares the retval from a Service::Resource method to be returned to the
183
182
  # client (i.e., validates it and dumps it as JSON).
184
183
  def _prepare_response(retval)
185
- retval ||= {}
186
-
187
184
  unless retval.is_a?(Hash)
188
185
  _error!(Errors::ServerError, "Invalid response: #{retval.inspect}")
189
186
  end
data/lib/rester/utils.rb CHANGED
@@ -49,6 +49,10 @@ module Rester
49
49
  def classify(str)
50
50
  str.to_s.split("_").map(&:capitalize).join
51
51
  end
52
+
53
+ def underscore(str)
54
+ str.scan(/[A-Z][a-z]*/).map(&:downcase).join('_')
55
+ end
52
56
  end # Class methods
53
57
  end # Utils
54
58
  end # Rester
@@ -1,3 +1,3 @@
1
1
  module Rester
2
- VERSION = '0.2.4'
2
+ VERSION = '0.3.0'
3
3
  end
data/lib/rester.rb CHANGED
@@ -16,12 +16,11 @@ module Rester
16
16
  ].each { |rake_file| load rake_file }
17
17
  end
18
18
 
19
- def connect(*args)
20
- if (service = args.first).is_a?(Class) && service < Service
21
- Client.new(Client::Adapters::LocalAdapter.new(*args))
22
- else
23
- Client.new(*args)
24
- end
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)
23
+ Client.new(adapter, params)
25
24
  end
26
25
  end # Class Methods
27
26
  end # Rester
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.2.4
4
+ version: 0.3.0
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-10-13 00:00:00.000000000 Z
12
+ date: 2015-11-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
@@ -37,14 +37,14 @@ dependencies:
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 4.0.13
40
+ version: '0'
41
41
  type: :runtime
42
42
  prerelease: false
43
43
  version_requirements: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 4.0.13
47
+ version: '0'
48
48
  - !ruby/object:Gem::Dependency
49
49
  name: rspec
50
50
  requirement: !ruby/object:Gem::Requirement
@@ -130,17 +130,20 @@ files:
130
130
  - lib/rester/client/adapters/http_adapter.rb
131
131
  - lib/rester/client/adapters/http_adapter/connection.rb
132
132
  - lib/rester/client/adapters/local_adapter.rb
133
+ - lib/rester/client/adapters/stub_adapter.rb
133
134
  - lib/rester/client/resource.rb
135
+ - lib/rester/client/response.rb
134
136
  - lib/rester/errors.rb
135
137
  - lib/rester/middleware.rb
136
138
  - lib/rester/middleware/base.rb
137
139
  - lib/rester/middleware/error_handling.rb
138
- - lib/rester/middleware/status_check.rb
140
+ - lib/rester/middleware/ping.rb
139
141
  - lib/rester/railtie.rb
142
+ - lib/rester/rspec.rb
140
143
  - lib/rester/service.rb
141
- - lib/rester/service/object.rb
142
- - lib/rester/service/object/validator.rb
143
144
  - lib/rester/service/request.rb
145
+ - lib/rester/service/resource.rb
146
+ - lib/rester/service/resource/params.rb
144
147
  - lib/rester/utils.rb
145
148
  - lib/rester/version.rb
146
149
  homepage: http://github.com/payout/rester
@@ -163,7 +166,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
163
166
  version: '0'
164
167
  requirements: []
165
168
  rubyforge_project:
166
- rubygems_version: 2.4.6
169
+ rubygems_version: 2.4.8
167
170
  signing_key:
168
171
  specification_version: 4
169
172
  summary: A framework for creating simple RESTful interfaces between services.
@@ -1,115 +0,0 @@
1
- require 'rack'
2
- require 'active_support/inflector'
3
-
4
- module Rester
5
- class Service
6
- class Object
7
- autoload(:Validator, 'rester/service/object/validator')
8
-
9
- REQUEST_METHOD_TO_INSTANCE_METHOD = {
10
- 'GET' => :get,
11
- 'PUT' => :update,
12
- 'DELETE' => :delete
13
- }.freeze
14
-
15
- REQUEST_METHOD_TO_CLASS_METHOD = {
16
- 'GET' => :search,
17
- 'POST' => :create
18
- }.freeze
19
-
20
- ########################################################################
21
- # DSL
22
- ########################################################################
23
- class << self
24
- ##
25
- # Specify the name of your identifier (Default: 'id')
26
- def id(name)
27
- (@id_name = name.to_sym).tap { |name|
28
- # Create the accessor method for the ID.
29
- define_method(name) { @id } unless name == :id
30
- }
31
- end
32
-
33
- ##
34
- # Mount another Service Object
35
- def mount(klass)
36
- raise "Only other Service Objects can be mounted." unless klass < Object
37
- start = self.name.split('::')[0..-2].join('::').length + 2
38
- mounts[klass.name[start..-1].pluralize.underscore] = klass
39
- end
40
-
41
- def params(opts={}, &block)
42
- (@_validator = Validator.new(opts)).instance_eval(&block)
43
- @_validator.freeze
44
- end
45
- end # DSL
46
-
47
- ########################################################################
48
- # Class Methods
49
- ########################################################################
50
- class << self
51
- def id_name
52
- @id_name ||= :id
53
- end
54
-
55
- def id_param
56
- "#{self.name.split('::').last.underscore}_#{id_name}"
57
- end
58
-
59
- def mounts
60
- (@__mounts ||= {})
61
- end
62
-
63
- def validator
64
- @_validator ||= Validator.new
65
- end
66
-
67
- ##
68
- # Helper method called at the class and instance level that calls the
69
- # specified method on the passed object with the params. Allows for
70
- # the arity of the method to be 0, 1 or -1.
71
- def process!(obj, meth, params)
72
- if meth && obj.respond_to?(meth)
73
- params = validator.validate(params)
74
- meth = obj.method(meth)
75
-
76
- case meth.arity.abs
77
- when 1
78
- meth.call(params)
79
- when 0
80
- meth.call
81
- else
82
- raise MethodDefinitionError, "#{meth} must take 0 or 1 argument"
83
- end
84
- else
85
- raise Errors::NotFoundError, meth
86
- end
87
- end
88
-
89
- def process(request_method, params={})
90
- meth = REQUEST_METHOD_TO_CLASS_METHOD[request_method]
91
- process!(self, meth, params)
92
- end
93
- end # Class Methods
94
-
95
- attr_reader :id
96
-
97
- def initialize(id)
98
- @id = id
99
- end
100
-
101
- def id_param
102
- self.class.id_param
103
- end
104
-
105
- def process(request_method, params={})
106
- meth = REQUEST_METHOD_TO_INSTANCE_METHOD[request_method]
107
- self.class.process!(self, meth, params)
108
- end
109
-
110
- def mounts
111
- self.class.mounts
112
- end
113
- end # Object
114
- end # Service
115
- end # Rester