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.

@@ -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
- err = catch :error do
8
+ result = catch :error do
9
9
  @app.call(@env)
10
10
  end
11
11
 
12
- error_response(err)
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 'active_support/json'
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 => :json,
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
- # TODO: Implement Accept header parsing.
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
- bodies.map! do |body|
53
- ActiveSupport::JSON.encode(body)
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
- [status, headers, bodies]
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 'grape/middleware/base'
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.insert(0, '/') unless prefix.index('/') == 0
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'].gsub!(prefix, '')
15
- env['PATH_INFO'].insert(0, '/') unless env['PATH_INFO'].index('/') == 0
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