solnic / virtus

[DISCONTINUED ] Attributes on Steroids for Plain Old Ruby Objects
MIT License
3.77k stars 228 forks source link

How to drop input attribute value if it cant be coerced? #262

Closed mainameiz closed 10 years ago

mainameiz commented 10 years ago

How to make this?

class Foo
  include Virtus.model

  attribute :bar, Integer
end

foo = Foo.new(bar: 'asd')
foo.bar.nil? # => true
solnic commented 10 years ago

You can use strict mode that will raise an error when something can't be coerced:

class Foo
  include Virtus.model(strict: true)

  attribute :bar, Integer
end

foo = Foo.new(bar: 'asd')
# => BOOM ERROR
mainameiz commented 10 years ago

I dont want exception to be raised, these exceptions need to be catched everywhere i use this object, so I want values that cant be coerced to be nil

mainameiz commented 10 years ago

I'm trying now to subclass from Virtus::Attribute::Coercer and reimplement #call method

solnic commented 10 years ago

Don't go this route, you can have a sanitizer that goes through all attributes and set nils everywhere where something was not coerced:

class Foo
  include Virtus.model

  attribute :bar, Integer
end

foo = Foo.new(bar: 'asd')

Foo.attribute_set.each do |attribute|
  foo[attribute.name] = nil unless attribute.value_coerced?(foo[attribute.name])
end
mainameiz commented 10 years ago

Thanks for example, but if Foo has other Virtus models as attributes this way becomes a hard way to solve the problem. Do you think about some sort of callbacks or hooks for coercion mechanism or setting to overwrite default Coercer class?

My solution:

    class Coercer < Virtus::Attribute::Coercer
      def call(value)
        coercers[value.class].public_send(method, value)
      rescue ::Coercible::UnsupportedCoercion
        nil
      end
    end

    class Virtus::Attribute
      def self.build_coercer(type, options = {})
        klass = options.fetch(:coercer_class) { Virtus::Attribute::Coercer }
        klass.new(type, options.fetch(:configured_coercer) { Virtus.coercer })
      end
    end

    class Foo
      include Virtus.model

      attribute :bar, Integer, coercer_class: Coercer
    end
solnic commented 10 years ago

Not really, recursion to the rescue:

class Foo
  include Virtus.model

  attribute :bar, Integer
end

class Bar
  include Virtus.model

  attribute :bar, Integer
  attribute :foo, Foo
end

object = Bar.new(bar: 'asd', foo: Foo.new(bar: 'asd'))

def sanitize(object)
  object.class.attribute_set.each do |attribute|
    value = object[attribute.name]

    if value.class.respond_to?(:attribute_set)
      sanitize(value)
    else
      object[attribute.name] = nil unless attribute.value_coerced?(value)
    end
  end

  object
end

puts sanitize(object).inspect

I would still highly recommend putting this sanitization outside of the virtus object layer. You're mixing too many concerns in one place not to mention really hacky approach with monkey-patching.

mainameiz commented 10 years ago

Ok

class Foo
  include Virtus.model

  attribute :bar, Integer

  def initialize(*)
    super
    self.class.sanitize(self)
  end

  def self.sanitize(object)
    ...
  end
end
mainameiz commented 10 years ago

Thanks for help! Awesome gem, good job and best wishes! :-)

solnic commented 10 years ago

Sure :)