Skip to content

Commit

Permalink
Refactor and extend coercion and type validation
Browse files Browse the repository at this point in the history
Addresses ruby-grape#1164, ruby-grape#690, ruby-grape#689, ruby-grape#693.
Depends on solnic/virtus#343

`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`.

`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`.
A number of pre-rolled attributes are available providing coercion
for `Date` and `DateTime` objects from various formats in
`lib/grape/validations/types/date.rb` and `date_time.rb`.
  • Loading branch information
dslh committed Sep 28, 2015
1 parent 210b4e1 commit f48f6b8
Show file tree
Hide file tree
Showing 16 changed files with 690 additions and 144 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

* Your contribution here.

* Refactor and extend coercion and type validation system - [@dslh](https://github.com/dslh).
* [#1163](https://github.com/ruby-grape/grape/pull/1163): First-class `JSON` parameter type - [@dslh](https://github.com/dslh).
* [#1161](https://github.com/ruby-grape/grape/pull/1161): Custom parameter coercion using `coerce_with` - [@dslh](https://github.com/dslh).
* [#1134](https://github.com/ruby-grape/grape/pull/1134): Adds a code of conduct - [@towanda](https://github.com/towanda).
Expand Down
27 changes: 23 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
- [Parameter Validation and Coercion](#parameter-validation-and-coercion)
- [Supported Parameter Types](#supported-parameter-types)
- [Custom Types and Coercions](#custom-types-and-coercions)
- [Multipart File Parameters](#multipart-file-parameters)
- [First-Class `JSON` Types](#first-class-json-types)
- [Validation of Nested Parameters](#validation-of-nested-parameters)
- [Dependent Parameters](#dependent-parameters)
Expand Down Expand Up @@ -732,7 +733,8 @@ The following are all valid types, supported out of the box by Grape:
* Boolean
* String
* Symbol
* Rack::Multipart::UploadedFile
* Rack::Multipart::UploadedFile (alias `File`)
* JSON

### Custom Types and Coercions

Expand Down Expand Up @@ -784,6 +786,23 @@ params do
end
```

### Multipart File Parameters

Grape makes use of `Rack::Request`'s built-in support for multipart
file parameters. Such parameters can be declared with `type: File`:

```ruby
params do
requires :avatar, type: File
end
post '/' do
# Parameter will be wrapped using Hashie:
params.avatar.filename # => 'avatar.png'
params.avatar.type # => 'image/png'
params.avatar.tempfile # => #<File>
end
```

### First-Class `JSON` Types

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

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

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

### Validation of Nested Parameters

Expand Down
2 changes: 1 addition & 1 deletion lib/grape.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,6 @@ module Presenters
end

require 'grape/util/content_types'
require 'grape/util/parameter_types'

require 'grape/validations/validators/base'
require 'grape/validations/attributes_iterator'
Expand All @@ -174,5 +173,6 @@ module Presenters
require 'grape/validations/validators/values'
require 'grape/validations/params_scope'
require 'grape/validations/validators/all_or_none'
require 'grape/validations/types'

require 'grape/version'
2 changes: 1 addition & 1 deletion lib/grape/dsl/parameters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def use(*names)
# the :using hash. The last key can be a hash, which specifies
# options for the parameters
# @option attrs :type [Class] the type to coerce this parameter to before
# passing it to the endpoint. See {Grape::ParameterTypes} for a list of
# passing it to the endpoint. See {Grape::Validations::Types} for a list of
# types that are supported automatically. Custom classes may be used
# where they define a class-level `::parse` method, or in conjunction
# with the `:coerce_with` parameter. `JSON` may be supplied to denote
Expand Down
58 changes: 0 additions & 58 deletions lib/grape/util/parameter_types.rb

This file was deleted.

110 changes: 110 additions & 0 deletions lib/grape/validations/types.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
require_relative 'types/custom_type_coercer'
require_relative 'types/json'
require_relative 'types/file'

module Grape
module Validations
module Types
# Types representing a single value, which are coerced through Virtus
# or special logic in Grape.
PRIMITIVES = [
# Numerical
Integer,
Float,
BigDecimal,
Numeric,

# Date/time
Date,
DateTime,
Time,

# Misc
Virtus::Attribute::Boolean,
String,
Symbol,
Rack::Multipart::UploadedFile
]

# Types representing data structures.
STRUCTURES = [
Hash,
Array,
Set
]

# Types for which Grape provides special coercion
# and type-checking logic.
SPECIAL = {
JSON => Json,
Array[JSON] => JsonArray,
::File => File,
Rack::Multipart::UploadedFile => File
}

# Is the given class a primitive type as recognized by Grape?
#
# @param type [Class] type to check
# @return [Boolean] whether or not the type is known by Grape as a valid
# type for a single value
def self.primitive?(type)
PRIMITIVES.include?(type)
end

# Is the given class a standard data structure (collection or map)
# as recognized by Grape?
#
# @param type [Class] type to check
# @return [Boolean] whether or not the type is known by Grape as a valid
# data structure type
# @note This method does not yet consider 'complex types', which inherit
# Virtus.model.
def self.structure?(type)
STRUCTURES.include?(type)
end

# Does the given class implement a type system that Grape
# (i.e. the underlying virtus attribute system) supports
# out-of-the-box? Currently supported are +axiom-types+
# and +virtus+.
#
# The type will be passed to +Virtus::Attribute.build+,
# and the resulting attribute object will be expected to
# respond correctly to +coerce+ and +value_coerced?+.
#
# @param type [Class] type to check
# @return [Boolean] +true+ where the type is recognized
def self.recognized?(type)
return false if type.is_a?(Array) || type.is_a?(Set)

type.is_a?(Virtus::Attribute) ||
type.ancestors.include?(Axiom::Types::Type) ||
type.include?(Virtus::Model::Core)
end

# Does Grape provide special coercion and validation
# routines for the given class? This does not include
# automatic handling for primitives, structures and
# otherwise recognized types. See {Types::SPECIAL}.
#
# @param type [Class] type to check
# @return [Boolean] +true+ if special routines are available
def self.special?(type)
SPECIAL.key? type
end

# A valid custom type must implement a class-level `parse` method, taking
# one String argument and returning the parsed value in its correct type.
# @param type [Class] type to check
# @return [Boolean] whether or not the type can be used as a custom type
def self.custom_type?(type)
!primitive?(type) &&
!structure?(type) &&
!recognized?(type) &&
!special?(type) &&
type.respond_to?(:parse) &&
type.method(:parse).arity == 1
end
end
end
end
Loading

0 comments on commit f48f6b8

Please sign in to comment.