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
Evaluate default procs in the context of the instance #217
Comments
It's intentional that the default values aren't evaluated in the instance's context. How would implicit dependencies between attributes be satisfied? For instance, if class Example
include Virtus.model
attribute :date_of_birth, Date, default: :parse_date
attribute :year, Integer, default: :default_year
attribute :month
attribute :day
def execute; end
private
def parse_date
Date.new(year, month, day)
end
def default_year
Date.today.year
end
end If you run that and only give Furthermore, since the individual components ( |
ActiveInteraction is capable of doing this. It's definitely verbose, but the release of v1.3 will help that (see #198). class Example < ActiveInteraction::Base
integer :year, :month, :day,
default: nil
date :birthday,
default: nil
# Calculate year, month, and day from birthday.
set_callback :validate, :after, -> {
return if ymd_valid? || !birthday_valid?
self.year = birthday.year
self.month = birthday.month
self.day = birthday.day
[:year, :month, :day].each { |k| errors.delete(k) }
}
# Calculate birthday from year, month, and day.
set_callback :validate, :after, -> {
return if birthday_valid? || !ymd_valid?
self.birthday = Date.new(year, month, day)
errors.delete(:birthday)
}
# Ensure the year, month, and day equal the birthday.
set_callback :validate, :after, -> {
return unless birthday_valid? && ymd_valid?
return if birthday == Date.new(year, month, day)
errors.add(:base, 'birthday must match year, month, and day')
}
# Ensure either the birthday or the year, month, and day were given.
set_callback :validate, :after, -> {
return if birthday_valid? || ymd_valid?
errors.add(:base, 'either birthday or year, month, and day must be given')
}
def execute
inputs
end
private
def ymd_valid?
year_valid? && month_valid? && day_valid?
end
def year_valid?
year? && !errors.include?(:year)
end
def month_valid?
month? && !errors.include?(:month)
end
def day_valid?
day? && !errors.include?(:day)
end
def birthday_valid?
birthday? && !errors.include?(:birthday)
end
end
Example.run!
# ActiveInteraction::InvalidInteractionError: either birthday or year, month, and day must be given
Example.run!(year: 2001, month: 2, day: 3)
# => {:year=>2001, :month=>2, :day=>3, :birthday=>#<Date: 2001-02-03 ((2451944j,0s,0n),+0s,2299161j)>}
Example.run!(birthday: Date.new(2002, 3, 4))
# => {:year=>2002, :month=>3, :day=>4, :birthday=>#<Date: 2002-03-04 ((2452338j,0s,0n),+0s,2299161j)>}
Example.run!(year: 2001, month: 2, day: 3, birthday: Date.new(2002, 3, 4))
# ActiveInteraction::InvalidInteractionError: birthday must match year, month, and day |
Okey, thanks for the feedback! Of course, this would require user to think about the order of dependencies. I guess When provided with both I'm not sure this example is a common use case for this pattern, though. I don't know, there may be better examples. It is just an idea to think about 😉 |
Thanks for bringing this to our attention! Considering the number of edge cases that need to be considered to handle even this simple example, I don't expect us to add this functionality to ActiveInteraction. We will keep it in mind, though. |
@aaronjensen is also interested in this feature. He opened a couple of pull requests (#221 & #222) for it. I'm reopening this for discussion. |
My main problem with this feature is that you can accomplish the same thing with callbacks. class A < ActiveInteraction::Base
string :x, default: -> { self.class.name }
def execute; inputs end
end
A.run! # => {:x=>"A"} class B < ActiveInteraction::Base
string :x, default: nil
set_callback :type_check, :after, -> { self.x = self.class.name }
def execute; inputs end
end
B.run! # => {:x=>"B"} It's a little more complicated to use callbacks, but I think it's worth it; you have to be explicit about when you want them evaluated (before or after type checking or validation). |
Unfortunately callbacks only work when you're calling |
That an interesting use case to consider. The only officially supported way to deal with interactions is to call Instead of using callbacks you could override the methods we define for you. class C < ActiveInteraction::Base
string :x, default: nil
alias_method :_x, :x
def x
@x ||= self.class.name
_x
end
def execute; inputs end
end
C.run! # => {:x=>"C"} Unfortunately that's a lot of boilerplate. A helper method could handle most of it. class ActiveInteraction::Base
def self.default(attribute, &block)
alias_method "_#{attribute}", attribute
define_method attribute do
unless instance_variable_get("@#{attribute}")
instance_variable_set("@#{attribute}", instance_eval(&block))
end
send("_#{attribute}")
end
end
end
class D < ActiveInteraction::Base
string :x, default: nil
default(:x) { p self; self.class.name }
def execute; inputs end
end
D.run! # => {:x=>"D"} |
Yeah that'd work. My method in that gist is a little bit more invasive/unstable to active_interaction changes. I'm ok w/ that though for the syntax elegance. I'm surprised (though not that surprised) that |
ActiveInteraction definitely grew out of our need for managing business logic in an API. I would love for it to be more generally useful in Rails apps, but I think that goal is a ways off. We made it to be basically Mutations + ActiveModel; extending into form objects makes a lot of sense. This change in particular can be implement in the backwards-compatible way (as I showed), meaning we could schedule it for 2.1 instead of 2.0. However if we wanted to support your way of doing things, I think we'd have to wait until 3.0. Edit: And then we could make form objects (and Rails) the focus of 3.0. |
you may be able to do it in a backwards compatible way by checking the arity of the proc and passing the model in if the arity is 1: Either way, sounds good, thanks. |
Ooh, I like that idea.That's a clean way to differentiate between normal procs and ones that need to be evaluated in a special context. string :x, default: 'normal string'
string :y, default: -> { 'lazy string' }
string :z, default: -> this { "#{this} string" } The only downside is that they wouldn't be able to use private methods. |
I would disagree that |
Ah, right you are. That's my bad. |
I would like to add this feature, but it will require a significant refactoring of ActiveInteraction's internals. #290 had the right idea, but it didn't work for hash inputs. I think this feature should be part of v3.0.0. You could argue that it could be released with a minor version bump, but it is technically changing the API we provide. |
I still want to add this feature. But it's going to be hard to implement. If anyone wants to take a crack at it, #290 is a good start. |
This has the potential to interact strangely with hash defaults. Since hash defaults have to either be hash :x, default: {} do
integer :a, default: 0
end
# x defaults to { a: 0 }
# This will not work.
hash :y, default: -> { x } do
integer :a
end
# This will work.
hash :z, default: {} do
integer :a, -> { x[:a] }
end That will quickly get annoying if you have a lot of fields in the hash. This isn't a problem per se, but it's yet another oddity when it comes to default values for hashes. |
Even though this is on the v3.0.0 milestone, I think it could be added to v2.2.0. We do not guarantee which binding will be used when evaluating defaults. In spite of that, it is always |
Evaluate default procs with the interaction's binding
Y'all should be happy to hear that this feature has been released as part of ActiveInteraction v2.2.0. Sorry it took so long 😄 |
Virtus allows to use a result of a method call as a default value, like this for example:
This is helpful, since this simple class can be initialized with both Date object or year/month/day triplet.
One could try to rewrite this class using ActiveIntraction the following way:
ActiveInteraction allows to use procs as default values, which is helpful for values like
Date.today
, but the example above blows up:The text was updated successfully, but these errors were encountered: