grape 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/README.md +7 -6
  4. data/UPGRADING.md +19 -0
  5. data/grape.gemspec +5 -5
  6. data/lib/grape/api/instance.rb +22 -58
  7. data/lib/grape/api.rb +2 -11
  8. data/lib/grape/dsl/desc.rb +27 -24
  9. data/lib/grape/dsl/inside_route.rb +12 -23
  10. data/lib/grape/dsl/parameters.rb +2 -2
  11. data/lib/grape/dsl/routing.rb +5 -12
  12. data/lib/grape/endpoint.rb +76 -79
  13. data/lib/grape/error_formatter/base.rb +51 -21
  14. data/lib/grape/error_formatter/json.rb +7 -24
  15. data/lib/grape/error_formatter/jsonapi.rb +7 -0
  16. data/lib/grape/error_formatter/serializable_hash.rb +7 -0
  17. data/lib/grape/error_formatter/txt.rb +13 -20
  18. data/lib/grape/error_formatter/xml.rb +3 -13
  19. data/lib/grape/error_formatter.rb +4 -12
  20. data/lib/grape/exceptions/base.rb +18 -30
  21. data/lib/grape/exceptions/validation.rb +5 -4
  22. data/lib/grape/exceptions/validation_errors.rb +2 -2
  23. data/lib/grape/formatter/base.rb +16 -0
  24. data/lib/grape/formatter/json.rb +4 -6
  25. data/lib/grape/formatter/serializable_hash.rb +1 -1
  26. data/lib/grape/formatter/txt.rb +3 -5
  27. data/lib/grape/formatter/xml.rb +4 -6
  28. data/lib/grape/formatter.rb +4 -12
  29. data/lib/grape/http/headers.rb +1 -0
  30. data/lib/grape/middleware/error.rb +2 -0
  31. data/lib/grape/middleware/formatter.rb +1 -1
  32. data/lib/grape/middleware/versioner/accept_version_header.rb +3 -3
  33. data/lib/grape/middleware/versioner/base.rb +82 -0
  34. data/lib/grape/middleware/versioner/header.rb +3 -9
  35. data/lib/grape/middleware/versioner/param.rb +0 -2
  36. data/lib/grape/middleware/versioner/path.rb +0 -2
  37. data/lib/grape/middleware/versioner.rb +5 -3
  38. data/lib/grape/namespace.rb +1 -1
  39. data/lib/grape/parser/base.rb +16 -0
  40. data/lib/grape/parser/json.rb +6 -8
  41. data/lib/grape/parser/jsonapi.rb +7 -0
  42. data/lib/grape/parser/xml.rb +6 -8
  43. data/lib/grape/parser.rb +5 -7
  44. data/lib/grape/path.rb +39 -56
  45. data/lib/grape/request.rb +2 -2
  46. data/lib/grape/router/base_route.rb +2 -2
  47. data/lib/grape/router/greedy_route.rb +2 -2
  48. data/lib/grape/router/pattern.rb +23 -18
  49. data/lib/grape/router/route.rb +13 -5
  50. data/lib/grape/router.rb +5 -5
  51. data/lib/grape/util/registry.rb +27 -0
  52. data/lib/grape/validations/contract_scope.rb +2 -39
  53. data/lib/grape/validations/params_scope.rb +7 -11
  54. data/lib/grape/validations/types/dry_type_coercer.rb +10 -6
  55. data/lib/grape/validations/validator_factory.rb +2 -2
  56. data/lib/grape/validations/validators/allow_blank_validator.rb +1 -1
  57. data/lib/grape/validations/validators/base.rb +5 -9
  58. data/lib/grape/validations/validators/coerce_validator.rb +1 -1
  59. data/lib/grape/validations/validators/contract_scope_validator.rb +41 -0
  60. data/lib/grape/validations/validators/default_validator.rb +1 -1
  61. data/lib/grape/validations/validators/except_values_validator.rb +1 -1
  62. data/lib/grape/validations/validators/length_validator.rb +1 -1
  63. data/lib/grape/validations/validators/regexp_validator.rb +1 -1
  64. data/lib/grape/validations/validators/values_validator.rb +15 -57
  65. data/lib/grape/validations.rb +8 -17
  66. data/lib/grape/version.rb +1 -1
  67. data/lib/grape.rb +1 -1
  68. metadata +14 -11
  69. data/lib/grape/middleware/versioner_helpers.rb +0 -75
  70. data/lib/grape/validations/types/build_coercer.rb +0 -92
