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
@@ -17,23 +17,70 @@ module Grape
17
17
  # env['api.version'] => 'v1'
18
18
  #
19
19
  class Path < Base
20
+ def initialize(app, **options)
21
+ super
22
+ @prefixes = [mount_path, Grape::Util::PathNormalizer.call(prefix)].select { |p| p.present? && p != '/' }.freeze
23
+ end
24
+
20
25
  def before
21
- path_info = Grape::Router.normalize_path(env[Rack::PATH_INFO])
26
+ routed = routed_version
27
+ return env[Grape::Env::API_VERSION] = routed if routed
28
+
29
+ path_info = env[Grape::Env::GRAPE_NORMALIZED_PATH] || Grape::Util::PathNormalizer.call(env[Rack::PATH_INFO])
22
30
  return if path_info == '/'
23
31
 
24
- [mount_path, Grape::Router.normalize_path(prefix)].each do |path|
25
- path_info.delete_prefix!(path) if path.present? && path != '/' && path_info.start_with?(path)
26
- end
32
+ # `each` rather than `reduce`: Array does not override Enumerable's,
33
+ # so the generic accumulator path costs more than the work it drives
34
+ # on a list that holds at most a mount path and a prefix — and most
35
+ # often nothing at all.
36
+ @prefixes.each { |prefix| path_info = path_info.delete_prefix(prefix) if path_info.start_with?(prefix) }
27
37
 
28
38
  slash_position = path_info.index('/', 1) # omit the first one
29
- return unless slash_position
39
+ return version_from_first_segment(path_info, slash_position) if slash_position
30
40
 
31
- potential_version = path_info[1..slash_position - 1]
32
- return unless potential_version.match?(pattern)
41
+ version_from_only_segment(path_info)
42
+ end
43
+
44
+ private
33
45
 
46
+ # Under path versioning every route pattern carries the version as a
47
+ # named capture (Pattern::Path#build_parts inserts it), constrained to
48
+ # the declared versions -- so the router has already sliced the segment
49
+ # out and validated it, and re-deriving it from PATH_INFO would repeat
50
+ # the normalize, prefix-strip and slice below for the same answer.
51
+ #
52
+ # Nil, and the path parsed as before, when there are no routing args at
53
+ # all (the middleware used outside a Grape router) and for the greedy
54
+ # routes behind auto-OPTIONS and 405, which capture nothing. An Array
55
+ # means the route declared a +:version+ segment of its own on top of the
56
+ # versioning one: Mustermann then reports the capture as every position
57
+ # it matched rather than the single segment recorded here, so that too
58
+ # is left to the path parse.
59
+ def routed_version
60
+ version = env[Grape::Env::GRAPE_ROUTING_ARGS]&.[](:version)
61
+ version if version.is_a?(String)
62
+ end
63
+
64
+ def version_from_first_segment(path_info, slash_position)
65
+ potential_version = path_info[1..(slash_position - 1)]
34
66
  version_not_found! unless potential_version_match?(potential_version)
35
67
  env[Grape::Env::API_VERSION] = potential_version
36
68
  end
69
+
70
+ # The path is a single segment (e.g. `GET /v1` — the root route of a
71
+ # path-versioned API). Nothing follows to disambiguate a version from a
72
+ # plain path or from a `.format` suffix (`/v1.json`), so the version is
73
+ # only recorded on an exact match against the declared versions, and an
74
+ # unmatched segment is left for the router to resolve — never a 404.
75
+ def version_from_only_segment(path_info)
76
+ candidate = path_info[1..]
77
+ candidate = candidate[0...candidate.rindex('.')] while candidate.include?('.') && !declared_version?(candidate)
78
+ env[Grape::Env::API_VERSION] = candidate if declared_version?(candidate)
79
+ end
80
+
81
+ def declared_version?(candidate)
82
+ versions.present? && versions.include?(candidate)
83
+ end
37
84
  end
38
85
  end
