grape 0.12.0 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of grape might be problematic. Click here for more details.

Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/Appraisals +9 -4
  3. data/CHANGELOG.md +265 -215
  4. data/CONTRIBUTING.md +4 -4
  5. data/Gemfile +0 -1
  6. data/Gemfile.lock +166 -0
  7. data/README.md +426 -161
  8. data/RELEASING.md +14 -6
  9. data/Rakefile +30 -33
  10. data/UPGRADING.md +54 -23
  11. data/benchmark/simple.rb +27 -0
  12. data/gemfiles/rack_1.5.2.gemfile +13 -0
  13. data/gemfiles/rails_3.gemfile +2 -2
  14. data/gemfiles/rails_4.gemfile +1 -2
  15. data/grape.gemspec +6 -7
  16. data/lib/grape/api.rb +24 -4
  17. data/lib/grape/dsl/callbacks.rb +20 -0
  18. data/lib/grape/dsl/configuration.rb +59 -2
  19. data/lib/grape/dsl/helpers.rb +8 -3
  20. data/lib/grape/dsl/inside_route.rb +100 -45
  21. data/lib/grape/dsl/parameters.rb +96 -7
  22. data/lib/grape/dsl/request_response.rb +1 -1
  23. data/lib/grape/dsl/routing.rb +17 -4
  24. data/lib/grape/dsl/settings.rb +36 -1
  25. data/lib/grape/dsl/validations.rb +7 -5
  26. data/lib/grape/endpoint.rb +102 -57
  27. data/lib/grape/error_formatter/base.rb +6 -6
  28. data/lib/grape/exceptions/base.rb +5 -5
  29. data/lib/grape/exceptions/invalid_version_header.rb +10 -0
  30. data/lib/grape/exceptions/unknown_parameter.rb +10 -0
  31. data/lib/grape/exceptions/validation_errors.rb +4 -3
  32. data/lib/grape/formatter/serializable_hash.rb +3 -2
  33. data/lib/grape/http/headers.rb +0 -1
  34. data/lib/grape/locale/en.yml +5 -1
  35. data/lib/grape/middleware/auth/base.rb +2 -2
  36. data/lib/grape/middleware/auth/dsl.rb +1 -1
  37. data/lib/grape/middleware/auth/strategies.rb +1 -1
  38. data/lib/grape/middleware/base.rb +8 -4
  39. data/lib/grape/middleware/error.rb +3 -2
  40. data/lib/grape/middleware/filter.rb +1 -1
  41. data/lib/grape/middleware/formatter.rb +64 -45
  42. data/lib/grape/middleware/globals.rb +3 -3
  43. data/lib/grape/middleware/versioner/accept_version_header.rb +5 -7
  44. data/lib/grape/middleware/versioner/header.rb +113 -50
  45. data/lib/grape/middleware/versioner/param.rb +5 -8
  46. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +20 -0
  47. data/lib/grape/middleware/versioner/path.rb +3 -6
  48. data/lib/grape/namespace.rb +13 -2
  49. data/lib/grape/path.rb +4 -3
  50. data/lib/grape/request.rb +40 -0
  51. data/lib/grape/route.rb +5 -0
  52. data/lib/grape/util/content_types.rb +9 -9
  53. data/lib/grape/util/env.rb +22 -0
  54. data/lib/grape/util/file_response.rb +21 -0
  55. data/lib/grape/util/inheritable_setting.rb +23 -2
  56. data/lib/grape/util/inheritable_values.rb +1 -1
  57. data/lib/grape/util/stackable_values.rb +5 -2
  58. data/lib/grape/util/strict_hash_configuration.rb +2 -1
  59. data/lib/grape/validations/attributes_iterator.rb +8 -3
  60. data/lib/grape/validations/params_scope.rb +164 -22
  61. data/lib/grape/validations/types/build_coercer.rb +53 -0
  62. data/lib/grape/validations/types/custom_type_coercer.rb +183 -0
  63. data/lib/grape/validations/types/file.rb +28 -0
  64. data/lib/grape/validations/types/json.rb +65 -0
  65. data/lib/grape/validations/types/multiple_type_coercer.rb +76 -0
  66. data/lib/grape/validations/types/variant_collection_coercer.rb +59 -0
  67. data/lib/grape/validations/types/virtus_collection_patch.rb +16 -0
  68. data/lib/grape/validations/types.rb +144 -0
  69. data/lib/grape/validations/validators/all_or_none.rb +1 -1
  70. data/lib/grape/validations/validators/allow_blank.rb +3 -3
  71. data/lib/grape/validations/validators/base.rb +7 -0
  72. data/lib/grape/validations/validators/coerce.rb +32 -34
  73. data/lib/grape/validations/validators/presence.rb +2 -3
  74. data/lib/grape/validations/validators/regexp.rb +2 -4
  75. data/lib/grape/validations/validators/values.rb +3 -3
  76. data/lib/grape/validations.rb +5 -0
  77. data/lib/grape/version.rb +2 -1
  78. data/lib/grape.rb +15 -12
  79. data/pkg/grape-0.13.0.gem +0 -0
  80. data/spec/grape/api/custom_validations_spec.rb +5 -4
  81. data/spec/grape/api/deeply_included_options_spec.rb +7 -7
  82. data/spec/grape/api/nested_helpers_spec.rb +4 -2
  83. data/spec/grape/api/shared_helpers_spec.rb +8 -8
  84. data/spec/grape/api_spec.rb +151 -54
  85. data/spec/grape/dsl/configuration_spec.rb +13 -0
  86. data/spec/grape/dsl/helpers_spec.rb +16 -2
  87. data/spec/grape/dsl/inside_route_spec.rb +40 -4
  88. data/spec/grape/dsl/parameters_spec.rb +0 -6
  89. data/spec/grape/dsl/routing_spec.rb +1 -1
  90. data/spec/grape/dsl/validations_spec.rb +18 -0
  91. data/spec/grape/endpoint_spec.rb +130 -6
  92. data/spec/grape/entity_spec.rb +10 -8
  93. data/spec/grape/exceptions/invalid_accept_header_spec.rb +1 -15
  94. data/spec/grape/exceptions/validation_errors_spec.rb +28 -0
  95. data/spec/grape/integration/rack_spec.rb +3 -2
  96. data/spec/grape/middleware/base_spec.rb +40 -16
  97. data/spec/grape/middleware/error_spec.rb +16 -15
  98. data/spec/grape/middleware/exception_spec.rb +45 -43
  99. data/spec/grape/middleware/formatter_spec.rb +34 -5
  100. data/spec/grape/middleware/versioner/header_spec.rb +79 -47
  101. data/spec/grape/path_spec.rb +10 -10
  102. data/spec/grape/presenters/presenter_spec.rb +2 -2
  103. data/spec/grape/request_spec.rb +100 -0
  104. data/spec/grape/util/inheritable_values_spec.rb +14 -0
  105. data/spec/grape/util/stackable_values_spec.rb +10 -0
  106. data/spec/grape/validations/params_scope_spec.rb +86 -0
  107. data/spec/grape/validations/types_spec.rb +95 -0
  108. data/spec/grape/validations/validators/coerce_spec.rb +364 -10
  109. data/spec/grape/validations/validators/values_spec.rb +27 -15
  110. data/spec/grape/validations_spec.rb +53 -24
  111. data/spec/shared/versioning_examples.rb +2 -2
  112. data/spec/spec_helper.rb +0 -1
  113. data/spec/support/versioned_helpers.rb +2 -2
  114. metadata +55 -14
  115. data/.gitignore +0 -46
  116. data/.rspec +0 -2
  117. data/.rubocop.yml +0 -7
  118. data/.rubocop_todo.yml +0 -84
  119. data/.travis.yml +0 -20
  120. data/.yardopts +0 -2
  121. data/lib/backports/active_support/deep_dup.rb +0 -49
  122. data/lib/backports/active_support/duplicable.rb +0 -88
  123. data/lib/grape/http/request.rb +0 -27
