Skip to content

Commit

Permalink
Refactor and extend coercion and type validation
Browse files Browse the repository at this point in the history
Addresses #1164, #690, #689, #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`. 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`.
A number of pre-rolled attributes are available providing coercion
for `Date` and `DateTime` objects from various formats in
`lib/grape/validations/formats/date.rb` and `date_time.rb`.
  • Loading branch information
dslh committed Sep 30, 2015
1 parent 00dc6d8 commit 07fce88
Show file tree
Hide file tree
Showing 20 changed files with 834 additions and 207 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.

* [#1167](https://github.com/ruby-grape/grape/pull/1167): 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
8 changes: 6 additions & 2 deletions lib/grape.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@
require 'active_support/notifications'
require 'multi_json'
require 'multi_xml'
require 'virtus'
require 'i18n'
require 'thread'

require 'virtus'
# Patch for Virtus::Attribute::Collection
# See the file for more details
require_relative 'virtus/attribute/collection_patch'

I18n.load_path << File.expand_path('../grape/locale/en.yml', __FILE__)

module Grape
Expand Down Expand Up @@ -159,7 +163,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 +177,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.

14 changes: 14 additions & 0 deletions lib/grape/validations/formats.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
require_relative 'formats/dates'
require_relative 'formats/date_times'

module Grape
module Validations
# Contains collections of constants that may be passed
# as the +type+ parameter of {Grape::Dsl::Parameters#requires}
# or {Grape::Dsl::Parameters#optional}, providing
# parameter coercion from a range of standard formats
# to a number of standard types.
module Formats
end
end
end
47 changes: 47 additions & 0 deletions lib/grape/validations/formats/date_times.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require 'time'
require_relative '../types/custom_type_coercer'

module Grape
module Validations
module Formats
# This module provides a set of ready-made +Virtus::Attribute+
# constants, suitable for use as the +type+ option for
# {Grape::Dsl::Parameters#requires} and {Grape::Dsl::Parameters#optional}.
# These definitions will coerce input strings to the standard
# ruby +DateTime+ type using the standard parsing methods defined
# on that class.
#
# This module is not required by default.
module DateTimes
# Parses timestamps using +DateTime.httpdate+
HttpDate = Types::CustomTypeCoercer.build(::DateTime, ::DateTime.method(:httpdate))

# Parses timestamps using +DateTime.iso8601+
Iso8601 = Types::CustomTypeCoercer.build(::DateTime, ::DateTime.method(:iso8601))

# Parses timestamps using +DateTime.jisx0301+
Jisx0301 = Types::CustomTypeCoercer.build(::DateTime, ::DateTime.method(:jisx0301))

# Parses julian dates using +DateTime.jd+.
# Time of day is not supported.
JulianDay = Types::CustomTypeCoercer.build(::DateTime, lambda do |val|
fail Grape::Exceptions::Validations, 'julian date must be an integer' unless val =~ /^\d*$/

::DateTime.jd val.to_i
end)

# Parses timestamps using +DateTime.rfc2822+
Rfc2822 = Types::CustomTypeCoercer.build(::DateTime, ::DateTime.method(:rfc2822))

# Parses timestamps using +DateTime.rfc3339+
Rfc3339 = Types::CustomTypeCoercer.build(::DateTime, ::DateTime.method(:rfc3339))

# Parses timestamps using +DateTime.rfc822+
Rfc822 = Types::CustomTypeCoercer.build(::DateTime, ::DateTime.method(:rfc822))

# Parses timestamps using +DateTime.xmlschema+
XmlSchema = Types::CustomTypeCoercer.build(::DateTime, ::DateTime.method(:xmlschema))
end
end
end
end
46 changes: 46 additions & 0 deletions lib/grape/validations/formats/dates.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
require 'date'
require_relative '../types/custom_type_coercer'

module Grape
module Validations
module Formats
# This module provides a set of ready-made +Virtus::Attribute+
# constants, suitable for use as the +type+ option for
# {Grape::Dsl::Parameters#requires} and {Grape::Dsl::Parameters#optional}.
# These definitions will coerce input strings to the standard
# ruby +Date+ type using the standard parsing methods defined
# on that class.
#
# This module is not required by default.
module Dates
# Parses dates using +Date.httpdate+
HttpDate = Types::CustomTypeCoercer.build(::Date, ::Date.method(:httpdate))

# Parses dates using +Date.iso8601+
Iso8601 = Types::CustomTypeCoercer.build(::Date, ::Date.method(:iso8601))

# Parses dates using +Date.jisx0301+
Jisx0301 = Types::CustomTypeCoercer.build(::Date, ::Date.method(:jisx0301))

# Parses dates using +Date.jd+
JulianDay = Types::CustomTypeCoercer.build(::Date, lambda do |val|
fail Grape::Exceptions::Validations, 'julian date must be an integer' unless val =~ /^\d*$/

::Date.jd val.to_i
end)

# Parses dates using +Date.rfc2822+
Rfc2822 = Types::CustomTypeCoercer.build(::Date, ::Date.method(:rfc2822))

# Parses dates using +Date.rfc3339+
Rfc3339 = Types::CustomTypeCoercer.build(::Date, ::Date.method(:rfc3339))

# Parses dates using +Date.rfc822+
Rfc822 = Types::CustomTypeCoercer.build(::Date, ::Date.method(:rfc822))

# Parses dates using +Date.xmlschema+
XmlSchema = Types::CustomTypeCoercer.build(::Date, ::Date.method(:xmlschema))
end
end
end
end
Loading

0 comments on commit 07fce88

Please sign in to comment.