39
86
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ # Marker module for a mountable Grape application. Both {Grape::API} (the
5
+ # remountable user-facing class) and {Grape::API::Instance} (the compiled
6
+ # engine) extend it, so a mounted Grape app can be told apart from a bare
7
+ # Rack app with `is_a?(Grape::Mountable)` — a single, explicit predicate
8
+ # rather than duck-typing on an incidental internal method such as
9
+ # `inheritable_setting`.
10
+ #
11
+ # `Grape::API` and `Grape::API::Instance` are not related by inheritance and
12
+ # do not even respond to the same methods (the former to `mount_instance`,
13
+ # the latter to `inheritable_setting`/`endpoints`), so there is no common
14
+ # ancestor to key an `is_a?` check on without this marker.
15
+ #
16
+ # It answers identity only ("is this a Grape app?"). Capability checks that
17
+ # go on to call a stage-specific method — e.g. `respond_to?(:endpoints)`
18
+ # before reading `endpoints` — must stay as they are, since a `Mountable`
19
+ # does not necessarily respond to every such method.
20
+ module Mountable
21
+ end
22
+ end
@@ -5,31 +5,39 @@ module Grape
5
5
  # logical grouping of endpoints as well as sharing common configuration.
6
6
  # May also be referred to as group, segment, or resource.
7
7
  class Namespace
8
- attr_reader :space, :options
8
+ attr_reader :space, :requirements, :options
9
9
 
10
10
  # @param space [String] the name of this namespace
11
11
  # @param options [Hash] options hash
12
12
  # @option options :requirements [Hash] param-regex pairs, all of which must
13
13
  # be met by a request's params for all endpoints in this namespace, or
14
14
  # validation will fail and return a 422.
15
- def initialize(space, options)
15
+ def initialize(space, requirements: nil, **options)
16
16
  @space = space.to_s
17
+ @requirements = requirements
17
18
  @options = options
18
19
  end
19
20
 
20
- # Retrieves the requirements from the options hash, if given.
21
- # @return [Hash]
22
- def requirements
23
- options[:requirements] || {}
24
- end
25
-
26
21
  # (see ::joined_space_path)
27
22
  def self.joined_space(settings)
28
23
  settings&.map(&:space)
29
24
  end
30
25
 
31
- # Join the namespaces from a list of settings to create a path prefix.
32
- # @param settings [Array] list of Grape::Util::InheritableSettings.
26
+ def ==(other)
27
+ other.class == self.class &&
28
+ other.space == space &&
29
+ other.requirements == requirements &&
30
+ other.options == options
31
+ end
32
+ alias eql? ==
33
+
34
+ def hash
35
+ [self.class, space, requirements, options].hash
36
+ end
37
+
38
+ # Join the namespaces from a list of Namespace objects to create a path
39
+ # prefix.
40
+ # @param settings [Array] list of Grape::Namespace objects.
33
41
  def self.joined_space_path(settings)
34
42
  JoinedSpaceCache[joined_space(settings)]
35
43
  end
@@ -38,7 +46,7 @@ module Grape
38
46
  def initialize
39
47
  super
40
48
  @cache = Hash.new do |h, joined_space|
41
- h[joined_space] = Grape::Router.normalize_path(joined_space.join('/'))
49
+ h[joined_space] = Grape::Util::PathNormalizer.call(joined_space.join('/'))
42
50
  end
43
51
  end
44
52
  end
@@ -8,6 +8,8 @@ module Grape
8
8
  raise NotImplementedError
9
9
  end
10
10
 
11
+ private
12
+
11
13
  def inherited(klass)
12
14
  super
13
15
  ParamsBuilder.register(klass)
@@ -4,7 +4,7 @@ module Grape
4
4
  module ParamsBuilder
5
5
  class HashWithIndifferentAccess < Base
6
6
  def self.call(params)
7
- params.with_indifferent_access
7
+ ActiveSupport::HashWithIndifferentAccess.new(params)
8
8
  end
