grape 1.1.0 → 1.2.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 (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