grape 0.4.1 → 0.5.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/CHANGELOG.md +29 -5
- data/Gemfile +1 -0
- data/README.md +169 -80
- data/Rakefile +19 -3
- data/lib/grape.rb +6 -3
- data/lib/grape/api.rb +24 -3
- data/lib/grape/endpoint.rb +34 -20
- data/lib/grape/http/request.rb +28 -0
- data/lib/grape/middleware/base.rb +1 -1
- data/lib/grape/middleware/filter.rb +1 -1
- data/lib/grape/middleware/formatter.rb +33 -22
- data/lib/grape/middleware/versioner.rb +2 -0
- data/lib/grape/middleware/versioner/accept_version_header.rb +67 -0
- data/lib/grape/middleware/versioner/header.rb +3 -1
- data/lib/grape/util/hash_stack.rb +11 -0
- data/lib/grape/validations.rb +47 -11
- data/lib/grape/validations/default.rb +24 -0
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api_spec.rb +171 -11
- data/spec/grape/endpoint_spec.rb +41 -1
- data/spec/grape/entity_spec.rb +38 -0
- data/spec/grape/middleware/formatter_spec.rb +48 -0
- data/spec/grape/middleware/versioner/accept_version_header_spec.rb +121 -0
- data/spec/grape/middleware/versioner_spec.rb +4 -1
- data/spec/grape/validations/default_spec.rb +67 -0
- data/spec/grape/validations_spec.rb +9 -0
- data/spec/shared/versioning_examples.rb +13 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/support/versioned_helpers.rb +6 -0
- metadata +11 -4
data/Rakefile
CHANGED
@@ -28,16 +28,32 @@ begin
|
|
28
28
|
namespace :doc do
|
29
29
|
YARD::Rake::YardocTask.new(:pages) do |t|
|
30
30
|
t.files = DOC_FILES
|
31
|
-
t.options = ['-o', '../grape.doc']
|
31
|
+
t.options = ['-o', '../grape.doc/docs']
|
32
32
|
end
|
33
33
|
|
34
34
|
namespace :pages do
|
35
|
+
|
36
|
+
desc "Check out gh-pages."
|
37
|
+
task :checkout do
|
38
|
+
dir = File.dirname(__FILE__) + '/../grape.doc'
|
39
|
+
unless Dir.exist?(dir)
|
40
|
+
Dir.mkdir(dir)
|
41
|
+
Dir.chdir(dir) do
|
42
|
+
system("git init")
|
43
|
+
system("git remote add origin git@github.com:intridea/grape.git")
|
44
|
+
system("git pull")
|
45
|
+
system("git checkout gh-pages")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
35
50
|
desc 'Generate and publish YARD docs to GitHub pages.'
|
36
|
-
task :publish => ['doc:pages'] do
|
51
|
+
task :publish => ['doc:pages:checkout', 'doc:pages'] do
|
37
52
|
Dir.chdir(File.dirname(__FILE__) + '/../grape.doc') do
|
53
|
+
system("git checkout gh-pages")
|
38
54
|
system("git add .")
|
39
55
|
system("git add -u")
|
40
|
-
system("git commit -m 'Generating docs for version #{
|
56
|
+
system("git commit -m 'Generating docs for version #{Grape::VERSION}.'")
|
41
57
|
system("git push origin gh-pages")
|
42
58
|
end
|
43
59
|
end
|
data/lib/grape.rb
CHANGED
@@ -9,6 +9,7 @@ require 'hashie'
|
|
9
9
|
require 'active_support/core_ext/hash/indifferent_access'
|
10
10
|
require 'active_support/ordered_hash'
|
11
11
|
require 'active_support/core_ext/object/conversions'
|
12
|
+
require 'active_support/core_ext/array/extract_options'
|
12
13
|
require 'grape/util/deep_merge'
|
13
14
|
require 'grape/util/content_types'
|
14
15
|
require 'multi_json'
|
@@ -25,6 +26,7 @@ module Grape
|
|
25
26
|
autoload :Namespace, 'grape/namespace'
|
26
27
|
autoload :Cookies, 'grape/cookies'
|
27
28
|
autoload :Validations, 'grape/validations'
|
29
|
+
autoload :Request, 'grape/http/request'
|
28
30
|
|
29
31
|
module Exceptions
|
30
32
|
autoload :Base, 'grape/exceptions/base'
|
@@ -73,9 +75,10 @@ module Grape
|
|
73
75
|
end
|
74
76
|
|
75
77
|
module Versioner
|
76
|
-
autoload :Path,
|
77
|
-
autoload :Header,
|
78
|
-
autoload :Param,
|
78
|
+
autoload :Path, 'grape/middleware/versioner/path'
|
79
|
+
autoload :Header, 'grape/middleware/versioner/header'
|
80
|
+
autoload :Param, 'grape/middleware/versioner/param'
|
81
|
+
autoload :AcceptVersionHeader, 'grape/middleware/versioner/accept_version_header'
|
79
82
|
end
|
80
83
|
end
|
81
84
|
|
data/lib/grape/api.rb
CHANGED
@@ -370,6 +370,17 @@ module Grape
|
|
370
370
|
end
|
371
371
|
end
|
372
372
|
|
373
|
+
# Thie method allows you to quickly define a parameter route segment
|
374
|
+
# in your API.
|
375
|
+
#
|
376
|
+
# @param param [Symbol] The name of the parameter you wish to declare.
|
377
|
+
# @option options [Regexp] You may supply a regular expression that the declared parameter must meet.
|
378
|
+
def route_param(param, options = {}, &block)
|
379
|
+
options = options.dup
|
380
|
+
options[:requirements] = { param.to_sym => options[:requirements] } if options[:requirements].is_a?(Regexp)
|
381
|
+
namespace(":#{param}", options, &block)
|
382
|
+
end
|
383
|
+
|
373
384
|
alias_method :group, :namespace
|
374
385
|
alias_method :resource, :namespace
|
375
386
|
alias_method :resources, :namespace
|
@@ -410,6 +421,12 @@ module Grape
|
|
410
421
|
@versions ||= []
|
411
422
|
end
|
412
423
|
|
424
|
+
def cascade(value = nil)
|
425
|
+
value.nil? ?
|
426
|
+
(settings.has_key?(:cascade) ? !! settings[:cascade] : true) :
|
427
|
+
set(:cascade, value)
|
428
|
+
end
|
429
|
+
|
413
430
|
protected
|
414
431
|
|
415
432
|
def prepare_routes
|
@@ -443,7 +460,10 @@ module Grape
|
|
443
460
|
|
444
461
|
def inherit_settings(other_stack)
|
445
462
|
settings.prepend other_stack
|
446
|
-
endpoints.each
|
463
|
+
endpoints.each do |e|
|
464
|
+
e.settings.prepend(other_stack)
|
465
|
+
e.options[:app].inherit_settings(other_stack) if e.options[:app].respond_to?(:inherit_settings, true)
|
466
|
+
end
|
447
467
|
end
|
448
468
|
end
|
449
469
|
|
@@ -471,8 +491,9 @@ module Grape
|
|
471
491
|
# errors from reaching upstream. This is effectivelly done by unsetting
|
472
492
|
# X-Cascade. Default :cascade is true.
|
473
493
|
def cascade?
|
474
|
-
|
475
|
-
cascade.
|
494
|
+
return !! self.class.settings[:cascade] if self.class.settings.has_key?(:cascade)
|
495
|
+
return !! self.class.settings[:version_options][:cascade] if self.class.settings[:version_options] && self.class.settings[:version_options].has_key?(:cascade)
|
496
|
+
true
|
476
497
|
end
|
477
498
|
|
478
499
|
reset!
|
data/lib/grape/endpoint.rb
CHANGED
@@ -169,9 +169,7 @@ module Grape
|
|
169
169
|
# The parameters passed into the request as
|
170
170
|
# well as parsed from URL segments.
|
171
171
|
def params
|
172
|
-
@params ||=
|
173
|
-
deep_merge(request.params).
|
174
|
-
deep_merge(env['rack.routing_args'] || {})
|
172
|
+
@params ||= @request.params
|
175
173
|
end
|
176
174
|
|
177
175
|
# A filtering method that will return a hash
|
@@ -180,20 +178,35 @@ module Grape
|
|
180
178
|
#
|
181
179
|
# @param params [Hash] The initial hash to filter. Usually this will just be `params`
|
182
180
|
# @param options [Hash] Can pass `:include_missing` and `:stringify` options.
|
183
|
-
def declared(params, options = {})
|
181
|
+
def declared(params, options = {}, declared_params = settings[:declared_params])
|
184
182
|
options[:include_missing] = true unless options.key?(:include_missing)
|
185
183
|
|
186
|
-
unless
|
184
|
+
unless declared_params
|
187
185
|
raise ArgumentError, "Tried to filter for declared parameters but none exist."
|
188
186
|
end
|
189
187
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
h[output_key] = params[k]
|
188
|
+
if params.is_a? Array
|
189
|
+
params.map do |param|
|
190
|
+
declared(param || {}, options, declared_params)
|
194
191
|
end
|
195
|
-
|
196
|
-
|
192
|
+
else
|
193
|
+
declared_params.inject({}) do |hash, key|
|
194
|
+
key = { key => nil } unless key.is_a? Hash
|
195
|
+
|
196
|
+
key.each_pair do |parent, children|
|
197
|
+
output_key = options[:stringify] ? parent.to_s : parent.to_sym
|
198
|
+
if params.key?(parent) || options[:include_missing]
|
199
|
+
hash[output_key] = if children
|
200
|
+
declared(params[parent] || {}, options, Array(children))
|
201
|
+
else
|
202
|
+
params[parent]
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
hash
|
208
|
+
end
|
209
|
+
end
|
197
210
|
end
|
198
211
|
|
199
212
|
# The API version as specified in the URL.
|
@@ -257,13 +270,7 @@ module Grape
|
|
257
270
|
|
258
271
|
# Retrieves all available request headers.
|
259
272
|
def headers
|
260
|
-
@headers ||= @
|
261
|
-
if k.start_with? 'HTTP_'
|
262
|
-
k = k[5..-1].gsub('_', '-').downcase.gsub(/^.|[-_\s]./) { |x| x.upcase }
|
263
|
-
h[k] = v
|
264
|
-
end
|
265
|
-
h
|
266
|
-
}
|
273
|
+
@headers ||= @request.headers
|
267
274
|
end
|
268
275
|
|
269
276
|
# Set response content-type
|
@@ -316,7 +323,13 @@ module Grape
|
|
316
323
|
# :with => API::Entities::User,
|
317
324
|
# :admin => current_user.admin?
|
318
325
|
# end
|
319
|
-
def present(
|
326
|
+
def present(*args)
|
327
|
+
options = args.count > 1 ? args.extract_options! : {}
|
328
|
+
key, object = if args.count == 2 && args.first.is_a?(Symbol)
|
329
|
+
args
|
330
|
+
else
|
331
|
+
[nil, args.first]
|
332
|
+
end
|
320
333
|
entity_class = options.delete(:with)
|
321
334
|
|
322
335
|
# auto-detect the entity from the first object in the collection
|
@@ -339,6 +352,7 @@ module Grape
|
|
339
352
|
end
|
340
353
|
|
341
354
|
representation = { root => representation } if root
|
355
|
+
representation = (@body || {}).merge({key => representation}) if key
|
342
356
|
body representation
|
343
357
|
end
|
344
358
|
|
@@ -369,7 +383,7 @@ module Grape
|
|
369
383
|
def run(env)
|
370
384
|
@env = env
|
371
385
|
@header = {}
|
372
|
-
@request =
|
386
|
+
@request = Grape::Request.new(@env)
|
373
387
|
|
374
388
|
self.extend helpers
|
375
389
|
cookies.read(@request)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Grape
|
2
|
+
class Request < Rack::Request
|
3
|
+
|
4
|
+
def params
|
5
|
+
@env['grape.request.params'] ||= begin
|
6
|
+
params = Hashie::Mash.new(super)
|
7
|
+
if env['rack.routing_args']
|
8
|
+
args = env['rack.routing_args'].dup
|
9
|
+
# preserve version from query string parameters
|
10
|
+
args.delete(:version)
|
11
|
+
params.deep_merge!(args)
|
12
|
+
end
|
13
|
+
params
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def headers
|
18
|
+
@env['grape.request.headers'] ||= @env.dup.inject({}) { |h, (k, v)|
|
19
|
+
if k.to_s.start_with? 'HTTP_'
|
20
|
+
k = k[5..-1].gsub('_', '-').downcase.gsub(/^.|[-_\s]./) { |x| x.upcase }
|
21
|
+
h[k] = v
|
22
|
+
end
|
23
|
+
h
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -3,7 +3,7 @@ module Grape
|
|
3
3
|
# This is a simple middleware for adding before and after filters
|
4
4
|
# to Grape APIs. It is used like so:
|
5
5
|
#
|
6
|
-
# use Grape::Middleware::Filter, :before => lambda{ do_something }, after
|
6
|
+
# use Grape::Middleware::Filter, :before => lambda{ do_something }, :after => lambda{ do_something }
|
7
7
|
class Filter < Base
|
8
8
|
def before
|
9
9
|
app.instance_eval &options[:before] if options[:before]
|
@@ -23,12 +23,14 @@ module Grape
|
|
23
23
|
|
24
24
|
def after
|
25
25
|
status, headers, bodies = *@app_response
|
26
|
-
|
26
|
+
# allow content-type to be explicitly overwritten
|
27
|
+
api_format = mime_types[headers["Content-Type"]] || env['api.format']
|
28
|
+
formatter = Grape::Formatter::Base.formatter_for api_format, options
|
27
29
|
begin
|
28
30
|
bodymap = bodies.collect do |body|
|
29
31
|
formatter.call body, env
|
30
32
|
end
|
31
|
-
rescue
|
33
|
+
rescue Grape::Exceptions::InvalidFormatter => e
|
32
34
|
throw :error, :status => 500, :message => e.message
|
33
35
|
end
|
34
36
|
headers['Content-Type'] = content_type_for(env['api.format']) unless headers['Content-Type']
|
@@ -39,37 +41,46 @@ module Grape
|
|
39
41
|
|
40
42
|
# store read input in env['api.request.input']
|
41
43
|
def read_body_input
|
42
|
-
if (request.post? || request.put? || request.patch?) &&
|
43
|
-
|
44
|
-
|
44
|
+
if (request.post? || request.put? || request.patch?) &&
|
45
|
+
(! request.form_data? || ! request.media_type) &&
|
46
|
+
(! request.parseable_data?) &&
|
47
|
+
(request.content_length.to_i > 0 || request.env['HTTP_TRANSFER_ENCODING'] == 'chunked')
|
48
|
+
|
49
|
+
if (input = env['rack.input'])
|
50
|
+
input.rewind
|
51
|
+
body = env['api.request.input'] = input.read
|
52
|
+
begin
|
53
|
+
read_rack_input(body) if body && body.length > 0
|
54
|
+
ensure
|
55
|
+
input.rewind
|
56
|
+
end
|
45
57
|
end
|
46
58
|
end
|
47
59
|
end
|
48
60
|
|
49
61
|
# store parsed input in env['api.request.body']
|
50
62
|
def read_rack_input(body)
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
end
|
63
|
+
fmt = mime_types[request.media_type] if request.media_type
|
64
|
+
fmt ||= options[:default_format]
|
65
|
+
if content_type_for(fmt)
|
66
|
+
parser = Grape::Parser::Base.parser_for fmt, options
|
67
|
+
if parser
|
68
|
+
begin
|
69
|
+
body = (env['api.request.body'] = parser.call(body, env))
|
70
|
+
if body.is_a?(Hash)
|
71
|
+
env['rack.request.form_hash'] = env['rack.request.form_hash'] ?
|
72
|
+
env['rack.request.form_hash'].merge(body) :
|
73
|
+
body
|
63
74
|
env['rack.request.form_input'] = env['rack.input']
|
64
|
-
rescue Exception => e
|
65
|
-
throw :error, :status => 400, :message => e.message
|
66
75
|
end
|
76
|
+
rescue Exception => e
|
77
|
+
throw :error, :status => 400, :message => e.message
|
67
78
|
end
|
68
79
|
else
|
69
|
-
|
80
|
+
env['api.request.body'] = body
|
70
81
|
end
|
71
|
-
|
72
|
-
|
82
|
+
else
|
83
|
+
throw :error, :status => 406, :message => "The requested content-type '#{request.media_type}' is not supported."
|
73
84
|
end
|
74
85
|
end
|
75
86
|
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'grape/middleware/base'
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
module Middleware
|
5
|
+
module Versioner
|
6
|
+
# This middleware sets various version related rack environment variables
|
7
|
+
# based on the HTTP Accept-Version header
|
8
|
+
#
|
9
|
+
# Example: For request header
|
10
|
+
# Accept-Version: v1
|
11
|
+
#
|
12
|
+
# The following rack env variables are set:
|
13
|
+
#
|
14
|
+
# env['api.version] => 'v1'
|
15
|
+
#
|
16
|
+
# If version does not match this route, then a 406 is raised with
|
17
|
+
# X-Cascade header to alert Rack::Mount to attempt the next matched
|
18
|
+
# route.
|
19
|
+
class AcceptVersionHeader < Base
|
20
|
+
|
21
|
+
def before
|
22
|
+
potential_version = (env['HTTP_ACCEPT_VERSION'] || '').strip
|
23
|
+
|
24
|
+
if strict?
|
25
|
+
# If no Accept-Version header:
|
26
|
+
if potential_version.empty?
|
27
|
+
throw :error, :status => 406, :headers => error_headers, :message => 'Accept-Version header must be set.'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
unless potential_version.empty?
|
32
|
+
# If the requested version is not supported:
|
33
|
+
if !versions.any? { |v| v.to_s == potential_version }
|
34
|
+
throw :error, :status => 406, :headers => error_headers, :message => 'The requested version is not supported.'
|
35
|
+
end
|
36
|
+
|
37
|
+
env['api.version'] = potential_version
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def versions
|
44
|
+
options[:versions] || []
|
45
|
+
end
|
46
|
+
|
47
|
+
def strict?
|
48
|
+
options[:version_options] && options[:version_options][:strict]
|
49
|
+
end
|
50
|
+
|
51
|
+
# By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking
|
52
|
+
# of routes (see [Rack::Mount](https://github.com/josh/rack-mount) for more information). To prevent
|
53
|
+
# this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
|
54
|
+
def cascade?
|
55
|
+
options[:version_options] && options[:version_options].has_key?(:cascade) ?
|
56
|
+
!! options[:version_options][:cascade] :
|
57
|
+
true
|
58
|
+
end
|
59
|
+
|
60
|
+
def error_headers
|
61
|
+
cascade? ? { 'X-Cascade' => 'pass' } : {}
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -99,7 +99,9 @@ module Grape
|
|
99
99
|
# of routes (see [Rack::Mount](https://github.com/josh/rack-mount) for more information). To prevent
|
100
100
|
# this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
|
101
101
|
def cascade?
|
102
|
-
options[:version_options] &&
|
102
|
+
options[:version_options] && options[:version_options].has_key?(:cascade) ?
|
103
|
+
!! options[:version_options][:cascade] :
|
104
|
+
true
|
103
105
|
end
|
104
106
|
|
105
107
|
def error_headers
|
@@ -42,6 +42,17 @@ module Grape
|
|
42
42
|
end
|
43
43
|
alias_method :[], :get
|
44
44
|
|
45
|
+
# Looks through the stack for the first frame that matches :key
|
46
|
+
#
|
47
|
+
# @param key [Symbol] key to look for in hash frames
|
48
|
+
# @return true if key exists, false otherwise
|
49
|
+
def has_key?(key)
|
50
|
+
(@stack.length - 1).downto(0).each do |i|
|
51
|
+
return true if @stack[i].key? key
|
52
|
+
end
|
53
|
+
false
|
54
|
+
end
|
55
|
+
|
45
56
|
# Replace a value on the top hash of the stack.
|
46
57
|
#
|
47
58
|
# @param key [Symbol] The key to set.
|