9
9
  end
10
10
  end
@@ -4,29 +4,12 @@ module Grape
4
4
  module ParamsBuilder
5
5
  extend Grape::Util::Registry
6
6
 
7
- SHORT_NAME_LOOKUP = {
8
- 'Grape::Extensions::Hash::ParamBuilder' => :hash,
9
- 'Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder' => :hash_with_indifferent_access,
10
- 'Grape::Extensions::Hashie::Mash::ParamBuilder' => :hashie_mash
11
- }.freeze
12
-
13
7
  module_function
14
8
 
15
9
  def params_builder_for(short_name)
16
- verified_short_name = verify_short_name!(short_name)
17
-
18
- raise Grape::Exceptions::UnknownParamsBuilder, verified_short_name unless registry.key?(verified_short_name)
19
-
20
- registry[verified_short_name]
21
- end
22
-
23
- def verify_short_name!(short_name)
24
- return short_name if short_name.is_a?(Symbol)
10
+ raise Grape::Exceptions::UnknownParamsBuilder, short_name unless registry.key?(short_name)
25
11
 
26
- class_name = short_name.name
27
- SHORT_NAME_LOOKUP[class_name].tap do |real_short_name|
28
- Grape.deprecator.warn "#{class_name} has been deprecated. Use short name :#{real_short_name} instead."
29
- end
12
+ registry[short_name]
30
13
  end
31
14
  end
32
15
  end
@@ -4,7 +4,7 @@ module Grape
4
4
  module Parser
5
5
  class Json < Base
6
6
  def self.call(object, _env)
7
- ::Grape::Json.load(object)
7
+ ::Grape::Json.parse(object)
8
8
  rescue ::Grape::Json::ParseError
9
9
  # handle JSON parsing errors via the rescue handlers or provide error message
10
10
  raise Grape::Exceptions::InvalidMessageBody.new('application/json')
