rester 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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