grape 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/Appraisals +3 -7
  3. data/CHANGELOG.md +21 -1
  4. data/Gemfile.lock +26 -26
  5. data/README.md +109 -29
  6. data/UPGRADING.md +45 -0
  7. data/gemfiles/rack_1.5.2.gemfile.lock +232 -0
  8. data/gemfiles/rails_3.gemfile +1 -1
  9. data/gemfiles/rails_3.gemfile.lock +288 -0
  10. data/gemfiles/rails_4.gemfile +1 -1
  11. data/gemfiles/rails_4.gemfile.lock +280 -0
  12. data/gemfiles/rails_5.gemfile +1 -1
  13. data/gemfiles/rails_5.gemfile.lock +312 -0
  14. data/lib/grape.rb +1 -0
  15. data/lib/grape/api.rb +74 -195
  16. data/lib/grape/api/instance.rb +242 -0
  17. data/lib/grape/dsl/desc.rb +17 -1
  18. data/lib/grape/dsl/middleware.rb +7 -0
  19. data/lib/grape/dsl/parameters.rb +9 -4
  20. data/lib/grape/dsl/routing.rb +5 -1
  21. data/lib/grape/endpoint.rb +1 -1
  22. data/lib/grape/exceptions/base.rb +9 -1
  23. data/lib/grape/exceptions/invalid_response.rb +9 -0
  24. data/lib/grape/locale/en.yml +1 -0
  25. data/lib/grape/middleware/error.rb +8 -2
  26. data/lib/grape/middleware/versioner/header.rb +2 -2
  27. data/lib/grape/validations/params_scope.rb +1 -0
  28. data/lib/grape/validations/validators/multiple_params_base.rb +1 -1
  29. data/lib/grape/version.rb +1 -1
  30. data/pkg/grape-1.2.0.gem +0 -0
  31. data/spec/grape/api/routes_with_requirements_spec.rb +59 -0
  32. data/spec/grape/api_remount_spec.rb +85 -0
  33. data/spec/grape/api_spec.rb +70 -1
  34. data/spec/grape/dsl/desc_spec.rb +17 -1
  35. data/spec/grape/dsl/middleware_spec.rb +8 -0
  36. data/spec/grape/dsl/routing_spec.rb +10 -0
  37. data/spec/grape/exceptions/base_spec.rb +61 -0
  38. data/spec/grape/exceptions/invalid_response_spec.rb +11 -0
  39. data/spec/grape/middleware/auth/dsl_spec.rb +3 -3
  40. data/spec/grape/middleware/exception_spec.rb +1 -1
  41. data/spec/grape/middleware/versioner/header_spec.rb +6 -0
  42. data/spec/grape/validations/params_scope_spec.rb +133 -0
  43. data/spec/spec_helper.rb +3 -1
  44. metadata +99 -87
  45. data/gemfiles/rack_1.5.2.gemfile +0 -35
  46. data/pkg/grape-0.17.0.gem +0 -0
  47. data/pkg/grape-0.19.0.gem +0 -0
@@ -4,6 +4,7 @@ module Grape
4
4
  include Grape::DSL::Settings
5
5
 
6
6
  # Add a description to the next namespace or function.
7
+ # @option options :summary [String] summary for this endpoint
7
8
  # @param description [String] descriptive string for this endpoint
8
9
  # or namespace
9
10
  # @param options [Hash] other properties you can set to describe the
@@ -17,6 +18,13 @@ module Grape
17
18
  # endpoint may return, with their meanings, in a 2d array
18
19
  # @option options :named [String] a specific name to help find this route
19
20
  # @option options :headers [Hash] HTTP headers this method can accept
21
+ # @option options :hidden [Boolean] hide the endpoint or not
22
+ # @option options :deprecated [Boolean] deprecate the endpoint or not
23
+ # @option options :is_array [Boolean] response entity is array or not
24
+ # @option options :nickname [String] nickname of the endpoint
25
+ # @option options :produces [Array[String]] a list of MIME types the endpoint produce
26
+ # @option options :consumes [Array[String]] a list of MIME types the endpoint consume
27
+ # @option options :tags [Array[String]] a list of tags
20
28
  # @yield a block yielding an instance context with methods mapping to
