sinatra-api 1.0.2 → 1.1.2
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.
- data/.yardopts +1 -1
- data/lib/sinatra/api.rb +50 -27
- data/lib/sinatra/api/callbacks.rb +29 -0
- data/lib/sinatra/api/config.rb +42 -0
- data/lib/sinatra/api/error_handler.rb +54 -0
- data/lib/sinatra/api/helpers.rb +1 -0
- data/lib/sinatra/api/parameter_validator.rb +90 -0
- data/lib/sinatra/api/parameter_validators/float_validator.rb +17 -0
- data/lib/sinatra/api/parameter_validators/integer_validator.rb +17 -0
- data/lib/sinatra/api/parameter_validators/string_validator.rb +17 -0
- data/lib/sinatra/api/parameters.rb +62 -20
- data/lib/sinatra/api/resource_aliases.rb +1 -1
- data/lib/sinatra/api/resources.rb +22 -3
- data/lib/sinatra/api/version.rb +1 -1
- data/spec/helpers/api_calls.rb +33 -0
- data/spec/helpers/rack_test_api_response.rb +45 -0
- data/spec/helpers/rspec_api_response_matchers.rb +135 -0
- data/spec/integration/parameter_validators/float_validator_spec.rb +85 -0
- data/spec/integration/parameter_validators/integer_validator_spec.rb +66 -0
- data/spec/integration/parameter_validators/string_validator_spec.rb +25 -0
- data/spec/integration/parameter_validators_spec.rb +3 -0
- data/spec/integration/parameters_spec.rb +144 -0
- data/spec/integration/resources_spec.rb +17 -0
- data/spec/spec_helper.rb +5 -11
- data/spec/unit/callbacks_spec.rb +1 -98
- metadata +16 -2
- data/spec/integration/helpers_spec.rb +0 -92
@@ -17,7 +17,7 @@ module Sinatra
|
|
17
17
|
return if key.to_s == resource_alias
|
18
18
|
|
19
19
|
self.resource_aliases[key] << resource_alias
|
20
|
-
logger.debug "API resource #{original} is now aliased as #{resource_alias}"
|
20
|
+
# logger.debug "API resource #{original} is now aliased as #{resource_alias}"
|
21
21
|
end
|
22
22
|
|
23
23
|
def aliases_for(resource)
|
@@ -24,6 +24,22 @@ module Sinatra::API
|
|
24
24
|
# and optional validators.
|
25
25
|
#
|
26
26
|
module Resources
|
27
|
+
|
28
|
+
def self.included(app)
|
29
|
+
app.set(:requires) do |*resources|
|
30
|
+
condition do
|
31
|
+
@required = resources.collect { |r| r.to_s }
|
32
|
+
@required.each do |r|
|
33
|
+
@parent_resource = api_locate_resource(r, @parent_resource)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
Sinatra::API.on :request do |instance|
|
39
|
+
@parent_resource = nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
27
43
|
private
|
28
44
|
|
29
45
|
# Attempt to locate a resource based on an ID supplied in a request parameter.
|
@@ -32,8 +48,9 @@ module Sinatra::API
|
|
32
48
|
# we attempt to locate and expose it to the route.
|
33
49
|
#
|
34
50
|
# A 404 is raised if:
|
35
|
-
#
|
36
|
-
#
|
51
|
+
#
|
52
|
+
# 1. the scope is missing
|
53
|
+
# 2. the resource couldn't be found in its scope
|
37
54
|
#
|
38
55
|
# If the resources were located, they're accessible using @folder or @page.
|
39
56
|
#
|
@@ -41,6 +58,7 @@ module Sinatra::API
|
|
41
58
|
# a resource.
|
42
59
|
#
|
43
60
|
# @example using :requires to reject a request with an invalid @page
|
61
|
+
#
|
44
62
|
# get '/folders/:folder_id/pages/:page_id', :requires => [ :page ] do
|
45
63
|
# @page.show # page is good
|
46
64
|
# @folder.show # so is its folder
|
@@ -55,7 +73,8 @@ module Sinatra::API
|
|
55
73
|
else; container.send("#{r.to_plural}")
|
56
74
|
end
|
57
75
|
|
58
|
-
|
76
|
+
Sinatra::API.logger.debug "locating resource #{r} with id #{resource_id} " +
|
77
|
+
"from #{collection} [#{container}]"
|
59
78
|
|
60
79
|
resource = collection.get(resource_id)
|
61
80
|
|
data/lib/sinatra/api/version.rb
CHANGED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec/helpers/rack_test_api_response'
|
2
|
+
require 'spec/helpers/rspec_api_response_matchers'
|
3
|
+
|
4
|
+
# Converts a Rack::Test HTTP mock response into an API one.
|
5
|
+
#
|
6
|
+
# @see Rack::Test::APIResponse
|
7
|
+
#
|
8
|
+
# @example usage
|
9
|
+
# rc = api_call get '/api/endpoint'
|
10
|
+
# rc.should fail(400, 'some error explanation')
|
11
|
+
# rc.should succeed(201)
|
12
|
+
# rc.messages.empty?.should be_true
|
13
|
+
# rc.status.should == :error
|
14
|
+
#
|
15
|
+
# @example using expect {}
|
16
|
+
# expect { api_call get '/foobar' }.to succeed(200)
|
17
|
+
def api_call(rack_response)
|
18
|
+
Rack::Test::APIResponse.new(rack_response)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Does the same thing as #api_call but wraps it into a block.
|
22
|
+
#
|
23
|
+
# @example usage
|
24
|
+
# api { get '/api/endpoint' }.should fail(403, 'not authorized')
|
25
|
+
#
|
26
|
+
# @see #api_call
|
27
|
+
def api(&block)
|
28
|
+
api_call(block.yield)
|
29
|
+
end
|
30
|
+
|
31
|
+
RSpec.configure do |config|
|
32
|
+
config.include RSpec::APIResponseMatchers
|
33
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Rack
|
2
|
+
module Test
|
3
|
+
class APIResponse
|
4
|
+
attr_reader :rr, :body, :status, :messages, :http_rc
|
5
|
+
|
6
|
+
def initialize(rack_response)
|
7
|
+
@rr = rack_response
|
8
|
+
@http_rc = @rr.status
|
9
|
+
begin;
|
10
|
+
@body = JSON.parse(@rr.body.empty? ? '{}' : @rr.body)
|
11
|
+
@body = @body.with_indifferent_access
|
12
|
+
rescue JSON::ParserError => e
|
13
|
+
raise "Invalid API response;" +
|
14
|
+
"body could not be parsed as JSON:\n#{@rr.body}\nException: #{e.message}"
|
15
|
+
end
|
16
|
+
|
17
|
+
@status = :success
|
18
|
+
@messages = []
|
19
|
+
|
20
|
+
unless blank?
|
21
|
+
if @body.is_a?(Hash)
|
22
|
+
@status = @body["status"].to_sym if @body.has_key?("status")
|
23
|
+
@messages = @body["messages"] if @body.has_key?("messages")
|
24
|
+
elsif @body.is_a?(Array)
|
25
|
+
@messages = @body
|
26
|
+
else
|
27
|
+
@messages = @body
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def blank?
|
33
|
+
@body.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
def succeeded?
|
37
|
+
!blank? && @status == :success
|
38
|
+
end
|
39
|
+
|
40
|
+
def failed?
|
41
|
+
!blank? && @status == :error
|
42
|
+
end
|
43
|
+
end # APIResponse
|
44
|
+
end # Test
|
45
|
+
end # Rack
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module RSpec
|
2
|
+
module APIResponseMatchers
|
3
|
+
class Fail
|
4
|
+
def initialize(http_rc = 400, *keywords)
|
5
|
+
@http_rc = http_rc
|
6
|
+
@keywords = (keywords || []).flatten
|
7
|
+
@raw_keywords = @keywords.dup
|
8
|
+
end
|
9
|
+
|
10
|
+
def matches?(api_rc)
|
11
|
+
if api_rc.is_a?(Proc)
|
12
|
+
api_rc = api_rc.yield
|
13
|
+
end
|
14
|
+
|
15
|
+
@api_rc = api_rc
|
16
|
+
|
17
|
+
return false if api_rc.http_rc != @http_rc
|
18
|
+
return false if api_rc.status != :error
|
19
|
+
|
20
|
+
if @keywords.empty?
|
21
|
+
return true if api_rc.messages.empty?
|
22
|
+
return false
|
23
|
+
end
|
24
|
+
|
25
|
+
if @keywords.size == 1
|
26
|
+
@keywords = @keywords.first.split(/\s/)
|
27
|
+
end
|
28
|
+
|
29
|
+
@keywords = Regexp.new(@keywords.join('.*'), 'i')
|
30
|
+
|
31
|
+
matched = false
|
32
|
+
api_rc.messages.each { |m|
|
33
|
+
if m.match(@keywords)
|
34
|
+
matched = true
|
35
|
+
break
|
36
|
+
end
|
37
|
+
}
|
38
|
+
|
39
|
+
matched
|
40
|
+
end # Fail#matches
|
41
|
+
|
42
|
+
def failure_message
|
43
|
+
m = "Expected: \n"
|
44
|
+
|
45
|
+
if @api_rc.status != :error
|
46
|
+
m << "* The API response status to be :error, but got #{@api_rc.status}\n"
|
47
|
+
end
|
48
|
+
|
49
|
+
if @api_rc.http_rc != @http_rc
|
50
|
+
m << "* The HTTP RC to be #{@http_rc}, but got #{@api_rc.http_rc}\n"
|
51
|
+
end
|
52
|
+
|
53
|
+
formatted_keywords = @raw_keywords.join(' ')
|
54
|
+
|
55
|
+
if @raw_keywords.any? && @api_rc.messages.any?
|
56
|
+
m << "* One of the following API response messages: \n"
|
57
|
+
m << @api_rc.messages.collect.with_index { |m, i| "\t#{i+1}. #{m}" }.join("\n")
|
58
|
+
m << "\n to be matched by the keywords: #{formatted_keywords}\n"
|
59
|
+
|
60
|
+
elsif @raw_keywords.any? && @api_rc.messages.empty?
|
61
|
+
m << "* The API response to contain some messages (got 0) and for at least\n" <<
|
62
|
+
" one of them to match the keywords #{formatted_keywords}\n"
|
63
|
+
|
64
|
+
elsif @raw_keywords.empty? && @api_rc.messages.any?
|
65
|
+
m << "* The API response to contain no messages, but got: \n"
|
66
|
+
m << @api_rc.messages.collect.with_index { |m, i| "\t#{i+1}. #{m}" }.join("\n")
|
67
|
+
m << "\n"
|
68
|
+
end
|
69
|
+
|
70
|
+
m
|
71
|
+
end
|
72
|
+
|
73
|
+
# def negative_failure_message
|
74
|
+
# "expected API response [status:#{@api_rc.status}] not to be :error, " <<
|
75
|
+
# "and API response [messages: #{@api_rc.messages}] not to match '#{@keywords}'"
|
76
|
+
# end
|
77
|
+
end # Fail
|
78
|
+
|
79
|
+
class Success
|
80
|
+
def initialize(http_rc)
|
81
|
+
@http_rc = http_rc
|
82
|
+
end
|
83
|
+
|
84
|
+
def matches?(api_rc)
|
85
|
+
if api_rc.is_a?(Proc)
|
86
|
+
api_rc = api_rc.yield
|
87
|
+
end
|
88
|
+
|
89
|
+
@api_rc = api_rc
|
90
|
+
|
91
|
+
return false unless @http_rc.include?(api_rc.http_rc)
|
92
|
+
return false if api_rc.status != :success
|
93
|
+
|
94
|
+
true
|
95
|
+
end
|
96
|
+
|
97
|
+
def failure_message
|
98
|
+
m = "Expected:\n"
|
99
|
+
|
100
|
+
if @api_rc.status != :success
|
101
|
+
m << "* The API response status to be :success, but got #{@api_rc.status}\n"
|
102
|
+
end
|
103
|
+
|
104
|
+
if @api_rc.http_rc != @http_rc
|
105
|
+
m << "* The HTTP RC to be #{@http_rc}, but got #{@api_rc.http_rc}\n"
|
106
|
+
end
|
107
|
+
|
108
|
+
if @api_rc.messages.any?
|
109
|
+
m << "* The API response messages to be empty, but got: \n"
|
110
|
+
m << @api_rc.messages.collect.with_index { |m, i| "\t#{i+1}. #{m}" }.join("\n")
|
111
|
+
m << "\n"
|
112
|
+
end
|
113
|
+
|
114
|
+
m
|
115
|
+
end
|
116
|
+
|
117
|
+
# def negative_failure_message
|
118
|
+
# m = "expected API response [status:#{@api_rc.status}] not to be :success"
|
119
|
+
# if @api_rc.messages.any?
|
120
|
+
# m << ", and no messages, but got: #{@api_rc.messages}"
|
121
|
+
# end
|
122
|
+
# m
|
123
|
+
# end
|
124
|
+
end
|
125
|
+
|
126
|
+
def fail(http_rc = 400, *keywords)
|
127
|
+
Fail.new(http_rc, keywords)
|
128
|
+
end
|
129
|
+
|
130
|
+
def succeed(http_rc = 200..205)
|
131
|
+
http_rc = http_rc.respond_to?(:to_a) ? http_rc.to_a : [ http_rc ]
|
132
|
+
Success.new(http_rc.flatten)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
describe Sinatra::API::FloatValidator do
|
2
|
+
include_examples 'integration specs'
|
3
|
+
|
4
|
+
it 'should accept a float' do
|
5
|
+
app.post '/' do
|
6
|
+
api_parameter! :rate, required: true, type: :float
|
7
|
+
api_params.to_json
|
8
|
+
end
|
9
|
+
|
10
|
+
rc = api_call post '/', { rate: 5.75 }.to_json
|
11
|
+
rc.should succeed
|
12
|
+
rc.body[:rate].should == 5.75
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should accept a float with scientific notation' do
|
16
|
+
app.post '/' do
|
17
|
+
api_parameter! :rate, required: true, type: :float
|
18
|
+
api_params.to_json
|
19
|
+
end
|
20
|
+
|
21
|
+
rc = api_call post '/', { rate: 5.75e3 }.to_json
|
22
|
+
rc.should succeed
|
23
|
+
rc.body[:rate].should == 5.75e3
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should accept a really big decimal' do
|
27
|
+
app.post '/' do
|
28
|
+
api_parameter! :rate, required: true, type: :float
|
29
|
+
api_params.to_json
|
30
|
+
end
|
31
|
+
|
32
|
+
rc = api_call post '/', { rate: 123456789.75e24 }.to_json
|
33
|
+
rc.should succeed
|
34
|
+
rc.body[:rate].should == 123456789.75e24
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should accept an integer' do
|
38
|
+
app.post '/' do
|
39
|
+
api_parameter! :rate, required: true, type: :float
|
40
|
+
api_params.to_json
|
41
|
+
end
|
42
|
+
|
43
|
+
rc = api_call post '/', { rate: 5 }.to_json
|
44
|
+
rc.should succeed
|
45
|
+
rc.body[:rate].should == 5.0
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should reject a non-numeric value' do
|
49
|
+
app.post '/' do
|
50
|
+
api_parameter! :rate, required: true, type: :float
|
51
|
+
api_params.to_json
|
52
|
+
end
|
53
|
+
|
54
|
+
rc = api_call post '/', { rate: 'asdf' }.to_json
|
55
|
+
rc.should fail(400, 'not a valid float')
|
56
|
+
|
57
|
+
rc = api_call post '/', { rate: [] }.to_json
|
58
|
+
rc.should fail(400, 'not a valid float')
|
59
|
+
|
60
|
+
rc = api_call post '/', { rate: {} }.to_json
|
61
|
+
rc.should fail(400, 'not a valid float')
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should coerce a float' do
|
65
|
+
app.post '/' do
|
66
|
+
api_parameter! :rate, required: true, type: :float
|
67
|
+
api_params.to_json
|
68
|
+
end
|
69
|
+
|
70
|
+
rc = api_call post '/', { rate: "5.8" }.to_json
|
71
|
+
rc.should succeed
|
72
|
+
rc.body[:rate].should == 5.8
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'should not coerce a float' do
|
76
|
+
app.post '/' do
|
77
|
+
api_parameter! :rate, required: true, type: :float, coerce: false
|
78
|
+
api_params.to_json
|
79
|
+
end
|
80
|
+
|
81
|
+
rc = api_call post '/', { rate: "15.25" }.to_json
|
82
|
+
rc.should succeed
|
83
|
+
rc.body[:rate].should == "15.25"
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
describe Sinatra::API::IntegerValidator do
|
2
|
+
include_examples 'integration specs'
|
3
|
+
|
4
|
+
it 'should accept a numeric value' do
|
5
|
+
app.post '/' do
|
6
|
+
api_parameter! :rate, required: true, type: :integer
|
7
|
+
api_params.to_json
|
8
|
+
end
|
9
|
+
|
10
|
+
rc = api_call post '/', { rate: 5 }.to_json
|
11
|
+
rc.should succeed
|
12
|
+
rc.body[:rate].should == 5
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should reject a non-numeric value' do
|
16
|
+
app.post '/' do
|
17
|
+
api_parameter! :rate, required: true, type: :integer
|
18
|
+
api_params.to_json
|
19
|
+
end
|
20
|
+
|
21
|
+
rc = api_call post '/', { rate: 'asdf' }.to_json
|
22
|
+
rc.should fail(400, 'not a valid integer')
|
23
|
+
|
24
|
+
rc = api_call post '/', { rate: '5.73' }.to_json
|
25
|
+
rc.should fail(400, 'not a valid integer')
|
26
|
+
|
27
|
+
rc = api_call post '/', { rate: [] }.to_json
|
28
|
+
rc.should fail(400, 'not a valid integer')
|
29
|
+
|
30
|
+
rc = api_call post '/', { rate: {} }.to_json
|
31
|
+
rc.should fail(400, 'not a valid integer')
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should round a float' do
|
35
|
+
app.post '/' do
|
36
|
+
api_parameter! :rate, required: true, type: :integer
|
37
|
+
api_params.to_json
|
38
|
+
end
|
39
|
+
|
40
|
+
rc = api_call post '/', { rate: 5.75 }.to_json
|
41
|
+
rc.should succeed
|
42
|
+
rc.body[:rate].should == 5
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should coerce an integer' do
|
46
|
+
app.post '/' do
|
47
|
+
api_parameter! :rate, required: true, type: :integer
|
48
|
+
api_params.to_json
|
49
|
+
end
|
50
|
+
|
51
|
+
rc = api_call post '/', { rate: "15" }.to_json
|
52
|
+
rc.should succeed
|
53
|
+
rc.body[:rate].should == 15
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should not coerce an integer' do
|
57
|
+
app.post '/' do
|
58
|
+
api_parameter! :rate, required: true, type: :integer, coerce: false
|
59
|
+
api_params.to_json
|
60
|
+
end
|
61
|
+
|
62
|
+
rc = api_call post '/', { rate: "15" }.to_json
|
63
|
+
rc.should succeed
|
64
|
+
rc.body[:rate].should == "15"
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
describe Sinatra::API::StringValidator do
|
2
|
+
include_examples 'integration specs'
|
3
|
+
|
4
|
+
it 'should accept a string value' do
|
5
|
+
app.get '/' do
|
6
|
+
api_parameter! :name, required: true, type: :string
|
7
|
+
api_parameter :name
|
8
|
+
end
|
9
|
+
|
10
|
+
get '/', { name: 'foo' }
|
11
|
+
last_response.status.should == 200
|
12
|
+
last_response.body.should == 'foo'
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should reject a non-string value' do
|
16
|
+
app.post '/' do
|
17
|
+
api_parameter! :name, required: true, type: :string
|
18
|
+
api_parameter :name
|
19
|
+
end
|
20
|
+
|
21
|
+
rc = api_call post '/', { name: 5 }.to_json
|
22
|
+
rc.should fail(400, '')
|
23
|
+
rc.body[:field_errors][:name].should match(/expected.*string/i)
|
24
|
+
end
|
25
|
+
end
|