data/lib/grape/path.rb CHANGED
@@ -1,76 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grape
4
- # Represents a path to an endpoint.
5
- class Path
6
- DEFAULT_FORMAT_SEGMENT = '(/.:format)'
7
- NO_VERSIONING_WITH_VALID_PATH_FORMAT_SEGMENT = '(.:format)'
8
- VERSION_SEGMENT = ':version'
9
-
10
- attr_reader :origin, :suffix
11
-
12
- def initialize(raw_path, raw_namespace, settings)
13
- @origin = PartsCache[build_parts(raw_path, raw_namespace, settings)]
14
- @suffix = build_suffix(raw_path, raw_namespace, settings)
15
- end
16
-
17
- def to_s
18
- "#{origin}#{suffix}"
19
- end
20
-
21
- private
22
-
23
- def build_suffix(raw_path, raw_namespace, settings)
24
- if uses_specific_format?(settings)
25
- "(.#{settings[:format]})"
26
- elsif !uses_path_versioning?(settings) || (valid_part?(raw_namespace) || valid_part?(raw_path))
27
- NO_VERSIONING_WITH_VALID_PATH_FORMAT_SEGMENT
28
- else
29
- DEFAULT_FORMAT_SEGMENT
30
- end
31
- end
32
-
33
- def build_parts(raw_path, raw_namespace, settings)
34
- [].tap do |parts|
35
- add_part(parts, settings[:mount_path])
36
- add_part(parts, settings[:root_prefix])
37
- parts << VERSION_SEGMENT if uses_path_versioning?(settings)
38
- add_part(parts, raw_namespace)
39
- add_part(parts, raw_path)
40
- end
41
- end
42
-
43
- def add_part(parts, value)
44
- parts << value if value && not_slash?(value)
45
- end
46
-
47
- def not_slash?(value)
48
- value != '/'
49
- end
50
-
51
- def uses_specific_format?(settings)
52
- return false unless settings.key?(:format) && settings.key?(:content_types)
53
-
54
- settings[:format] && Array(settings[:content_types]).size == 1
55
- end
56
-
57
- def uses_path_versioning?(settings)
58
- return false unless settings.key?(:version) && settings[:version_options]&.key?(:using)
59
-
60
- settings[:version] && settings[:version_options][:using] == :path
61
- end
62
-
63
- def valid_part?(part)
64
- part&.match?(/^\S/) && not_slash?(part)
65
- end
66
-
67
- class PartsCache < Grape::Util::Cache
68
- def initialize
69
- super
70
- @cache = Hash.new do |h, parts|
71
- h[parts] = Grape::Router.normalize_path(parts.join('/'))
72
- end
73
- end
74
- end
75
- end
4
+ # @deprecated +Grape::Path+ moved to {Grape::Router::Pattern::Path}, since it
5
+ # is a router-internal detail that only exists to build a
6
+ # {Grape::Router::Pattern}. Reference the new constant instead.
7
+ Path = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
8
+ 'Grape::Path',
9
+ 'Grape::Router::Pattern::Path',
10
+ Grape.deprecator
11
+ )
76
12
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ # Marks a body as JSON that has already been rendered, so the JSON formatters
5
+ # serve it verbatim instead of encoding it a second time.
6
+ #
7
+ # +Grape::Formatter::Json+ calls +to_json+ on whatever the endpoint returned, so a
8
+ # String holding pre-rendered JSON — a blob cached in Redis, a +json_agg+ column read
9
+ # straight from Postgres, output from another serializer — comes back encoded twice:
10
+ #
11
+ # '{"a":1}' => "\"{\\\"a\\\":1}\""
12
+ #
13
+ # Wrapping the body opts that one response out of encoding:
14
+ #
15
+ # get '/cached' do
16
+ # body Grape::PrecompiledJson.new(Rails.cache.read('payload'))
17
+ # end
18
+ #
19
+ # An Array is joined into a JSON array without its members being parsed, which is
20
+ # what makes a cached collection cheap — N rendered blobs are spliced together
21
+ # rather than round-tripped:
22
+ #
23
+ # Grape::PrecompiledJson.new(['{"id":1}', '{"id":2}']).to_s # => '[{"id":1},{"id":2}]'
24
+ #
25
+ # +Array#join+ calls +to_s+ on each member, so members may themselves be
26
+ # +PrecompiledJson+ instances.
27
+ #
28
+ # Wrap only the whole body. A wrapper nested inside a Hash or Array that is then
29
+ # handed to an encoder is serialized as an ordinary object — +ActiveSupport::JSON+
30
+ # renders it through +as_json+ as +{"value":"{\"a\":1}"}+ — because no encoder knows
31
+ # to unwrap it. Nothing checks that the String actually holds JSON; that is the
32
+ # caller's responsibility.
33
+ class PrecompiledJson
34
+ # @param value [String, Array<String>] pre-rendered JSON
35
+ def initialize(value)
36
+ @value = value
37
+ end
38
+
39
+ # @return [String] the JSON to serve
40
+ # @raise [Grape::Exceptions::InvalidFormatter] if the value is neither a String
41
+ # nor an Array; the formatter middleware turns this into a 500 rather than
42
+ # letting a body Rack cannot serve reach the SPEC check.
43
+ def to_s
44
+ return @value if @value.is_a?(String)
45
+ return "[#{@value.join(',')}]" if @value.is_a?(Array)
46
+
47
+ raise Grape::Exceptions::InvalidFormatter.new(@value.class, 'json')
48
+ end
49
+ end
50
+ end
data/lib/grape/request.rb CHANGED
@@ -132,8 +132,8 @@ module Grape
132
132
  X-UA-Compatible
133
133
  X-WebKit-CS
134
134
  X-XSS-Protection
