grape 0.11.0 → 0.12.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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +23 -80
- data/.travis.yml +1 -1
- data/CHANGELOG.md +27 -0
- data/Gemfile +1 -1
- data/Guardfile +1 -1
- data/LICENSE +1 -1
- data/README.md +131 -30
- data/Rakefile +1 -1
- data/UPGRADING.md +110 -1
- data/gemfiles/rails_3.gemfile +1 -1
- data/gemfiles/rails_4.gemfile +1 -1
- data/grape.gemspec +4 -4
- data/lib/grape.rb +92 -62
- data/lib/grape/api.rb +10 -10
- data/lib/grape/cookies.rb +1 -1
- data/lib/grape/dsl/configuration.rb +7 -7
- data/lib/grape/dsl/helpers.rb +3 -3
- data/lib/grape/dsl/inside_route.rb +50 -21
- data/lib/grape/dsl/parameters.rb +25 -6
- data/lib/grape/dsl/request_response.rb +1 -1
- data/lib/grape/dsl/routing.rb +11 -10
- data/lib/grape/dsl/settings.rb +1 -1
- data/lib/grape/endpoint.rb +21 -19
- data/lib/grape/error_formatter/json.rb +1 -1
- data/lib/grape/exceptions/base.rb +1 -1
- data/lib/grape/exceptions/validation.rb +1 -1
- data/lib/grape/exceptions/validation_errors.rb +2 -2
- data/lib/grape/formatter/base.rb +1 -1
- data/lib/grape/formatter/json.rb +1 -1
- data/lib/grape/formatter/serializable_hash.rb +4 -4
- data/lib/grape/formatter/txt.rb +1 -1
- data/lib/grape/formatter/xml.rb +1 -1
- data/lib/grape/http/headers.rb +27 -0
- data/lib/grape/http/request.rb +1 -1
- data/lib/grape/middleware/error.rb +10 -4
- data/lib/grape/middleware/formatter.rb +13 -9
- data/lib/grape/middleware/globals.rb +2 -1
- data/lib/grape/middleware/versioner/accept_version_header.rb +2 -2
- data/lib/grape/middleware/versioner/header.rb +4 -4
- data/lib/grape/middleware/versioner/param.rb +2 -2
- data/lib/grape/middleware/versioner/path.rb +1 -1
- data/lib/grape/namespace.rb +2 -1
- data/lib/grape/parser/json.rb +1 -1
- data/lib/grape/parser/xml.rb +1 -1
- data/lib/grape/path.rb +3 -3
- data/lib/grape/presenters/presenter.rb +9 -0
- data/lib/grape/validations/params_scope.rb +3 -3
- data/lib/grape/validations/validators/allow_blank.rb +1 -1
- data/lib/grape/validations/validators/coerce.rb +6 -5
- data/lib/grape/validations/validators/default.rb +2 -2
- data/lib/grape/validations/validators/multiple_params_base.rb +1 -0
- data/lib/grape/validations/validators/regexp.rb +1 -1
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api/custom_validations_spec.rb +47 -0
- data/spec/grape/api/deeply_included_options_spec.rb +56 -0
- data/spec/grape/api_spec.rb +64 -42
- data/spec/grape/dsl/configuration_spec.rb +2 -2
- data/spec/grape/dsl/helpers_spec.rb +1 -1
- data/spec/grape/dsl/inside_route_spec.rb +75 -19
- data/spec/grape/dsl/parameters_spec.rb +59 -10
- data/spec/grape/dsl/request_response_spec.rb +62 -2
- data/spec/grape/dsl/routing_spec.rb +116 -18
- data/spec/grape/endpoint_spec.rb +57 -5
- data/spec/grape/entity_spec.rb +1 -1
- data/spec/grape/exceptions/body_parse_errors_spec.rb +5 -5
- data/spec/grape/exceptions/invalid_accept_header_spec.rb +32 -32
- data/spec/grape/exceptions/validation_errors_spec.rb +1 -1
- data/spec/grape/integration/rack_spec.rb +4 -3
- data/spec/grape/middleware/auth/strategies_spec.rb +2 -2
- data/spec/grape/middleware/base_spec.rb +2 -2
- data/spec/grape/middleware/error_spec.rb +1 -1
- data/spec/grape/middleware/exception_spec.rb +5 -5
- data/spec/grape/middleware/formatter_spec.rb +10 -10
- data/spec/grape/middleware/globals_spec.rb +27 -0
- data/spec/grape/middleware/versioner/accept_version_header_spec.rb +1 -1
- data/spec/grape/middleware/versioner/header_spec.rb +1 -1
- data/spec/grape/middleware/versioner/param_spec.rb +1 -1
- data/spec/grape/middleware/versioner/path_spec.rb +1 -1
- data/spec/grape/path_spec.rb +6 -4
- data/spec/grape/presenters/presenter_spec.rb +70 -0
- data/spec/grape/util/inheritable_values_spec.rb +1 -1
- data/spec/grape/util/stackable_values_spec.rb +1 -1
- data/spec/grape/util/strict_hash_configuration_spec.rb +1 -1
- data/spec/grape/validations/params_scope_spec.rb +64 -0
- data/spec/grape/validations/validators/allow_blank_spec.rb +10 -0
- data/spec/grape/validations/validators/coerce_spec.rb +48 -18
- data/spec/grape/validations/validators/default_spec.rb +110 -20
- data/spec/grape/validations/validators/presence_spec.rb +41 -3
- data/spec/grape/validations/validators/regexp_spec.rb +7 -2
- data/spec/grape/validations_spec.rb +20 -1
- data/spec/support/file_streamer.rb +11 -0
- data/spec/support/versioned_helpers.rb +1 -1
- metadata +14 -2
data/lib/grape/formatter/base.rb
CHANGED
data/lib/grape/formatter/json.rb
CHANGED
@@ -2,7 +2,7 @@ module Grape
|
|
2
2
|
module Formatter
|
3
3
|
module SerializableHash
|
4
4
|
class << self
|
5
|
-
def call(object,
|
5
|
+
def call(object, _env)
|
6
6
|
return object if object.is_a?(String)
|
7
7
|
return MultiJson.dump(serialize(object)) if serializable?(object)
|
8
8
|
return object.to_json if object.respond_to?(:to_json)
|
@@ -12,15 +12,15 @@ module Grape
|
|
12
12
|
private
|
13
13
|
|
14
14
|
def serializable?(object)
|
15
|
-
object.respond_to?(:serializable_hash) || object.
|
15
|
+
object.respond_to?(:serializable_hash) || object.is_a?(Array) && !object.map { |o| o.respond_to? :serializable_hash }.include?(false) || object.is_a?(Hash)
|
16
16
|
end
|
17
17
|
|
18
18
|
def serialize(object)
|
19
19
|
if object.respond_to? :serializable_hash
|
20
20
|
object.serializable_hash
|
21
|
-
elsif object.
|
21
|
+
elsif object.is_a?(Array) && !object.map { |o| o.respond_to? :serializable_hash }.include?(false)
|
22
22
|
object.map(&:serializable_hash)
|
23
|
-
elsif object.
|
23
|
+
elsif object.is_a?(Hash)
|
24
24
|
object.inject({}) do |h, (k, v)|
|
25
25
|
h[k] = serialize(v)
|
26
26
|
h
|
data/lib/grape/formatter/txt.rb
CHANGED
data/lib/grape/formatter/xml.rb
CHANGED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Grape
|
2
|
+
module Http
|
3
|
+
module Headers
|
4
|
+
# https://github.com/rack/rack/blob/master/lib/rack.rb
|
5
|
+
HTTP_VERSION = 'HTTP_VERSION'.freeze
|
6
|
+
PATH_INFO = 'PATH_INFO'.freeze
|
7
|
+
QUERY_STRING = 'QUERY_STRING'.freeze
|
8
|
+
CONTENT_TYPE = 'Content-Type'.freeze
|
9
|
+
|
10
|
+
GET = 'GET'.freeze
|
11
|
+
POST = 'POST'.freeze
|
12
|
+
PUT = 'PUT'.freeze
|
13
|
+
PATCH = 'PATCH'.freeze
|
14
|
+
DELETE = 'DELETE'.freeze
|
15
|
+
HEAD = 'HEAD'.freeze
|
16
|
+
OPTIONS = 'OPTIONS'.freeze
|
17
|
+
|
18
|
+
HTTP_ACCEPT_VERSION = 'HTTP_ACCEPT_VERSION'.freeze
|
19
|
+
X_CASCADE = 'X-Cascade'.freeze
|
20
|
+
HTTP_TRANSFER_ENCODING = 'HTTP_TRANSFER_ENCODING'.freeze
|
21
|
+
HTTP_ACCEPT = 'HTTP_ACCEPT'.freeze
|
22
|
+
|
23
|
+
ACCEPT = 'accept'.freeze
|
24
|
+
FORMAT = 'format'.freeze
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/grape/http/request.rb
CHANGED
@@ -17,7 +17,7 @@ module Grape
|
|
17
17
|
def headers
|
18
18
|
@headers ||= env.dup.inject({}) do |h, (k, v)|
|
19
19
|
if k.to_s.start_with? 'HTTP_'
|
20
|
-
k = k[5..-1].
|
20
|
+
k = k[5..-1].tr('_', '-').downcase.gsub(/^.|[-_\s]./) { |x| x.upcase }
|
21
21
|
h[k] = v
|
22
22
|
end
|
23
23
|
h
|
@@ -29,7 +29,7 @@ module Grape
|
|
29
29
|
rescue StandardError => e
|
30
30
|
is_rescuable = rescuable?(e.class)
|
31
31
|
if e.is_a?(Grape::Exceptions::Base) && !is_rescuable
|
32
|
-
handler =
|
32
|
+
handler = ->(arg) { error_response(arg) }
|
33
33
|
else
|
34
34
|
raise unless is_rescuable
|
35
35
|
handler = find_handler(e.class)
|
@@ -47,7 +47,7 @@ module Grape
|
|
47
47
|
end
|
48
48
|
|
49
49
|
def rescuable?(klass)
|
50
|
-
options[:rescue_all] || (options[:rescue_handlers] || []).any? { |error,
|
50
|
+
options[:rescue_all] || (options[:rescue_handlers] || []).any? { |error, _handler| klass <= error } || (options[:base_only_rescue_handlers] || []).include?(klass)
|
51
51
|
end
|
52
52
|
|
53
53
|
def exec_handler(e, &handler)
|
@@ -58,20 +58,26 @@ module Grape
|
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
61
|
+
def error!(message, status = options[:default_status], headers = {}, backtrace = [])
|
62
|
+
headers = { Grape::Http::Headers::CONTENT_TYPE => content_type }.merge(headers)
|
63
|
+
rack_response(format_message(message, backtrace), status, headers)
|
64
|
+
end
|
65
|
+
|
61
66
|
def handle_error(e)
|
62
67
|
error_response(message: e.message, backtrace: e.backtrace)
|
63
68
|
end
|
64
69
|
|
70
|
+
# TODO: This method is deprecated. Refactor out.
|
65
71
|
def error_response(error = {})
|
66
72
|
status = error[:status] || options[:default_status]
|
67
73
|
message = error[:message] || options[:default_message]
|
68
|
-
headers = {
|
74
|
+
headers = { Grape::Http::Headers::CONTENT_TYPE => content_type }
|
69
75
|
headers.merge!(error[:headers]) if error[:headers].is_a?(Hash)
|
70
76
|
backtrace = error[:backtrace] || []
|
71
77
|
rack_response(format_message(message, backtrace), status, headers)
|
72
78
|
end
|
73
79
|
|
74
|
-
def rack_response(message, status = options[:default_status], headers = {
|
80
|
+
def rack_response(message, status = options[:default_status], headers = { Grape::Http::Headers::CONTENT_TYPE => content_type })
|
75
81
|
Rack::Response.new([message], status, headers).finish
|
76
82
|
end
|
77
83
|
|
@@ -26,17 +26,21 @@ module Grape
|
|
26
26
|
def after
|
27
27
|
status, headers, bodies = *@app_response
|
28
28
|
# allow content-type to be explicitly overwritten
|
29
|
-
api_format = mime_types[headers[
|
29
|
+
api_format = mime_types[headers[Grape::Http::Headers::CONTENT_TYPE]] || env['api.format']
|
30
30
|
formatter = Grape::Formatter::Base.formatter_for api_format, options
|
31
31
|
begin
|
32
|
-
bodymap = bodies.collect
|
33
|
-
|
34
|
-
|
32
|
+
bodymap = if bodies.respond_to?(:collect)
|
33
|
+
bodies.collect do |body|
|
34
|
+
formatter.call body, env
|
35
|
+
end
|
36
|
+
else
|
37
|
+
bodies
|
38
|
+
end
|
35
39
|
rescue Grape::Exceptions::InvalidFormatter => e
|
36
40
|
throw :error, status: 500, message: e.message
|
37
41
|
end
|
38
|
-
headers[
|
39
|
-
Rack::Response.new(bodymap, status, headers)
|
42
|
+
headers[Grape::Http::Headers::CONTENT_TYPE] = content_type_for(env['api.format']) unless headers[Grape::Http::Headers::CONTENT_TYPE]
|
43
|
+
Rack::Response.new(bodymap, status, headers)
|
40
44
|
end
|
41
45
|
|
42
46
|
private
|
@@ -50,7 +54,7 @@ module Grape
|
|
50
54
|
if (request.post? || request.put? || request.patch? || request.delete?) &&
|
51
55
|
(!request.form_data? || !request.media_type) &&
|
52
56
|
(!request.parseable_data?) &&
|
53
|
-
(request.content_length.to_i > 0 || request.env[
|
57
|
+
(request.content_length.to_i > 0 || request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == 'chunked')
|
54
58
|
|
55
59
|
if (input = env['rack.input'])
|
56
60
|
input.rewind
|
@@ -115,7 +119,7 @@ module Grape
|
|
115
119
|
end
|
116
120
|
|
117
121
|
def format_from_params
|
118
|
-
fmt = Rack::Utils.parse_nested_query(env[
|
122
|
+
fmt = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[Grape::Http::Headers::FORMAT]
|
119
123
|
# avoid symbol memory leak on an unknown format
|
120
124
|
return fmt.to_sym if content_type_for(fmt)
|
121
125
|
fmt
|
@@ -129,7 +133,7 @@ module Grape
|
|
129
133
|
end
|
130
134
|
|
131
135
|
def mime_array
|
132
|
-
accept = headers[
|
136
|
+
accept = headers[Grape::Http::Headers::ACCEPT]
|
133
137
|
return [] unless accept
|
134
138
|
|
135
139
|
accept_into_mime_and_quality = %r{
|
@@ -4,7 +4,8 @@ module Grape
|
|
4
4
|
module Middleware
|
5
5
|
class Globals < Base
|
6
6
|
def before
|
7
|
-
|
7
|
+
request = Grape::Request.new(@env)
|
8
|
+
@env['grape.request'] = request
|
8
9
|
@env['grape.request.headers'] = request.headers
|
9
10
|
@env['grape.request.params'] = request.params if @env['rack.input']
|
10
11
|
end
|
@@ -18,7 +18,7 @@ module Grape
|
|
18
18
|
# route.
|
19
19
|
class AcceptVersionHeader < Base
|
20
20
|
def before
|
21
|
-
potential_version = (env[
|
21
|
+
potential_version = (env[Grape::Http::Headers::HTTP_ACCEPT_VERSION] || '').strip
|
22
22
|
|
23
23
|
if strict?
|
24
24
|
# If no Accept-Version header:
|
@@ -59,7 +59,7 @@ module Grape
|
|
59
59
|
end
|
60
60
|
|
61
61
|
def error_headers
|
62
|
-
cascade? ? {
|
62
|
+
cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {}
|
63
63
|
end
|
64
64
|
end
|
65
65
|
end
|
@@ -67,8 +67,8 @@ module Grape
|
|
67
67
|
def available_media_types
|
68
68
|
available_media_types = []
|
69
69
|
|
70
|
-
content_types.each do |extension,
|
71
|
-
versions.
|
70
|
+
content_types.each do |extension, _media_type|
|
71
|
+
versions.reverse_each do |version|
|
72
72
|
available_media_types += ["application/vnd.#{vendor}-#{version}+#{extension}", "application/vnd.#{vendor}-#{version}"]
|
73
73
|
end
|
74
74
|
available_media_types << "application/vnd.#{vendor}+#{extension}"
|
@@ -84,7 +84,7 @@ module Grape
|
|
84
84
|
end
|
85
85
|
|
86
86
|
def rack_accept_header
|
87
|
-
Rack::Accept::MediaType.new env[
|
87
|
+
Rack::Accept::MediaType.new env[Grape::Http::Headers::HTTP_ACCEPT]
|
88
88
|
rescue RuntimeError => e
|
89
89
|
raise Grape::Exceptions::InvalidAcceptHeader.new(e.message, error_headers)
|
90
90
|
end
|
@@ -113,7 +113,7 @@ module Grape
|
|
113
113
|
end
|
114
114
|
|
115
115
|
def error_headers
|
116
|
-
cascade? ? {
|
116
|
+
cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {}
|
117
117
|
end
|
118
118
|
|
119
119
|
# @param [String] media_type a content type
|
@@ -27,10 +27,10 @@ module Grape
|
|
27
27
|
|
28
28
|
def before
|
29
29
|
paramkey = options[:parameter]
|
30
|
-
potential_version = Rack::Utils.parse_nested_query(env[
|
30
|
+
potential_version = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[paramkey]
|
31
31
|
unless potential_version.nil?
|
32
32
|
if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
|
33
|
-
throw :error, status: 404, message: '404 API Version Not Found', headers: {
|
33
|
+
throw :error, status: 404, message: '404 API Version Not Found', headers: { Grape::Http::Headers::X_CASCADE => 'pass' }
|
34
34
|
end
|
35
35
|
env['api.version'] = potential_version
|
36
36
|
env['rack.request.query_hash'].delete(paramkey) if env.key? 'rack.request.query_hash'
|
data/lib/grape/namespace.rb
CHANGED
data/lib/grape/parser/json.rb
CHANGED
data/lib/grape/parser/xml.rb
CHANGED
data/lib/grape/path.rb
CHANGED
@@ -17,7 +17,7 @@ module Grape
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def root_prefix
|
20
|
-
split_setting(:root_prefix
|
20
|
+
split_setting(:root_prefix)
|
21
21
|
end
|
22
22
|
|
23
23
|
def uses_specific_format?
|
@@ -38,7 +38,7 @@ module Grape
|
|
38
38
|
|
39
39
|
def suffix
|
40
40
|
if uses_specific_format?
|
41
|
-
|
41
|
+
"(.#{settings[:format]})"
|
42
42
|
elsif !uses_path_versioning? || (has_namespace? || has_path?)
|
43
43
|
'(.:format)'
|
44
44
|
else
|
@@ -68,7 +68,7 @@ module Grape
|
|
68
68
|
parts.flatten.reject { |part| part == '/' }
|
69
69
|
end
|
70
70
|
|
71
|
-
def split_setting(key
|
71
|
+
def split_setting(key)
|
72
72
|
return if settings[key].nil?
|
73
73
|
settings[key].to_s.split('/')
|
74
74
|
end
|
@@ -117,7 +117,7 @@ module Grape
|
|
117
117
|
doc_attrs[:desc] = desc if desc
|
118
118
|
|
119
119
|
default = validations[:default]
|
120
|
-
doc_attrs[:default] = default if default
|
120
|
+
doc_attrs[:default] = default if validations.key?(:default)
|
121
121
|
|
122
122
|
values = validations[:values]
|
123
123
|
doc_attrs[:values] = values if values
|
@@ -181,9 +181,9 @@ module Grape
|
|
181
181
|
def validate_value_coercion(coerce_type, values)
|
182
182
|
return unless coerce_type && values
|
183
183
|
return if values.is_a?(Proc)
|
184
|
-
coerce_type = coerce_type.first if coerce_type.
|
184
|
+
coerce_type = coerce_type.first if coerce_type.is_a?(Array)
|
185
185
|
value_types = values.is_a?(Range) ? [values.begin, values.end] : values
|
186
|
-
if value_types.any? { |v| !v.
|
186
|
+
if value_types.any? { |v| !v.is_a?(coerce_type) }
|
187
187
|
fail Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values)
|
188
188
|
end
|
189
189
|
end
|
@@ -32,7 +32,7 @@ module Grape
|
|
32
32
|
val.is_a?(TrueClass) || val.is_a?(FalseClass) || (val.is_a?(String) && val.empty?)
|
33
33
|
elsif klass == Rack::Multipart::UploadedFile
|
34
34
|
val.is_a?(Hashie::Mash) && val.key?(:tempfile)
|
35
|
-
elsif [DateTime, Date, Numeric].any?{ |vclass| vclass >= klass }
|
35
|
+
elsif [DateTime, Date, Numeric].any? { |vclass| vclass >= klass }
|
36
36
|
return true if val.is_a?(String) && val.empty?
|
37
37
|
val.is_a?(klass)
|
38
38
|
else
|
@@ -41,8 +41,8 @@ module Grape
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def valid_type?(val)
|
44
|
-
if @option.is_a?(Array)
|
45
|
-
_valid_array_type?(@option
|
44
|
+
if @option.is_a?(Array) || @option.is_a?(Set)
|
45
|
+
_valid_array_type?(@option.first, val)
|
46
46
|
else
|
47
47
|
_valid_single_type?(@option, val)
|
48
48
|
end
|
@@ -50,8 +50,9 @@ module Grape
|
|
50
50
|
|
51
51
|
def coerce_value(type, val)
|
52
52
|
# Don't coerce things other than nil to Arrays or Hashes
|
53
|
-
return val || []
|
54
|
-
return val ||
|
53
|
+
return val || [] if type == Array
|
54
|
+
return val || Set.new if type == Set
|
55
|
+
return val || {} if type == Hash
|
55
56
|
|
56
57
|
converter = Virtus::Attribute.build(type)
|
57
58
|
converter.coerce(val)
|
@@ -11,12 +11,12 @@ module Grape
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def validate!(params)
|
14
|
+
return unless @scope.should_validate?(params)
|
15
|
+
|
14
16
|
attrs = AttributesIterator.new(self, @scope, params)
|
15
|
-
parent_element = @scope.element
|
16
17
|
attrs.each do |resource_params, attr_name|
|
17
18
|
if resource_params[attr_name].nil?
|
18
19
|
validate_param!(attr_name, resource_params)
|
19
|
-
params[parent_element] = resource_params if parent_element && params[parent_element].nil?
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|