grape 2.4.0 → 4.0.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 (170) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +351 -0
  3. data/CONTRIBUTING.md +18 -10
  4. data/README.md +457 -228
  5. data/UPGRADING.md +1240 -1
  6. data/grape.gemspec +7 -7
  7. data/lib/grape/api/instance.rb +65 -116
  8. data/lib/grape/api.rb +53 -62
  9. data/lib/grape/content_types.rb +58 -3
  10. data/lib/grape/cookies.rb +2 -6
  11. data/lib/grape/declared_params_handler.rb +116 -0
  12. data/lib/grape/dry_types.rb +48 -4
  13. data/lib/grape/dsl/callbacks.rb +8 -58
  14. data/lib/grape/dsl/declared.rb +35 -0
  15. data/lib/grape/dsl/desc.rb +24 -67
  16. data/lib/grape/dsl/entity.rb +106 -0
  17. data/lib/grape/dsl/helpers.rb +80 -65
  18. data/lib/grape/dsl/inside_route.rb +56 -296
  19. data/lib/grape/dsl/logger.rb +4 -9
  20. data/lib/grape/dsl/middleware.rb +22 -40
  21. data/lib/grape/dsl/parameters.rb +86 -93
  22. data/lib/grape/dsl/request_response.rb +139 -139
  23. data/lib/grape/dsl/rescue_options.rb +25 -0
  24. data/lib/grape/dsl/routing.rb +328 -203
  25. data/lib/grape/dsl/settings.rb +29 -135
  26. data/lib/grape/dsl/validations.rb +38 -44
  27. data/lib/grape/dsl/version_options.rb +23 -0
  28. data/lib/grape/endpoint/options.rb +25 -0
  29. data/lib/grape/endpoint.rb +285 -245
  30. data/lib/grape/env.rb +8 -3
  31. data/lib/grape/error_formatter/base.rb +27 -21
  32. data/lib/grape/error_formatter/json.rb +6 -0
  33. data/lib/grape/error_formatter/txt.rb +10 -10
  34. data/lib/grape/error_formatter.rb +6 -2
  35. data/lib/grape/exceptions/base.rb +20 -44
  36. data/lib/grape/exceptions/error_response.rb +48 -0
  37. data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
  38. data/lib/grape/exceptions/internal_server_error.rb +16 -0
  39. data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
  40. data/lib/grape/exceptions/invalid_formatter.rb +1 -1
  41. data/lib/grape/exceptions/invalid_message_body.rb +1 -1
  42. data/lib/grape/exceptions/invalid_version_header.rb +1 -1
  43. data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
  44. data/lib/grape/exceptions/method_not_allowed.rb +1 -1
  45. data/lib/grape/exceptions/missing_group_type.rb +0 -2
  46. data/lib/grape/exceptions/missing_mime_type.rb +1 -1
  47. data/lib/grape/exceptions/request_error.rb +11 -0
  48. data/lib/grape/exceptions/unknown_auth_strategy.rb +1 -1
  49. data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
  50. data/lib/grape/exceptions/unknown_parameter.rb +1 -1
  51. data/lib/grape/exceptions/unknown_params_builder.rb +1 -1
  52. data/lib/grape/exceptions/unknown_validator.rb +1 -1
  53. data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
  54. data/lib/grape/exceptions/validation.rb +25 -7
  55. data/lib/grape/exceptions/validation_array_errors.rb +4 -0
  56. data/lib/grape/exceptions/validation_errors.rb +21 -23
  57. data/lib/grape/formatter/json.rb +1 -0
  58. data/lib/grape/formatter/serializable_hash.rb +6 -9
  59. data/lib/grape/json.rb +38 -2
  60. data/lib/grape/locale/en.yml +3 -5
  61. data/lib/grape/middleware/auth/base.rb +4 -3
  62. data/lib/grape/middleware/auth/dsl.rb +9 -23
  63. data/lib/grape/middleware/base.rb +41 -36
  64. data/lib/grape/middleware/error.rb +271 -73
  65. data/lib/grape/middleware/formatter.rb +152 -63
  66. data/lib/grape/middleware/precomputed_content_types.rb +51 -0
  67. data/lib/grape/middleware/stack.rb +20 -11
  68. data/lib/grape/middleware/versioner/accept_version_header.rb +3 -3
  69. data/lib/grape/middleware/versioner/base.rb +40 -48
  70. data/lib/grape/middleware/versioner/header.rb +5 -23
  71. data/lib/grape/middleware/versioner/param.rb +2 -2
  72. data/lib/grape/middleware/versioner/path.rb +54 -7
  73. data/lib/grape/mountable.rb +22 -0
  74. data/lib/grape/namespace.rb +19 -11
  75. data/lib/grape/params_builder/base.rb +2 -0
  76. data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
  77. data/lib/grape/params_builder.rb +2 -19
  78. data/lib/grape/parser/json.rb +1 -1
  79. data/lib/grape/path.rb +8 -72
  80. data/lib/grape/precompiled_json.rb +50 -0
  81. data/lib/grape/request.rb +57 -20
  82. data/lib/grape/router/base_route.rb +53 -7
  83. data/lib/grape/router/greedy_route.rb +16 -6
  84. data/lib/grape/router/mustermann_pattern.rb +44 -0
  85. data/lib/grape/router/pattern/path.rb +78 -0
  86. data/lib/grape/router/pattern.rb +65 -37
  87. data/lib/grape/router/route.rb +66 -18
  88. data/lib/grape/router.rb +126 -115
  89. data/lib/grape/serve_stream/file_body.rb +7 -0
  90. data/lib/grape/serve_stream/sendfile_response.rb +3 -5
  91. data/lib/grape/serve_stream/stream_response.rb +7 -0
  92. data/lib/grape/testing.rb +33 -0
  93. data/lib/grape/util/api_description.rb +67 -0
  94. data/lib/grape/util/cache.rb +22 -2
  95. data/lib/grape/util/deep_freeze.rb +34 -0
  96. data/lib/grape/util/freeze_on_new.rb +20 -0
  97. data/lib/grape/util/inheritable_setting.rb +777 -39
  98. data/lib/grape/util/lazy/base.rb +16 -0
  99. data/lib/grape/util/lazy/block.rb +2 -9
  100. data/lib/grape/util/lazy/value.rb +2 -9
  101. data/lib/grape/util/lazy/value_enumerable.rb +13 -16
  102. data/lib/grape/util/media_type.rb +13 -9
  103. data/lib/grape/util/path_normalizer.rb +37 -0
  104. data/lib/grape/util/registry.rb +13 -3
  105. data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
  106. data/lib/grape/util/stackable_values.rb +39 -10
  107. data/lib/grape/util/translation.rb +42 -0
  108. data/lib/grape/validations/attributes_iterator.rb +61 -29
  109. data/lib/grape/validations/coerce_options.rb +21 -0
  110. data/lib/grape/validations/contract_scope.rb +8 -13
  111. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  112. data/lib/grape/validations/oneof_collector.rb +35 -0
  113. data/lib/grape/validations/param_scope_tracker.rb +62 -0
  114. data/lib/grape/validations/params_documentation.rb +52 -0
  115. data/lib/grape/validations/params_scope.rb +217 -304
  116. data/lib/grape/validations/shared_options.rb +19 -0
  117. data/lib/grape/validations/single_attribute_iterator.rb +6 -4
  118. data/lib/grape/validations/types/array_coercer.rb +7 -10
  119. data/lib/grape/validations/types/custom_type_coercer.rb +47 -85
  120. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  121. data/lib/grape/validations/types/dry_type_coercer.rb +10 -15
  122. data/lib/grape/validations/types/json.rb +1 -3
  123. data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
  124. data/lib/grape/validations/types/primitive_coercer.rb +14 -33
  125. data/lib/grape/validations/types/set_coercer.rb +1 -1
  126. data/lib/grape/validations/types/variant_collection_coercer.rb +16 -3
  127. data/lib/grape/validations/types.rb +29 -51
  128. data/lib/grape/validations/validations_spec.rb +164 -0
  129. data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
  130. data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
  131. data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
  132. data/lib/grape/validations/validators/base.rb +118 -34
  133. data/lib/grape/validations/validators/coerce_validator.rb +25 -37
  134. data/lib/grape/validations/validators/contract_scope_validator.rb +10 -5
  135. data/lib/grape/validations/validators/default_validator.rb +13 -20
  136. data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
  137. data/lib/grape/validations/validators/except_values_validator.rb +14 -4
  138. data/lib/grape/validations/validators/length_validator.rb +23 -22
  139. data/lib/grape/validations/validators/multiple_params_base.rb +12 -9
  140. data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
  141. data/lib/grape/validations/validators/oneof_validator.rb +51 -0
  142. data/lib/grape/validations/validators/presence_validator.rb +4 -2
  143. data/lib/grape/validations/validators/regexp_validator.rb +11 -3
  144. data/lib/grape/validations/validators/same_as_validator.rb +7 -15
  145. data/lib/grape/validations/validators/values_validator.rb +34 -26
  146. data/lib/grape/version.rb +1 -2
  147. data/lib/grape/xml.rb +8 -1
  148. data/lib/grape.rb +58 -31
  149. metadata +62 -39
  150. data/lib/grape/api/helpers.rb +0 -9
  151. data/lib/grape/dsl/api.rb +0 -17
  152. data/lib/grape/dsl/configuration.rb +0 -15
  153. data/lib/grape/exceptions/conflicting_types.rb +0 -11
  154. data/lib/grape/exceptions/empty_message_body.rb +0 -11
  155. data/lib/grape/exceptions/invalid_parameters.rb +0 -11
  156. data/lib/grape/exceptions/missing_option.rb +0 -11
  157. data/lib/grape/exceptions/too_deep_parameters.rb +0 -11
  158. data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
  159. data/lib/grape/exceptions/unknown_options.rb +0 -11
  160. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -24
  161. data/lib/grape/extensions/hash.rb +0 -27
  162. data/lib/grape/extensions/hashie/mash.rb +0 -24
  163. data/lib/grape/middleware/globals.rb +0 -14
  164. data/lib/grape/types/invalid_value.rb +0 -8
  165. data/lib/grape/util/base_inheritable.rb +0 -43
  166. data/lib/grape/util/inheritable_values.rb +0 -29
  167. data/lib/grape/util/reverse_stackable_values.rb +0 -15
  168. data/lib/grape/util/strict_hash_configuration.rb +0 -108
  169. data/lib/grape/validations/attributes_doc.rb +0 -60
  170. data/lib/grape/validations/validator_factory.rb +0 -15
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ class DeclaredParamsHandler
5
+ def initialize(include_missing: true, evaluate_given: false, stringify: false, contract_key_map: nil)
6
+ @include_missing = include_missing
7
+ @evaluate_given = evaluate_given
8
+ @stringify = stringify
9
+ @contract_key_map = contract_key_map
10
+ end
11
+
12
+ def call(passed_params, declared_params, route_params, renamed_params)
13
+ recursive_declared(
14
+ passed_params,
15
+ declared_params:,
16
+ route_params:,
17
+ renamed_params:
18
+ )
19
+ end
20
+
21
+ private
22
+
23
+ def recursive_declared(passed_params, declared_params:, route_params:, renamed_params:, params_nested_path: [])
24
+ res = if passed_params.is_a?(Array)
25
+ passed_params.map do |passed_param|
26
+ recursive_declared(passed_param, declared_params:, params_nested_path:, renamed_params:, route_params:)
27
+ end
28
+ else
29
+ declared_hash(passed_params, declared_params:, params_nested_path:, renamed_params:, route_params:)
30
+ end
31
+
32
+ @contract_key_map&.each { |key_map| key_map.write(passed_params, res) }
33
+
34
+ res
35
+ end
36
+
37
+ def declared_hash(passed_params, declared_params:, params_nested_path:, renamed_params:, route_params:)
38
+ declared_params.each_with_object(passed_params.class.new) do |declared_param_attr, memo|
39
+ next if @evaluate_given && !declared_param_attr.scope.attr_meets_dependency?(passed_params)
40
+
41
+ declared_hash_attr(
42
+ passed_params,
43
+ declared_param: declared_param_attr.key,
44
+ params_nested_path:,
45
+ memo:,
46
+ renamed_params:,
47
+ route_params:
48
+ )
49
+ end
50
+ end
51
+
52
+ def declared_hash_attr(passed_params, declared_param:, params_nested_path:, memo:, renamed_params:, route_params:)
53
+ return declare_leaf(passed_params, declared_param:, params_nested_path:, memo:, renamed_params:, route_params:) unless declared_param.is_a?(Hash)
54
+
55
+ declared_param.each_pair do |parent, children|
56
+ declare_nested(passed_params, parent:, children:, params_nested_path:, memo:, renamed_params:, route_params:)
57
+ end
58
+ end
59
+
60
+ def declare_nested(passed_params, parent:, children:, params_nested_path:, memo:, renamed_params:, route_params:)
61
+ return unless @include_missing || passed_params.key?(parent)
62
+
63
+ memo_key = build_memo_key(params_nested_path, parent, renamed_params)
64
+ passed_children = passed_params[parent] || passed_params.class.new
65
+ nested_path = nested_path_for(params_nested_path, parent)
66
+
67
+ memo[memo_key] = handle_passed_param(nested_path, route_params:, has_passed_children: passed_children.any?) do
68
+ recursive_declared(
69
+ passed_children,
70
+ declared_params: children,
71
+ params_nested_path: nested_path,
72
+ renamed_params:,
73
+ route_params:
74
+ )
75
+ end
76
+ end
77
+
78
+ # The declared param has no children. Find its value or set it to nil.
79
+ def declare_leaf(passed_params, declared_param:, params_nested_path:, memo:, renamed_params:, route_params:)
80
+ return unless @include_missing || (passed_params.respond_to?(:key?) && passed_params.key?(declared_param))
81
+
82
+ memo_key = build_memo_key(params_nested_path, declared_param, renamed_params)
83
+ passed_param = passed_params[declared_param]
84
+
85
+ memo[memo_key] = passed_param || handle_passed_param(nested_path_for(params_nested_path, declared_param), route_params:) do
86
+ passed_param
87
+ end
88
+ end
89
+
90
+ def build_memo_key(params_nested_path, declared_param, renamed_params)
91
+ renamed_param_name = renamed_params[nested_path_for(params_nested_path, declared_param)]
92
+ param = renamed_param_name || declared_param
93
+ @stringify ? param.to_s : param.to_sym
94
+ end
95
+
96
+ def nested_path_for(parent_path, key)
97
+ parent_path + [key.to_s]
98
+ end
99
+
100
+ def handle_passed_param(params_nested_path, route_params:, has_passed_children: false)
101
+ return yield if has_passed_children
102
+
103
+ key = params_nested_path.first
104
+ key += "[#{params_nested_path[1..].join('][')}]" if params_nested_path.size > 1
105
+
106
+ type = route_params.dig(key, :type)
107
+ return yield if type.nil?
108
+
109
+ return {} if type == 'Hash' && route_params.keys.none? { |k| k != key && k.start_with?("#{key}[") }
110
+ return [] if type == 'Array' || (type.start_with?('[') && !type.include?(','))
111
+ return Set.new if type == 'Set' || type.start_with?('#<Set', 'Set')
112
+
113
+ yield
114
+ end
115
+ end
116
+ end
@@ -2,9 +2,53 @@
2
2
 
