grape 0.1.1 → 2.4.0

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 (197) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1167 -0
  3. data/CONTRIBUTING.md +156 -0
  4. data/LICENSE +1 -1
  5. data/README.md +4192 -0
  6. data/UPGRADING.md +1697 -0
  7. data/grape.gemspec +28 -105
  8. data/grape.png +0 -0
  9. data/lib/grape/api/helpers.rb +9 -0
  10. data/lib/grape/api/instance.rb +247 -0
  11. data/lib/grape/api.rb +158 -194
  12. data/lib/grape/content_types.rb +31 -0
  13. data/lib/grape/cookies.rb +50 -0
  14. data/lib/grape/dry_types.rb +10 -0
  15. data/lib/grape/dsl/api.rb +17 -0
  16. data/lib/grape/dsl/callbacks.rb +69 -0
  17. data/lib/grape/dsl/configuration.rb +15 -0
  18. data/lib/grape/dsl/desc.rb +124 -0
  19. data/lib/grape/dsl/headers.rb +21 -0
  20. data/lib/grape/dsl/helpers.rb +108 -0
  21. data/lib/grape/dsl/inside_route.rb +454 -0
  22. data/lib/grape/dsl/logger.rb +22 -0
  23. data/lib/grape/dsl/middleware.rb +54 -0
  24. data/lib/grape/dsl/parameters.rb +266 -0
  25. data/lib/grape/dsl/request_response.rb +171 -0
  26. data/lib/grape/dsl/routing.rb +248 -0
  27. data/lib/grape/dsl/settings.rb +179 -0
  28. data/lib/grape/dsl/validations.rb +57 -0
  29. data/lib/grape/endpoint.rb +398 -68
  30. data/lib/grape/env.rb +20 -0
  31. data/lib/grape/error_formatter/base.rb +68 -0
  32. data/lib/grape/error_formatter/json.rb +28 -0
  33. data/lib/grape/error_formatter/serializable_hash.rb +7 -0
  34. data/lib/grape/error_formatter/txt.rb +21 -0
  35. data/lib/grape/error_formatter/xml.rb +11 -0
  36. data/lib/grape/error_formatter.rb +15 -0
  37. data/lib/grape/exceptions/base.rb +76 -0
  38. data/lib/grape/exceptions/conflicting_types.rb +11 -0
  39. data/lib/grape/exceptions/empty_message_body.rb +11 -0
  40. data/lib/grape/exceptions/incompatible_option_values.rb +11 -0
  41. data/lib/grape/exceptions/invalid_accept_header.rb +11 -0
  42. data/lib/grape/exceptions/invalid_formatter.rb +11 -0
  43. data/lib/grape/exceptions/invalid_message_body.rb +11 -0
  44. data/lib/grape/exceptions/invalid_parameters.rb +11 -0
  45. data/lib/grape/exceptions/invalid_response.rb +11 -0
  46. data/lib/grape/exceptions/invalid_version_header.rb +11 -0
  47. data/lib/grape/exceptions/invalid_versioner_option.rb +11 -0
  48. data/lib/grape/exceptions/invalid_with_option_for_represent.rb +11 -0
  49. data/lib/grape/exceptions/method_not_allowed.rb +11 -0
  50. data/lib/grape/exceptions/missing_group_type.rb +13 -0
  51. data/lib/grape/exceptions/missing_mime_type.rb +11 -0
  52. data/lib/grape/exceptions/missing_option.rb +11 -0
  53. data/lib/grape/exceptions/missing_vendor_option.rb +11 -0
  54. data/lib/grape/exceptions/too_deep_parameters.rb +11 -0
  55. data/lib/grape/exceptions/too_many_multipart_files.rb +11 -0
  56. data/lib/grape/exceptions/unknown_auth_strategy.rb +11 -0
  57. data/lib/grape/exceptions/unknown_options.rb +11 -0
  58. data/lib/grape/exceptions/unknown_parameter.rb +11 -0
  59. data/lib/grape/exceptions/unknown_params_builder.rb +11 -0
  60. data/lib/grape/exceptions/unknown_validator.rb +11 -0
  61. data/lib/grape/exceptions/unsupported_group_type.rb +13 -0
  62. data/lib/grape/exceptions/validation.rb +25 -0
  63. data/lib/grape/exceptions/validation_array_errors.rb +14 -0
  64. data/lib/grape/exceptions/validation_errors.rb +53 -0
  65. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +24 -0
  66. data/lib/grape/extensions/hash.rb +27 -0
  67. data/lib/grape/extensions/hashie/mash.rb +24 -0
  68. data/lib/grape/formatter/base.rb +16 -0
  69. data/lib/grape/formatter/json.rb +13 -0
  70. data/lib/grape/formatter/serializable_hash.rb +39 -0
  71. data/lib/grape/formatter/txt.rb +11 -0
  72. data/lib/grape/formatter/xml.rb +13 -0
  73. data/lib/grape/formatter.rb +17 -0
  74. data/lib/grape/json.rb +10 -0
  75. data/lib/grape/locale/en.yml +59 -0
  76. data/lib/grape/middleware/auth/base.rb +23 -0
  77. data/lib/grape/middleware/auth/dsl.rb +39 -0
  78. data/lib/grape/middleware/auth/strategies.rb +25 -0
  79. data/lib/grape/middleware/auth/strategy_info.rb +15 -0
  80. data/lib/grape/middleware/base.rb +89 -18
  81. data/lib/grape/middleware/error.rb +129 -10
  82. data/lib/grape/middleware/filter.rb +19 -0
  83. data/lib/grape/middleware/formatter.rb +124 -82
  84. data/lib/grape/middleware/globals.rb +14 -0
  85. data/lib/grape/middleware/stack.rb +109 -0
  86. data/lib/grape/middleware/versioner/accept_version_header.rb +38 -0
  87. data/lib/grape/middleware/versioner/base.rb +74 -0
  88. data/lib/grape/middleware/versioner/header.rb +127 -0
  89. data/lib/grape/middleware/versioner/param.rb +32 -0
  90. data/lib/grape/middleware/versioner/path.rb +40 -0
  91. data/lib/grape/middleware/versioner.rb +21 -21
  92. data/lib/grape/namespace.rb +46 -0
  93. data/lib/grape/params_builder/base.rb +18 -0
  94. data/lib/grape/params_builder/hash.rb +11 -0
  95. data/lib/grape/params_builder/hash_with_indifferent_access.rb +11 -0
  96. data/lib/grape/params_builder/hashie_mash.rb +11 -0
  97. data/lib/grape/params_builder.rb +32 -0
  98. data/lib/grape/parser/base.rb +16 -0
  99. data/lib/grape/parser/json.rb +14 -0
  100. data/lib/grape/parser/xml.rb +14 -0
  101. data/lib/grape/parser.rb +15 -0
  102. data/lib/grape/path.rb +76 -0
  103. data/lib/grape/presenters/presenter.rb +11 -0
  104. data/lib/grape/railtie.rb +9 -0
  105. data/lib/grape/request.rb +190 -0
  106. data/lib/grape/router/base_route.rb +39 -0
  107. data/lib/grape/router/greedy_route.rb +20 -0
  108. data/lib/grape/router/pattern.rb +81 -0
  109. data/lib/grape/router/route.rb +62 -0
  110. data/lib/grape/router.rb +190 -0
  111. data/lib/grape/serve_stream/file_body.rb +36 -0
  112. data/lib/grape/serve_stream/sendfile_response.rb +21 -0
  113. data/lib/grape/serve_stream/stream_response.rb +23 -0
  114. data/lib/grape/types/invalid_value.rb +8 -0
  115. data/lib/grape/util/base_inheritable.rb +43 -0
  116. data/lib/grape/util/cache.rb +17 -0
  117. data/lib/grape/util/endpoint_configuration.rb +8 -0
  118. data/lib/grape/util/header.rb +13 -0
  119. data/lib/grape/util/inheritable_setting.rb +100 -0
  120. data/lib/grape/util/inheritable_values.rb +29 -0
  121. data/lib/grape/util/lazy/block.rb +29 -0
  122. data/lib/grape/util/lazy/value.rb +38 -0
  123. data/lib/grape/util/lazy/value_array.rb +21 -0
  124. data/lib/grape/util/lazy/value_enumerable.rb +34 -0
  125. data/lib/grape/util/lazy/value_hash.rb +21 -0
  126. data/lib/grape/util/media_type.rb +70 -0
  127. data/lib/grape/util/registry.rb +27 -0
  128. data/lib/grape/util/reverse_stackable_values.rb +15 -0
  129. data/lib/grape/util/stackable_values.rb +36 -0
  130. data/lib/grape/util/strict_hash_configuration.rb +108 -0
  131. data/lib/grape/validations/attributes_doc.rb +60 -0
  132. data/lib/grape/validations/attributes_iterator.rb +62 -0
  133. data/lib/grape/validations/contract_scope.rb +34 -0
  134. data/lib/grape/validations/multiple_attributes_iterator.rb +13 -0
  135. data/lib/grape/validations/params_scope.rb +538 -0
  136. data/lib/grape/validations/single_attribute_iterator.rb +26 -0
  137. data/lib/grape/validations/types/array_coercer.rb +61 -0
  138. data/lib/grape/validations/types/custom_type_coercer.rb +164 -0
  139. data/lib/grape/validations/types/custom_type_collection_coercer.rb +56 -0
  140. data/lib/grape/validations/types/dry_type_coercer.rb +66 -0
  141. data/lib/grape/validations/types/file.rb +31 -0
  142. data/lib/grape/validations/types/invalid_value.rb +17 -0
  143. data/lib/grape/validations/types/json.rb +71 -0
  144. data/lib/grape/validations/types/multiple_type_coercer.rb +57 -0
  145. data/lib/grape/validations/types/primitive_coercer.rb +73 -0
  146. data/lib/grape/validations/types/set_coercer.rb +35 -0
  147. data/lib/grape/validations/types/variant_collection_coercer.rb +51 -0
  148. data/lib/grape/validations/types.rb +213 -0
  149. data/lib/grape/validations/validator_factory.rb +15 -0
  150. data/lib/grape/validations/validators/all_or_none_of_validator.rb +16 -0
  151. data/lib/grape/validations/validators/allow_blank_validator.rb +20 -0
  152. data/lib/grape/validations/validators/as_validator.rb +14 -0
  153. data/lib/grape/validations/validators/at_least_one_of_validator.rb +15 -0
  154. data/lib/grape/validations/validators/base.rb +88 -0
  155. data/lib/grape/validations/validators/coerce_validator.rb +75 -0
  156. data/lib/grape/validations/validators/contract_scope_validator.rb +41 -0
  157. data/lib/grape/validations/validators/default_validator.rb +37 -0
  158. data/lib/grape/validations/validators/exactly_one_of_validator.rb +17 -0
  159. data/lib/grape/validations/validators/except_values_validator.rb +24 -0
  160. data/lib/grape/validations/validators/length_validator.rb +49 -0
  161. data/lib/grape/validations/validators/multiple_params_base.rb +34 -0
  162. data/lib/grape/validations/validators/mutual_exclusion_validator.rb +16 -0
  163. data/lib/grape/validations/validators/presence_validator.rb +15 -0
  164. data/lib/grape/validations/validators/regexp_validator.rb +16 -0
  165. data/lib/grape/validations/validators/same_as_validator.rb +29 -0
  166. data/lib/grape/validations/validators/values_validator.rb +60 -0
  167. data/lib/grape/validations.rb +21 -0
  168. data/lib/grape/version.rb +6 -0
  169. data/lib/grape/xml.rb +10 -0
  170. data/lib/grape.rb +81 -17
  171. metadata +250 -192
  172. data/.document +0 -5
  173. data/.gitignore +0 -23
  174. data/.rspec +0 -2
  175. data/.rvmrc +0 -1
  176. data/Gemfile +0 -21
  177. data/Gemfile.lock +0 -58
  178. data/README.markdown +0 -75
  179. data/Rakefile +0 -70
  180. data/VERSION +0 -1
  181. data/autotest/discover.rb +0 -1
  182. data/lib/grape/middleware/auth/basic.rb +0 -30
  183. data/lib/grape/middleware/auth/oauth2.rb +0 -55
  184. data/lib/grape/middleware/prefixer.rb +0 -21
  185. data/lib/grape/middleware_stack.rb +0 -35
  186. data/spec/grape/api_spec.rb +0 -343
  187. data/spec/grape/endpoint_spec.rb +0 -104
  188. data/spec/grape/middleware/auth/basic_spec.rb +0 -31
  189. data/spec/grape/middleware/auth/oauth2_spec.rb +0 -88
  190. data/spec/grape/middleware/base_spec.rb +0 -63
  191. data/spec/grape/middleware/error_spec.rb +0 -49
  192. data/spec/grape/middleware/formatter_spec.rb +0 -87
  193. data/spec/grape/middleware/prefixer_spec.rb +0 -30
  194. data/spec/grape/middleware/versioner_spec.rb +0 -40
  195. data/spec/grape/middleware_stack_spec.rb +0 -47
  196. data/spec/grape_spec.rb +0 -1
  197. data/spec/spec_helper.rb +0 -20
