grape 0.2.3 → 0.2.4

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.

Files changed (42) hide show
  1. data/.yardopts +2 -0
  2. data/CHANGELOG.markdown +19 -0
  3. data/Gemfile +2 -0
  4. data/README.markdown +166 -131
  5. data/Rakefile +1 -3
  6. data/lib/grape.rb +0 -3
  7. data/lib/grape/api.rb +16 -4
  8. data/lib/grape/cookies.rb +32 -30
  9. data/lib/grape/endpoint.rb +25 -11
  10. data/lib/grape/entity.rb +5 -0
  11. data/lib/grape/error_formatter/base.rb +2 -1
  12. data/lib/grape/middleware/base.rb +9 -2
  13. data/lib/grape/middleware/error.rb +11 -11
  14. data/lib/grape/middleware/formatter.rb +8 -5
  15. data/lib/grape/middleware/versioner/header.rb +1 -3
  16. data/lib/grape/middleware/versioner/path.rb +15 -2
  17. data/lib/grape/util/deep_merge.rb +4 -4
  18. data/lib/grape/validations.rb +2 -3
  19. data/lib/grape/version.rb +1 -1
  20. data/spec/grape/api_spec.rb +316 -175
  21. data/spec/grape/endpoint_spec.rb +159 -57
  22. data/spec/grape/entity_spec.rb +80 -80
  23. data/spec/grape/middleware/auth/basic_spec.rb +3 -3
  24. data/spec/grape/middleware/auth/digest_spec.rb +4 -4
  25. data/spec/grape/middleware/auth/oauth2_spec.rb +4 -4
  26. data/spec/grape/middleware/base_spec.rb +9 -9
  27. data/spec/grape/middleware/error_spec.rb +4 -4
  28. data/spec/grape/middleware/exception_spec.rb +13 -13
  29. data/spec/grape/middleware/formatter_spec.rb +25 -25
  30. data/spec/grape/middleware/versioner/header_spec.rb +23 -23
  31. data/spec/grape/middleware/versioner/param_spec.rb +8 -8
  32. data/spec/grape/middleware/versioner/path_spec.rb +8 -8
  33. data/spec/grape/middleware/versioner_spec.rb +6 -3
  34. data/spec/grape/util/hash_stack_spec.rb +20 -20
  35. data/spec/grape/validations/presence_spec.rb +1 -1
  36. data/spec/grape/validations/regexp_spec.rb +2 -2
  37. data/spec/grape/validations_spec.rb +4 -4
  38. data/spec/shared/versioning_examples.rb +48 -20
  39. metadata +5 -7
  40. data/.document +0 -5
  41. data/lib/grape/middleware/prefixer.rb +0 -21
  42. data/spec/grape/middleware/prefixer_spec.rb +0 -30
data/Rakefile CHANGED
@@ -19,18 +19,16 @@ task :default => :spec
19
19
 
20
20
  begin
21
21
  require 'yard'
22
- YARD_OPTS = ['-m', 'markdown', '-M', 'maruku']
23
22
  DOC_FILES = ['lib/**/*.rb', 'README.markdown']
24
23
 
25
24
  YARD::Rake::YardocTask.new(:doc) do |t|
26
25
  t.files = DOC_FILES
27
- t.options = YARD_OPTS
28
26
  end
29
27
 
30
28
  namespace :doc do
31
29
  YARD::Rake::YardocTask.new(:pages) do |t|
32
30
  t.files = DOC_FILES
33
- t.options = YARD_OPTS + ['-o', '../grape.doc']
31
+ t.options = ['-o', '../grape.doc']
34
32
  end
35
33
 
36
34
  namespace :pages do
@@ -4,8 +4,6 @@ require 'rack/builder'
4
4
  module Grape
5
5
  autoload :API, 'grape/api'
6
6
  autoload :Endpoint, 'grape/endpoint'
7
- autoload :MiddlewareStack, 'grape/middleware_stack'
8
- autoload :Client, 'grape/client'
9
7
  autoload :Route, 'grape/route'
10
8
  autoload :Entity, 'grape/entity'
11
9
  autoload :Cookies, 'grape/cookies'
@@ -39,7 +37,6 @@ module Grape
39
37
 
