Forum: Ruby on Rails Stymied trying to extend AR with Plugin

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
4a9cd711ccb80f1ab3ce17450c2857e4?d=identicon&s=25 Greg Willits (-gw-)
on 2009-04-11 08:08
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!(/\"/,'&#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

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

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 method `include' 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
4a9cd711ccb80f1ab3ce17450c2857e4?d=identicon&s=25 Greg Willits (-gw-)
on 2009-04-11 08:57
Greg Willits 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
81b61875e41eaa58887543635d556fca?d=identicon&s=25 Frederick Cheung (Guest)
on 2009-04-11 10:25
(Received via mailing list)
On Apr 11, 7:57 am, Greg Willits <rails-mailing-l...@andreas-s.net>
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
4a9cd711ccb80f1ab3ce17450c2857e4?d=identicon&s=25 Greg Willits (-gw-)
on 2009-04-11 11:24
Frederick Cheung wrote:
> On Apr 11, 7:57�am, Greg Willits <rails-mailing-l...@andreas-s.net>
> 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! :-)

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
This topic is locked and can not be replied to.