135
- ].each_with_object({}) do |header, response|
136
- response["HTTP_#{header.upcase.tr('-', '_')}"] = header
135
+ ].to_h do |header|
136
+ ["HTTP_#{header.upcase.tr('-', '_')}", header]
137
137
  end.freeze
138
138
 
139
139
  alias rack_params params
@@ -141,7 +141,7 @@ module Grape
141
141
 
142
142
  def initialize(env, build_params_with: nil)
143
143
  super(env)
144
- @params_builder = Grape::ParamsBuilder.params_builder_for(build_params_with || Grape.config.param_builder)
144
+ @build_params_with = build_params_with
145
145
  end
146
146
 
147
147
  def params
@@ -153,38 +153,75 @@ module Grape
153
153
  end
154
154
 
155
155
  def cookies
156
- @cookies ||= Grape::Cookies.new(-> { rack_cookies })
156
+ @cookies ||= Grape::Cookies.new(rack_cookies)
157
157
  end
158
158
 
159
- # needs to be public until extensions param_builder are removed
160
- def grape_routing_args
161
- # preserve version from query string parameters
162
- env[Grape::Env::GRAPE_ROUTING_ARGS]&.except(:version, :route_info) || {}
159
+ # True once the cookie jar has been materialized (a cookie was read or
160
+ # written this request). Lets the endpoint skip response-cookie flushing,
161
+ # and the Grape::Cookies allocation it triggers, when no cookie was
162
+ # touched on the request.
163
+ def cookies?
164
+ !@cookies.nil?
163
165
  end
164
166
 
165
167
  private
166
168
 
169
+ # Resolved on first use rather than in the constructor: a request that never
170
+ # reads +params+ -- an endpoint with no validations whose block does not ask
171
+ # for them -- pays neither the registry lookup nor the global config read,
172
+ # and every request builds a Grape::Request.
173
+ def params_builder
174
+ @params_builder ||= Grape::ParamsBuilder.params_builder_for(@build_params_with || Grape.config[:param_builder])
175
+ end
176
+
167
177
  def make_params
168
- @params_builder.call(rack_params).deep_merge!(grape_routing_args)
169
- rescue EOFError
170
- raise Grape::Exceptions::EmptyMessageBody.new(content_type)
171
- rescue Rack::Multipart::MultipartPartLimitError, Rack::Multipart::MultipartTotalPartLimitError
172
- raise Grape::Exceptions::TooManyMultipartFiles.new(Rack::Utils.multipart_part_limit)
173
- rescue Rack::QueryParser::ParamsTooDeepError
174
- raise Grape::Exceptions::TooDeepParameters.new(Rack::Utils.param_depth_limit)
175
- rescue Rack::Utils::ParameterTypeError
176
- raise Grape::Exceptions::ConflictingTypes
177
- rescue Rack::Utils::InvalidParameterError
178
- raise Grape::Exceptions::InvalidParameters
178
+ params = params_builder.call(rack_params)
179
+ filtered = routing_args_as_params(env[Grape::Env::GRAPE_ROUTING_ARGS])
180
+ return params if filtered.blank?
181
+
182
+ params.deep_merge!(filtered)
183
+ rescue *Grape::RACK_ERRORS
184
+ raise Grape::Exceptions::RequestError
185
+ end
186
+
187
+ # The routing args carry two things that are not request params:
188
+ # +:route_info+, which is always Grape's own, and +:version+, which is only
189
+ # Grape's own when the API declared a version — that is captured as a path
190
+ # segment and exposed through +env['api.version']+ instead.
191
+ #
192
+ # An API that declares no version can legitimately name a param +:version+
193
+ # (`route_param :version`, `get '/:version'`), and that capture belongs to
194
+ # the application. Dropping it unconditionally left `params[:version]` nil
195
+ # on a route that had matched, losing the segment silently.
196
+ def routing_args_as_params(routing_args)
197
+ return if routing_args.nil?
198
+ return routing_args.except(:version, :route_info) if grape_owns_version?(routing_args)
199
+
200
+ routing_args.except(:route_info)
201
+ end
202
+
203
+ # A route reports a +version+ only when the API declared one, which is the
204
+ # case where the captured segment is Grape's rather than the application's.
205
+ def grape_owns_version?(routing_args)
206
+ return false unless routing_args.key?(:version)
207
+
208
+ route = routing_args[:route_info]
209
+ route.respond_to?(:version) && !route.version.nil?
179
210
  end