@@ -3,6 +3,8 @@ require 'grape/middleware/base'
3
3
  module Grape
4
4
  module Middleware
5
5
  class Formatter < Base
6
+ CHUNKED = 'chunked'.freeze
7
+
6
8
  def default_options
7
9
  {
8
10
  default_format: :txt,
@@ -11,13 +13,6 @@ module Grape
11
13
  }
12
14
  end
13
15
 
14
- def headers
15
- env.dup.inject({}) do |h, (k, v)|
16
- h[k.to_s.downcase[5..-1]] = v if k.to_s.downcase.start_with?('http_')
17
- h
18
- end
19
- end
20
-
21
16
  def before
22
17
  negotiate_content_type
23
18
  read_body_input
@@ -25,46 +20,70 @@ module Grape
25
20
 
26
21
  def after
27
22
  status, headers, bodies = *@app_response
28
- # allow content-type to be explicitly overwritten
29
- api_format = mime_types[headers[Grape::Http::Headers::CONTENT_TYPE]] || env['api.format']
30
- formatter = Grape::Formatter::Base.formatter_for api_format, options
31
- begin
32
- bodymap = if bodies.respond_to?(:collect)
33
- bodies.collect do |body|
34
- formatter.call body, env
35
- end
36
- else
37
- bodies
38
- end
39
- rescue Grape::Exceptions::InvalidFormatter => e
40
- throw :error, status: 500, message: e.message
23
+
24
+ if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
25
+ @app_response
26
+ else
27
+ build_formatted_response(status, headers, bodies)
41
28
  end
