grape 0.2.6 → 0.3.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.

Files changed (50) hide show
  1. data/{CHANGELOG.markdown → CHANGELOG.md} +21 -1
  2. data/Gemfile +1 -0
  3. data/{README.markdown → README.md} +178 -125
  4. data/grape.gemspec +1 -1
  5. data/lib/grape.rb +25 -3
  6. data/lib/grape/api.rb +43 -20
  7. data/lib/grape/endpoint.rb +32 -13
  8. data/lib/grape/exceptions/base.rb +50 -1
  9. data/lib/grape/exceptions/invalid_formatter.rb +13 -0
  10. data/lib/grape/exceptions/invalid_versioner_option.rb +14 -0
  11. data/lib/grape/exceptions/invalid_with_option_for_represent.rb +15 -0
  12. data/lib/grape/exceptions/missing_mime_type.rb +14 -0
  13. data/lib/grape/exceptions/missing_option.rb +13 -0
  14. data/lib/grape/exceptions/missing_vendor_option.rb +13 -0
  15. data/lib/grape/exceptions/unknown_options.rb +14 -0
  16. data/lib/grape/exceptions/unknown_validator.rb +12 -0
  17. data/lib/grape/exceptions/{validation_error.rb → validation.rb} +3 -1
  18. data/lib/grape/formatter/xml.rb +2 -1
  19. data/lib/grape/locale/en.yml +20 -0
  20. data/lib/grape/middleware/base.rb +0 -5
  21. data/lib/grape/middleware/error.rb +1 -2
  22. data/lib/grape/middleware/formatter.rb +9 -5
  23. data/lib/grape/middleware/versioner.rb +1 -1
  24. data/lib/grape/middleware/versioner/header.rb +16 -6
  25. data/lib/grape/middleware/versioner/param.rb +1 -1
  26. data/lib/grape/middleware/versioner/path.rb +1 -1
  27. data/lib/grape/util/content_types.rb +0 -2
  28. data/lib/grape/validations.rb +7 -14
  29. data/lib/grape/validations/coerce.rb +2 -1
  30. data/lib/grape/validations/presence.rb +2 -1
  31. data/lib/grape/validations/regexp.rb +2 -1
  32. data/lib/grape/version.rb +1 -1
  33. data/spec/grape/api_spec.rb +150 -5
  34. data/spec/grape/endpoint_spec.rb +51 -157
  35. data/spec/grape/entity_spec.rb +142 -520
  36. data/spec/grape/exceptions/invalid_formatter_spec.rb +18 -0
  37. data/spec/grape/exceptions/invalid_versioner_option_spec.rb +18 -0
  38. data/spec/grape/exceptions/missing_mime_type_spec.rb +24 -0
  39. data/spec/grape/exceptions/missing_option_spec.rb +18 -0
  40. data/spec/grape/exceptions/unknown_options_spec.rb +18 -0
  41. data/spec/grape/exceptions/unknown_validator_spec.rb +18 -0
  42. data/spec/grape/middleware/formatter_spec.rb +40 -34
  43. data/spec/grape/middleware/versioner/header_spec.rb +78 -20
  44. data/spec/grape/middleware/versioner/path_spec.rb +12 -8
  45. data/spec/grape/validations/coerce_spec.rb +1 -0
  46. data/spec/grape/validations/presence_spec.rb +8 -8
  47. data/spec/grape/validations_spec.rb +26 -3
  48. data/spec/spec_helper.rb +3 -6
  49. metadata +44 -9
  50. data/lib/grape/entity.rb +0 -386
data/grape.gemspec CHANGED
@@ -18,13 +18,13 @@ Gem::Specification.new do |s|
18
18
  s.add_runtime_dependency 'rack-mount'
19
19
  s.add_runtime_dependency 'rack-accept'
20
20
  s.add_runtime_dependency 'activesupport'
21
- # s.add_runtime_dependency 'rack-jsonp'
22
21
  s.add_runtime_dependency 'multi_json', '>= 1.3.2'
23
22
  s.add_runtime_dependency 'multi_xml', '>= 0.5.2'
24
23
  s.add_runtime_dependency 'hashie', '~> 1.2'
25
24
  s.add_runtime_dependency 'virtus'
26
25
  s.add_runtime_dependency 'builder'
