Skip to content
This repository
Browse code

New configuration option config.plugin_paths which may be a single pa…

…th like the default 'vendor/plugins' or an array of paths: ['vendor/plugins', 'lib/plugins']. Plugins are discovered in nested paths, so you can organize your plugins directory as you like. Refactor load_plugin from load_plugins. Simplify initializer unit test. Closes #2757.

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@2904 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
commit 6c434e8b8e420c76f86ba6605e98426fddca5031 1 parent 5c1eb89
Jeremy Kemper authored November 07, 2005
6  railties/CHANGELOG
... ...
@@ -1,5 +1,11 @@
1 1
 *SVN*
2 2
 
  3
+* New configuration option config.plugin_paths which may be a single path like the default 'vendor/plugins' or an array of paths: ['vendor/plugins', 'lib/plugins'].  [Jeremy Kemper]
  4
+
  5
+* Plugins are discovered in nested paths, so you can organize your plugins directory as you like.  [Jeremy Kemper]
  6
+
  7
+* Refactor load_plugin from load_plugins.  #2757 [alex.r.moon@gmail.com]
  8
+
3 9
 * Make use of silence_stderr in script/lighttpd, script/plugin, and Rails::Info [Sam Stephenson]
4 10
 
5 11
 * Enable HTTP installation of plugins when svn isn't avaialable. Closes #2661. [Chad Fowler]
135  railties/lib/initializer.rb
... ...
@@ -1,4 +1,5 @@
1 1
 require 'logger'
  2
+require 'set'
2 3
 
3 4
 RAILS_ENV = (ENV['RAILS_ENV'] || 'development').dup unless defined?(RAILS_ENV)
4 5
 
@@ -22,7 +23,10 @@ module Rails
22 23
   class Initializer
23 24
     # The Configuration instance used by this Initializer instance.
24 25
     attr_reader :configuration
25  
-    
  26
+
  27
+    # The set of loaded plugins.
  28
+    attr_reader :loaded_plugins
  29
+
26 30
     # Runs the initializer. By default, this will invoke the #process method,
27 31
     # which simply executes all of the initialization routines. Alternately,
28 32
     # you can specify explicitly which initialization routine you want:
@@ -40,8 +44,9 @@ def self.run(command = :process, configuration = Configuration.new)
40 44
     # instance.
41 45
     def initialize(configuration)
42 46
       @configuration = configuration
  47
+      @loaded_plugins = Set.new
43 48
     end
44  
-    
  49
+
45 50
     # Sequentially step through all of the available initialization routines,
46 51
     # in order:
47 52
     #
@@ -120,32 +125,19 @@ def require_frameworks
120 125
     def load_framework_info
121 126
       require 'rails_info'
122 127
     end
123  
-    
124  
-    # Loads all plugins in the <tt>vendor/plugins</tt> directory. Each
125  
-    # subdirectory of <tt>vendor/plugins</tt> is inspected as follows:
  128
+
  129
+    # Loads all plugins in <tt>config.plugin_paths</tt>.  <tt>plugin_paths</tt>
  130
+    # defaults to <tt>vendor/plugins</tt> but may also be set to a list of
  131
+    # paths, such as
  132
+    #   config.plugin_paths = ['lib/plugins', 'vendor/plugins']
126 133
     #
127  
-    # * if the directory has a +lib+ subdirectory, add it to the load path
128  
-    # * if the directory contains an <tt>init.rb</tt> file, read it in and
129  
-    #   eval it.
  134
+    # Each plugin discovered in <tt>plugin_paths</tt> is initialized:
  135
+    # * add its +lib+ directory, if present, to the beginning of the load path
  136
+    # * evaluate <tt>init.rb</tt> if present
130 137
     #
131 138
     # After all plugins are loaded, duplicates are removed from the load path.
132 139
     def load_plugins
133  
-      config = configuration
134  
-
135  
-      Dir.glob("#{configuration.plugins_path}/*") do |directory|
136  
-        next if File.basename(directory)[0] == ?. || !File.directory?(directory)
137  
-
138  
-        if File.directory?("#{directory}/lib")
139  
-          $LOAD_PATH.unshift "#{directory}/lib"
140  
-        end
141  
-
142  
-        if File.exist?("#{directory}/init.rb")
143  
-          silence_warnings do
144  
-            eval(IO.read("#{directory}/init.rb"), binding)
145  
-          end
146  
-        end
147  
-      end
148  
-
  140