@@ -2,24 +2,16 @@
2
2
 
3
3
  module Grape
4
4
  module Formatter
5
- module_function
5
+ extend Grape::Util::Registry
6
6
 
7
- DEFAULTS = {
8
- json: Grape::Formatter::Json,
9
- jsonapi: Grape::Formatter::Json,
10
- serializable_hash: Grape::Formatter::SerializableHash,
11
- txt: Grape::Formatter::Txt,
12
- xml: Grape::Formatter::Xml
13
- }.freeze
7
+ module_function
14
8
 
15
9
  DEFAULT_LAMBDA_FORMATTER = ->(obj, _env) { obj }
16
10
 
17
11
  def formatter_for(api_format, formatters)
18
- select_formatter(formatters, api_format) || DEFAULT_LAMBDA_FORMATTER
19
- end
12
+ return formatters[api_format] if formatters&.key?(api_format)
20
13
 
21
- def select_formatter(formatters, api_format)
22
- formatters&.key?(api_format) ? formatters[api_format] : DEFAULTS[api_format]
14
+ registry[api_format] || DEFAULT_LAMBDA_FORMATTER
23
15
  end
24
16
  end
25
17
  end
@@ -6,6 +6,7 @@ module Grape
6
6
  HTTP_ACCEPT_VERSION = 'HTTP_ACCEPT_VERSION'
7
7
  HTTP_ACCEPT = 'HTTP_ACCEPT'
8
8
  HTTP_TRANSFER_ENCODING = 'HTTP_TRANSFER_ENCODING'
9
+ HTTP_VERSION = 'HTTP_VERSION'
9
10
 
10
11
  ALLOW = 'Allow'
11
12
  LOCATION = 'Location'
@@ -65,6 +65,7 @@ module Grape
65
65
 
66
66
  def error_response(error = {})
67
67
  status = error[:status] || options[:default_status]
68
+ env[Grape::Env::API_ENDPOINT].status(status) # error! may not have been called
68
69
  message = error[:message] || options[:default_message]
69
70
  headers = { Rack::CONTENT_TYPE => content_type }.tap do |h|
70
71
  h.merge!(error[:headers]) if error[:headers].is_a?(Hash)
@@ -130,6 +131,7 @@ module Grape
130
131
  end
131
132
 
132
133
  def error!(message, status = options[:default_status], headers = {}, backtrace = [], original_exception = nil)
