grape 2.0.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +96 -1
  3. data/README.md +364 -317
  4. data/UPGRADING.md +205 -7
  5. data/grape.gemspec +7 -7
  6. data/lib/grape/api/instance.rb +14 -11
  7. data/lib/grape/api.rb +19 -10
  8. data/lib/grape/content_types.rb +13 -10
  9. data/lib/grape/cookies.rb +2 -1
  10. data/lib/grape/dry_types.rb +0 -2
  11. data/lib/grape/dsl/desc.rb +22 -20
  12. data/lib/grape/dsl/headers.rb +1 -1
  13. data/lib/grape/dsl/helpers.rb +7 -3
  14. data/lib/grape/dsl/inside_route.rb +51 -15
  15. data/lib/grape/dsl/parameters.rb +5 -4
  16. data/lib/grape/dsl/request_response.rb +14 -18
  17. data/lib/grape/dsl/routing.rb +20 -4
  18. data/lib/grape/dsl/validations.rb +13 -0
  19. data/lib/grape/endpoint.rb +43 -35
  20. data/lib/grape/{util/env.rb → env.rb} +0 -5
  21. data/lib/grape/error_formatter/json.rb +13 -4
  22. data/lib/grape/error_formatter/txt.rb +11 -10
  23. data/lib/grape/error_formatter.rb +13 -25
  24. data/lib/grape/exceptions/base.rb +3 -3
  25. data/lib/grape/exceptions/validation.rb +0 -2
  26. data/lib/grape/exceptions/validation_array_errors.rb +1 -0
  27. data/lib/grape/exceptions/validation_errors.rb +2 -4
  28. data/lib/grape/extensions/hash.rb +5 -1
  29. data/lib/grape/formatter.rb +15 -25
  30. data/lib/grape/http/headers.rb +18 -34
  31. data/lib/grape/{util/json.rb → json.rb} +1 -3
  32. data/lib/grape/locale/en.yml +4 -0
  33. data/lib/grape/middleware/auth/base.rb +0 -2
  34. data/lib/grape/middleware/auth/dsl.rb +0 -2
  35. data/lib/grape/middleware/base.rb +14 -15
  36. data/lib/grape/middleware/error.rb +61 -54
  37. data/lib/grape/middleware/formatter.rb +18 -15
  38. data/lib/grape/middleware/globals.rb +1 -3
  39. data/lib/grape/middleware/stack.rb +4 -5
  40. data/lib/grape/middleware/versioner/accept_version_header.rb +8 -33
  41. data/lib/grape/middleware/versioner/header.rb +62 -123
  42. data/lib/grape/middleware/versioner/param.rb +5 -23
  43. data/lib/grape/middleware/versioner/path.rb +11 -33
  44. data/lib/grape/middleware/versioner.rb +5 -14
  45. data/lib/grape/middleware/versioner_helpers.rb +75 -0
  46. data/lib/grape/namespace.rb +3 -4
  47. data/lib/grape/parser.rb +8 -24
  48. data/lib/grape/path.rb +24 -29
  49. data/lib/grape/request.rb +4 -12
  50. data/lib/grape/router/base_route.rb +39 -0
  51. data/lib/grape/router/greedy_route.rb +20 -0
  52. data/lib/grape/router/pattern.rb +39 -30
  53. data/lib/grape/router/route.rb +22 -59
  54. data/lib/grape/router.rb +32 -37
  55. data/lib/grape/util/base_inheritable.rb +4 -4
  56. data/lib/grape/util/cache.rb +0 -3
  57. data/lib/grape/util/endpoint_configuration.rb +1 -1
  58. data/lib/grape/util/header.rb +13 -0
  59. data/lib/grape/util/inheritable_values.rb +0 -2
  60. data/lib/grape/util/lazy/block.rb +29 -0
  61. data/lib/grape/util/lazy/object.rb +45 -0
  62. data/lib/grape/util/lazy/value.rb +38 -0
  63. data/lib/grape/util/lazy/value_array.rb +21 -0
  64. data/lib/grape/util/lazy/value_enumerable.rb +34 -0
  65. data/lib/grape/util/lazy/value_hash.rb +21 -0
  66. data/lib/grape/util/media_type.rb +70 -0
  67. data/lib/grape/util/reverse_stackable_values.rb +1 -6
  68. data/lib/grape/util/stackable_values.rb +1 -6
  69. data/lib/grape/util/strict_hash_configuration.rb +3 -3
  70. data/lib/grape/validations/attributes_doc.rb +38 -36
  71. data/lib/grape/validations/attributes_iterator.rb +1 -0
  72. data/lib/grape/validations/contract_scope.rb +71 -0
  73. data/lib/grape/validations/params_scope.rb +22 -19
  74. data/lib/grape/validations/types/array_coercer.rb +0 -2
  75. data/lib/grape/validations/types/build_coercer.rb +69 -71
  76. data/lib/grape/validations/types/dry_type_coercer.rb +1 -11
  77. data/lib/grape/validations/types/json.rb +0 -2
  78. data/lib/grape/validations/types/primitive_coercer.rb +0 -2
  79. data/lib/grape/validations/types/set_coercer.rb +0 -3
  80. data/lib/grape/validations/types.rb +0 -3
  81. data/lib/grape/validations/validators/base.rb +1 -0
  82. data/lib/grape/validations/validators/default_validator.rb +5 -1
  83. data/lib/grape/validations/validators/exactly_one_of_validator.rb +1 -1
  84. data/lib/grape/validations/validators/length_validator.rb +49 -0
  85. data/lib/grape/validations/validators/values_validator.rb +6 -1
  86. data/lib/grape/validations.rb +3 -7
  87. data/lib/grape/version.rb +1 -1
  88. data/lib/grape/{util/xml.rb → xml.rb} +1 -1
  89. data/lib/grape.rb +30 -274
  90. metadata +31 -38
  91. data/lib/grape/eager_load.rb +0 -20
  92. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +0 -24
  93. data/lib/grape/router/attribute_translator.rb +0 -63
  94. data/lib/grape/util/lazy_block.rb +0 -27
  95. data/lib/grape/util/lazy_object.rb +0 -43
  96. data/lib/grape/util/lazy_value.rb +0 -91
  97. data/lib/grape/util/registrable.rb +0 -15
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ class Router
5
+ class BaseRoute
6
+ delegate_missing_to :@options
7
+
8
+ attr_reader :index, :pattern, :options
9
+
10
+ def initialize(**options)
11
+ @options = ActiveSupport::OrderedOptions.new.update(options)
12
+ end
13
+
14
+ alias attributes options
15
+
16
+ def regexp_capture_index
17
+ CaptureIndexCache[index]
18
+ end
19
+
20
+ def pattern_regexp
21
+ pattern.to_regexp
22
+ end
23
+
24
+ def to_regexp(index)
25
+ @index = index
26
+ Regexp.new("(?<#{regexp_capture_index}>#{pattern_regexp})")
27
+ end
28
+
29
+ class CaptureIndexCache < Grape::Util::Cache
30
+ def initialize
31
+ super
32
+ @cache = Hash.new do |h, index|
33
+ h[index] = "_#{index}"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Act like a Grape::Router::Route but for greedy_match
4
+ # see @neutral_map
5
+
6
+ module Grape
7
+ class Router
8
+ class GreedyRoute < BaseRoute
9
+ def initialize(pattern:, **options)
10
+ @pattern = pattern
11
+ super(**options)
12
+ end
13
+
14
+ # Grape::Router:Route defines params as a function
15
+ def params(_input = nil)
16
+ options[:params] || {}
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,62 +1,71 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'forwardable'
4
- require 'mustermann/grape'
5
- require 'grape/util/cache'
6
-
7
3
  module Grape