@@ -1,106 +1,148 @@
1
- require 'grape/middleware/base'
2
- require 'multi_json'
1
+ # frozen_string_literal: true
3
2
 
4
3
  module Grape
5
4
  module Middleware
6
5
  class Formatter < Base
7
- CONTENT_TYPES = {
8
- :xml => 'application/xml',
9
- :json => 'application/json',
10
- :atom => 'application/atom+xml',
11
- :rss => 'application/rss+xml',
12
- :txt => 'text/plain'
13
- }
14
-
15
- def default_options
16
- {
17
- :default_format => :txt,
18
- :content_types => {}
19
- }
20
- end
21
-
22
- def content_types
23
- CONTENT_TYPES.merge(options[:content_types])
24
- end
25
-
26
- def mime_types
27
- content_types.invert
28
- end
29
-
30
- def headers
31
- env.dup.inject({}){|h,(k,v)| h[k.downcase] = v; h}
32
- end
33
-
6
+ DEFAULT_OPTIONS = {
7
+ default_format: :txt
8
+ }.freeze
9
+
34
10
  def before
35
- fmt = format_from_extension || format_from_header || options[:default_format]
36
-
37
- if content_types.key?(fmt)
38
- env['api.format'] = fmt
39
- else
40
- throw :error, :status => 406, :message => 'The requested format is not supported.'
41
- end
11
+ negotiate_content_type
12
+ read_body_input
42
13
  end