134
+ env[Grape::Env::API_ENDPOINT].status(status) # not error! inside route
133
135
  rack_response(
134
136
  status, headers.reverse_merge(Rack::CONTENT_TYPE => content_type),
135
137
  format_message(message, backtrace, original_exception)
@@ -53,7 +53,7 @@ module Grape
53
53
  end
54
54
 
55
55
  def fetch_formatter(headers, options)
56
- api_format = mime_types[headers[Rack::CONTENT_TYPE]] || env[Grape::Env::API_FORMAT]
56
+ api_format = env.fetch(Grape::Env::API_FORMAT) { mime_types[headers[Rack::CONTENT_TYPE]] }
57
57
  Grape::Formatter.formatter_for(api_format, options[:formatters])
58
58
  end
59
59
 
@@ -17,10 +17,10 @@ module Grape
17
17
  # X-Cascade header to alert Grape::Router to attempt the next matched
18
18
  # route.
19
19
  class AcceptVersionHeader < Base
20
- include VersionerHelpers
21
-
22
20
  def before
23
- potential_version = env[Grape::Http::Headers::HTTP_ACCEPT_VERSION]&.strip
21
+ potential_version = env[Grape::Http::Headers::HTTP_ACCEPT_VERSION]
22
+ potential_version = potential_version.scrub unless potential_version.nil?
23
+
24
24
  not_acceptable!('Accept-Version header must be set.') if strict? && potential_version.blank?
25
25
 
26
26
  return if potential_version.blank?
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Middleware
5
+ module Versioner
6
+ class Base < Grape::Middleware::Base
7
+ DEFAULT_PATTERN = /.*/i.freeze
8
+ DEFAULT_PARAMETER = 'apiver'
9
+
10
+ def self.inherited(klass)
11
+ super
12
+ Versioner.register(klass)
13
+ end
14
+
15
+ def default_options
16
+ {
17
+ versions: nil,
18
+ prefix: nil,
19
+ mount_path: nil,
20
+ pattern: DEFAULT_PATTERN,
21
+ version_options: {
22
+ strict: false,
23
+ cascade: true,
24
+ parameter: DEFAULT_PARAMETER
25
+ }
26
+ }
27
+ end
28
+
29
+ def versions
30
+ options[:versions]
31
+ end
32
+
33
+ def prefix
34
+ options[:prefix]
35
+ end
36
+
37
+ def mount_path
38
+ options[:mount_path]
39
+ end
40
+
41
+ def pattern
42
+ options[:pattern]
43
+ end
44
+
45
+ def version_options
46
+ options[:version_options]
47
+ end
48
+
49
+ def strict?
50
+ version_options[:strict]
51
+ end
52
+
53
+ # By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking
54
+ # of routes (see Grape::Router) for more information). To prevent
55
+ # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
56
+ def cascade?
57
+ version_options[:cascade]
58
+ end
59
+
60
+ def parameter_key
61
+ version_options[:parameter]
62
+ end
63
+
64
+ def vendor
65
+ version_options[:vendor]
66
+ end
67
+
68
+ def error_headers
69
+ cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {}
70
+ end
71
+
72
+ def potential_version_match?(potential_version)
73
+ versions.blank? || versions.any? { |v| v.to_s == potential_version }
74
+ end
75
+
76
+ def version_not_found!
77
+ throw :error, status: 404, message: '404 API Version Not Found', headers: { Grape::Http::Headers::X_CASCADE => 'pass' }
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -22,8 +22,6 @@ module Grape
22
22
  # X-Cascade header to alert Grape::Router to attempt the next matched
23
23
  # route.
24
24
  class Header < Base
25
- include VersionerHelpers
26
-
27
25
  def before
28
26
  match_best_quality_media_type! do |media_type|
29
27
  env.update(
@@ -46,14 +44,10 @@ module Grape
46
44
  if media_type
47
45
  yield media_type
48
46
  else
49
- fail!(allowed_methods)
47
+ fail!
50
48
  end
51
49
  end
52
50
 
53
- def allowed_methods
54
- env[Grape::Env::GRAPE_ALLOWED_METHODS]
55
- end
56
-
57
51
  def accept_header
58
52
  env[Grape::Http::Headers::HTTP_ACCEPT]
59
53
  end
@@ -93,8 +87,8 @@ module Grape
93
87
  raise Grape::Exceptions::InvalidVersionHeader.new(message, error_headers)
94
88
  end
95
89
 
96
- def fail!(grape_allowed_methods)
97
- return grape_allowed_methods if grape_allowed_methods.present?
90
+ def fail!
91
+ return if env[Grape::Env::GRAPE_ALLOWED_METHODS].present?
98
92
 
99
93
  media_types = q_values_mime_types.map { |mime_type| Grape::Util::MediaType.parse(mime_type) }
100
94
  vendor_not_found!(media_types) || version_not_found!(media_types)
@@ -19,8 +19,6 @@ module Grape
19
19
  #
20
20
  # env['api.version'] => 'v1'
21
21
  class Param < Base
22
- include VersionerHelpers
23
-
24
22
  def before
25
23
  potential_version = Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING])[parameter_key]
26
24
  return if potential_version.blank?
@@ -17,8 +17,6 @@ module Grape
17
17
  # env['api.version'] => 'v1'
18
18
  #
19
19
  class Path < Base
20
- include VersionerHelpers
21
-
22
20
  def before