42
- headers[Grape::Http::Headers::CONTENT_TYPE] = content_type_for(env['api.format']) unless headers[Grape::Http::Headers::CONTENT_TYPE]
43
- Rack::Response.new(bodymap, status, headers)
44
29
  end
45
30
 
46
31
  private
47
32
 
33
+ def build_formatted_response(status, headers, bodies)
34
+ headers = ensure_content_type(headers)
35
+
36
+ if bodies.is_a?(Grape::Util::FileResponse)
37
+ Rack::Response.new([], status, headers) do |resp|
38
+ resp.body = bodies.file
39
+ end
40
+ else
41
+ # Allow content-type to be explicitly overwritten
42
+ formatter = fetch_formatter(headers, options)
43
+ bodymap = bodies.collect { |body| formatter.call(body, env) }
44
+ Rack::Response.new(bodymap, status, headers)
45
+ end
46
+ rescue Grape::Exceptions::InvalidFormatter => e
47
+ throw :error, status: 500, message: e.message
48
+ end
49
+
50
+ def fetch_formatter(headers, options)
51
+ api_format = mime_types[headers[Grape::Http::Headers::CONTENT_TYPE]] || env[Grape::Env::API_FORMAT]
52
+ Grape::Formatter::Base.formatter_for(api_format, options)
53
+ end
54
+
55
+ # Set the content type header for the API format if it is not already present.
56
+ #
57
+ # @param headers [Hash]
58
+ # @return [Hash]
59
+ def ensure_content_type(headers)
60
+ if headers[Grape::Http::Headers::CONTENT_TYPE]
61
+ headers
62
+ else
63
+ headers.merge(Grape::Http::Headers::CONTENT_TYPE => content_type_for(env[Grape::Env::API_FORMAT]))
64
+ end
65
+ end
66
+
48
67
  def request
49
68
  @request ||= Rack::Request.new(env)
50
69
  end
51
70
 
52
71
  # store read input in env['api.request.input']
53
72
  def read_body_input
54
- if (request.post? || request.put? || request.patch? || request.delete?) &&
55
- (!request.form_data? || !request.media_type) &&
56
- (!request.parseable_data?) &&
57
- (request.content_length.to_i > 0 || request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == 'chunked')
58
-
59
- if (input = env['rack.input'])
60
- input.rewind
61
- body = env['api.request.input'] = input.read
62
- begin
63
- read_rack_input(body) if body && body.length > 0
64
- ensure
65
- input.rewind
66
- end
67
- end
73
+ return unless
74
+ (request.post? || request.put? || request.patch? || request.delete?) &&
75
+ (!request.form_data? || !request.media_type) &&
76
+ (!request.parseable_data?) &&
77
+ (request.content_length.to_i > 0 || request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == CHUNKED)
78
+
79
+ return unless (input = env[Grape::Env::RACK_INPUT])
80
+
81
+ input.rewind
82
+ body = env[Grape::Env::API_REQUEST_INPUT] = input.read
83
+ begin
84
+ read_rack_input(body) if body && body.length > 0
85
+ ensure
86
+ input.rewind
68
87
  end
69
88
  end
70
89
 
@@ -76,14 +95,14 @@ module Grape
76
95
  parser = Grape::Parser::Base.parser_for fmt, options
77
96
  if parser
78
97
  begin
79
- body = (env['api.request.body'] = parser.call(body, env))
98
+ body = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env))
80
99
  if body.is_a?(Hash)
81
- if env['rack.request.form_hash']
82
- env['rack.request.form_hash'] = env['rack.request.form_hash'].merge(body)
100
+ if env[Grape::Env::RACK_REQUEST_FORM_HASH]
101
+ env[Grape::Env::RACK_REQUEST_FORM_HASH] = env[Grape::Env::RACK_REQUEST_FORM_HASH].merge(body)
83
102
  else