21
29
  # each of the above, except that :entity is also aliased as #success
22
30
  # and :http_codes is aliased as #failure.
@@ -78,13 +86,21 @@ module Grape
78
86
  def desc_container
79
87
  Module.new do
80
88
  include Grape::Util::StrictHashConfiguration.module(
89
+ :summary,
81
90
  :description,
82
91
  :detail,
83
92
  :params,
84
93
  :entity,
85
94
  :http_codes,
86
95
  :named,
87
- :headers
96
+ :headers,
97
+ :hidden,
98
+ :deprecated,
99
+ :is_array,
100
+ :nickname,
101
+ :produces,
102
+ :consumes,
103
+ :tags
88
104
  )
89
105
 
90
106
  def config_context.success(*args)
@@ -21,6 +21,13 @@ module Grape
21
21
  namespace_stackable(:middleware, arr)
22
22
  end
23
23
 
24
+ def insert(*args, &block)
25
+ arr = [:insert, *args]
26
+ arr << block if block_given?
27
+
28
+ namespace_stackable(:middleware, arr)
29
+ end
30
+
24
31
  def insert_before(*args, &block)
25
32
  arr = [:insert_before, *args]
26
33
  arr << block if block_given?
@@ -211,10 +211,15 @@ module Grape
211
211
  # block yet.
212
212
  # @return [Boolean] whether the parameter has been defined
213
213
  def declared_param?(param)
214
- # @declared_params also includes hashes of options and such, but those
215
- # won't be flattened out.
216
- @declared_params.flatten.any? do |declared_param|
217
- first_hash_key_or_param(declared_param) == param
214
+ if lateral?
215
+ # Elements of @declared_params of lateral scope are pushed in @parent. So check them in @parent.
216
+ @parent.declared_param?(param)
217
+ else
218
+ # @declared_params also includes hashes of options and such, but those
219
+ # won't be flattened out.
220
+ @declared_params.flatten.any? do |declared_param|
221
+ first_hash_key_or_param(declared_param) == param
222
+ end
218
223
  end
219
224
  end
220
225
 
@@ -77,9 +77,13 @@ module Grape
77
77
  namespace_inheritable(:do_not_route_options, true)
78
78
  end
79
79
 
80
- def mount(mounts)
80
+ def mount(mounts, opts = {})
81
81
  mounts = { mounts => '/' } unless mounts.respond_to?(:each_pair)
82
82
  mounts.each_pair do |app, path|
83
+ if app.respond_to?(:mount_instance)
84
+ mount(app.mount_instance(configuration: opts[:with] || {}) => path)
85
+ next
86
+ end
83
87
  in_setting = inheritable_setting
84
88
 
85
89
  if app.respond_to?(:inheritable_setting, true)
@@ -200,7 +200,7 @@ module Grape
200
200
  end
201
201
 
202
202
  def merge_route_options(**default)
203
- options[:route_options].clone.reverse_merge(**default)
203
+ options[:route_options].clone.merge(**default)
204
204
  end
205
205
 
206
206
  def map_routes
@@ -74,7 +74,15 @@ module Grape
74
74
  options = options.dup
75
75
  options[:default] &&= options[:default].to_s
76
76
  message = ::I18n.translate(key, **options)
77
- message.present? ? message : ::I18n.translate(key, locale: FALLBACK_LOCALE, **options)
77
+ message.present? ? message : fallback_message(key, **options)
78
+ end
79
+
80
+ def fallback_message(key, **options)
81
+ if ::I18n.enforce_available_locales && !::I18n.available_locales.include?(FALLBACK_LOCALE)
82
+ key
83
+ else
84
+ ::I18n.translate(key, locale: FALLBACK_LOCALE, **options)
85
+ end
78
86
  end
79
87
  end
80
88
  end
