grape 1.1.0 → 1.2.5
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +128 -43
- data/LICENSE +1 -1
- data/README.md +394 -47
- data/UPGRADING.md +111 -0
- data/grape.gemspec +3 -1
- data/lib/grape.rb +98 -66
- data/lib/grape/api.rb +136 -175
- data/lib/grape/api/instance.rb +280 -0
- data/lib/grape/config.rb +32 -0
- data/lib/grape/dsl/callbacks.rb +20 -0
- data/lib/grape/dsl/desc.rb +39 -7
- data/lib/grape/dsl/inside_route.rb +12 -6
- data/lib/grape/dsl/middleware.rb +7 -0
- data/lib/grape/dsl/parameters.rb +9 -4
- data/lib/grape/dsl/routing.rb +5 -1
- data/lib/grape/dsl/validations.rb +4 -3
- data/lib/grape/eager_load.rb +18 -0
- data/lib/grape/endpoint.rb +42 -26
- data/lib/grape/error_formatter.rb +1 -1
- data/lib/grape/exceptions/base.rb +9 -1
- data/lib/grape/exceptions/invalid_response.rb +9 -0
- data/lib/grape/exceptions/validation_errors.rb +4 -2
- data/lib/grape/formatter.rb +1 -1
- data/lib/grape/locale/en.yml +2 -0
- data/lib/grape/middleware/auth/base.rb +2 -4
- data/lib/grape/middleware/base.rb +2 -0
- data/lib/grape/middleware/error.rb +9 -4
- data/lib/grape/middleware/helpers.rb +10 -0
- data/lib/grape/middleware/stack.rb +1 -1
- data/lib/grape/middleware/versioner/header.rb +4 -4
- data/lib/grape/parser.rb +1 -1
- data/lib/grape/request.rb +1 -1
- data/lib/grape/router/attribute_translator.rb +2 -0
- data/lib/grape/router/route.rb +2 -2
- data/lib/grape/util/base_inheritable.rb +34 -0
- data/lib/grape/util/endpoint_configuration.rb +6 -0
- data/lib/grape/util/inheritable_values.rb +5 -25
- data/lib/grape/util/lazy_block.rb +25 -0
- data/lib/grape/util/lazy_value.rb +95 -0
- data/lib/grape/util/reverse_stackable_values.rb +7 -36
- data/lib/grape/util/stackable_values.rb +19 -22
- data/lib/grape/validations/attributes_iterator.rb +5 -3
- data/lib/grape/validations/multiple_attributes_iterator.rb +11 -0
- data/lib/grape/validations/params_scope.rb +20 -14
- data/lib/grape/validations/single_attribute_iterator.rb +13 -0
- data/lib/grape/validations/types/custom_type_coercer.rb +1 -1
- data/lib/grape/validations/types/file.rb +1 -1
- data/lib/grape/validations/validator_factory.rb +6 -11
- data/lib/grape/validations/validators/all_or_none.rb +6 -13
- data/lib/grape/validations/validators/as.rb +2 -3
- data/lib/grape/validations/validators/at_least_one_of.rb +5 -13
- data/lib/grape/validations/validators/base.rb +11 -10
- data/lib/grape/validations/validators/coerce.rb +4 -0
- data/lib/grape/validations/validators/default.rb +1 -1
- data/lib/grape/validations/validators/exactly_one_of.rb +6 -23
- data/lib/grape/validations/validators/multiple_params_base.rb +14 -10
- data/lib/grape/validations/validators/mutual_exclusion.rb +6 -18
- data/lib/grape/validations/validators/same_as.rb +23 -0
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api/defines_boolean_in_params_spec.rb +37 -0
- data/spec/grape/api/routes_with_requirements_spec.rb +59 -0
- data/spec/grape/api_remount_spec.rb +466 -0
- data/spec/grape/api_spec.rb +379 -1
- data/spec/grape/config_spec.rb +17 -0
- data/spec/grape/dsl/desc_spec.rb +40 -16
- data/spec/grape/dsl/middleware_spec.rb +8 -0
- data/spec/grape/dsl/routing_spec.rb +10 -0
- data/spec/grape/endpoint_spec.rb +40 -4
- data/spec/grape/exceptions/base_spec.rb +65 -0
- data/spec/grape/exceptions/invalid_response_spec.rb +11 -0
- data/spec/grape/exceptions/validation_errors_spec.rb +6 -4
- data/spec/grape/integration/rack_spec.rb +22 -6
- data/spec/grape/middleware/auth/dsl_spec.rb +3 -3
- data/spec/grape/middleware/base_spec.rb +8 -0
- data/spec/grape/middleware/exception_spec.rb +1 -1
- data/spec/grape/middleware/formatter_spec.rb +15 -5
- data/spec/grape/middleware/versioner/header_spec.rb +6 -0
- data/spec/grape/named_api_spec.rb +19 -0
- data/spec/grape/request_spec.rb +24 -0
- data/spec/grape/validations/multiple_attributes_iterator_spec.rb +29 -0
- data/spec/grape/validations/params_scope_spec.rb +184 -8
- data/spec/grape/validations/single_attribute_iterator_spec.rb +33 -0
- data/spec/grape/validations/validators/all_or_none_spec.rb +138 -30
- data/spec/grape/validations/validators/at_least_one_of_spec.rb +173 -29
- data/spec/grape/validations/validators/coerce_spec.rb +10 -2
- data/spec/grape/validations/validators/exactly_one_of_spec.rb +202 -38
- data/spec/grape/validations/validators/mutual_exclusion_spec.rb +184 -27
- data/spec/grape/validations/validators/same_as_spec.rb +63 -0
- data/spec/grape/validations_spec.rb +33 -21
- data/spec/spec_helper.rb +4 -1
- metadata +35 -23
- data/Appraisals +0 -32
- data/Dangerfile +0 -2
- data/Gemfile +0 -33
- data/Gemfile.lock +0 -231
- data/Guardfile +0 -10
- data/RELEASING.md +0 -111
- data/Rakefile +0 -25
- data/benchmark/simple.rb +0 -27
- data/benchmark/simple_with_type_coercer.rb +0 -22
- data/gemfiles/multi_json.gemfile +0 -35
- data/gemfiles/multi_xml.gemfile +0 -35
- data/gemfiles/rack_1.5.2.gemfile +0 -35
- data/gemfiles/rack_edge.gemfile +0 -35
- data/gemfiles/rails_3.gemfile +0 -36
- data/gemfiles/rails_4.gemfile +0 -35
- data/gemfiles/rails_5.gemfile +0 -35
- data/gemfiles/rails_edge.gemfile +0 -35
- data/pkg/grape-0.17.0.gem +0 -0
- data/pkg/grape-0.19.0.gem +0 -0
@@ -158,7 +158,7 @@ module Grape
|
|
158
158
|
# necessary.
|
159
159
|
def enforce_symbolized_keys(type, method)
|
160
160
|
# Collections have all values processed individually
|
161
|
-
if
|
161
|
+
if [Array, Set].include?(type)
|
162
162
|
lambda do |val|
|
163
163
|
method.call(val).tap do |new_value|
|
164
164
|
new_value.map do |item|
|
@@ -19,7 +19,7 @@ module Grape
|
|
19
19
|
# Rack::Request creates a Hash with filename,
|
20
20
|
# content type and an IO object. Do a bit of basic
|
21
21
|
# duck-typing.
|
22
|
-
value.is_a?(::Hash) && value.key?(:tempfile)
|
22
|
+
value.is_a?(::Hash) && value.key?(:tempfile) && value[:tempfile].is_a?(Tempfile)
|
23
23
|
end
|
24
24
|
end
|
25
25
|
end
|
@@ -1,17 +1,12 @@
|
|
1
1
|
module Grape
|
2
2
|
module Validations
|
3
3
|
class ValidatorFactory
|
4
|
-
def
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
@validator_class.new(@options[:attributes],
|
11
|
-
@options[:options],
|
12
|
-
@options[:required],
|
13
|
-
@options[:params_scope],
|
14
|
-
@options[:opts])
|
4
|
+
def self.create_validator(**options)
|
5
|
+
options[:validator_class].new(options[:attributes],
|
6
|
+
options[:options],
|
7
|
+
options[:required],
|
8
|
+
options[:params_scope],
|
9
|
+
options[:opts])
|
15
10
|
end
|
16
11
|
end
|
17
12
|
end
|
@@ -1,19 +1,12 @@
|
|
1
|
+
require 'grape/validations/validators/multiple_params_base'
|
2
|
+
|
1
3
|
module Grape
|
2
4
|
module Validations
|
3
|
-
require 'grape/validations/validators/multiple_params_base'
|
4
5
|
class AllOrNoneOfValidator < MultipleParamsBase
|
5
|
-
def
|
6
|
-
|
7
|
-
if
|
8
|
-
|
9
|
-
end
|
10
|
-
params
|
11
|
-
end
|
12
|
-
|
13
|
-
private
|
14
|
-
|
15
|
-
def only_subset_present
|
16
|
-
scoped_params.any? { |resource_params| !keys_in_common(resource_params).empty? && keys_in_common(resource_params).length < attrs.length }
|
6
|
+
def validate_params!(params)
|
7
|
+
keys = keys_in_common(params)
|
8
|
+
return if keys.empty? || keys.length == all_keys.length
|
9
|
+
raise Grape::Exceptions::Validation, params: all_keys, message: message(:all_or_none)
|
17
10
|
end
|
18
11
|
end
|
19
12
|
end
|
@@ -2,13 +2,12 @@ module Grape
|
|
2
2
|
module Validations
|
3
3
|
class AsValidator < Base
|
4
4
|
def initialize(attrs, options, required, scope, opts = {})
|
5
|
-
@
|
5
|
+
@renamed_options = options
|
6
6
|
super
|
7
7
|
end
|
8
8
|
|
9
9
|
def validate_param!(attr_name, params)
|
10
|
-
params[@
|
11
|
-
params.delete(attr_name)
|
10
|
+
params[@renamed_options] = params[attr_name]
|
12
11
|
end
|
13
12
|
end
|
14
13
|
end
|
@@ -1,19 +1,11 @@
|
|
1
|
+
require 'grape/validations/validators/multiple_params_base'
|
2
|
+
|
1
3
|
module Grape
|
2
4
|
module Validations
|
3
|
-
require 'grape/validations/validators/multiple_params_base'
|
4
5
|
class AtLeastOneOfValidator < MultipleParamsBase
|
5
|
-
def
|
6
|
-
|
7
|
-
|
8
|
-
raise Grape::Exceptions::Validation, params: all_keys, message: message(:at_least_one)
|
9
|
-
end
|
10
|
-
params
|
11
|
-
end
|
12
|
-
|
13
|
-
private
|
14
|
-
|
15
|
-
def no_exclusive_params_are_present
|
16
|
-
scoped_params.any? { |resource_params| keys_in_common(resource_params).empty? }
|
6
|
+
def validate_params!(params)
|
7
|
+
return unless keys_in_common(params).empty?
|
8
|
+
raise Grape::Exceptions::Validation, params: all_keys, message: message(:at_least_one)
|
17
9
|
end
|
18
10
|
end
|
19
11
|
end
|
@@ -35,18 +35,19 @@ module Grape
|
|
35
35
|
# @raise [Grape::Exceptions::Validation] if validation failed
|
36
36
|
# @return [void]
|
37
37
|
def validate!(params)
|
38
|
-
attributes =
|
38
|
+
attributes = SingleAttributeIterator.new(self, @scope, params)
|
39
|
+
# we collect errors inside array because
|
40
|
+
# there may be more than one error per field
|
39
41
|
array_errors = []
|
42
|
+
|
40
43
|
attributes.each do |resource_params, attr_name|
|
41
44
|
next if !@scope.required? && resource_params.empty?
|
42
|
-
next unless @required || (resource_params.respond_to?(:key?) && resource_params.key?(attr_name))
|
43
45
|
next unless @scope.meets_dependency?(resource_params, params)
|
44
|
-
|
45
46
|
begin
|
46
|
-
|
47
|
+
if @required || resource_params.respond_to?(:key?) && resource_params.key?(attr_name)
|
48
|
+
validate_param!(attr_name, resource_params)
|
49
|
+
end
|
47
50
|
rescue Grape::Exceptions::Validation => e
|
48
|
-
# we collect errors inside array because
|
49
|
-
# there may be more than one error per field
|
50
51
|
array_errors << e
|
51
52
|
end
|
52
53
|
end
|
@@ -56,10 +57,10 @@ module Grape
|
|
56
57
|
|
57
58
|
def self.convert_to_short_name(klass)
|
58
59
|
ret = klass.name.gsub(/::/, '/')
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
60
|
+
ret.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
61
|
+
ret.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
62
|
+
ret.tr!('-', '_')
|
63
|
+
ret.downcase!
|
63
64
|
File.basename(ret, '_validator')
|
64
65
|
end
|
65
66
|
|
@@ -18,7 +18,7 @@ module Grape
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def validate!(params)
|
21
|
-
attrs =
|
21
|
+
attrs = SingleAttributeIterator.new(self, @scope, params)
|
22
22
|
attrs.each do |resource_params, attr_name|
|
23
23
|
if resource_params.is_a?(Hash) && resource_params[attr_name].nil?
|
24
24
|
validate_param!(attr_name, resource_params)
|
@@ -1,28 +1,11 @@
|
|
1
|
+
require 'grape/validations/validators/multiple_params_base'
|
2
|
+
|
1
3
|
module Grape
|
2
4
|
module Validations
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
if scope_requires_params && none_of_restricted_params_is_present
|
8
|
-
raise Grape::Exceptions::Validation, params: all_keys, message: message(:exactly_one)
|
9
|
-
end
|
10
|
-
params
|
11
|
-
end
|
12
|
-
|
13
|
-
def message(default_key = nil)
|
14
|
-
options = instance_variable_get(:@option)
|
15
|
-
if options_key?(:message)
|
16
|
-
(options_key?(default_key, options[:message]) ? options[:message][default_key] : options[:message])
|
17
|
-
else
|
18
|
-
default_key
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
private
|
23
|
-
|
24
|
-
def none_of_restricted_params_is_present
|
25
|
-
scoped_params.any? { |resource_params| keys_in_common(resource_params).empty? }
|
5
|
+
class ExactlyOneOfValidator < MultipleParamsBase
|
6
|
+
def validate_params!(params)
|
7
|
+
return if keys_in_common(params).length == 1
|
8
|
+
raise Grape::Exceptions::Validation, params: all_keys, message: message(:exactly_one)
|
26
9
|
end
|
27
10
|
end
|
28
11
|
end
|
@@ -1,26 +1,30 @@
|
|
1
1
|
module Grape
|
2
2
|
module Validations
|
3
3
|
class MultipleParamsBase < Base
|
4
|
-
attr_reader :scoped_params
|
5
|
-
|
6
4
|
def validate!(params)
|
7
|
-
|
8
|
-
|
9
|
-
end
|
5
|
+
attributes = MultipleAttributesIterator.new(self, @scope, params)
|
6
|
+
array_errors = []
|
10
7
|
|
11
|
-
|
8
|
+
attributes.each do |resource_params|
|
9
|
+
begin
|
10
|
+
validate_params!(resource_params)
|
11
|
+
rescue Grape::Exceptions::Validation => e
|
12
|
+
array_errors << e
|
13
|
+
end
|
14
|
+
end
|
12
15
|
|
13
|
-
|
14
|
-
@scope.required? || scoped_params.any?(&:any?)
|
16
|
+
raise Grape::Exceptions::ValidationArrayErrors, array_errors if array_errors.any?
|
15
17
|
end
|
16
18
|
|
19
|
+
private
|
20
|
+
|
17
21
|
def keys_in_common(resource_params)
|
18
22
|
return [] unless resource_params.is_a?(Hash)
|
19
|
-
|
23
|
+
all_keys & resource_params.keys.map! { |attr| @scope.full_name(attr) }
|
20
24
|
end
|
21
25
|
|
22
26
|
def all_keys
|
23
|
-
attrs.map(
|
27
|
+
attrs.map { |attr| @scope.full_name(attr) }
|
24
28
|
end
|
25
29
|
end
|
26
30
|
end
|
@@ -1,24 +1,12 @@
|
|
1
|
+
require 'grape/validations/validators/multiple_params_base'
|
2
|
+
|
1
3
|
module Grape
|
2
4
|
module Validations
|
3
|
-
require 'grape/validations/validators/multiple_params_base'
|
4
5
|
class MutualExclusionValidator < MultipleParamsBase
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
if two_or_more_exclusive_params_are_present
|
10
|
-
raise Grape::Exceptions::Validation, params: processing_keys_in_common, message: message(:mutual_exclusion)
|
11
|
-
end
|
12
|
-
params
|
13
|
-
end
|
14
|
-
|
15
|
-
private
|
16
|
-
|
17
|
-
def two_or_more_exclusive_params_are_present
|
18
|
-
scoped_params.any? do |resource_params|
|
19
|
-
@processing_keys_in_common = keys_in_common(resource_params)
|
20
|
-
@processing_keys_in_common.length > 1
|
21
|
-
end
|
6
|
+
def validate_params!(params)
|
7
|
+
keys = keys_in_common(params)
|
8
|
+
return if keys.length <= 1
|
9
|
+
raise Grape::Exceptions::Validation, params: keys, message: message(:mutual_exclusion)
|
22
10
|
end
|
23
11
|
end
|
24
12
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Grape
|
2
|
+
module Validations
|
3
|
+
class SameAsValidator < Base
|
4
|
+
def validate_param!(attr_name, params)
|
5
|
+
confirmation = options_key?(:value) ? @option[:value] : @option
|
6
|
+
return if params[attr_name] == params[confirmation]
|
7
|
+
raise Grape::Exceptions::Validation,
|
8
|
+
params: [@scope.full_name(attr_name)],
|
9
|
+
message: build_message
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def build_message
|
15
|
+
if options_key?(:message)
|
16
|
+
@option[:message]
|
17
|
+
else
|
18
|
+
format I18n.t(:same_as, scope: 'grape.errors.messages'), parameter: @option
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/grape/version.rb
CHANGED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Grape::API::Instance do
|
4
|
+
describe 'boolean constant' do
|
5
|
+
module DefinesBooleanInstanceSpec
|
6
|
+
class API < Grape::API
|
7
|
+
params do
|
8
|
+
requires :message, type: Boolean
|
9
|
+
end
|
10
|
+
post :echo do
|
11
|
+
{ class: params[:message].class.name, value: params[:message] }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def app
|
17
|
+
DefinesBooleanInstanceSpec::API
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:expected_body) do
|
21
|
+
{ class: 'TrueClass', value: true }.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'sets Boolean as a Virtus::Attribute::Boolean' do
|
25
|
+
post '/echo?message=true'
|
26
|
+
expect(last_response.status).to eq(201)
|
27
|
+
expect(last_response.body).to eq expected_body
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'Params endpoint type' do
|
31
|
+
subject { DefinesBooleanInstanceSpec::API.new.router.map['POST'].first.options[:params]['message'][:type] }
|
32
|
+
it 'params type is a Virtus::Attribute::Boolean' do
|
33
|
+
is_expected.to eq 'Virtus::Attribute::Boolean'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -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,466 @@
|
|
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
|
+
describe 'with dynamic configuration' do
|
67
|
+
context 'when mounting an endpoint conditional on a configuration' do
|
68
|
+
subject(:a_remounted_api) do
|
69
|
+
Class.new(Grape::API) do
|
70
|
+
get 'always' do
|
71
|
+
'success'
|
72
|
+
end
|
73
|
+
|
74
|
+
given configuration[:mount_sometimes] do
|
75
|
+
get 'sometimes' do
|
76
|
+
'sometimes'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'mounts the endpoints only when configured to do so' do
|
83
|
+
root_api.mount({ a_remounted_api => 'with_conditional' }, with: { mount_sometimes: true })
|
84
|
+
root_api.mount({ a_remounted_api => 'without_conditional' }, with: { mount_sometimes: false })
|
85
|
+
|
86
|
+
get '/with_conditional/always'
|
87
|
+
expect(last_response.body).to eq 'success'
|
88
|
+
|
89
|
+
get '/with_conditional/sometimes'
|
90
|
+
expect(last_response.body).to eq 'sometimes'
|
91
|
+
|
92
|
+
get '/without_conditional/always'
|
93
|
+
expect(last_response.body).to eq 'success'
|
94
|
+
|
95
|
+
get '/without_conditional/sometimes'
|
96
|
+
expect(last_response.status).to eq 404
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context 'when using an expression derived from a configuration' do
|
101
|
+
subject(:a_remounted_api) do
|
102
|
+
Class.new(Grape::API) do
|
103
|
+
get(mounted { "api_name_#{configuration[:api_name]}" }) do
|
104
|
+
'success'
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
before do
|
110
|
+
root_api.mount a_remounted_api, with: {
|
111
|
+
api_name: 'a_name'
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'mounts the endpoint with the name' do
|
116
|
+
get 'api_name_a_name'
|
117
|
+
expect(last_response.body).to eq 'success'
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'does not mount the endpoint with a null name' do
|
121
|
+
get 'api_name_'
|
122
|
+
expect(last_response.body).not_to eq 'success'
|
123
|
+
end
|
124
|
+
|
125
|
+
context 'when the expression lives in a namespace' do
|
126
|
+
subject(:a_remounted_api) do
|
127
|
+
Class.new(Grape::API) do
|
128
|
+
namespace :base do
|
129
|
+
get(mounted { "api_name_#{configuration[:api_name]}" }) do
|
130
|
+
'success'
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'mounts the endpoint with the name' do
|
137
|
+
get 'base/api_name_a_name'
|
138
|
+
expect(last_response.body).to eq 'success'
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'does not mount the endpoint with a null name' do
|
142
|
+
get 'base/api_name_'
|
143
|
+
expect(last_response.body).not_to eq 'success'
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context 'when executing a standard block within a `mounted` block with all dynamic params' do
|
149
|
+
subject(:a_remounted_api) do
|
150
|
+
Class.new(Grape::API) do
|
151
|
+
mounted do
|
152
|
+
desc configuration[:description] do
|
153
|
+
headers configuration[:headers]
|
154
|
+
end
|
155
|
+
get configuration[:endpoint] do
|
156
|
+
configuration[:response]
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
let(:api_endpoint) { 'custom_endpoint' }
|
163
|
+
let(:api_response) { 'custom response' }
|
164
|
+
let(:endpoint_description) { 'this is a custom API' }
|
165
|
+
let(:headers) do
|
166
|
+
{
|
167
|
+
'XAuthToken' => {
|
168
|
+
'description' => 'Validates your identity',
|
169
|
+
'required' => true
|
170
|
+
}
|
171
|
+
}
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'mounts the API and obtains the description and headers definition' do
|
175
|
+
root_api.mount a_remounted_api, with: {
|
176
|
+
description: endpoint_description,
|
177
|
+
headers: headers,
|
178
|
+
endpoint: api_endpoint,
|
179
|
+
response: api_response
|
180
|
+
}
|
181
|
+
get api_endpoint
|
182
|
+
expect(last_response.body).to eq api_response
|
183
|
+
expect(a_remounted_api.instances.last.endpoints.first.options[:route_options][:description])
|
184
|
+
.to eq endpoint_description
|
185
|
+
expect(a_remounted_api.instances.last.endpoints.first.options[:route_options][:headers])
|
186
|
+
.to eq headers
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
context 'when executing a custom block on mount' do
|
191
|
+
subject(:a_remounted_api) do
|
192
|
+
Class.new(Grape::API) do
|
193
|
+
get 'always' do
|
194
|
+
'success'
|
195
|
+
end
|
196
|
+
|
197
|
+
mounted do
|
198
|
+
configuration[:endpoints].each do |endpoint_name, endpoint_response|
|
199
|
+
get endpoint_name do
|
200
|
+
endpoint_response
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
it 'mounts the endpoints only when configured to do so' do
|
208
|
+
root_api.mount a_remounted_api, with: { endpoints: { 'api_name' => 'api_response' } }
|
209
|
+
get 'api_name'
|
210
|
+
expect(last_response.body).to eq 'api_response'
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
context 'when the configuration is part of the arguments of a method' do
|
215
|
+
subject(:a_remounted_api) do
|
216
|
+
Class.new(Grape::API) do
|
217
|
+
get configuration[:endpoint_name] do
|
218
|
+
'success'
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
it 'mounts the endpoint in the location it is configured' do
|
224
|
+
root_api.mount a_remounted_api, with: { endpoint_name: 'some_location' }
|
225
|
+
get '/some_location'
|
226
|
+
expect(last_response.body).to eq 'success'
|
227
|
+
|
228
|
+
get '/different_location'
|
229
|
+
expect(last_response.status).to eq 404
|
230
|
+
|
231
|
+
root_api.mount a_remounted_api, with: { endpoint_name: 'new_location' }
|
232
|
+
get '/new_location'
|
233
|
+
expect(last_response.body).to eq 'success'
|
234
|
+
end
|
235
|
+
|
236
|
+
context 'when the configuration is the value in a key-arg pair' do
|
237
|
+
subject(:a_remounted_api) do
|
238
|
+
Class.new(Grape::API) do
|
239
|
+
version 'v1', using: :param, parameter: configuration[:version_param]
|
240
|
+
get 'endpoint' do
|
241
|
+
'version 1'
|
242
|
+
end
|
243
|
+
|
244
|
+
version 'v2', using: :param, parameter: configuration[:version_param]
|
245
|
+
get 'endpoint' do
|
246
|
+
'version 2'
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
it 'takes the param from the configuration' do
|
252
|
+
root_api.mount a_remounted_api, with: { version_param: 'param_name' }
|
253
|
+
|
254
|
+
get '/endpoint?param_name=v1'
|
255
|
+
expect(last_response.body).to eq 'version 1'
|
256
|
+
|
257
|
+
get '/endpoint?param_name=v2'
|
258
|
+
expect(last_response.body).to eq 'version 2'
|
259
|
+
|
260
|
+
get '/endpoint?wrong_param_name=v2'
|
261
|
+
expect(last_response.body).to eq 'version 1'
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
context 'on the DescSCope' do
|
267
|
+
subject(:a_remounted_api) do
|
268
|
+
Class.new(Grape::API) do
|
269
|
+
desc 'The description of this' do
|
270
|
+
tags ['not_configurable_tag', configuration[:a_configurable_tag]]
|
271
|
+
end
|
272
|
+
get 'location' do
|
273
|
+
'success'
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
it 'mounts the endpoint with the appropiate tags' do
|
279
|
+
root_api.mount({ a_remounted_api => 'integer' }, with: { a_configurable_tag: 'a configured tag' })
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
context 'on the ParamScope' do
|
284
|
+
subject(:a_remounted_api) do
|
285
|
+
Class.new(Grape::API) do
|
286
|
+
params do
|
287
|
+
requires configuration[:required_param], type: configuration[:required_type]
|
288
|
+
end
|
289
|
+
|
290
|
+
get 'location' do
|
291
|
+
'success'
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
it 'mounts the endpoint in the location it is configured' do
|
297
|
+
root_api.mount({ a_remounted_api => 'string' }, with: { required_param: 'param_key', required_type: String })
|
298
|
+
root_api.mount({ a_remounted_api => 'integer' }, with: { required_param: 'param_integer', required_type: Integer })
|
299
|
+
|
300
|
+
get '/string/location', param_key: 'a'
|
301
|
+
expect(last_response.body).to eq 'success'
|
302
|
+
|
303
|
+
get '/string/location', param_integer: 1
|
304
|
+
expect(last_response.status).to eq 400
|
305
|
+
|
306
|
+
get '/integer/location', param_integer: 1
|
307
|
+
expect(last_response.body).to eq 'success'
|
308
|
+
|
309
|
+
get '/integer/location', param_integer: 'a'
|
310
|
+
expect(last_response.status).to eq 400
|
311
|
+
end
|
312
|
+
|
313
|
+
context 'on dynamic checks' do
|
314
|
+
subject(:a_remounted_api) do
|
315
|
+
Class.new(Grape::API) do
|
316
|
+
params do
|
317
|
+
optional :restricted_values, values: -> { [configuration[:allowed_value], 'always'] }
|
318
|
+
end
|
319
|
+
|
320
|
+
get 'location' do
|
321
|
+
'success'
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
it 'can read the configuration on lambdas' do
|
327
|
+
root_api.mount a_remounted_api, with: { allowed_value: 'sometimes' }
|
328
|
+
get '/location', restricted_values: 'always'
|
329
|
+
expect(last_response.body).to eq 'success'
|
330
|
+
get '/location', restricted_values: 'sometimes'
|
331
|
+
expect(last_response.body).to eq 'success'
|
332
|
+
get '/location', restricted_values: 'never'
|
333
|
+
expect(last_response.status).to eq 400
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
context 'when the configuration is read within a namespace' do
|
339
|
+
before do
|
340
|
+
a_remounted_api.namespace 'api' do
|
341
|
+
get "/#{configuration[:path]}" do
|
342
|
+
'10 votes'
|
343
|
+
end
|
344
|
+
end
|
345
|
+
root_api.mount a_remounted_api, with: { path: 'votes' }
|
346
|
+
root_api.mount a_remounted_api, with: { path: 'scores' }
|
347
|
+
end
|
348
|
+
|
349
|
+
it 'will use the dynamic configuration on all routes' do
|
350
|
+
get 'api/votes'
|
351
|
+
expect(last_response.body).to eql '10 votes'
|
352
|
+
get 'api/scores'
|
353
|
+
expect(last_response.body).to eql '10 votes'
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
context 'a very complex configuration example' do
|
358
|
+
before do
|
359
|
+
top_level_api = Class.new(Grape::API) do
|
360
|
+
remounted_api = Class.new(Grape::API) do
|
361
|
+
get configuration[:endpoint_name] do
|
362
|
+
configuration[:response]
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
expression_namespace = mounted { configuration[:namespace].to_s * 2 }
|
367
|
+
given(mounted { configuration[:should_mount_expressed] != false }) do
|
368
|
+
namespace expression_namespace do
|
369
|
+
mount remounted_api, with: { endpoint_name: configuration[:endpoint_name], response: configuration[:endpoint_response] }
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
root_api.mount top_level_api, with: configuration_options
|
374
|
+
end
|
375
|
+
|
376
|
+
context 'when the namespace should be mounted' do
|
377
|
+
let(:configuration_options) do
|
378
|
+
{
|
379
|
+
should_mount_expressed: true,
|
380
|
+
namespace: 'bang',
|
381
|
+
endpoint_name: 'james',
|
382
|
+
endpoint_response: 'bond'
|
383
|
+
}
|
384
|
+
end
|
385
|
+
|
386
|
+
it 'gets a response' do
|
387
|
+
get 'bangbang/james'
|
388
|
+
expect(last_response.body).to eq 'bond'
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
context 'when should be mounted is nil' do
|
393
|
+
let(:configuration_options) do
|
394
|
+
{
|
395
|
+
should_mount_expressed: nil,
|
396
|
+
namespace: 'bang',
|
397
|
+
endpoint_name: 'james',
|
398
|
+
endpoint_response: 'bond'
|
399
|
+
}
|
400
|
+
end
|
401
|
+
|
402
|
+
it 'gets a response' do
|
403
|
+
get 'bangbang/james'
|
404
|
+
expect(last_response.body).to eq 'bond'
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
context 'when it should not be mounted' do
|
409
|
+
let(:configuration_options) do
|
410
|
+
{
|
411
|
+
should_mount_expressed: false,
|
412
|
+
namespace: 'bang',
|
413
|
+
endpoint_name: 'james',
|
414
|
+
endpoint_response: 'bond'
|
415
|
+
}
|
416
|
+
end
|
417
|
+
|
418
|
+
it 'gets a response' do
|
419
|
+
get 'bangbang/james'
|
420
|
+
expect(last_response.body).not_to eq 'bond'
|
421
|
+
end
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
context 'when the configuration is read in a helper' do
|
426
|
+
subject(:a_remounted_api) do
|
427
|
+
Class.new(Grape::API) do
|
428
|
+
helpers do
|
429
|
+
def printed_response
|
430
|
+
configuration[:some_value]
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
get 'location' do
|
435
|
+
printed_response
|
436
|
+
end
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
it 'will use the dynamic configuration on all routes' do
|
441
|
+
root_api.mount(a_remounted_api, with: { some_value: 'response value' })
|
442
|
+
|
443
|
+
get '/location'
|
444
|
+
expect(last_response.body).to eq 'response value'
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
context 'when the configuration is read within the response block' do
|
449
|
+
subject(:a_remounted_api) do
|
450
|
+
Class.new(Grape::API) do
|
451
|
+
get 'location' do
|
452
|
+
configuration[:some_value]
|
453
|
+
end
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
it 'will use the dynamic configuration on all routes' do
|
458
|
+
root_api.mount(a_remounted_api, with: { some_value: 'response value' })
|
459
|
+
|
460
|
+
get '/location'
|
461
|
+
expect(last_response.body).to eq 'response value'
|
462
|
+
end
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end
|
466
|
+
end
|