+      find_plugins(configuration.plugin_paths).each { |path| load_plugin path }
149 141
       $LOAD_PATH.uniq!
150 142
     end
151 143
 
@@ -260,8 +252,62 @@ def initialize_framework_settings
260 252
         end
261 253
       end
262 254
     end
  255
+
  256
+    protected
  257
+      # Return a list of plugin paths within base_path.  A plugin path is
  258
+      # a directory that contains either a lib directory or an init.rb file.
  259
+      # This recurses into directories which are not plugin paths, so you
  260
+      # may organize your plugins which the plugin path.
  261
+      def find_plugins(*base_paths)
  262
+        base_paths.flatten.inject([]) do |plugins, base_path|
  263
+          Dir.glob(File.join(base_path, '*')).each do |path|
  264
+            if plugin_path?(path)
  265
+              plugins << path
  266
+            elsif File.directory?(path)
  267
+              plugins += find_plugins(path)
  268
+            end
  269
+          end
  270
+          plugins
  271
+        end
  272
+      end
  273
+
  274
+      def plugin_path?(path)
  275
+        File.directory?(path) and (File.directory?(File.join(path, 'lib')) or File.file?(File.join(path, 'init.rb')))
  276
+      end
  277
+
  278
+      # Load the plugin at <tt>path</tt> unless already loaded.
  279
+      #
  280
+      # Each plugin is initialized:
  281
+      # * add its +lib+ directory, if present, to the beginning of the load path
  282
+      # * evaluate <tt>init.rb</tt> if present
  283
+      #
  284
+      # Returns <tt>true</tt> if the plugin is successfully loaded or
  285
+      # <tt>false</tt> if it is already loaded (similar to Kernel#require).
  286
+      # Raises <tt>LoadError</tt> if the plugin is not found.
  287
+      def load_plugin(path)
  288
+        name = File.basename(path)
  289
+        return false if loaded_plugins.include?(name)
  290
+
  291
+        # Catch nonexistent and empty plugins.
  292
+        raise LoadError, "No such plugin: #{path}" unless plugin_path?(path)
  293
+
  294
+        lib_path  = File.join(path, 'lib')
  295
+        init_path = File.join(path, 'init.rb')
  296
+        has_lib   = File.directory?(lib_path)
  297
+        has_init  = File.file?(init_path)
  298
+
  299
+        # Add lib to load path.
  300
+        $LOAD_PATH.unshift(lib_path) if has_lib
  301
+
  302
+        # Evaluate init.rb.
  303
+        silence_warnings { eval(IO.read(init_path), binding) } if has_init
  304
+
  305
+        # Add to set of loaded plugins.
  306
+        loaded_plugins << name
  307
+        true
  308
+      end
263 309
   end
264  
-  
  310
+
265 311
   # The Configuration class holds all the parameters for the Initializer and
266 312
   # ships with defaults that suites most Rails applications. But it's possible
267 313
   # to overwrite everything. Usually, you'll create an Configuration file
@@ -339,6 +385,10 @@ class Configuration
339 385
     # any method of +nil+. Set to +false+ for the standard Ruby behavior.
340 386
     attr_accessor :whiny_nils
341 387
     
  388
+    # The path to the root of the plugins directory. By default, it is in
  389
+    # <tt>vendor/plugins</tt>.
  390
+    attr_accessor :plugin_paths
  391
+
342 392
     # Create a new Configuration instance, initialized with the default
343 393
     # values.
344 394
     def initialize
@@ -351,8 +401,9 @@ def initialize
351 401
       self.cache_classes                = default_cache_classes
352 402
       self.breakpoint_server            = default_breakpoint_server
353 403
       self.whiny_nils                   = default_whiny_nils
  404
+      self.plugin_paths                 = default_plugin_paths
354 405
       self.database_configuration_file  = default_database_configuration_file
355  
-      
  406
+
356 407
       for framework in default_frameworks
357 408
         self.send("#{framework}=", OrderedOptions.new)
358 409
       end
@@ -368,15 +419,9 @@ def database_configuration
368 419
     # The path to the current environment's file (development.rb, etc.). By
369 420
     # default the file is at <tt>config/environments/#{environment}.rb</tt>.
370 421
     def environment_path
371  
-      "#{RAILS_ROOT}/config/environments/#{environment}.rb"
  422
+      "#{root_path}/config/environments/#{environment}.rb"
372 423
     end
373 424
 
