grape 0.0.0.alpha.2 → 0.1.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.
Potentially problematic release.
This version of grape might be problematic. Click here for more details.
- data/.rspec +1 -1
- data/Gemfile +5 -3
- data/Gemfile.lock +17 -13
- data/README.rdoc +10 -1
- data/VERSION +1 -1
- data/grape.gemspec +28 -10
- data/lib/grape.rb +19 -7
- data/lib/grape/api.rb +174 -0
- data/lib/grape/endpoint.rb +65 -0
- data/lib/grape/middleware/auth/basic.rb +30 -0
- data/lib/grape/middleware/base.rb +7 -0
- data/lib/grape/middleware/error.rb +3 -2
- data/lib/grape/middleware/formatter.rb +55 -8
- data/lib/grape/middleware/prefixer.rb +5 -4
- data/lib/grape/middleware/versioner.rb +4 -0
- data/lib/grape/middleware_stack.rb +35 -0
- data/spec/grape/api_spec.rb +283 -0
- data/spec/grape/endpoint_spec.rb +75 -0
- data/spec/grape/middleware/auth/basic_spec.rb +31 -0
- data/spec/grape/middleware/formatter_spec.rb +54 -2
- data/spec/grape/middleware/prefixer_spec.rb +5 -0
- data/spec/grape/middleware/versioner_spec.rb +11 -0
- data/spec/grape/middleware_stack_spec.rb +47 -0
- data/spec/spec_helper.rb +5 -0
- metadata +62 -26
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rack/auth/basic'
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
module Middleware
|
5
|
+
module Auth
|
6
|
+
class Basic < Grape::Middleware::Base
|
7
|
+
attr_reader :authenticator
|
8
|
+
|
9
|
+
def initialize(app, options = {}, &authenticator)
|
10
|
+
super(app, options)
|
11
|
+
@authenticator = authenticator
|
12
|
+
end
|
13
|
+
|
14
|
+
def basic_request
|
15
|
+
Rack::Auth::Basic::Request.new(env)
|
16
|
+
end
|
17
|
+
|
18
|
+
def credentials
|
19
|
+
basic_request.provided?? basic_request.credentials : [nil, nil]
|
20
|
+
end
|
21
|
+
|
22
|
+
def before
|
23
|
+
unless authenticator.call(*credentials)
|
24
|
+
throw :error, :status => 401, :message => "API Authorization Failed."
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -3,6 +3,8 @@ module Grape
|
|
3
3
|
class Base
|
4
4
|
attr_reader :app, :env, :options
|
5
5
|
|
6
|
+
# @param [Rack Application] app The standard argument for a Rack middleware.
|
7
|
+
# @param [Hash] options A hash of options, simply stored for use by subclasses.
|
6
8
|
def initialize(app, options = {})
|
7
9
|
@app = app
|
8
10
|
@options = default_options.merge(options)
|
@@ -21,7 +23,12 @@ module Grape
|
|
21
23
|
after || @app_response
|
22
24
|
end
|
23
25
|
|
26
|
+
# @abstract
|
27
|
+
# Called before the application is called in the middleware lifecycle.
|
24
28
|
def before; end
|
29
|
+
# @abstract
|
30
|
+
# Called after the application is called in the middleware lifecycle.
|
31
|
+
# @returns [Response, nil] a Rack SPEC response or nil to call the application afterwards.
|
25
32
|
def after; end
|
26
33
|
|
27
34
|
def request
|
@@ -5,11 +5,12 @@ module Grape
|
|
5
5
|
class Error < Base
|
6
6
|
def call!(env)
|
7
7
|
@env = env
|
8
|
-
|
8
|
+
result = catch :error do
|
9
9
|
@app.call(@env)
|
10
10
|
end
|
11
11
|
|
12
|
-
|
12
|
+
result ||= {}
|
13
|
+
result.is_a?(Hash) ? error_response(result) : result
|
13
14
|
end
|
14
15
|
|
15
16
|
def error_response(error = {})
|
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'grape/middleware/base'
|
2
|
-
require '
|
2
|
+
require 'multi_json'
|
3
3
|
|
4
4
|
module Grape
|
5
5
|
module Middleware
|
@@ -8,12 +8,13 @@ module Grape
|
|
8
8
|
:xml => 'application/xml',
|
9
9
|
:json => 'application/json',
|
10
10
|
:atom => 'application/atom+xml',
|
11
|
-
:rss => 'application/rss+xml'
|
11
|
+
:rss => 'application/rss+xml',
|
12
|
+
:txt => 'text/plain'
|
12
13
|
}
|
13
14
|
|
14
15
|
def default_options
|
15
16
|
{
|
16
|
-
:default_format => :
|
17
|
+
:default_format => :txt,
|
17
18
|
:content_types => {}
|
18
19
|
}
|
19
20
|
end
|
@@ -22,9 +23,17 @@ module Grape
|
|
22
23
|
CONTENT_TYPES.merge(options[:content_types])
|
23
24
|
end
|
24
25
|
|
26
|
+
def mime_types
|
27
|
+
content_types.invert
|
28
|
+
end
|
29
|
+
|
30
|
+
def headers
|
31
|
+
env.dup.inject({}){|h,(k,v)| h[k.downcase] = v; h}
|
32
|
+
end
|
33
|
+
|
25
34
|
def before
|
26
35
|
fmt = format_from_extension || format_from_header || options[:default_format]
|
27
|
-
|
36
|
+
|
28
37
|
if content_types.key?(fmt)
|
29
38
|
env['api.format'] = fmt
|
30
39
|
else
|
@@ -44,15 +53,53 @@ module Grape
|
|
44
53
|
end
|
45
54
|
|
46
55
|
def format_from_header
|
47
|
-
|
56
|
+
mime_array.each do |t|
|
57
|
+
if mime_types.key?(t)
|
58
|
+
return mime_types[t]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
|
64
|
+
def mime_array
|
65
|
+
accept = headers['accept']
|
66
|
+
if accept
|
67
|
+
accept.gsub(/\b/,'').
|
68
|
+
scan(/(\w+\/[\w+]+)(?:;[^,]*q=([0-9.]+)[^,]*)?/i).
|
69
|
+
sort_by{|a| -a[1].to_f}.
|
70
|
+
map{|a| a[0]}
|
71
|
+
else
|
72
|
+
[]
|
73
|
+
end
|
48
74
|
end
|
49
75
|
|
50
76
|
def after
|
51
77
|
status, headers, bodies = *@app_response
|
52
|
-
|
53
|
-
|
78
|
+
bodymap = []
|
79
|
+
bodies.each do |body|
|
80
|
+
bodymap << case env['api.format']
|
81
|
+
when :json
|
82
|
+
encode_json(body)
|
83
|
+
when :txt
|
84
|
+
encode_txt(body)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
headers['Content-Type'] = 'application/json'
|
88
|
+
Rack::Response.new(bodymap, status, headers).to_a
|
89
|
+
end
|
90
|
+
|
91
|
+
def encode_json(object)
|
92
|
+
if object.respond_to? :serializable_hash
|
93
|
+
MultiJson.encode(object.serializable_hash)
|
94
|
+
elsif object.respond_to? :to_json
|
95
|
+
object.to_json
|
96
|
+
else
|
97
|
+
MultiJson.encode(object)
|
54
98
|
end
|
55
|
-
|
99
|
+
end
|
100
|
+
|
101
|
+
def encode_txt(object)
|
102
|
+
body.respond_to?(:to_txt) ? body.to_txt : body.to_s
|
56
103
|
end
|
57
104
|
end
|
58
105
|
end
|
@@ -1,18 +1,19 @@
|
|
1
|
-
require '
|
1
|
+
require 'rack/mount/utils'
|
2
|
+
require 'grape'
|
2
3
|
|
3
4
|
module Grape
|
4
5
|
module Middleware
|
5
6
|
class Prefixer < Base
|
6
7
|
def prefix
|
7
8
|
prefix = options[:prefix] || ""
|
8
|
-
prefix
|
9
|
+
prefix = Rack::Mount::Utils.normalize_path(prefix)
|
9
10
|
prefix
|
10
11
|
end
|
11
12
|
|
12
13
|
def before
|
13
14
|
if env['PATH_INFO'].index(prefix) == 0
|
14
|
-
env['PATH_INFO'].
|
15
|
-
env['PATH_INFO'].
|
15
|
+
env['PATH_INFO'].sub!(prefix, '')
|
16
|
+
env['PATH_INFO'] = Rack::Mount::Utils.normalize_path(env['PATH_INFO'])
|
16
17
|
end
|
17
18
|
end
|
18
19
|
end
|
@@ -13,6 +13,10 @@ module Grape
|
|
13
13
|
pieces = env['PATH_INFO'].split('/')
|
14
14
|
potential_version = pieces[1]
|
15
15
|
if potential_version =~ options[:pattern]
|
16
|
+
if options[:versions] && !options[:versions].include?(potential_version)
|
17
|
+
throw :error, :status => 404, :message => "404 API Version Not Found"
|
18
|
+
end
|
19
|
+
|
16
20
|
truncated_path = "/#{pieces[2..-1].join('/')}"
|
17
21
|
env['api.version'] = potential_version
|
18
22
|
env['PATH_INFO'] = truncated_path
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'grape'
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
class MiddlewareStack
|
5
|
+
attr_reader :stack
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@stack = []
|
9
|
+
end
|
10
|
+
|
11
|
+
# Add a new middleware to the stack. Syntax
|
12
|
+
# is identical to normal middleware <tt>#use</tt>
|
13
|
+
# functionality.
|
14
|
+
#
|
15
|
+
# @param [Class] klass The middleware class.
|
16
|
+
def use(klass, *args)
|
17
|
+
if index = @stack.index(@stack.find{|a| a.first == klass})
|
18
|
+
@stack[index] = [klass, *args]
|
19
|
+
else
|
20
|
+
@stack << [klass, *args]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Apply this middleware stack to a
|
25
|
+
# Rack application.
|
26
|
+
def to_app(app)
|
27
|
+
b = Rack::Builder.new
|
28
|
+
for middleware in stack
|
29
|
+
b.use *middleware
|
30
|
+
end
|
31
|
+
b.run app
|
32
|
+
b.to_app
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,283 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Grape::API do
|
4
|
+
subject { Class.new(Grape::API) }
|
5
|
+
before { subject.default_format :txt }
|
6
|
+
|
7
|
+
def app; subject end
|
8
|
+
|
9
|
+
describe '.prefix' do
|
10
|
+
it 'should route through with the prefix' do
|
11
|
+
subject.prefix 'awesome/sauce'
|
12
|
+
subject.get :hello do
|
13
|
+
"Hello there."
|
14
|
+
end
|
15
|
+
|
16
|
+
get 'awesome/sauce/hello'
|
17
|
+
last_response.body.should == "Hello there."
|
18
|
+
|
19
|
+
get '/hello'
|
20
|
+
last_response.status.should == 404
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '.version' do
|
25
|
+
it 'should set the API version' do
|
26
|
+
subject.version 'v1'
|
27
|
+
subject.get :hello do
|
28
|
+
"Version: #{request.env['api.version']}"
|
29
|
+
end
|
30
|
+
|
31
|
+
get '/v1/hello'
|
32
|
+
last_response.body.should == "Version: v1"
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should add the prefix before the API version' do
|
36
|
+
subject.prefix 'api'
|
37
|
+
subject.version 'v1'
|
38
|
+
subject.get :hello do
|
39
|
+
"Version: #{request.env['api.version']}"
|
40
|
+
end
|
41
|
+
|
42
|
+
get '/api/v1/hello'
|
43
|
+
last_response.body.should == "Version: v1"
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should be able to specify version as a nesting' do
|
47
|
+
subject.version 'v2'
|
48
|
+
subject.get '/awesome' do
|
49
|
+
"Radical"
|
50
|
+
end
|
51
|
+
|
52
|
+
subject.version 'v1' do
|
53
|
+
get '/legacy' do
|
54
|
+
"Totally"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
get '/v1/awesome'
|
59
|
+
last_response.status.should == 404
|
60
|
+
get '/v2/awesome'
|
61
|
+
last_response.status.should == 200
|
62
|
+
get '/v1/legacy'
|
63
|
+
last_response.status.should == 200
|
64
|
+
get '/v2/legacy'
|
65
|
+
last_response.status.should == 404
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should be able to specify multiple versions' do
|
69
|
+
subject.version 'v1', 'v2'
|
70
|
+
subject.get 'awesome' do
|
71
|
+
"I exist"
|
72
|
+
end
|
73
|
+
|
74
|
+
get '/v1/awesome'
|
75
|
+
last_response.status.should == 200
|
76
|
+
get '/v2/awesome'
|
77
|
+
last_response.status.should == 200
|
78
|
+
get '/v3/awesome'
|
79
|
+
last_response.status.should == 404
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe '.namespace' do
|
84
|
+
it 'should be retrievable and converted to a path' do
|
85
|
+
subject.namespace :awesome do
|
86
|
+
namespace.should == '/awesome'
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'should come after the prefix and version' do
|
91
|
+
subject.prefix :rad
|
92
|
+
subject.version :v1
|
93
|
+
|
94
|
+
subject.namespace :awesome do
|
95
|
+
compile_path('hello').should == '/rad/:version/awesome/hello'
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'should cancel itself after the block is over' do
|
100
|
+
subject.namespace :awesome do
|
101
|
+
namespace.should == '/awesome'
|
102
|
+
end
|
103
|
+
|
104
|
+
subject.namespace.should == '/'
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'should be stackable' do
|
108
|
+
subject.namespace :awesome do
|
109
|
+
namespace :rad do
|
110
|
+
namespace.should == '/awesome/rad'
|
111
|
+
end
|
112
|
+
namespace.should == '/awesome'
|
113
|
+
end
|
114
|
+
subject.namespace.should == '/'
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'should be callable with nil just to push onto the stack' do
|
118
|
+
subject.namespace do
|
119
|
+
version 'v2'
|
120
|
+
compile_path('hello').should == '/:version/hello'
|
121
|
+
end
|
122
|
+
subject.compile_path('hello').should == '/hello'
|
123
|
+
end
|
124
|
+
|
125
|
+
%w(group resource resources).each do |als|
|
126
|
+
it "`.#{als}` should be an alias" do
|
127
|
+
subject.send(als, :awesome) do
|
128
|
+
namespace.should == "/awesome"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe '.route' do
|
135
|
+
it 'should allow for no path' do
|
136
|
+
subject.namespace :votes do
|
137
|
+
get do
|
138
|
+
"Votes"
|
139
|
+
end
|
140
|
+
|
141
|
+
post do
|
142
|
+
"Created a Vote"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
get '/votes'
|
147
|
+
last_response.body.should == 'Votes'
|
148
|
+
post '/votes'
|
149
|
+
last_response.body.should == 'Created a Vote'
|
150
|
+
end
|
151
|
+
|
152
|
+
verbs = %w(post get head delete put)
|
153
|
+
verbs.each do |verb|
|
154
|
+
it "should allow and properly constrain a #{verb.upcase} method" do
|
155
|
+
subject.send(verb, '/example') do
|
156
|
+
verb
|
157
|
+
end
|
158
|
+
send(verb, '/example')
|
159
|
+
last_response.body.should == verb
|
160
|
+
# Call it with a method other than the properly constrained one.
|
161
|
+
send(verbs[(verbs.index(verb) + 1) % verbs.size], '/example')
|
162
|
+
last_response.status.should == 404
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'should return a 201 response code for POST by default' do
|
167
|
+
subject.post('example') do
|
168
|
+
"Created"
|
169
|
+
end
|
170
|
+
|
171
|
+
post '/example'
|
172
|
+
last_response.status.should == 201
|
173
|
+
last_response.body.should == 'Created'
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
describe '.basic' do
|
178
|
+
it 'should protect any resources on the same scope' do
|
179
|
+
subject.http_basic do |u,p|
|
180
|
+
u == 'allow'
|
181
|
+
end
|
182
|
+
subject.get(:hello){ "Hello, world."}
|
183
|
+
get '/hello'
|
184
|
+
last_response.status.should == 401
|
185
|
+
get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic('allow','whatever')
|
186
|
+
last_response.status.should == 200
|
187
|
+
end
|
188
|
+
|
189
|
+
it 'should be scopable' do
|
190
|
+
subject.get(:hello){ "Hello, world."}
|
191
|
+
subject.namespace :admin do
|
192
|
+
http_basic do |u,p|
|
193
|
+
u == 'allow'
|
194
|
+
end
|
195
|
+
|
196
|
+
get(:hello){ "Hello, world." }
|
197
|
+
end
|
198
|
+
|
199
|
+
get '/hello'
|
200
|
+
last_response.status.should == 200
|
201
|
+
get '/admin/hello'
|
202
|
+
last_response.status.should == 401
|
203
|
+
end
|
204
|
+
|
205
|
+
it 'should be callable via .auth as well' do
|
206
|
+
subject.auth :http_basic do |u,p|
|
207
|
+
u == 'allow'
|
208
|
+
end
|
209
|
+
|
210
|
+
subject.get(:hello){ "Hello, world."}
|
211
|
+
get '/hello'
|
212
|
+
last_response.status.should == 401
|
213
|
+
get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic('allow','whatever')
|
214
|
+
last_response.status.should == 200
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
describe '.helpers' do
|
219
|
+
it 'should be accessible from the endpoint' do
|
220
|
+
subject.helpers do
|
221
|
+
def hello
|
222
|
+
"Hello, world."
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
subject.get '/howdy' do
|
227
|
+
hello
|
228
|
+
end
|
229
|
+
|
230
|
+
get '/howdy'
|
231
|
+
last_response.body.should == 'Hello, world.'
|
232
|
+
end
|
233
|
+
|
234
|
+
it 'should be scopable' do
|
235
|
+
subject.helpers do
|
236
|
+
def generic
|
237
|
+
'always there'
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
subject.namespace :admin do
|
242
|
+
helpers do
|
243
|
+
def secret
|
244
|
+
'only in admin'
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
get '/secret' do
|
249
|
+
[generic, secret].join ':'
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
subject.get '/generic' do
|
254
|
+
[generic, respond_to?(:secret)].join ':'
|
255
|
+
end
|
256
|
+
|
257
|
+
get '/generic'
|
258
|
+
last_response.body.should == 'always there:false'
|
259
|
+
get '/admin/secret'
|
260
|
+
last_response.body.should == 'always there:only in admin'
|
261
|
+
end
|
262
|
+
|
263
|
+
it 'should be reopenable' do
|
264
|
+
subject.helpers do
|
265
|
+
def one
|
266
|
+
1
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
subject.helpers do
|
271
|
+
def two
|
272
|
+
2
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
subject.get 'howdy' do
|
277
|
+
[one, two]
|
278
|
+
end
|
279
|
+
|
280
|
+
lambda{get '/howdy'}.should_not raise_error
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|