84
- env['rack.request.form_hash'] = body
103
+ env[Grape::Env::RACK_REQUEST_FORM_HASH] = body
85
104
  end
86
- env['rack.request.form_input'] = env['rack.input']
105
+ env[Grape::Env::RACK_REQUEST_FORM_INPUT] = env[Grape::Env::RACK_INPUT]
87
106
  end
88
107
  rescue Grape::Exceptions::Base => e
89
108
  raise e
@@ -91,7 +110,7 @@ module Grape
91
110
  throw :error, status: 400, message: e.message
92
111
  end
93
112
  else
94
- env['api.request.body'] = body
113
+ env[Grape::Env::API_REQUEST_BODY] = body
95
114
  end
96
115
  else
97
116
  throw :error, status: 406, message: "The requested content-type '#{request.media_type}' is not supported."
@@ -101,7 +120,7 @@ module Grape
101
120
  def negotiate_content_type
102
121
  fmt = format_from_extension || format_from_params || options[:format] || format_from_header || options[:default_format]
103
122
  if content_type_for(fmt)
104
- env['api.format'] = fmt
123
+ env[Grape::Env::API_FORMAT] = fmt
105
124
  else
106
125
  throw :error, status: 406, message: "The requested format '#{fmt}' is not supported."
107
126
  end
@@ -133,7 +152,7 @@ module Grape
133
152
  end
134
153
 
135
154
  def mime_array
136
- accept = headers[Grape::Http::Headers::ACCEPT]
155
+ accept = env[Grape::Http::Headers::HTTP_ACCEPT]
137
156
  return [] unless accept
138
157
 
139
158
  accept_into_mime_and_quality = %r{
@@ -149,7 +168,7 @@ module Grape
149
168
 
150
169
  accept.scan(accept_into_mime_and_quality)
151
170
  .sort_by { |_, quality_preference| -quality_preference.to_f }
152
- .map { |mime, _| mime.sub(vendor_prefix_pattern, '') }
171
+ .flat_map { |mime, _| [mime, mime.sub(vendor_prefix_pattern, '')] }
153
172
  end
154
173
  end
155
174
  end
@@ -5,9 +5,9 @@ module Grape
5
5
  class Globals < Base
6
6
  def before
7
7
  request = Grape::Request.new(@env)
8
- @env['grape.request'] = request
9
- @env['grape.request.headers'] = request.headers
10
- @env['grape.request.params'] = request.params if @env['rack.input']
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[Grape::Env::RACK_INPUT]
11
11
  end
12
12
  end
13
13
  end
@@ -27,14 +27,12 @@ module Grape
27
27
  end
28
28
  end
29
29
 
30
- unless potential_version.empty?
31
- # If the requested version is not supported:
32
- unless versions.any? { |v| v.to_s == potential_version }
33
- throw :error, status: 406, headers: error_headers, message: 'The requested version is not supported.'
34
- end
30
+ return if potential_version.empty?
35
31
 
36
- env['api.version'] = potential_version
37
- end
32
+ # If the requested version is not supported:
33
+ throw :error, status: 406, headers: error_headers, message: 'The requested version is not supported.' unless versions.any? { |v| v.to_s == potential_version }
34
+
35
+ env[Grape::Env::API_VERSION] = potential_version
38
36
  end
39
37
 
40
38
  private
@@ -1,4 +1,5 @@
1
1
  require 'grape/middleware/base'
2
+ require 'grape/middleware/versioner/parse_media_type_patch'
2
3
 
3
4
  module Grape
4
5
  module Middleware
@@ -8,13 +9,13 @@ module Grape
8
9
  # application/vnd.:vendor-:version+:format
9
10
  #
10
11
  # Example: For request header
11
- # Accept: application/vnd.mycompany-v1+json
12
+ # Accept: application/vnd.mycompany.a-cool-resource-v1+json
12
13
  #
13
14
  # The following rack env variables are set:
14
15
  #
15
16
  # env['api.type'] => 'application'
16
- # env['api.subtype'] => 'vnd.mycompany-v1+json'
17
- # env['api.vendor] => 'mycompany'
17
+ # env['api.subtype'] => 'vnd.mycompany.a-cool-resource-v1+json'
18
+ # env['api.vendor] => 'mycompany.a-cool-resource'
18
19
  # env['api.version] => 'v1'
19
20
  # env['api.format] => 'json'
20
21
  #
@@ -22,54 +23,88 @@ module Grape
22
23
  # X-Cascade header to alert Rack::Mount to attempt the next matched
23
24
  # route.
24
25
  class Header < Base
25
- def before
26
- header = rack_accept_header
26
+ VENDOR_VERSION_HEADER_REGEX =
27
+ /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))?(?:\+([a-z0-9*\-.]+))?\z/
27
28
 