43
-
44
- def format_from_extension
45
- parts = request.path.split('.')
46
- hit = parts.last.to_sym
47
-
48
- if parts.size <= 1
49
- nil
14
+
15
+ def after
16
+ return unless @app_response
17
+
18
+ status, headers, bodies = *@app_response
19
+
20
+ if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
21
+ [status, headers, []]
50
22
  else
51
- hit
23
+ build_formatted_response(status, headers, bodies)
52
24
  end
53
25
  end
54
-
55
- def format_from_header
56
- mime_array.each do |t|
57
- if mime_types.key?(t)
58
- return mime_types[t]
26
+
27
+ private
28
+
29
+ def build_formatted_response(status, headers, bodies)
30
+ headers = ensure_content_type(headers)
31
+
32
+ if bodies.is_a?(Grape::ServeStream::StreamResponse)
33
+ Grape::ServeStream::SendfileResponse.new([], status, headers) do |resp|
34
+ resp.body = bodies.stream
35
+ end
36
+ else
37
+ # Allow content-type to be explicitly overwritten
38
+ formatter = fetch_formatter(headers, options)
39
+ bodymap = ActiveSupport::Notifications.instrument('format_response.grape', formatter: formatter, env: env) do
40
+ bodies.collect { |body| formatter.call(body, env) }
59
41
  end
