开发者

Ruby structure for extendable handler/plugin architechture

I'm writing something that is a bit like Facebook's shared link preview.

I would like to make it easily extendable for new sites by just dropping in a new file for each new site I want to write a custom parser for. I have the basic idea of the design pattern figured out but don't have enough experience with modules to nail the details. I'm sure there are plenty of examples of something like this in other 开发者_运维百科projects.

The result should be something like this:

> require 'link'
=> true
> Link.new('http://youtube.com/foo').preview
=> {:title => 'Xxx', :description => 'Yyy', :embed => '<zzz/>' }
> Link.new('http://stackoverflow.com/bar').preview
=> {:title => 'Xyz', :description => 'Zyx' }

And the code would be something like this:

#parsers/youtube.rb
module YoutubeParser
  url_match /(youtube\.com)|(youtu.be)\//
  def preview
    get_stuff_using youtube_api
  end
end

#parsers/stackoverflow.rb
module SOFParser
  url_match /stachoverflow.com\//
  def preview
    get_stuff
  end
end

#link.rb
class Link
   def initialize(url)
     extend self with the module that has matching regexp
   end
end


# url_processor.rb
class UrlProcessor
  # registers url handler for given pattern
  def self.register_url pattern, &block
    @patterns ||= {}
    @patterns[pattern] = block
  end

  def self.process_url url
    _, handler = @patterns.find{|p, _| url =~ p}
    if handler
      handler.call(url)
    else
      {}
    end
  end
end

# plugins/so_plugin.rb
class SOPlugin
  UrlProcessor.register_url /stackoverflow\.com/ do |url|
    {:title => 'foo', :description => 'bar'}
  end
end

# plugins/youtube_plugin.rb
class YoutubePlugin
  UrlProcessor.register_url /youtube\.com/ do |url|
    {:title => 'baz', :description => 'boo'}
  end
end

p UrlProcessor.process_url 'http://www.stackoverflow.com/1234'
#=>{:title=>"foo", :description=>"bar"}
p UrlProcessor.process_url 'http://www.youtube.com/1234'
#=>{:title=>"baz", :description=>"boo"}
p UrlProcessor.process_url 'http://www.foobar.com/1234'
#=>{}

You just need to require every .rb from plugins directory.


If you're willing to take this approach you should probably scan the filed for the mathing string and then include the right one.

In the same situation I attempted a different approach. I'm extending the module with new methods, @@registering them so that I won't register two identically named methods. So far it works good, though the project I started is nowhere near leaving the specific domain of one tangled mess of a particular web-site.

This is the main file.

module Onigiri
  extend self
  @@registry ||= {}

  class OnigiriHandlerTaken < StandardError
    def description
      "There was an attempt to override registered handler. This usually indicates a bug in Onigiri."
    end
  end

  def clean(data, *params)
    dupe = Onigiri::Document.parse data
    params.flatten.each do |method|
      dupe = dupe.send(method) if @@registry[method]
    end
    dupe.to_html
  end

  class Document < Nokogiri::HTML::DocumentFragment
  end

  private

  def register_handler(name)
    unless @@registry[name]
      @@registry[name] = true
    else
      raise OnigiriHandlerTaken
    end
  end

end

And here's the extending file.

# encoding: utf-8
module Onigiri
  register_handler :fix_backslash
  class Document
    def fix_backslash
      dupe = dup
      attrset = ['src', 'longdesc', 'href', 'action']
      dupe.css("[#{attrset.join('], [')}]").each do |target|
        attrset.each do |attr|
          target[attr] = target[attr].gsub("\\", "/") if target[attr]
        end
      end
      dupe
    end
  end
end

Another way I see is to use a set of different (but behaviorally indistinguishable) classes with a simple decision making mechanism to call a right one. A simple hash that holds class names and corresponding url_matcher would probably suffice.

Hope this helps.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