8
4
  class Router
9
5
  class Pattern
10
- DEFAULT_PATTERN_OPTIONS = { uri_decode: true }.freeze
11
- DEFAULT_SUPPORTED_CAPTURE = %i[format version].freeze
6
+ extend Forwardable
7
+
8
+ DEFAULT_CAPTURES = %w[format version].freeze
12
9
 
13
10
  attr_reader :origin, :path, :pattern, :to_regexp
14
11
 
15
- extend Forwardable
16
12
  def_delegators :pattern, :named_captures, :params
17
13
  def_delegators :to_regexp, :===
18
14
  alias match? ===
19
15
 
20
16
  def initialize(pattern, **options)
21
- @origin = pattern
22
- @path = build_path(pattern, **options)
23
- @pattern = Mustermann::Grape.new(@path, **pattern_options(options))
17
+ @origin = pattern
18
+ @path = build_path(pattern, anchor: options[:anchor], suffix: options[:suffix])
19
+ @pattern = build_pattern(@path, options)
24
20
  @to_regexp = @pattern.to_regexp
25
21
  end
26
22
 
23
+ def captures_default
24
+ to_regexp.names
25
+ .delete_if { |n| DEFAULT_CAPTURES.include?(n) }
26
+ .to_h { |k| [k, ''] }
27
+ end
28
+
27
29
  private