42
+ Rack::Response.new(bodymap, status, headers)
60
43
  end
61
- nil
44
+ rescue Grape::Exceptions::InvalidFormatter => e
45
+ throw :error, status: 500, message: e.message, backtrace: e.backtrace, original_exception: e
46
+ end
47
+
48
+ def fetch_formatter(headers, options)
49
+ api_format = env.fetch(Grape::Env::API_FORMAT) { mime_types[headers[Rack::CONTENT_TYPE]] }
50
+ Grape::Formatter.formatter_for(api_format, options[:formatters])
62
51
  end
63
-
64
- def mime_array
65
- accept = headers['accept']
66
- if accept
67
- accept.gsub(/\b/,'').
68
- scan(/(\w+\/[\w+]+)(?:;[^,]*q=([0-9.]+)[^,]*)?/i).
69
- sort_by{|a| -a[1].to_f}.
70
- map{|a| a[0]}
52
+
53
+ # Set the content type header for the API format if it is not already present.
54
+ #
55
+ # @param headers [Hash]
56
+ # @return [Hash]
57
+ def ensure_content_type(headers)
58
+ if headers[Rack::CONTENT_TYPE]
59
+ headers
71
60
  else
72
- []
61
+ headers.merge(Rack::CONTENT_TYPE => content_type_for(env[Grape::Env::API_FORMAT]))
73
62
  end
74
63
  end
