grape 1.3.0 → 1.5.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +119 -1
  3. data/LICENSE +1 -1
  4. data/README.md +123 -29
  5. data/UPGRADING.md +265 -39
  6. data/lib/grape/api/instance.rb +32 -31
  7. data/lib/grape/api.rb +5 -5
  8. data/lib/grape/content_types.rb +34 -0
  9. data/lib/grape/dsl/callbacks.rb +1 -1
  10. data/lib/grape/dsl/helpers.rb +2 -1
  11. data/lib/grape/dsl/inside_route.rb +77 -43
  12. data/lib/grape/dsl/parameters.rb +12 -8
  13. data/lib/grape/dsl/routing.rb +12 -11
  14. data/lib/grape/dsl/validations.rb +18 -1
  15. data/lib/grape/eager_load.rb +1 -1
  16. data/lib/grape/endpoint.rb +8 -6
  17. data/lib/grape/exceptions/base.rb +0 -4
  18. data/lib/grape/exceptions/validation.rb +1 -1
  19. data/lib/grape/exceptions/validation_errors.rb +12 -13
  20. data/lib/grape/http/headers.rb +26 -0
  21. data/lib/grape/middleware/auth/base.rb +3 -3
  22. data/lib/grape/middleware/base.rb +4 -5
  23. data/lib/grape/middleware/error.rb +11 -13
  24. data/lib/grape/middleware/formatter.rb +3 -3
  25. data/lib/grape/middleware/stack.rb +10 -2
  26. data/lib/grape/middleware/versioner/header.rb +4 -4
  27. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +2 -1
  28. data/lib/grape/middleware/versioner/path.rb +1 -1
  29. data/lib/grape/namespace.rb +12 -2
  30. data/lib/grape/path.rb +13 -3
  31. data/lib/grape/request.rb +13 -8
  32. data/lib/grape/router/attribute_translator.rb +26 -5
  33. data/lib/grape/router/pattern.rb +17 -16
  34. data/lib/grape/router/route.rb +5 -24
  35. data/lib/grape/router.rb +26 -30
  36. data/lib/grape/{serve_file → serve_stream}/file_body.rb +1 -1
  37. data/lib/grape/{serve_file → serve_stream}/sendfile_response.rb +1 -1
  38. data/lib/grape/{serve_file/file_response.rb → serve_stream/stream_response.rb} +8 -8
  39. data/lib/grape/util/base_inheritable.rb +15 -8
  40. data/lib/grape/util/cache.rb +20 -0
  41. data/lib/grape/util/lazy_object.rb +43 -0
  42. data/lib/grape/util/lazy_value.rb +1 -0
  43. data/lib/grape/util/reverse_stackable_values.rb +2 -0
  44. data/lib/grape/util/stackable_values.rb +7 -20
  45. data/lib/grape/validations/attributes_iterator.rb +8 -0
  46. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  47. data/lib/grape/validations/params_scope.rb +10 -8
  48. data/lib/grape/validations/single_attribute_iterator.rb +1 -1
  49. data/lib/grape/validations/types/array_coercer.rb +14 -5
  50. data/lib/grape/validations/types/build_coercer.rb +5 -8
  51. data/lib/grape/validations/types/custom_type_coercer.rb +16 -2
  52. data/lib/grape/validations/types/dry_type_coercer.rb +36 -1
  53. data/lib/grape/validations/types/file.rb +15 -12
  54. data/lib/grape/validations/types/invalid_value.rb +24 -0
  55. data/lib/grape/validations/types/json.rb +40 -36
  56. data/lib/grape/validations/types/primitive_coercer.rb +15 -6
  57. data/lib/grape/validations/types/set_coercer.rb +6 -4
  58. data/lib/grape/validations/types/variant_collection_coercer.rb +1 -1
  59. data/lib/grape/validations/types.rb +7 -9
  60. data/lib/grape/validations/validator_factory.rb +1 -1
  61. data/lib/grape/validations/validators/as.rb +1 -1
  62. data/lib/grape/validations/validators/base.rb +8 -8
  63. data/lib/grape/validations/validators/coerce.rb +11 -15
  64. data/lib/grape/validations/validators/default.rb +3 -5
  65. data/lib/grape/validations/validators/exactly_one_of.rb +4 -2
  66. data/lib/grape/validations/validators/except_values.rb +1 -1
  67. data/lib/grape/validations/validators/multiple_params_base.rb +2 -1
  68. data/lib/grape/validations/validators/regexp.rb +1 -1
  69. data/lib/grape/validations/validators/values.rb +1 -1
  70. data/lib/grape/version.rb +1 -1
  71. data/lib/grape.rb +5 -5
  72. data/spec/grape/api/instance_spec.rb +50 -0
  73. data/spec/grape/api_remount_spec.rb +9 -4
  74. data/spec/grape/api_spec.rb +82 -6
  75. data/spec/grape/dsl/inside_route_spec.rb +182 -33
  76. data/spec/grape/endpoint/declared_spec.rb +601 -0
  77. data/spec/grape/endpoint_spec.rb +0 -521
  78. data/spec/grape/entity_spec.rb +7 -1
  79. data/spec/grape/exceptions/validation_errors_spec.rb +2 -2
  80. data/spec/grape/integration/rack_sendfile_spec.rb +12 -8
  81. data/spec/grape/middleware/auth/strategies_spec.rb +1 -1
  82. data/spec/grape/middleware/error_spec.rb +1 -1
  83. data/spec/grape/middleware/formatter_spec.rb +3 -3
  84. data/spec/grape/middleware/stack_spec.rb +10 -0
  85. data/spec/grape/path_spec.rb +4 -4
  86. data/spec/grape/request_spec.rb +1 -1
  87. data/spec/grape/validations/instance_behaivour_spec.rb +1 -1
  88. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +13 -3
  89. data/spec/grape/validations/params_scope_spec.rb +26 -0
  90. data/spec/grape/validations/single_attribute_iterator_spec.rb +17 -6
  91. data/spec/grape/validations/types/array_coercer_spec.rb +35 -0
  92. data/spec/grape/validations/types/primitive_coercer_spec.rb +135 -0
  93. data/spec/grape/validations/types/set_coercer_spec.rb +34 -0
  94. data/spec/grape/validations/types_spec.rb +1 -1
  95. data/spec/grape/validations/validators/coerce_spec.rb +366 -86
  96. data/spec/grape/validations/validators/default_spec.rb +170 -0
  97. data/spec/grape/validations/validators/exactly_one_of_spec.rb +12 -12
  98. data/spec/grape/validations/validators/except_values_spec.rb +1 -0
  99. data/spec/grape/validations/validators/values_spec.rb +1 -1
  100. data/spec/grape/validations_spec.rb +298 -30
  101. data/spec/integration/eager_load/eager_load_spec.rb +15 -0
  102. data/spec/shared/versioning_examples.rb +20 -20
  103. data/spec/spec_helper.rb +3 -10
  104. data/spec/support/chunks.rb +14 -0
  105. data/spec/support/eager_load.rb +19 -0
  106. data/spec/support/versioned_helpers.rb +4 -6
  107. metadata +27 -10
  108. data/lib/grape/util/content_types.rb +0 -28