27
26
 
27
+ s.add_development_dependency 'grape-entity', '>= 0.2.0'
28
28
  s.add_development_dependency 'rake'
29
29
  s.add_development_dependency 'maruku'
30
30
  s.add_development_dependency 'yard'
data/lib/grape.rb CHANGED
@@ -1,17 +1,39 @@
1
+ require 'logger'
1
2
  require 'rack'
3
+ require 'rack/mount'
2
4
  require 'rack/builder'
5
+ require 'rack/accept'
6
+ require 'rack/auth/basic'
7
+ require 'rack/auth/digest/md5'
8
+ require 'hashie'
9
+ require 'active_support/all'
10
+ require 'grape/util/deep_merge'
11
+ require 'grape/util/content_types'
12
+ require 'multi_json'
13
+ require 'multi_xml'
14
+ require 'virtus'
15
+ require 'i18n'
16
+
17
+ I18n.load_path << File.expand_path('../grape/locale/en.yml', __FILE__)
3
18
 
4
19
  module Grape
5
20
  autoload :API, 'grape/api'
6
21
  autoload :Endpoint, 'grape/endpoint'
7
22
  autoload :Route, 'grape/route'
8
- autoload :Entity, 'grape/entity'
9
23
  autoload :Cookies, 'grape/cookies'
10
24
  autoload :Validations, 'grape/validations'
11
25
 
12
26
  module Exceptions
13
- autoload :Base, 'grape/exceptions/base'
14
- autoload :ValidationError, 'grape/exceptions/validation_error'
27
+ autoload :Base, 'grape/exceptions/base'
28
+ autoload :Validation, 'grape/exceptions/validation'
29
+ autoload :MissingVendorOption, 'grape/exceptions/missing_vendor_option'
30
+ autoload :MissingMimeType, 'grape/exceptions/missing_mime_type'
31
+ autoload :MissingOption, 'grape/exceptions/missing_option'
32
+ autoload :InvalidFormatter, 'grape/exceptions/invalid_formatter'
33
+ autoload :InvalidVersionerOption, 'grape/exceptions/invalid_versioner_option'
34
+ autoload :UnknownValidator, 'grape/exceptions/unknown_validator'
35
+ autoload :UnknownOptions, 'grape/exceptions/unknown_options'
36
+ autoload :InvalidWithOptionForRepresent, 'grape/exceptions/invalid_with_option_for_represent'
15
37
  end
16
38
 
17
39
  module ErrorFormatter
data/lib/grape/api.rb CHANGED
@@ -1,10 +1,3 @@
1
- require 'rack/mount'
2
- require 'rack/auth/basic'
3
- require 'rack/auth/digest/md5'
4
- require 'logger'
5
- require 'grape/util/deep_merge'
6
- require 'grape/util/content_types'
7
-
8
1
  module Grape
9
2
  # The API class is the primary entry point for
10
3
  # creating Grape APIs.Users should subclass this
@@ -73,12 +66,21 @@ module Grape
73
66
  settings.imbue(key, value)
74
67
  end
75
68
 
76
- # Define a root URL prefix for your entire
77
- # API.
69
+ # Define a root URL prefix for your entire API.
78
70
  def prefix(prefix = nil)
79
71
  prefix ? set(:root_prefix, prefix) : settings[:root_prefix]
80
72
  end
81
73
 
74
+ # Do not route HEAD requests to GET requests automatically
75
+ def do_not_route_head!
76
+ set(:do_not_route_head, true)
77
+ end
78
+
79
+ # Do not automatically route OPTIONS
80
+ def do_not_route_options!
81
+ set(:do_not_route_options, true)
82
+ end
83
+
82
84
  # Specify an API version.
83
85
  #
84
86
  # @example API with legacy support.
@@ -102,7 +104,7 @@ module Grape
102
104
  options ||= {}
103
105
  options = {:using => :path}.merge!(options)
104
106
 
105
- raise ArgumentError, "Must specify :vendor option." if options[:using] == :header && !options.has_key?(:vendor)
107
+ raise Grape::Exceptions::MissingVendorOption.new if options[:using] == :header && !options.has_key?(:vendor)
106
108
 
107
109
  @versions = versions | args
108
110
  nest(block) do
