-
-
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
Conversation
bb2d67c
to
b80344a
Compare
# params[:color] is already a Color. | ||
params[:color].value | ||
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.
Run this "code" through RuboCop, just to keep things consistent, change " to ', add a space between methods, etc.
This is great. Needs a CHANGELOG entry, please. I am not in love with the name That said, I think the constructor should also be the coercer. This avoids the whole interface problem and allows you to use pretty much any type, no? class Color
attr_reader :value
def initialize(value)
fail 'Invalid color' unless %w(blue red green).include?(value)
@value = value
end
end |
Thanks for the feedback! Using the initializer would be clean, but also a bit limiting. It's fine if you're creating the custom type from scratch for just this purpose, but it would restrict you if you wanted to use an existing class as a type. Suppose you already had a Color class which initializes with R, G, and B values, for example. Let me know what you think. If we go with the extra-class-method approach, I think |
b80344a
to
5f9143e
Compare
It could just be Monday, but I don't see why it wouldn't work with an existing class, care to provide an example? :) |
Maybe I'm misunderstanding, but suppose we already have a Color class used by the rest of our API, like so: class Color
def initialize(r, g, b)
@r, @g, @b = r, g, b
end
end The constructor takes three integer arguments, so it couldn't directly be used to coerce the value. However, if we add a class Color
def self.coerce(param)
r, g, b = param.unpack('A2A2A2').map(&:hex)
new(r, g, b)
end
end Then you could hit endpoints like |
5f9143e
to
81dd14a
Compare
In the example above you would still have to extend Some library gives you class Color
def initialize(r, g, b)
@r, @g, @b = r, g, b
end
def to_s
"#{@r}|#{@g}|#{@b}"
end
end you open it up: class Color
alias_method :_initialize, :initialize
def initialize(* args)
if args.length == 3
_initialize(*args)
elsif args.length == 1
@r, @g, @b = args.first.unpack('A2A2A2').map(&:hex)
else
fail 'Oh no.'
end
end
end And: 2.0.0-p598 :183 > Color.new(10, 20, 55)
=> #<Color:0x007f8e5595ceb0 @r=10, @g=20, @b=55>
2.0.0-p598 :184 > Color.new('FFCC11')
=> #<Color:0x007f8e55965718 @r=255, @g=204, @b=17> Now that I wrote it I think it's worse. Ruby actually has a way to deal with this for many times, eg. DateTime. I think the method should be called |
Yeah, I'll push up a change to make it Edit: This was complicated slightly by what I described above; since |
@@ -3,7 +3,7 @@ Next Release | |||
|
|||
#### Features | |||
|
|||
* Your contribution here. |
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 ;)
See comments above and squash your commits, please. |
17fe211
to
b7fa42c
Compare
Any class can now be used as a type for a parameter, so long as it defines a class-level `parse` method. This method should raise on invalid values and otherwise return the coerced value. Also, I added the Grape::ParameterTypes module, which serves right now only as a basis to determine if a given type is a custom type or not, but in the future could be used to actually validate the types that are given via the params DSL.
b7fa42c
to
4f5dcf1
Compare
@dblock changes made. I'm not entirely sure the Part of the problem is, Grape delegates coercion directly to Virtus, and Virtus' coercion logic is pretty complex -- for example, if you specify a type which has included |
converter_options = {} | ||
if ParameterTypes.custom_type?(type) | ||
converter_options[:coercer] = type.method(:parse) | ||
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.
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 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.
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.
I'd have to dig :) I think it should tell you to do converter_options[:coercer] = type.method(:parse) if ...
. It's really nbd.
This is great. If you want to address any of the above in a future PR, please feel free. Merging. |
This implements #993. Originally, I thought it'd be possible to do this simply by having the custom type extend Virtus::Attribute, but a) it turned out to not be that easy and b) I figured it'd be better to define a pattern for this that isn't directly coupled to Virtus.
So, my change allows you to take any class and coerce parameters into it. All that's required is that the class implement a
from_param
class method. This name is arbitrary, but I feel it matches up with the intent of the change... totally open to suggestions, though. As a quick example:Things to consider:
CoerceValidator#coerce_value
hides any errors inside the coercion method with a "is invalid" message. This is probably acceptable for a first pass, though.param_scope_spec
orcoerce_spec
? I'm leaning towards the latter, even though they're currently in the former.from_param
method?