@@ -19,15 +19,15 @@ module Grape
19
19
  rescue_subclasses: true, # rescue subclasses of exceptions listed
20
20
  rescue_options: {
21
21
  backtrace: false, # true to display backtrace, true to let Grape handle Grape::Exceptions
22
- original_exception: false, # true to display exception
22
+ original_exception: false # true to display exception
23
23
  },
24
24
  rescue_handlers: {}, # rescue handler blocks
25
25
  base_only_rescue_handlers: {}, # rescue handler blocks rescuing only the base class
26
- all_rescue_handler: nil, # rescue handler block to rescue from all exceptions
26
+ all_rescue_handler: nil # rescue handler block to rescue from all exceptions
27
27
  }
28
28
  end
29
29
 
30
- def initialize(app, **options)
30
+ def initialize(app, *options)
31
31
  super
32
32
  self.class.send(:include, @options[:helpers]) if @options[:helpers]
33
33
  end
@@ -38,15 +38,15 @@ module Grape
38
38
  error_response(catch(:error) do
39
39
  return @app.call(@env)
40
40
  end)
41
- rescue Exception => error # rubocop:disable Lint/RescueException
41
+ rescue Exception => e # rubocop:disable Lint/RescueException
42
42
  handler =
43
- rescue_handler_for_base_only_class(error.class) ||
44
- rescue_handler_for_class_or_its_ancestor(error.class) ||
45
- rescue_handler_for_grape_exception(error.class) ||
46
- rescue_handler_for_any_class(error.class) ||
43
+ rescue_handler_for_base_only_class(e.class) ||
44
+ rescue_handler_for_class_or_its_ancestor(e.class) ||
45
+ rescue_handler_for_grape_exception(e.class) ||
46
+ rescue_handler_for_any_class(e.class) ||
47
47
  raise