3
3
  module Grape
4
4
  module DryTypes
5
- # Call +Dry.Types()+ to add all registered types to +DryTypes+ which is
6
- # a container in this case. Check documentation for more information
7
- # https://dry-rb.org/gems/dry-types/1.2/getting-started/
8
- include Dry.Types()
5
+ # https://dry-rb.org/gems/dry-types/main/getting-started/
6
+ # limit to what Grape is using
7
+ include Dry.Types(:params, :coercible, :strict)
8
+
9
+ class StrictCache < Grape::Util::Cache
10
+ MAPPING = {
11
+ Grape::API::Boolean => DryTypes::Strict::Bool,
12
+ BigDecimal => DryTypes::Strict::Decimal,
13
+ Numeric => DryTypes::Strict::Integer | DryTypes::Strict::Float | DryTypes::Strict::Decimal,
14
+ TrueClass => DryTypes::Strict::Bool.constrained(eql: true),
15
+ FalseClass => DryTypes::Strict::Bool.constrained(eql: false)
16
+ }.freeze
17
+
18
+ def initialize
19
+ super
20
+ @cache = Hash.new do |h, strict_type|
21
+ h[strict_type] = MAPPING.fetch(strict_type) do
22
+ DryTypes.wrapped_dry_types_const_get(DryTypes::Strict, strict_type)
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ class ParamsCache < Grape::Util::Cache
29
+ MAPPING = {
30
+ Grape::API::Boolean => DryTypes::Params::Bool,
31
+ BigDecimal => DryTypes::Params::Decimal,
32
+ Numeric => DryTypes::Params::Integer | DryTypes::Params::Float | DryTypes::Params::Decimal,
33
+ TrueClass => DryTypes::Params::Bool.constrained(eql: true),
34
+ FalseClass => DryTypes::Params::Bool.constrained(eql: false),
35
+ String => DryTypes::Coercible::String
36
+ }.freeze
37
+
38
+ def initialize
39
+ super
40
+ @cache = Hash.new do |h, params_type|
41
+ h[params_type] = MAPPING.fetch(params_type) do
42
+ DryTypes.wrapped_dry_types_const_get(DryTypes::Params, params_type)
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ def self.wrapped_dry_types_const_get(dry_type, type)
49
+ dry_type.const_get(type.name, false)
50
+ rescue NameError
51
+ raise ArgumentError, "type `#{type}` should support coercion via `[]`" unless type.respond_to?(:[])
52
+ end
9
53
  end
