1 module Radiant
2 class ExtensionPath
3 # This class holds information about extensions that may be loaded. It has two roles: to remember the
4 # location of the extension so that we don't have to search for it again, and to look within that path
5 # for significant application subdirectories.
6 #
7 # We can't just retrieve this information from the Extension class because the initializer sets up
8 # most of the application load_paths before plugins (including extensions) are loaded. You can think
9 # of this as a sort of pre-extension class preparing the way for extension loading.
10 #
11 # You can use instances of this class to retrieve information about a particular extension:
12 #
13 # ExtensionPath.new(:name, :path)
14 # ExtensionPath.find(:name) #=> ExtensionPath instance
15 # ExtensionPath.find(:name).plugin_paths #=> "path/vendor/plugins" if it exists and is a directory
16 # ExtensionPath.for(:name) #=> "path"
17 #
18 # The initializer calls class methods to get overall lists (in configured order) of enabled load paths:
19 #
20 # ExtensionPath.enabled #=> ["path", "path", "path", "path"]
21 # ExtensionPath.plugin_paths #=> ["path/vendor/plugins", "path/vendor/plugins"]
22
23 attr_accessor :name, :path
24 @@known_paths = {}
25
26 def initialize(options = {}) #:nodoc
27 @name, @path = options[:name], options[:path]
28 @@known_paths[@name.to_sym] = self
29 end
30
31 def required
32 File.join(path, "#{name}_extension")
33 end
34
35 def to_s
36 path
37 end
38
39 # Builds a new ExtensionPath object from the supplied path, working out the name of the extension by
40 # stripping the extra bits from radiant-something-extension-1.0.0 to leave just 'something'. The object
41 # is returned, and also remembered here for later use by the initializer (to find load paths) and the
42 # ExtensionLoader, to load and activate the extension.
43 #
44 # If two arguments are given, the second is taken to be the full extension name.
45 #
46 def self.from_path(path, name=nil)
47 name = path if name.blank?
48 name = File.basename(name).gsub(/^radiant-|-extension(-[\d\.a-z]+|-[a-z\d]+)*$/, '')
49 new(:name => name, :path => path)
50 end
51
52 # Forgets all recorded extension paths.
53 # Currently only used in testing.
54 #
55 def self.clear_paths!
56 @@known_paths = {}
57 end
58
59 # Returns a list of all the likely load paths found within this extension root. It includes all of these
60 # that exist and are directories:
61 #
62 # * path
63 # * path/lib
64 # * path/app/models
65 # * path/app/controllers
66 # * path/app/metal
67 # * path/app/helpers
68 # * path/test/helpers
69 #
70 # You can call the class method ExtensionPath.load_paths to get a flattened list of all the load paths in all the enabled extensions.
71 #
72 def load_paths
73 %w(lib app/models app/controllers app/metal app/helpers test/helpers).collect { |d| check_subdirectory(d) }.push(path).flatten.compact
74 end
75
76 # Returns a list of all the +vendor/plugin+ paths found within this extension root.
77 # Call the class method ExtensionPath.plugin_paths to get a list of the plugin paths found in all enabled extensions.
78 #
79 def plugin_paths
80 check_subdirectory("vendor/plugins")
81 end
82
83 # Returns a list of names of all the locale files found within this extension root.
84 # Call the class method ExtensionPath.locale_paths to get a list of the locale files found in all enabled extensions
85 # in reverse order so that locale definitions override one another correctly.
86 #
87 def locale_paths
88 if check_subdirectory("config/locales")
89 Dir[File.join("#{path}","config/locales","*.{rb,yml}")]
90 end
91 end
92
93 # Returns the app/helpers path if it is found within this extension root.
94 # Call the class method ExtensionPath.helper_paths to get a list of the helper paths found in all enabled extensions.
95 #
96 def helper_paths
97 check_subdirectory("app/helpers")
98 end
99
100 # Returns the app/models path if it is found within this extension root.
101 # Call the class method ExtensionPath.model_paths to get a list of the model paths found in all enabled extensions.
102 #
103 def model_paths
104 check_subdirectory("app/models")
105 end
106
107 # Returns the app/controllers path if it is found within this extension root.
108 # Call the class method ExtensionPath.controller_paths to get a list of the controller paths found in all enabled extensions.
109 #
110 def controller_paths
111 check_subdirectory("app/controllers")
112 end
113
114 # Returns the app/views path if it is found within this extension root.
115 # Call the class method ExtensionPath.view_paths to get a list of the view paths found in all enabled extensions
116 # in reverse order so that views override one another correctly.
117 #
118 def view_paths
119 check_subdirectory("app/views")
120 end
121
122 # Returns the app/metal path if it is found within this extension root.
123 # Call the class method ExtensionPath.metal_paths to get a list of the metal paths found in all enabled extensions.
124 #
125 def metal_paths
126 check_subdirectory("app/metal")
127 end
128
129 # Returns a list of all the rake task files found within this extension root.
130 #
131 def rake_task_paths
132 if check_subdirectory("lib/tasks")
133 Dir[File.join("#{path}","lib/tasks/**","*.rake")]
134 end
135 end
136
137 # Returns a list of extension subdirectories that should be marked for eager loading. At the moment that
138 # includes all the controller, model and helper paths. The main purpose here is to ensure that extension
139 # controllers are loaded before running cucumber features, and there may be a better way to achieve that.
140 #
141 # Call the class method ExtensionPath.eager_load_paths to get a list for all enabled extensions.
142 #
143 def eager_load_paths
144 [controller_paths, model_paths, helper_paths].flatten.compact
145 end
146
147 class << self
148 # Returns the ExtensionPath object for the given extension name.
149 #
150 def find(name)
151 raise LoadError, "Cannot return path for unknown extension: #{name}" unless @@known_paths[name.to_sym]
152 @@known_paths[name.to_sym]
153 end
154
155 # Returns the root path recorded for the given extension name.
156 #
157 def for(name)
158 find(name).path
159 end
160
161 # Returns a list of path objects for all the enabled extensions in the configured order.
162 # If a configured extension has not been found during initialization, a LoadError will be thrown here.
163 #
164 # Note that at this stage, in line with the usage of config.extensions = [], the extension names
165 # are being passed around as symbols.
166 #
167 def enabled
168 enabled_extensions = Radiant.configuration.enabled_extensions
169 @@known_paths.values_at(*enabled_extensions).compact
170 end
171
172 # Returns a list of the root paths to all the enabled extensions, in the configured order.
173 #
174 def enabled_paths
175 enabled.map(&:path)
176 end
177
178 [:load_paths, :plugin_paths, :helper_paths, :model_paths, :controller_paths, :eager_load_paths].each do |m|
179 define_method(m) do
180 enabled.map{|ep| ep.send(m)}.flatten.compact
181 end
182 end
183 [:locale_paths, :view_paths, :metal_paths, :rake_task_paths].each do |m|
184 define_method(m) do
185 enabled.map{|ep| ep.send(m)}.flatten.compact.reverse
186 end
187 end
188 end
189
190 private
191
192 # If the supplied path within the extension root exists and is a directory, its absolute path is returned. Otherwise, nil.
193 #
194 def check_subdirectory(subpath)
195 subdirectory = File.join(path, subpath)
196 subdirectory if File.directory?(subdirectory)
197 end
198
199 end
200 en