I'm having lots of fun with ActiveModel's serialization, specifically the tangled web of as_json
and serializable_hash
.
My app has a large collection of models that share behavior by including a module, we'll call it SharedBehavior
.
My team has decided we have a default format we want all these classes to follow when being cast to JSON (for rendering in a Rails app), but some of them should behave a little differently. Due to odd behavior in these two methods from the ActiveModel library, adding whitelisted or blacklisted attributes in the models themselves gets overridden by the method definition in this module, and then passed on to the super declarations in ActiveModel.
For this reason, I'd like this module to only apply its definition of these methods to models if they are not explicitly overridden in those models (in essence, take the module out of the ancestor chain for a few method calls), but I still need the shared behavior from this module.
I tried solving this by conditionally, dynamically applying the method on module inclusion in IRB:
class A
def foo
puts 'in A'
end
end
module D
def self.included(base)
unless base.instance_methods(false).include?(:foo)
define_method(:foo) do
puts 'in D'
super()
end
end
end
end
class B < A
include D
end
class C < A
include D
def foo
puts 'in C'
super
end
end
With this declaration, I expected the output of C.new.foo
to be
in C
in A
but it was instead
in C
in D
in A
My only other thought is to move this logic out into another module and include that module in every class (there are about 54 of them) that does not explicitly override this method, but there are a couple downsides to that:
- It introduces a bit of implicit coupling in the project that a new model include this module iff it does not want to override this method implementation
- The current implementation of these serialization methods in the module have to do with behavior and attributes established by that module, so I feel like it would be unintuitive to have a second module that knows about and depends on those implementation details of
SharedBehavior
, though the second module would have almost nothing to do with the first.
Can anyone else think of another solution, or maybe spot an oversight of mine in the code example above that would allow me to make a call in the included
hook? (I also tried switching the order in which the C
class defined the foo
method and included the D
module, but saw exactly the same behavior).
There are two tricky bugs here.
include D
before definingfoo
inC
, so when theincluded
hook is called,foo
won't be defined inbase
. You need toinclude D
at the end of the class.foo
inD
. So after includingD
inB
,D#foo
is defined, meaning it's still included inC
even if you fix the previous bug. You needbase
to be the receiver ofdefine_method
.But there's an interesting twist: fixing the second bug makes the first bug irrelevant. By defining
foo
inbase
directly, it will be overwritten by any later definitions. It would be like doingSo to summarize, you just need