48
48
 
49
- run_rescue_handler(handler, error)
49
+ run_rescue_handler(handler, e)
50
50
  end
51
51
  end
52
52
 
@@ -65,15 +65,13 @@ module Grape
65
65
  message = error[:message] || options[:default_message]
66
66
  headers = { Grape::Http::Headers::CONTENT_TYPE => content_type }
67
67
  headers.merge!(error[:headers]) if error[:headers].is_a?(Hash)
68
- backtrace = error[:backtrace] || error[:original_exception] && error[:original_exception].backtrace || []
68
+ backtrace = error[:backtrace] || error[:original_exception]&.backtrace || []
69
69
  original_exception = error.is_a?(Exception) ? error : error[:original_exception] || nil
70
70
  rack_response(format_message(message, backtrace, original_exception), status, headers)
71
71
  end
72
72
 
73
73
  def rack_response(message, status = options[:default_status], headers = { Grape::Http::Headers::CONTENT_TYPE => content_type })
74
- if headers[Grape::Http::Headers::CONTENT_TYPE] == TEXT_HTML
75
- message = ERB::Util.html_escape(message)
76
- end
74
+ message = ERB::Util.html_escape(message) if headers[Grape::Http::Headers::CONTENT_TYPE] == TEXT_HTML
77
75
  Rack::Response.new([message], status, headers)
78
76
  end
79
77
 
@@ -36,9 +36,9 @@ module Grape
36
36
  def build_formatted_response(status, headers, bodies)
37
37
  headers = ensure_content_type(headers)
38
38
 
39
- if bodies.is_a?(Grape::ServeFile::FileResponse)
40
- Grape::ServeFile::SendfileResponse.new([], status, headers) do |resp|
41
- resp.body = bodies.file
39
+ if bodies.is_a?(Grape::ServeStream::StreamResponse)
40
+ Grape::ServeStream::SendfileResponse.new([], status, headers) do |resp|
41
+ resp.body = bodies.stream
42
42
  end
43
43
  else
44
44
  # Allow content-type to be explicitly overwritten
@@ -30,6 +30,10 @@ module Grape
30
30
  def inspect
31
31
  klass.to_s
32
32
  end
33
+
34
+ def use_in(builder)
35
+ builder.use(@klass, *@args, &@block)
36
+ end
33
37
  end
34
38
 
35
39
  include Enumerable
@@ -62,6 +66,7 @@ module Grape
62
66
  middleware = self.class::Middleware.new(*args, &block)
63
67
  middlewares.insert(index, middleware)
64
68
  end
69
+ ruby2_keywords :insert if respond_to?(:ruby2_keywords, true)
65
70
 
66
71
  alias insert_before insert
67
72
 
@@ -69,16 +74,19 @@ module Grape
69
74
  index = assert_index(index, :after)
70
75
  insert(index + 1, *args, &block)
71
76
  end
77
+ ruby2_keywords :insert_after if respond_to?(:ruby2_keywords, true)
72
78
 
73
79
  def use(*args, &block)
74
80
  middleware = self.class::Middleware.new(*args, &block)
75
81
  middlewares.push(middleware)
76
82
  end
83
+ ruby2_keywords :use if respond_to?(:ruby2_keywords, true)
77
84
 
78
85
  def merge_with(middleware_specs)
79
86
  middleware_specs.each do |operation, *args|
80
87
  if args.last.is_a?(Proc)
81
- public_send(operation, *args, &args.pop)
88
+ last_proc = args.pop
89
+ public_send(operation, *args, &last_proc)
82
90
  else