23
21
  path_info = Grape::Router.normalize_path(env[Rack::PATH_INFO])
24
22
  return if path_info == '/'
@@ -11,14 +11,16 @@
11
11
  module Grape
12
12
  module Middleware
13
13
  module Versioner
14
+ extend Grape::Util::Registry
15
+
14
16
  module_function
15
17
 
16
18
  # @param strategy [Symbol] :path, :header, :accept_version_header or :param
17
19
  # @return a middleware class based on strategy
18
20
  def using(strategy)
19
- Grape::Middleware::Versioner.const_get(:"#{strategy.to_s.camelize}")
20
- rescue NameError
21
- raise Grape::Exceptions::InvalidVersionerOption, strategy
21
+ raise Grape::Exceptions::InvalidVersionerOption, strategy unless registry.key?(strategy)
22
+
23
+ registry[strategy]
22
24
  end
23
25
  end
24
26
  end
@@ -12,7 +12,7 @@ module Grape
12
12
  # @option options :requirements [Hash] param-regex pairs, all of which must
13
13
  # be met by a request's params for all endpoints in this namespace, or
14
14
  # validation will fail and return a 422.
15
- def initialize(space, **options)
15
+ def initialize(space, options)
16
16
  @space = space.to_s
17
17
  @options = options
18
18
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Parser
5
+ class Base
6
+ def self.call(_object, _env)
7
+ raise NotImplementedError
8
+ end
9
+
10
+ def self.inherited(klass)
11
+ super
12
+ Parser.register(klass)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -2,14 +2,12 @@
2
2
 
3
3
  module Grape
4
4
  module Parser
5
- module Json
6
- class << self
7
- def call(object, _env)
8
- ::Grape::Json.load(object)
9
- rescue ::Grape::Json::ParseError
10
- # handle JSON parsing errors via the rescue handlers or provide error message
11
- raise Grape::Exceptions::InvalidMessageBody.new('application/json')
12
- end
5
+ class Json < Base
6
+ def self.call(object, _env)
7
+ ::Grape::Json.load(object)
8
+ rescue ::Grape::Json::ParseError
9
+ # handle JSON parsing errors via the rescue handlers or provide error message
10
+ raise Grape::Exceptions::InvalidMessageBody.new('application/json')
13
11
  end
14
12
  end
15
13
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Parser
5
+ class Jsonapi < Json; end
6
+ end
7
+ end
@@ -2,14 +2,12 @@
2
2
 
3
3
  module Grape
4
4
  module Parser
5
- module Xml
6
- class << self
7
- def call(object, _env)
8
- ::Grape::Xml.parse(object)
9
- rescue ::Grape::Xml::ParseError
10
- # handle XML parsing errors via the rescue handlers or provide error message
11
- raise Grape::Exceptions::InvalidMessageBody.new('application/xml')
12
- end
5
+ class Xml < Base
6
+ def self.call(object, _env)
7
+ ::Grape::Xml.parse(object)
8
+ rescue ::Grape::Xml::ParseError
9
+ # handle XML parsing errors via the rescue handlers or provide error message
10
+ raise Grape::Exceptions::InvalidMessageBody.new('application/xml')
13
11
  end
14
12
  end
15
13
  end
data/lib/grape/parser.rb CHANGED
@@ -2,16 +2,14 @@
2
2
 
3
3
  module Grape
4
4
  module Parser
5
- module_function
5
+ extend Grape::Util::Registry
6
6
 
7
- DEFAULTS = {
8
- json: Grape::Parser::Json,
9
- jsonapi: Grape::Parser::Json,
10
- xml: Grape::Parser::Xml
11
- }.freeze
7
+ module_function
12
8
 
13
9
  def parser_for(format, parsers = nil)
14
- parsers&.key?(format) ? parsers[format] : DEFAULTS[format]
10
+ return parsers[format] if parsers&.key?(format)
11
+
12
+ registry[format]
15
13
  end
16
14
  end
17
15
  end
data/lib/grape/path.rb CHANGED
@@ -3,65 +3,66 @@
3
3
  module Grape