28
- if strict?
29
- # If no Accept header:
30
- if header.qvalues.empty?
31
- fail Grape::Exceptions::InvalidAcceptHeader.new('Accept header must be set.', error_headers)
32
- end
33
- # Remove any acceptable content types with ranges.
34
- header.qvalues.reject! do |media_type, _|
35
- Rack::Accept::Header.parse_media_type(media_type).find { |s| s == '*' }
36
- end
37
- # If all Accept headers included a range:
38
- if header.qvalues.empty?
39
- fail Grape::Exceptions::InvalidAcceptHeader.new('Accept header must not contain ranges ("*").',
40
- error_headers)
41
- end
42
- end
29
+ HAS_VENDOR_REGEX = /\Avnd\.[a-z0-9.\-_!#\$&\^]+/
30
+ HAS_VERSION_REGEX = /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))+/
43
31
 
44
- media_type = header.best_of available_media_types
32
+ def before
33
+ strict_header_checks if strict?
45
34
 
46
35
  if media_type
47
- type, subtype = Rack::Accept::Header.parse_media_type media_type
48
- env['api.type'] = type
49
- env['api.subtype'] = subtype
50
-
51
- if /\Avnd\.([a-z0-9*.]+)(?:-([a-z0-9*\-.]+))?(?:\+([a-z0-9*\-.+]+))?\z/ =~ subtype
52
- env['api.vendor'] = Regexp.last_match[1]
53
- env['api.version'] = Regexp.last_match[2]
54
- env['api.format'] = Regexp.last_match[3] # weird that Grape::Middleware::Formatter also does this
55
- end
56
- # If none of the available content types are acceptable:
57
- elsif strict?
58
- fail Grape::Exceptions::InvalidAcceptHeader.new('406 Not Acceptable', error_headers)
59
- # If all acceptable content types specify a vendor or version that doesn't exist:
60
- elsif header.values.all? { |header_value| has_vendor?(header_value) || version?(header_value) }
61
- fail Grape::Exceptions::InvalidAcceptHeader.new('API vendor or version not found.', error_headers)
36
+ media_type_header_handler
37
+ elsif headers_contain_wrong_vendor?
38
+ fail_with_invalid_accept_header!('API vendor not found.')
39
+ elsif headers_contain_wrong_version?
40
+ fail_with_invalid_version_header!('API version not found.')
62
41
  end
63
42
  end
64
43
 
65
44
  private
66
45
 
46
+ def strict_header_checks
47
+ strict_accept_header_presence_check
48
+ strict_version_vendor_accept_header_presence_check
49
+ end
50
+
51
+ def strict_accept_header_presence_check
52
+ return unless header.qvalues.empty?
53
+ fail_with_invalid_accept_header!('Accept header must be set.')
54
+ end
55
+
56
+ def strict_version_vendor_accept_header_presence_check
57
+ return unless versions.present?
58
+ return if an_accept_header_with_version_and_vendor_is_present?
59
+ fail_with_invalid_accept_header!('API vendor or version not found.')
60
+ end
61
+
62
+ def an_accept_header_with_version_and_vendor_is_present?
63
+ header.qvalues.keys.any? do |h|
64
+ VENDOR_VERSION_HEADER_REGEX =~ h.sub('application/', '')
65
+ end
66
+ end
67
+
68
+ def header
69
+ @header ||= rack_accept_header
70
+ end
71
+
72
+ def media_type
73
+ @media_type ||= header.best_of(available_media_types)
74
+ end
75
+
76
+ def media_type_header_handler
77
+ type, subtype = Rack::Accept::Header.parse_media_type(media_type)
78
+ env[Grape::Env::API_TYPE] = type
79
+ env[Grape::Env::API_SUBTYPE] = subtype
80
+
81
+ return unless VENDOR_VERSION_HEADER_REGEX =~ subtype
82
+
83
+ env[Grape::Env::API_VENDOR] = Regexp.last_match[1]
84
+ env[Grape::Env::API_VERSION] = Regexp.last_match[2]
85
+ # weird that Grape::Middleware::Formatter also does this
86
+ env[Grape::Env::API_FORMAT] = Regexp.last_match[3]
87
+ end
88
+
89
+ def fail_with_invalid_accept_header!(message)
90
+ fail Grape::Exceptions::InvalidAcceptHeader
91
+ .new(message, error_headers)
92
+ end
93
+
94
+ def fail_with_invalid_version_header!(message)
95
+ fail Grape::Exceptions::InvalidVersionHeader
96
+ .new(message, error_headers)
97
+ end
98
+
67
99
  def available_media_types
