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 +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
|