@@ -0,0 +1,9 @@
1
+ module Grape
2
+ module Exceptions
3
+ class InvalidResponse < Base
4
+ def initialize
5
+ super(message: compose_message(:invalid_response))
6
+ end
7
+ end
8
+ end
9
+ end
@@ -49,4 +49,5 @@ en:
49
49
  invalid_version_header:
50
50
  problem: 'Invalid version header'
51
51
  resolution: '%{message}'
52
+ invalid_response: 'Invalid response'
52
53
 
@@ -73,7 +73,7 @@ module Grape
73
73
  if headers[Grape::Http::Headers::CONTENT_TYPE] == TEXT_HTML
74
74
  message = ERB::Util.html_escape(message)
75
75
  end
76
- Rack::Response.new([message], status, headers).finish
76
+ Rack::Response.new([message], status, headers)
77
77
  end
78
78
 
79
79
  def format_message(message, backtrace, original_exception = nil)
@@ -127,7 +127,13 @@ module Grape
127
127
  handler = public_method(handler)
128
128
  end
129
129
 
130
- handler.arity.zero? ? instance_exec(&handler) : instance_exec(error, &handler)
130
+ response = handler.arity.zero? ? instance_exec(&handler) : instance_exec(error, &handler)
131
+
132
+ if response.is_a?(Rack::Response)
133
+ response
134
+ else
135
+ run_rescue_handler(:default_rescue_handler, Grape::Exceptions::InvalidResponse.new)
136
+ end
131
137
  end
132
138
  end
133
139
  end
@@ -173,7 +173,7 @@ module Grape
173
173
  # @return [Boolean] whether the content type sets a vendor
174
174
  def vendor?(media_type)
175
175
  _, subtype = Rack::Accept::Header.parse_media_type(media_type)
176
- subtype[HAS_VENDOR_REGEX]
176
+ subtype.present? && subtype[HAS_VENDOR_REGEX]
177
177
  end
178
178
 
179
179
  def request_vendor(media_type)
@@ -190,7 +190,7 @@ module Grape
190
190
  # @return [Boolean] whether the content type sets an API version
191
191
  def version?(media_type)
192
192
  _, subtype = Rack::Accept::Header.parse_media_type(media_type)
193
- subtype[HAS_VERSION_REGEX]
193
+ subtype.present? && subtype[HAS_VERSION_REGEX]
194
194
  end
195
195
  end
196
196
  end
@@ -121,6 +121,7 @@ module Grape
121
121
  if opts && opts[:as]
122
122
  @api.route_setting(:aliased_params, @api.route_setting(:aliased_params) || [])
123
123
  @api.route_setting(:aliased_params) << { attrs.first => opts[:as] }
124
+ attrs = [opts[:as]]
124
125
  end
125
126
 
126
127
  @declared_params.concat attrs
@@ -11,7 +11,7 @@ module Grape
11
11
  private
12
12
 
13
13
  def scope_requires_params
14
- @scope.required? || scoped_params.any?(&:any?)
14
+ @scope.required? || scoped_params.any? { |param| param.respond_to?(:any?) && param.any? }
15
15
  end
16
16
 
17
17
  def keys_in_common(resource_params)
@@ -1,4 +1,4 @@
1
1
  module Grape
2
2
  # The current version of Grape.
3
- VERSION = '1.1.0'.freeze
3
+ VERSION = '1.2.0'.freeze
4
4
  end
