grape 0.15.0 → 0.16.1

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -1
  3. data/Gemfile.lock +16 -15
  4. data/README.md +41 -47
  5. data/UPGRADING.md +62 -0
  6. data/gemfiles/rails_3.gemfile.lock +225 -0
  7. data/grape.gemspec +2 -2
  8. data/lib/grape.rb +31 -26
  9. data/lib/grape/api.rb +39 -23
  10. data/lib/grape/dsl/inside_route.rb +8 -4
  11. data/lib/grape/dsl/routing.rb +2 -1
  12. data/lib/grape/endpoint.rb +43 -62
  13. data/lib/grape/error_formatter.rb +4 -2
  14. data/lib/grape/error_formatter/base.rb +10 -6
  15. data/lib/grape/formatter.rb +4 -2
  16. data/lib/grape/http/headers.rb +1 -0
  17. data/lib/grape/middleware/formatter.rb +2 -2
  18. data/lib/grape/middleware/versioner/accept_version_header.rb +2 -2
  19. data/lib/grape/middleware/versioner/header.rb +2 -2
  20. data/lib/grape/middleware/versioner/path.rb +2 -2
  21. data/lib/grape/namespace.rb +1 -1
  22. data/lib/grape/parser.rb +4 -2
  23. data/lib/grape/path.rb +3 -3
  24. data/lib/grape/request.rb +2 -2
  25. data/lib/grape/router.rb +156 -0
  26. data/lib/grape/router/attribute_translator.rb +40 -0
  27. data/lib/grape/router/pattern.rb +55 -0
  28. data/lib/grape/router/route.rb +105 -0
  29. data/lib/grape/serve_file/file_body.rb +34 -0
  30. data/lib/grape/{util → serve_file}/file_response.rb +1 -1
  31. data/lib/grape/{util → serve_file}/sendfile_response.rb +1 -1
  32. data/lib/grape/util/env.rb +1 -1
  33. data/lib/grape/util/registrable.rb +13 -0
  34. data/lib/grape/validations/types/custom_type_coercer.rb +2 -0
  35. data/lib/grape/version.rb +1 -1
  36. data/spec/grape/api/invalid_format_spec.rb +43 -0
  37. data/spec/grape/api/recognize_path_spec.rb +21 -0
  38. data/spec/grape/api/required_parameters_with_invalid_method_spec.rb +26 -0
  39. data/spec/grape/api_spec.rb +110 -38
  40. data/spec/grape/dsl/inside_route_spec.rb +267 -240
  41. data/spec/grape/endpoint_spec.rb +10 -0
  42. data/spec/grape/entity_spec.rb +2 -2
  43. data/spec/grape/middleware/formatter_spec.rb +23 -4
  44. data/spec/grape/middleware/versioner/header_spec.rb +1 -1
  45. data/spec/grape/middleware/versioner/path_spec.rb +1 -1
  46. data/spec/grape/parser_spec.rb +82 -0
  47. data/spec/grape/request_spec.rb +2 -2
  48. data/spec/grape/validations/params_scope_spec.rb +2 -2
  49. data/spec/grape/validations/validators/coerce_spec.rb +51 -0
  50. data/spec/grape/validations_spec.rb +1 -1
  51. data/tmp/Gemfile.lock +63 -0
  52. metadata +70 -55
  53. data/lib/grape/route.rb +0 -32
@@ -13,7 +13,7 @@ Gem::Specification.new do |s|
13
13
  s.license = 'MIT'
14
14
 
15
15
  s.add_runtime_dependency 'rack', '>= 1.3.0'
16
- s.add_runtime_dependency 'rack-mount'
16
+ s.add_runtime_dependency 'mustermann19', '~> 0.4.3'
17
17
  s.add_runtime_dependency 'rack-accept'
18
18
  s.add_runtime_dependency 'activesupport'
19
19
  s.add_runtime_dependency 'multi_json', '>= 1.3.2'
@@ -23,7 +23,7 @@ Gem::Specification.new do |s|
23
23
  s.add_runtime_dependency 'builder'
24
24
 
25
25
  s.add_development_dependency 'grape-entity', '>= 0.4.4'
26
- s.add_development_dependency 'rake'
26
+ s.add_development_dependency 'rake', '~> 10'
27
27
  s.add_development_dependency 'maruku'
28
28
  s.add_development_dependency 'yard'
29
29
  s.add_development_dependency 'rack-test'
@@ -1,6 +1,5 @@
1
1
  require 'logger'
2
2
  require 'rack'
3
- require 'rack/mount'
4
3
  require 'rack/builder'
