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.
- data/{CHANGELOG.markdown → CHANGELOG.md} +21 -1
- data/Gemfile +1 -0
- data/{README.markdown → README.md} +178 -125
- data/grape.gemspec +1 -1
- data/lib/grape.rb +25 -3
- data/lib/grape/api.rb +43 -20
- data/lib/grape/endpoint.rb +32 -13
- data/lib/grape/exceptions/base.rb +50 -1
- data/lib/grape/exceptions/invalid_formatter.rb +13 -0
- data/lib/grape/exceptions/invalid_versioner_option.rb +14 -0
- data/lib/grape/exceptions/invalid_with_option_for_represent.rb +15 -0
- data/lib/grape/exceptions/missing_mime_type.rb +14 -0
- data/lib/grape/exceptions/missing_option.rb +13 -0
- data/lib/grape/exceptions/missing_vendor_option.rb +13 -0
- data/lib/grape/exceptions/unknown_options.rb +14 -0
- data/lib/grape/exceptions/unknown_validator.rb +12 -0
- data/lib/grape/exceptions/{validation_error.rb → validation.rb} +3 -1
- data/lib/grape/formatter/xml.rb +2 -1
- data/lib/grape/locale/en.yml +20 -0
- data/lib/grape/middleware/base.rb +0 -5
- data/lib/grape/middleware/error.rb +1 -2
- data/lib/grape/middleware/formatter.rb +9 -5
- data/lib/grape/middleware/versioner.rb +1 -1
- data/lib/grape/middleware/versioner/header.rb +16 -6
- data/lib/grape/middleware/versioner/param.rb +1 -1
- data/lib/grape/middleware/versioner/path.rb +1 -1
- data/lib/grape/util/content_types.rb +0 -2
- data/lib/grape/validations.rb +7 -14
- data/lib/grape/validations/coerce.rb +2 -1
- data/lib/grape/validations/presence.rb +2 -1
- data/lib/grape/validations/regexp.rb +2 -1
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api_spec.rb +150 -5
- data/spec/grape/endpoint_spec.rb +51 -157
- data/spec/grape/entity_spec.rb +142 -520
- data/spec/grape/exceptions/invalid_formatter_spec.rb +18 -0
- data/spec/grape/exceptions/invalid_versioner_option_spec.rb +18 -0
- data/spec/grape/exceptions/missing_mime_type_spec.rb +24 -0
- data/spec/grape/exceptions/missing_option_spec.rb +18 -0
- data/spec/grape/exceptions/unknown_options_spec.rb +18 -0
- data/spec/grape/exceptions/unknown_validator_spec.rb +18 -0
- data/spec/grape/middleware/formatter_spec.rb +40 -34
- data/spec/grape/middleware/versioner/header_spec.rb +78 -20
- data/spec/grape/middleware/versioner/path_spec.rb +12 -8
- data/spec/grape/validations/coerce_spec.rb +1 -0
- data/spec/grape/validations/presence_spec.rb +8 -8
- data/spec/grape/validations_spec.rb +26 -3
- data/spec/spec_helper.rb +3 -6
- metadata +44 -9
- 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,
|
14
|
-
autoload :
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
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,
|
data/lib/grape/endpoint.rb
CHANGED
@@ -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 =
|
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
|
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
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|