grape 1.3.0 → 1.3.1

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +3 -4
  4. data/lib/grape.rb +2 -3
  5. data/lib/grape/api.rb +2 -2
  6. data/lib/grape/api/instance.rb +4 -4
  7. data/lib/grape/content_types.rb +34 -0
  8. data/lib/grape/dsl/helpers.rb +1 -1
  9. data/lib/grape/dsl/inside_route.rb +10 -9
  10. data/lib/grape/dsl/parameters.rb +4 -4
  11. data/lib/grape/dsl/routing.rb +6 -4
  12. data/lib/grape/exceptions/base.rb +0 -4
  13. data/lib/grape/exceptions/validation_errors.rb +11 -12
  14. data/lib/grape/http/headers.rb +25 -0
  15. data/lib/grape/middleware/base.rb +1 -3
  16. data/lib/grape/middleware/stack.rb +2 -1
  17. data/lib/grape/middleware/versioner/header.rb +3 -3
  18. data/lib/grape/middleware/versioner/path.rb +1 -1
  19. data/lib/grape/namespace.rb +12 -2
  20. data/lib/grape/path.rb +11 -1
  21. data/lib/grape/request.rb +12 -7
  22. data/lib/grape/router.rb +16 -7
  23. data/lib/grape/router/pattern.rb +17 -16
  24. data/lib/grape/router/route.rb +2 -2
  25. data/lib/grape/util/base_inheritable.rb +4 -0
  26. data/lib/grape/util/cache.rb +20 -0
  27. data/lib/grape/util/lazy_object.rb +43 -0
  28. data/lib/grape/util/reverse_stackable_values.rb +1 -1
  29. data/lib/grape/util/stackable_values.rb +6 -21
  30. data/lib/grape/validations/params_scope.rb +1 -1
  31. data/lib/grape/validations/types/file.rb +1 -0
  32. data/lib/grape/validations/types/primitive_coercer.rb +7 -4
  33. data/lib/grape/validations/validators/coerce.rb +1 -1
  34. data/lib/grape/validations/validators/exactly_one_of.rb +4 -2
  35. data/lib/grape/version.rb +1 -1
  36. data/spec/grape/api_spec.rb +7 -6
  37. data/spec/grape/exceptions/validation_errors_spec.rb +2 -2
  38. data/spec/grape/middleware/formatter_spec.rb +2 -2
  39. data/spec/grape/middleware/stack_spec.rb +9 -0
  40. data/spec/grape/validations/instance_behaivour_spec.rb +1 -1
  41. data/spec/grape/validations/types/primitive_coercer_spec.rb +75 -0
  42. data/spec/grape/validations/validators/coerce_spec.rb +15 -51
  43. data/spec/grape/validations/validators/exactly_one_of_spec.rb +12 -12
  44. data/spec/grape/validations_spec.rb +8 -12
  45. data/spec/spec_helper.rb +3 -0
  46. data/spec/support/eager_load.rb +19 -0
  47. metadata +12 -6
  48. 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
@@ -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,5 +1,7 @@
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_'
@@ -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,6 +1,7 @@
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
@@ -16,12 +17,20 @@ module Grape
16
17
  end
17
18
  end
18
19
 
20
+ class NormalizePathCache < Grape::Util::Cache
21
+ def initialize
22
+ @cache = Hash.new do |h, path|
23
+ normalized_path = +"/#{path}"
24
+ normalized_path.squeeze!('/')
25
+ normalized_path.sub!(%r{/+\Z}, '')
26
+ normalized_path = '/' if normalized_path.empty?
27
+ h[path] = -normalized_path
28
+ end
29
+ end
30
+ end
31
+
19
32
  def self.normalize_path(path)
20
- path = +"/#{path}"
21
- path.squeeze!('/')
22
- path.sub!(%r{/+\Z}, '')
23
- path = '/' if path == ''
24
- path
33
+ NormalizePathCache[path]
25
34
  end
26
35
 
27
36
  def self.supported_methods
@@ -106,7 +115,7 @@ module Grape
106
115
  env,
107
116
  neighbor.allow_header,
108
117
  neighbor.endpoint
109
- ) if neighbor && method == 'OPTIONS' && !cascade
118
+ ) if neighbor && method == Grape::Http::Headers::OPTIONS && !cascade
110
119
 
111
120
  route = match?(input, '*')
112
121
  return neighbor.endpoint.call(env) if neighbor && cascade && route
@@ -160,7 +169,7 @@ module Grape
160
169
  end
161
170
 
162
171
  def call_with_allow_headers(env, methods, endpoint)
163
- env[Grape::Env::GRAPE_ALLOWED_METHODS] = methods
172
+ env[Grape::Env::GRAPE_ALLOWED_METHODS] = methods.join(', ').freeze
164
173
  endpoint.call(env)
165
174
  end
166
175
 
@@ -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,8 +8,8 @@ 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
15
  attr_accessor :pattern, :translator, :app, :index, :regexp, :options
@@ -31,6 +31,10 @@ module Grape
31
31
  combined.uniq!
32
32
  combined
33
33
  end
34
+
35
+ def key?(name)
36
+ inherited_values.key?(name) || new_values.key?(name)
37
+ end
34
38
  end
35
39
  end
36
40
  end