5
4
  require 'rack/accept'
6
5
  require 'rack/auth/basic'
@@ -33,8 +32,8 @@ module Grape
33
32
  eager_autoload do
34
33
  autoload :API
35
34
  autoload :Endpoint
35
+ autoload :Router
36
36
 
37
- autoload :Route
38
37
  autoload :Namespace
39
38
 
40
39
  autoload :Path
@@ -78,28 +77,6 @@ module Grape
78
77
  autoload :MethodNotAllowed
79
78
  end
80
79
 
81
- module ErrorFormatter
82
- extend ActiveSupport::Autoload
83
- autoload :Base
84
- autoload :Json
85
- autoload :Txt
86
- autoload :Xml
87
- end
88
-
89
- module Formatter
90
- extend ActiveSupport::Autoload
91
- autoload :Json
92
- autoload :SerializableHash
93
- autoload :Txt
94
- autoload :Xml
95
- end
96
-
97
- module Parser
98
- extend ActiveSupport::Autoload
99
- autoload :Json
100
- autoload :Xml
101
- end
102
-
103
80
  module Middleware
104
81
  extend ActiveSupport::Autoload
105
82
  autoload :Base
@@ -131,8 +108,29 @@ module Grape
131
108
  autoload :StackableValues
132
109
  autoload :InheritableSetting
133
110
  autoload :StrictHashConfiguration
134
- autoload :FileResponse
135
- autoload :SendfileResponse
111
+ autoload :Registrable
112
+ end
113
+
114
+ module ErrorFormatter
115
+ extend ActiveSupport::Autoload
116
+ autoload :Base
117
+ autoload :Json
118
+ autoload :Txt
119
+ autoload :Xml
120
+ end
121
+
122
+ module Formatter
123
+ extend ActiveSupport::Autoload
124
+ autoload :Json
125
+ autoload :SerializableHash
126
+ autoload :Txt
127
+ autoload :Xml
128
+ end
129
+
130
+ module Parser
131
+ extend ActiveSupport::Autoload
132
+ autoload :Json
133
+ autoload :Xml
136
134
  end
137
135
 
138
136
  module DSL
@@ -163,6 +161,13 @@ module Grape
163
161
  extend ActiveSupport::Autoload
164
162
  autoload :Presenter
165
163
  end
164
+
165
+ module ServeFile
166
+ extend ActiveSupport::Autoload
167
+ autoload :FileResponse
168
+ autoload :FileBody
169
+ autoload :SendfileResponse
170
+ end
166
171
  end
167
172
 
168
173
  require 'grape/util/content_types'
@@ -1,3 +1,5 @@
1
+ require 'grape/router'
2
+
1
3
  module Grape
2
4
  # The API class is the primary entry point for creating Grape APIs. Users
3
5
  # should subclass this class in order to build an API.
@@ -52,6 +54,12 @@ module Grape
52
54
  end
53
55
  end
54
56
 
57
+ # see Grape::Router#recognize_path
58
+ def recognize_path(path)
59
+ LOCK.synchronize { compile } unless instance
60
+ instance.router.recognize_path(path)
61
+ end
62
+
55
63
  protected
56
64
 
57
65
  def prepare_routes
@@ -84,27 +92,30 @@ module Grape
84
92
  end
85
93
  end
86
94
 
95
+ attr_reader :router
96
+
87
97
  # Builds the routes from the defined endpoints, effectively compiling
88
98
  # this API into a usable form.
89
99
  def initialize
90
- @route_set = Rack::Mount::RouteSet.new
100
+ @router = Router.new
91
101
  add_head_not_allowed_methods_and_options_methods
92
102
  self.class.endpoints.each do |endpoint|
93
- endpoint.mount_in(@route_set)
103
+ endpoint.mount_in(@router)
94
104
  end
95
105
 
96
- @route_set.freeze
106
+ @router.compile!
107
+ @router.freeze
97
108
  end
98
109
 
99
110
  # Handle a request. See Rack documentation for what `env` is.
100
111
  def call(env)
101
- result = @route_set.call(env)
112
+ result = @router.call(env)
102
113
  result[1].delete(Grape::Http::Headers::X_CASCADE) unless cascade?
103
114
  result
104
115
  end
105
116
 
106
117
  # Some requests may return a HTTP 404 error if grape cannot find a matching
107
- # route. In this case, Rack::Mount adds a X-Cascade header to the response
118
+ # route. In this case, Grape::Router adds a X-Cascade header to the response
108
119
  # and sets it to 'pass', indicating to grape's parents they should keep