374  
-    # The path to the root of the plugins directory. By default, it is in
375  
-    # <tt>vendor/plugins</tt>.
376  
-    def plugins_path
377  
-      "#{RAILS_ROOT}/vendor/plugins"
378  
-    end
379  
-    
380 425
     # Return the currently selected environment. By default, it returns the
381 426
     # value of the +RAILS_ENV+ constant.
382 427
     def environment
@@ -384,17 +429,21 @@ def environment
384 429
     end
385 430
 
386 431
     private
  432
+      def root_path
  433
+        ::RAILS_ROOT
  434
+      end
  435
+
387 436
       def default_frameworks
388 437
         [ :active_record, :action_controller, :action_view, :action_mailer, :action_web_service ]
389 438
       end
390 439
     
391 440
       def default_load_paths
392  
-        paths = ["#{RAILS_ROOT}/test/mocks/#{environment}"]
  441
+        paths = ["#{root_path}/test/mocks/#{environment}"]
393 442
 
394 443
         # Then model subdirectories.
395 444
         # TODO: Don't include .rb models as load paths
396  
-        paths.concat(Dir["#{RAILS_ROOT}/app/models/[_a-z]*"])
397  
-        paths.concat(Dir["#{RAILS_ROOT}/components/[_a-z]*"])
  445
+        paths.concat(Dir["#{root_path}/app/models/[_a-z]*"])
  446
+        paths.concat(Dir["#{root_path}/components/[_a-z]*"])
398 447
 
399 448
         # Followed by the standard includes.
400 449
         # TODO: Don't include dirs for frameworks that are not used
@@ -416,11 +465,11 @@ def default_load_paths
416 465
           vendor/rails/activerecord/lib
417 466
           vendor/rails/actionmailer/lib
418 467
           vendor/rails/actionwebservice/lib
419  
-        ).map { |dir| "#{RAILS_ROOT}/#{dir}" }.select { |dir| File.directory?(dir) }
  468
+        ).map { |dir| "#{root_path}/#{dir}" }.select { |dir| File.directory?(dir) }
420 469
       end
421 470
 
422 471
       def default_log_path
423  
-        File.join(RAILS_ROOT, 'log', "#{environment}.log")
  472
+        File.join(root_path, 'log', "#{environment}.log")
424 473
       end
425 474
       
426 475
       def default_log_level
@@ -428,15 +477,15 @@ def default_log_level
428 477
       end
429 478
       
430 479
       def default_database_configuration_file
431  
-        File.join(RAILS_ROOT, 'config', 'database.yml')
  480
+        File.join(root_path, 'config', 'database.yml')
432 481
       end
433 482
       
434 483
       def default_view_path
435  
-        File.join(RAILS_ROOT, 'app', 'views')
  484
+        File.join(root_path, 'app', 'views')
436 485
       end
437 486
       
438 487
       def default_controller_paths
439  
-        [ File.join(RAILS_ROOT, 'app', 'controllers'), File.join(RAILS_ROOT, 'components') ]
  488
+        [ File.join(root_path, 'app', 'controllers'), File.join(root_path, 'components') ]
440 489
       end
441 490
       
442 491
       def default_dependency_mechanism
@@ -454,6 +503,10 @@ def default_breakpoint_server
454 503
       def default_whiny_nils
455 504
         false
456 505
       end
  506
+
  507
+      def default_plugin_paths
  508
+        ["#{root_path}/vendor/plugins"]
  509
+      end
457 510
   end
458 511
 end
459 512
 
2  railties/test/fixtures/environment_with_constant.rb
... ...
@@ -1 +1 @@
1  
-SET_FROM_ENV = "success"
  1
+$initialize_test_set_from_env = "success"
2  railties/test/fixtures/plugins/default/stubby/init.rb
... ...
@@ -0,0 +1,2 @@
  1
+require 'stubby_mixin'
  2
+raise unless defined? StubbyMixin
2  railties/test/fixtures/plugins/default/stubby/lib/stubby_mixin.rb
... ...
@@ -0,0 +1,2 @@
  1
+module StubbyMixin
  2
+end
21  railties/test/initializer_test.rb
@@ -15,22 +15,19 @@ def initialize(envpath)
15 15
     def environment_path
16 16
       @envpath
17 17
     end
18  
-  end
19 18
 
20  
-  def setup
21  
-    Object.const_set(:RAILS_ROOT, "") rescue nil
22  
-  end
23  
-  
24  
-  def teardown
25  
-    Object.remove_const(:RAILS_ROOT) rescue nil
  19
+    protected
  20
+      def root_path
  21
+        File.dirname(__FILE__)
  22
