Grape: Switch from virtus to dry-types

Created on 5 Apr 2016  路  9Comments  路  Source: ruby-grape/grape

I don't really know what you do with virtus (I think it's coercion) but I think it could be a good idea to replace virtus by dry-types (http://dry-rb.org/gems/dry-types/) for many reasons:

  1. This reddit post from Solnic, the virtus creator: https://www.reddit.com/r/ruby/comments/3sjb24/virtus_to_be_abandoned_by_its_creator/
  2. Performance (from the dry-types doc): "roughly 10-12x faster than Virtus"
discuss! feature request

Most helpful comment

Happy to see this proposal. I was also wondering if maybe you could leverage dry-validation dry-shema too.

All 9 comments

Thanks for sharing this @guizmaii! I read the post with great interest. I would be open to a PR that replaces Virtus with something else, we can look at the implications.

Happy to see this proposal. I was also wondering if maybe you could leverage dry-validation dry-shema too.

Very good, if possible, I want to get involved @dblock

Looking forward to pull requests @totothink.

Looks like this enhancement requires a lot of refactoring. Grape tied on Axiom::Types and Virtus coercion logic. It instantiates Virtus::Attribute objects, which I don't sure have Dry::Types equivalent. So to achieve success for this issue we need to completely refactor Grape::Validations module and it's ancestors.

require 'dry/schema'

module Validation
  class DSL
    attr_reader :schema

    OPTS_MAPPING = {
      regexp: -> v { { format?: v } },
      type: -> v { v.name.downcase.to_sym }
    }

    def initialize
      @schema = Dry::Schema::DSL.new(processor_type: Dry::Schema::Params)
    end

    def call
      schema.call
    end

    def requires(name, opts)
      schema.required(name).value(*convert_opts(opts))
      self
    end

    def optional(name, opts)
      schema.optional(name).value(*convert_opts(opts))
      self
    end

    private

    def convert_opts(opts)
      opts.each_with_object([]) do |(k, v), arr|
        arg = OPTS_MAPPING.fetch(k).(v)
        arr << arg
      end
    end
  end

  def params(&block)
    @schema =
      begin
        dsl = DSL.new
        dsl.instance_eval(&block)
        dsl.call
      end
  end

  def schema
    @schema
  end
end

class Grape
  extend Validation

  params do
    requires :id, type: Integer
    optional :text, type: String, regexp: /\A[a-z]+\z/
  end
end

puts Grape.schema.(id: '1', text: '123').inspect
#<Dry::Schema::Result{:id=>1, :text=>"123"} errors={:text=>["is in invalid format"]}>

puts Grape.schema.(id: '1', text: 'foo').inspect
#<Dry::Schema::Result{:id=>1, :text=>"foo"} errors={}>

馃憢 so I just wrote this, as a quick PoC. This uses dry-types under the hood for coercion.

as already mentioned above, dry-schema(that extracted from dry-validation) looks like something that is more suitable for grape needs(coercion/validation of input params). though, it should be checked if the interface of the library can be easily used without its DSL.

if something will be refactored, it would be nice to see a solution with an ability to disable/replace validation component at all. I see params block as "meta description" of a user input for a specific endpoint. this description can be used by different consumers(validation component/documentation component/tests generator component/etc). as the result, we should have single "cross-cutting" DSL(as we have now) which provides an endpoint schema to consumers(we do not have now). so I don't really like the snippet above, even if it is a POC :)

You may be interested then in the fact dry-schema exposes the definition via AST which can be reused for any purpose you mentioned

schema = Dry::Schema.Params do
  required(:user).type(:hash).schema do
    required(:email).type(:string)
    required(:age).type(:integer)

    required(:address).type(:hash).schema do
      required(:street).type(:string)
      required(:city).type(:string)
      required(:zipcode).type(:string)

      required(:location).type(:hash).schema do
        required(:lat).type(:float)
        required(:lng).type(:float)
      end
    end
  end
end
schema.to_ast
=> [:set,
 [[:and,
   [[:predicate, [:key?, [[:name, :user], [:input, Undefined]]]],
    [:key,
     [:user,
      [:and,
       [[:predicate, [:hash?, [[:input, Undefined]]]],
        [:set,
         [[:predicate, [:key?, [[:name, :email], [:input, Undefined]]]],
          [:predicate, [:key?, [[:name, :age], [:input, Undefined]]]],
          [:and,
           [[:predicate, [:key?, [[:name, :address], [:input, Undefined]]]],
            [:key,
             [:address,
              [:and,
               [[:predicate, [:hash?, [[:input, Undefined]]]],
                [:set,
                 [[:predicate, [:key?, [[:name, :street], [:input, Undefined]]]],
                  [:predicate, [:key?, [[:name, :city], [:input, Undefined]]]],
                  [:predicate, [:key?, [[:name, :zipcode], [:input, Undefined]]]],
                  [:and,
                   [[:predicate, [:key?, [[:name, :location], [:input, Undefined]]]],
                    [:key,
                     [:location,
                      [:and,
                       [[:predicate, [:hash?, [[:input, Undefined]]]],
                        [:set, [[:predicate, [:key?, [[:name, :lat], [:input, Undefined]]]], [:predicate, [:key?, [[:name, :lng], [:input, Undefined]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]

There's also another definition, with actual types

Hello community, you are welcome to review my PR replacing virtus with dry-types. :slightly_smiling_face: Later we might bring dry-schema as well, but it will require more work and it will bring lots of breaking changes. So, let's start with dry-types :wink:

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jellybob picture jellybob  路  4Comments

dblock picture dblock  路  6Comments

TangMonk picture TangMonk  路  8Comments

hoffm picture hoffm  路  5Comments

dblock picture dblock  路  7Comments