109
120
  # looking for a matching route on other resources.
110
121
  #
@@ -126,20 +137,24 @@ module Grape
126
137
  # will return an HTTP 405 response for any HTTP method that the resource
127
138
  # cannot handle.
128
139
  def add_head_not_allowed_methods_and_options_methods
129
- methods_per_path = {}
140
+ routes_map = {}
130
141
 
131
142
  self.class.endpoints.each do |endpoint|
132
143
  routes = endpoint.routes
133
144
  routes.each do |route|
134
- route_path = route.route_path
135
- .gsub(/\(.*\)/, '') # ignore any optional portions
136
- .gsub(%r{\:[^\/.?]+}, ':x') # substitute variable names to avoid conflicts
137
-
138
- methods_per_path[route_path] ||= []
139
- methods_per_path[route_path] << route.route_method
145
+ # using the :any shorthand produces [nil] for route methods, substitute all manually
146
+ route_key = route.pattern.to_regexp
147
+ routes_map[route_key] ||= {}
148
+ route_settings = routes_map[route_key]
149
+ route_settings[:pattern] = route.pattern
150
+ route_settings[:requirements] = route.requirements
151
+ route_settings[:path] = route.origin
152
+ route_settings[:methods] ||= []
153
+ route_settings[:methods] << route.request_method
154
+ route_settings[:endpoint] = route.app
140
155
 
141
156
  # using the :any shorthand produces [nil] for route methods, substitute all manually
142
- methods_per_path[route_path] = %w(GET PUT POST DELETE PATCH HEAD OPTIONS) if methods_per_path[route_path].compact.empty?
157
+ route_settings[:methods] = %w(GET PUT POST DELETE PATCH HEAD OPTIONS) if route_settings[:methods].include?('ANY')
143
158
  end
144
159
  end
145
160
 
@@ -149,7 +164,9 @@ module Grape
149
164
  # informations again.
150
165
  without_root_prefix do
151
166
  without_versioning do
152
- methods_per_path.each do |path, methods|
167
+ routes_map.each do |_, config|
168
+ methods = config[:methods]
169
+ path = config[:path]
153
170
  allowed_methods = methods.dup
154
171
 
155
172
  unless self.class.namespace_inheritable(:do_not_route_head)
@@ -159,18 +176,19 @@ module Grape
159
176
  allow_header = (self.class.namespace_inheritable(:do_not_route_options) ? allowed_methods : [Grape::Http::Headers::OPTIONS] | allowed_methods).join(', ')
160
177
 
161
178
  unless self.class.namespace_inheritable(:do_not_route_options)
162
- generate_options_method(path, allow_header) unless allowed_methods.include?(Grape::Http::Headers::OPTIONS)
179
+ generate_options_method(path, allow_header, config) unless allowed_methods.include?(Grape::Http::Headers::OPTIONS)
163
180
  end
164
181
 
165
- generate_not_allowed_method(path, allowed_methods, allow_header)
182
+ attributes = config.merge(allowed_methods: allowed_methods, allow_header: allow_header)
183
+ generate_not_allowed_method(config[:pattern], attributes)
166
184
  end
167
185
  end
168
186
  end
169
187
  end
170
188
 
171
189
  # Generate an 'OPTIONS' route for a pre-exisiting user defined route
172
- def generate_options_method(path, allow_header)
173
- self.class.options(path, {}) do
190
+ def generate_options_method(path, allow_header, options = {})
191
+ self.class.options(path, options) do
174
192
  header 'Allow', allow_header
175
193
  status 204
176
194
  ''
@@ -179,15 +197,13 @@ module Grape
179
197
 
180
198
  # Generate a route that returns an HTTP 405 response for a user defined
181
199
  # path on methods not specified
182
- def generate_not_allowed_method(path, allowed_methods, allow_header)
183
- not_allowed_methods = %w(GET PUT POST DELETE PATCH HEAD) - allowed_methods
200
+ def generate_not_allowed_method(pattern, attributes = {})
201
+ not_allowed_methods = %w(GET PUT POST DELETE PATCH HEAD) - attributes[:allowed_methods]
184
202
  not_allowed_methods << Grape::Http::Headers::OPTIONS if self.class.namespace_inheritable(:do_not_route_options)
185
203
 
186
204
  return if not_allowed_methods.empty?
187
205
 
188
- self.class.route(not_allowed_methods, path) do
189
- fail Grape::Exceptions::MethodNotAllowed, header.merge('Allow' => allow_header)
190
- end
206
+ @router.associate_routes(pattern, attributes.merge(not_allowed_methods: not_allowed_methods))
191
207
  end
