grape 0.13.0 → 0.14.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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/Appraisals +9 -4
  3. data/CHANGELOG.md +28 -0
  4. data/Gemfile +0 -1
  5. data/Gemfile.lock +166 -0
  6. data/README.md +305 -163
  7. data/Rakefile +30 -33
  8. data/UPGRADING.md +31 -0
  9. data/benchmark/simple.rb +27 -0
  10. data/gemfiles/rack_1.5.2.gemfile +13 -0
  11. data/gemfiles/rails_3.gemfile +2 -2
  12. data/gemfiles/rails_4.gemfile +1 -2
  13. data/grape.gemspec +5 -4
  14. data/lib/grape.rb +9 -5
  15. data/lib/grape/dsl/configuration.rb +5 -2
  16. data/lib/grape/dsl/helpers.rb +8 -3
  17. data/lib/grape/dsl/inside_route.rb +67 -44
  18. data/lib/grape/dsl/parameters.rb +21 -12
  19. data/lib/grape/dsl/request_response.rb +1 -1
  20. data/lib/grape/dsl/routing.rb +3 -4
  21. data/lib/grape/endpoint.rb +63 -28
  22. data/lib/grape/error_formatter/base.rb +6 -6
  23. data/lib/grape/exceptions/base.rb +5 -5
  24. data/lib/grape/exceptions/invalid_version_header.rb +10 -0
  25. data/lib/grape/formatter/serializable_hash.rb +3 -2
  26. data/lib/grape/locale/en.yml +4 -1
  27. data/lib/grape/middleware/auth/base.rb +2 -2
  28. data/lib/grape/middleware/auth/dsl.rb +1 -1
  29. data/lib/grape/middleware/auth/strategies.rb +1 -1
  30. data/lib/grape/middleware/base.rb +7 -4
  31. data/lib/grape/middleware/error.rb +3 -2
  32. data/lib/grape/middleware/filter.rb +1 -1
  33. data/lib/grape/middleware/formatter.rb +47 -44
  34. data/lib/grape/middleware/globals.rb +3 -3
  35. data/lib/grape/middleware/versioner/accept_version_header.rb +5 -7
  36. data/lib/grape/middleware/versioner/header.rb +113 -50
  37. data/lib/grape/middleware/versioner/param.rb +5 -8
  38. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +20 -0
  39. data/lib/grape/middleware/versioner/path.rb +3 -6
  40. data/lib/grape/path.rb +3 -3
  41. data/lib/grape/request.rb +40 -0
  42. data/lib/grape/util/content_types.rb +9 -9
  43. data/lib/grape/util/env.rb +22 -0
  44. data/lib/grape/util/strict_hash_configuration.rb +2 -1
  45. data/lib/grape/validations/attributes_iterator.rb +8 -3
  46. data/lib/grape/validations/params_scope.rb +83 -15
  47. data/lib/grape/validations/types.rb +144 -0
  48. data/lib/grape/validations/types/build_coercer.rb +53 -0
  49. data/lib/grape/validations/types/custom_type_coercer.rb +183 -0
  50. data/lib/grape/validations/types/file.rb +28 -0
  51. data/lib/grape/validations/types/json.rb +65 -0
  52. data/lib/grape/validations/types/multiple_type_coercer.rb +76 -0
  53. data/lib/grape/validations/types/variant_collection_coercer.rb +59 -0
  54. data/lib/grape/validations/types/virtus_collection_patch.rb +16 -0
  55. data/lib/grape/validations/validators/all_or_none.rb +1 -1
  56. data/lib/grape/validations/validators/allow_blank.rb +3 -3
  57. data/lib/grape/validations/validators/base.rb +7 -0
  58. data/lib/grape/validations/validators/coerce.rb +31 -42
  59. data/lib/grape/validations/validators/presence.rb +2 -3
  60. data/lib/grape/validations/validators/regexp.rb +2 -4
  61. data/lib/grape/validations/validators/values.rb +3 -3
  62. data/lib/grape/version.rb +1 -1
  63. data/pkg/grape-0.13.0.gem +0 -0
  64. data/spec/grape/api/custom_validations_spec.rb +5 -4
  65. data/spec/grape/api/deeply_included_options_spec.rb +7 -7
  66. data/spec/grape/api/nested_helpers_spec.rb +4 -2
  67. data/spec/grape/api/shared_helpers_spec.rb +8 -8
  68. data/spec/grape/api_spec.rb +88 -54
  69. data/spec/grape/dsl/configuration_spec.rb +13 -0
  70. data/spec/grape/dsl/helpers_spec.rb +16 -2
  71. data/spec/grape/dsl/inside_route_spec.rb +3 -2
  72. data/spec/grape/dsl/parameters_spec.rb +0 -6
  73. data/spec/grape/dsl/routing_spec.rb +1 -1
  74. data/spec/grape/endpoint_spec.rb +61 -20
  75. data/spec/grape/entity_spec.rb +10 -8
  76. data/spec/grape/exceptions/invalid_accept_header_spec.rb +1 -15
  77. data/spec/grape/integration/rack_spec.rb +3 -2
  78. data/spec/grape/middleware/base_spec.rb +7 -5
  79. data/spec/grape/middleware/error_spec.rb +16 -15
  80. data/spec/grape/middleware/exception_spec.rb +45 -43
  81. data/spec/grape/middleware/formatter_spec.rb +34 -0
  82. data/spec/grape/middleware/versioner/header_spec.rb +79 -47
  83. data/spec/grape/path_spec.rb +10 -10
  84. data/spec/grape/presenters/presenter_spec.rb +2 -2
  85. data/spec/grape/request_spec.rb +100 -0
  86. data/spec/grape/validations/params_scope_spec.rb +11 -9
  87. data/spec/grape/validations/types_spec.rb +95 -0
  88. data/spec/grape/validations/validators/coerce_spec.rb +335 -2
  89. data/spec/grape/validations/validators/values_spec.rb +15 -15
  90. data/spec/grape/validations_spec.rb +53 -24
  91. data/spec/shared/versioning_examples.rb +2 -2
  92. data/spec/spec_helper.rb +0 -1
  93. data/spec/support/versioned_helpers.rb +2 -2
  94. metadata +51 -13
  95. data/.gitignore +0 -46
  96. data/.rspec +0 -2
  97. data/.rubocop.yml +0 -7
  98. data/.rubocop_todo.yml +0 -84
  99. data/.travis.yml +0 -20
  100. data/.yardopts +0 -2
  101. data/lib/grape/http/request.rb +0 -35
  102. data/lib/grape/util/parameter_types.rb +0 -58
  103. data/spec/grape/util/parameter_types_spec.rb +0 -54
