grape 0.15.0 → 0.16.1
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/CHANGELOG.md +16 -1
- data/Gemfile.lock +16 -15
- data/README.md +41 -47
- data/UPGRADING.md +62 -0
- data/gemfiles/rails_3.gemfile.lock +225 -0
- data/grape.gemspec +2 -2
- data/lib/grape.rb +31 -26
- data/lib/grape/api.rb +39 -23
- data/lib/grape/dsl/inside_route.rb +8 -4
- data/lib/grape/dsl/routing.rb +2 -1
- data/lib/grape/endpoint.rb +43 -62
- data/lib/grape/error_formatter.rb +4 -2
- data/lib/grape/error_formatter/base.rb +10 -6
- data/lib/grape/formatter.rb +4 -2
- data/lib/grape/http/headers.rb +1 -0
- data/lib/grape/middleware/formatter.rb +2 -2
- data/lib/grape/middleware/versioner/accept_version_header.rb +2 -2
- data/lib/grape/middleware/versioner/header.rb +2 -2
- data/lib/grape/middleware/versioner/path.rb +2 -2
- data/lib/grape/namespace.rb +1 -1
- data/lib/grape/parser.rb +4 -2
- data/lib/grape/path.rb +3 -3
- data/lib/grape/request.rb +2 -2
- data/lib/grape/router.rb +156 -0
- data/lib/grape/router/attribute_translator.rb +40 -0
- data/lib/grape/router/pattern.rb +55 -0
- data/lib/grape/router/route.rb +105 -0
- data/lib/grape/serve_file/file_body.rb +34 -0
- data/lib/grape/{util → serve_file}/file_response.rb +1 -1
- data/lib/grape/{util → serve_file}/sendfile_response.rb +1 -1
- data/lib/grape/util/env.rb +1 -1
- data/lib/grape/util/registrable.rb +13 -0
- data/lib/grape/validations/types/custom_type_coercer.rb +2 -0
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api/invalid_format_spec.rb +43 -0
- data/spec/grape/api/recognize_path_spec.rb +21 -0
- data/spec/grape/api/required_parameters_with_invalid_method_spec.rb +26 -0
- data/spec/grape/api_spec.rb +110 -38
- data/spec/grape/dsl/inside_route_spec.rb +267 -240
- data/spec/grape/endpoint_spec.rb +10 -0
- data/spec/grape/entity_spec.rb +2 -2
- data/spec/grape/middleware/formatter_spec.rb +23 -4
- data/spec/grape/middleware/versioner/header_spec.rb +1 -1
- data/spec/grape/middleware/versioner/path_spec.rb +1 -1
- data/spec/grape/parser_spec.rb +82 -0
- data/spec/grape/request_spec.rb +2 -2
- data/spec/grape/validations/params_scope_spec.rb +2 -2
- data/spec/grape/validations/validators/coerce_spec.rb +51 -0
- data/spec/grape/validations_spec.rb +1 -1
- data/tmp/Gemfile.lock +63 -0
- metadata +70 -55
- data/lib/grape/route.rb +0 -32
@@ -1,8 +1,10 @@
|
|
1
1
|
module Grape
|
2
2
|
module ErrorFormatter
|
3
|
+
extend Util::Registrable
|
4
|
+
|
3
5
|
class << self
|
4
6
|
def builtin_formatters
|
5
|
-
{
|
7
|
+
@builtin_formatters ||= {
|
6
8
|
serializable_hash: Grape::ErrorFormatter::Json,
|
7
9
|
json: Grape::ErrorFormatter::Json,
|
8
10
|
jsonapi: Grape::ErrorFormatter::Json,
|
@@ -12,7 +14,7 @@ module Grape
|
|
12
14
|
end
|
13
15
|
|
14
16
|
def formatters(options)
|
15
|
-
builtin_formatters.merge(options[:error_formatters] || {})
|
17
|
+
builtin_formatters.merge(default_elements).merge(options[:error_formatters] || {})
|
16
18
|
end
|
17
19
|
|
18
20
|
def formatter_for(api_format, options = {})
|
@@ -3,14 +3,18 @@ module Grape
|
|
3
3
|
module Base
|
4
4
|
def present(message, env)
|
5
5
|
present_options = {}
|
6
|
-
|
6
|
+
presented_message = message
|
7
|
+
if presented_message.is_a?(Hash)
|
8
|
+
presented_message = presented_message.dup
|
9
|
+
present_options[:with] = presented_message.delete(:with)
|
10
|
+
end
|
7
11
|
|
8
|
-
presenter = env[Grape::Env::API_ENDPOINT].entity_class_for_obj(
|
12
|
+
presenter = env[Grape::Env::API_ENDPOINT].entity_class_for_obj(presented_message, present_options)
|
9
13
|
|
10
|
-
unless presenter || env[Grape::Env::
|
14
|
+
unless presenter || env[Grape::Env::GRAPE_ROUTING_ARGS].nil?
|
11
15
|
# env['api.endpoint'].route does not work when the error occurs within a middleware
|
12
16
|
# the Endpoint does not have a valid env at this moment
|
13
|
-
http_codes = env[Grape::Env::
|
17
|
+
http_codes = env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info].http_codes || []
|
14
18
|
found_code = http_codes.find do |http_code|
|
15
19
|
(http_code[0].to_i == env[Grape::Env::API_ENDPOINT].status) && http_code[2].respond_to?(:represent)
|
16
20
|
end if env[Grape::Env::API_ENDPOINT].request
|
@@ -21,10 +25,10 @@ module Grape
|
|
21
25
|
if presenter
|
22
26
|
embeds = { env: env }
|
23
27
|
embeds[:version] = env[Grape::Env::API_VERSION] if env[Grape::Env::API_VERSION]
|
24
|
-
|
28
|
+
presented_message = presenter.represent(presented_message, embeds).serializable_hash
|
25
29
|
end
|
26
30
|
|
27
|
-
|
31
|
+
presented_message
|
28
32
|
end
|
29
33
|
end
|
30
34
|
end
|
data/lib/grape/formatter.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
module Grape
|
2
2
|
module Formatter
|
3
|
+
extend Util::Registrable
|
4
|
+
|
3
5
|
class << self
|
4
6
|
def builtin_formmaters
|
5
|
-
{
|
7
|
+
@builtin_formatters ||= {
|
6
8
|
json: Grape::Formatter::Json,
|
7
9
|
jsonapi: Grape::Formatter::Json,
|
8
10
|
serializable_hash: Grape::Formatter::SerializableHash,
|
@@ -12,7 +14,7 @@ module Grape
|
|
12
14
|
end
|
13
15
|
|
14
16
|
def formatters(options)
|
15
|
-
builtin_formmaters.merge(options[:formatters] || {})
|
17
|
+
builtin_formmaters.merge(default_elements).merge(options[:formatters] || {})
|
16
18
|
end
|
17
19
|
|
18
20
|
def formatter_for(api_format, options = {})
|
data/lib/grape/http/headers.rb
CHANGED
@@ -34,8 +34,8 @@ module Grape
|
|
34
34
|
def build_formatted_response(status, headers, bodies)
|
35
35
|
headers = ensure_content_type(headers)
|
36
36
|
|
37
|
-
if bodies.is_a?(Grape::
|
38
|
-
Grape::
|
37
|
+
if bodies.is_a?(Grape::ServeFile::FileResponse)
|
38
|
+
Grape::ServeFile::SendfileResponse.new([], status, headers) do |resp|
|
39
39
|
resp.body = bodies.file
|
40
40
|
end
|
41
41
|
else
|
@@ -14,7 +14,7 @@ module Grape
|
|
14
14
|
# env['api.version'] => 'v1'
|
15
15
|
#
|
16
16
|
# If version does not match this route, then a 406 is raised with
|
17
|
-
# X-Cascade header to alert
|
17
|
+
# X-Cascade header to alert Grape::Router to attempt the next matched
|
18
18
|
# route.
|
19
19
|
class AcceptVersionHeader < Base
|
20
20
|
def before
|
@@ -46,7 +46,7 @@ module Grape
|
|
46
46
|
end
|
47
47
|
|
48
48
|
# By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking
|
49
|
-
# of routes (see
|
49
|
+
# of routes (see Grape::Router) for more information). To prevent
|
50
50
|
# this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
|
51
51
|
def cascade?
|
52
52
|
if options[:version_options] && options[:version_options].key?(:cascade)
|
@@ -20,7 +20,7 @@ module Grape
|
|
20
20
|
# env['api.format] => 'json'
|
21
21
|
#
|
22
22
|
# If version does not match this route, then a 406 is raised with
|
23
|
-
# X-Cascade header to alert
|
23
|
+
# X-Cascade header to alert Grape::Router to attempt the next matched
|
24
24
|
# route.
|
25
25
|
class Header < Base
|
26
26
|
VENDOR_VERSION_HEADER_REGEX =
|
@@ -154,7 +154,7 @@ module Grape
|
|
154
154
|
|
155
155
|
# By default those errors contain an `X-Cascade` header set to `pass`,
|
156
156
|
# which allows nesting and stacking of routes
|
157
|
-
# (see
|
157
|
+
# (see Grape::Router for more
|
158
158
|
# information). To prevent # this behavior, and not add the `X-Cascade`
|
159
159
|
# header, one can set the `:cascade` option to `false`.
|
160
160
|
def cascade?
|
@@ -28,7 +28,7 @@ module Grape
|
|
28
28
|
|
29
29
|
if prefix && path.index(prefix) == 0
|
30
30
|
path.sub!(prefix, '')
|
31
|
-
path =
|
31
|
+
path = Grape::Router.normalize_path(path)
|
32
32
|
end
|
33
33
|
|
34
34
|
pieces = path.split('/')
|
@@ -41,7 +41,7 @@ module Grape
|
|
41
41
|
private
|
42
42
|
|
43
43
|
def prefix
|
44
|
-
|
44
|
+
Grape::Router.normalize_path(options[:prefix].to_s) if options[:prefix]
|
45
45
|
end
|
46
46
|
end
|
47
47
|
end
|
data/lib/grape/namespace.rb
CHANGED
@@ -29,7 +29,7 @@ module Grape
|
|
29
29
|
# Join the namespaces from a list of settings to create a path prefix.
|
30
30
|
# @param settings [Array] list of Grape::Util::InheritableSettings.
|
31
31
|
def self.joined_space_path(settings)
|
32
|
-
|
32
|
+
Grape::Router.normalize_path(joined_space(settings))
|
33
33
|
end
|
34
34
|
end
|
35
35
|
end
|
data/lib/grape/parser.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
module Grape
|
2
2
|
module Parser
|
3
|
+
extend Util::Registrable
|
4
|
+
|
3
5
|
class << self
|
4
6
|
def builtin_parsers
|
5
|
-
{
|
7
|
+
@builtin_parsers ||= {
|
6
8
|
json: Grape::Parser::Json,
|
7
9
|
jsonapi: Grape::Parser::Json,
|
8
10
|
xml: Grape::Parser::Xml
|
@@ -10,7 +12,7 @@ module Grape
|
|
10
12
|
end
|
11
13
|
|
12
14
|
def parsers(options)
|
13
|
-
builtin_parsers.merge(options[:parsers] || {})
|
15
|
+
builtin_parsers.merge(default_elements).merge(options[:parsers] || {})
|
14
16
|
end
|
15
17
|
|
16
18
|
def parser_for(api_format, options = {})
|
data/lib/grape/path.rb
CHANGED
@@ -2,7 +2,7 @@ module Grape
|
|
2
2
|
# Represents a path to an endpoint.
|
3
3
|
class Path
|
4
4
|
def self.prepare(raw_path, namespace, settings)
|
5
|
-
Path.new(raw_path, namespace, settings)
|
5
|
+
Path.new(raw_path, namespace, settings)
|
6
6
|
end
|
7
7
|
|
8
8
|
attr_reader :raw_path, :namespace, :settings
|
@@ -22,7 +22,7 @@ module Grape
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def uses_specific_format?
|
25
|
-
!!(settings[:format] && settings[:content_types].size == 1)
|
25
|
+
!!(settings[:format] && Array(settings[:content_types]).size == 1)
|
26
26
|
end
|
27
27
|
|
28
28
|
def uses_path_versioning?
|
@@ -48,7 +48,7 @@ module Grape
|
|
48
48
|
end
|
49
49
|
|
50
50
|
def path
|
51
|
-
|
51
|
+
Grape::Router.normalize_path(parts.join('/'))
|
52
52
|
end
|
53
53
|
|
54
54
|
def path_with_suffix
|
data/lib/grape/request.rb
CHANGED
@@ -16,8 +16,8 @@ module Grape
|
|
16
16
|
|
17
17
|
def build_params
|
18
18
|
params = Hashie::Mash.new(rack_params)
|
19
|
-
if env[Grape::Env::
|
20
|
-
args = env[Grape::Env::
|
19
|
+
if env[Grape::Env::GRAPE_ROUTING_ARGS]
|
20
|
+
args = env[Grape::Env::GRAPE_ROUTING_ARGS].dup
|
21
21
|
# preserve version from query string parameters
|
22
22
|
args.delete(:version)
|
23
23
|
args.delete(:route_info)
|
data/lib/grape/router.rb
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'grape/router/route'
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
class Router
|
5
|
+
attr_reader :map, :compiled
|
6
|
+
|
7
|
+
class Any < AttributeTranslator
|
8
|
+
def initialize(pattern, attributes = {})
|
9
|
+
@pattern = pattern
|
10
|
+
super(attributes)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@neutral_map = []
|
16
|
+
@map = Hash.new { |hash, key| hash[key] = [] }
|
17
|
+
@optimized_map = Hash.new { |hash, key| hash[key] = // }
|
18
|
+
end
|
19
|
+
|
20
|
+
def compile!
|
21
|
+
return if compiled
|
22
|
+
@union = Regexp.union(@neutral_map.map(&:regexp))
|
23
|
+
map.each do |method, routes|
|
24
|
+
@optimized_map[method] = routes.map.with_index do |route, index|
|
25
|
+
route.index = index
|
26
|
+
route.regexp = /(?<_#{index}>#{route.pattern.to_regexp})/
|
27
|
+
end
|
28
|
+
@optimized_map[method] = Regexp.union(@optimized_map[method])
|
29
|
+
end
|
30
|
+
@compiled = true
|
31
|
+
end
|
32
|
+
|
33
|
+
def append(route)
|
34
|
+
map[route.request_method.to_s.upcase] << route
|
35
|
+
end
|
36
|
+
|
37
|
+
def associate_routes(pattern, options = {})
|
38
|
+
regexp = /(?<_#{@neutral_map.length}>)#{pattern.to_regexp}/
|
39
|
+
@neutral_map << Any.new(pattern, options.merge(regexp: regexp, index: @neutral_map.length))
|
40
|
+
end
|
41
|
+
|
42
|
+
def call(env)
|
43
|
+
with_optimization do
|
44
|
+
identity(env) || rotation(env) { |route| route.exec(env) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def recognize_path(input)
|
49
|
+
any = with_optimization { greedy_match?(input) }
|
50
|
+
return if any == default_response
|
51
|
+
any.endpoint
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def identity(env)
|
57
|
+
transaction(env) do |input, method, routing_args|
|
58
|
+
route = match?(input, method)
|
59
|
+
if route
|
60
|
+
env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(routing_args, route, input)
|
61
|
+
route.exec(env)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def rotation(env)
|
67
|
+
transaction(env) do |input, method, routing_args|
|
68
|
+
response = nil
|
69
|
+
routes_for(method).each do |route|
|
70
|
+
next unless route.match?(input)
|
71
|
+
env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(routing_args, route, input)
|
72
|
+
response = yield(route)
|
73
|
+
break unless cascade?(response)
|
74
|
+
end
|
75
|
+
response
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def transaction(env)
|
80
|
+
input, method, routing_args = *extract_required_args(env)
|
81
|
+
response = yield(input, method, routing_args)
|
82
|
+
|
83
|
+
return response if response && !(cascade = cascade?(response))
|
84
|
+
neighbor = greedy_match?(input)
|
85
|
+
return unless neighbor
|
86
|
+
|
87
|
+
(!cascade && neighbor) ? method_not_allowed(env, neighbor.allow_header, neighbor.endpoint) : nil
|
88
|
+
end
|
89
|
+
|
90
|
+
def make_routing_args(default_args, route, input)
|
91
|
+
args = default_args || { route_info: route }
|
92
|
+
args.merge(route.params(input))
|
93
|
+
end
|
94
|
+
|
95
|
+
def extract_required_args(env)
|
96
|
+
input = string_for(env[Grape::Http::Headers::PATH_INFO])
|
97
|
+
method = env[Grape::Http::Headers::REQUEST_METHOD]
|
98
|
+
routing_args = env[Grape::Env::GRAPE_ROUTING_ARGS]
|
99
|
+
[input, method, routing_args]
|
100
|
+
end
|
101
|
+
|
102
|
+
def with_optimization
|
103
|
+
compile! unless compiled
|
104
|
+
yield || default_response
|
105
|
+
end
|
106
|
+
|
107
|
+
def default_response
|
108
|
+
[404, { Grape::Http::Headers::X_CASCADE => 'pass' }, ['404 Not Found']]
|
109
|
+
end
|
110
|
+
|
111
|
+
def match?(input, method)
|
112
|
+
current_regexp = @optimized_map[method]
|
113
|
+
return unless current_regexp.match(input)
|
114
|
+
last_match = Regexp.last_match
|
115
|
+
@map[method].detect { |route| last_match["_#{route.index}"] }
|
116
|
+
end
|
117
|
+
|
118
|
+
def greedy_match?(input)
|
119
|
+
return unless @union.match(input)
|
120
|
+
last_match = Regexp.last_match
|
121
|
+
@neutral_map.detect { |route| last_match["_#{route.index}"] }
|
122
|
+
end
|
123
|
+
|
124
|
+
def method_not_allowed(env, methods, endpoint)
|
125
|
+
current = endpoint.dup
|
126
|
+
current.instance_eval do
|
127
|
+
run_filters befores, :before
|
128
|
+
@method_not_allowed = true
|
129
|
+
@block = proc do
|
130
|
+
fail Grape::Exceptions::MethodNotAllowed, header.merge('Allow' => methods)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
current.call(env)
|
134
|
+
end
|
135
|
+
|
136
|
+
def cascade?(response)
|
137
|
+
response && response[1][Grape::Http::Headers::X_CASCADE] == 'pass'
|
138
|
+
end
|
139
|
+
|
140
|
+
def routes_for(method)
|
141
|
+
map[method] + map['ANY']
|
142
|
+
end
|
143
|
+
|
144
|
+
def string_for(input)
|
145
|
+
self.class.normalize_path(input)
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.normalize_path(path)
|
149
|
+
path = "/#{path}"
|
150
|
+
path.squeeze!('/')
|
151
|
+
path.sub!(%r{/+\Z}, '')
|
152
|
+
path = '/' if path == ''
|
153
|
+
path
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
module Grape
|
5
|
+
class Router
|
6
|
+
class AttributeTranslator < DelegateClass(OpenStruct)
|
7
|
+
def self.register(*attributes)
|
8
|
+
AttributeTranslator.supported_attributes.concat(attributes)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.supported_attributes
|
12
|
+
@supported_attributes ||= []
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(attributes = {})
|
16
|
+
ostruct = OpenStruct.new(attributes)
|
17
|
+
super ostruct
|
18
|
+
@attributes = attributes
|
19
|
+
self.class.supported_attributes.each do |name|
|
20
|
+
ostruct.send(:"#{name}=", nil) unless ostruct.respond_to?(name)
|
21
|
+
self.class.instance_eval do
|
22
|
+
define_method(name) { instance_variable_get(:"@#{name}") }
|
23
|
+
end if name == :format
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_h
|
28
|
+
@attributes.each_with_object({}) do |(key, _), attributes|
|
29
|
+
attributes[key.to_sym] = send(:"#{key}")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def accessor_available?(name)
|
36
|
+
respond_to?(name) && respond_to?(:"#{name}=")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'mustermann/grape'
|
3
|
+
|
4
|
+
module Grape
|
5
|
+
class Router
|
6
|
+
class Pattern
|
7
|
+
DEFAULT_PATTERN_OPTIONS = { uri_decode: true, type: :grape }.freeze
|
8
|
+
DEFAULT_SUPPORTED_CAPTURE = [:format, :version].freeze
|
9
|
+
|
10
|
+
attr_reader :origin, :path, :capture, :pattern
|
11
|
+
|
12
|
+
extend Forwardable
|
13
|
+
def_delegators :pattern, :named_captures, :params
|
14
|
+
def_delegators :@regexp, :===
|
15
|
+
alias_method :match?, :===
|
16
|
+
|
17
|
+
def initialize(pattern, options = {})
|
18
|
+
@origin = pattern
|
19
|
+
@path = build_path(pattern, options)
|
20
|
+
@capture = extract_capture(options)
|
21
|
+
@pattern = Mustermann.new(@path, pattern_options)
|
22
|
+
@regexp = to_regexp
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_regexp
|
26
|
+
@to_regexp ||= @pattern.to_regexp
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def pattern_options
|
32
|
+
options = DEFAULT_PATTERN_OPTIONS.dup
|
33
|
+
options.merge!(capture: capture) if capture.present?
|
34
|
+
options
|
35
|
+
end
|
36
|
+
|
37
|
+
def build_path(pattern, options = {})
|
38
|
+
pattern << '*path' unless options[:anchor] || pattern.end_with?('*path')
|
39
|
+
pattern + options[:suffix].to_s
|
40
|
+
end
|
41
|
+
|
42
|
+
def extract_capture(options = {})
|
43
|
+
requirements = {}.merge(options[:requirements])
|
44
|
+
supported_capture.each_with_object(requirements) do |field, capture|
|
45
|
+
option = Array(options[field])
|
46
|
+
capture[field] = option.map(&:to_s) if option.present?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def supported_capture
|
51
|
+
DEFAULT_SUPPORTED_CAPTURE
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|