40
38
  module Middleware
41
39
  autoload :Base, 'grape/middleware/base'
42
- autoload :Prefixer, 'grape/middleware/prefixer'
43
40
  autoload :Versioner, 'grape/middleware/versioner'
44
41
  autoload :Formatter, 'grape/middleware/formatter'
45
42
  autoload :Error, 'grape/middleware/error'
@@ -125,9 +125,14 @@ module Grape
125
125
  end
126
126
 
127
127
  # Specify the format for the API's serializers.
128
- # May be `:json` or `:txt`.
128
+ # May be `:json`, `:xml`, `:txt`, etc.
129
129
  def format(new_format = nil)
130
- new_format ? set(:format, new_format.to_sym) : settings[:format]
130
+ if new_format
131
+ set(:format, new_format.to_sym)
132
+ set(:default_error_formatter, Grape::ErrorFormatter::Base.formatter_for(new_format, {}))
133
+ else
134
+ settings[:format]
135
+ end
131
136
  end
132
137
 
133
138
  # Specify a custom formatter for a content-type.
@@ -135,6 +140,11 @@ module Grape
135
140
  settings.imbue(:formatters, content_type.to_sym => new_formatter)
136
141
  end
137
142
 
143
+ # Specify a default error formatter.
144
+ def default_error_formatter(new_formatter = nil)
145
+ new_formatter ? set(:default_error_formatter, new_formatter) : settings[:default_error_formatter]
146
+ end
147
+
138
148
  def error_formatter(format, new_formatter)
139
149
  settings.imbue(:error_formatters, format.to_sym => new_formatter)
140
150
  end
@@ -209,10 +219,11 @@ module Grape
209
219
  # When called without a block, all known helpers within this scope
210
220
  # are included.
211
221
  #
212
- # @param mod [Module] optional module of methods to include
213
- # @param &block [Block] optional block of methods to include
222
+ # @param [Module] new_mod optional module of methods to include
223
+ # @param [Block] block optional block of methods to include
214
224
  #
215
225
  # @example Define some helpers.
226
+ #
216
227
  # class ExampleAPI < Grape::API
217
228
  # helpers do
218
229
  # def current_user
@@ -220,6 +231,7 @@ module Grape
220
231
  # end
221
232
  # end
222
233
  # end
234
+ #
223
235
  def helpers(new_mod = nil, &block)
224
236
  if block_given? || new_mod
225
237
  mod = settings.peek[:helpers] || Module.new
@@ -1,41 +1,43 @@
1
1
  module Grape
2
2
  class Cookies
3
3
 
4
- def initialize
5
- @cookies = {}
6
- @send_cookies = {}
7
- end
4
+ def initialize
5
+ @cookies = {}
6
+ @send_cookies = {}
7
+ end
8
8
 
9
- def read(request)
10
- request.cookies.each do |name, value|
11
- @cookies[name.to_sym] = value
12
- end
9
+ def read(request)
10
+ request.cookies.each do |name, value|
11
+ @cookies[name.to_s] = value
13
12
  end
13
+ end
14
14
 
15
- def write(header)
16
- @cookies.select { |key, value|
17
- @send_cookies[key.to_sym] == true
18
- }.each { |name, value|
19
- Rack::Utils.set_cookie_header!(
20
- header, name, value.instance_of?(Hash) ? value : { :value => value })
21
- }
22
- end
15
+ def write(header)
16
+ @cookies.select { |key, value|
17
+ @send_cookies[key] == true
18
+ }.each { |name, value|
19
+ cookie_value = value.is_a?(Hash) ? value : { :value => value }
20
+ Rack::Utils.set_cookie_header! header, name, cookie_value
21
+ }
22
+ end
23
23
 
24
- def [](name)
25
- @cookies[name]
26
- end
24
+ def [](name)
25
+ @cookies[name.to_s]
26
+ end
27
27
 
28
- def []=(name, value)
29
- @cookies[name.to_sym] = value
30
- @send_cookies[name.to_sym] = true
31
- end
28
+ def []=(name, value)
29
+ @cookies[name.to_s] = value
30
+ @send_cookies[name.to_s] = true
31
+ end
32
32
 