@@ -0,0 +1,53 @@
1
+ module Grape
2
+ module Validations
3
+ module Types
4
+ # Work out the +Virtus::Attribute+ object to
5
+ # use for coercing strings to the given +type+.
6
+ # Coercion +method+ will be inferred if none is
7
+ # supplied.
8
+ #
9
+ # If a +Virtus::Attribute+ object already built
10
+ # with +Virtus::Attribute.build+ is supplied as
11
+ # the +type+ it will be returned and +method+
12
+ # will be ignored.
13
+ #
14
+ # See {CustomTypeCoercer} for further details
15
+ # about coercion and type-checking inference.
16
+ #
17
+ # @param type [Class] the type to which input strings
18
+ # should be coerced
19
+ # @param method [Class,#call] the coercion method to use
20
+ # @return [Virtus::Attribute] object to be used
21
+ # for coercion and type validation
22
+ def self.build_coercer(type, method = nil)
23
+ # Accept pre-rolled virtus attributes without interference
24
+ return type if type.is_a? Virtus::Attribute
25
+
26
+ converter_options = {
27
+ nullify_blank: true
28
+ }
29
+ conversion_type = type
30
+
31
+ # Use a special coercer for multiply-typed parameters.
32
+ if Types.multiple?(type)
33
+ converter_options[:coercer] = Types::MultipleTypeCoercer.new(type, method)
34
+ conversion_type = Object
35
+
36
+ # Use a special coercer for custom types and coercion methods.
37
+ elsif method || Types.custom?(type)
38
+ converter_options[:coercer] = Types::CustomTypeCoercer.new(type, method)
39
+
40
+ # Grape swaps in its own Virtus::Attribute implementations
41
+ # for certain special types that merit first-class support
42
+ # (but not if a custom coercion method has been supplied).
43
+ elsif Types.special?(type)
44
+ conversion_type = Types::SPECIAL[type]
45
+ end
46
+
47
+ # Virtus will infer coercion and validation rules
48
+ # for many common ruby types.
49
+ Virtus::Attribute.build(conversion_type, converter_options)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,183 @@
1
+ module Grape
2
+ module Validations
3
+ module Types
4
+ # Instances of this class may be passed to
5
+ # +Virtus::Attribute.build+ as the +:coercer+
6
+ # option for custom types that do not otherwise
7
+ # satisfy the requirements for +Virtus::Attribute::coerce+
8
+ # and +Virtus::Attribute::value_coerced?+ to work
9
+ # as expected.
10
+ #
11
+ # Subclasses of +Virtus::Attribute+ or +Axiom::Types::Type+
12
+ # (or for which an axiom type can be inferred, i.e. the
13
+ # primitives, +Date+, +Time+, etc.) do not need any such
14
+ # coercer to be passed with them.
15
+ #
16
+ # Coercion
17
+ # --------
18
+ #
19
+ # This class will detect type classes that implement
20
+ # a class-level +parse+ method. The method should accept one
21
+ # +String+ argument and should return the value coerced to
22
+ # the appropriate type. The method may raise an exception if
23
+ # there are any problems parsing the string.
24
+ #
25
+ # Alternately an optional +method+ may be supplied (see the
26
+ # +coerce_with+ option of {Grape::Dsl::Parameters#requires}).
27
+ # This may be any class or object implementing +parse+ or +call+,
28
+ # with the same contract as described above.
29
+ #
30
+ # Type Checking
31
+ # -------------
32
+ #
33
+ # Calls to +value_coerced?+ will consult this class to check
34
+ # that the coerced value produced above is in fact of the
35
+ # expected type. By default this class performs a basic check
36
+ # against the type supplied, but this behaviour will be
37
+ # overridden if the class implements a class-level
38
+ # +coerced?+ or +parsed?+ method. This method
39
+ # will receive a single parameter that is the coerced value
40
+ # and should return +true+ iff the value meets type expectations.
41
+ # Arbitrary assertions may be made here but the grape validation
42
+ # system should be preferred.
43
+ #
44
+ # Alternately a proc or other object responding to +call+ may be
45
+ # supplied in place of a type. This should implement the same
46
+ # contract as +coerced?+, and must be supplied with a coercion
47
+ # +method+.
48
+ class CustomTypeCoercer
49
+ # Uses +Virtus::Attribute.build+ to build a new
50
+ # attribute that makes use of this class for
51
+ # coercion and type validation logic.
52
+ #
53
+ # @return [Virtus::Attribute]
54
+ def self.build(type, method = nil)
55
+ Virtus::Attribute.build(type, coercer: new(type, method))
56
+ end
57
+
58
+ # A new coercer for the given type specification
59
+ # and coercion method.
60
+ #
61
+ # @param type [Class,#coerced?,#parsed?,#call?]
62
+ # specifier for the target type. See class docs.
63
+ # @param method [#parse,#call]
64
+ # optional coercion method. See class docs.
65
+ def initialize(type, method = nil)
66
+ coercion_method = infer_coercion_method type, method
67
+
68
+ @method = enforce_symbolized_keys type, coercion_method
69
+
70
+ @type_check = infer_type_check(type)
71
+ end
72
+
73
+ # This method is called from somewhere within
74
+ # +Virtus::Attribute::coerce+ in order to coerce
75
+ # the given value.
76
+ #
77
+ # @param value [String] value to be coerced, in grape
78
+ # this should always be a string.
79
+ # @return [Object] the coerced result
80
+ def call(value)
81
+ @method.call value
82
+ end
83
+
84
+ # This method is called from somewhere within
85
+ # +Virtus::Attribute::value_coerced?+ in order to
86
+ # assert that the value has been coerced successfully.
87
+ #
88
+ # @param _primitive [Axiom::Types::Type] primitive type
89
+ # for the coercion as detected by axiom-types' inference
90
+ # system. For custom types this is typically not much use
91
+ # (i.e. it is +Axiom::Types::Object+) unless special
92
+ # inference rules have been declared for the type.
93
+ # @param value [Object] a coerced result returned from {#call}
94
+ # @return [true,false] whether or not the coerced value
95
+ # satisfies type requirements.
96
+ def success?(_primitive, value)
97
+ @type_check.call value
98
+ end
99
+
100
+ private
101
+
102
+ # Determine the coercion method we're expected to use
103
+ # based on the parameters given.
104
+ #
105
+ # @param type see #new
106
+ # @param method see #new
107
+ # @return [#call] coercion method
108
+ def infer_coercion_method(type, method)
109
+ if method
110
+ if method.respond_to? :parse
111
+ method.method :parse
112
+ else
113
+ method
114
+ end
115
+ else
116
+ # Try to use parse() declared on the target type.
117
+ # This may raise an exception, but we are out of ideas anyway.
118
+ type.method :parse
119
+ end
120
+ end
121
+
122
+ # Determine how the type validity of a coerced
123
+ # value should be decided.
124
+ #
125
+ # @param type see #new
126
+ # @return [#call] a procedure which accepts a single parameter
127
+ # and returns +true+ if the passed object is of the correct type.
128
+ def infer_type_check(type)
129
+ # First check for special class methods
130
+ if type.respond_to? :coerced?
131
+ type.method :coerced?
132
+ elsif type.respond_to? :parsed?
133
+ type.method :parsed?
134
+ elsif type.respond_to? :call
135
+ # Arbitrary proc passed for type validation.
136
+ # Note that this will fail unless a method is also
137
+ # passed, or if the type also implements a parse() method.
138
+ type
139
+ else
140
+ # By default, do a simple type check
141
+ ->(value) { value.is_a? type }
142
+ end
143
+ end
144
+
145
+ # Enforce symbolized keys for complex types
146
+ # by wrapping the coercion method such that
147
+ # any Hash objects in the immediate heirarchy
148
+ # are passed through +Hashie.symbolize_keys!+.
149
+ # This helps common libs such as JSON to work easily.
150
+ #
151
+ # @param type see #new
152
+ # @param method see #infer_coercion_method
153
+ # @return [#call] +method+ wrapped in an additional
154
+ # key-conversion step, or just returns +method+
155
+ # itself if no conversion is deemed to be
156
+ # necessary.
157
+ def enforce_symbolized_keys(type, method)
158
+ # Collections have all values processed individually
159
+ if type == Array || type == Set
160
+ lambda do |val|
161
+ method.call(val).tap do |new_value|
162
+ new_value.each do |item|
163
+ Hashie.symbolize_keys!(item) if item.is_a? Hash
164
+ end
165
+ end
166
+ end
167
+
168
+ # Hash objects are processed directly
169
+ elsif type == Hash
170
+ lambda do |val|
171
+ Hashie.symbolize_keys! method.call(val)
172
+ end
173
+
174
+ # Simple types are not processed.
175
+ # This includes Array<primitive> types.
176
+ else
177
+ method
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,28 @@
1
+ module Grape
2
+ module Validations
3
+ module Types
4
+ # +Virtus::Attribute+ implementation for parameters
5
+ # that are multipart file objects. Actual handling
6
+ # of these objects is provided by +Rack::Request+;
7
+ # this class is here only to assert that rack's
8
+ # handling has succeeded, and to prevent virtus
9
+ # from interfering.
10
+ class File < Virtus::Attribute
11
+ def coerce(input)
12
+ # Processing of multipart file objects
13
+ # is already taken care of by Rack::Request.
14
+ # Nothing to do here.
15
+ input
16
+ end
17
+
18
+ def value_coerced?(value)
19
+ # Rack::Request creates a Hash with filename,
20
+ # content type and an IO object. Grape wraps that
21
+ # using hashie for convenience. Do a bit of basic
22
+ # duck-typing.
23
+ value.is_a?(Hashie::Mash) && value.key?(:tempfile)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,65 @@
1
+ require 'json'
2
+
3
+ module Grape
4
+ module Validations
5
+ module Types
6
+ # +Virtus::Attribute+ implementation that handles coercion
7
+ # and type checking for parameters that are complex types
8
+ # given as JSON-encoded strings. It accepts both JSON objects
9
+ # and arrays of objects, and will coerce the input to a +Hash+
10
+ # or +Array+ object respectively. In either case the Grape
11
+ # validation system will apply nested validation rules to
12
+ # all returned objects.
13
+ class Json < Virtus::Attribute
14
+ # Coerce the input into a JSON-like data structure.
15
+ #
16
+ # @param input [String] a JSON-encoded parameter value
17
+ # @return [Hash,Array<Hash>,nil]
18
+ def coerce(input)
19
+ # Allow nulls and blank strings
20
+ return if input.nil? || input =~ /^\s*$/
21
+ JSON.parse(input, symbolize_names: true)
22
+ end
23
+
24
+ # Checks that the input was parsed successfully
25
+ # and isn't something odd such as an array of primitives.
26
+ #
27
+ # @param value [Object] result of {#coerce}
28
+ # @return [true,false]
29
+ def value_coerced?(value)
30
+ value.is_a?(::Hash) || coerced_collection?(value)
31
+ end
32
+
33
+ protected
34
+
35
+ # Is the value an array of JSON-like objects?
36
+ #
37
+ # @param value [Object] result of {#coerce}
38
+ # @return [true,false]
39
+ def coerced_collection?(value)
40
+ value.is_a?(::Array) && value.all? { |i| i.is_a? ::Hash }
41
+ end
42
+ end
43
+
44
+ # Specialization of the {Json} attribute that is guaranteed
45
+ # to return an array of objects. Accepts both JSON-encoded
46
+ # objects and arrays of objects, but wraps single objects
47
+ # in an Array.
48
+ class JsonArray < Json
49
+ # See {Json#coerce}. Wraps single objects in an array.
50
+ #
51
+ # @param input [String] JSON-encoded parameter value
52
+ # @return [Array<Hash>]
53
+ def coerce(input)
54
+ json = super
55
+ Array.wrap(json) unless json.nil?
56
+ end
57
+
58
+ # See {Json#coerced_collection?}
59
+ def value_coerced?(value)
60
+ coerced_collection? value
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,76 @@
1
+ module Grape
2
+ module Validations
3
+ module Types
4
+ # This class is intended for use with Grape endpoint parameters that
5
+ # have been declared to be of variant-type using the +:types+ option.
6
+ # +MultipleTypeCoercer+ will build a coercer for each type declared
7
+ # in the array passed to +:types+ using {Types.build_coercer}. It will
8
+ # apply these coercers to parameter values in the order given to
9
+ # +:types+, and will return the value returned by the first coercer
10
+ # to successfully coerce the parameter value. Therefore if +String+ is
11
+ # an allowed type it should be declared last, since it will always
12
+ # successfully "coerce" the value.
13
+ class MultipleTypeCoercer
14
+ # Construct a new coercer that will attempt to coerce
15
+ # values to the given list of types in the given order.
16
+ #
17
+ # @param types [Array<Class>] list of allowed types
18
+ # @param method [#call,#parse] method by which values should be
19
+ # coerced. See class docs for default behaviour.
20
+ def initialize(types, method = nil)
21
+ @method = method.respond_to?(:parse) ? method.method(:parse) : method
22
+
23
+ @type_coercers = types.map do |type|
24
+ if Types.multiple? type
25
+ VariantCollectionCoercer.new type
26
+ else
27
+ Types.build_coercer type
28
+ end
29
+ end
30
+ end
31
+
32
+ # This method is called from somewhere within
33
+ # +Virtus::Attribute::coerce+ in order to coerce
34
+ # the given value.
35
+ #
36
+ # @param value [String] value to be coerced, in grape
37
+ # this should always be a string.
38
+ # @return [Object,InvalidValue] the coerced result, or an instance
39
+ # of {InvalidValue} if the value could not be coerced.
40
+ def call(value)
41
+ return @method.call(value) if @method
42
+
43
+ @type_coercers.each do |coercer|
44
+ coerced = coercer.coerce(value)
45
+
46
+ return coerced if coercer.value_coerced? coerced
47
+ end
48
+
49
+ # Declare that we couldn't coerce the value in such a way
50
+ # that Grape won't ask us again if the value is valid
51
+ InvalidValue.new
52
+ end
53
+
54
+ # This method is called from somewhere within
55
+ # +Virtus::Attribute::value_coerced?+ in order to
56
+ # assert that the value has been coerced successfully.
57
+ # Due to Grape's design this will in fact only be called
58
+ # if a custom coercion method is being used, since {#call}
59
+ # returns an {InvalidValue} object if the value could not
60
+ # be coerced.
61
+ #
62
+ # @param _primitive [Axiom::Types::Type] primitive type
63
+ # for the coercion as detected by axiom-types' inference
64
+ # system. For custom types this is typically not much use
65
+ # (i.e. it is +Axiom::Types::Object+) unless special
66
+ # inference rules have been declared for the type.
67
+ # @param value [Object] a coerced result returned from {#call}
68
+ # @return [true,false] whether or not the coerced value
69
+ # satisfies type requirements.
70
+ def success?(_primitive, value)
71
+ @type_coercers.any? { |coercer| coercer.value_coerced? value }
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,59 @@
1
+ module Grape
2
+ module Validations
3
+ module Types
4
+ # This class wraps {MultipleTypeCoercer}, for use with collections
5
+ # that allow members of more than one type.
6
+ class VariantCollectionCoercer < Virtus::Attribute
7
+ # Construct a new coercer that will attempt to coerce
8
+ # a list of values such that all members are of one of
9
+ # the given types. The container may also optionally be
10
+ # coerced to a +Set+. An arbitrary coercion +method+ may
11
+ # be supplied, which will be passed the entire collection
12
+ # as a parameter and should return a new collection, or
13
+ # may return the same one if no coercion was required.
14
+ #
15
+ # @param types [Array<Class>,Set<Class>] list of allowed types,
16
+ # also specifying the container type
17
+ # @param method [#call,#parse] method by which values should be coerced
18
+ def initialize(types, method = nil)
19
+ @types = types
20
+ @method = method.respond_to?(:parse) ? method.method(:parse) : method
21
+
22
+ # If we have a coercion method, pass it in here to save
23
+ # building another one, even though we call it directly.
24
+ @member_coercer = MultipleTypeCoercer.new types, method
25
+ end
26
+
27
+ # Coerce the given value.
28
+ #
29
+ # @param value [Array<String>] collection of values to be coerced
30
+ # @return [Array<Object>,Set<Object>,InvalidValue]
31
+ # the coerced result, or an instance
32
+ # of {InvalidValue} if the value could not be coerced.
33
+ def coerce(value)
34
+ return InvalidValue.new unless value.is_a? Array
35
+
36
+ value =
37
+ if @method
38
+ @method.call(value)
39
+ else
40
+ value.map { |v| @member_coercer.call(v) }
41
+ end
42
+ return Set.new value if @types.is_a? Set
43
+
44
+ value
45
+ end
46
+
47
+ # Assert that the value has been coerced successfully.
48
+ #
49
+ # @param value [Object] a coerced result returned from {#coerce}
50
+ # @return [true,false] whether or not the coerced value
51
+ # satisfies type requirements.
52
+ def value_coerced?(value)
53
+ value.is_a?(@types.class) &&
54
+ value.all? { |v| @member_coercer.success?(@types, v) }
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end