Skip to content
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

Support custom types #1039

Merged
merged 1 commit into from
Jun 24, 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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ Next Release

#### Features

* Your contribution here.
* [#1039](https://github.com/intridea/grape/pull/1039): Added support for custom parameter types - [@rnubel](https://github.com/rnubel).
Copy link
Member

Choose a reason for hiding this comment

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

Put back this line below yours ;)

* Your contribution here!

#### Fixes

Expand Down
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
- [Declared](#declared)
- [Include Missing](#include-missing)
- [Parameter Validation and Coercion](#parameter-validation-and-coercion)
- [Supported Parameter Types](#supported-parameter-types)
- [Custom Types](#custom-types)
- [Built-in Validators](#built-in-validators)
- [Namespace Validation and Coercion](#namespace-validation-and-coercion)
- [Custom Validators](#custom-validators)
Expand Down Expand Up @@ -730,6 +732,54 @@ params do
end
```

#### Supported Parameter Types

The following are all valid types, supported out of the box by Grape:

* Integer
* Float
* BigDecimal
* Numeric
* Date
* DateTime
* Time
* Boolean
* String
* Symbol
* Rack::Multipart::UploadedFile

#### Custom Types

Aside from the default set of supported types listed above, any class can be
used as a type so long as it defines a class-level `parse` method. This method
must take one string argument and return an instance of the correct type, or
raise an exception to indicate the value was invalid. E.g.,

```ruby
class Color
attr_reader :value
def initialize(color)
@value = color
end

def self.parse(value)
fail 'Invalid color' unless %w(blue red green).include?(value)
new(value)
end
end

# ...

params do
requires :color, type: Color, default: Color.new('blue')
end

get '/stuff' do
# params[:color] is already a Color.
params[:color].value
end
```
Copy link
Member

Choose a reason for hiding this comment

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

Run this "code" through RuboCop, just to keep things consistent, change " to ', add a space between methods, etc.


#### Validation of Nested Parameters

Parameters can be nested using `group` or by calling `requires` or `optional` with a block.
Expand Down
4 changes: 3 additions & 1 deletion lib/grape.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/hash/deep_merge'
require 'active_support/dependencies/autoload'
require 'grape/util/content_types'
require 'multi_json'
require 'multi_xml'
require 'virtus'
Expand Down Expand Up @@ -160,6 +159,9 @@ module Presenters
end
end

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

require 'grape/validations/validators/base'
require 'grape/validations/attributes_iterator'
require 'grape/validations/validators/allow_blank'
Expand Down
58 changes: 58 additions & 0 deletions lib/grape/util/parameter_types.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
module Grape
module ParameterTypes
# 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
]

# @param type [Class] type to check
# @returns [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

# @param type [Class] type to check
# @returns [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

# 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
# @returns [Boolean] whether or not the type can be used as a custom type
def self.custom_type?(type)
!primitive?(type) &&
!structure?(type) &&
type.respond_to?(:parse) &&
type.method(:parse).arity == 1
end
end
end
9 changes: 8 additions & 1 deletion lib/grape/validations/validators/coerce.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,14 @@ def coerce_value(type, val)
return val || Set.new if type == Set
return val || {} if type == Hash

converter = Virtus::Attribute.build(type)
# To support custom types that Virtus can't easily coerce, pass in an
# explicit coercer. Custom types must implement a `parse` class method.
converter_options = {}
if ParameterTypes.custom_type?(type)
converter_options[:coercer] = type.method(:parse)
Copy link
Member

Choose a reason for hiding this comment

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

Nitpick: if ParameterTypes knows custom_type? it should also have ParameterTypes.custom_type_parser_for(type) or something like that, otherwise there's an assumption that custom_type means parse exists in two places.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. The responsibility for knowing how to coerce a custom type should live in just one place. I'll put that refactoring into a separate PR.

end
Copy link
Member

Choose a reason for hiding this comment

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

I am surprised that RuboCop didn't flag this one, the if would go after the line. Maybe we autoignored it, if you don't mind looking into it for a separate PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What are you referring to? It looks like only a few cops are turned off and I don't think any of them apply here.

Copy link
Member

Choose a reason for hiding this comment

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

I'd have to dig :) I think it should tell you to do converter_options[:coercer] = type.method(:parse) if .... It's really nbd.


converter = Virtus::Attribute.build(type, converter_options)
converter.coerce(val)

# not the prettiest but some invalid coercion can currently trigger
Expand Down
54 changes: 54 additions & 0 deletions spec/grape/util/parameter_types_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
require 'spec_helper'

describe Grape::ParameterTypes do
class FooType
def self.parse(_)
end
end

class BarType
def self.parse
end
end

describe '::primitive?' do
[
Integer, Float, Numeric, BigDecimal,
Virtus::Attribute::Boolean, String, Symbol,
Date, DateTime, Time, Rack::Multipart::UploadedFile
].each do |type|
it "recognizes #{type} as a primitive" do
expect(described_class.primitive?(type)).to be_truthy
end
end

it 'identifies unknown types' do
expect(described_class.primitive?(Object)).to be_falsy
expect(described_class.primitive?(FooType)).to be_falsy
end
end

describe '::structure?' do
[
Hash, Array, Set
].each do |type|
it "recognizes #{type} as a structure" do
expect(described_class.structure?(type)).to be_truthy
end
end
end

describe '::custom_type?' do
it 'returns false if the type does not respond to :parse' do
expect(described_class.custom_type?(Object)).to be_falsy
end

it 'returns true if the type responds to :parse with one argument' do
expect(described_class.custom_type?(FooType)).to be_truthy
end

it 'returns false if the type\'s #parse method takes other than one argument' do
expect(described_class.custom_type?(BarType)).to be_falsy
end
end
end
29 changes: 29 additions & 0 deletions spec/grape/validations/params_scope_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,35 @@ def app
end
end

context 'when using custom types' do
class CustomType
attr_reader :value
def self.parse(value)
fail if value == 'invalid'
new(value)
end

def initialize(value)
@value = value
end
end

it 'coerces the parameter via the type\'s parse method' do
subject.params do
requires :foo, type: CustomType
end
subject.get('/types') { params[:foo].value }

get '/types', foo: 'valid'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('valid')

get '/types', foo: 'invalid'
expect(last_response.status).to eq(400)
expect(last_response.body).to match(/foo is invalid/)
end
end

context 'array without coerce type explicitly given' do
it 'sets the type based on first element' do
subject.params do
Expand Down