75
-
76
- def after
77
- status, headers, bodies = *@app_response
78
- bodymap = []
79
- bodies.each do |body|
80
- bodymap << case env['api.format']
81
- when :json
82
- encode_json(body)
83
- when :txt
84
- encode_txt(body)
64
+
65
+ def read_body_input
66
+ input = rack_request.body # reads RACK_INPUT
67
+ return if input.nil?
68
+ return unless read_body_input?
69
+
70
+ input.try(:rewind)
71
+ body = env[Grape::Env::API_REQUEST_INPUT] = input.read
72
+ begin
73
+ read_rack_input(body)
74
+ ensure
75
+ input.try(:rewind)
76
+ end
77
+ end
78
+
79
+ def read_rack_input(body)
80
+ return if body.empty?
81
+
82
+ media_type = rack_request.media_type
83
+ fmt = media_type ? mime_types[media_type] : options[:default_format]
84
+
85
+ throw :error, status: 415, message: "The provided content-type '#{media_type}' is not supported." unless content_type_for(fmt)
86
+ parser = Grape::Parser.parser_for fmt, options[:parsers]
87
+ if parser
88
+ begin
89
+ body = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env))
90
+ if body.is_a?(Hash)
91
+ env[Rack::RACK_REQUEST_FORM_HASH] = if env.key?(Rack::RACK_REQUEST_FORM_HASH)
92
+ env[Rack::RACK_REQUEST_FORM_HASH].merge(body)
93
+ else
94
+ body
95
+ end
96
+ env[Rack::RACK_REQUEST_FORM_INPUT] = env[Rack::RACK_INPUT]
97
+ end
98
+ rescue Grape::Exceptions::Base => e
99
+ raise e
100
+ rescue StandardError => e
101
+ throw :error, status: 400, message: e.message, backtrace: e.backtrace, original_exception: e
85
102
  end
103
+ else
104
+ env[Grape::Env::API_REQUEST_BODY] = body
86
105
  end
87
- headers['Content-Type'] = 'application/json'
88
- Rack::Response.new(bodymap, status, headers).to_a
89
106
  end
90
-
91
- def encode_json(object)
92
- if object.respond_to? :serializable_hash
93
- MultiJson.encode(object.serializable_hash)
94
- elsif object.respond_to? :to_json
95
- object.to_json
107
+
108
+ # this middleware will not try to format the following content-types since Rack already handles them
109
+ # when calling Rack's `params` function
110
+ # - application/x-www-form-urlencoded
111
+ # - multipart/form-data
112
+ # - multipart/related
113
+ # - multipart/mixed
114
+ def read_body_input?
115
+ (rack_request.post? || rack_request.put? || rack_request.patch? || rack_request.delete?) &&
116
+ !(rack_request.form_data? && rack_request.content_type) &&
117
+ !rack_request.parseable_data? &&
118
+ (rack_request.content_length.to_i.positive? || rack_request.env['HTTP_TRANSFER_ENCODING'] == 'chunked')
119
+ end
120
+
121
+ def negotiate_content_type
122
+ fmt = format_from_extension || query_params['format'] || options[:format] || format_from_header || options[:default_format]
123
+ if content_type_for(fmt)
124
+ env[Grape::Env::API_FORMAT] = fmt.to_sym
96
125
  else
97
- MultiJson.encode(object)
126
+ throw :error, status: 406, message: "The requested format '#{fmt}' is not supported."
98
127
  end
99
128
  end
100
-
101
- def encode_txt(object)
102
- object.respond_to?(:to_txt) ? object.to_txt : object.to_s
129
+
130
+ def format_from_extension
131
+ request_path = rack_request.path.try(:scrub)
132
+ dot_pos = request_path.rindex('.')
133
+ return unless dot_pos
134
+
135
+ extension = request_path[dot_pos + 1..]
136
+ extension if content_type_for(extension)
137
+ end
138
+
139
+ def format_from_header
140
+ accept_header = env['HTTP_ACCEPT'].try(:scrub)
141
+ return if accept_header.blank?
142
+
143
+ media_type = Rack::Utils.best_q_match(accept_header, mime_types.keys)
144
+ mime_types[media_type] if media_type
103
145
  end
104
146
  end
105
147
  end
