grape 1.3.0 → 1.5.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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +90 -0
  3. data/LICENSE +1 -1
  4. data/README.md +104 -21
  5. data/UPGRADING.md +243 -39
  6. data/lib/grape.rb +4 -5
  7. data/lib/grape/api.rb +4 -4
  8. data/lib/grape/api/instance.rb +32 -31
  9. data/lib/grape/content_types.rb +34 -0
  10. data/lib/grape/dsl/helpers.rb +2 -1
  11. data/lib/grape/dsl/inside_route.rb +76 -42
  12. data/lib/grape/dsl/parameters.rb +4 -4
  13. data/lib/grape/dsl/routing.rb +8 -8
  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_errors.rb +11 -12
  19. data/lib/grape/http/headers.rb +26 -0
  20. data/lib/grape/middleware/base.rb +3 -4
  21. data/lib/grape/middleware/error.rb +10 -12
  22. data/lib/grape/middleware/formatter.rb +3 -3
  23. data/lib/grape/middleware/stack.rb +19 -5
  24. data/lib/grape/middleware/versioner/header.rb +4 -4
  25. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +2 -1
  26. data/lib/grape/middleware/versioner/path.rb +1 -1
  27. data/lib/grape/namespace.rb +12 -2
  28. data/lib/grape/path.rb +13 -3
  29. data/lib/grape/request.rb +13 -8
  30. data/lib/grape/router.rb +26 -30
  31. data/lib/grape/router/attribute_translator.rb +25 -4
  32. data/lib/grape/router/pattern.rb +17 -16
  33. data/lib/grape/router/route.rb +5 -24
  34. data/lib/grape/{serve_file → serve_stream}/file_body.rb +1 -1
  35. data/lib/grape/{serve_file → serve_stream}/sendfile_response.rb +1 -1
  36. data/lib/grape/{serve_file/file_response.rb → serve_stream/stream_response.rb} +8 -8
  37. data/lib/grape/util/base_inheritable.rb +15 -8
  38. data/lib/grape/util/cache.rb +20 -0
  39. data/lib/grape/util/lazy_object.rb +43 -0
  40. data/lib/grape/util/lazy_value.rb +1 -0
  41. data/lib/grape/util/reverse_stackable_values.rb +2 -0
  42. data/lib/grape/util/stackable_values.rb +7 -20
  43. data/lib/grape/validations/params_scope.rb +6 -5
  44. data/lib/grape/validations/types.rb +6 -5
  45. data/lib/grape/validations/types/array_coercer.rb +14 -5
  46. data/lib/grape/validations/types/build_coercer.rb +5 -8
  47. data/lib/grape/validations/types/custom_type_coercer.rb +14 -2
  48. data/lib/grape/validations/types/dry_type_coercer.rb +36 -1
  49. data/lib/grape/validations/types/file.rb +15 -12
  50. data/lib/grape/validations/types/json.rb +40 -36
  51. data/lib/grape/validations/types/primitive_coercer.rb +15 -6
  52. data/lib/grape/validations/types/set_coercer.rb +6 -4
  53. data/lib/grape/validations/types/variant_collection_coercer.rb +1 -1
  54. data/lib/grape/validations/validators/as.rb +1 -1
  55. data/lib/grape/validations/validators/base.rb +2 -4
  56. data/lib/grape/validations/validators/coerce.rb +4 -11
  57. data/lib/grape/validations/validators/default.rb +3 -5
  58. data/lib/grape/validations/validators/exactly_one_of.rb +4 -2
  59. data/lib/grape/validations/validators/except_values.rb +1 -1
  60. data/lib/grape/validations/validators/regexp.rb +1 -1
  61. data/lib/grape/validations/validators/values.rb +1 -1
  62. data/lib/grape/version.rb +1 -1
  63. data/spec/grape/api/instance_spec.rb +50 -0
  64. data/spec/grape/api_spec.rb +82 -6
  65. data/spec/grape/dsl/inside_route_spec.rb +182 -33
  66. data/spec/grape/endpoint/declared_spec.rb +590 -0
  67. data/spec/grape/endpoint_spec.rb +0 -521
  68. data/spec/grape/entity_spec.rb +6 -0
  69. data/spec/grape/exceptions/validation_errors_spec.rb +2 -2
  70. data/spec/grape/integration/rack_sendfile_spec.rb +12 -8
  71. data/spec/grape/middleware/auth/strategies_spec.rb +1 -1
  72. data/spec/grape/middleware/error_spec.rb +1 -1
  73. data/spec/grape/middleware/formatter_spec.rb +3 -3
  74. data/spec/grape/middleware/stack_spec.rb +12 -1
  75. data/spec/grape/path_spec.rb +4 -4
  76. data/spec/grape/validations/instance_behaivour_spec.rb +1 -1
  77. data/spec/grape/validations/params_scope_spec.rb +26 -0
  78. data/spec/grape/validations/types/array_coercer_spec.rb +35 -0
  79. data/spec/grape/validations/types/primitive_coercer_spec.rb +135 -0
  80. data/spec/grape/validations/types/set_coercer_spec.rb +34 -0
  81. data/spec/grape/validations/types_spec.rb +1 -1
  82. data/spec/grape/validations/validators/coerce_spec.rb +329 -77
  83. data/spec/grape/validations/validators/default_spec.rb +170 -0
  84. data/spec/grape/validations/validators/exactly_one_of_spec.rb +12 -12
  85. data/spec/grape/validations/validators/except_values_spec.rb +1 -0
  86. data/spec/grape/validations/validators/values_spec.rb +1 -1
  87. data/spec/grape/validations_spec.rb +30 -30
  88. data/spec/integration/eager_load/eager_load_spec.rb +15 -0
  89. data/spec/spec_helper.rb +3 -10
  90. data/spec/support/chunks.rb +14 -0
  91. data/spec/support/eager_load.rb +19 -0
  92. data/spec/support/versioned_helpers.rb +3 -5
  93. metadata +121 -105
  94. data/lib/grape/util/content_types.rb +0 -28
@@ -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
@@ -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?
@@ -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
@@ -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)
@@ -6,17 +6,38 @@ module Grape
6
6
  class AttributeTranslator
7
7
  attr_reader :attributes, :request_method, :requirements
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)
@@ -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