10
54
  end
@@ -2,66 +2,16 @@
2
2
 
3
3
  module Grape
4
4
  module DSL
5
- # Blocks can be executed before or after every API call, using `before`, `after`,
6
- # `before_validation` and `after_validation`.
7
- #
8
- # Before and after callbacks execute in the following order:
9
- #
10
- # 1. `before`
11
- # 2. `before_validation`
12
- # 3. _validations_
13
- # 4. `after_validation`
14
- # 5. _the API call_
15
- # 6. `after`
16
- #
17
- # Steps 4, 5 and 6 only happen if validation succeeds.
18
5
  module Callbacks
19
- extend ActiveSupport::Concern
6
+ # before: execute the given block before validation, coercion, or any endpoint
7
+ # before_validation: execute the given block after `before`, but prior to validation or coercion
8
+ # after_validation: execute the given block after validations and coercions, but before any endpoint code
9
+ # after: execute the given block after the endpoint code has run except in unsuccessful
10
+ # finally: execute the given block after the endpoint code even if unsuccessful
20
11
 
21
- include Grape::DSL::Configuration
22
-
23
- module ClassMethods
24
- # Execute the given block before validation, coercion, or any endpoint
25
- # code is executed.
26
- def before(&block)
27
- namespace_stackable(:befores, block)
28
- end
29
-
30
- # Execute the given block after `before`, but prior to validation or
31
- # coercion.
32
- def before_validation(&block)
33
- namespace_stackable(:before_validations, block)
34
- end
35
-
36
- # Execute the given block after validations and coercions, but before
37
- # any endpoint code.
38
- def after_validation(&block)
39
- namespace_stackable(:after_validations, block)
40
- end
41
-
42
- # Execute the given block after the endpoint code has run.
43
- def after(&block)
44
- namespace_stackable(:afters, block)
45
- end
46
-
47
- # Allows you to specify a something that will always be executed after a call
48
- # API call. Unlike the `after` block, this code will run even on
49
- # unsuccesful requests.
50
- # @example
51
- # class ExampleAPI < Grape::API
52
- # before do
53
- # ApiLogger.start
54
- # end
55
- # finally do
56
- # ApiLogger.close
57
- # end
58
- # end
59
- #
60
- # This will make sure that the ApiLogger is opened and closed around every
61
- # request
62
- # @param ensured_block [Proc] The block to be executed after every api_call
63
- def finally(&block)
64
- namespace_stackable(:finallies, block)
12
+ %i[before before_validation after_validation after finally].each do |callback_name|
13
+ define_method callback_name do |&block|
14
+ inheritable_setting.add_callback(callback_name, block)
65
15
  end