Binary file
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+
3
+ describe Grape::Endpoint do
4
+ subject { Class.new(Grape::API) }
5
+
6
+ def app
7
+ subject
8
+ end
9
+
10
+ context 'get' do
11
+ it 'routes to a namespace param with dots' do
12
+ subject.namespace ':ns_with_dots', requirements: { ns_with_dots: %r{[^\/]+} } do
13
+ get '/' do
14
+ params[:ns_with_dots]
15
+ end
16
+ end
17
+
18
+ get '/test.id.with.dots'
19
+ expect(last_response.status).to eq 200
20
+ expect(last_response.body).to eq 'test.id.with.dots'
21
+ end
22
+
23
+ it 'routes to a path with multiple params with dots' do
24
+ subject.get ':id_with_dots/:another_id_with_dots', requirements: { id_with_dots: %r{[^\/]+},
25
+ another_id_with_dots: %r{[^\/]+} } do
26
+ "#{params[:id_with_dots]}/#{params[:another_id_with_dots]}"
27
+ end
28
+
29
+ get '/test.id/test2.id'
30
+ expect(last_response.status).to eq 200
31
+ expect(last_response.body).to eq 'test.id/test2.id'
32
+ end
33
+
34
+ it 'routes to namespace and path params with dots, with overridden requirements' do
35
+ subject.namespace ':ns_with_dots', requirements: { ns_with_dots: %r{[^\/]+} } do
36
+ get ':another_id_with_dots', requirements: { ns_with_dots: %r{[^\/]+},
37
+ another_id_with_dots: %r{[^\/]+} } do
38
+ "#{params[:ns_with_dots]}/#{params[:another_id_with_dots]}"
39
+ end
40
+ end
41
+
42
+ get '/test.id/test2.id'
43
+ expect(last_response.status).to eq 200
44
+ expect(last_response.body).to eq 'test.id/test2.id'
45
+ end
46
+
47
+ it 'routes to namespace and path params with dots, with merged requirements' do
48
+ subject.namespace ':ns_with_dots', requirements: { ns_with_dots: %r{[^\/]+} } do
49
+ get ':another_id_with_dots', requirements: { another_id_with_dots: %r{[^\/]+} } do
50
+ "#{params[:ns_with_dots]}/#{params[:another_id_with_dots]}"
51
+ end
52
+ end
53
+
54
+ get '/test.id/test2.id'
55
+ expect(last_response.status).to eq 200
56
+ expect(last_response.body).to eq 'test.id/test2.id'
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,85 @@
1
+ require 'spec_helper'
2
+ require 'shared/versioning_examples'
3
+
4
+ describe Grape::API do
5
+ subject(:a_remounted_api) { Class.new(Grape::API) }
6
+ let(:root_api) { Class.new(Grape::API) }
7
+
8
+ def app
9
+ root_api
10
+ end
11
+
12
+ describe 'remounting an API' do
13
+ context 'with a defined route' do
14
+ before do
15
+ a_remounted_api.get '/votes' do
16
+ '10 votes'
17
+ end
18
+ end
19
+
20
+ context 'when mounting one instance' do
21
+ before do
22
+ root_api.mount a_remounted_api
23
+ end
24
+
25
+ it 'can access the endpoint' do
26
+ get '/votes'
27
+ expect(last_response.body).to eql '10 votes'
28
+ end
29
+ end
30
+
31
+ context 'when mounting twice' do
32
+ before do
33
+ root_api.mount a_remounted_api => '/posts'
34
+ root_api.mount a_remounted_api => '/comments'
35
+ end
36
+
37
+ it 'can access the votes in both places' do
38
+ get '/posts/votes'
39
+ expect(last_response.body).to eql '10 votes'
40
+ get '/comments/votes'
41
+ expect(last_response.body).to eql '10 votes'
42
+ end
43
+ end
44
+
45
+ context 'when mounting on namespace' do
46
+ before do
47
+ stub_const('StaticRefToAPI', a_remounted_api)
48
+ root_api.namespace 'posts' do
49
+ mount StaticRefToAPI
50
+ end
51
+
52
+ root_api.namespace 'comments' do
53
+ mount StaticRefToAPI
54
+ end
55
+ end
56
+
57
+ it 'can access the votes in both places' do
58
+ get '/posts/votes'
59
+ expect(last_response.body).to eql '10 votes'
60
+ get '/comments/votes'
61
+ expect(last_response.body).to eql '10 votes'
62
+ end
63
+ end
64
+ end
65
+
66
+ context 'with a dynamically configured route' do
67
+ before do
68
+ a_remounted_api.namespace 'api' do
69
+ get "/#{configuration[:path]}" do
70
+ '10 votes'
71
+ end
72
+ end
73
+ root_api.mount a_remounted_api, with: { path: 'votes' }
74
+ root_api.mount a_remounted_api, with: { path: 'scores' }
75
+ end
76
+
77
+ it 'will use the dynamic configuration on all routes' do
78
+ get 'api/votes'
79
+ expect(last_response.body).to eql '10 votes'
80
+ get 'api/scores'
81
+ expect(last_response.body).to eql '10 votes'
82
+ end
83
+ end
84
+ end
85
+ end
@@ -1348,6 +1348,28 @@ XML
1348
1348
  end
