Stymied trying to extend AR with Plugin


#1

Lots of code below, since it’s usually the details that matter. Bit of a
long one, but I think it’s generic enough to be a good one for the
archives.

Experimenting with how to extend ActiveRecord. So I picked a little
example project of an input scrubber. The purpose for the code is to
automatically sanitize attribute data prior to it being saved. Don’t
focus on the scrubber itself, it’s just a arbitrary task for something
to play with (obviously it’s not a full scale sanitizer).

I have the Peepcode plugins book, and I have checked several blogs. I
have tinkered for hours now, trying simple Ruby-only steps to understand
the various ruby dynamics with extend, include, included, etc. So I’m
generally getting the concepts (and working simple examples), but
getting lost in the details of how to get this particular idea to work.

So let’s start with an implementation hard coded inside one model which
works just fine.

class Example < ActiveRecord::Base

@@scrub_attributes = [:author_name, :title, :content]

def before_save
self.attributes.each do |key,value|
if @@scrub_attributes.include?(key.to_sym)
scrub_value(value) if !value.nil?
end
end
end

def scrub_value(input_value)
input_value.gsub!(/"/,’"’)
input_value.gsub!(/`/,’’’)
input_value.gsub!(/’/,’’’)
input_value.gsub!(/(/,’(’)
input_value.gsub!(/)/,’)’)
input_value.gsub!(/</,’<’)
input_value.gsub!(/>/,’>’)
input_value.gsub!(/j\sa\sv\sa\ss\sc\sr\si\sp\st\s:/i,’’)
end

end

#================================================================

But, of course, what I really want for the model code API is this:

class Example < ActiveRecord::Base
scrub_attributes :author_name, :title, :content
end

So, I started work on a plugin. Below is where I am at so far. I have
boiled (or rather logged) the problem down to two details. I have marked
a few points of reference to talk about. Have a gander, and I’ll meet
you at the end of the code…

#================================================================

Plugin structure:
/plugins
init.rb
/ar_extensions
/lib
/gw
attribute_scrubber.rb

init.rb contains
ActiveRecord::Base.send(:include, GW::AttributeScrubber)

module GW
module AttributeScrubber

def self.included(base_module)
   class << base_module
     @@attributes_to_scrub = []
   end
  base_module.extend ClassMethods
  base_module.send(:include, InstanceMethods)  # <---- (1)
end

module ClassMethods
  def scrub_attributes(*attr_names)
    @@attributes_to_scrub = attr_names
  end
  def attributes_to_scrub
    return @@attributes_to_scrub
  end
end

module InstanceMethods
  def scrub_value(input_value)        # <---- (2)
    input_value.gsub!(/\"/,'&#34;')
    input_value.gsub!(/`/,'&#39;')
    input_value.gsub!(/\'/,'&#39;')
    input_value.gsub!(/\(/,'&#40;')
    input_value.gsub!(/\)/,'&#41;')
    input_value.gsub!(/</,'&lt;')
    input_value.gsub!(/>/,'&gt;')
    input_value.gsub!(/j\s*a\s*v\s*a\s*s\s*c\s*r\s*i\s*p\s*t\s*\:/i,'')
  end
end

def self.append_features(base_module)
  base_module.before_save do |model|
    model.attributes.each do |key,value|
      if @@attributes_to_scrub.include?(key.to_sym) #<--- (3)
        scrub_value(value) if !value.nil? # <---- (4)
      end
    end
  end
end

end
end

#================================================================

So, the problem I am having starts with (1). The PeepCode book says this
code should be

def self.included(base_module)
  base_module.extend ClassMethods
  base_module.include InstanceMethods
end

But that generates an error
included': private methodinclude’ called

Thus, I’m trying base_module.send(:include, InstanceMethods), but that
doesn’t seem to be working either because down at (4) I get errors that
method scrub_value doesn’t exist.

Before we get to (4) though, it turns out that at (3)
@@attributes_to_scrub, which I am expecting to be a class var for
ActiveRecord doesn’t exist for the before_save method. I discovered that
even though the @@attributes_to_scrub contained in scrub_attributes does
havethe values I expect, at (3) there are no values. Namespace problem?

I hard coded an array in place of the class var at (3), and then I get
the error that the method scrub_value doesn’t exist, so I know the
InstanceMethods are not being included, or at least not in the namespace
I’m expecting. But I can get some simple Ruby-only code to work this way
just fine. So, I’m confused about that.

Other than running into the snags at (3) and (4), it all seems to run.

Clues appreciated. Thanks.

– gw


#2

Greg W. wrote:

Lots of code below, since it’s usually the details that matter. Bit of a
long one, but I think it’s generic enough to be a good one for the
archives.

OK, nevermind – got it sorted out.

required this change:

def self.append_features(base_module)
base_module.before_create do |model|
model.attributes.each do |key,value|
if model.class.attributes_to_scrub.include?(key.to_sym)
model.scrub_value(value) if !value.nil?
end
end
end
end

– gw


#3

On Apr 11, 7:57 am, Greg W. removed_email_address@domain.invalid
wrote:

base_module.before_create do |model|
model.attributes.each do |key,value|
if model.class.attributes_to_scrub.include?(key.to_sym)
model.scrub_value(value) if !value.nil?
end
end
end
end

In case you’re curious why, that;s because blocks are closures - in
particular they remember self, so you were trying to call scrub_value
on the module, read that variable form the module etc.

Fred


#4

Frederick C. wrote:

On Apr 11, 7:57�am, Greg W. removed_email_address@domain.invalid
wrote:

� base_module.before_create do |model|
� � model.attributes.each do |key,value|
� � � if model.class.attributes_to_scrub.include?(key.to_sym)
� � � � model.scrub_value(value) if !value.nil?
� � � end
� � end
� end
end

In case you’re curious why, that;s because blocks are closures - in
particular they remember self, so you were trying to call scrub_value
on the module, read that variable form the module etc.

Thx. Always curious as to why! :slight_smile:

What was most curious is that the @@var created by the class << block is
different than the @@var in the scrub_attributes method. By using
model.class.attributes_to_scrub I don’t even need the class <<
statements anymore.

So, yeah, I need to do more atomic experiments to understand the scoping
of modules & classes. I thought modules didn’t hold vars, but I get they
hold at least class vars (or something else is going on I didn’t pick up
on).

– gw