192
208
 
193
209
  # Allows definition of endpoints that ignore the versioning configuration
@@ -191,8 +191,12 @@ module Grape
191
191
  #
192
192
  # GET /file # => "contents of file"
193
193
  def file(value = nil)
194
- if value
195
- @file = Grape::Util::FileResponse.new(value)
194
+ if value.is_a?(String)
195
+ file_body = Grape::ServeFile::FileBody.new(value)
196
+ @file = Grape::ServeFile::FileResponse.new(file_body)
197
+ elsif !value.is_a?(NilClass)
198
+ warn '[DEPRECATION] Argument as FileStreamer-like object is deprecated. Use path to file instead.'
199
+ @file = Grape::ServeFile::FileResponse.new(value)
196
200
  else
197
201
  @file
198
202
  end
@@ -269,10 +273,10 @@ module Grape
269
273
  #
270
274
  # desc "Returns the route description."
271
275
  # get '/' do
272
- # route.route_description
276
+ # route.description
273
277
  # end
274
278
  def route
275
- env[Grape::Env::RACK_ROUTING_ARGS][:route_info]
279
+ env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info]
276
280
  end
277
281
 
278
282
  # Attempt to locate the Entity class for a given object, if not given
@@ -82,7 +82,7 @@ module Grape
82
82
  in_setting = inheritable_setting
83
83
 
84
84
  if app.respond_to?(:inheritable_setting, true)
85
- mount_path = Rack::Mount::Utils.normalize_path(path)
85
+ mount_path = Grape::Router.normalize_path(path)
86
86
  app.top_level_setting.namespace_stackable[:mount_path] = mount_path
87
87
 
88
88
  app.inherit_settings(inheritable_setting)
@@ -98,6 +98,7 @@ module Grape
98
98
  method: :any,
99
99
  path: path,
100
100
  app: app,
101
+ forward_match: !app.respond_to?(:inheritable_setting),
101
102
  for: self
102
103
  )
103
104
  end
@@ -110,7 +110,7 @@ module Grape
110
110
  end
111
111
 
112
112
  def routes
113
- @routes ||= endpoints ? endpoints.collect(&:routes).flatten : prepare_routes
113
+ @routes ||= endpoints ? endpoints.collect(&:routes).flatten : to_routes
114
114
  end
115
115
 
116
116
  def reset_routes!
@@ -119,29 +119,36 @@ module Grape
119
119
  @routes = nil
120
120
  end
121
121
 
122
- def mount_in(route_set)
122
+ def mount_in(router)
123
123
  if endpoints
124
- endpoints.each do |e|
125
- e.mount_in(route_set)
126
- end
124
+ endpoints.each { |e| e.mount_in(router) }
127
125
  else
128
126
  reset_routes!
129
-
130
127
  routes.each do |route|
131
- methods = [route.route_method]
132
- if !namespace_inheritable(:do_not_route_head) && route.route_method == Grape::Http::Headers::GET
128
+ methods = [route.request_method]
129
+ if !namespace_inheritable(:do_not_route_head) && route.request_method == Grape::Http::Headers::GET
133
130
  methods << Grape::Http::Headers::HEAD
134
131
  end
135
132
  methods.each do |method|
136
- route_set.add_route(self, {
137
- path_info: route.route_compiled,
138
- request_method: method
139
- }, route_info: route)
133
+ unless route.request_method.to_s.upcase == method
134
+ route = Grape::Router::Route.new(method, route.origin, route.attributes.to_h)
135
+ end
136
+ router.append(route.apply(self))
140
137
  end
141
138
  end
142
139
  end
143
140
  end
144
141
 
142
+ def to_routes
143
+ route_options = prepare_default_route_attributes
144
+ map_routes do |method, path|
145
+ path = prepare_path(path)
146
+ params = merge_route_options(route_options.merge(suffix: path.suffix))
147
+ route = Router::Route.new(method, path.path, params)
148
+ route.apply(self)
149
+ end.flatten
150
+ end
151
+
145
152
  def prepare_routes_requirements
146
153
  endpoint_requirements = options[:route_options][:requirements] || {}
147
154
  all_requirements = (namespace_stackable(:namespace).map(&:requirements) << endpoint_requirements)
@@ -150,41 +157,30 @@ module Grape
150
157
  end
151
158
  end
152
159
 