28
30
 
29
- def pattern_options(options)
30
- capture = extract_capture(**options)
31
- options = DEFAULT_PATTERN_OPTIONS.dup
32
- options[:capture] = capture if capture.present?
33
- options
31
+ def build_pattern(path, options)
32
+ Mustermann::Grape.new(
33
+ path,
34
+ uri_decode: true,
35
+ params: options[:params],
36
+ capture: extract_capture(**options)
37
+ )
34
38
  end
35
39
 
36
- def build_path(pattern, anchor: false, suffix: nil, **_options)
37
- unless anchor || pattern.end_with?('*path')
38
- pattern = +pattern
39
- pattern << '/' unless pattern.end_with?('/')
40
- pattern << '*path'
41
- end
40
+ def build_path(pattern, anchor: false, suffix: nil)
41
+ PatternCache[[build_path_from_pattern(pattern, anchor: anchor), suffix]]
42
+ end
42
43
 
43
- pattern = -pattern.split('/').tap do |parts|
44
- parts[parts.length - 1] = "?#{parts.last}"
45
- end.join('/') if pattern.end_with?('*path')
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?
46
50
 
47
- PatternCache[[pattern, suffix]]
51
+ options[:requirements].merge(sliced_options)
48
52
  end
49
53
 
50
- def extract_capture(requirements: {}, **options)
51
- requirements = {}.merge(requirements)
52
- DEFAULT_SUPPORTED_CAPTURE.each_with_object(requirements) do |field, capture|
53
- option = Array(options[field])
54
- capture[field] = option.map(&:to_s) if option.present?
54
+ def build_path_from_pattern(pattern, anchor: false)
55
+ if pattern.end_with?('*path')
56
+ pattern.dup.insert(pattern.rindex('/') + 1, '?')
57
+ elsif anchor
58
+ pattern
59
+ elsif pattern.end_with?('/')
60
+ "#{pattern}?*path"
61
+ else
62
+ "#{pattern}/?*path"
55
63
  end
56
64
  end
57
65
 
58
66
  class PatternCache < Grape::Util::Cache
59
67
  def initialize
68
+ super
60
69
  @cache = Hash.new do |h, (pattern, suffix)|
61
70
  h[[pattern, suffix]] = -"#{pattern}#{suffix}"
62
71
  end
@@ -1,57 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/router/pattern'
4
- require 'grape/router/attribute_translator'
5
- require 'forwardable'
6
- require 'pathname'
7
-
8
3
  module Grape
9
4
  class Router