4
4
  # Represents a path to an endpoint.
5
5
  class Path
6
- attr_reader :raw_path, :namespace, :settings
6
+ DEFAULT_FORMAT_SEGMENT = '(/.:format)'
7
+ NO_VERSIONING_WITH_VALID_PATH_FORMAT_SEGMENT = '(.:format)'
8
+ VERSION_SEGMENT = ':version'
7
9
 
8
- def initialize(raw_path, namespace, settings)
9
- @raw_path = raw_path
10
- @namespace = namespace
11
- @settings = settings
12
- end
10
+ attr_reader :origin, :suffix
13
11
 
14
- def mount_path
15
- settings[:mount_path]
12
+ def initialize(raw_path, raw_namespace, settings)
13
+ @origin = PartsCache[build_parts(raw_path, raw_namespace, settings)]
14
+ @suffix = build_suffix(raw_path, raw_namespace, settings)
16
15
  end
17
16
 
18
- def root_prefix
19
- settings[:root_prefix]
17
+ def to_s
18
+ "#{origin}#{suffix}"
20
19
  end
21
20
 
22
- def uses_specific_format?
23
- return false unless settings.key?(:format) && settings.key?(:content_types)
21
+ private
24
22
 
25
- settings[:format] && Array(settings[:content_types]).size == 1
23
+ def build_suffix(raw_path, raw_namespace, settings)
24
+ if uses_specific_format?(settings)
25
+ "(.#{settings[:format]})"
26
+ elsif !uses_path_versioning?(settings) || (valid_part?(raw_namespace) || valid_part?(raw_path))
27
+ NO_VERSIONING_WITH_VALID_PATH_FORMAT_SEGMENT
28
+ else
29
+ DEFAULT_FORMAT_SEGMENT
30
+ end
26
31
  end
27
32
 
28
- def uses_path_versioning?
29
- return false unless settings.key?(:version) && settings[:version_options]&.key?(:using)
30
-
31
- settings[:version] && settings[:version_options][:using] == :path
33
+ def build_parts(raw_path, raw_namespace, settings)
34
+ [].tap do |parts|
35
+ add_part(parts, settings[:mount_path])
36
+ add_part(parts, settings[:root_prefix])
37
+ parts << VERSION_SEGMENT if uses_path_versioning?(settings)
38
+ add_part(parts, raw_namespace)
39
+ add_part(parts, raw_path)
40
+ end
32
41
  end
33
42
 
34
- def namespace?
35
- namespace&.match?(/^\S/) && not_slash?(namespace)
43
+ def add_part(parts, value)
44
+ parts << value if value && not_slash?(value)
36
45
  end
37
46
 
38
- def path?
39
- raw_path&.match?(/^\S/) && not_slash?(raw_path)
47
+ def not_slash?(value)
48
+ value != '/'
40
49
  end
41
50
 
42
- def suffix
43
- if uses_specific_format?
44
- "(.#{settings[:format]})"
45
- elsif !uses_path_versioning? || (namespace? || path?)
46
- '(.:format)'
47
- else
48
- '(/.:format)'
49
- end
50
- end
51
+ def uses_specific_format?(settings)
52
+ return false unless settings.key?(:format) && settings.key?(:content_types)
51
53
 
52
- def path
53
- PartsCache[parts]
54
+ settings[:format] && Array(settings[:content_types]).size == 1
54
55
  end
55
56
 
56
- def path_with_suffix
57
- "#{path}#{suffix}"
58
- end
57
+ def uses_path_versioning?(settings)
58
+ return false unless settings.key?(:version) && settings[:version_options]&.key?(:using)
59
59
 
60
- def to_s
61
- path_with_suffix
60
+ settings[:version] && settings[:version_options][:using] == :path
62
61
  end
63
62
 
64
- private
63
+ def valid_part?(part)
64
+ part&.match?(/^\S/) && not_slash?(part)
65
+ end
65
66
 
66
67
  class PartsCache < Grape::Util::Cache
67
68
  def initialize
@@ -71,23 +72,5 @@ module Grape
71
72
  end
72
73
  end
73
74
  end
