-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Support custom types #1039
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
|
@@ -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 | ||
``` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick: if ParameterTypes knows There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = Virtus::Attribute.build(type, converter_options) | ||
converter.coerce(val) | ||
|
||
# not the prettiest but some invalid coercion can currently trigger | ||
|
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 |
There was a problem hiding this comment.
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 ;)