66
16
  end
67
17
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module DSL
5
+ module Declared
6
+ # Denotes a situation where a DSL method has been invoked in a
7
+ # filter which it should not yet be available in
8
+ class MethodNotYetAvailable < StandardError
9
+ def initialize(msg = '#declared is not available prior to parameter validation')
10
+ super
11
+ end
12
+ end
13
+
14
+ # A filtering method that will return a hash
15
+ # consisting only of keys that have been declared by a
16
+ # `params` statement against the current/target endpoint or parent
17
+ # namespaces.
18
+ # @param params [Hash] The initial hash to filter. Usually this will just be `params`
19
+ # @param options [Hash] Can pass `:include_missing`, `:stringify` and `:include_parent_namespaces`
20
+ # options. `:include_parent_namespaces` defaults to true, hence must be set to false if
21
+ # you want only to return params declared against the current/target endpoint.
22
+ def declared(passed_params, include_parent_namespaces: true, include_missing: true, evaluate_given: false, stringify: false)
23
+ raise MethodNotYetAvailable unless before_filter_passed
24
+
25
+ contract_key_map = inheritable_setting.contract_key_maps
26
+ handler = DeclaredParamsHandler.new(include_missing:, evaluate_given:, stringify:, contract_key_map:)
27
+ declared_params = include_parent_namespaces ? inheritable_setting.route_declared_params : (inheritable_setting.declared_params.last || [])
28
+ renamed_params = inheritable_setting.route_renamed_params
29
+ route_params = config.params
30
+
31
+ handler.call(passed_params, declared_params, route_params, renamed_params)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -3,34 +3,14 @@
3
3
  module Grape