33
- def each(&block)
34
- @cookies.each(&block)
35
- end
33
+ def each(&block)
34
+ @cookies.each(&block)
35
+ end
36
36
 
37
- def delete(name)
38
- self.[]=(name, { :value => 'deleted', :expires => Time.at(0) })
39
- end
37
+ def delete(name, opts = {})
38
+ options = opts.merge({ :value => 'deleted', :expires => Time.at(0) })
39
+ self.[]=(name, options)
40
40
  end
41
- end
41
+
42
+ end
43
+ end
@@ -6,9 +6,9 @@ module Grape
6
6
  # An Endpoint is the proxy scope in which all routing
7
7
  # blocks are executed. In other words, any methods
8
8
  # on the instance level of this class may be called
9
- # from inside a `get`, `post`, etc. block.
9
+ # from inside a `get`, `post`, etc.
10
10
  class Endpoint
11
- attr_accessor :block, :options, :settings
11
+ attr_accessor :block, :source, :options, :settings
12
12
  attr_reader :env, :request
13
13
 
14
14
  class << self
@@ -40,6 +40,7 @@ module Grape
40
40
  @settings = settings
41
41
  if block_given?
42
42
  method_name = "#{options[:method]} #{settings.gather(:namespace).join( "/")} #{Array(options[:path]).join("/")}"
43
+ @source = block
43
44
  @block = self.class.generate_api_method(method_name, &block)
44
45
  end
45
46
  @options = options
@@ -55,12 +56,12 @@ module Grape
55
56
  end
56
57
 
57
58
  def routes
58
- @routes ||= prepare_routes
59
+ @routes ||= endpoints ? endpoints.collect(&:routes).flatten : prepare_routes
59
60
  end
60
61
 
61
62
  def mount_in(route_set)
62
- if options[:app] && options[:app].respond_to?(:endpoints)
63
- options[:app].endpoints.each{|e| e.mount_in(route_set)}
63
+ if endpoints
64
+ endpoints.each{|e| e.mount_in(route_set)}
64
65
  else
65
66
  routes.each do |route|