@@ -0,0 +1,20 @@
1
+ # frozen_String_literal: true
2
+
3
+ require 'singleton'
4
+ require 'forwardable'
5
+
6
+ module Grape
7
+ module Util
8
+ class Cache
9
+ include Singleton
10
+
11
+ attr_reader :cache
12
+
13
+ class << self
14
+ extend Forwardable
15
+ def_delegators :cache, :[]
16
+ def_delegators :instance, :cache
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Based on https://github.com/HornsAndHooves/lazy_object
4
+
5
+ module Grape
6
+ module Util
7
+ class LazyObject < BasicObject
8
+ attr_reader :callable
9
+
10
+ def initialize(&callable)
11
+ @callable = callable
12
+ end
13
+
14
+ def __target_object__
15
+ @__target_object__ ||= callable.call
16
+ end
17
+
18
+ def ==(other)
19
+ __target_object__ == other
20
+ end
21
+
22
+ def !=(other)
23
+ __target_object__ != other
24
+ end
25
+
26
+ def !
27
+ !__target_object__
28
+ end
29
+
30
+ def method_missing(method_name, *args, &block)
31
+ if __target_object__.respond_to?(method_name)
32
+ __target_object__.send(method_name, *args, &block)
33
+ else
34
+ super
35
+ end
36
+ end
37
+
38
+ def respond_to_missing?(method_name, include_priv = false)
39
+ __target_object__.respond_to?(method_name, include_priv)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -9,7 +9,7 @@ module Grape
9
9
 
10
10
  def concat_values(inherited_value, new_value)
11
11
  [].tap do |value|
12
- value.concat(new_value)
12
+ value.concat(new_value) if new_value
13
13
  value.concat(inherited_value)
14
14
  end
15
15
  end
@@ -5,30 +5,19 @@ require_relative 'base_inheritable'
5
5
  module Grape
6
6
  module Util
7
7
  class StackableValues < BaseInheritable
8
- attr_reader :frozen_values
9
-
10
- def initialize(*_args)
11
- super
12
-
13
- @frozen_values = {}
14
- end
15
-
16
8
  # Even if there is no value, an empty array will be returned.
17
9
  def [](name)
18
- return @frozen_values[name] if @frozen_values.key? name
10
+ inherited_value = inherited_values[name]
11
+ new_value = new_values[name]
19
12
 
20
- inherited_value = @inherited_values[name]
21
- new_value = @new_values[name] || []
22
-
23
- return new_value unless inherited_value
13
+ return new_value || [] unless inherited_value
24
14
 
25
15
  concat_values(inherited_value, new_value)
26
16
  end
27
17
 
28
18
  def []=(name, value)
29
- raise if @frozen_values.key? name
30
- @new_values[name] ||= []
31
- @new_values[name].push value
19
+ new_values[name] ||= []
20
+ new_values[name].push value
32
21
  end
33
22
 
34
23
  def to_hash
@@ -37,16 +26,12 @@ module Grape
37
26
  end
38
27
  end
39
28
 
40
- def freeze_value(key)
41
- @frozen_values[key] = self[key].freeze
42
- end
43
-
44
29
  protected
45
30
 
46
31
  def concat_values(inherited_value, new_value)
47
32
  [].tap do |value|
48
33
  value.concat(inherited_value)
49
- value.concat(new_value)
34
+ value.concat(new_value) if new_value
50
35
  end
51
36
  end
52
37
  end
@@ -244,7 +244,7 @@ module Grape
244
244
  end
245
245
 
246
246
  def validates(attrs, validations)
247
- doc_attrs = { required: validations.keys.include?(:presence) }
247
+ doc_attrs = { required: validations.key?(:presence) }
248
248
 
249
249
  coerce_type = infer_coercion(validations)
250
250
 
@@ -8,6 +8,7 @@ module Grape
8
8
  # this class is here only to assert that rack's handling has succeeded.
9
9
  class File
10
10
  def call(input)
11
+ return if input.nil?
11
12
  return InvalidValue.new unless coerced?(input)
12
13
 
13
14
  # Processing of multipart file objects
@@ -6,17 +6,20 @@ module Grape
6
6
  module Validations
7
7
  module Types
8
8
  # Coerces the given value to a type defined via a +type+ argument during
9
- # initialization.
9
+ # initialization. When +strict+ is true, it doesn't coerce a value but check
10
+ # that it has the proper type.
10
11
  class PrimitiveCoercer < DryTypeCoercer
11
12
  MAPPING = {
12
13
  Grape::API::Boolean => DryTypes::Params::Bool,
13
14
 
14
- # unfortunatelly, a +Params+ scope doesn't contain String
15
- String => DryTypes::Coercible::String
15
+ # unfortunately, a +Params+ scope doesn't contain String
16
+ String => DryTypes::Coercible::String,
17
+ BigDecimal => DryTypes::Coercible::Decimal
16
18
  }.freeze
17
19
 
18
20
  STRICT_MAPPING = {
19
- Grape::API::Boolean => DryTypes::Strict::Bool
21
+ Grape::API::Boolean => DryTypes::Strict::Bool,
22
+ BigDecimal => DryTypes::Strict::Decimal
20
23
  }.freeze
21
24
 
22
25
  def initialize(type, strict = false)
@@ -76,7 +76,7 @@ module Grape
76
76
  converter.call(val)
77
77
 
78
78
  # Some custom types might fail, so it should be treated as an invalid value
79
- rescue
79
+ rescue StandardError
80
80
  Types::InvalidValue.new
81
81
  end
82
82
 
@@ -6,8 +6,10 @@ module Grape
6
6
  module Validations
7
7
  class ExactlyOneOfValidator < MultipleParamsBase
8
8
  def validate_params!(params)
9
- return if keys_in_common(params).length == 1
10
- raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:exactly_one))
9
+ keys = keys_in_common(params)
10
+ return if keys.length == 1
11
+ raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:exactly_one)) if keys.length.zero?
12
+ raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion))
11
13
  end
12
14
  end
13
15
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Grape
4
4
  # The current version of Grape.
5
- VERSION = '1.3.0'
5
+ VERSION = '1.3.1'
6
6
  end