@@ -134,7 +136,7 @@ module Grape
134
136
  set(:default_error_formatter, Grape::ErrorFormatter::Base.formatter_for(new_format, {}))
135
137
  # define a single mime type
136
138
  mime_type = content_types[new_format.to_sym]
137
- raise "missing mime type for #{new_format}" unless mime_type
139
+ raise Grape::Exceptions::MissingMimeType.new(new_format) unless mime_type
138
140
  settings.imbue(:content_types, new_format.to_sym => mime_type)
139
141
  else
140
142
  settings[:format]
@@ -225,7 +227,7 @@ module Grape
225
227
  # @param model_class [Class] The model class that will be represented.
226
228
  # @option options [Class] :with The entity class that will represent the model.
227
229
  def represent(model_class, options)
228
- raise ArgumentError, "You must specify an entity class in the :with option." unless options[:with] && options[:with].is_a?(Class)
230
+ raise Grape::Exceptions::InvalidWithOptionForRepresent.new unless options[:with] && options[:with].is_a?(Class)
229
231
  imbue(:representations, model_class => options[:with])
230
232
  end
231
233
 
@@ -294,16 +296,19 @@ module Grape
294
296
  end
295
297
 
296
298
  def mount(mounts)
297
- mounts = {mounts => '/'} unless mounts.respond_to?(:each_pair)
299
+ mounts = { mounts => '/' } unless mounts.respond_to?(:each_pair)
298
300
  mounts.each_pair do |app, path|
299
301
  if app.respond_to?(:inherit_settings)
300
- app.inherit_settings(settings.clone)
302
+ app_settings = settings.clone
303
+ mount_path = Rack::Mount::Utils.normalize_path([ settings[:mount_path], path ].compact.join("/"))
304
+ app_settings.set :mount_path, mount_path
305
+ app.inherit_settings(app_settings)
301
306
  end
302
- endpoints << Grape::Endpoint.new(settings.clone,
307
+ endpoints << Grape::Endpoint.new(settings.clone, {
303
308
  :method => :any,
304
309
  :path => path,
305
310
  :app => app
306
- )
311
+ })
307
312
  end
308
313
  end
309
314
 
@@ -431,7 +436,7 @@ module Grape
431
436
  end
432
437
  end
433
438
 
434
- def inherited(subclass)
439
+ def inherited(subclass)
435
440
  subclass.reset!
436
441
  subclass.logger = logger.clone
437
442
  end
@@ -452,7 +457,22 @@ module Grape
452
457
  end
453
458
 
454
459
  def call(env)
455
- @route_set.call(env)
460
+ status, headers, body = @route_set.call(env)
461
+ headers.delete('X-Cascade') unless cascade?
462
+ [ status, headers, body ]
463
+ end
464
+
465
+ # Some requests may return a HTTP 404 error if grape cannot find a matching
466
+ # route. In this case, Rack::Mount adds a X-Cascade header to the response
467
+ # and sets it to 'pass', indicating to grape's parents they should keep
468
+ # looking for a matching route on other resources.
469
+ #
470
+ # In some applications (e.g. mounting grape on rails), one might need to trap
471
+ # errors from reaching upstream. This is effectivelly done by unsetting
472
+ # X-Cascade. Default :cascade is true.
473
+ def cascade?
474
+ cascade = ((self.class.settings || {})[:version_options] || {})[:cascade]
475
+ cascade.nil? ? true : cascade
456
476
  end
457
477
 
458
478
  reset!
@@ -473,16 +493,19 @@ module Grape
473
493
  resources.flatten.each do |route|
474
494
  allowed_methods[route.route_compiled] << route.route_method
475
495
  end
476
-
477
496
  allowed_methods.each do |path_info, methods|
497
+ if methods.include?('GET') && ! methods.include?("HEAD") && ! self.class.settings[:do_not_route_head]
498
+ methods = methods | [ 'HEAD' ]
499
+ end
478
500
  allow_header = (["OPTIONS"] | methods).join(", ")
479
- unless methods.include?("OPTIONS")
501
+ unless methods.include?("OPTIONS") || self.class.settings[:do_not_route_options]
480
502
  @route_set.add_route( proc { [204, { 'Allow' => allow_header }, []]}, {
481
503
  :path_info => path_info,
482
504
  :request_method => "OPTIONS"
483
505
  })