68
100
  available_media_types = []
69
101
 
70
102
  content_types.each do |extension, _media_type|
71
103
  versions.reverse_each do |version|
72
- available_media_types += ["application/vnd.#{vendor}-#{version}+#{extension}", "application/vnd.#{vendor}-#{version}"]
104
+ available_media_types += [
105
+ "application/vnd.#{vendor}-#{version}+#{extension}",
106
+ "application/vnd.#{vendor}-#{version}"
107
+ ]
73
108
  end
74
109
  available_media_types << "application/vnd.#{vendor}+#{extension}"
75
110
  end
@@ -83,10 +118,22 @@ module Grape
83
118
  available_media_types.flatten
84
119
  end
85
120
 
121
+ def headers_contain_wrong_vendor?
122
+ header.values.all? do |header_value|
123
+ vendor?(header_value) && request_vendor(header_value) != vendor
124
+ end
125
+ end
126
+
127
+ def headers_contain_wrong_version?
128
+ header.values.all? do |header_value|
129
+ version?(header_value) && !versions.include?(request_version(header_value))
130
+ end
131
+ end
132
+
86
133
  def rack_accept_header
87
134
  Rack::Accept::MediaType.new env[Grape::Http::Headers::HTTP_ACCEPT]
88
135
  rescue RuntimeError => e
89
- raise Grape::Exceptions::InvalidAcceptHeader.new(e.message, error_headers)
136
+ fail_with_invalid_accept_header!(e.message)
90
137
  end
91
138
 
92
139
  def versions
@@ -94,19 +141,25 @@ module Grape
94
141
  end
95
142
 
96
143
  def vendor
97
- options[:version_options] && options[:version_options][:vendor]
144
+ version_options && version_options[:vendor]
98
145
  end
99
146
 
100
147
  def strict?
101
- options[:version_options] && options[:version_options][:strict]
148
+ version_options && version_options[:strict]
149
+ end
150
+
151
+ def version_options
152
+ options[:version_options]
102
153
  end
103
154
 
104
- # By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking
105
- # of routes (see [Rack::Mount](https://github.com/josh/rack-mount) for more information). To prevent
106
- # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
155
+ # By default those errors contain an `X-Cascade` header set to `pass`,
156
+ # which allows nesting and stacking of routes
157
+ # (see [Rack::Mount](https://github.com/josh/rack-mount) for more
158
+ # information). To prevent # this behavior, and not add the `X-Cascade`
159
+ # header, one can set the `:cascade` option to `false`.
107
160
  def cascade?
108
- if options[:version_options] && options[:version_options].key?(:cascade)
109
- !!options[:version_options][:cascade]
161
+ if version_options && version_options.key?(:cascade)
162
+ !!version_options[:cascade]
110
163
  else
111
164
  true
112
165
  end
@@ -118,16 +171,26 @@ module Grape
118
171
 
119
172
  # @param [String] media_type a content type
120
173
  # @return [Boolean] whether the content type sets a vendor
121
- def has_vendor?(media_type)
122
- _, subtype = Rack::Accept::Header.parse_media_type media_type
123
- subtype[/\Avnd\.[a-z0-9*.]+/]
174
+ def vendor?(media_type)
175
+ _, subtype = Rack::Accept::Header.parse_media_type(media_type)
176
+ subtype[HAS_VENDOR_REGEX]
177
+ end
178
+
179
+ def request_vendor(media_type)
180
+ _, subtype = Rack::Accept::Header.parse_media_type(media_type)
181
+ subtype.match(VENDOR_VERSION_HEADER_REGEX)[1]
182
+ end
183
+
184
+ def request_version(media_type)
185
+ _, subtype = Rack::Accept::Header.parse_media_type(media_type)
186
+ subtype.match(VENDOR_VERSION_HEADER_REGEX)[2]
124
187
  end
125
188
 
126
189
  # @param [String] media_type a content type
127
190
  # @return [Boolean] whether the content type sets an API version
128
191
  def version?(media_type)
