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.

Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +2 -2
  3. data/CHANGELOG.md +237 -215
  4. data/CONTRIBUTING.md +4 -4
  5. data/README.md +133 -10
  6. data/RELEASING.md +14 -6
  7. data/Rakefile +1 -1
  8. data/UPGRADING.md +23 -23
  9. data/grape.gemspec +1 -3
  10. data/lib/grape/api.rb +24 -4
  11. data/lib/grape/dsl/callbacks.rb +20 -0
  12. data/lib/grape/dsl/configuration.rb +54 -0
  13. data/lib/grape/dsl/inside_route.rb +33 -1
  14. data/lib/grape/dsl/parameters.rb +80 -0
  15. data/lib/grape/dsl/routing.rb +14 -0
  16. data/lib/grape/dsl/settings.rb +36 -1
  17. data/lib/grape/dsl/validations.rb +7 -5
  18. data/lib/grape/endpoint.rb +42 -32
  19. data/lib/grape/exceptions/unknown_parameter.rb +10 -0
  20. data/lib/grape/exceptions/validation_errors.rb +4 -3
  21. data/lib/grape/http/headers.rb +0 -1
  22. data/lib/grape/http/request.rb +12 -4
  23. data/lib/grape/locale/en.yml +1 -0
  24. data/lib/grape/middleware/base.rb +1 -0
  25. data/lib/grape/middleware/formatter.rb +39 -23
  26. data/lib/grape/namespace.rb +13 -2
  27. data/lib/grape/path.rb +1 -0
  28. data/lib/grape/route.rb +5 -0
  29. data/lib/grape/util/file_response.rb +21 -0
  30. data/lib/grape/util/inheritable_setting.rb +23 -2
  31. data/lib/grape/util/inheritable_values.rb +1 -1
  32. data/lib/grape/util/parameter_types.rb +58 -0
  33. data/lib/grape/util/stackable_values.rb +5 -2
  34. data/lib/grape/validations/params_scope.rb +83 -9
  35. data/lib/grape/validations/validators/coerce.rb +11 -2
  36. data/lib/grape/validations.rb +5 -0
  37. data/lib/grape/version.rb +2 -1
  38. data/lib/grape.rb +7 -8
  39. data/spec/grape/api_spec.rb +63 -0
  40. data/spec/grape/dsl/inside_route_spec.rb +37 -2
  41. data/spec/grape/dsl/validations_spec.rb +18 -0
  42. data/spec/grape/endpoint_spec.rb +83 -0
  43. data/spec/grape/exceptions/validation_errors_spec.rb +28 -0
  44. data/spec/grape/middleware/base_spec.rb +33 -11
  45. data/spec/grape/middleware/formatter_spec.rb +0 -5
  46. data/spec/grape/util/inheritable_values_spec.rb +14 -0
  47. data/spec/grape/util/parameter_types_spec.rb +54 -0
  48. data/spec/grape/util/stackable_values_spec.rb +10 -0
  49. data/spec/grape/validations/params_scope_spec.rb +84 -0
  50. data/spec/grape/validations/validators/coerce_spec.rb +29 -8
  51. data/spec/grape/validations/validators/values_spec.rb +12 -0
  52. metadata +9 -6
  53. data/lib/backports/active_support/deep_dup.rb +0 -49
  54. 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
- # allow content-type to be explicitly overwritten
29
- api_format = mime_types[headers[Grape::Http::Headers::CONTENT_TYPE]] || env['api.format']
30
- formatter = Grape::Formatter::Base.formatter_for api_format, options
31
- begin
32
- bodymap = if bodies.respond_to?(:collect)
33
- bodies.collect do |body|
34
- formatter.call body, env
35
- end
36
- else
37
- bodies
38
- end
39
- rescue Grape::Exceptions::InvalidFormatter => e
40
- throw :error, status: 500, message: e.message
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
- headers[Grape::Http::Headers::CONTENT_TYPE] = content_type_for(env['api.format']) unless headers[Grape::Http::Headers::CONTENT_TYPE]
43
- Rack::Response.new(bodymap, status, headers)
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 = headers[Grape::Http::Headers::ACCEPT]
152
+ accept = env[Grape::Http::Headers::HTTP_ACCEPT]
137
153
  return [] unless accept
138
154
 
139
155
  accept_into_mime_and_quality = %r{
@@ -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
- # options:
6
- # requirements: a hash
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
@@ -1,4 +1,5 @@
1
1
  module Grape
2
+ # Represents a path to an endpoint.
2
3
  class Path
3
4
  def self.prepare(raw_path, namespace, settings)
4
5
  Path.new(raw_path, namespace, settings).path_with_suffix
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
- def self.reset_global! # only for testing
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 seperate namespace by default?
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,
@@ -36,7 +36,7 @@ module Grape
36
36
  def initialize_copy(other)
37
37
  super
38
38
  self.inherited_values = other.inherited_values
39
- self.new_values = other.new_values.deep_dup
39
+ self.new_values = other.new_values.dup
40
40
  end
41
41
 
42
42
  protected
@@ -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]].compact.flatten(1)
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.deep_dup
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 = opts[:element]
10
- @parent = opts[:parent]
11
- @api = opts[:api]
12
- @optional = opts[:optional] || false
13
- @type = opts[: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
- return "#{@parent.full_name(@element)}[#{name}]" if @parent
29
- name.to_s
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
- ParamsScope.new(api: @api, element: attrs.first, parent: self, optional: optional, type: opts[:type], &block)
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 @parent
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 @option.is_a?(Array) || @option.is_a?(Set)
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
- converter = Virtus::Attribute.build(type)
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
@@ -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
@@ -1,3 +1,4 @@
1
1
  module Grape
2
- VERSION = '0.12.0'
2
+ # The current version of Grape.
3
+ VERSION = '0.13.0'
3
4
  end
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 'grape/util/content_types'
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'
@@ -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