484
506
  end
485
507
  not_allowed_methods = %w(GET PUT POST DELETE PATCH HEAD) - methods
508
+ not_allowed_methods << "OPTIONS" if self.class.settings[:do_not_route_options]
486
509
  not_allowed_methods.each do |bad_method|
487
510
  @route_set.add_route( proc { [405, { 'Allow' => allow_header }, []]}, {
488
511
  :path_info => path_info,
@@ -1,7 +1,3 @@
1
- require 'rack'
2
- require 'grape'
3
- require 'hashie'
4
-
5
1
  module Grape
6
2
  # An Endpoint is the proxy scope in which all routing
7
3
  # blocks are executed. In other words, any methods
@@ -39,17 +35,22 @@ module Grape
39
35
  def initialize(settings, options = {}, &block)
40
36
  @settings = settings
41
37
  if block_given?
42
- method_name = "#{options[:method]} #{settings.gather(:namespace).join( "/")} #{Array(options[:path]).join("/")}"
38
+ method_name = [
39
+ options[:method],
40
+ settings.gather(:namespace).join("/"),
41
+ settings.gather(:mount_path).join("/"),
42
+ Array(options[:path]).join("/")
43
+ ].join(" ")
43
44
  @source = block
44
45
  @block = self.class.generate_api_method(method_name, &block)
45
46
  end
46
47
  @options = options
47
48
 
48
- raise ArgumentError, "Must specify :path option." unless options.key?(:path)
49
+ raise Grape::Exceptions::MissingOption.new(:path) unless options.key?(:path)
49
50
  options[:path] = Array(options[:path])
50
51
  options[:path] = ['/'] if options[:path].empty?
51
52
 
52
- raise ArgumentError, "Must specify :method option." unless options.key?(:method)
53
+ raise Grape::Exceptions::MissingOption.new(:method) unless options.key?(:method)
53
54
  options[:method] = Array(options[:method])
54
55
 
55
56
  options[:route_options] ||= {}
@@ -61,13 +62,19 @@ module Grape
61
62
 
62
63
  def mount_in(route_set)
63
64
  if endpoints
64
- endpoints.each{|e| e.mount_in(route_set)}
65
+ endpoints.each { |e| e.mount_in(route_set) }
65
66
  else
66
67
  routes.each do |route|
67
- route_set.add_route(self, {
68
- :path_info => route.route_compiled,
69
- :request_method => route.route_method,
70
- }, { :route_info => route })
68
+ methods = [ route.route_method ]
69
+ if ! settings[:do_not_route_head] && route.route_method == "GET"
70
+ methods << "HEAD"
71
+ end
72
+ methods.each do |method|
73
+ route_set.add_route(self, {
74
+ :path_info => route.route_compiled,
75
+ :request_method => method,
76
+ }, { :route_info => route })
77
+ end
71
78
  end
72
79
  end
73
80
  end
@@ -110,6 +117,7 @@ module Grape
110
117
 
111
118
  def prepare_path(path)
112
119
  parts = []
120
+ parts << settings[:mount_path].to_s.split("/") if settings[:mount_path]
113
121
  parts << settings[:root_prefix].to_s.split("/") if settings[:root_prefix]
114
122
 
115
123
  uses_path_versioning = settings[:version] && settings[:version_options][:using] == :path
@@ -117,7 +125,7 @@ module Grape
117
125
  path_is_empty = path && (path.to_s =~ /^\s*$/ || path.to_s == '/')
118
126
 
119
127
  parts << ':version' if uses_path_versioning
120
- if !uses_path_versioning || (!namespace_is_empty || !path_is_empty)
128
+ if ! uses_path_versioning || (! namespace_is_empty || ! path_is_empty)
121
129
  parts << namespace.to_s if namespace
122
130
  parts << path.to_s if path
123
131
  format_suffix = '(.:format)'
@@ -243,6 +251,17 @@ module Grape
243
251
  end
244
252
  end
245
253
 
254
+ # Retrieves all available request headers.
255
+ def headers
256
+ @headers ||= @env.dup.inject({}) { |h, (k, v)|
257
+ if k.start_with? 'HTTP_'
258
+ k = k[5..-1].gsub('_', '-').downcase.gsub(/^.|[-_\s]./) { |x| x.upcase }
259
+ h[k] = v
260
+ end
261
+ h
262
+ }
263
+ end
264
+
246
265
  # Set response content-type
247
266
  def content_type(val)
248
267
  header('Content-Type', val)
@@ -1,6 +1,10 @@
1
1
  module Grape
2
2
  module Exceptions
3
3
  class Base < StandardError
4
+
5
+ BASE_MESSAGES_KEY = 'grape.errors.messages'
6
+ BASE_ATTRIBUTES_KEY = 'grape.errors.attributes'
7
+
4
8
  attr_reader :status, :message, :headers
5
9
 
6
10
  def initialize(args = {})
@@ -11,7 +15,52 @@ module Grape
11
15
 
12
16
  def [](index)
13
17
  self.send(index)
14
- end
18
+ end
19
+
20
+ protected
21
+ # TODO: translate attribute first
22
+ # if BASE_ATTRIBUTES_KEY.key respond to a string message, then short_message is returned
23
+ # if BASE_ATTRIBUTES_KEY.key respond to a Hash, means it may have problem , summary and resolution
24
+ def compose_message(key, attributes = {} )
25
+ short_message = translate_message(key, attributes)
26
+ if short_message.is_a? Hash
27
+ @problem = problem(key, attributes)
28
+ @summary = summary(key, attributes)
29
+ @resolution = resolution(key, attributes)
30
+ [ ["Problem", @problem], ["Summary", @summary], ["Resolution", @resolution]].reduce("") do |message, detail_array|
31
+ message << "\n#{detail_array[0]}:\n #{detail_array[1]}" unless detail_array[1].blank?
32
+ message
33
+ end
34
+ else
35
+ short_message
36
+ end
37
+ end
38
+
39
+ def problem(key, attributes)
40
+ translate_message("#{key}.problem", attributes)
41
+ end
42
+
43
+ def summary(key, attributes)
44
+ translate_message("#{key}.summary", attributes)
45
+ end
46
+
47
+ def resolution(key, attributes)
48
+ translate_message("#{key}.resolution", attributes)
49
+ end
50
+
51
+
52
+ def translate_attribute(key, options = {})
53
+ translate("#{BASE_ATTRIBUTES_KEY}.#{key}", { :default => key }.merge(options))
54
+ end
55
+
56
+ def translate_message(key, options = {})
57
+ translate("#{BASE_MESSAGES_KEY}.#{key}", {:default => '' }.merge(options))
58
+ end
59
+
60
+ def translate(key, options = {})
61
+ ::I18n.translate(key, options)
62
+ end
63
+
15
64
  end
16
65
  end
17
66
  end
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+ module Grape
3
+ module Exceptions
4
+ class InvalidFormatter < Base
5
+
6
+ def initialize(klass, to_format)
7
+ super(:message => compose_message("invalid_formatter", :klass => klass, :to_format => to_format))
8
+ end
9
+
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+ module Grape
3
+ module Exceptions
4
+
5
+ class InvalidVersionerOption < Base
6
+
7
+ def initialize(strategy)
8
+ super(:message => compose_message("invalid_versioner_option", :strategy => strategy))
9
+ end
10
+
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+ module Grape
3
+ module Exceptions
4
+ class InvalidWithOptionForRepresent < Base
5
+
6
+ def initialize
7
+ super(:message => compose_message("invalid_with_option_for_represent"))
8
+ end
9
+
10
+ end
11
+
12
+ end
13
+
14
+ end
15
+
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+ module Grape
3
+ module Exceptions
4
+ class MissingMimeType < Base
5
+
6
+ def initialize(new_format)
7
+ super(:message => compose_message("missing_mime_type", :new_format => new_format))
8
+ end
9
+
10
+ end
11
+
12
+ end
13
+
14
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+ module Grape
3
+ module Exceptions
4
+ class MissingOption < Base
5
+
6
+ def initialize(option)
7
+ super(:message => compose_message("missing_option", :option => option))
8
+ end
9
+
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+ module Grape
3
+ module Exceptions
4
+ class MissingVendorOption < Base
5
+
6
+ def initialize
7
+ super(:message => compose_message("missing_vendor_option"))
8
+ end
9
+
10
+ end
11
+
12
+ end
13
+ end