4
4
  module DSL
5
5
  module Desc
6
- include Grape::DSL::Settings
7
-
8
- ROUTE_ATTRIBUTES = %i[
9
- body_name
10
- consumes
11
- default
12
- deprecated
13
- description
14
- detail
15
- entity
16
- headers
17
- hidden
18
- http_codes
19
- is_array
20
- named
21
- nickname
22
- params
23
- produces
24
- security
25
- summary
26
- tags
27
- ].freeze
6
+ extend Grape::DSL::Settings
28
7
 
29
8
  # Add a description to the next namespace or function.
30
9
  # @param description [String] descriptive string for this endpoint
31
10
  # or namespace
32
11
  # @param options [Hash] other properties you can set to describe the
33
- # endpoint or namespace. Optional.
12
+ # endpoint or namespace. Optional. Pass these as keyword arguments;
13
+ # passing a positional options Hash is deprecated.
34
14
  # @option options :detail [String] additional detail about this endpoint
35
15
  # @option options :summary [String] summary for this endpoint
36
16
  # @option options :params [Hash] param types and info. normally, you set
@@ -39,6 +19,9 @@ module Grape
39
19
  # successful call to this action
40
20
  # @option options :http_codes [Array[Array]] possible HTTP codes this
41
21
  # endpoint may return, with their meanings, in a 2d array
