Skip to content

Commit cf6d2dd

Browse files
committed
Refactor and extend coercion and type validation
Addresses #1164, #690, #689, #693. `Grape::ParameterTypes` is renamed `Grape::Validations::Types` to reflect that it should probably be bundled with an eventual `grape-validations` gem. It is expanded to include two new categories of types, 'special' and 'recognized' (see 'lib/grape/validations/types.rb'). `CoerceValidator` now makes use of `Virtus::Attribute::value_coerced?`, simplifying its internals. `CustomTypeCoercer` is introduced, attempting to standardize support for custom types by decoupling coercion and type-checking logic from the `type` class supplied to `Grape::Dsl::Parameters::requires`. The process for inferring which logic to use for each type and coercion method is encoded in `lib/grape/validations/types/build_coercer.rb`. `JSON`, `Array[JSON]` and `Rack::Multipart::UploadedFile (a.k.a `File`) are designated 'special' types, for which special implementations of `Virtus::Attribute` are provided. Instances of `Virtus::Attribute` built with `Virtus::Attribute.build` may now also be passed as the `type` parameter for `requires`. Depends on a monkey patch to `Virtus::Attribute::Collection`, included in `lib/grape/validations/types/virtus_collection_patch.rb`. See pull request 343 on solnic/virtus for more details.
1 parent 00dc6d8 commit cf6d2dd

File tree

15 files changed

+630
-207
lines changed

15 files changed

+630
-207
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
* Your contribution here.
77