129
- _, subtype = Rack::Accept::Header.parse_media_type media_type
130
- subtype[/\Avnd\.[a-z0-9*.]+-[a-z0-9*\-.]+/]
192
+ _, subtype = Rack::Accept::Header.parse_media_type(media_type)
193
+ subtype[HAS_VERSION_REGEX]
131
194
  end
132
195
  end
133
196
  end
@@ -21,20 +21,17 @@ module Grape
21
21
  class Param < Base
22
22
  def default_options
23
23
  {
24
- parameter: 'apiver'
24
+ parameter: 'apiver'.freeze
25
25
  }
26
26
  end
27
27
 
28
28
  def before
29
29
  paramkey = options[:parameter]
30
30
  potential_version = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[paramkey]
31
- unless potential_version.nil?
32
- if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
33
- throw :error, status: 404, message: '404 API Version Not Found', headers: { Grape::Http::Headers::X_CASCADE => 'pass' }
34
- end
35
- env['api.version'] = potential_version
36
- env['rack.request.query_hash'].delete(paramkey) if env.key? 'rack.request.query_hash'
37
- end
31
+ return if potential_version.nil?
32
+ throw :error, status: 404, message: '404 API Version Not Found', headers: { Grape::Http::Headers::X_CASCADE => 'pass' } if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
33
+ env[Grape::Env::API_VERSION] = potential_version
34
+ env[Grape::Env::RACK_REQUEST_QUERY_HASH].delete(paramkey) if env.key? Grape::Env::RACK_REQUEST_QUERY_HASH
38
35
  end
39
36
  end
40
37
  end
