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/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 #{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, 'grape/middleware/versioner/path'
77
- autoload :Header, 'grape/middleware/versioner/header'
78
- autoload :Param, 'grape/middleware/versioner/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{|e| e.settings.prepend(other_stack)}
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
- cascade = ((self.class.settings || {})[:version_options] || {})[:cascade]
475
- cascade.nil? ? true : 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!
@@ -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 ||= Hashie::Mash.new.
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 settings[:declared_params]
184
+ unless declared_params
187
185
  raise ArgumentError, "Tried to filter for declared parameters but none exist."
188
186
  end
189
187
 
190
- settings[:declared_params].inject({}){|h,k|
191
- output_key = options[:stringify] ? k.to_s : k.to_sym
192
- if params.key?(output_key) || options[:include_missing]
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
- h
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 ||= @env.dup.inject({}) { |h, (k, v)|
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(object, options = {})
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 = Rack::Request.new(@env)
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
@@ -34,7 +34,7 @@ module Grape
34
34
  def after; end
35
35
 
36
36
  def request
37
- Rack::Request.new(self.env)
37
+ Grape::Request.new(self.env)
38
38
  end
39
39
 
40
40
  def response
@@ -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: => lambda{ do_something }
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
- formatter = Grape::Formatter::Base.formatter_for env['api.format'], options
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 Exception => e
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?) && (! request.form_data?) && (! request.parseable_data?) && (request.content_length.to_i > 0)
43
- if env['rack.input'] && (body = (env['api.request.input'] = env['rack.input'].read)).length > 0
44
- read_rack_input body
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
- begin
52
- fmt = mime_types[request.media_type] if request.media_type
53
- if content_type_for(fmt)
54
- parser = Grape::Parser::Base.parser_for fmt, options
55
- if parser
56
- begin
57
- body = (env['api.request.body'] = parser.call(body, env))
58
- if body.is_a?(Hash)
59
- env['rack.request.form_hash'] = env['rack.request.form_hash'] ?
60
- env['rack.request.form_hash'].merge(body) :
61
- body
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
- throw :error, :status => 406, :message => "The requested content-type '#{request.media_type}' is not supported."
80
+ env['api.request.body'] = body
70
81
  end
71
- ensure
72
- env['rack.input'].rewind
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
 
@@ -21,6 +21,8 @@ module Grape
21
21
  Header
22
22
  when :param
23
23
  Param
24
+ when :accept_version_header
25
+ AcceptVersionHeader
24
26
  else
25
27
  raise Grape::Exceptions::InvalidVersionerOption.new(strategy)
26
28
  end
@@ -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] && (options[:version_options].has_key?(:cascade) ? options[:version_options][:cascade] : true)
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.