74
-
75
- def parts
76
- [].tap do |parts|
77
- add_part(parts, mount_path)
78
- add_part(parts, root_prefix)
79
- parts << ':version' if uses_path_versioning?
80
- add_part(parts, namespace)
81
- add_part(parts, raw_path)
82
- end
83
- end
84
-
85
- def add_part(parts, value)
86
- parts << value if value && not_slash?(value)
87
- end
88
-
89
- def not_slash?(value)
90
- value != '/'
91
- end
92
75
  end
93
76
  end
data/lib/grape/request.rb CHANGED
@@ -6,8 +6,8 @@ module Grape
6
6
 
7
7
  alias rack_params params
8
8
 
9
- def initialize(env, **options)
10
- extend options[:build_params_with] || Grape.config.param_builder
9
+ def initialize(env, build_params_with: nil)
10
+ extend build_params_with || Grape.config.param_builder
11
11
  super(env)
12
12
  end
13
13
 
@@ -7,8 +7,8 @@ module Grape
7
7
 
8
8
  attr_reader :index, :pattern, :options
9
9
 
10
- def initialize(**options)
11
- @options = ActiveSupport::OrderedOptions.new.update(options)
10
+ def initialize(options)
11
+ @options = options.is_a?(ActiveSupport::OrderedOptions) ? options : ActiveSupport::OrderedOptions.new.update(options)
12
12
  end
13
13
 
14
14
  alias attributes options
@@ -6,9 +6,9 @@
6
6
  module Grape
7
7
  class Router
8
8
  class GreedyRoute < BaseRoute
9
- def initialize(pattern:, **options)
9
+ def initialize(pattern, options)
10
10
  @pattern = pattern
11
- super(**options)
11
+ super(options)
12
12
  end
13
13
 
14
14
  # Grape::Router:Route defines params as a function
@@ -9,14 +9,14 @@ module Grape
9
9
 
10
10
  attr_reader :origin, :path, :pattern, :to_regexp
11
11
 
12
- def_delegators :pattern, :named_captures, :params
12
+ def_delegators :pattern, :params
13
13
  def_delegators :to_regexp, :===
14
14
  alias match? ===
15
15
 
16
- def initialize(pattern, **options)
17
- @origin = pattern
18
- @path = build_path(pattern, anchor: options[:anchor], suffix: options[:suffix])
19
- @pattern = build_pattern(@path, options)
16
+ def initialize(origin, suffix, options)
17
+ @origin = origin
18
+ @path = build_path(origin, options[:anchor], suffix)
19
+ @pattern = build_pattern(@path, options[:params], options[:format], options[:version], options[:requirements])
20
20
  @to_regexp = @pattern.to_regexp
21
21
  end
22
22
 
@@ -28,30 +28,31 @@ module Grape
28
28
 
29
29
  private
30
30
 
31
- def build_pattern(path, options)
31
+ def build_pattern(path, params, format, version, requirements)
32
32
  Mustermann::Grape.new(
33
33
  path,
34
34
  uri_decode: true,
35
- params: options[:params],
36
- capture: extract_capture(**options)
35
+ params: params,
36
+ capture: extract_capture(format, version, requirements)
37
37
  )
38
38
  end
39
39
 
40
- def build_path(pattern, anchor: false, suffix: nil)
41
- PatternCache[[build_path_from_pattern(pattern, anchor: anchor), suffix]]
40
+ def build_path(pattern, anchor, suffix)
41
+ PatternCache[[build_path_from_pattern(pattern, anchor), suffix]]
42
42
  end
43
43
 
44
- def extract_capture(**options)
45
- sliced_options = options
46
- .slice(:format, :version)
47
- .delete_if { |_k, v| v.blank? }
48
- .transform_values { |v| Array.wrap(v).map(&:to_s) }
49
- return sliced_options if options[:requirements].blank?
44
+ def extract_capture(format, version, requirements)
45
+ capture = {}.tap do |h|
46
+ h[:format] = map_str(format) if format.present?
47
+ h[:version] = map_str(version) if version.present?
48
+ end
49
+
50
+ return capture if requirements.blank?
50
51
 