1349
1349
  end
1350
1350
 
1351
+ describe '.insert' do
1352
+ it 'inserts middleware in a specific location in the stack' do
1353
+ m = Class.new(Grape::Middleware::Base) do
1354
+ def call(env)
1355
+ env['phony.args'] ||= []
1356
+ env['phony.args'] << @options[:message]
1357
+ @app.call(env)
1358
+ end
1359
+ end
1360
+
1361
+ subject.use ApiSpec::PhonyMiddleware, 'bye'
1362
+ subject.insert 0, m, message: 'good'
1363
+ subject.insert 0, m, message: 'hello'
1364
+ subject.get '/' do
1365
+ env['phony.args'].join(' ')
1366
+ end
1367
+
1368
+ get '/'
1369
+ expect(last_response.body).to eql 'hello good bye'
1370
+ end
1371
+ end
1372
+
1351
1373
  describe '.http_basic' do
1352
1374
  it 'protects any resources on the same scope' do
1353
1375
  subject.http_basic do |u, _p|
@@ -1723,6 +1745,16 @@ XML
1723
1745
  expect(last_response.status).to eql 500
1724
1746
  expect(last_response.body).to eq('Formatter Error')
1725
1747
  end
1748
+
1749
+ it 'uses default_rescue_handler to handle invalid response from rescue_from' do
1750
+ subject.rescue_from(:all) { 'error' }
1751
+ subject.get('/') { raise }
1752
+
1753
+ expect_any_instance_of(Grape::Middleware::Error).to receive(:default_rescue_handler).and_call_original
1754
+ get '/'
1755
+ expect(last_response.status).to eql 500
1756
+ expect(last_response.body).to eql 'Invalid response'
1757
+ end
1726
1758
  end
1727
1759
 
1728
1760
  describe '.rescue_from klass, block' do
@@ -3177,6 +3209,43 @@ XML
3177
3209
  expect { a.mount b }.to_not raise_error
3178
3210
  end
3179
3211
  end
3212
+
3213
+ context 'when including a module' do
3214
+ let(:included_module) do
3215
+ Module.new do
3216
+ def self.included(base)
3217
+ base.extend(ClassMethods)
3218
+ end
3219
+ module ClassMethods
3220
+ def my_method
3221
+ @test = true
3222
+ end
3223
+ end
3224
+ end
3225
+ end
3226
+
3227
+ it 'should correctly include module in nested mount' do
3228
+ module_to_include = included_module
3229
+ v1 = Class.new(Grape::API) do
3230
+ version :v1, using: :path
3231
+ include module_to_include
3232
+ my_method
3233
+ end
3234
+ v2 = Class.new(Grape::API) do
3235
+ version :v2, using: :path
3236
+ end
3237
+ segment_base = Class.new(Grape::API) do
3238
+ mount v1
3239
+ mount v2
3240
+ end
3241
+
3242
+ Class.new(Grape::API) do
3243
+ mount segment_base
3244
+ end
3245
+
3246
+ expect(v1.my_method).to be_truthy
3247
+ end
3248
+ end
3180
3249
  end
3181
3250
  end
3182
3251
 
@@ -3192,7 +3261,7 @@ XML
3192
3261
  it 'sets the instance' do
3193
3262
  expect(subject.instance).to be_nil
3194
3263
  subject.compile
3195
- expect(subject.instance).to be_kind_of(subject)
3264
+ expect(subject.instance).to be_kind_of(subject.base_instance)
3196
3265
  end
3197
3266
  end
3198
3267