180
211
 
212
+ # Uses a plain `each_header` block instead of `each_header.with_object`:
213
+ # `with_object` can only pass the block one value plus the memo, so the
214
+ # `k, v` pair would be boxed into a throwaway Array on every header. A
215
+ # two-arg block receives `k`/`v` directly and allocates nothing extra.
181
216
  def build_headers
182
- each_header.with_object(Grape::Util::Header.new) do |(k, v), headers|
217
+ headers = Grape::Util::Header.new
218
+ each_header do |k, v|
183
219
  next unless k.start_with? 'HTTP_'
184
220
 
185
221
  transformed_header = KNOWN_HEADERS.fetch(k) { -k[5..].tr('_', '-').downcase }
186
222
  headers[transformed_header] = v
187
223
  end
224
+ headers
188
225
  end
189
226
  end
190
227
  end
@@ -3,29 +3,75 @@
3
3
  module Grape
4
4
  class Router
5
5
  class BaseRoute
6
+ extend Forwardable
7
+
6
8
  delegate_missing_to :@options
7
9
 
8
- attr_reader :index, :pattern, :options
10
+ attr_reader :options, :pattern, :prefix, :settings, :namespace
11
+
12
+ # +version+, +anchor+ and +requirements+ shape the matcher, so they are
13
+ # read from the pattern rather than stored again on the route.
14
+ def_delegators :@pattern, :path, :origin, :version, :anchor, :requirements
15
+ def_delegators :@options, :description, *Grape::Util::ApiDescription::DSL_METHODS
9
16
 
10
- def initialize(options)
17
+ def initialize(pattern, options = {}, namespace: nil, prefix: nil, settings: nil)
18
+ @pattern = pattern
11
19
  @options = options.is_a?(ActiveSupport::OrderedOptions) ? options : ActiveSupport::OrderedOptions.new.update(options)
20
+ @namespace = namespace
21
+ @prefix = prefix
22
+ @settings = settings
12
23
  end
13
24
 
14
- alias attributes options
25
+ # +success+ and +failure+ are the +desc+ DSL's names for +entity+ and
26
+ # +http_codes+: the block form writes the canonical key, a keyword option
27
+ # keeps the name it was written with, so both spellings are read here.
28
+ # Without this they resolve through +delegate_missing_to+, which answers
29
+ # nil for whichever of the two keys the description did not use.
30
+ def success
31
+ @options[:entity] || @options[:success]
32
+ end
33
+
34
+ def failure
35
+ @options[:http_codes] || @options[:failure]
36
+ end
15
37
 
16
- def regexp_capture_index
17
- CaptureIndexCache[index]
38
+ # @deprecated Use {#default_response}, the name grape-swagger asks for.
39
+ # This one has to be written out rather than delegated like the rest:
40
+ # an ActiveSupport::OrderedOptions answers an unknown name with that
41
+ # key's value, but +default+ is not unknown to it — it is +Hash#default+,
42
+ # the Hash's own default value — so a delegator would report nil for
43
+ # every route.
44
+ def default
45
+ Grape.deprecator.warn('`Grape::Router::Route#default` is deprecated. Use `#default_response` instead.')
46
+ default_response
18
47
  end
19
48
 
