Skip to content

Refactor and extend coercion and type validation #1167

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 30, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've added type: File, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, doc'd and spec'd.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe should be added to changelog as a line, like that we support File now out of the box.

* [#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
5 changes: 3 additions & 2 deletions lib/grape.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
require 'active_support/notifications'
require 'multi_json'
require 'multi_xml'
require 'virtus'
require 'i18n'
require 'thread'

require 'virtus'

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

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

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

# Patch for Virtus::Attribute::Collection
# See the file for more details
require_relative 'types/virtus_collection_patch'

module Grape
module Validations
# Module for code related to grape's system for
# coercion and type validation of incoming request
# parameters.
#
# Grape uses a number of tests and assertions to
# work out exactly how a parameter should be handled,
# based on the +type+ and +coerce_with+ options that
# may be supplied to {Grape::Dsl::Parameters#requires}
# and {Grape::Dsl::Parameters#optional}. The main
# entry point for this process is {Types.build_coercer}.
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)
!primitive?(type) &&
!structure?(type) &&
!recognized?(type) &&
!special?(type) &&
type.respond_to?(:parse) &&
type.method(:parse).arity == 1
end
end
end
end
50 changes: 50 additions & 0 deletions lib/grape/validations/types/build_coercer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
module Grape
module Validations
module Types
# Work out the +Virtus::Attribute+ object to
# use for coercing strings to the given +type+.
# Coercion +method+ will be inferred if none is
# supplied.
#
# If a +Virtus::Attribute+ object already built
# with +Virtus::Attribute.build+ is supplied as
# the +type+ it will be returned and +method+
# will be ignored.
#
# See {CustomTypeCoercer} for further details
# about coercion and type-checking inference.
#
# @param type [Class] the type to which input strings
# should be coerced
# @param method [Class,#call] the coercion method to use
# @return [Virtus::Attribute] object to be used
# for coercion and type validation
def self.build_coercer(type, method = nil)
if type.is_a? Virtus::Attribute
# Accept pre-rolled virtus attributes without interference
type
else
converter_options = {
nullify_blank: true
}
conversion_type = type

# Use a special coercer for custom types and coercion methods.
if method || Types.custom?(type)
converter_options[:coercer] = Types::CustomTypeCoercer.new(type, method)

# Grape swaps in its own Virtus::Attribute implementations
# for certain special types that merit first-class support
# (but not if a custom coercion method has been supplied).
elsif Types.special?(type)
conversion_type = Types::SPECIAL[type]
end

# Virtus will infer coercion and validation rules
# for many common ruby types.
Virtus::Attribute.build(conversion_type, converter_options)
end
end
end
end
end
Loading