grape 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
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)