83
91
  public_send(operation, *args)
84
92
  end
@@ -89,7 +97,7 @@ module Grape
89
97
  def build(builder = Rack::Builder.new)
90
98
  others.shift(others.size).each(&method(:merge_with))
91
99
  middlewares.each do |m|
92
- m.block ? builder.use(m.klass, *m.args, &m.block) : builder.use(m.klass, *m.args)
100
+ m.use_in(builder)
93
101
  end
94
102
  builder
95
103
  end
@@ -26,10 +26,10 @@ module Grape
26
26
  # route.
27
27
  class Header < Base
28
28
  VENDOR_VERSION_HEADER_REGEX =
29
- /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))?(?:\+([a-z0-9*\-.]+))?\z/
29
+ /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))?(?:\+([a-z0-9*\-.]+))?\z/.freeze
30
30
 
31
- HAS_VENDOR_REGEX = /\Avnd\.[a-z0-9.\-_!#\$&\^]+/
32
- HAS_VERSION_REGEX = /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))+/
31
+ HAS_VENDOR_REGEX = /\Avnd\.[a-z0-9.\-_!#\$&\^]+/.freeze
32
+ HAS_VERSION_REGEX = /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))+/.freeze
33
33
 
34
34
  def before
35
35
  strict_header_checks if strict?
@@ -63,7 +63,7 @@ module Grape
63
63
 
64
64
  def an_accept_header_with_version_and_vendor_is_present?
65
65
  header.qvalues.keys.any? do |h|
66
- VENDOR_VERSION_HEADER_REGEX =~ h.sub('application/', '')
66
+ VENDOR_VERSION_HEADER_REGEX.match?(h.sub('application/', ''))
67
67
  end
68
68
  end
69
69
 
@@ -3,11 +3,12 @@
3
3
  module Rack
4
4
  module Accept
5
5
  module Header