66
67
  route_set.add_route(self, {
@@ -109,7 +110,7 @@ module Grape
109
110
 
110
111
  def prepare_path(path)
111
112
  parts = []
112
- parts << settings[:root_prefix] if settings[:root_prefix]
113
+ parts << settings[:root_prefix].to_s.split("/") if settings[:root_prefix]
113
114
 
114
115
  uses_path_versioning = settings[:version] && settings[:version_options][:using] == :path
115
116
  namespace_is_empty = namespace && (namespace.to_s =~ /^\s*$/ || namespace.to_s == '/')
@@ -118,11 +119,12 @@ module Grape
118
119
  parts << ':version' if uses_path_versioning
119
120
  if !uses_path_versioning || (!namespace_is_empty || !path_is_empty)
120
121
  parts << namespace.to_s if namespace
121
- parts << path.to_s if path && '/' != path
122
+ parts << path.to_s if path
122
123
  format_suffix = '(.:format)'
123
124
  else
124
125
  format_suffix = '(/.:format)'
125
126
  end
127
+ parts = parts.flatten.select { |part| part != '/' }
126
128
  Rack::Mount::Utils.normalize_path(parts.join('/') + format_suffix)
127
129
  end
128
130
 
@@ -186,9 +188,10 @@ module Grape
186
188
  # Pull out request body params if the content type matches and we're on a POST or PUT
187
189
  def body_params
188
190
  if ['POST', 'PUT'].include?(request.request_method.to_s.upcase) && request.content_length.to_i > 0
189
- return case env['CONTENT_TYPE']
191
+ return @body_params ||=
192
+ case env['CONTENT_TYPE']
190
193
  when 'application/json'
191
- MultiJson.decode(request.body.read)
194
+ MultiJson.load(request.body.read)
192
195
  when 'application/xml'
193
196
  MultiXml.parse(request.body.read)
194
197
  else
@@ -345,6 +348,16 @@ module Grape
345
348
 
346
349
  protected
347
350
 
351
+ # Return the collection of endpoints within this endpoint.
352
+ # This is the case when an Grape::API mounts another Grape::API.
353
+ def endpoints
354
+ if options[:app] && options[:app].respond_to?(:endpoints)
355
+ options[:app].endpoints
356
+ else
357
+ nil
358
+ end
359
+ end
360
+
348
361
  def run(env)
349
362
  @env = env
350
363
  @header = {}
@@ -377,18 +390,19 @@ module Grape
377
390
  :default_status => settings[:default_error_status] || 403,
378
391
  :rescue_all => settings[:rescue_all],
379
392
  :rescued_errors => aggregate_setting(:rescued_errors),
393
+ :default_error_formatter => settings[:default_error_formatter],
380
394
  :error_formatters => settings[:error_formatters],
381
395
  :rescue_options => settings[:rescue_options],
382
396
  :rescue_handlers => merged_setting(:rescue_handlers)
383
397
 
384
398
  b.use Rack::Auth::Basic, settings[:auth][:realm], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_basic
385
399
  b.use Rack::Auth::Digest::MD5, settings[:auth][:realm], settings[:auth][:opaque], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_digest
386
- b.use Grape::Middleware::Prefixer, :prefix => settings[:root_prefix] if settings[:root_prefix]
387
400
 
388
401
  if settings[:version]
389
402
  b.use Grape::Middleware::Versioner.using(settings[:version_options][:using]), {
390
403
  :versions => settings[:version],
391
- :version_options => settings[:version_options]
404
+ :version_options => settings[:version_options],
405
+ :prefix => settings[:root_prefix]
392
406
  }
393
407
  end
394
408
 
@@ -330,6 +330,11 @@ module Grape
330
330
 
331
331
  alias :as_json :serializable_hash
332
332
 
333
+ def to_json(options = {})
334
+ options = options.to_h if options && options.respond_to?(:to_h)
335
+ MultiJson.dump(serializable_hash(options))
336
+ end
337
+
333
338
  protected
334
339
 
335
340
  def key_for(attribute)
@@ -5,6 +5,7 @@ module Grape
5
5
  class << self
6
6
 
7
7
  FORMATTERS = {
8
+ :serializable_hash => Grape::ErrorFormatter::Json,
8
9
  :json => Grape::ErrorFormatter::Json,
9
10
  :txt => Grape::ErrorFormatter::Txt,
10
11
  :xml => Grape::ErrorFormatter::Xml
@@ -18,7 +19,7 @@ module Grape
18
19
  spec = formatters(options)[api_format]
19
20
  case spec
20
21
  when nil
21
- lambda { |message, backtrace, options| message }
22
+ options[:default_error_formatter] || Grape::ErrorFormatter::Txt
22
23
  when Symbol
23
24
  method(spec)
24
25
  else
@@ -1,4 +1,5 @@
1
1
  require 'active_support/ordered_hash'
2
+ require 'active_support/core_ext/hash/indifferent_access'
2
3
  require 'multi_json'
3
4
  require 'multi_xml'
4
5
 
@@ -24,7 +25,9 @@ module Grape
24
25
  @options = default_options.merge(options)
25
26
  end
26
27
 
27
- def default_options; {} end
28
+ def default_options
29
+ {}
30
+ end
28
31
 
29
32
  def call(env)
30
33
  dup.call!(env)
@@ -53,12 +56,16 @@ module Grape
53
56
  Rack::Response.new(@app_response)
54
57
  end
55
58
 
59
+ def content_type_for(format)
60
+ HashWithIndifferentAccess.new(content_types)[format]
61
+ end
62
+
56
63
  def content_types
57
64
  options[:content_types] || CONTENT_TYPES
58
65
  end
59
66
 
60
67
  def content_type
61
- content_types[env['api.format'] || options[:format]] || 'text/html'
68
+ content_type_for(env['api.format'] || options[:format]) || 'text/html'
62
69
  end
63
70
 
64
71
  def mime_types
@@ -6,17 +6,17 @@ module Grape
6
6
  class Error < Base
7
7
 
8
8
  def default_options
9
- {
10
- :default_status => 403, # default status returned on error
11
- :default_message => "",
12
- :format => :txt,
13
- :formatters => {},
14
- :error_formatters => {},
15
- :rescue_all => false, # true to rescue all exceptions
16
- :rescue_options => { :backtrace => false }, # true to display backtrace
17
- :rescue_handlers => {}, # rescue handler blocks
18
- :rescued_errors => []
19
- }
9
+ {
10
+ :default_status => 403, # default status returned on error
11
+ :default_message => "",
12
+ :format => :txt,
13
+ :formatters => {},
14
+ :error_formatters => {},
15
+ :rescue_all => false, # true to rescue all exceptions
16
+ :rescue_options => { :backtrace => false }, # true to display backtrace
17
+ :rescue_handlers => {}, # rescue handler blocks
18
+ :rescued_errors => []
19
+ }
20
20
  end
21
21
 
22
22
  def call!(env)
@@ -18,7 +18,7 @@ module Grape
18
18
 
19
19
  def before
20
20
  fmt = format_from_extension || format_from_params || options[:format] || format_from_header || options[:default_format]
21
- if content_types.key?(fmt)
21
+ if content_type_for(fmt)
22
22
  if !env['rack.input'].nil? and (body = env['rack.input'].read).strip.length != 0
23
23
  parser = Grape::Parser::Base.parser_for fmt, options
24
24
  unless parser.nil?
@@ -42,15 +42,18 @@ module Grape
42
42
  parts = request.path.split('.')
43
43
 
44
44
  if parts.size > 1
45
- extension = parts.last.to_sym
46
- return extension if content_types.key?(extension)
45
+ extension = parts.last
46
+ # avoid symbol memory leak on an unknown format
47
+ return extension.to_sym if content_type_for(extension)
47
48
  end
48
49
  nil
49
50
  end
50
51
 
51
52
  def format_from_params
52
53
  fmt = Rack::Utils.parse_nested_query(env['QUERY_STRING'])["format"]
53
- fmt ? fmt.to_sym : nil
54
+ # avoid symbol memory leak on an unknown format
55
+ return fmt.to_sym if content_type_for(fmt)
56
+ fmt
54
57
  end
55
58
 
56
59
  def format_from_header
@@ -76,7 +79,7 @@ module Grape
76
79
  bodymap = bodies.collect do |body|
77
80
  formatter.call body, env
78
81
  end
79
- headers['Content-Type'] = content_types[env['api.format']] unless headers['Content-Type']
82
+ headers['Content-Type'] = content_type_for(env['api.format']) unless headers['Content-Type']
80
83
  Rack::Response.new(bodymap, status, headers).to_a
81
84
  end
82
85
 
@@ -19,11 +19,9 @@ module Grape
19
19
  # env['api.version] => 'v1'
20
20
  # env['api.format] => 'format'
21
21
  #
22
- # If version does not match this route, then a 406 is throw with
22
+ # If version does not match this route, then a 406 is raised with
23
23
  # X-Cascade header to alert Rack::Mount to attempt the next matched
24
24
  # route.
25
- #
26
- # @throws [RuntimeError] if Accept header is invalid
27
25
  class Header < Base
28
26
 
29
27
  def before
@@ -24,7 +24,14 @@ module Grape
24
24
  end
25
25
 
26
26
  def before
27
- pieces = env['PATH_INFO'].split('/')
27
+ path = env['PATH_INFO'].dup
28
+
29
+ if prefix && path.index(prefix) == 0
30
+ path.sub!(prefix, '')
31
+ path = Rack::Mount::Utils.normalize_path(path)
32
+ end
33
+
34
+ pieces = path.split('/')
28
35
  potential_version = pieces[1]
29
36
  if potential_version =~ options[:pattern]
30
37
  if options[:versions] && !options[:versions].include?(potential_version)
@@ -33,9 +40,15 @@ module Grape
33
40
 
34
41
  truncated_path = "/#{pieces[2..-1].join('/')}"
35
42
  env['api.version'] = potential_version
36
- env['PATH_INFO'] = truncated_path
37
43
  end
38
44
  end
45
+
46
+ private
47
+
48
+ def prefix
49
+ Rack::Mount::Utils.normalize_path(options[:prefix].to_s) if options[:prefix]
50
+ end
51
+
39
52
  end
40
53
  end
41
54
  end