8+
* [#1167](https://github.com/ruby-grape/grape/pull/1167): Refactor and extend coercion and type validation system - [@dslh](https://github.com/dslh).
89
* [#1163](https://github.com/ruby-grape/grape/pull/1163): First-class `JSON` parameter type - [@dslh](https://github.com/dslh).
910
* [#1161](https://github.com/ruby-grape/grape/pull/1161): Custom parameter coercion using `coerce_with` - [@dslh](https://github.com/dslh).
1011
* [#1134](https://github.com/ruby-grape/grape/pull/1134): Adds a code of conduct - [@towanda](https://github.com/towanda).

README.md

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
- [Parameter Validation and Coercion](#parameter-validation-and-coercion)
3232
- [Supported Parameter Types](#supported-parameter-types)
3333
- [Custom Types and Coercions](#custom-types-and-coercions)
34+
- [Multipart File Parameters](#multipart-file-parameters)
3435
- [First-Class `JSON` Types](#first-class-json-types)
3536
- [Validation of Nested Parameters](#validation-of-nested-parameters)
3637
- [Dependent Parameters](#dependent-parameters)
@@ -732,7 +733,8 @@ The following are all valid types, supported out of the box by Grape:
732733
* Boolean
733734
* String
734735
* Symbol
735-
* Rack::Multipart::UploadedFile
736+
* Rack::Multipart::UploadedFile (alias `File`)
737+
* JSON
736738

737739
### Custom Types and Coercions
738740

@@ -784,6 +786,23 @@ params do
784786
end
785787
```
786788

789+
### Multipart File Parameters
790+
791+
Grape makes use of `Rack::Request`'s built-in support for multipart
792+
file parameters. Such parameters can be declared with `type: File`:
793+
794+
```ruby
795+
params do
796+
requires :avatar, type: File
797+
end
798+
post '/' do
799+
# Parameter will be wrapped using Hashie:
800+
params.avatar.filename # => 'avatar.png'
801+
params.avatar.type # => 'image/png'
802+
params.avatar.tempfile # => #<File>
803+
end
804+
```
805+
787806
### First-Class `JSON` Types
788807

789808
Grape supports complex parameters given as JSON-formatted strings using the special `type: JSON`
@@ -810,9 +829,7 @@ client.get('/', json: '[{"int":4}]') # => HTTP 400
810829
```
811830

812831
Additionally `type: Array[JSON]` may be used, which explicitly marks the parameter as an array
813-
of objects. If a single object is supplied it will be wrapped. For stricter control over the
814-
type of JSON structure which may be supplied, use `type: Array, coerce_with: JSON` or
815-
`type: Hash, coerce_with: JSON`.
832+
of objects. If a single object is supplied it will be wrapped.
816833

817834
```ruby
818835
params do
@@ -824,6 +841,8 @@ get '/' do
824841
params[:json].each { |obj| ... } # always works
825842
end
826843
```
844+
For stricter control over the type of JSON structure which may be supplied,
845+
use `type: Array, coerce_with: JSON` or `type: Hash, coerce_with: JSON`.
827846

828847
### Validation of Nested Parameters
829848

lib/grape.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@
1919
require 'active_support/notifications'
2020
require 'multi_json'
2121
require 'multi_xml'
22-
require 'virtus'
2322
require 'i18n'
2423
require 'thread'
2524

25+
require 'virtus'
26+
2627
I18n.load_path << File.expand_path('../grape/locale/en.yml', __FILE__)
2728

2829
module Grape
@@ -159,7 +160,6 @@ module Presenters
159160
end
160161

161162
require 'grape/util/content_types'
162-
require 'grape/util/parameter_types'
163163

164164
require 'grape/validations/validators/base'
165165
require 'grape/validations/attributes_iterator'
@@ -174,5 +174,6 @@ module Presenters
174174
require 'grape/validations/validators/values'
175175
require 'grape/validations/params_scope'
176176
require 'grape/validations/validators/all_or_none'
177+
require 'grape/validations/types'
177178

178179
require 'grape/version'

lib/grape/dsl/parameters.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def use(*names)
4949
# the :using hash. The last key can be a hash, which specifies
5050
# options for the parameters
5151
# @option attrs :type [Class] the type to coerce this parameter to before
52-
# passing it to the endpoint. See {Grape::ParameterTypes} for a list of
52+
# passing it to the endpoint. See {Grape::Validations::Types} for a list of
5353
# types that are supported automatically. Custom classes may be used
5454
# where they define a class-level `::parse` method, or in conjunction
5555
# with the `:coerce_with` parameter. `JSON` may be supplied to denote

lib/grape/util/parameter_types.rb

Lines changed: 0 additions & 58 deletions
This file was deleted.

lib/grape/validations/types.rb

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
require_relative 'types/build_coercer'
2+
require_relative 'types/custom_type_coercer'
3+
require_relative 'types/json'
4+
require_relative 'types/file'
5+
6+
# Patch for Virtus::Attribute::Collection
7+
# See the file for more details
8+
require_relative 'types/virtus_collection_patch'
9+
10+
module Grape
11+
module Validations
12+
# Module for code related to grape's system for
13+
# coercion and type validation of incoming request
14+
# parameters.
15+
#
16+
# Grape uses a number of tests and assertions to
17+
# work out exactly how a parameter should be handled,
18+
# based on the +type+ and +coerce_with+ options that
19+
# may be supplied to {Grape::Dsl::Parameters#requires}
20+
# and {Grape::Dsl::Parameters#optional}. The main
21+
# entry point for this process is {Types.build_coercer}.
22+
module Types
23+
# Types representing a single value, which are coerced through Virtus
24+
# or special logic in Grape.
25+
PRIMITIVES = [
26+
# Numerical
27+
Integer,
28+
Float,
29+
BigDecimal,
30+
Numeric,
31+
32+
# Date/time
33+
Date,
34+
DateTime,
35+
Time,
36+
37+
# Misc
38+
Virtus::Attribute::Boolean,
39+
String,
40+
Symbol,
41+
Rack::Multipart::UploadedFile
42+
]
43+
44+
# Types representing data structures.
45+
STRUCTURES = [
46+
Hash,
47+
Array,
48+
Set
49+
]
50+
51+
# Types for which Grape provides special coercion
52+
# and type-checking logic.
53+
SPECIAL = {
54+
JSON => Json,
55+
Array[JSON] => JsonArray,
56+
::File => File,
57+
Rack::Multipart::UploadedFile => File
58+
}
59+
60+
# Is the given class a primitive type as recognized by Grape?
61+
#
62+
# @param type [Class] type to check
63+
# @return [Boolean] whether or not the type is known by Grape as a valid
64+
# type for a single value
65+
def self.primitive?(type)
66+
PRIMITIVES.include?(type)
67+
end
68+
69+
# Is the given class a standard data structure (collection or map)
70+
# as recognized by Grape?
71+
#
72+
# @param type [Class] type to check
73+
# @return [Boolean] whether or not the type is known by Grape as a valid
74+
# data structure type
75+
# @note This method does not yet consider 'complex types', which inherit
76+
# Virtus.model.
77+
def self.structure?(type)
78+
STRUCTURES.include?(type)
79+
end
80+
81+
# Does the given class implement a type system that Grape
82+
# (i.e. the underlying virtus attribute system) supports
83+
# out-of-the-box? Currently supported are +axiom-types+
84+
# and +virtus+.
85+
#
86+
# The type will be passed to +Virtus::Attribute.build+,
87+
# and the resulting attribute object will be expected to
88+
# respond correctly to +coerce+ and +value_coerced?+.
89+
#
90+
# @param type [Class] type to check
91+
# @return [Boolean] +true+ where the type is recognized
92+
def self.recognized?(type)
93+
return false if type.is_a?(Array) || type.is_a?(Set)
94+
95+
type.is_a?(Virtus::Attribute) ||
96+
type.ancestors.include?(Axiom::Types::Type) ||
97+
type.include?(Virtus::Model::Core)
98+
end
99+
100+
# Does Grape provide special coercion and validation
101+
# routines for the given class? This does not include
102+
# automatic handling for primitives, structures and
103+
# otherwise recognized types. See {Types::SPECIAL}.
104+
#
105+
# @param type [Class] type to check
106+
# @return [Boolean] +true+ if special routines are available
107+
def self.special?(type)
108+
SPECIAL.key? type
109+
end
110+
111+
# A valid custom type must implement a class-level `parse` method, taking
112+
# one String argument and returning the parsed value in its correct type.
113+
# @param type [Class] type to check
114+
# @return [Boolean] whether or not the type can be used as a custom type
115+
def self.custom?(type)
116+
!primitive?(type) &&
117+
!structure?(type) &&
118+
!recognized?(type) &&
119+
!special?(type) &&
120+
type.respond_to?(:parse) &&
121+
type.method(:parse).arity == 1
122+
end
123+
end
124+
end
125+
end
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
if type.is_a? Virtus::Attribute
24+
# Accept pre-rolled virtus attributes without interference
25+
type
26+
else
27+
converter_options = {
28+
nullify_blank: true
29+
}
30+
conversion_type = type
31+
32+
# Use a special coercer for custom types and coercion methods.
33+
if method || Types.custom?(type)
34+
converter_options[:coercer] = Types::CustomTypeCoercer.new(type, method)
35+
36+
# Grape swaps in its own Virtus::Attribute implementations
37+
# for certain special types that merit first-class support
38+
# (but not if a custom coercion method has been supplied).
39+
elsif Types.special?(type)
40+
conversion_type = Types::SPECIAL[type]
41+
end
42+
43+
# Virtus will infer coercion and validation rules
44+
# for many common ruby types.
45+
Virtus::Attribute.build(conversion_type, converter_options)
46+
end
47+
end
48+
end
49+
end
50+
end

0 commit comments

Comments
 (0)