summaryrefslogtreecommitdiffstats
path: root/amarok/src/mediadevice/daap/mongrel/lib/gem_plugin.rb
blob: 1d86abf494ec7c757ab757754985e46d0a887e2b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
require 'singleton'
require 'rubygems'

# Implements a dynamic plugin loading, configuration, and discovery system
# based on RubyGems and a simple additional name space that looks like a URI.
#
# A plugin is created and put into a category with the following code:
#
#  class MyThing < GemPlugin::Plugin "/things"
#    ...
#  end
# 
# What this does is sets up your MyThing in the plugin registry via GemPlugin::Manager.
# You can then later get this plugin with GemPlugin::Manager.create("/things/mything")
# and can also pass in options as a second parameter.
#
# This isn't such a big deal, but the power is really from the GemPlugin::Manager.load
# method.  This method will go through the installed gems and require_gem any
# that depend on the gem_plugin RubyGem.  You can arbitrarily include or exclude
# gems based on what they also depend on, thus letting you load these gems when appropriate.
#
# Since this system was written originally for the Mongrel project that'll be the
# best example of using it.
#
# Imagine you have a neat plugin for Mongrel called snazzy_command that gives the
# mongrel_rails a new command snazzy (like:  mongrel_rails snazzy).  You'd like
# people to be able to grab this plugin if they want and use it, because it's snazzy.
#
# First thing you do is create a gem of your project and make sure that it depends
# on "mongrel" AND "gem_plugin".  This Q_SIGNALS to the GemPlugin system that this is
# a plugin for mongrel.
#
# Next you put this code into a file like lib/init.rb (can be anything really):
#
#  class Snazzy < GemPlugin::Plugin "/commands"
#    ...
#  end
#  
# Then when you create your gem you have the following bits in your Rakefile:
#
#  spec.add_dependency('mongrel', '>= 0.3.9')
#  spec.add_dependency('gem_plugin', '>= 0.1')
#  spec.autorequire = 'init.rb'
#
# Finally, you just have to now publish this gem for people to install and Mongrel
# will "magically" be able to install it.
#
# The "magic" part though is pretty simple and done via the GemPlugin::Manager.load
# method.  Read that to see how it is really done.
module GemPlugin

  EXCLUDE = true
  INCLUDE = false

  class PluginNotLoaded < StandardError; end

  
  # This class is used by people who use gem plugins (but don't necessarily make them)
  # to add plugins to their own systems.  It provides a way to load plugins, list them,
  # and create them as needed.
  #
  # It is a singleton so you use like this:  GemPlugins::Manager.instance.load
  class Manager
    include Singleton
    attr_reader :plugins
    attr_reader :gems
    

    def initialize
      # plugins that have been loaded
      @plugins = {}

      # keeps track of gems which have been loaded already by the manager *and*
      # where they came from so that they can be referenced later
      @gems = {}
    end


    # Responsible for going through the list of available gems and loading 
    # any plugins requested.  It keeps track of what it's loaded already
    # and won't load them again.
    #
    # It accepts one parameter which is a hash of gem depends that should include
    # or exclude a gem from being loaded.  A gem must depend on gem_plugin to be
    # considered, but then each system has to add it's own INCLUDE to make sure
    # that only plugins related to it are loaded.
    #
    # An example again comes from Mongrel.  In order to load all Mongrel plugins:
    #
    #  GemPlugin::Manager.instance.load "mongrel" => GemPlugin::INCLUDE
    #
    # Which will load all plugins that depend on mongrel AND gem_plugin.  Now, one
    # extra thing we do is we delay loading Rails Mongrel plugins until after rails
    # is configured.  Do do this the mongrel_rails script has:
    #
    # GemPlugin::Manager.instance.load "mongrel" => GemPlugin::INCLUDE, "rails" => GemPlugin::EXCLUDE
    # The only thing to remember is that this is saying "include a plugin if it
    # depends on gem_plugin, mongrel, but NOT rails".  If a plugin also depends on other
    # stuff then it's loaded just fine.  Only gem_plugin, mongrel, and rails are
    # ever used to determine if it should be included.
    #
    # NOTE: Currently RubyGems will run autorequire on gems that get required AND
    # on their dependencies.  This really messes with people running edge rails
    # since activerecord or other stuff gets loaded for just touching a gem plugin.
    # To prevent this load requires the full path to the "init.rb" file, which
    # avoids the RubyGems autorequire magic.
    def load(needs = {})
      sdir = File.join(Gem.dir, "specifications")
      gems = Gem::SourceIndex.from_installed_gems(sdir)
      needs = needs.merge({"gem_plugin" => INCLUDE})
      
      gems.each do |path, gem|
        # don't load gems more than once
        next if @gems.has_key? gem.name        
        check = needs.dup

        # rolls through the depends and inverts anything it finds
        gem.dependencies.each do |dep|
          # this will fail if a gem is depended more than once
          if check.has_key? dep.name
            check[dep.name] = !check[dep.name]
          end
        end
        
        # now since excluded gems start as true, inverting them
        # makes them false so we'll skip this gem if any excludes are found
        if (check.select {|name,test| !test}).length == 0
          # looks like no needs were set to false, so it's good
          gem_dir = File.join(Gem.dir, "gems", "#{gem.name}-#{gem.version}")
          require File.join(gem_dir, "lib", gem.name, "init.rb")
          @gems[gem.name] = gem_dir
        end
      end

      return nil
    end


    # Not necessary for you to call directly, but this is
    # how GemPlugin::Base.inherited actually adds a 
    # plugin to a category.
    def register(category, name, klass)
      @plugins[category] ||= {}
      @plugins[category][name.downcase] = klass
    end
   
 
    # Resolves the given name (should include /category/name) to
    # find the plugin class and create an instance.  You can
    # pass a second hash option that is then given to the Plugin 
    # to configure it.
    def create(name, options = {})
      last_slash = name.rindex("/")
      category = name[0 ... last_slash]
      plugin = name[last_slash .. -1]

      map = @plugins[category]
      if not map
        raise "Plugin category #{category} does not exist"
      elsif not map.has_key? plugin
        raise "Plugin #{plugin} does not exist in category #{category}"
      else
        map[plugin].new(options)
      end
    end
    

    # Simply says whether the given gem has been loaded yet or not.
    def loaded?(gem_name)
      @gems.has_key? gem_name
    end


    # GemPlugins can have a 'resources' directory which is packaged with them
    # and can hold any data resources the plugin may need.  The main problem
    # is that it's difficult to figure out where these resources are 
    # actually located on the file system.  The resource method tries to 
    # locate the real path for a given resource path.
    #
    # Let's say you have a 'resources/stylesheets/default.css' file in your
    # gem distribution, then finding where this file really is involves:
    #
    #   Manager.instance.resource("mygem", "/stylesheets/default.css")
    #
    # You either get back the full path to the resource or you get a nil
    # if it doesn't exist.
    #
    # If you request a path for a GemPlugin that hasn't been loaded yet
    # then it will throw an PluginNotLoaded exception.  The gem may be
    # present on your system in this case, but you just haven't loaded
    # it with Manager.instance.load properly.
    def resource(gem_name, path)
      if not loaded? gem_name
        raise PluginNotLoaded.new("Plugin #{gem_name} not loaded when getting resource #{path}")
      end
      
      file = File.join(@gems[gem_name], "resources", path)

      if File.exist? file
        return file
      else
        return nil
      end
    end

    
    # While Manager.resource will find arbitrary resources, a special
    # case is when you need to load a set of configuration defaults.
    # GemPlugin normalizes this to be if you have a file "resources/defaults.yaml"
    # then you'll be able to load them via Manager.config.
    #
    # How you use the method is you get the options the user wants set, pass
    # them to Manager.instance.config, and what you get back is a new Hash
    # with the user's settings overriding the defaults.
    #
    #   opts = Manager.instance.config "mygem", :age => 12, :max_load => .9
    #
    # In the above case, if defaults had {:age => 14} then it would be 
    # changed to 12.
    #
    # This loads the .yaml file on the fly every time, so doing it a 
    # whole lot is very stupid.  If you need to make frequent calls to
    # this, call it once with no options (Manager.instance.config) then
    # use the returned defaults directly from then on.
    def config(gem_name, options={})
      config_file = Manager.instance.resource(gem_name, "/defaults.yaml")
      if config_file
        begin
          defaults = YAML.load_file(config_file)
          return defaults.merge(options)
        rescue
          raise "Error loading config #{config_file} for gem #{gem_name}"
        end
      else
        return options
      end
    end
  end

  # This base class for plugins really does nothing
  # more than wire up the new class into the right category.
  # It is not thread-safe yet but will be soon.
  class Base
    
    attr_reader :options


    # See Mongrel::Plugin for an explanation.
    def Base.inherited(klass)
      name = "/" + klass.to_s.downcase
      Manager.instance.register(@@category, name, klass)
      @@category = nil
    end
    
    # See Mongrel::Plugin for an explanation.
    def Base.category=(category)
      @@category = category
    end

    def initialize(options = {})
      @options = options
    end

  end
  
  # This nifty function works with the GemPlugin::Base to give you
  # the syntax:
  #
  #  class MyThing < GemPlugin::Plugin "/things"
  #    ...
  #  end
  #
  # What it does is temporarily sets the GemPlugin::Base.category, and then
  # returns GemPlugin::Base.  Since the next immediate thing Ruby does is
  # use this returned class to create the new class, GemPlugin::Base.inherited
  # gets called.  GemPlugin::Base.inherited then uses the set category, class name,
  # and class to register the plugin in the right way.
  def GemPlugin::Plugin(c)
    Base.category = c
    Base
  end

end