22
+ # @option options :default_response [Hash] the definition and entity used
23
+ # to present this endpoint's default response. Was `:default`, which is
24
+ # deprecated.
42
25
  # @option options :named [String] a specific name to help find this route
43
26
  # @option options :body_name [String] override the autogenerated body name param
44
27
  # @option options :headers [Hash] HTTP headers this method can accept
@@ -70,54 +53,28 @@ module Grape
70
53
  # # ...
71
54
  # end
72
55
  #
73
- def desc(description, options = nil, &config_block)
74
- opts =
75
- if config_block
76
- desc_container(endpoint_configuration).then do |config_class|
77
- config_class.configure do
78
- description(description)
79
- end
80
-
81
- config_class.configure(&config_block)
82
- config_class.settings
83
- end
56
+ def desc(description, **options, &config_block)
57
+ resolved_options =
58
+ if options.key?(:default)
59
+ Grape.deprecator.warn('The `default` option of `desc` is deprecated. Use `default_response` instead.')
60
+ # Rebuilt rather than mutated in place: an explicit +default_response+
61
+ # still wins, since the merged Hash is the one that keeps its key.
62
+ { default_response: options[:default] }.merge(options.except(:default))
84
63
  else
85
- options&.merge(description: description) || { description: description }
86
- end
87
-
88
- namespace_setting :description, opts
89
- route_setting :description, opts
90
- end
91
-
92
- # Returns an object which configures itself via an instance-context DSL.
93
- def desc_container(endpoint_configuration)
94
- Module.new do
95
- include Grape::Util::StrictHashConfiguration.module(*ROUTE_ATTRIBUTES)
96
- config_context.define_singleton_method(:configuration) do
97
- endpoint_configuration
64
+ options
98
65
  end