10
- class Route
11
- ROUTE_ATTRIBUTE_REGEXP = /route_([_a-zA-Z]\w*)/.freeze
12
- SOURCE_LOCATION_REGEXP = /^(.*?):(\d+?)(?::in `.+?')?$/.freeze
13
- FIXED_NAMED_CAPTURES = %w[format version].freeze
14
-
15
- attr_accessor :pattern, :translator, :app, :index, :options
16
-
17
- alias attributes translator
18
-
5
+ class Route < BaseRoute
19
6
  extend Forwardable
20
- def_delegators :pattern, :path, :origin
21
- delegate Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES => :attributes
22
7
 
23
- def method_missing(method_id, *arguments)
24
- match = ROUTE_ATTRIBUTE_REGEXP.match(method_id.to_s)
25
- if match
26
- method_name = match.captures.last.to_sym
27
- warn_route_methods(method_name, caller(1).shift)
28
- @options[method_name]
29
- else
30
- super
31
- end
32
- end
33
-
34
- def respond_to_missing?(method_id, _)
35
- ROUTE_ATTRIBUTE_REGEXP.match?(method_id.to_s)
36
- end
37
-
38
- def route_method
39
- warn_route_methods(:method, caller(1).shift, :request_method)
40
- request_method
41
- end
8
+ attr_reader :app, :request_method
42
9
 
43
- def route_path
44
- warn_route_methods(:path, caller(1).shift)
45
- pattern.path
46
- end
10
+ def_delegators :pattern, :path, :origin
47
11
 
48
12
  def initialize(method, pattern, **options)
49
- method_s = method.to_s
50
- method_upcase = Grape::Http::Headers.find_supported_method(method_s) || method_s.upcase
51
-
52
- @options = options.merge(method: method_upcase)
53
- @pattern = Pattern.new(pattern, **options)
54
- @translator = AttributeTranslator.new(**options, request_method: method_upcase)
13
+ @request_method = upcase_method(method)
14
+ @pattern = Grape::Router::Pattern.new(pattern, **options)
15
+ super(**options)
55
16
  end
56
17
 
57
18
  def exec(env)
@@ -64,27 +25,29 @@ module Grape
64
25
  end
65
26
 
66
27
  def match?(input)
67
- translator.respond_to?(:forward_match) && translator.forward_match ? input.start_with?(pattern.origin) : pattern.match?(input)
28
+ return false if input.blank?
29
+
30
+ options[:forward_match] ? input.start_with?(pattern.origin) : pattern.match?(input)
68
31
  end
69
32
 
70
33
  def params(input = nil)
71
- if input.nil?
72
- pattern.named_captures.keys.each_with_object(translator.params) do |(key), defaults|
73
- defaults[key] ||= '' unless FIXED_NAMED_CAPTURES.include?(key) || defaults.key?(key)
74
- end
75
- else
76
- parsed = pattern.params(input)
77
- parsed ? parsed.delete_if { |_, value| value.nil? }.symbolize_keys : {}
78
- end
34
+ return params_without_input if input.blank?
35
+
36
+ parsed = pattern.params(input)
37
+ return {} unless parsed
38
+
39
+ parsed.compact.symbolize_keys
79
40
  end
80
41
 
81
42
  private
82
43
 
83
- def warn_route_methods(name, location, expected = nil)
84
- path, line = *location.scan(SOURCE_LOCATION_REGEXP).first
85
- path = File.realpath(path) if Pathname.new(path).relative?
86
- expected ||= name
87
- Grape.deprecator.warn("#{path}:#{line}: The route_xxx methods such as route_#{name} have been deprecated, please use #{expected}.")
44
+ def params_without_input
45
+ pattern.captures_default.merge(attributes.params)
46
+ end
47
+
48
+ def upcase_method(method)
49
+ method_s = method.to_s
50
+ Grape::Http::Headers::SUPPORTED_METHODS.detect { |m| m.casecmp(method_s).zero? } || method_s.upcase
88
51
  end
89
52
  end
90
53
  end
data/lib/grape/router.rb CHANGED
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/router/route'
4
- require 'grape/util/cache'
5
-
6
3
  module Grape
7
4
  class Router
8
5
  attr_reader :map, :compiled
@@ -15,10 +12,6 @@ module Grape
15
12
  path
16
13
  end
17
14
 
18
- def self.supported_methods
19
- @supported_methods ||= Grape::Http::Headers::SUPPORTED_METHODS + ['*']
20
- end
21
-
22
15
  def initialize
23
16
  @neutral_map = []
24
17
  @neutral_regexes = []
@@ -31,13 +24,12 @@ module Grape
31
24
 
32
25
  @union = Regexp.union(@neutral_regexes)
33
26
  @neutral_regexes = nil
34
- self.class.supported_methods.each do |method|
27
+ (Grape::Http::Headers::SUPPORTED_METHODS + ['*']).each do |method|
28
+ next unless map.key?(method)
29
+
35
30
  routes = map[method]
36
- @optimized_map[method] = routes.map.with_index do |route, index|
37
- route.index = index
38
- Regexp.new("(?<_#{index}>#{route.pattern.to_regexp})")
39
- end
40
- @optimized_map[method] = Regexp.union(@optimized_map[method])
31
+ optimized_map = routes.map.with_index { |route, index| route.to_regexp(index) }
32
+ @optimized_map[method] = Regexp.union(optimized_map)
41
33
  end
42
34
  @compiled = true
43
35
  end
@@ -47,8 +39,10 @@ module Grape
47
39
  end
48
40
 
49
41
  def associate_routes(pattern, **options)
50
- @neutral_regexes << Regexp.new("(?<_#{@neutral_map.length}>)#{pattern.to_regexp}")
51
- @neutral_map << Grape::Router::AttributeTranslator.new(**options, pattern: pattern, index: @neutral_map.length)
42
+ Grape::Router::GreedyRoute.new(pattern: pattern, **options).then do |greedy_route|
43
+ @neutral_regexes << greedy_route.to_regexp(@neutral_map.length)
44
+ @neutral_map << greedy_route
45
+ end
52
46
  end
53
47
 
54
48
  def call(env)
@@ -91,26 +85,33 @@ module Grape
91
85
 
92
86
  def transaction(env)
93
87
  input, method = *extract_input_and_method(env)
94
- response = yield(input, method)
95
88
 
96
- return response if response && !(cascade = cascade?(response))
89
+ # using a Proc is important since `return` will exit the enclosing function
90
+ cascade_or_return_response = proc do |response|
91
+ if response
92
+ cascade?(response).tap do |cascade|
93
+ return response unless cascade
97
94
 
95
+ # we need to close the body if possible before dismissing
96
+ response[2].close if response[2].respond_to?(:close)
97
+ end
98
+ end
99
+ end
100
+
101
+ last_response_cascade = cascade_or_return_response.call(yield(input, method))
98
102
  last_neighbor_route = greedy_match?(input)
99
103
 
100
104
  # If last_neighbor_route exists and request method is OPTIONS,
101
105
  # return response by using #call_with_allow_headers.
102
- return call_with_allow_headers(env, last_neighbor_route) if last_neighbor_route && method == Grape::Http::Headers::OPTIONS && !cascade
106
+ return call_with_allow_headers(env, last_neighbor_route) if last_neighbor_route && method == Rack::OPTIONS && !last_response_cascade
103
107
 
104
108
  route = match?(input, '*')
105
109
 
106
- return last_neighbor_route.endpoint.call(env) if last_neighbor_route && cascade && route
110
+ return last_neighbor_route.endpoint.call(env) if last_neighbor_route && last_response_cascade && route
107
111
 
108
- if route
109
- response = process_route(route, env)
110
- return response if response && !(cascade = cascade?(response))
111
- end
112
+ last_response_cascade = cascade_or_return_response.call(process_route(route, env)) if route
112
113
 
113
- return call_with_allow_headers(env, last_neighbor_route) if !cascade && last_neighbor_route
114
+ return call_with_allow_headers(env, last_neighbor_route) if !last_response_cascade && last_neighbor_route
114
115
 
115
116
  nil
116
117
  end
@@ -122,12 +123,12 @@ module Grape
122
123
 
123
124
  def make_routing_args(default_args, route, input)
124
125
  args = default_args || { route_info: route }
125
- args.merge(route.params(input) || {})
126
+ args.merge(route.params(input))
126
127
  end
127
128
 
128
129
  def extract_input_and_method(env)
129
- input = string_for(env[Grape::Http::Headers::PATH_INFO])
130
- method = env[Grape::Http::Headers::REQUEST_METHOD]
130
+ input = string_for(env[Rack::PATH_INFO])
131
+ method = env[Rack::REQUEST_METHOD]
131
132
  [input, method]
132
133
  end
133
134
 
@@ -137,22 +138,16 @@ module Grape
137
138
  end
138
139
 
139
140
  def default_response
140
- [404, { Grape::Http::Headers::X_CASCADE => 'pass' }, ['404 Not Found']]
141
+ headers = Grape::Util::Header.new.merge(Grape::Http::Headers::X_CASCADE => 'pass')
142
+ [404, headers, ['404 Not Found']]
141
143
  end
142
144
 
143
145
  def match?(input, method)
144
- current_regexp = @optimized_map[method]
145
- return unless current_regexp.match(input)
146
-
147
- last_match = Regexp.last_match
148
- @map[method].detect { |route| last_match["_#{route.index}"] }
146
+ @optimized_map[method].match(input) { |m| @map[method].detect { |route| m[route.regexp_capture_index] } }
149
147
  end
150
148
 
151
149
  def greedy_match?(input)
152
- return unless @union.match(input)
153
-
154
- last_match = Regexp.last_match
155
- @neutral_map.detect { |route| last_match["_#{route.index}"] }
150
+ @union.match(input) { |m| @neutral_map.detect { |route| m[route.regexp_capture_index] } }
156
151
  end
157
152
 
158
153
  def call_with_allow_headers(env, route)
@@ -26,10 +26,10 @@ module Grape
26
26
 
27
27
  def keys
28
28
  if new_values.any?
29
- combined = inherited_values.keys
30
- combined.concat(new_values.keys)
31
- combined.uniq!
32
- combined
29
+ inherited_values.keys.tap do |combined|
30
+ combined.concat(new_values.keys)
31
+ combined.uniq!
32
+ end
33
33
  else
34
34
  inherited_values.keys
35
35
  end
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'singleton'
4
- require 'forwardable'
5
-
6
3
  module Grape
7
4
  module Util
8
5
  class Cache
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Grape
4
4
  module Util
5
- class EndpointConfiguration < LazyValueHash
5
+ class EndpointConfiguration < Lazy::ValueHash
6
6
  end
7
7
  end
8
8
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ if Gem::Version.new(Rack.release) >= Gem::Version.new('3')
6
+ require 'rack/headers'
7
+ Header = Rack::Headers
8
+ else
9
+ require 'rack/utils'
10
+ Header = Rack::Utils::HeaderHash
11
+ end
12
+ end
13
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'base_inheritable'
4
-
5
3
  module Grape
6
4
  module Util
7
5
  class InheritableValues < BaseInheritable
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module Lazy
6
+ class Block
7
+ def initialize(&new_block)
8
+ @block = new_block
9
+ end
10
+
11
+ def evaluate_from(configuration)
12
+ @block.call(configuration)
13
+ end
14
+
15
+ def evaluate
16
+ @block.call({})
17
+ end
18
+
19
+ def lazy?
20
+ true
21
+ end
22
+
23
+ def to_s
24
+ evaluate.to_s
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Based on https://github.com/HornsAndHooves/lazy_object
4
+
5
+ module Grape
6
+ module Util
7
+ module Lazy
8
+ class Object < BasicObject
9
+ attr_reader :callable
10
+
11
+ def initialize(&callable)
12
+ @callable = callable
13
+ end
14
+
15
+ def __target_object__
16
+ @__target_object__ ||= callable.call
17
+ end
18
+
19
+ def ==(other)
20
+ __target_object__ == other
21
+ end
22
+
23
+ def !=(other)
24
+ __target_object__ != other
25
+ end
26
+
27
+ def !
28
+ !__target_object__
29
+ end
30
+
31
+ def method_missing(method_name, *args, &block)
32
+ if __target_object__.respond_to?(method_name)
33
+ __target_object__.send(method_name, *args, &block)
34
+ else
35
+ super
36
+ end
37
+ end
38
+
39
+ def respond_to_missing?(method_name, include_priv = false)
40
+ __target_object__.respond_to?(method_name, include_priv)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module Lazy
6
+ class Value
7
+ attr_reader :access_keys
8
+
9
+ def initialize(value, access_keys = [])
10
+ @value = value
11
+ @access_keys = access_keys
12
+ end
13
+
14
+ def evaluate_from(configuration)
15
+ matching_lazy_value = configuration.fetch(@access_keys)
16
+ matching_lazy_value.evaluate
17
+ end
18
+
19
+ def evaluate
20
+ @value
21
+ end
22
+
23
+ def lazy?
24
+ true
25
+ end
26
+
27
+ def reached_by(parent_access_keys, access_key)
28
+ @access_keys = parent_access_keys + [access_key]
29
+ self
30
+ end
31
+
32
+ def to_s
33
+ evaluate.to_s
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module Lazy
6
+ class ValueArray < ValueEnumerable
7
+ def initialize(array)
8
+ super
9
+ @value_hash = []
10
+ array.each_with_index do |value, index|
11
+ self[index] = value
12
+ end
13
+ end
14
+
15
+ def evaluate
16
+ @value_hash.map(&:evaluate)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module Lazy
6
+ class ValueEnumerable < Value
7
+ def [](key)
8
+ if @value_hash[key].nil?
9
+ Value.new(nil).reached_by(access_keys, key)
10
+ else
11
+ @value_hash[key].reached_by(access_keys, key)
12
+ end
13
+ end
14
+
15
+ def fetch(access_keys)
16
+ fetched_keys = access_keys.dup
17
+ value = self[fetched_keys.shift]
18
+ fetched_keys.any? ? value.fetch(fetched_keys) : value
19
+ end
20
+
21
+ def []=(key, value)
22
+ @value_hash[key] = case value
23
+ when Hash
24
+ ValueHash.new(value)
25
+ when Array
26
+ ValueArray.new(value)
27
+ else
28
+ Value.new(value)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module Lazy
6
+ class ValueHash < ValueEnumerable
7
+ def initialize(hash)
8
+ super
9
+ @value_hash = ActiveSupport::HashWithIndifferentAccess.new
10
+ hash.each do |key, value|
11
+ self[key] = value
12
+ end
13
+ end
14
+
15
+ def evaluate
16
+ @value_hash.transform_values(&:evaluate)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end