6
+ ALLOWED_CHARACTERS = %r{^([a-z*]+)\/([a-z0-9*\&\^\-_#\$!.+]+)(?:;([a-z0-9=;]+))?$}.freeze
6
7
  class << self
7
8
  # Corrected version of https://github.com/mjackson/rack-accept/blob/master/lib/rack/accept/header.rb#L40-L44
8
9
  def parse_media_type(media_type)
9
10
  # see http://tools.ietf.org/html/rfc6838#section-4.2 for allowed characters in media type names
10
- m = media_type.to_s.match(%r{^([a-z*]+)\/([a-z0-9*\&\^\-_#\$!.+]+)(?:;([a-z0-9=;]+))?$})
11
+ m = media_type&.match(ALLOWED_CHARACTERS)
11
12
  m ? [m[1], m[2], m[3] || ''] : []
12
13
  end
13
14
  end
@@ -36,7 +36,7 @@ module Grape
36
36
 
37
37
  pieces = path.split('/')
38
38
  potential_version = pieces[1]
39
- return unless potential_version =~ options[:pattern]
39
+ return unless potential_version&.match?(options[:pattern])
40
40
  throw :error, status: 404, message: '404 API Version Not Found' if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
41
41
  env[Grape::Env::API_VERSION] = potential_version
42
42
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'grape/util/cache'
4
+
3
5
  module Grape
4
6
  # A container for endpoints or other namespaces, which allows for both
5
7
  # logical grouping of endpoints as well as sharing common configuration.
@@ -25,13 +27,21 @@ module Grape
25
27
 
26
28
  # (see ::joined_space_path)
27
29
  def self.joined_space(settings)
28
- (settings || []).map(&:space).join('/')
30
+ settings&.map(&:space)
29
31
  end
30
32
 
31
33
  # Join the namespaces from a list of settings to create a path prefix.
32
34
  # @param settings [Array] list of Grape::Util::InheritableSettings.
33
35
  def self.joined_space_path(settings)
34
- Grape::Router.normalize_path(joined_space(settings))
36
+ Grape::Router.normalize_path(JoinedSpaceCache[joined_space(settings)])
37
+ end
38
+
39
+ class JoinedSpaceCache < Grape::Util::Cache
40
+ def initialize
41
+ @cache = Hash.new do |h, joined_space|
42
+ h[joined_space] = -joined_space.join('/')
43
+ end
44
+ end
35
45
  end
36
46
  end
37
47
  end
data/lib/grape/path.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'grape/util/cache'
4
+
3
5
  module Grape
4
6
  # Represents a path to an endpoint.
5
7
  class Path
@@ -40,11 +42,11 @@ module Grape
40
42
  end
41
43
 
42
44
  def namespace?
43
- namespace && namespace.to_s =~ /^\S/ && namespace != '/'
45
+ namespace&.match?(/^\S/) && namespace != '/'
44
46
  end
45
47
 
46
48
  def path?
47
- raw_path && raw_path.to_s =~ /^\S/ && raw_path != '/'
49
+ raw_path&.match?(/^\S/) && raw_path != '/'
48
50
  end
49
51
 
50
52
  def suffix
@@ -58,7 +60,7 @@ module Grape
58
60
  end
59
61
 
60
62
  def path
61
- Grape::Router.normalize_path(parts.join('/'))
63
+ Grape::Router.normalize_path(PartsCache[parts])
62
64
  end
63
65
 
64
66
  def path_with_suffix
@@ -71,6 +73,14 @@ module Grape
71
73
 
72
74
  private
73
75
 
76
+ class PartsCache < Grape::Util::Cache
77
+ def initialize
78
+ @cache = Hash.new do |h, parts|
79
+ h[parts] = -parts.join('/')
80
+ end
81
+ end
82
+ end
83
+
74
84
  def parts
75
85
  parts = [mount_path, root_prefix].compact
76
86
  parts << ':version' if uses_path_versioning?
data/lib/grape/request.rb CHANGED
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'grape/util/lazy_object'
4
+
3
5
  module Grape
4
6
  class Request < Rack::Request
5
7
  HTTP_PREFIX = 'HTTP_'
6
8
 
7
9
  alias rack_params params
8
10
 
9
- def initialize(env, options = {})
11
+ def initialize(env, **options)
10
12
  extend options[:build_params_with] || Grape.config.param_builder
11
13
  super(env)
12
14
  end
@@ -30,14 +32,17 @@ module Grape
30
32
  end
31
33
 
32
34
  def build_headers
33
- headers = {}
34
- env.each_pair do |k, v|
35
- next unless k.to_s.start_with? HTTP_PREFIX
36
-
37
- k = k[5..-1].split('_').each(&:capitalize!).join('-')
38
- headers[k] = v
35
+ Grape::Util::LazyObject.new do
36
+ env.each_pair.with_object({}) do |(k, v), headers|
37
+ next unless k.to_s.start_with? HTTP_PREFIX
38
+ transformed_header = Grape::Http::Headers::HTTP_HEADERS[k] || transform_header(k)
39
+ headers[transformed_header] = v
40
+ end
39
41
  end
40
- headers
42
+ end
43
+
44
+ def transform_header(header)
45
+ -header[5..-1].split('_').each(&:capitalize!).join('-')
41
46
  end
42
47
  end
43
48
  end
@@ -4,19 +4,40 @@ module Grape
4
4
  class Router
5
5
  # this could be an OpenStruct, but doesn't work in Ruby 2.3.0, see https://bugs.ruby-lang.org/issues/12251
6
6
  class AttributeTranslator
7
- attr_reader :attributes, :request_method, :requirements
7
+ attr_reader :attributes
8
8
 
9
- def initialize(attributes = {})
9
+ ROUTE_ATTRIBUTES = %i[
10
+ prefix
11
+ version
12
+ settings
13
+ format
14
+ description
15
+ http_codes
16
+ headers
17
+ entity
18
+ details
19
+ requirements
20
+ request_method
21
+ namespace
22
+ ].freeze
23
+
24
+ ROUTER_ATTRIBUTES = %i[pattern index].freeze
25
+
26
+ def initialize(**attributes)
10
27
  @attributes = attributes
11
- @request_method = attributes[:request_method]
12
- @requirements = attributes[:requirements]
28
+ end
29
+
30
+ (ROUTER_ATTRIBUTES + ROUTE_ATTRIBUTES).each do |attr|
31
+ define_method attr do
32
+ attributes[attr]
33
+ end
13
34
  end
14
35
 
15
36
  def to_h
16
37
  attributes
17
38
  end
18
39
 
19
- def method_missing(method_name, *args) # rubocop:disable Style/MethodMissing
40
+ def method_missing(method_name, *args)
20
41
  if setter?(method_name[-1])
21
42
  attributes[method_name[0..-1]] = *args
22
43
  else
@@ -2,35 +2,32 @@
2
2
 
3
3
  require 'forwardable'
4
4
  require 'mustermann/grape'
5
+ require 'grape/util/cache'
5
6
 
6
7
  module Grape
7
8
  class Router
8
9
  class Pattern
9
- DEFAULT_PATTERN_OPTIONS = { uri_decode: true, type: :grape }.freeze
10
+ DEFAULT_PATTERN_OPTIONS = { uri_decode: true }.freeze
10
11
  DEFAULT_SUPPORTED_CAPTURE = %i[format version].freeze
11
12
 
12
- attr_reader :origin, :path, :capture, :pattern
13
+ attr_reader :origin, :path, :pattern, :to_regexp
13
14
 
14
15
  extend Forwardable
15
16
  def_delegators :pattern, :named_captures, :params
16
- def_delegators :@regexp, :===
17
+ def_delegators :to_regexp, :===
17
18
  alias match? ===
18
19
 
19
20
  def initialize(pattern, **options)
20
21
  @origin = pattern
21
22
  @path = build_path(pattern, **options)
22
- @capture = extract_capture(**options)
23
- @pattern = Mustermann.new(@path, **pattern_options)
24
- @regexp = to_regexp
25
- end
26
-
27
- def to_regexp
28
- @to_regexp ||= @pattern.to_regexp
23
+ @pattern = Mustermann::Grape.new(@path, **pattern_options(options))
24
+ @to_regexp = @pattern.to_regexp
29
25
  end
30
26
 
31
27
  private
32
28
 
33
- def pattern_options
29
+ def pattern_options(options)
30
+ capture = extract_capture(**options)
34
31
  options = DEFAULT_PATTERN_OPTIONS.dup
35
32
  options[:capture] = capture if capture.present?
36
33
  options
@@ -43,23 +40,27 @@ module Grape
43
40
  pattern << '*path'
44
41
  end
45
42
 
46
- pattern = pattern.split('/').tap do |parts|
43
+ pattern = -pattern.split('/').tap do |parts|
47
44
  parts[parts.length - 1] = '?' + parts.last
48
45
  end.join('/') if pattern.end_with?('*path')
49
46
 
50
- "#{pattern}#{suffix}"
47
+ PatternCache[[pattern, suffix]]
51
48
  end
52
49
 
53
50
  def extract_capture(requirements: {}, **options)
54
51
  requirements = {}.merge(requirements)
55
- supported_capture.each_with_object(requirements) do |field, capture|
52
+ DEFAULT_SUPPORTED_CAPTURE.each_with_object(requirements) do |field, capture|
56
53
  option = Array(options[field])
57
54
  capture[field] = option.map(&:to_s) if option.present?
58
55
  end
59
56
  end
60
57
 
61
- def supported_capture
62
- DEFAULT_SUPPORTED_CAPTURE
58
+ class PatternCache < Grape::Util::Cache
59
+ def initialize
60
+ @cache = Hash.new do |h, (pattern, suffix)|
61
+ h[[pattern, suffix]] = -"#{pattern}#{suffix}"
62
+ end
63
+ end
63
64
  end
64
65
  end
65
66
  end
@@ -8,16 +8,17 @@ require 'pathname'
8
8
  module Grape
9
9
  class Router
10
10
  class Route
11
- ROUTE_ATTRIBUTE_REGEXP = /route_([_a-zA-Z]\w*)/
12
- SOURCE_LOCATION_REGEXP = /^(.*?):(\d+?)(?::in `.+?')?$/
11
+ ROUTE_ATTRIBUTE_REGEXP = /route_([_a-zA-Z]\w*)/.freeze
12
+ SOURCE_LOCATION_REGEXP = /^(.*?):(\d+?)(?::in `.+?')?$/.freeze
13
13
  FIXED_NAMED_CAPTURES = %w[format version].freeze
14
14
 
15
- attr_accessor :pattern, :translator, :app, :index, :regexp, :options
15
+ attr_accessor :pattern, :translator, :app, :index, :options
16
16
 
17
17
  alias attributes translator
18
18
 
19
19
  extend Forwardable
20
20
  def_delegators :pattern, :path, :origin
21
+ delegate Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES => :attributes
21
22
 
22
23
  def method_missing(method_id, *arguments)
23
24
  match = ROUTE_ATTRIBUTE_REGEXP.match(method_id.to_s)
@@ -31,26 +32,7 @@ module Grape
31
32
  end
32
33
 
33
34
  def respond_to_missing?(method_id, _)
34
- ROUTE_ATTRIBUTE_REGEXP.match(method_id.to_s)
35
- end
36
-
37
- %i[
38
- prefix
39
- version
40
- settings
41
- format
42
- description
43
- http_codes
44
- headers
45
- entity
46
- details
47
- requirements
48
- request_method
49
- namespace
50
- ].each do |method_name|
51
- define_method method_name do
52
- attributes.public_send method_name
53
- end
35
+ ROUTE_ATTRIBUTE_REGEXP.match?(method_id.to_s)
54
36
  end
55
37
 
56
38
  def route_method
@@ -67,7 +49,6 @@ module Grape
67
49
  method_s = method.to_s
68
50
  method_upcase = Grape::Http::Headers.find_supported_method(method_s) || method_s.upcase
69
51
 
70
- @suffix = options[:suffix]
71
52
  @options = options.merge(method: method_upcase)
72
53
  @pattern = Pattern.new(pattern, **options)
73
54
  @translator = AttributeTranslator.new(**options, request_method: method_upcase)
data/lib/grape/router.rb CHANGED
@@ -1,21 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'grape/router/route'
4
+ require 'grape/util/cache'
4
5
 
5
6
  module Grape
6
7
  class Router
7
8
  attr_reader :map, :compiled
8
9
 
9
- class Any < AttributeTranslator
10
- attr_reader :pattern, :regexp, :index
11
- def initialize(pattern, regexp, index, **attributes)
12
- @pattern = pattern
13
- @regexp = regexp
14
- @index = index
15
- super(attributes)
16
- end
17
- end
18
-
19
10
  def self.normalize_path(path)
20
11
  path = +"/#{path}"
21
12
  path.squeeze!('/')
@@ -30,18 +21,20 @@ module Grape
30
21
 
31
22
  def initialize
32
23
  @neutral_map = []
24
+ @neutral_regexes = []
33
25
  @map = Hash.new { |hash, key| hash[key] = [] }
34
26
  @optimized_map = Hash.new { |hash, key| hash[key] = // }
35
27
  end
36
28
 
37
29
  def compile!
38
30
  return if compiled
39
- @union = Regexp.union(@neutral_map.map(&:regexp))
31
+ @union = Regexp.union(@neutral_regexes)
32
+ @neutral_regexes = nil
40
33
  self.class.supported_methods.each do |method|
41
34
  routes = map[method]
42
35
  @optimized_map[method] = routes.map.with_index do |route, index|
43
36
  route.index = index
44
- route.regexp = Regexp.new("(?<_#{index}>#{route.pattern.to_regexp})")
37
+ Regexp.new("(?<_#{index}>#{route.pattern.to_regexp})")
45
38
  end
46
39
  @optimized_map[method] = Regexp.union(@optimized_map[method])
47
40
  end
@@ -53,8 +46,8 @@ module Grape
53
46
  end
54
47
 
55
48
  def associate_routes(pattern, **options)
56
- regexp = Regexp.new("(?<_#{@neutral_map.length}>)#{pattern.to_regexp}")
57
- @neutral_map << Any.new(pattern, regexp, @neutral_map.length, **options)
49
+ @neutral_regexes << Regexp.new("(?<_#{@neutral_map.length}>)#{pattern.to_regexp}")
50
+ @neutral_map << Grape::Router::AttributeTranslator.new(**options, pattern: pattern, index: @neutral_map.length)
58
51
  end
59
52
 
60
53
  def call(env)
@@ -98,37 +91,34 @@ module Grape
98
91
  response = yield(input, method)
99
92
 
100
93
  return response if response && !(cascade = cascade?(response))
101
- neighbor = greedy_match?(input)
94
+ last_neighbor_route = greedy_match?(input)
102
95
 
103
- # If neighbor exists and request method is OPTIONS,
96
+ # If last_neighbor_route exists and request method is OPTIONS,
104
97
  # return response by using #call_with_allow_headers.
105
- return call_with_allow_headers(
106
- env,
107
- neighbor.allow_header,
108
- neighbor.endpoint
109
- ) if neighbor && method == 'OPTIONS' && !cascade
98
+ return call_with_allow_headers(env, last_neighbor_route) if last_neighbor_route && method == Grape::Http::Headers::OPTIONS && !cascade
110
99
 
111
100
  route = match?(input, '*')
112
- return neighbor.endpoint.call(env) if neighbor && cascade && route
101
+
102
+ return last_neighbor_route.endpoint.call(env) if last_neighbor_route && cascade && route
113
103
 
114
104
  if route
115
105
  response = process_route(route, env)
116
106
  return response if response && !(cascade = cascade?(response))
117
107
  end
118
108
 
119
- !cascade && neighbor ? call_with_allow_headers(env, neighbor.allow_header, neighbor.endpoint) : nil
109
+ return call_with_allow_headers(env, last_neighbor_route) if !cascade && last_neighbor_route
110
+
111
+ nil
120
112
  end
121
113
 
122
114
  def process_route(route, env)
123
- input, = *extract_input_and_method(env)
124
- routing_args = env[Grape::Env::GRAPE_ROUTING_ARGS]
125
- env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(routing_args, route, input)
115
+ prepare_env_from_route(env, route)
126
116
  route.exec(env)
127
117
  end
128
118
 
129
119
  def make_routing_args(default_args, route, input)
130
120
  args = default_args || { route_info: route }
131
- args.merge(route.params(input))
121
+ args.merge(route.params(input) || {})
132
122
  end
133
123
 
134
124
  def extract_input_and_method(env)
@@ -159,9 +149,15 @@ module Grape
159
149
  @neutral_map.detect { |route| last_match["_#{route.index}"] }
160
150
  end
161
151
 
162
- def call_with_allow_headers(env, methods, endpoint)
163
- env[Grape::Env::GRAPE_ALLOWED_METHODS] = methods
164
- endpoint.call(env)
152
+ def call_with_allow_headers(env, route)
153
+ prepare_env_from_route(env, route)
154
+ env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.allow_header.join(', ').freeze
155
+ route.endpoint.call(env)
156
+ end
157
+
158
+ def prepare_env_from_route(env, route)
159
+ input, = *extract_input_and_method(env)
160
+ env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(env[Grape::Env::GRAPE_ROUTING_ARGS], route, input)
165
161
  end
166
162
 
167
163
  def cascade?(response)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grape
4
- module ServeFile
4
+ module ServeStream
5
5
  CHUNK_SIZE = 16_384
6
6
 
7
7
  # Class helps send file through API
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grape
4
- module ServeFile
4
+ module ServeStream
5
5
  # Response should respond to to_path method
6
6
  # for using Rack::SendFile middleware
7
7
  class SendfileResponse < Rack::Response
@@ -1,22 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grape
4
- module ServeFile
5
- # A simple class used to identify responses which represent files and do not
4
+ module ServeStream
5
+ # A simple class used to identify responses which represent streams (or files) and do not
6
6
  # need to be formatted or pre-read by Rack::Response
7
- class FileResponse
8
- attr_reader :file
7
+ class StreamResponse
8
+ attr_reader :stream
9
9
 
10
- # @param file [Object]
11
- def initialize(file)
12
- @file = file
10
+ # @param stream [Object]
11
+ def initialize(stream)
12
+ @stream = stream
13
13
  end
14
14
 
15
15
  # Equality provided mostly for tests.
16
16
  #
17
17
  # @return [Boolean]
18
18
  def ==(other)
19
- file == other.file
19
+ stream == other.stream
20
20
  end
21
21
  end
22
22
  end