grape 0.1.5 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of grape might be problematic. Click here for more details.
- data/.gitignore +13 -1
- data/.rspec +1 -1
- data/.travis.yml +1 -0
- data/Gemfile +10 -0
- data/Guardfile +15 -0
- data/README.markdown +472 -67
- data/grape.gemspec +4 -4
- data/lib/grape.rb +17 -5
- data/lib/grape/api.rb +227 -124
- data/lib/grape/cookies.rb +41 -0
- data/lib/grape/endpoint.rb +256 -27
- data/lib/grape/entity.rb +227 -0
- data/lib/grape/middleware/auth/oauth2.rb +1 -2
- data/lib/grape/middleware/base.rb +59 -6
- data/lib/grape/middleware/error.rb +15 -6
- data/lib/grape/middleware/filter.rb +17 -0
- data/lib/grape/middleware/formatter.rb +25 -31
- data/lib/grape/middleware/versioner.rb +20 -20
- data/lib/grape/middleware/versioner/header.rb +59 -0
- data/lib/grape/middleware/versioner/path.rb +42 -0
- data/lib/grape/route.rb +23 -0
- data/lib/grape/util/hash_stack.rb +100 -0
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api_spec.rb +651 -162
- data/spec/grape/endpoint_spec.rb +216 -18
- data/spec/grape/entity_spec.rb +320 -0
- data/spec/grape/middleware/auth/basic_spec.rb +2 -2
- data/spec/grape/middleware/auth/digest_spec.rb +4 -6
- data/spec/grape/middleware/exception_spec.rb +1 -0
- data/spec/grape/middleware/formatter_spec.rb +81 -27
- data/spec/grape/middleware/versioner/header_spec.rb +148 -0
- data/spec/grape/middleware/versioner/path_spec.rb +40 -0
- data/spec/grape/middleware/versioner_spec.rb +6 -34
- data/spec/grape/util/hash_stack_spec.rb +133 -0
- data/spec/shared/versioning_examples.rb +77 -0
- data/spec/spec_helper.rb +11 -3
- data/spec/support/basic_auth_encode_helpers.rb +4 -0
- data/spec/support/rack_patch.rb +25 -0
- data/spec/support/versioned_helpers.rb +34 -0
- metadata +140 -241
- data/.yardoc/checksums +0 -13
- data/.yardoc/objects/Grape.dat +0 -0
- data/.yardoc/objects/Grape/API.dat +0 -0
- data/.yardoc/objects/Grape/API/auth_c.dat +0 -0
- data/.yardoc/objects/Grape/API/build_endpoint_c.dat +0 -0
- data/.yardoc/objects/Grape/API/call_c.dat +0 -0
- data/.yardoc/objects/Grape/API/compile_path_c.dat +0 -0
- data/.yardoc/objects/Grape/API/default_format_c.dat +0 -0
- data/.yardoc/objects/Grape/API/delete_c.dat +0 -0
- data/.yardoc/objects/Grape/API/get_c.dat +0 -0
- data/.yardoc/objects/Grape/API/group_c.dat +0 -0
- data/.yardoc/objects/Grape/API/head_c.dat +0 -0
- data/.yardoc/objects/Grape/API/helpers_c.dat +0 -0
- data/.yardoc/objects/Grape/API/http_basic_c.dat +0 -0
- data/.yardoc/objects/Grape/API/inherited_c.dat +0 -0
- data/.yardoc/objects/Grape/API/logger_c.dat +0 -0
- data/.yardoc/objects/Grape/API/namespace_c.dat +0 -0
- data/.yardoc/objects/Grape/API/nest_c.dat +0 -0
- data/.yardoc/objects/Grape/API/post_c.dat +0 -0
- data/.yardoc/objects/Grape/API/prefix_c.dat +0 -0
- data/.yardoc/objects/Grape/API/put_c.dat +0 -0
- data/.yardoc/objects/Grape/API/reset_21_c.dat +0 -0
- data/.yardoc/objects/Grape/API/resource_c.dat +0 -0
- data/.yardoc/objects/Grape/API/resources_c.dat +0 -0
- data/.yardoc/objects/Grape/API/route_c.dat +0 -0
- data/.yardoc/objects/Grape/API/route_set_c.dat +0 -0
- data/.yardoc/objects/Grape/API/scope_c.dat +0 -0
- data/.yardoc/objects/Grape/API/set_c.dat +0 -0
- data/.yardoc/objects/Grape/API/settings_c.dat +0 -0
- data/.yardoc/objects/Grape/API/settings_stack_c.dat +0 -0
- data/.yardoc/objects/Grape/API/version_c.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/block_3D_c.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/block_c.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/call_c.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/call_i.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/env_i.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/error_21_i.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/generate_c.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/header_i.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/params_i.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/request_i.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/status_i.dat +0 -0
- data/.yardoc/objects/Grape/Endpoint/version_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/Basic.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/Basic/authenticator_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/Basic/basic_request_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/Basic/before_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/Basic/credentials_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/Basic/initialize_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/OAuth2.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/OAuth2/before_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/OAuth2/default_options_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/OAuth2/error_out_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/OAuth2/parse_authorization_header_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/OAuth2/token_class_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Auth/OAuth2/verify_token_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/after_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/app_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/before_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/call_21_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/call_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/default_options_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/env_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/initialize_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/options_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/request_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Base/response_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Error.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Error/call_21_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Error/error_response_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/CONTENT_TYPES.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/after_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/before_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/content_types_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/default_options_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/encode_json_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/encode_txt_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/format_from_extension_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/format_from_header_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/headers_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/mime_array_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Formatter/mime_types_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Prefixer.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Prefixer/before_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Prefixer/prefix_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Versioner.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Versioner/before_i.dat +0 -0
- data/.yardoc/objects/Grape/Middleware/Versioner/default_options_i.dat +0 -0
- data/.yardoc/objects/Grape/MiddlewareStack.dat +0 -0
- data/.yardoc/objects/Grape/MiddlewareStack/initialize_i.dat +0 -0
- data/.yardoc/objects/Grape/MiddlewareStack/stack_i.dat +0 -0
- data/.yardoc/objects/Grape/MiddlewareStack/to_app_i.dat +0 -0
- data/.yardoc/objects/Grape/MiddlewareStack/use_i.dat +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/.yardoc/proxy_types +0 -2
- data/Gemfile.lock +0 -52
- data/autotest/discover.rb +0 -1
@@ -0,0 +1,100 @@
|
|
1
|
+
module Grape
|
2
|
+
module Util
|
3
|
+
# HashStack is a stack of hashes. When retrieving a value, keys of the top
|
4
|
+
# hash on the stack take precendent over the lower keys.
|
5
|
+
class HashStack
|
6
|
+
# Unmerged array of hashes to represent the stack.
|
7
|
+
# The top of the stack is the last element.
|
8
|
+
attr_reader :stack
|
9
|
+
|
10
|
+
# TODO: handle aggregates
|
11
|
+
def initialize
|
12
|
+
@stack = [{}]
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns the top hash on the stack
|
16
|
+
def peek
|
17
|
+
@stack.last
|
18
|
+
end
|
19
|
+
|
20
|
+
# Add a new hash to the top of the stack.
|
21
|
+
#
|
22
|
+
# @param hash [Hash] optional hash to be pushed. Defaults to empty hash
|
23
|
+
# @return [HashStack]
|
24
|
+
def push(hash = {})
|
25
|
+
@stack.push(hash)
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def pop
|
30
|
+
@stack.pop
|
31
|
+
end
|
32
|
+
|
33
|
+
# Looks through the stack for the first frame that matches :key
|
34
|
+
#
|
35
|
+
# @param key [Symbol] key to look for in hash frames
|
36
|
+
# @return value of given key after merging the stack
|
37
|
+
def get(key)
|
38
|
+
(@stack.length - 1).downto(0).each do |i|
|
39
|
+
return @stack[i][key] if @stack[i].key? key
|
40
|
+
end
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
alias_method :[], :get
|
44
|
+
|
45
|
+
# Replace a value on the top hash of the stack.
|
46
|
+
#
|
47
|
+
# @param key [Symbol] The key to set.
|
48
|
+
# @param value [Object] The value to set.
|
49
|
+
def set(key, value)
|
50
|
+
peek[key.to_sym] = value
|
51
|
+
end
|
52
|
+
alias_method :[]=, :set
|
53
|
+
|
54
|
+
# Replace multiple values on the top hash of the stack.
|
55
|
+
#
|
56
|
+
# @param hash [Hash] Hash of values to be merged in.
|
57
|
+
def update(hash)
|
58
|
+
peek.merge!(hash)
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
# Adds addition value into the top hash of the stack
|
63
|
+
def imbue(key, value)
|
64
|
+
current = peek[key.to_sym]
|
65
|
+
if current.is_a?(Array)
|
66
|
+
current.concat(value)
|
67
|
+
elsif current.is_a?(Hash)
|
68
|
+
current.merge!(value)
|
69
|
+
else
|
70
|
+
set(key, value)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Prepend another HashStack's to self
|
75
|
+
def prepend(hash_stack)
|
76
|
+
@stack.unshift *hash_stack.stack
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
# Concatenate another HashStack's to self
|
81
|
+
def concat(hash_stack)
|
82
|
+
@stack.concat hash_stack.stack
|
83
|
+
self
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_s
|
87
|
+
@stack.to_s
|
88
|
+
end
|
89
|
+
|
90
|
+
def clone
|
91
|
+
new_stack = HashStack.new
|
92
|
+
stack.each do |frame|
|
93
|
+
new_stack.push frame.clone
|
94
|
+
end
|
95
|
+
new_stack.stack.shift
|
96
|
+
new_stack
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
data/lib/grape/version.rb
CHANGED
data/spec/grape/api_spec.rb
CHANGED
@@ -1,130 +1,105 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'shared/versioning_examples'
|
2
3
|
|
3
4
|
describe Grape::API do
|
4
5
|
subject { Class.new(Grape::API) }
|
5
|
-
|
6
|
-
|
6
|
+
|
7
7
|
def app; subject end
|
8
|
-
|
8
|
+
|
9
9
|
describe '.prefix' do
|
10
10
|
it 'should route through with the prefix' do
|
11
11
|
subject.prefix 'awesome/sauce'
|
12
12
|
subject.get :hello do
|
13
13
|
"Hello there."
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
get 'awesome/sauce/hello'
|
17
17
|
last_response.body.should eql "Hello there."
|
18
|
-
|
18
|
+
|
19
19
|
get '/hello'
|
20
20
|
last_response.status.should eql 404
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
-
describe '.version' do
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
24
|
+
describe '.version using path' do
|
25
|
+
it_should_behave_like 'versioning' do
|
26
|
+
let(:macro_options) do
|
27
|
+
{
|
28
|
+
:using => :path
|
29
|
+
}
|
29
30
|
end
|
30
|
-
|
31
|
-
get '/v1/hello'
|
32
|
-
last_response.body.should eql "Version: v1"
|
33
31
|
end
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '.version using header' do
|
35
|
+
it_should_behave_like 'versioning' do
|
36
|
+
let(:macro_options) do
|
37
|
+
{
|
38
|
+
:using => :header,
|
39
|
+
:vendor => 'mycompany',
|
40
|
+
:format => 'json'
|
41
|
+
}
|
40
42
|
end
|
41
|
-
|
42
|
-
get '/api/v1/hello'
|
43
|
-
last_response.body.should eql "Version: v1"
|
44
43
|
end
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
get '/v1/awesome'
|
59
|
-
last_response.status.should eql 404
|
60
|
-
get '/v2/awesome'
|
61
|
-
last_response.status.should eql 200
|
62
|
-
get '/v1/legacy'
|
63
|
-
last_response.status.should eql 200
|
64
|
-
get '/v2/legacy'
|
65
|
-
last_response.status.should eql 404
|
44
|
+
|
45
|
+
# Behavior as defined by rfc2616 when no header is defined
|
46
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
47
|
+
describe 'no specified accept header' do
|
48
|
+
# subject.version 'v1', :using => :header
|
49
|
+
# subject.get '/hello' do
|
50
|
+
# 'hello'
|
51
|
+
# end
|
52
|
+
|
53
|
+
# it 'should route' do
|
54
|
+
# get '/hello'
|
55
|
+
# last_response.status.should eql 200
|
56
|
+
# end
|
66
57
|
end
|
67
|
-
|
68
|
-
it 'should
|
69
|
-
subject.version 'v1', 'v2'
|
70
|
-
subject.get 'awesome' do
|
71
|
-
"I exist"
|
72
|
-
end
|
58
|
+
|
59
|
+
it 'should route if any media type is allowed' do
|
73
60
|
|
74
|
-
get '/v1/awesome'
|
75
|
-
last_response.status.should eql 200
|
76
|
-
get '/v2/awesome'
|
77
|
-
last_response.status.should eql 200
|
78
|
-
get '/v3/awesome'
|
79
|
-
last_response.status.should eql 404
|
80
61
|
end
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
end
|
93
|
-
|
94
|
-
get '/v2/version'
|
95
|
-
last_response.status.should == 200
|
96
|
-
last_response.body.should == 'v2'
|
97
|
-
get '/v1/version'
|
98
|
-
last_response.status.should == 200
|
99
|
-
last_response.body.should == 'version v1'
|
62
|
+
end
|
63
|
+
|
64
|
+
describe '.represent' do
|
65
|
+
it 'should require a :with option' do
|
66
|
+
expect{ subject.represent Object, {} }.to raise_error(ArgumentError)
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should add the association to the :representations setting' do
|
70
|
+
klass = Class.new
|
71
|
+
subject.represent Object, :with => klass
|
72
|
+
subject.settings[:representations][Object].should == klass
|
100
73
|
end
|
101
|
-
|
102
74
|
end
|
103
|
-
|
75
|
+
|
104
76
|
describe '.namespace' do
|
105
77
|
it 'should be retrievable and converted to a path' do
|
106
78
|
subject.namespace :awesome do
|
107
79
|
namespace.should == '/awesome'
|
108
80
|
end
|
109
81
|
end
|
110
|
-
|
82
|
+
|
111
83
|
it 'should come after the prefix and version' do
|
112
84
|
subject.prefix :rad
|
113
|
-
subject.version :
|
114
|
-
|
85
|
+
subject.version 'v1', :using => :path
|
86
|
+
|
115
87
|
subject.namespace :awesome do
|
116
|
-
|
88
|
+
get('/hello'){ "worked" }
|
117
89
|
end
|
90
|
+
|
91
|
+
get "/rad/v1/awesome/hello"
|
92
|
+
last_response.body.should == "worked"
|
118
93
|
end
|
119
|
-
|
94
|
+
|
120
95
|
it 'should cancel itself after the block is over' do
|
121
96
|
subject.namespace :awesome do
|
122
97
|
namespace.should == '/awesome'
|
123
98
|
end
|
124
|
-
|
99
|
+
|
125
100
|
subject.namespace.should == '/'
|
126
101
|
end
|
127
|
-
|
102
|
+
|
128
103
|
it 'should be stackable' do
|
129
104
|
subject.namespace :awesome do
|
130
105
|
namespace :rad do
|
@@ -134,16 +109,34 @@ describe Grape::API do
|
|
134
109
|
end
|
135
110
|
subject.namespace.should == '/'
|
136
111
|
end
|
112
|
+
|
113
|
+
it 'should accept path segments correctly' do
|
114
|
+
subject.namespace :members do
|
115
|
+
namespace "/:member_id" do
|
116
|
+
namespace.should == '/members/:member_id'
|
117
|
+
get '/' do
|
118
|
+
params[:member_id]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
get '/members/23'
|
123
|
+
last_response.body.should == "23"
|
124
|
+
end
|
137
125
|
|
138
126
|
it 'should be callable with nil just to push onto the stack' do
|
139
127
|
subject.namespace do
|
140
|
-
version 'v2'
|
141
|
-
|
128
|
+
version 'v2', :using => :path
|
129
|
+
get('/hello'){ "inner" }
|
142
130
|
end
|
143
|
-
subject.
|
131
|
+
subject.get('/hello'){ "outer" }
|
132
|
+
|
133
|
+
get '/v2/hello'
|
134
|
+
last_response.body.should == "inner"
|
135
|
+
get '/hello'
|
136
|
+
last_response.body.should == "outer"
|
144
137
|
end
|
145
138
|
|
146
|
-
%w(group resource resources).each do |als|
|
139
|
+
%w(group resource resources segment).each do |als|
|
147
140
|
it "`.#{als}` should be an alias" do
|
148
141
|
subject.send(als, :awesome) do
|
149
142
|
namespace.should == "/awesome"
|
@@ -151,61 +144,81 @@ describe Grape::API do
|
|
151
144
|
end
|
152
145
|
end
|
153
146
|
end
|
154
|
-
|
147
|
+
|
155
148
|
describe '.route' do
|
156
149
|
it 'should allow for no path' do
|
157
150
|
subject.namespace :votes do
|
158
151
|
get do
|
159
152
|
"Votes"
|
160
153
|
end
|
161
|
-
|
154
|
+
|
162
155
|
post do
|
163
156
|
"Created a Vote"
|
164
157
|
end
|
165
158
|
end
|
166
|
-
|
159
|
+
|
167
160
|
get '/votes'
|
168
161
|
last_response.body.should eql 'Votes'
|
169
162
|
post '/votes'
|
170
163
|
last_response.body.should eql 'Created a Vote'
|
171
164
|
end
|
172
|
-
|
165
|
+
|
173
166
|
it 'should allow for multiple paths' do
|
174
|
-
subject.get("/abc", "/def") do
|
167
|
+
subject.get(["/abc", "/def"]) do
|
175
168
|
"foo"
|
176
169
|
end
|
177
|
-
|
170
|
+
|
178
171
|
get '/abc'
|
179
172
|
last_response.body.should eql 'foo'
|
180
173
|
get '/def'
|
181
174
|
last_response.body.should eql 'foo'
|
182
175
|
end
|
183
176
|
|
184
|
-
|
185
|
-
|
186
|
-
"
|
177
|
+
context "format" do
|
178
|
+
before(:each) do
|
179
|
+
subject.get("/abc") do
|
180
|
+
RSpec::Mocks::Mock.new(:to_json => 'abc', :to_txt => 'def')
|
181
|
+
end
|
187
182
|
end
|
188
|
-
|
189
|
-
|
190
|
-
|
183
|
+
|
184
|
+
it "should allow .json" do
|
185
|
+
get '/abc.json'
|
186
|
+
last_response.status.should == 200
|
187
|
+
last_response.body.should eql 'abc' # json-encoded symbol
|
188
|
+
end
|
189
|
+
|
190
|
+
it "should allow .txt" do
|
191
|
+
get '/abc.txt'
|
192
|
+
last_response.status.should == 200
|
193
|
+
last_response.body.should eql 'def' # raw text
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'should allow for format without corrupting a param' do
|
198
|
+
subject.get('/:id') do
|
199
|
+
{"id" => params[:id]}
|
200
|
+
end
|
201
|
+
|
202
|
+
get '/awesome.json'
|
203
|
+
last_response.body.should eql '{"id":"awesome"}'
|
191
204
|
end
|
192
205
|
|
193
206
|
it 'should allow for format in namespace with no path' do
|
194
207
|
subject.namespace :abc do
|
195
208
|
get do
|
196
|
-
"json"
|
209
|
+
["json"]
|
197
210
|
end
|
198
211
|
end
|
199
|
-
|
212
|
+
|
200
213
|
get '/abc.json'
|
201
|
-
last_response.body.should eql '"json"'
|
214
|
+
last_response.body.should eql '["json"]'
|
202
215
|
end
|
203
|
-
|
216
|
+
|
204
217
|
it 'should allow for multiple verbs' do
|
205
218
|
subject.route([:get, :post], '/abc') do
|
206
219
|
"hiya"
|
207
220
|
end
|
208
|
-
|
221
|
+
|
209
222
|
get '/abc'
|
210
223
|
last_response.body.should eql 'hiya'
|
211
224
|
post '/abc'
|
@@ -214,11 +227,9 @@ describe Grape::API do
|
|
214
227
|
|
215
228
|
it 'should allow for multipart paths' do
|
216
229
|
|
217
|
-
|
218
230
|
subject.route([:get, :post], '/:id/first') do
|
219
231
|
"first"
|
220
232
|
end
|
221
|
-
|
222
233
|
|
223
234
|
subject.route([:get, :post], '/:id') do
|
224
235
|
"ola"
|
@@ -226,7 +237,6 @@ describe Grape::API do
|
|
226
237
|
subject.route([:get, :post], '/:id/first/second') do
|
227
238
|
"second"
|
228
239
|
end
|
229
|
-
|
230
240
|
|
231
241
|
get '/1'
|
232
242
|
last_response.body.should eql 'ola'
|
@@ -240,19 +250,19 @@ describe Grape::API do
|
|
240
250
|
last_response.body.should eql 'second'
|
241
251
|
|
242
252
|
end
|
243
|
-
|
253
|
+
|
244
254
|
it 'should allow for :any as a verb' do
|
245
255
|
subject.route(:any, '/abc') do
|
246
256
|
"lol"
|
247
257
|
end
|
248
|
-
|
249
|
-
%w(get post put delete).each do |m|
|
258
|
+
|
259
|
+
%w(get post put delete options patch).each do |m|
|
250
260
|
send(m, '/abc')
|
251
261
|
last_response.body.should eql 'lol'
|
252
262
|
end
|
253
263
|
end
|
254
|
-
|
255
|
-
verbs = %w(post get head delete put)
|
264
|
+
|
265
|
+
verbs = %w(post get head delete put options patch)
|
256
266
|
verbs.each do |verb|
|
257
267
|
it "should allow and properly constrain a #{verb.upcase} method" do
|
258
268
|
subject.send(verb, '/example') do
|
@@ -265,18 +275,44 @@ describe Grape::API do
|
|
265
275
|
last_response.status.should eql 404
|
266
276
|
end
|
267
277
|
end
|
268
|
-
|
278
|
+
|
269
279
|
it 'should return a 201 response code for POST by default' do
|
270
280
|
subject.post('example') do
|
271
281
|
"Created"
|
272
282
|
end
|
273
|
-
|
283
|
+
|
274
284
|
post '/example'
|
275
285
|
last_response.status.should eql 201
|
276
286
|
last_response.body.should eql 'Created'
|
277
287
|
end
|
278
288
|
end
|
279
289
|
|
290
|
+
describe 'filters' do
|
291
|
+
it 'should add a before filter' do
|
292
|
+
subject.before { @foo = 'first' }
|
293
|
+
subject.before { @bar = 'second' }
|
294
|
+
subject.get '/' do
|
295
|
+
"#{@foo} #{@bar}"
|
296
|
+
end
|
297
|
+
|
298
|
+
get '/'
|
299
|
+
last_response.body.should eql 'first second'
|
300
|
+
end
|
301
|
+
|
302
|
+
it 'should add a after filter' do
|
303
|
+
m = double('after mock')
|
304
|
+
subject.after { m.do_something! }
|
305
|
+
subject.after { m.do_something! }
|
306
|
+
subject.get '/' do
|
307
|
+
@var ||= 'default'
|
308
|
+
end
|
309
|
+
|
310
|
+
m.should_receive(:do_something!).exactly(2).times
|
311
|
+
get '/'
|
312
|
+
last_response.body.should eql 'default'
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
280
316
|
context 'format' do
|
281
317
|
before do
|
282
318
|
subject.get("/foo") { "bar" }
|
@@ -311,26 +347,32 @@ describe Grape::API do
|
|
311
347
|
def initialize(app, *args)
|
312
348
|
@args = args
|
313
349
|
@app = app
|
350
|
+
@block = true if block_given?
|
314
351
|
end
|
315
352
|
|
316
353
|
def call(env)
|
317
354
|
env['phony.args'] ||= []
|
318
355
|
env['phony.args'] << @args
|
356
|
+
env['phony.block'] = true if @block
|
319
357
|
@app.call(env)
|
320
358
|
end
|
321
359
|
end
|
322
360
|
|
323
361
|
describe '.middleware' do
|
324
362
|
it 'should include middleware arguments from settings' do
|
325
|
-
|
363
|
+
settings = Grape::Util::HashStack.new
|
364
|
+
settings.stub!(:stack).and_return([{:middleware => [[PhonyMiddleware, 'abc', 123]]}])
|
365
|
+
subject.stub!(:settings).and_return(settings)
|
326
366
|
subject.middleware.should eql [[PhonyMiddleware, 'abc', 123]]
|
327
367
|
end
|
328
368
|
|
329
369
|
it 'should include all middleware from stacked settings' do
|
330
|
-
|
370
|
+
settings = Grape::Util::HashStack.new
|
371
|
+
settings.stub!(:stack).and_return [
|
331
372
|
{:middleware => [[PhonyMiddleware, 123],[PhonyMiddleware, 'abc']]},
|
332
373
|
{:middleware => [[PhonyMiddleware, 'foo']]}
|
333
374
|
]
|
375
|
+
subject.stub!(:settings).and_return(settings)
|
334
376
|
|
335
377
|
subject.middleware.should eql [
|
336
378
|
[PhonyMiddleware, 123],
|
@@ -353,7 +395,7 @@ describe Grape::API do
|
|
353
395
|
middleware.should == [[PhonyMiddleware, 123],[PhonyMiddleware, 'abc']]
|
354
396
|
end
|
355
397
|
|
356
|
-
subject.middleware.should eql [[PhonyMiddleware, 123]]
|
398
|
+
subject.middleware.should eql [[PhonyMiddleware, 123]]
|
357
399
|
end
|
358
400
|
|
359
401
|
it 'should actually call the middleware' do
|
@@ -365,6 +407,36 @@ describe Grape::API do
|
|
365
407
|
get '/'
|
366
408
|
last_response.body.should eql 'hello'
|
367
409
|
end
|
410
|
+
|
411
|
+
it 'should add a block if one is given' do
|
412
|
+
block = lambda{ }
|
413
|
+
subject.use PhonyMiddleware, &block
|
414
|
+
subject.middleware.should eql [[PhonyMiddleware, block]]
|
415
|
+
end
|
416
|
+
|
417
|
+
it 'should use a block if one is given' do
|
418
|
+
block = lambda{ }
|
419
|
+
subject.use PhonyMiddleware, &block
|
420
|
+
subject.get '/' do
|
421
|
+
env['phony.block'].inspect
|
422
|
+
end
|
423
|
+
|
424
|
+
get '/'
|
425
|
+
last_response.body.should == 'true'
|
426
|
+
end
|
427
|
+
|
428
|
+
it 'should not destroy the middleware settings on multiple runs' do
|
429
|
+
block = lambda{ }
|
430
|
+
subject.use PhonyMiddleware, &block
|
431
|
+
subject.get '/' do
|
432
|
+
env['phony.block'].inspect
|
433
|
+
end
|
434
|
+
|
435
|
+
2.times do
|
436
|
+
get '/'
|
437
|
+
last_response.body.should == 'true'
|
438
|
+
end
|
439
|
+
end
|
368
440
|
end
|
369
441
|
end
|
370
442
|
describe '.basic' do
|
@@ -375,39 +447,39 @@ describe Grape::API do
|
|
375
447
|
subject.get(:hello){ "Hello, world."}
|
376
448
|
get '/hello'
|
377
449
|
last_response.status.should eql 401
|
378
|
-
get '/hello', {}, 'HTTP_AUTHORIZATION' =>
|
450
|
+
get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow','whatever')
|
379
451
|
last_response.status.should eql 200
|
380
452
|
end
|
381
|
-
|
453
|
+
|
382
454
|
it 'should be scopable' do
|
383
455
|
subject.get(:hello){ "Hello, world."}
|
384
456
|
subject.namespace :admin do
|
385
457
|
http_basic do |u,p|
|
386
458
|
u == 'allow'
|
387
459
|
end
|
388
|
-
|
460
|
+
|
389
461
|
get(:hello){ "Hello, world." }
|
390
462
|
end
|
391
|
-
|
463
|
+
|
392
464
|
get '/hello'
|
393
465
|
last_response.status.should eql 200
|
394
466
|
get '/admin/hello'
|
395
467
|
last_response.status.should eql 401
|
396
468
|
end
|
397
|
-
|
469
|
+
|
398
470
|
it 'should be callable via .auth as well' do
|
399
471
|
subject.auth :http_basic do |u,p|
|
400
472
|
u == 'allow'
|
401
473
|
end
|
402
|
-
|
474
|
+
|
403
475
|
subject.get(:hello){ "Hello, world."}
|
404
476
|
get '/hello'
|
405
477
|
last_response.status.should eql 401
|
406
|
-
get '/hello', {}, 'HTTP_AUTHORIZATION' =>
|
478
|
+
get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow','whatever')
|
407
479
|
last_response.status.should eql 200
|
408
480
|
end
|
409
481
|
end
|
410
|
-
|
482
|
+
|
411
483
|
describe '.helpers' do
|
412
484
|
it 'should be accessible from the endpoint' do
|
413
485
|
subject.helpers do
|
@@ -415,97 +487,114 @@ describe Grape::API do
|
|
415
487
|
"Hello, world."
|
416
488
|
end
|
417
489
|
end
|
418
|
-
|
490
|
+
|
419
491
|
subject.get '/howdy' do
|
420
492
|
hello
|
421
493
|
end
|
422
|
-
|
494
|
+
|
423
495
|
get '/howdy'
|
424
496
|
last_response.body.should eql 'Hello, world.'
|
425
497
|
end
|
426
|
-
|
498
|
+
|
427
499
|
it 'should be scopable' do
|
428
500
|
subject.helpers do
|
429
501
|
def generic
|
430
502
|
'always there'
|
431
503
|
end
|
432
504
|
end
|
433
|
-
|
505
|
+
|
434
506
|
subject.namespace :admin do
|
435
507
|
helpers do
|
436
508
|
def secret
|
437
509
|
'only in admin'
|
438
510
|
end
|
439
511
|
end
|
440
|
-
|
512
|
+
|
441
513
|
get '/secret' do
|
442
514
|
[generic, secret].join ':'
|
443
515
|
end
|
444
516
|
end
|
445
|
-
|
517
|
+
|
446
518
|
subject.get '/generic' do
|
447
519
|
[generic, respond_to?(:secret)].join ':'
|
448
520
|
end
|
449
|
-
|
521
|
+
|
450
522
|
get '/generic'
|
451
523
|
last_response.body.should eql 'always there:false'
|
452
524
|
get '/admin/secret'
|
453
525
|
last_response.body.should eql 'always there:only in admin'
|
454
526
|
end
|
455
|
-
|
527
|
+
|
456
528
|
it 'should be reopenable' do
|
457
529
|
subject.helpers do
|
458
530
|
def one
|
459
531
|
1
|
460
532
|
end
|
461
533
|
end
|
462
|
-
|
534
|
+
|
463
535
|
subject.helpers do
|
464
536
|
def two
|
465
537
|
2
|
466
538
|
end
|
467
539
|
end
|
468
|
-
|
540
|
+
|
469
541
|
subject.get 'howdy' do
|
470
542
|
[one, two]
|
471
543
|
end
|
472
|
-
|
544
|
+
|
473
545
|
lambda{get '/howdy'}.should_not raise_error
|
474
546
|
end
|
547
|
+
|
548
|
+
it 'should allow for modules' do
|
549
|
+
mod = Module.new do
|
550
|
+
def hello
|
551
|
+
"Hello, world."
|
552
|
+
end
|
553
|
+
end
|
554
|
+
subject.helpers mod
|
555
|
+
|
556
|
+
subject.get '/howdy' do
|
557
|
+
hello
|
558
|
+
end
|
559
|
+
|
560
|
+
get '/howdy'
|
561
|
+
last_response.body.should eql 'Hello, world.'
|
562
|
+
end
|
475
563
|
end
|
476
|
-
|
564
|
+
|
477
565
|
describe '.scope' do
|
566
|
+
# TODO: refactor this to not be tied to versioning. How about a generic
|
567
|
+
# .setting macro?
|
478
568
|
it 'should scope the various settings' do
|
479
|
-
subject.
|
480
|
-
|
569
|
+
subject.prefix 'new'
|
570
|
+
|
481
571
|
subject.scope :legacy do
|
482
|
-
|
483
|
-
|
572
|
+
prefix 'legacy'
|
484
573
|
get '/abc' do
|
485
|
-
|
574
|
+
'abc'
|
486
575
|
end
|
487
576
|
end
|
488
|
-
|
577
|
+
|
489
578
|
subject.get '/def' do
|
490
|
-
|
579
|
+
'def'
|
491
580
|
end
|
492
581
|
|
493
|
-
get '/
|
582
|
+
get '/new/abc'
|
494
583
|
last_response.status.should eql 404
|
495
|
-
get '/
|
584
|
+
get '/legacy/abc'
|
496
585
|
last_response.status.should eql 200
|
497
|
-
get '/
|
586
|
+
get '/legacy/def'
|
498
587
|
last_response.status.should eql 404
|
499
|
-
get '/
|
588
|
+
get '/new/def'
|
500
589
|
last_response.status.should eql 200
|
501
590
|
end
|
502
591
|
end
|
503
|
-
|
592
|
+
|
504
593
|
describe ".rescue_from" do
|
505
594
|
it 'should not rescue errors when rescue_from is not set' do
|
506
595
|
subject.get '/exception' do
|
507
596
|
raise "rain!"
|
508
|
-
end
|
597
|
+
end
|
509
598
|
lambda { get '/exception' }.should raise_error
|
510
599
|
end
|
511
600
|
|
@@ -525,18 +614,18 @@ describe Grape::API do
|
|
525
614
|
|
526
615
|
get '/rescued'
|
527
616
|
last_response.status.should eql 403
|
528
|
-
|
617
|
+
|
529
618
|
lambda{ get '/unrescued' }.should raise_error
|
530
619
|
end
|
531
620
|
end
|
532
|
-
|
621
|
+
|
533
622
|
describe ".error_format" do
|
534
623
|
it 'should rescue all errors and return :txt' do
|
535
624
|
subject.rescue_from :all
|
536
625
|
subject.error_format :txt
|
537
626
|
subject.get '/exception' do
|
538
627
|
raise "rain!"
|
539
|
-
end
|
628
|
+
end
|
540
629
|
get '/exception'
|
541
630
|
last_response.body.should eql "rain!"
|
542
631
|
end
|
@@ -546,7 +635,7 @@ describe Grape::API do
|
|
546
635
|
subject.error_format :txt
|
547
636
|
subject.get '/exception' do
|
548
637
|
raise "rain!"
|
549
|
-
end
|
638
|
+
end
|
550
639
|
get '/exception'
|
551
640
|
last_response.body.start_with?("rain!\r\n").should be_true
|
552
641
|
end
|
@@ -556,7 +645,7 @@ describe Grape::API do
|
|
556
645
|
subject.error_format :json
|
557
646
|
subject.get '/exception' do
|
558
647
|
raise "rain!"
|
559
|
-
end
|
648
|
+
end
|
560
649
|
get '/exception'
|
561
650
|
last_response.body.should eql '{"error":"rain!"}'
|
562
651
|
end
|
@@ -565,9 +654,9 @@ describe Grape::API do
|
|
565
654
|
subject.error_format :json
|
566
655
|
subject.get '/exception' do
|
567
656
|
raise "rain!"
|
568
|
-
end
|
657
|
+
end
|
569
658
|
get '/exception'
|
570
|
-
json =
|
659
|
+
json = MultiJson.decode(last_response.body)
|
571
660
|
json["error"].should eql 'rain!'
|
572
661
|
json["backtrace"].length.should > 0
|
573
662
|
end
|
@@ -575,7 +664,7 @@ describe Grape::API do
|
|
575
664
|
subject.error_format :txt
|
576
665
|
subject.get '/error' do
|
577
666
|
error!("Access Denied", 401)
|
578
|
-
end
|
667
|
+
end
|
579
668
|
get '/error'
|
580
669
|
last_response.body.should eql "Access Denied"
|
581
670
|
end
|
@@ -583,19 +672,30 @@ describe Grape::API do
|
|
583
672
|
subject.error_format :json
|
584
673
|
subject.get '/error' do
|
585
674
|
error!("Access Denied", 401)
|
586
|
-
end
|
675
|
+
end
|
587
676
|
get '/error'
|
588
677
|
last_response.body.should eql '{"error":"Access Denied"}'
|
589
678
|
end
|
590
679
|
end
|
591
|
-
|
680
|
+
|
681
|
+
describe ".content_type" do
|
682
|
+
it "sets additional content-type" do
|
683
|
+
subject.content_type :xls, "application/vnd.ms-excel"
|
684
|
+
subject.get(:hello) do
|
685
|
+
"some binary content"
|
686
|
+
end
|
687
|
+
get '/hello.xls'
|
688
|
+
last_response.content_type.should == "application/vnd.ms-excel"
|
689
|
+
end
|
690
|
+
end
|
691
|
+
|
592
692
|
describe ".default_error_status" do
|
593
693
|
it 'should allow setting default_error_status' do
|
594
694
|
subject.rescue_from :all
|
595
695
|
subject.default_error_status 200
|
596
696
|
subject.get '/exception' do
|
597
697
|
raise "rain!"
|
598
|
-
end
|
698
|
+
end
|
599
699
|
get '/exception'
|
600
700
|
last_response.status.should eql 200
|
601
701
|
end
|
@@ -603,9 +703,398 @@ describe Grape::API do
|
|
603
703
|
subject.rescue_from :all
|
604
704
|
subject.get '/exception' do
|
605
705
|
raise "rain!"
|
606
|
-
end
|
706
|
+
end
|
607
707
|
get '/exception'
|
608
708
|
last_response.status.should eql 403
|
609
709
|
end
|
610
710
|
end
|
711
|
+
|
712
|
+
context "routes" do
|
713
|
+
describe "empty api structure" do
|
714
|
+
it "returns an empty array of routes" do
|
715
|
+
subject.routes.should == []
|
716
|
+
end
|
717
|
+
end
|
718
|
+
describe "single method api structure" do
|
719
|
+
before(:each) do
|
720
|
+
subject.get :ping do
|
721
|
+
'pong'
|
722
|
+
end
|
723
|
+
end
|
724
|
+
it "returns one route" do
|
725
|
+
subject.routes.size.should == 1
|
726
|
+
route = subject.routes[0]
|
727
|
+
route.route_version.should be_nil
|
728
|
+
route.route_path.should == "/ping(.:format)"
|
729
|
+
route.route_method.should == "GET"
|
730
|
+
end
|
731
|
+
end
|
732
|
+
describe "api structure with two versions and a namespace" do
|
733
|
+
class TwitterAPI < Grape::API
|
734
|
+
# version v1
|
735
|
+
version 'v1', :using => :path
|
736
|
+
get "version" do
|
737
|
+
api.version
|
738
|
+
end
|
739
|
+
# version v2
|
740
|
+
version 'v2', :using => :path
|
741
|
+
prefix 'p'
|
742
|
+
namespace "n1" do
|
743
|
+
namespace "n2" do
|
744
|
+
get "version" do
|
745
|
+
api.version
|
746
|
+
end
|
747
|
+
end
|
748
|
+
end
|
749
|
+
end
|
750
|
+
it "should return versions" do
|
751
|
+
TwitterAPI::versions.should == [ 'v1', 'v2' ]
|
752
|
+
end
|
753
|
+
it "should set route paths" do
|
754
|
+
TwitterAPI::routes.size.should >= 2
|
755
|
+
TwitterAPI::routes[0].route_path.should == "/:version/version(.:format)"
|
756
|
+
TwitterAPI::routes[1].route_path.should == "/p/:version/n1/n2/version(.:format)"
|
757
|
+
end
|
758
|
+
it "should set route versions" do
|
759
|
+
TwitterAPI::routes[0].route_version.should == 'v1'
|
760
|
+
TwitterAPI::routes[1].route_version.should == 'v2'
|
761
|
+
end
|
762
|
+
it "should set a nested namespace" do
|
763
|
+
TwitterAPI::routes[1].route_namespace.should == "/n1/n2"
|
764
|
+
end
|
765
|
+
it "should set prefix" do
|
766
|
+
TwitterAPI::routes[1].route_prefix.should == 'p'
|
767
|
+
end
|
768
|
+
end
|
769
|
+
describe "api structure with additional parameters" do
|
770
|
+
before(:each) do
|
771
|
+
subject.get 'split/:string', { :params => { "token" => "a token" }, :optional_params => { "limit" => "the limit" } } do
|
772
|
+
params[:string].split(params[:token], (params[:limit] || 0).to_i)
|
773
|
+
end
|
774
|
+
end
|
775
|
+
it "should split a string" do
|
776
|
+
get "/split/a,b,c.json", :token => ','
|
777
|
+
last_response.body.should == '["a","b","c"]'
|
778
|
+
end
|
779
|
+
it "should split a string with limit" do
|
780
|
+
get "/split/a,b,c.json", :token => ',', :limit => '2'
|
781
|
+
last_response.body.should == '["a","b,c"]'
|
782
|
+
end
|
783
|
+
it "should set route_params" do
|
784
|
+
subject.routes.size.should == 1
|
785
|
+
subject.routes[0].route_params.should == { "string" => "", "token" => "a token" }
|
786
|
+
subject.routes[0].route_optional_params.should == { "limit" => "the limit" }
|
787
|
+
end
|
788
|
+
end
|
789
|
+
end
|
790
|
+
|
791
|
+
context "desc" do
|
792
|
+
describe "empty api structure" do
|
793
|
+
it "returns an empty array of routes" do
|
794
|
+
subject.desc "grape api"
|
795
|
+
subject.routes.should == []
|
796
|
+
end
|
797
|
+
end
|
798
|
+
describe "single method with a desc" do
|
799
|
+
before(:each) do
|
800
|
+
subject.desc "ping method"
|
801
|
+
subject.get :ping do
|
802
|
+
'pong'
|
803
|
+
end
|
804
|
+
end
|
805
|
+
it "returns route description" do
|
806
|
+
subject.routes[0].route_description.should == "ping method"
|
807
|
+
end
|
808
|
+
end
|
809
|
+
describe "single method with a an array of params and a desc hash block" do
|
810
|
+
before(:each) do
|
811
|
+
subject.desc "ping method", { :params => { "x" => "y" } }
|
812
|
+
subject.get "ping/:x" do
|
813
|
+
'pong'
|
814
|
+
end
|
815
|
+
end
|
816
|
+
it "returns route description" do
|
817
|
+
subject.routes[0].route_description.should == "ping method"
|
818
|
+
end
|
819
|
+
end
|
820
|
+
describe "api structure with multiple methods and descriptions" do
|
821
|
+
before(:each) do
|
822
|
+
class JitterAPI < Grape::API
|
823
|
+
desc "first method"
|
824
|
+
get "first" do; end
|
825
|
+
get "second" do; end
|
826
|
+
desc "third method"
|
827
|
+
get "third" do; end
|
828
|
+
end
|
829
|
+
end
|
830
|
+
it "should return a description for the first method" do
|
831
|
+
JitterAPI::routes[0].route_description.should == "first method"
|
832
|
+
JitterAPI::routes[1].route_description.should be_nil
|
833
|
+
JitterAPI::routes[2].route_description.should == "third method"
|
834
|
+
end
|
835
|
+
end
|
836
|
+
describe "api structure with multiple methods, namespaces, descriptions and options" do
|
837
|
+
before(:each) do
|
838
|
+
class LitterAPI < Grape::API
|
839
|
+
desc "first method"
|
840
|
+
get "first" do; end
|
841
|
+
get "second" do; end
|
842
|
+
namespace "ns" do
|
843
|
+
desc "ns second", :foo => "bar"
|
844
|
+
get "second" do; end
|
845
|
+
end
|
846
|
+
desc "third method", :details => "details of third method"
|
847
|
+
get "third" do; end
|
848
|
+
desc "Reverses a string.", { :params =>
|
849
|
+
{ "s" => { :desc => "string to reverse", :type => "string" }}
|
850
|
+
}
|
851
|
+
get "reverse" do
|
852
|
+
params[:s].reverse
|
853
|
+
end
|
854
|
+
end
|
855
|
+
end
|
856
|
+
it "should return a description for the first method" do
|
857
|
+
LitterAPI::routes[0].route_description.should == "first method"
|
858
|
+
LitterAPI::routes[1].route_description.should be_nil
|
859
|
+
LitterAPI::routes[2].route_description.should == "ns second"
|
860
|
+
LitterAPI::routes[2].route_foo.should == "bar"
|
861
|
+
LitterAPI::routes[3].route_description.should == "third method"
|
862
|
+
LitterAPI::routes[4].route_description.should == "Reverses a string."
|
863
|
+
LitterAPI::routes[4].route_params.should == { "s" => { :desc => "string to reverse", :type => "string" }}
|
864
|
+
end
|
865
|
+
end
|
866
|
+
end
|
867
|
+
|
868
|
+
describe ".rescue_from klass, block" do
|
869
|
+
it 'should rescue Exception' do
|
870
|
+
subject.rescue_from RuntimeError do |e|
|
871
|
+
rack_response("rescued from #{e.message}", 202)
|
872
|
+
end
|
873
|
+
subject.get '/exception' do
|
874
|
+
raise "rain!"
|
875
|
+
end
|
876
|
+
get '/exception'
|
877
|
+
last_response.status.should eql 202
|
878
|
+
last_response.body.should == 'rescued from rain!'
|
879
|
+
end
|
880
|
+
it 'should rescue an error via rescue_from :all' do
|
881
|
+
class ConnectionError < RuntimeError; end
|
882
|
+
subject.rescue_from :all do |e|
|
883
|
+
rack_response("rescued from #{e.class.name}", 500)
|
884
|
+
end
|
885
|
+
subject.get '/exception' do
|
886
|
+
raise ConnectionError
|
887
|
+
end
|
888
|
+
get '/exception'
|
889
|
+
last_response.status.should eql 500
|
890
|
+
last_response.body.should == 'rescued from ConnectionError'
|
891
|
+
end
|
892
|
+
it 'should rescue a specific error' do
|
893
|
+
class ConnectionError < RuntimeError; end
|
894
|
+
subject.rescue_from ConnectionError do |e|
|
895
|
+
rack_response("rescued from #{e.class.name}", 500)
|
896
|
+
end
|
897
|
+
subject.get '/exception' do
|
898
|
+
raise ConnectionError
|
899
|
+
end
|
900
|
+
get '/exception'
|
901
|
+
last_response.status.should eql 500
|
902
|
+
last_response.body.should == 'rescued from ConnectionError'
|
903
|
+
end
|
904
|
+
it 'should rescue multiple specific errors' do
|
905
|
+
class ConnectionError < RuntimeError; end
|
906
|
+
class DatabaseError < RuntimeError; end
|
907
|
+
subject.rescue_from ConnectionError do |e|
|
908
|
+
rack_response("rescued from #{e.class.name}", 500)
|
909
|
+
end
|
910
|
+
subject.rescue_from DatabaseError do |e|
|
911
|
+
rack_response("rescued from #{e.class.name}", 500)
|
912
|
+
end
|
913
|
+
subject.get '/connection' do
|
914
|
+
raise ConnectionError
|
915
|
+
end
|
916
|
+
subject.get '/database' do
|
917
|
+
raise DatabaseError
|
918
|
+
end
|
919
|
+
get '/connection'
|
920
|
+
last_response.status.should eql 500
|
921
|
+
last_response.body.should == 'rescued from ConnectionError'
|
922
|
+
get '/database'
|
923
|
+
last_response.status.should eql 500
|
924
|
+
last_response.body.should == 'rescued from DatabaseError'
|
925
|
+
end
|
926
|
+
it 'should not rescue a different error' do
|
927
|
+
class CommunicationError < RuntimeError; end
|
928
|
+
subject.rescue_from RuntimeError do |e|
|
929
|
+
rack_response("rescued from #{e.class.name}", 500)
|
930
|
+
end
|
931
|
+
subject.get '/uncaught' do
|
932
|
+
raise CommunicationError
|
933
|
+
end
|
934
|
+
lambda { get '/uncaught' }.should raise_error(CommunicationError)
|
935
|
+
end
|
936
|
+
end
|
937
|
+
|
938
|
+
describe '.mount' do
|
939
|
+
let(:mounted_app){ lambda{|env| [200, {}, ["MOUNTED"]]} }
|
940
|
+
|
941
|
+
context 'with a bare rack app' do
|
942
|
+
before do
|
943
|
+
subject.mount mounted_app => '/mounty'
|
944
|
+
end
|
945
|
+
|
946
|
+
it 'should make a bare Rack app available at the endpoint' do
|
947
|
+
get '/mounty'
|
948
|
+
last_response.body.should == 'MOUNTED'
|
949
|
+
end
|
950
|
+
|
951
|
+
it 'should anchor the routes, passing all subroutes to it' do
|
952
|
+
get '/mounty/awesome'
|
953
|
+
last_response.body.should == 'MOUNTED'
|
954
|
+
end
|
955
|
+
|
956
|
+
it 'should be able to cascade' do
|
957
|
+
subject.mount lambda{ |env|
|
958
|
+
headers = {}
|
959
|
+
headers['X-Cascade'] == 'pass' unless env['PATH_INFO'].include?('boo')
|
960
|
+
[200, headers, ["Farfegnugen"]]
|
961
|
+
} => '/'
|
962
|
+
|
963
|
+
get '/boo'
|
964
|
+
last_response.body.should == 'Farfegnugen'
|
965
|
+
get '/mounty'
|
966
|
+
last_response.body.should == 'MOUNTED'
|
967
|
+
end
|
968
|
+
end
|
969
|
+
|
970
|
+
context 'without a hash' do
|
971
|
+
it 'should call through setting the route to "/"' do
|
972
|
+
subject.mount mounted_app
|
973
|
+
get '/'
|
974
|
+
last_response.body.should == 'MOUNTED'
|
975
|
+
end
|
976
|
+
end
|
977
|
+
|
978
|
+
context 'mounting an API' do
|
979
|
+
it 'should apply the settings of the mounting api' do
|
980
|
+
subject.version 'v1', :using => :path
|
981
|
+
|
982
|
+
subject.namespace :cool do
|
983
|
+
app = Class.new(Grape::API)
|
984
|
+
app.get('/awesome') do
|
985
|
+
"yo"
|
986
|
+
end
|
987
|
+
|
988
|
+
mount app
|
989
|
+
end
|
990
|
+
get '/v1/cool/awesome'
|
991
|
+
last_response.body.should == 'yo'
|
992
|
+
end
|
993
|
+
end
|
994
|
+
end
|
995
|
+
|
996
|
+
describe '.endpoints' do
|
997
|
+
it 'should add one for each route created' do
|
998
|
+
subject.get '/'
|
999
|
+
subject.post '/'
|
1000
|
+
subject.endpoints.size.should == 2
|
1001
|
+
end
|
1002
|
+
end
|
1003
|
+
|
1004
|
+
describe '.compile' do
|
1005
|
+
it 'should set the instance' do
|
1006
|
+
subject.instance.should be_nil
|
1007
|
+
subject.compile
|
1008
|
+
subject.instance.should be_kind_of(subject)
|
1009
|
+
end
|
1010
|
+
end
|
1011
|
+
|
1012
|
+
describe '.change!' do
|
1013
|
+
it 'should invalidate any compiled instance' do
|
1014
|
+
subject.compile
|
1015
|
+
subject.change!
|
1016
|
+
subject.instance.should be_nil
|
1017
|
+
end
|
1018
|
+
end
|
1019
|
+
|
1020
|
+
describe ".route" do
|
1021
|
+
context "plain" do
|
1022
|
+
before(:each) do
|
1023
|
+
subject.get '/' do
|
1024
|
+
route.route_path
|
1025
|
+
end
|
1026
|
+
subject.get '/path' do
|
1027
|
+
route.route_path
|
1028
|
+
end
|
1029
|
+
end
|
1030
|
+
it 'should provide access to route info' do
|
1031
|
+
get '/'
|
1032
|
+
last_response.body.should == "/(.:format)"
|
1033
|
+
get '/path'
|
1034
|
+
last_response.body.should == "/path(.:format)"
|
1035
|
+
end
|
1036
|
+
end
|
1037
|
+
context "with desc" do
|
1038
|
+
before(:each) do
|
1039
|
+
subject.desc 'returns description'
|
1040
|
+
subject.get '/description' do
|
1041
|
+
route.route_description
|
1042
|
+
end
|
1043
|
+
subject.desc 'returns parameters', { :params => { "x" => "y" }}
|
1044
|
+
subject.get '/params/:id' do
|
1045
|
+
route.route_params[params[:id]]
|
1046
|
+
end
|
1047
|
+
end
|
1048
|
+
it 'should return route description' do
|
1049
|
+
get '/description'
|
1050
|
+
last_response.body.should == "returns description"
|
1051
|
+
end
|
1052
|
+
it 'should return route parameters' do
|
1053
|
+
get '/params/x'
|
1054
|
+
last_response.body.should == "y"
|
1055
|
+
end
|
1056
|
+
end
|
1057
|
+
end
|
1058
|
+
context "format" do
|
1059
|
+
context ":txt" do
|
1060
|
+
before(:each) do
|
1061
|
+
subject.format :txt
|
1062
|
+
subject.get '/meaning_of_life' do
|
1063
|
+
{ :meaning_of_life => 42 }
|
1064
|
+
end
|
1065
|
+
end
|
1066
|
+
it "should force txt without an extension" do
|
1067
|
+
get '/meaning_of_life'
|
1068
|
+
last_response.body.should == { :meaning_of_life => 42 }.to_s
|
1069
|
+
end
|
1070
|
+
it "should not force txt with an extension" do
|
1071
|
+
get '/meaning_of_life.json'
|
1072
|
+
last_response.body.should == { :meaning_of_life => 42 }.to_json
|
1073
|
+
end
|
1074
|
+
it "should force txt from a non-accepting header" do
|
1075
|
+
get '/meaning_of_life', {}, { 'HTTP_ACCEPT' => 'application/json' }
|
1076
|
+
last_response.body.should == { :meaning_of_life => 42 }.to_s
|
1077
|
+
end
|
1078
|
+
end
|
1079
|
+
context ":json" do
|
1080
|
+
before(:each) do
|
1081
|
+
subject.format :json
|
1082
|
+
subject.get '/meaning_of_life' do
|
1083
|
+
{ :meaning_of_life => 42 }
|
1084
|
+
end
|
1085
|
+
end
|
1086
|
+
it "should force json without an extension" do
|
1087
|
+
get '/meaning_of_life'
|
1088
|
+
last_response.body.should == { :meaning_of_life => 42 }.to_json
|
1089
|
+
end
|
1090
|
+
it "should not force json with an extension" do
|
1091
|
+
get '/meaning_of_life.txt'
|
1092
|
+
last_response.body.should == { :meaning_of_life => 42 }.to_s
|
1093
|
+
end
|
1094
|
+
it "should force json from a non-accepting header" do
|
1095
|
+
get '/meaning_of_life', {}, { 'HTTP_ACCEPT' => 'text/html' }
|
1096
|
+
last_response.body.should == { :meaning_of_life => 42 }.to_json
|
1097
|
+
end
|
1098
|
+
end
|
1099
|
+
end
|
611
1100
|
end
|