@@ -0,0 +1,20 @@
1
+ module Rack
2
+ module Accept
3
+ module Header
4
+ class << self
5
+ # Corrected version of https://github.com/mjackson/rack-accept/blob/master/lib/rack/accept/header.rb#L40-L44
6
+ def parse_media_type(media_type)
7
+ # see http://tools.ietf.org/html/rfc6838#section-4.2 for allowed characters in media type names
8
+ m = media_type.to_s.match(%r{^([a-z*]+)\/([a-z0-9*\&\^\-_#\$!.+]+)(?:;([a-z0-9=;]+))?$})
9
+ m ? [m[1], m[2], m[3] || ''] : []
10
+ end
11
+ end
12
+ end
13
+
14
+ class MediaType
15
+ def parse_media_type(media_type)
16
+ Header.parse_media_type(media_type)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -33,12 +33,9 @@ module Grape
33
33
 
34
34
  pieces = path.split('/')
35
35
  potential_version = pieces[1]
36
- if potential_version =~ options[:pattern]
37
- if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
38
- throw :error, status: 404, message: '404 API Version Not Found'
39
- end
40
- env['api.version'] = potential_version
41
- end
36
+ return unless potential_version =~ options[:pattern]
37
+ throw :error, status: 404, message: '404 API Version Not Found' if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
38
+ env[Grape::Env::API_VERSION] = potential_version
42
39
  end
43
40
 
44
41
  private
@@ -1,22 +1,33 @@
1
1
  module Grape
2
+ # A container for endpoints or other namespaces, which allows for both
3
+ # logical grouping of endpoints as well as sharing commonconfiguration.
4
+ # May also be referred to as group, segment, or resource.
2
5
  class Namespace
3
6
  attr_reader :space, :options
4
7
 
5
- # options:
6
- # requirements: a hash
8
+ # @param space [String] the name of this namespace
9
+ # @param options [Hash] options hash
10
+ # @option options :requirements [Hash] param-regex pairs, all of which must
11
+ # be met by a request's params for all endpoints in this namespace, or
12
+ # validation will fail and return a 422.
7
13
  def initialize(space, options = {})
8
14
  @space = space.to_s
9
15
  @options = options
10
16
  end
11
17
 
18
+ # Retrieves the requirements from the options hash, if given.
19
+ # @return [Hash]
12
20
  def requirements
13
21
  options[:requirements] || {}
14
22
  end
15
23
 
24
+ # (see ::joined_space_path)
16
25
  def self.joined_space(settings)
17
26
  (settings || []).map(&:space).join('/')
18
27
  end
19
28
 
29
+ # Join the namespaces from a list of settings to create a path prefix.
30
+ # @param settings [Array] list of Grape::Util::InheritableSettings.
20
31
  def self.joined_space_path(settings)
21
32
  Rack::Mount::Utils.normalize_path(joined_space(settings))
22
33
  end
data/lib/grape/path.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  module Grape
2
+ # Represents a path to an endpoint.
2
3
  class Path
3
4
  def self.prepare(raw_path, namespace, settings)
4
5
  Path.new(raw_path, namespace, settings).path_with_suffix
@@ -28,18 +29,18 @@ module Grape
28
29
  !!(settings[:version] && settings[:version_options][:using] == :path)
29
30
  end
30
31
 
31
- def has_namespace?
32
+ def namespace?
32
33
  namespace && namespace.to_s =~ /^\S/ && namespace != '/'
33
34
  end
34
35
 
35
- def has_path?
36
+ def path?
36
37
  raw_path && raw_path.to_s =~ /^\S/ && raw_path != '/'
37
38
  end
38
39
 
39
40
  def suffix
40
41
  if uses_specific_format?
41
42
  "(.#{settings[:format]})"
42
- elsif !uses_path_versioning? || (has_namespace? || has_path?)
43
+ elsif !uses_path_versioning? || (namespace? || path?)
43
44
  '(.:format)'
44
45
  else
45
46
  '(/.:format)'
@@ -0,0 +1,40 @@
1
+ module Grape
2
+ class Request < Rack::Request
3
+ HTTP_PREFIX = 'HTTP_'.freeze
4
+
5
+ alias_method :rack_params, :params
6
+
7
+ def params
8
+ @params ||= build_params
9
+ end
10
+
11
+ def headers
12
+ @headers ||= build_headers
13
+ end
14
+
15
+ private
16
+
17
+ def build_params
18
+ params = Hashie::Mash.new(rack_params)
19
+ if env[Grape::Env::RACK_ROUTING_ARGS]
20
+ args = env[Grape::Env::RACK_ROUTING_ARGS].dup
21
+ # preserve version from query string parameters
22
+ args.delete(:version)
23
+ args.delete(:route_info)
24
+ params.deep_merge!(args)
25
+ end
26
+ params
27
+ end
28
+
29
+ def build_headers
30
+ headers = {}
31
+ env.each_pair do |k, v|
32
+ next unless k.to_s.start_with? HTTP_PREFIX
33
+
34
+ k = k[5..-1].split('_').each(&:capitalize!).join('-')
35
+ headers[k] = v
36
+ end
37
+ headers
38
+ end
39
+ end
40
+ end
data/lib/grape/route.rb CHANGED
@@ -1,10 +1,12 @@
1
1
  module Grape
2
2
  # A compiled route for inspection.
3
3
  class Route
4
+ # @api private
4
5
  def initialize(options = {})
5
6
  @options = options || {}
6
7
  end
7
8
 
9
+ # @api private
8
10
  def method_missing(method_id, *arguments)
9
11
  match = /route_([_a-zA-Z]\w*)/.match(method_id.to_s)
10
12
  if match
@@ -14,12 +16,15 @@ module Grape
14
16
  end
15
17
  end
16
18
 
19
+ # Generate a short, human-readable representation of this route.
17
20
  def to_s
18
21
  "version=#{route_version}, method=#{route_method}, path=#{route_path}"
19
22
  end
20
23
 
21
24
  private
22
25
 
26
+ # This is defined so that certain Ruby methods which attempt to call #to_ary
27
+ # on objects, e.g. Array#join, will not hit #method_missing.
23
28
  def to_ary
24
29
  nil
25
30
  end
@@ -1,18 +1,18 @@
1
1
  module Grape
2
2
  module ContentTypes
3
3
  # Content types are listed in order of preference.
4
- CONTENT_TYPES = ActiveSupport::OrderedHash[
5
- :xml, 'application/xml',
6
- :serializable_hash, 'application/json',
7
- :json, 'application/json',
8
- :binary, 'application/octet-stream',
9
- :txt, 'text/plain'
10
- ]
4
+ CONTENT_TYPES = {
5
+ xml: 'application/xml',
6
+ serializable_hash: 'application/json',
7
+ json: 'application/json',
8
+ binary: 'application/octet-stream',
9
+ txt: 'text/plain'
10
+ }
11
11
 
12
12
  def self.content_types_for_settings(settings)
13
- return nil if settings.nil? || settings.blank?
13
+ return if settings.blank?
14
14
 
15
- settings.each_with_object(ActiveSupport::OrderedHash.new) { |value, result| result.merge!(value) }
15
+ settings.each_with_object({}) { |value, result| result.merge!(value) }
16
16
  end
17
17
 
18
18
  def self.content_types_for(from_settings)