153
- def prepare_routes_path_params(path)
154
- path_params = {}
155
-
156
- # named parameters in the api path
157
- regex = Rack::Mount::RegexpWithNamedGroups.new(path)
158
- named_params = regex.named_captures.map { |nc| nc[0] } - %w(version format)
159
- named_params.each { |named_param| path_params[named_param] = '' }
160
+ def prepare_default_route_attributes
161
+ {
162
+ namespace: namespace,
163
+ version: prepare_version,
164
+ requirements: prepare_routes_requirements,
165
+ prefix: namespace_inheritable(:root_prefix),
166
+ anchor: options[:route_options].fetch(:anchor, true),
167
+ settings: inheritable_setting.route.except(:saved_declared_params, :saved_validations),
168
+ forward_match: options[:forward_match]
169
+ }
170
+ end
160
171
 
161
- # route parameters declared via desc or appended to the api declaration
162
- route_params = options[:route_options][:params]
163
- path_params.merge! route_params if route_params
172
+ def prepare_version
173
+ version = namespace_inheritable(:version) || []
174
+ return if version.length == 0
175
+ version.length == 1 ? version.first.to_s : version
176
+ end
164
177
 
165
- path_params
178
+ def merge_route_options(default = {})
179
+ options[:route_options].clone.reverse_merge(default)
166
180
  end
167
181
 
168
- def prepare_routes
169
- options[:method].map do |method|
170
- options[:path].map do |path|
171
- prepared_path = prepare_path(path)
172
- anchor = options[:route_options].fetch(:anchor, true)
173
- path = compile_path(prepared_path, anchor && !options[:app], prepare_routes_requirements)
174
- request_method = (method.to_s.upcase unless method == :any)
175
-
176
- Route.new(options[:route_options].clone.merge(
177
- prefix: namespace_inheritable(:root_prefix),
178
- version: namespace_inheritable(:version) ? namespace_inheritable(:version).join('|') : nil,
179
- namespace: namespace,
180
- method: request_method,
181
- path: prepared_path,
182
- params: prepare_routes_path_params(path),
183
- compiled: path,
184
- settings: inheritable_setting.route.except(:saved_declared_params, :saved_validations)
185
- ))
186
- end
187
- end.flatten
182
+ def map_routes
183
+ options[:method].map { |method| options[:path].map { |path| yield method, path } }
188
184
  end
189
185
 
190
186
  def prepare_path(path)
@@ -196,13 +192,6 @@ module Grape
196
192
  @namespace ||= Namespace.joined_space_path(namespace_stackable(:namespace))
197
193
  end
198
194
 
199
- def compile_path(prepared_path, anchor = true, requirements = {})
200
- endpoint_options = {}
201
- endpoint_options[:version] = /#{namespace_inheritable(:version).join('|')}/ if namespace_inheritable(:version)
202
- endpoint_options.merge!(requirements)
203
- Rack::Mount::Strexp.compile(prepared_path, endpoint_options, %w( / . ? ), anchor)
204
- end
205
-
206
195
  def call(env)
207
196
  lazy_initialize!
208
197
  dup.call!(env)
@@ -242,7 +231,7 @@ module Grape
242
231
 
243
232
  run_filters before_validations, :before_validation
244
233
 
245
- run_validators validations, request
234
+ run_validators validations, request unless @method_not_allowed
246
235
 
247
236
  run_filters after_validations, :after_validation
248
237
 
@@ -275,11 +264,7 @@ module Grape
275
264
  (namespace_stackable(:middleware) || []).each do |m|
276
265
  m = m.dup
277
266
  block = m.pop if m.last.is_a?(Proc)
278
- if block
279
- b.use(*m, &block)
280
- else
281
- b.use(*m)
282
- end
267
+ block ? b.use(*m, &block) : b.use(*m)
283
268
  end
284
269
 
285
270
  if namespace_inheritable(:version)
@@ -305,9 +290,7 @@ module Grape
305
290
  def build_helpers
306
291
  helpers = namespace_stackable(:helpers) || []
307
292
  Module.new do
308
- helpers.each do |mod_to_include|
309
- include mod_to_include
310
- end
293
+ helpers.each { |mod_to_include| include mod_to_include }
311
294
  end
312
295
  end
313
296
 
@@ -348,9 +331,7 @@ module Grape
348
331
 
349
332
  def run_filters(filters, type = :other)
350
333
  ActiveSupport::Notifications.instrument('endpoint_run_filters.grape', endpoint: self, filters: filters, type: type) do
351
- (filters || []).each do |filter|
352
- instance_eval(&filter)
353
- end
334
+ (filters || []).each { |filter| instance_eval(&filter) }
354
335
  end
355
336
  post_extension = DSL::InsideRoute.post_filter_methods(type)
356
337
  extend post_extension if post_extension