106
- end
148
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Middleware
5
+ class Globals < Base
6
+ def before
7
+ request = Grape::Request.new(@env, build_params_with: @options[:build_params_with])
8
+ @env[Grape::Env::GRAPE_REQUEST] = request
9
+ @env[Grape::Env::GRAPE_REQUEST_HEADERS] = request.headers
10
+ @env[Grape::Env::GRAPE_REQUEST_PARAMS] = request.params if @env[Rack::RACK_INPUT]
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Middleware
5
+ # Class to handle the stack of middlewares based on ActionDispatch::MiddlewareStack
6
+ # It allows to insert and insert after
7
+ class Stack
8
+ extend Forwardable
9
+ class Middleware
10
+ attr_reader :args, :block, :klass
11
+
12
+ def initialize(klass, args, block)
13
+ @klass = klass
14
+ @args = args
15
+ @block = block
16
+ end
17
+
18
+ def name
19
+ klass.name
20
+ end
21
+
22
+ def ==(other)
23
+ case other
24
+ when Middleware
25
+ klass == other.klass
26
+ when Class
27
+ klass == other || (name.nil? && klass.superclass == other)
28
+ end
29
+ end
30
+
31
+ def inspect
32
+ klass.to_s
33
+ end
34
+
35
+ def build(builder)
36
+ # we need to force the ruby2_keywords_hash for middlewares that initialize contains keywords
37
+ # like ActionDispatch::RequestId since middleware arguments are serialized
38
+ # https://rubyapi.org/3.4/o/hash#method-c-ruby2_keywords_hash
39
+ args[-1] = Hash.ruby2_keywords_hash(args[-1]) if args.last.is_a?(Hash) && Hash.respond_to?(:ruby2_keywords_hash)
40
+ builder.use(klass, *args, &block)
41
+ end
42
+ end
43
+
44
+ include Enumerable
45
+
46
+ attr_accessor :middlewares, :others
47
+
48
+ def_delegators :middlewares, :each, :size, :last, :[]
49
+
50
+ def initialize
51
+ @middlewares = []
52
+ @others = []
53
+ end
54
+
55
+ def insert(index, klass, *args, &block)
56
+ index = assert_index(index, :before)
57
+ middlewares.insert(index, self.class::Middleware.new(klass, args, block))
58
+ end
59
+
60
+ alias insert_before insert
61
+
62
+ def insert_after(index, *args, &block)
63
+ index = assert_index(index, :after)
64
+ insert(index + 1, *args, &block)
65
+ end
66
+
67
+ def use(klass, *args, &block)
68
+ middleware = self.class::Middleware.new(klass, args, block)
69
+ middlewares.push(middleware)
70
+ end
71
+
72
+ def merge_with(middleware_specs)
73
+ middleware_specs.each do |operation, klass, *args|
74
+ if args.last.is_a?(Proc)
75
+ last_proc = args.pop
76
+ public_send(operation, klass, *args, &last_proc)
77
+ else
78
+ public_send(operation, klass, *args)
79
+ end
80
+ end
81
+ end
82
+
83
+ # @return [Rack::Builder] the builder object with our middlewares applied
84
+ def build
85
+ Rack::Builder.new.tap do |builder|
86
+ others.shift(others.size).each { |m| merge_with(m) }
87
+ middlewares.each do |m|
88
+ m.build(builder)
89
+ end
90
+ end
91
+ end
92
+
93
+ # @description Add middlewares with :use operation to the stack. Store others with :insert_* operation for later
94
+ # @param [Array] other_specs An array of middleware specifications (e.g. [[:use, klass], [:insert_before, *args]])
95
+ def concat(other_specs)
96
+ use, not_use = other_specs.partition { |o| o.first == :use }
97
+ others << not_use
98
+ merge_with(use)
99
+ end
100
+
101
+ protected
102
+
103
+ def assert_index(index, where)
104
+ i = index.is_a?(Integer) ? index : middlewares.index(index)
105
+ i || raise("No such middleware to insert #{where}: #{index.inspect}")
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Middleware
5
+ module Versioner
6
+ # This middleware sets various version related rack environment variables
7
+ # based on the HTTP Accept-Version header
8
+ #
9
+ # Example: For request header
10
+ # Accept-Version: v1
11
+ #
12
+ # The following rack env variables are set:
13
+ #
14
+ # env['api.version'] => 'v1'
15
+ #
16
+ # If version does not match this route, then a 406 is raised with
17
+ # X-Cascade header to alert Grape::Router to attempt the next matched
18
+ # route.
19
+ class AcceptVersionHeader < Base
20
+ def before
21
+ potential_version = env['HTTP_ACCEPT_VERSION'].try(:scrub)
22
+ not_acceptable!('Accept-Version header must be set.') if strict? && potential_version.blank?
23
+
24
+ return if potential_version.blank?
25
+
26
+ not_acceptable!('The requested version is not supported.') unless potential_version_match?(potential_version)
27
+ env[Grape::Env::API_VERSION] = potential_version
28
+ end
29
+
30
+ private
31
+
32
+ def not_acceptable!(message)
33
+ throw :error, status: 406, headers: error_headers, message: message
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Middleware
5
+ module Versioner
6
+ class Base < Grape::Middleware::Base
7
+ DEFAULT_OPTIONS = {
8
+ pattern: /.*/i.freeze,
9
+ version_options: {
10
+ strict: false,
11
+ cascade: true,
12
+ parameter: 'apiver'
13
+ }.freeze
14
+ }.freeze
15
+
16
+ def self.inherited(klass)
17
+ super
18
+ Versioner.register(klass)
19
+ end
20
+
21
+ def versions
22
+ options[:versions]
23
+ end
24
+
25
+ def prefix
26
+ options[:prefix]
27
+ end
28
+
29
+ def mount_path
30
+ options[:mount_path]
31
+ end
32
+
33
+ def pattern
34
+ options[:pattern]
35
+ end
36
+
37
+ def version_options
38
+ options[:version_options]
39
+ end
40
+
41
+ def strict?
42
+ version_options[:strict]
43
+ end
44
+
45
+ # By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking
46
+ # of routes (see Grape::Router) for more information). To prevent
47
+ # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
48
+ def cascade?
49
+ version_options[:cascade]
50
+ end
51
+
52
+ def parameter_key
53
+ version_options[:parameter]
54
+ end
55
+
56
+ def vendor
57
+ version_options[:vendor]
58
+ end
59
+
60
+ def error_headers
61
+ cascade? ? { 'X-Cascade' => 'pass' } : {}
62
+ end
63
+
64
+ def potential_version_match?(potential_version)
65
+ versions.blank? || versions.any? { |v| v.to_s == potential_version }
66
+ end
67
+
68
+ def version_not_found!
69
+ throw :error, status: 404, message: '404 API Version Not Found', headers: { 'X-Cascade' => 'pass' }
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Middleware
5
+ module Versioner
6
+ # This middleware sets various version related rack environment variables
7
+ # based on the HTTP Accept header with the pattern:
8
+ # application/vnd.:vendor-:version+:format
9
+ #
10
+ # Example: For request header
11
+ # Accept: application/vnd.mycompany.a-cool-resource-v1+json
12
+ #
13
+ # The following rack env variables are set:
14
+ #
15
+ # env['api.type'] => 'application'
16
+ # env['api.subtype'] => 'vnd.mycompany.a-cool-resource-v1+json'
17
+ # env['api.vendor] => 'mycompany.a-cool-resource'
18
+ # env['api.version] => 'v1'
19
+ # env['api.format] => 'json'
20
+ #
21
+ # If version does not match this route, then a 406 is raised with
22
+ # X-Cascade header to alert Grape::Router to attempt the next matched
23
+ # route.
24
+ class Header < Base
25
+ def before
26
+ match_best_quality_media_type! do |media_type|
27
+ env.update(
28
+ Grape::Env::API_TYPE => media_type.type,
29
+ Grape::Env::API_SUBTYPE => media_type.subtype,
30
+ Grape::Env::API_VENDOR => media_type.vendor,
31
+ Grape::Env::API_VERSION => media_type.version,
32
+ Grape::Env::API_FORMAT => media_type.format
33
+ )
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def match_best_quality_media_type!
40
+ return unless vendor
41
+
42
+ strict_header_checks!
43
+ media_type = Grape::Util::MediaType.best_quality(accept_header, available_media_types)
44
+ if media_type
45
+ yield media_type
46
+ else
47
+ fail!
48
+ end
49
+ end
50
+
51
+ def accept_header
52
+ env['HTTP_ACCEPT']
53
+ end
54
+
55
+ def strict_header_checks!
56
+ return unless strict?
57
+
58
+ accept_header_check!
59
+ version_and_vendor_check!
60
+ end
61
+
62
+ def accept_header_check!
63
+ return if accept_header.present?
64
+
65
+ invalid_accept_header!('Accept header must be set.')
66
+ end
67
+
68
+ def version_and_vendor_check!
69
+ return if versions.blank? || version_and_vendor?
70
+
71
+ invalid_accept_header!('API vendor or version not found.')
72
+ end
73
+
74
+ def q_values_mime_types
75
+ @q_values_mime_types ||= Rack::Utils.q_values(accept_header).map(&:first)
76
+ end
77
+
78
+ def version_and_vendor?
79
+ q_values_mime_types.any? { |mime_type| Grape::Util::MediaType.match?(mime_type) }
80
+ end
81
+
82
+ def invalid_accept_header!(message)
83
+ raise Grape::Exceptions::InvalidAcceptHeader.new(message, error_headers)
84
+ end
85
+
86
+ def invalid_version_header!(message)
87
+ raise Grape::Exceptions::InvalidVersionHeader.new(message, error_headers)
88
+ end
89
+
90
+ def fail!
91
+ return if env[Grape::Env::GRAPE_ALLOWED_METHODS].present?
92
+
93
+ media_types = q_values_mime_types.map { |mime_type| Grape::Util::MediaType.parse(mime_type) }
94
+ vendor_not_found!(media_types) || version_not_found!(media_types)
95
+ end
96
+
97
+ def vendor_not_found!(media_types)
98
+ return unless media_types.all? { |media_type| media_type&.vendor && media_type.vendor != vendor }
99
+
100
+ invalid_accept_header!('API vendor not found.')
101
+ end
102
+
103
+ def version_not_found!(media_types)
104
+ return unless media_types.all? { |media_type| media_type&.version && versions&.exclude?(media_type.version) }
105
+
106
+ invalid_version_header!('API version not found.')
107
+ end
108
+
109
+ def available_media_types
110
+ [].tap do |available_media_types|
111
+ base_media_type = "application/vnd.#{vendor}"
112
+ content_types.each_key do |extension|
113
+ versions&.reverse_each do |version|
114
+ available_media_types << "#{base_media_type}-#{version}+#{extension}"
115
+ available_media_types << "#{base_media_type}-#{version}"
116
+ end
117
+ available_media_types << "#{base_media_type}+#{extension}"
118
+ end
119
+
120
+ available_media_types << base_media_type
121
+ available_media_types.concat(content_types.values.flatten)
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Middleware
5
+ module Versioner
6
+ # This middleware sets various version related rack environment variables
7
+ # based on the request parameters and removes that parameter from the
8
+ # request parameters for subsequent middleware and API.
9
+ # If the version substring does not match any potential initialized
10
+ # versions, a 404 error is thrown.
11
+ # If the version substring is not passed the version (highest mounted)
12
+ # version will be used.
13
+ #
14
+ # Example: For a uri path
15
+ # /resource?apiver=v1
16
+ #
17
+ # The following rack env variables are set and path is rewritten to
18
+ # '/resource':
19
+ #
20
+ # env['api.version'] => 'v1'
21
+ class Param < Base
22
+ def before
23
+ potential_version = query_params[parameter_key]
24
+ return if potential_version.blank?
25
+
26
+ version_not_found! unless potential_version_match?(potential_version)
27
+ env[Grape::Env::API_VERSION] = env[Rack::RACK_REQUEST_QUERY_HASH].delete(parameter_key)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Middleware
5
+ module Versioner
6
+ # This middleware sets various version related rack environment variables
7
+ # based on the uri path and removes the version substring from the uri
8
+ # path. If the version substring does not match any potential initialized
9
+ # versions, a 404 error is thrown.
10
+ #
11
+ # Example: For a uri path
12
+ # /v1/resource
13
+ #
14
+ # The following rack env variables are set and path is rewritten to
15
+ # '/resource':
16
+ #
17
+ # env['api.version'] => 'v1'
18
+ #
19
+ class Path < Base
20
+ def before
21
+ path_info = Grape::Router.normalize_path(env[Rack::PATH_INFO])
22
+ return if path_info == '/'
23
+
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
27
+
28
+ slash_position = path_info.index('/', 1) # omit the first one
29
+ return unless slash_position
30
+
31
+ potential_version = path_info[1..slash_position - 1]
32
+ return unless potential_version.match?(pattern)
33
+
34
+ version_not_found! unless potential_version_match?(potential_version)
35
+ env[Grape::Env::API_VERSION] = potential_version
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end