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 +4 -4
- data/lib/rester/client/adapters/adapter.rb +9 -0
- data/lib/rester/client/adapters/http_adapter.rb +12 -0
- data/lib/rester/client/adapters/local_adapter.rb +7 -3
- data/lib/rester/client/adapters/stub_adapter.rb +90 -0
- data/lib/rester/client/adapters.rb +8 -0
- data/lib/rester/client/resource.rb +3 -8
- data/lib/rester/client/response.rb +28 -0
- data/lib/rester/client.rb +33 -17
- data/lib/rester/errors.rb +4 -0
- data/lib/rester/middleware/error_handling.rb +9 -3
- data/lib/rester/middleware/{status_check.rb → ping.rb} +3 -3
- data/lib/rester/middleware.rb +1 -1
- data/lib/rester/rspec.rb +152 -0
- data/lib/rester/service/{object/validator.rb → resource/params.rb} +94 -22
- data/lib/rester/service/resource.rb +127 -0
- data/lib/rester/service.rb +16 -19
- data/lib/rester/utils.rb +4 -0
- data/lib/rester/version.rb +1 -1
- data/lib/rester.rb +5 -6
- metadata +11 -8
- data/lib/rester/service/object.rb +0 -115
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 781dcd45b4ea9347717fba375241286931b28444
|
4
|
+
data.tar.gz: 3319242e32b49c30316711918748809e736880fa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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' =>
|
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 #
|
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(
|
12
|
-
|
13
|
-
|
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('
|
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
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
@@ -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
|
27
|
-
body_h = {
|
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
|
5
|
+
class Ping < Base
|
6
6
|
def call(env)
|
7
|
-
if %r{\A/
|
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 #
|
13
|
+
end # Ping
|
14
14
|
end # Middleware
|
15
15
|
end # Rester
|
data/lib/rester/middleware.rb
CHANGED
data/lib/rester/rspec.rb
ADDED
@@ -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::
|
3
|
-
class
|
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
|
-
|
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
|
55
|
+
klass = @_validators[key].first
|
52
56
|
|
53
57
|
_parse_with_class(klass, value).tap do |obj|
|
54
|
-
|
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
|
data/lib/rester/service.rb
CHANGED
@@ -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(:
|
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::
|
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
|
72
|
-
(@
|
71
|
+
def resources(version_module)
|
72
|
+
(@__resources ||= {})[version_module] ||= _load_resources(version_module)
|
73
73
|
end
|
74
74
|
|
75
|
-
def
|
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::
|
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::
|
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::
|
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 =
|
148
|
+
obj = _load_resource(request, name)
|
149
149
|
|
150
150
|
loop {
|
151
|
-
obj
|
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
|
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::
|
166
|
+
# Loads the appropriate Service::Resource for the request. This will return
|
168
167
|
# the class, not an instance.
|
169
|
-
def
|
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::
|
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
data/lib/rester/version.rb
CHANGED
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(
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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.
|
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
|
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:
|
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:
|
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/
|
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.
|
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
|