51
- options[:requirements].merge(sliced_options)
52
+ requirements.merge(capture)
52
53
  end
53
54
 
54
- def build_path_from_pattern(pattern, anchor: false)
55
+ def build_path_from_pattern(pattern, anchor)
55
56
  if pattern.end_with?('*path')
56
57
  pattern.dup.insert(pattern.rindex('/') + 1, '?')
57
58
  elsif anchor
@@ -63,6 +64,10 @@ module Grape
63
64
  end
64
65
  end
65
66
 
67
+ def map_str(value)
68
+ Array.wrap(value).map(&:to_s)
69
+ end
70
+
66
71
  class PatternCache < Grape::Util::Cache
67
72
  def initialize
68
73
  super
@@ -5,14 +5,22 @@ module Grape
5
5
  class Route < BaseRoute
6
6
  extend Forwardable
7
7
 
8
+ FORWARD_MATCH_METHOD = ->(input, pattern) { input.start_with?(pattern.origin) }
9
+ NON_FORWARD_MATCH_METHOD = ->(input, pattern) { pattern.match?(input) }
10
+
8
11
  attr_reader :app, :request_method
9
12
 
10
13
  def_delegators :pattern, :path, :origin
11
14
 
12
- def initialize(method, pattern, **options)
15
+ def initialize(method, origin, path, options)
13
16
  @request_method = upcase_method(method)
14
- @pattern = Grape::Router::Pattern.new(pattern, **options)
15
- super(**options)
17
+ @pattern = Grape::Router::Pattern.new(origin, path, options)
18
+ @match_function = options[:forward_match] ? FORWARD_MATCH_METHOD : NON_FORWARD_MATCH_METHOD
19
+ super(options)
20
+ end
21
+
22
+ def convert_to_head_request!
23
+ @request_method = Rack::HEAD
16
24
  end
17
25
 
18
26
  def exec(env)
@@ -27,7 +35,7 @@ module Grape
27
35
  def match?(input)
28
36
  return false if input.blank?
29
37
 
30
- options[:forward_match] ? input.start_with?(pattern.origin) : pattern.match?(input)
38
+ @match_function.call(input, pattern)
31
39
  end
32
40
 
33
41
  def params(input = nil)
@@ -42,7 +50,7 @@ module Grape
42
50
  private
43
51
 
44
52
  def params_without_input
45
- pattern.captures_default.merge(attributes.params)
53
+ @params_without_input ||= pattern.captures_default.merge(attributes.params)
46
54
  end
47
55
 
48
56
  def upcase_method(method)
data/lib/grape/router.rb CHANGED
@@ -38,8 +38,8 @@ module Grape
38
38
  map[route.request_method] << route
39
39
  end
40
40
 
41
- def associate_routes(pattern, **options)
42
- Grape::Router::GreedyRoute.new(pattern: pattern, **options).then do |greedy_route|
41
+ def associate_routes(pattern, options)
42
+ Grape::Router::GreedyRoute.new(pattern, options).then do |greedy_route|
43
43
  @neutral_regexes << greedy_route.to_regexp(@neutral_map.length)
44
44
  @neutral_map << greedy_route
45
45
  end
@@ -107,7 +107,7 @@ module Grape
107
107
 
108
108
  route = match?(input, '*')
109
109
 
110
- return last_neighbor_route.endpoint.call(env) if last_neighbor_route && last_response_cascade && route
110
+ return last_neighbor_route.options[:endpoint].call(env) if last_neighbor_route && last_response_cascade && route
111
111
 
112
112
  last_response_cascade = cascade_or_return_response.call(process_route(route, env)) if route
113
113
 
@@ -152,8 +152,8 @@ module Grape
152
152
 
153
153
  def call_with_allow_headers(env, route)
154
154
  prepare_env_from_route(env, route)
155
- env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.allow_header.join(', ').freeze
156
- route.endpoint.call(env)
155
+ env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.options[:allow_header]
156
+ route.options[:endpoint].call(env)
157
157
  end
158
158
 
159
159
  def prepare_env_from_route(env, route)