grape 0.12.0 → 0.13.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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +2 -2
- data/CHANGELOG.md +237 -215
- data/CONTRIBUTING.md +4 -4
- data/README.md +133 -10
- data/RELEASING.md +14 -6
- data/Rakefile +1 -1
- data/UPGRADING.md +23 -23
- data/grape.gemspec +1 -3
- data/lib/grape/api.rb +24 -4
- data/lib/grape/dsl/callbacks.rb +20 -0
- data/lib/grape/dsl/configuration.rb +54 -0
- data/lib/grape/dsl/inside_route.rb +33 -1
- data/lib/grape/dsl/parameters.rb +80 -0
- data/lib/grape/dsl/routing.rb +14 -0
- data/lib/grape/dsl/settings.rb +36 -1
- data/lib/grape/dsl/validations.rb +7 -5
- data/lib/grape/endpoint.rb +42 -32
- data/lib/grape/exceptions/unknown_parameter.rb +10 -0
- data/lib/grape/exceptions/validation_errors.rb +4 -3
- data/lib/grape/http/headers.rb +0 -1
- data/lib/grape/http/request.rb +12 -4
- data/lib/grape/locale/en.yml +1 -0
- data/lib/grape/middleware/base.rb +1 -0
- data/lib/grape/middleware/formatter.rb +39 -23
- data/lib/grape/namespace.rb +13 -2
- data/lib/grape/path.rb +1 -0
- data/lib/grape/route.rb +5 -0
- data/lib/grape/util/file_response.rb +21 -0
- data/lib/grape/util/inheritable_setting.rb +23 -2
- data/lib/grape/util/inheritable_values.rb +1 -1
- data/lib/grape/util/parameter_types.rb +58 -0
- data/lib/grape/util/stackable_values.rb +5 -2
- data/lib/grape/validations/params_scope.rb +83 -9
- data/lib/grape/validations/validators/coerce.rb +11 -2
- data/lib/grape/validations.rb +5 -0
- data/lib/grape/version.rb +2 -1
- data/lib/grape.rb +7 -8
- data/spec/grape/api_spec.rb +63 -0
- data/spec/grape/dsl/inside_route_spec.rb +37 -2
- data/spec/grape/dsl/validations_spec.rb +18 -0
- data/spec/grape/endpoint_spec.rb +83 -0
- data/spec/grape/exceptions/validation_errors_spec.rb +28 -0
- data/spec/grape/middleware/base_spec.rb +33 -11
- data/spec/grape/middleware/formatter_spec.rb +0 -5
- data/spec/grape/util/inheritable_values_spec.rb +14 -0
- data/spec/grape/util/parameter_types_spec.rb +54 -0
- data/spec/grape/util/stackable_values_spec.rb +10 -0
- data/spec/grape/validations/params_scope_spec.rb +84 -0
- data/spec/grape/validations/validators/coerce_spec.rb +29 -8
- data/spec/grape/validations/validators/values_spec.rb +12 -0
- metadata +9 -6
- data/lib/backports/active_support/deep_dup.rb +0 -49
- data/lib/backports/active_support/duplicable.rb +0 -88
@@ -11,13 +11,6 @@ module Grape
|
|
11
11
|
}
|
12
12
|
end
|
13
13
|
|
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
14
|
def before
|
22
15
|
negotiate_content_type
|
23
16
|
read_body_input
|
@@ -25,26 +18,49 @@ module Grape
|
|
25
18
|
|
26
19
|
def after
|
27
20
|
status, headers, bodies = *@app_response
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
21
|
+
|
22
|
+
if bodies.is_a?(Grape::Util::FileResponse)
|
23
|
+
headers = ensure_content_type(headers)
|
24
|
+
|
25
|
+
response =
|
26
|
+
Rack::Response.new([], status, headers) do |resp|
|
27
|
+
resp.body = bodies.file
|
28
|
+
end
|
29
|
+
else
|
30
|
+
# Allow content-type to be explicitly overwritten
|
31
|
+
api_format = mime_types[headers[Grape::Http::Headers::CONTENT_TYPE]] || env['api.format']
|
32
|
+
formatter = Grape::Formatter::Base.formatter_for(api_format, options)
|
33
|
+
|
34
|
+
begin
|
35
|
+
bodymap = bodies.collect do |body|
|
36
|
+
formatter.call(body, env)
|
37
|
+
end
|
38
|
+
|
39
|
+
headers = ensure_content_type(headers)
|
40
|
+
|
41
|
+
response = Rack::Response.new(bodymap, status, headers)
|
42
|
+
rescue Grape::Exceptions::InvalidFormatter => e
|
43
|
+
throw :error, status: 500, message: e.message
|
44
|
+
end
|
41
45
|
end
|
42
|
-
|
43
|
-
|
46
|
+
|
47
|
+
response
|
44
48
|
end
|
45
49
|
|
46
50
|
private
|
47
51
|
|
52
|
+
# Set the content type header for the API format if it is not already present.
|
53
|
+
#
|
54
|
+
# @param headers [Hash]
|
55
|
+
# @return [Hash]
|
56
|
+
def ensure_content_type(headers)
|
57
|
+
if headers[Grape::Http::Headers::CONTENT_TYPE]
|
58
|
+
headers
|
59
|
+
else
|
60
|
+
headers.merge(Grape::Http::Headers::CONTENT_TYPE => content_type_for(env['api.format']))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
48
64
|
def request
|
49
65
|
@request ||= Rack::Request.new(env)
|
50
66
|
end
|
@@ -133,7 +149,7 @@ module Grape
|
|
133
149
|
end
|
134
150
|
|
135
151
|
def mime_array
|
136
|
-
accept =
|
152
|
+
accept = env[Grape::Http::Headers::HTTP_ACCEPT]
|
137
153
|
return [] unless accept
|
138
154
|
|
139
155
|
accept_into_mime_and_quality = %r{
|
data/lib/grape/namespace.rb
CHANGED
@@ -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
|
-
#
|
6
|
-
#
|
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
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
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Grape
|
2
|
+
module Util
|
3
|
+
# A simple class used to identify responses which represent files and do not
|
4
|
+
# need to be formatted or pre-read by Rack::Response
|
5
|
+
class FileResponse
|
6
|
+
attr_reader :file
|
7
|
+
|
8
|
+
# @param file [Object]
|
9
|
+
def initialize(file)
|
10
|
+
@file = file
|
11
|
+
end
|
12
|
+
|
13
|
+
# Equality provided mostly for tests.
|
14
|
+
#
|
15
|
+
# @return [Boolean]
|
16
|
+
def ==(other)
|
17
|
+
file == other.file
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -1,22 +1,31 @@
|
|
1
1
|
module Grape
|
2
2
|
module Util
|
3
|
+
# A branchable, inheritable settings object which can store both stackable
|
4
|
+
# and inheritable values (see InheritableValues and StackableValues).
|
3
5
|
class InheritableSetting
|
4
6
|
attr_accessor :route, :api_class, :namespace, :namespace_inheritable, :namespace_stackable
|
5
7
|
attr_accessor :parent, :point_in_time_copies
|
6
8
|
|
9
|
+
# Retrieve global settings.
|
7
10
|
def self.global
|
8
11
|
@global ||= {}
|
9
12
|
end
|
10
13
|
|
11
|
-
|
14
|
+
# Clear all global settings.
|
15
|
+
# @api private
|
16
|
+
# @note only for testing
|
17
|
+
def self.reset_global!
|
12
18
|
@global = {}
|
13
19
|
end
|
14
20
|
|
21
|
+
# Instantiate a new settings instance, with blank values. The fresh
|
22
|
+
# instance can then be set to inherit from an existing instance (see
|
23
|
+
# #inherit_from).
|
15
24
|
def initialize
|
16
25
|
self.route = {}
|
17
26
|
self.api_class = {}
|
18
27
|
self.namespace = InheritableValues.new # only inheritable from a parent when
|
19
|
-
# used with a mount, or should every API::Class be a
|
28
|
+
# used with a mount, or should every API::Class be a separate namespace by default?
|
20
29
|
self.namespace_inheritable = InheritableValues.new
|
21
30
|
self.namespace_stackable = StackableValues.new
|
22
31
|
|
@@ -25,10 +34,15 @@ module Grape
|
|
25
34
|
self.parent = nil
|
26
35
|
end
|
27
36
|
|
37
|
+
# Return the class-level global properties.
|
28
38
|
def global
|
29
39
|
self.class.global
|
30
40
|
end
|
31
41
|
|
42
|
+
# Set our inherited values to the given parent's current values. Also,
|
43
|
+
# update the inherited values on any settings instances which were forked
|
44
|
+
# from us.
|
45
|
+
# @param parent [InheritableSetting]
|
32
46
|
def inherit_from(parent)
|
33
47
|
return if parent.nil?
|
34
48
|
|
@@ -41,6 +55,10 @@ module Grape
|
|
41
55
|
point_in_time_copies.map { |cloned_one| cloned_one.inherit_from parent }
|
42
56
|
end
|
43
57
|
|
58
|
+
# Create a point-in-time copy of this settings instance, with clones of
|
59
|
+
# all our values. Note that, should this instance's parent be set or
|
60
|
+
# changed via #inherit_from, it will copy that inheritence to any copies
|
61
|
+
# which were made.
|
44
62
|
def point_in_time_copy
|
45
63
|
self.class.new.tap do |new_setting|
|
46
64
|
point_in_time_copies << new_setting
|
@@ -56,10 +74,13 @@ module Grape
|
|
56
74
|
end
|
57
75
|
end
|
58
76
|
|
77
|
+
# Resets the instance store of per-route settings.
|
78
|
+
# @api private
|
59
79
|
def route_end
|
60
80
|
@route = {}
|
61
81
|
end
|
62
82
|
|
83
|
+
# Return a serializable hash of our values.
|
63
84
|
def to_hash
|
64
85
|
{
|
65
86
|
global: global.clone,
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Grape
|
2
|
+
module ParameterTypes
|
3
|
+
# Types representing a single value, which are coerced through Virtus
|
4
|
+
# or special logic in Grape.
|
5
|
+
PRIMITIVES = [
|
6
|
+
# Numerical
|
7
|
+
Integer,
|
8
|
+
Float,
|
9
|
+
BigDecimal,
|
10
|
+
Numeric,
|
11
|
+
|
12
|
+
# Date/time
|
13
|
+
Date,
|
14
|
+
DateTime,
|
15
|
+
Time,
|
16
|
+
|
17
|
+
# Misc
|
18
|
+
Virtus::Attribute::Boolean,
|
19
|
+
String,
|
20
|
+
Symbol,
|
21
|
+
Rack::Multipart::UploadedFile
|
22
|
+
]
|
23
|
+
|
24
|
+
# Types representing data structures.
|
25
|
+
STRUCTURES = [
|
26
|
+
Hash,
|
27
|
+
Array,
|
28
|
+
Set
|
29
|
+
]
|
30
|
+
|
31
|
+
# @param type [Class] type to check
|
32
|
+
# @return [Boolean] whether or not the type is known by Grape as a valid
|
33
|
+
# type for a single value
|
34
|
+
def self.primitive?(type)
|
35
|
+
PRIMITIVES.include?(type)
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param type [Class] type to check
|
39
|
+
# @return [Boolean] whether or not the type is known by Grape as a valid
|
40
|
+
# data structure type
|
41
|
+
# @note This method does not yet consider 'complex types', which inherit
|
42
|
+
# Virtus.model.
|
43
|
+
def self.structure?(type)
|
44
|
+
STRUCTURES.include?(type)
|
45
|
+
end
|
46
|
+
|
47
|
+
# A valid custom type must implement a class-level `parse` method, taking
|
48
|
+
# one String argument and returning the parsed value in its correct type.
|
49
|
+
# @param type [Class] type to check
|
50
|
+
# @return [Boolean] whether or not the type can be used as a custom type
|
51
|
+
def self.custom_type?(type)
|
52
|
+
!primitive?(type) &&
|
53
|
+
!structure?(type) &&
|
54
|
+
type.respond_to?(:parse) &&
|
55
|
+
type.method(:parse).arity == 1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -13,7 +13,10 @@ module Grape
|
|
13
13
|
|
14
14
|
def [](name)
|
15
15
|
return @froozen_values[name] if @froozen_values.key? name
|
16
|
-
[@inherited_values[name], @new_values[name]]
|
16
|
+
value = [@inherited_values[name], @new_values[name]]
|
17
|
+
value.compact!
|
18
|
+
value.flatten!(1)
|
19
|
+
value
|
17
20
|
end
|
18
21
|
|
19
22
|
def []=(name, value)
|
@@ -45,7 +48,7 @@ module Grape
|
|
45
48
|
def initialize_copy(other)
|
46
49
|
super
|
47
50
|
self.inherited_values = other.inherited_values
|
48
|
-
self.new_values = other.new_values.
|
51
|
+
self.new_values = other.new_values.dup
|
49
52
|
end
|
50
53
|
end
|
51
54
|
end
|
@@ -5,12 +5,26 @@ module Grape
|
|
5
5
|
|
6
6
|
include Grape::DSL::Parameters
|
7
7
|
|
8
|
+
# Open up a new ParamsScope, allowing parameter definitions per
|
9
|
+
# Grape::DSL::Params.
|
10
|
+
# @param opts [Hash] options for this scope
|
11
|
+
# @option opts :element [Symbol] the element that contains this scope; for
|
12
|
+
# this to be relevant, @parent must be set
|
13
|
+
# @option opts :parent [ParamsScope] the scope containing this scope
|
14
|
+
# @option opts :api [API] the API endpoint to modify
|
15
|
+
# @option opts :optional [Boolean] whether or not this scope needs to have
|
16
|
+
# any parameters set or not
|
17
|
+
# @option opts :type [Class] a type meant to govern this scope (deprecated)
|
18
|
+
# @option opts :dependent_on [Symbol] if present, this scope should only
|
19
|
+
# validate if this param is present in the parent scope
|
20
|
+
# @yield the instance context, open for parameter definitions
|
8
21
|
def initialize(opts, &block)
|
9
|
-
@element
|
10
|
-
@parent
|
11
|
-
@api
|
12
|
-
@optional
|
13
|
-
@type
|
22
|
+
@element = opts[:element]
|
23
|
+
@parent = opts[:parent]
|
24
|
+
@api = opts[:api]
|
25
|
+
@optional = opts[:optional] || false
|
26
|
+
@type = opts[:type]
|
27
|
+
@dependent_on = opts[:dependent_on]
|
14
28
|
@declared_params = []
|
15
29
|
|
16
30
|
instance_eval(&block) if block_given?
|
@@ -18,27 +32,59 @@ module Grape
|
|
18
32
|
configure_declared_params
|
19
33
|
end
|
20
34
|
|
35
|
+
# @return [Boolean] whether or not this entire scope needs to be
|
36
|
+
# validated
|
21
37
|
def should_validate?(parameters)
|
22
38
|
return false if @optional && params(parameters).respond_to?(:all?) && params(parameters).all?(&:blank?)
|
39
|
+
return false if @dependent_on && params(parameters).try(:[], @dependent_on).blank?
|
23
40
|
return true if parent.nil?
|
24
41
|
parent.should_validate?(parameters)
|
25
42
|
end
|
26
43
|
|
44
|
+
# @return [String] the proper attribute name, with nesting considered.
|
27
45
|
def full_name(name)
|
28
|
-
|
29
|
-
|
46
|
+
case
|
47
|
+
when nested?
|
48
|
+
# Find our containing element's name, and append ours.
|
49
|
+
"#{@parent.full_name(@element)}[#{name}]"
|
50
|
+
when lateral?
|
51
|
+
# Find the name of the element as if it was at the
|
52
|
+
# same nesting level as our parent.
|
53
|
+
@parent.full_name(name)
|
54
|
+
else
|
55
|
+
# We must be the root scope, so no prefix needed.
|
56
|
+
name.to_s
|
57
|
+
end
|
30
58
|
end
|
31
59
|
|
60
|
+
# @return [Boolean] whether or not this scope is the root-level scope
|
32
61
|
def root?
|
33
62
|
!@parent
|
34
63
|
end
|
35
64
|
|
65
|
+
# A nested scope is contained in one of its parent's elements.
|
66
|
+
# @return [Boolean] whether or not this scope is nested
|
67
|
+
def nested?
|
68
|
+
@parent && @element
|
69
|
+
end
|
70
|
+
|
71
|
+
# A lateral scope is subordinate to its parent, but its keys are at the
|
72
|
+
# same level as its parent and thus is not contained within an element.
|
73
|
+
# @return [Boolean] whether or not this scope is lateral
|
74
|
+
def lateral?
|
75
|
+
@parent && !@element
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [Boolean] whether or not this scope needs to be present, or can
|
79
|
+
# be blank
|
36
80
|
def required?
|
37
81
|
!@optional
|
38
82
|
end
|
39
83
|
|
40
84
|
protected
|
41
85
|
|
86
|
+
# Adds a parameter declaration to our list of validations.
|
87
|
+
# @param attrs [Array] (see Grape::DSL::Parameters#requires)
|
42
88
|
def push_declared_params(attrs)
|
43
89
|
@declared_params.concat attrs
|
44
90
|
end
|
@@ -79,6 +125,13 @@ module Grape
|
|
79
125
|
validates(attrs, validations)
|
80
126
|
end
|
81
127
|
|
128
|
+
# Returns a new parameter scope, subordinate to the current one and nested
|
129
|
+
# under the parameter corresponding to `attrs.first`.
|
130
|
+
# @param attrs [Array] the attributes passed to the `requires` or
|
131
|
+
# `optional` invocation that opened this scope.
|
132
|
+
# @param optional [Boolean] whether the parameter this are nested under
|
133
|
+
# is optional or not (and hence, whether this block's params will be).
|
134
|
+
# @yield parameter scope
|
82
135
|
def new_scope(attrs, optional = false, &block)
|
83
136
|
# if required params are grouped and no type or unsupported type is provided, raise an error
|
84
137
|
type = attrs[1] ? attrs[1][:type] : nil
|
@@ -88,12 +141,30 @@ module Grape
|
|
88
141
|
end
|
89
142
|
|
90
143
|
opts = attrs[1] || { type: Array }
|
91
|
-
|
144
|
+
self.class.new(api: @api, element: attrs.first, parent: self, optional: optional, type: opts[:type], &block)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Returns a new parameter scope, not nested under any current-level param
|
148
|
+
# but instead at the same level as the current scope.
|
149
|
+
# @param options [Hash] options to control how this new scope behaves
|
150
|
+
# @option options :dependent_on [Symbol] if given, specifies that this
|
151
|
+
# scope should only validate if this parameter from the above scope is
|
152
|
+
# present
|
153
|
+
# @yield parameter scope
|
154
|
+
def new_lateral_scope(options, &block)
|
155
|
+
self.class.new(
|
156
|
+
api: @api,
|
157
|
+
element: nil,
|
158
|
+
parent: self,
|
159
|
+
options: @optional,
|
160
|
+
type: Hash,
|
161
|
+
dependent_on: options[:dependent_on],
|
162
|
+
&block)
|
92
163
|
end
|
93
164
|
|
94
165
|
# Pushes declared params to parent or settings
|
95
166
|
def configure_declared_params
|
96
|
-
if
|
167
|
+
if nested?
|
97
168
|
@parent.push_declared_params [element => @declared_params]
|
98
169
|
else
|
99
170
|
@api.namespace_stackable(:declared_params, @declared_params)
|
@@ -183,6 +254,9 @@ module Grape
|
|
183
254
|
return if values.is_a?(Proc)
|
184
255
|
coerce_type = coerce_type.first if coerce_type.is_a?(Array)
|
185
256
|
value_types = values.is_a?(Range) ? [values.begin, values.end] : values
|
257
|
+
if coerce_type == Virtus::Attribute::Boolean
|
258
|
+
value_types = value_types.map { |type| Virtus::Attribute.build(type) }
|
259
|
+
end
|
186
260
|
if value_types.any? { |v| !v.is_a?(coerce_type) }
|
187
261
|
fail Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values)
|
188
262
|
end
|
@@ -41,7 +41,9 @@ module Grape
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def valid_type?(val)
|
44
|
-
if
|
44
|
+
if val.instance_of?(InvalidValue)
|
45
|
+
false
|
46
|
+
elsif @option.is_a?(Array) || @option.is_a?(Set)
|
45
47
|
_valid_array_type?(@option.first, val)
|
46
48
|
else
|
47
49
|
_valid_single_type?(@option, val)
|
@@ -54,7 +56,14 @@ module Grape
|
|
54
56
|
return val || Set.new if type == Set
|
55
57
|
return val || {} if type == Hash
|
56
58
|
|
57
|
-
|
59
|
+
# To support custom types that Virtus can't easily coerce, pass in an
|
60
|
+
# explicit coercer. Custom types must implement a `parse` class method.
|
61
|
+
converter_options = {}
|
62
|
+
if ParameterTypes.custom_type?(type)
|
63
|
+
converter_options[:coercer] = type.method(:parse)
|
64
|
+
end
|
65
|
+
|
66
|
+
converter = Virtus::Attribute.build(type, converter_options)
|
58
67
|
converter.coerce(val)
|
59
68
|
|
60
69
|
# not the prettiest but some invalid coercion can currently trigger
|
data/lib/grape/validations.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
module Grape
|
2
|
+
# Registry to store and locate known Validators.
|
2
3
|
module Validations
|
3
4
|
class << self
|
4
5
|
attr_accessor :validators
|
@@ -6,6 +7,10 @@ module Grape
|
|
6
7
|
|
7
8
|
self.validators = {}
|
8
9
|
|
10
|
+
# Register a new validator, so it can be used to validate parameters.
|
11
|
+
# @param short_name [String] all lower-case, no spaces
|
12
|
+
# @param klass [Class] the validator class. Should inherit from
|
13
|
+
# Validations::Base.
|
9
14
|
def self.register_validator(short_name, klass)
|
10
15
|
validators[short_name] = klass
|
11
16
|
end
|
data/lib/grape/version.rb
CHANGED
data/lib/grape.rb
CHANGED
@@ -9,19 +9,13 @@ require 'hashie'
|
|
9
9
|
require 'set'
|
10
10
|
require 'active_support/version'
|
11
11
|
require 'active_support/core_ext/hash/indifferent_access'
|
12
|
-
|
13
|
-
if ActiveSupport::VERSION::MAJOR >= 4
|
14
|
-
require 'active_support/core_ext/object/deep_dup'
|
15
|
-
else
|
16
|
-
require_relative 'backports/active_support/deep_dup'
|
17
|
-
end
|
18
|
-
|
19
12
|
require 'active_support/ordered_hash'
|
20
13
|
require 'active_support/core_ext/object/conversions'
|
21
14
|
require 'active_support/core_ext/array/extract_options'
|
22
15
|
require 'active_support/core_ext/hash/deep_merge'
|
16
|
+
require 'active_support/core_ext/hash/except'
|
23
17
|
require 'active_support/dependencies/autoload'
|
24
|
-
require '
|
18
|
+
require 'active_support/notifications'
|
25
19
|
require 'multi_json'
|
26
20
|
require 'multi_xml'
|
27
21
|
require 'virtus'
|
@@ -66,6 +60,7 @@ module Grape
|
|
66
60
|
autoload :InvalidVersionerOption
|
67
61
|
autoload :UnknownValidator
|
68
62
|
autoload :UnknownOptions
|
63
|
+
autoload :UnknownParameter
|
69
64
|
autoload :InvalidWithOptionForRepresent
|
70
65
|
autoload :IncompatibleOptionValues
|
71
66
|
autoload :MissingGroupTypeError, 'grape/exceptions/missing_group_type'
|
@@ -129,6 +124,7 @@ module Grape
|
|
129
124
|
autoload :StackableValues
|
130
125
|
autoload :InheritableSetting
|
131
126
|
autoload :StrictHashConfiguration
|
127
|
+
autoload :FileResponse
|
132
128
|
end
|
133
129
|
|
134
130
|
module DSL
|
@@ -159,6 +155,9 @@ module Grape
|
|
159
155
|
end
|
160
156
|
end
|
161
157
|
|
158
|
+
require 'grape/util/content_types'
|
159
|
+
require 'grape/util/parameter_types'
|
160
|
+
|
162
161
|
require 'grape/validations/validators/base'
|
163
162
|
require 'grape/validations/attributes_iterator'
|
164
163
|
require 'grape/validations/validators/allow_blank'
|
data/spec/grape/api_spec.rb
CHANGED
@@ -795,6 +795,37 @@ describe Grape::API do
|
|
795
795
|
expect(last_response.body).to eq(file)
|
796
796
|
end
|
797
797
|
|
798
|
+
it 'returns the content of the file with file' do
|
799
|
+
file_content = 'This is some file content'
|
800
|
+
test_file = Tempfile.new('test')
|
801
|
+
test_file.write file_content
|
802
|
+
test_file.rewind
|
803
|
+
|
804
|
+
subject.get('/file') { file test_file }
|
805
|
+
get '/file'
|
806
|
+
expect(last_response.headers['Content-Length']).to eq('25')
|
807
|
+
expect(last_response.headers['Content-Type']).to eq('text/plain')
|
808
|
+
expect(last_response.body).to eq(file_content)
|
809
|
+
end
|
810
|
+
|
811
|
+
it 'streams the content of the file with stream' do
|
812
|
+
test_stream = Enumerator.new do |blk|
|
813
|
+
blk.yield 'This is some'
|
814
|
+
blk.yield ' file content'
|
815
|
+
end
|
816
|
+
|
817
|
+
subject.use Rack::Chunked
|
818
|
+
subject.get('/stream') { stream test_stream }
|
819
|
+
get '/stream', {}, 'HTTP_VERSION' => 'HTTP/1.1'
|
820
|
+
|
821
|
+
expect(last_response.headers['Content-Type']).to eq('text/plain')
|
822
|
+
expect(last_response.headers['Content-Length']).to eq(nil)
|
823
|
+
expect(last_response.headers['Cache-Control']).to eq('no-cache')
|
824
|
+
expect(last_response.headers['Transfer-Encoding']).to eq('chunked')
|
825
|
+
|
826
|
+
expect(last_response.body).to eq("c\r\nThis is some\r\nd\r\n file content\r\n0\r\n\r\n")
|
827
|
+
end
|
828
|
+
|
798
829
|
it 'sets content type for error' do
|
799
830
|
subject.get('/error') { error!('error in plain text', 500) }
|
800
831
|
get '/error'
|
@@ -2123,6 +2154,38 @@ describe Grape::API do
|
|
2123
2154
|
{ description: 'Reverses a string.', params: { 's' => { desc: 'string to reverse', type: 'string' } } }
|
2124
2155
|
]
|
2125
2156
|
end
|
2157
|
+
it 'does not inherit param descriptions in consequent namespaces' do
|
2158
|
+
subject.desc 'global description'
|
2159
|
+
subject.params do
|
2160
|
+
requires :param1
|
2161
|
+
optional :param2
|
2162
|
+
end
|
2163
|
+
subject.namespace 'ns1' do
|
2164
|
+
get do; end
|
2165
|
+
end
|
2166
|
+
subject.params do
|
2167
|
+
optional :param2
|
2168
|
+
end
|
2169
|
+
subject.namespace 'ns2' do
|
2170
|
+
get do; end
|
2171
|
+
end
|
2172
|
+
routes_doc = subject.routes.map { |route|
|
2173
|
+
{ description: route.route_description, params: route.route_params }
|
2174
|
+
}
|
2175
|
+
expect(routes_doc).to eq [
|
2176
|
+
{ description: 'global description',
|
2177
|
+
params: {
|
2178
|
+
'param1' => { required: true },
|
2179
|
+
'param2' => { required: false }
|
2180
|
+
}
|
2181
|
+
},
|
2182
|
+
{ description: 'global description',
|
2183
|
+
params: {
|
2184
|
+
'param2' => { required: false }
|
2185
|
+
}
|
2186
|
+
}
|
2187
|
+
]
|
2188
|
+
end
|
2126
2189
|
it 'merges the parameters of the namespace with the parameters of the method' do
|
2127
2190
|
subject.desc 'namespace'
|
2128
2191
|
subject.params do
|