+      end
26 23
   end
27  
-  
  24
+
28 25
   def test_load_environment_with_constant
29 26
     config = ConfigurationMock.new("#{File.dirname(__FILE__)}/fixtures/environment_with_constant.rb")
  27
+    assert_nil $initialize_test_set_from_env
30 28
     Rails::Initializer.run(:load_environment, config)
31  
-    assert Object.const_defined?(:SET_FROM_ENV)
32  
-    assert_equal "success", SET_FROM_ENV
  29
+    assert_equal "success", $initialize_test_set_from_env
33 30
   ensure
34  
-    Object.remove_const(:SET_FROM_ENV) rescue nil
  31
+    $initialize_test_set_from_env = nil
35 32
   end
36  
-end
  33
+end
72  railties/test/plugin_test.rb
... ...
@@ -0,0 +1,72 @@
  1
+$:.unshift File.dirname(__FILE__) + "/../lib"
  2
+$:.unshift File.dirname(__FILE__) + "/../../activesupport/lib"
  3
+
  4
+require 'test/unit'
  5
+require 'active_support'
  6
+require 'initializer'
  7
+
  8
+class PluginTest < Test::Unit::TestCase
  9
+  class TestConfig < Rails::Configuration
  10
+    protected
  11
+      def root_path
  12
+        File.dirname(__FILE__)
  13
+      end
  14
+  end
  15
+
  16
+  def setup
  17
+    @init = Rails::Initializer.new(TestConfig.new)
  18
+  end
  19
+
  20
+  def test_plugin_path?
  21
+    assert @init.send(:plugin_path?, "#{File.dirname(__FILE__)}/fixtures/plugins/default/stubby")
  22
+    assert !@init.send(:plugin_path?, "#{File.dirname(__FILE__)}/fixtures/plugins/default/empty")
  23
+    assert !@init.send(:plugin_path?, "#{File.dirname(__FILE__)}/fixtures/plugins/default/jalskdjflkas")
  24
+  end
  25
+
  26
+  def test_find_plugins
  27
+    base    = "#{File.dirname(__FILE__)}/fixtures/plugins"
  28
+    default = "#{base}/default"
  29
+    alt     = "#{base}/alternate"
  30
+    acts    = "#{default}/acts"
  31
+    assert_equal ["#{acts}/acts_as_chunky_bacon"], @init.send(:find_plugins, acts)
  32
+    assert_equal ["#{acts}/acts_as_chunky_bacon", "#{default}/stubby"], @init.send(:find_plugins, default).sort
  33
+    assert_equal ["#{alt}/a", "#{acts}/acts_as_chunky_bacon", "#{default}/stubby"], @init.send(:find_plugins, base).sort
  34
+  end
  35
+
  36
+  def test_load_plugin
  37
+    stubby = "#{File.dirname(__FILE__)}/fixtures/plugins/default/stubby"
  38
+    expected = Set.new(['stubby'])
  39
+
  40
+    assert @init.send(:load_plugin, stubby)
  41
+    assert_equal expected, @init.loaded_plugins
  42
+
  43
+    assert !@init.send(:load_plugin, stubby)
  44
+    assert_equal expected, @init.loaded_plugins
  45
+
  46
+    assert_raise(LoadError) { @init.send(:load_plugin, 'lakjsdfkasljdf') }
  47
+    assert_equal expected, @init.loaded_plugins
  48
+  end
  49
+
  50
+  def test_load_default_plugins
  51
+    assert_loaded_plugins %w(stubby acts_as_chunky_bacon), 'default'
  52
+  end
  53
+
  54
+  def test_load_alternate_plugins
  55
+    assert_loaded_plugins %w(a), 'alternate'
  56
+  end
  57
+
  58
+  def test_load_plugins_from_two_sources
  59
+    assert_loaded_plugins %w(a stubby acts_as_chunky_bacon), ['default', 'alternate']
  60
+  end
  61
+
  62
+  protected
  63
+    def assert_loaded_plugins(plugins, path)
  64
+      assert_equal Set.new(plugins), load_plugins(path)
  65
+    end
  66
+
  67
+    def load_plugins(*paths)
  68
+      @init.configuration.plugin_paths = paths.flatten.map { |p| "#{File.dirname(__FILE__)}/fixtures/plugins/#{p}" }
  69
+      @init.load_plugins
  70
+      @init.loaded_plugins
  71
+    end
  72
+end

0 notes on commit 6c434e8

Please sign in to comment.
Something went wrong with that request. Please try again.