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

Creating custom validators is difficult #2012

Closed
stanhu opened this issue Mar 16, 2020 · 8 comments · Fixed by #2029
Closed

Creating custom validators is difficult #2012

stanhu opened this issue Mar 16, 2020 · 8 comments · Fixed by #2029

Comments

@stanhu
Copy link
Contributor

stanhu commented Mar 16, 2020

I'm attempting to port a custom validator to Grape v1.3.1:

requires :custom, type: ::API::Validations::Types::CustomObject, desc: 'This is a test'

At first, I tried to take https://github.com/ruby-grape/grape/blob/41ee988c06b87b036d12c4c7247d06b75bfa0b7f/lib/grape/validations/types/file.rb as an example:

module API
  module Validations
    module Types
      class CustomObject
        def coerce(input)
          input
        end

        def coerced?(value)
          value.is_a?(::CustomObject)
        end
      end
    end
  end
end

In Grape v1.1.0, this worked (ignore the coerced_value? vs.coerced? change and lack of inheritance of Virtus::Attribute) because BuildCoercer.create_coercer_instance would just fall through and create a Virtus Attribute: https://github.com/ruby-grape/grape/blob/v1.1.0/lib/grape/validations/types/build_coercer.rb#L71.

In Grape v1.3.1, with the switch to dry-types, all unknown types fall through to PrimitiveCoercer and fails:

  1. PrimitiveCoercer.new type, strict
    .
  2. This quietly fails validation because super in eventually calls @coercer[val] in .
  3. This fails because @coercer is nil.

To get around this, I saw the custom? check appears to look for self.parse:

type.respond_to?(:parse) &&
type.method(:parse).arity == 1
. I ended up with:

module API
  module Validations
    module Types
      class CustomObject
        def self.coerce(input)
          input
        end

        def self.coerced?(value)
          value.is_a?(::CustomObject)
        end

        def self.parse(value)
          value
        end
      end
    end
  end
end

This appears to work for my purposes, but it seems inconsistent (not to mention undocumented) compared to the built-in validators present in Grape.

Note that dry-types has a simple mechanism for checking types: https://dry-rb.org/gems/dry-types/master/custom-types. Ideally, we'd be able to pass in one of these custom types so we don't need all that code above, but this doesn't work either for the same reasons above:

requires :custom, type: DryTypes.Instance(Range), desc: 'This is a test'

@dnesteryuk My questions are:

  1. Should the Grape type validations look more consistent between Json, File, and custom ones (e.g. class vs. instance methods, self.parse method)?
  2. If so, how can we make them more consistent? Otherwise, is this just a documentation issue?
  3. Should Grape handle custom dry-types (e.g. https://dry-rb.org/gems/dry-types/1.2/custom-types) natively?
@stanhu stanhu changed the title Creating custom validators to work is difficult Creating custom validators is difficult Mar 16, 2020
@dnesteryuk
Copy link
Member

dnesteryuk commented Mar 16, 2020

@stanhu First of all there is documentation about the required parse method. Probably, we need to update internal classes, a PR is welcome 🙂

I know that Dry-types supports custom types, however, I don't think we need to couple Grape and Dry-types that match. Maintenance of Virtus was dropped, we don't know what might happen to Dry-Types in the future. Also, there are differences in coercing, you might've seen this method, so custom types maintained by Dry-types could bring breaking changes. In general, what can be simpler than

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

A simple plain Ruby class which everyone can understand without checking documentation. 😉

@dblock
Copy link
Member

dblock commented Mar 16, 2020

You could roll out a grape-dry-types gem that makes the connection, even if trivial.

@stanhu
Copy link
Contributor Author

stanhu commented Mar 16, 2020

@dnesteryuk Ah, thank you! I don't know how I missed that block of documentation. I'll look at improving the internal classes to reflect this.

@stanhu
Copy link
Contributor Author

stanhu commented Mar 17, 2020

@dnesteryuk Actually, the example above doesn't work because coerced? has to return true as well:

return InvalidValue.new unless coerced?(coerced_val)

I think we need to update the documentation to reflect that? Something like:

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

  def self.parse(value)
    fail 'Invalid color' unless self.coerced?(value)
    new(value)
  end

  def self.coerced?(value)
    %w(blue red green).include?(value)
  end
end

@stanhu
Copy link
Contributor Author

stanhu commented Mar 17, 2020

I'll revise my statement because it's not right. dry-types has stronger type checking now, so if the parse returns an object other than the type specified in the API, then it will be invalid. For example:

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

  def self.parse(value)
    fail 'Invalid color' unless self.parsed?(value)
    new SomeThing(value)
  end

  def self.parsed?(value)
    %w(blue red green).include?(value)
  end
end

That's because the following block comes into play:

if type.respond_to? :coerced?
type.method :coerced?
elsif type.respond_to? :parsed?
type.method :parsed?
elsif type.respond_to? :call
# Arbitrary proc passed for type validation.
# Note that this will fail unless a method is also
# passed, or if the type also implements a parse() method.
type
elsif type.is_a?(Enumerable)
->(value) { value.respond_to?(:all?) && value.all? { |item| item.is_a? type[0] } }
else
# By default, do a simple type check
->(value) { value.is_a? type }
end

@dblock
Copy link
Member

dblock commented Mar 27, 2020

What do you think we shall we do with this issue, @stanhu?

stanhu added a commit to stanhu/grape that referenced this issue Mar 27, 2020
* Provide an example for taking an existing type and porting it to
  the dry-types. This would have saved me some time.

* Removed examples of using `Virtus.model`. It seems confusing to
  support dry-types and Virtus at the same time.

Closes ruby-grape#2012
stanhu added a commit to stanhu/grape that referenced this issue Mar 27, 2020
* Provide an example for taking an existing type and porting it to
  the dry-types. This would have saved me some time.

* Removed examples of using `Virtus.model`. It seems confusing to
  support dry-types and Virtus at the same time.

Closes ruby-grape#2012
@stanhu
Copy link
Contributor Author

stanhu commented Mar 27, 2020

@dblock I submitted #2029. Let me know if that makes sense.

Would you mind releasing 1.3.2 soon? Thanks!

@dblock
Copy link
Member

dblock commented Mar 28, 2020

Thank you. Opened #2030 for a release, add +1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants