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.
@@ -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