sinatra-api 1.0.2 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- # 1. the scope is missing (@space for folder, @space or @folder for page)
36
- # 2. the resource couldn't be identified in its scope (@space or @folder)
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
- puts "locating resource #{r} with id #{resource_id} from #{collection} [#{container}]"
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
 
@@ -21,6 +21,6 @@
21
21
 
22
22
  module Sinatra
23
23
  module API
24
- VERSION = "1.0.2"
24
+ VERSION = "1.1.2"
25
25
  end
26
26
  end
@@ -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