adzap/validates_timeliness — pull request #107 by jimherz (2013-09-27 23:26:00+00:00)
Title: Fix STI-related bug in validates_timeliness exposed in Rails 3.2

This pull request addresses a previously reported issue: https://github.com/adzap/validates_timeliness/issues/88

The cause is quite complicated, but I'll try to keep my explanation brief.

The issue is exposed by changes in ActiveRecord::AttributeMethods#define_attribute_methods between rails 3.1 and 3.2. This method now calls itself on an AR subclass's superclass. The effect of this is that ValidatesTimeliness::AttributeMethods#define_attribute_methods is called on all levels of an STI hierarchy. Hence, define_timeliness_methods is called on all levels of an STI hierarchy. And herein lies the problem:

```ruby
module ValidatesTimeliness
module AttributeMethods
extend ActiveSupport::Concern

included do
class_attribute :timeliness_validated_attributes
self.timeliness_validated_attributes = []
end
...

end
```

As you can see above, timeliness_validated_attributes is defined as a class_attribute, which means that any subclasses will "inherit" a copy of it, including the values added to it by the superclass. For instance,

```ruby
class Author < ActiveRecord::Base
validates_date :published_on
validates_date :famous_on
end

class BadAuthor < Author
validates_date :died_on
end

Loading development environment (Rails 3.2.14)
1.9.3-p448 :001 > Author.timeliness_validated_attributes
=> [:published_on, :famous_on]
1.9.3-p448 :002 > BadAuthor.timeliness_validated_attributes
=> [:published_on, :famous_on, :died_on]
```

Validates_timeliness generates "#{attr_name}=" methods for each of the attributes in a class's timeliness_validated_attributes. The methods are defined in an anonymous module which is inserted into the corresponding class's ancestor chain.

In the context of our STI example, this effectively means that the same "published_on=" method is defined twice in two separate modules in BadAuthor's heirarchy. Via super, these calls are chained together with the result of the first being the argument to the next. When you combine this with :use_plugin_parser set to true, it messes up the @timeliness_cache and, hence, the published_on_before_type_cast, and, hence, the validation of an invalid date...

The solution is to use a class level instance variable instead of a class_attribute. This way, each variable with a validates_date declaration (or validates_time, etc.) is only tracked and acted upon once via the class it is declared in.

Here is the change:

```ruby
module ValidatesTimeliness
module AttributeMethods
extend ActiveSupport::Concern

included do
class << self
attr_accessor :timeliness_validated_attributes
end
self.timeliness_validated_attributes = []
end
...

end
```

And now things look right:

```ruby
Loading development environment (Rails 3.2.14)
1.9.3-p448 :001 > Author.timeliness_validated_attributes
=> [:published_on, :famous_on]
1.9.3-p448 :002 > BadAuthor.timeliness_validated_attributes
=> [:died_on]
```

Phew! Long explanation for such a small change :)