49
+ # Assigned eagerly in {#to_regexp} (router compilation) rather than
50
+ # memoized here: this reader is called from request-time route matching
51
+ # on instances shared across threads, so it must not write state.
52
+ attr_reader :regexp_capture_index
53
+
54
+ # The number of the group this route occupies in the union the router
55
+ # compiled it into. Only the union can say what it is, so it is written
56
+ # back by {Router#compile!} right after building one, and like
57
+ # +regexp_capture_index+ it is never assigned at request time.
58
+ attr_reader :regexp_capture_group
59
+
20
60
  def pattern_regexp
21
- pattern.to_regexp
61
+ @pattern.to_regexp
22
62
  end
23
63
 
24
64
  def to_regexp(index)
25
- @index = index
65
+ @regexp_capture_index = CaptureIndexCache[index]
26
66
  Regexp.new("(?<#{regexp_capture_index}>#{pattern_regexp})")
27
67
  end
28
68
 
69
+ # @api private
70
+ # @see #regexp_capture_group
71
+ def resolve_capture_group!(union_named_captures)
72
+ @regexp_capture_group = union_named_captures.fetch(regexp_capture_index).first
73
+ end
74
+
29
75
  class CaptureIndexCache < Grape::Util::Cache
30
76
  def initialize
31
77
  super
@@ -6,14 +6,24 @@
6
6
  module Grape
7
7
  class Router
8
8
  class GreedyRoute < BaseRoute
9
- def initialize(pattern, options)
10
- @pattern = pattern
11
- super(options)
9
+ extend Forwardable
10
+
11
+ def_delegators :@endpoint, :call
12
+
13
+ attr_reader :endpoint, :allow_header
14
+
15
+ def initialize(pattern, endpoint:, allow_header:)
16
+ super(pattern)
17
+ @endpoint = endpoint
18
+ @allow_header = allow_header
19
+ end
20
+
21
+ def params
22
+ nil
12
23
  end
13
24
 
14
- # Grape::Router:Route defines params as a function
15
- def params(_input = nil)
16
- options[:params] || {}
25
+ def params_for(_input)
26
+ nil
17
27
  end
18
28
  end
19
29
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ class Router
5
+ # Grape-style path patterns for Mustermann: `:param`, `*splat`, `{name}` /
6
+ # `{+splat}`, `( )` optionals, `|`, and an Integer digit-only constraint
7
+ # (driven by Grape's `params` option).
8
+ #
9
+ # Inlined from the mustermann-grape gem (MIT) by namusyaka, Konstantin Haase
10
+ # and Daniel Doubrovkine. Grape instantiates this class directly (see
11
+ # {Grape::Router::Pattern}), so unlike the gem it is not registered as a
12
+ # Mustermann `type: :grape`.
13
+ class MustermannPattern < ::Mustermann::AST::Pattern
14
+ supported_options :params
15
+
16
+ on(nil, '?', ')') { |c| unexpected(c) }
17
+
18
+ on('*') { |_c| scan(/\w+/) ? node(:named_splat, buffer.matched) : node(:splat) }
19
+ on(':') do |_c|
20
+ param_name = scan(/\w+/)
21
+ # Integer params (declared via Grape's `params` option) match digits only;
22
+ # any other capture matches a single path segment (anything but / ? # .).
23
+ param_type = pattern&.options&.dig(:params, param_name, :type)
24
+ constraint = param_type == 'Integer' ? /\d/ : '[^/?#.]'
25
+ node(:capture, param_name, constraint:) { scan(/\w+/) }
26
+ end
27
+ on('\\') { |_c| node(:char, expect(/./)) }
28
+ on('(') { |_c| node(:optional, node(:group) { read unless scan(')') }) }
29
+ on('|') { |_c| node(:or) }
30
+
31
+ on('{') do |_c|
32
+ type = scan('+') ? :named_splat : :capture
33
+ name = expect(/[\w.]+/)
34
+ type = :splat if (type == :named_splat) && (name == 'splat')
35
+ expect('}')
36
+ node(type, name)
37
+ end
38
+
39
+ suffix('?') do |_c, element|
40
+ node(:optional, element)
41
+ end
42
+ end
43
+ end
44
+ end