grape 1.3.0 → 1.3.1

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