99
66
 
100
- def config_context.success(*args)
101
- entity(*args)
102
- end
103
-
104
- def config_context.failure(*args)
105
- http_codes(*args)
67
+ settings =
68
+ if config_block
69
+ endpoint_config = defined?(configuration) ? configuration : nil
70
+ Grape::Util::ApiDescription.new(description, endpoint_config, &config_block).settings
71
+ else
72
+ resolved_options.merge(description:)
106
73
  end
107
- end
108
- end
109
-
110
- private
111
-
112
- def endpoint_configuration
113
- return {} unless defined?(configuration)
114
-
115
- if configuration.respond_to?(:evaluate)
116
- configuration.evaluate
117
- # Within `given` or `mounted blocks` the configuration is already evaluated
118
- elsif configuration.is_a?(Hash)
119
- configuration
120
- end
74
+ # Only the route scope is consumed downstream (by +route+ and the
75
+ # route's readers, e.g. +http_codes+); the namespace scope was
76
+ # write-only, so it is no longer populated.
77
+ inheritable_setting.route_description = settings
121
78
  end
122
79
  end
123
80
  end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module DSL
5
+ module Entity
6
+ # Allows you to make use of Grape Entities by setting
7
+ # the response body to the serializable hash of the
8
+ # entity provided in the `:with` option. This has the
9
+ # added benefit of automatically passing along environment
10
+ # and version information to the serialization, making it
11
+ # very easy to do conditional exposures. See Entity docs
12
+ # for more info.
13
+ #
14
+ # @param args [Array] either `(object)` or `(key, object)` where key is a Symbol
15
+ # used to nest the representation under that key in the response body.
16
+ # @param root [Symbol, String, nil] wraps the representation under this root key.
17
+ # @param with [Class, nil] the entity class to use for representation.
18
+ # If omitted, the entity class is inferred from the object via {#entity_class_for_obj}.
19
+ # @param options [Hash] additional options forwarded to the entity's `represent` call.
20
+ #
21
+ # @example
22
+ #
23
+ # get '/users/:id' do
24
+ # present User.find(params[:id]),
25
+ # with: API::Entities::User,
26
+ # admin: current_user.admin?
27
+ # end
28
+ def present(*args, root: nil, with: nil, **options)
29
+ key, object = args.count == 2 && args.first.is_a?(Symbol) ? args : [nil, args.first]
30
+ entity_class = with || entity_class_for_obj(object)
31
+ representation = entity_class ? entity_representation_for(entity_class, object, options) : object
32
+ representation = { root => representation } if root
33
+
34
+ if key
35
+ representation = body&.merge(key => representation) || { key => representation }
36
+ elsif entity_class.present? && body
37
+ raise ArgumentError, "representation of type `#{representation.class}` cannot be merged" unless representation.respond_to?(:merge)
38
+
39
+ representation = body.merge(representation)
40
+ end
41
+
42
+ body representation
43
+ end
44
+
45
+ # Attempt to locate the Entity class for a given object, if not given
46
+ # explicitly. This is done by looking for the presence of Klass::Entity,
47
+ # where Klass is the class of the `object` parameter, or one of its
48
+ # ancestors. Object is excluded from the search: top-level constants
49
+ # live on it, so a global ::Entity class is not a representer.
50
+ # @param object [Object] the object to locate the Entity class for
51
+ # @return [Class] the located Entity class, or nil if none is found
52
+ def entity_class_for_obj(object)
53
+ entity_for_class(object.class) || entity_for_class(element_class(object))
54
+ end
55
+
56
+ private
57
+
58
+ # The class standing in for a collection or wrapper: ActiveRecord::Relation
59
+ # and the like expose #klass, anything else falls back to the class of its
60
+ # first element.
61
+ #
62
+ # Consulted only once the object's own class has come up empty, because
63
+ # both tests are duck-typed and plenty of single objects answer them —
64
+ # a Struct is Enumerable, so it responds to #first, and so does any model
65
+ # that includes Enumerable. Asking this first meant `represent Model,
66
+ # with: Entity` was silently ignored for those, the entity for the
67
+ # *element* type being looked up instead. Deferring it also keeps #first
68
+ # from being called at all when the object resolves on its own class.
69
+ #
70
+ # @param object [Object] the object to represent.
71
+ # @return [Class, nil]
72
+ def element_class(object)
73
+ return object.klass if object.respond_to?(:klass)
74
+
75
+ object.first.class if object.respond_to?(:first)
76
+ end
77
+
78
+ # @param klass [Class, nil] the class to look an entity up for.
79
+ # @return [Class, nil] the registered or conventionally named entity.
80
+ def entity_for_class(klass)
81
+ return if klass.nil?
82
+
83
+ representations = inheritable_setting.representations
84
+ if representations
85
+ potential = klass.ancestors.detect { |ancestor| representations.key?(ancestor) }
86
+ return representations[potential] if potential && representations[potential]
87
+ end
88
+
89
+ owner = klass.ancestors.detect { |ancestor| ancestor != Object && ancestor.const_defined?(:Entity, false) }
90
+ return unless owner
91
+
92
+ entity = owner.const_get(:Entity, false)
93
+ entity if entity.respond_to?(:represent)
94
+ end
95
+
96
+ # @param entity_class [Class] the entity class to use for representation.
97
+ # @param object [Object] the object to represent.
98
+ # @param options [Hash] additional options forwarded to the entity's `represent` call.
99
+ # @return the representation of the given object as done through the given entity_class.
100
+ def entity_representation_for(entity_class, object, options)
101
+ embeds = env.key?(Grape::Env::API_VERSION) ? { env:, version: env[Grape::Env::API_VERSION] } : { env: }
102
+ entity_class.represent(object, **embeds, **options)
103
+ end
104
+ end
105
+ end
106
+ end