merb-slices 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Generators +4 -0
- data/LICENSE +20 -0
- data/README +102 -0
- data/Rakefile +65 -0
- data/TODO +0 -0
- data/lib/generators/base.rb +21 -0
- data/lib/generators/full.rb +21 -0
- data/lib/generators/templates/full/LICENSE +20 -0
- data/lib/generators/templates/full/README +170 -0
- data/lib/generators/templates/full/Rakefile +48 -0
- data/lib/generators/templates/full/TODO +15 -0
- data/lib/generators/templates/full/app/controllers/application.rb +5 -0
- data/lib/generators/templates/full/app/controllers/main.rb +7 -0
- data/lib/generators/templates/full/app/helpers/application_helper.rb +64 -0
- data/lib/generators/templates/full/app/views/layout/%symbol_name%.html.erb +16 -0
- data/lib/generators/templates/full/app/views/main/index.html.erb +1 -0
- data/lib/generators/templates/full/lib/%base_name%.rb +78 -0
- data/lib/generators/templates/full/lib/%base_name%/merbtasks.rb +166 -0
- data/lib/generators/templates/full/lib/%base_name%/slicetasks.rb +18 -0
- data/lib/generators/templates/full/public/javascripts/master.js +0 -0
- data/lib/generators/templates/full/public/stylesheets/master.css +2 -0
- data/lib/generators/templates/full/spec/%base_name%_spec.rb +130 -0
- data/lib/generators/templates/full/spec/controllers/main_spec.rb +61 -0
- data/lib/generators/templates/full/spec/spec_helper.rb +44 -0
- data/lib/generators/templates/full/stubs/app/controllers/application.rb +2 -0
- data/lib/generators/templates/full/stubs/app/controllers/main.rb +2 -0
- data/lib/generators/templates/thin/LICENSE +20 -0
- data/lib/generators/templates/thin/README +130 -0
- data/lib/generators/templates/thin/Rakefile +46 -0
- data/lib/generators/templates/thin/TODO +7 -0
- data/lib/generators/templates/thin/application.rb +36 -0
- data/lib/generators/templates/thin/lib/%base_name%.rb +93 -0
- data/lib/generators/templates/thin/lib/%base_name%/merbtasks.rb +106 -0
- data/lib/generators/templates/thin/lib/%base_name%/slicetasks.rb +18 -0
- data/lib/generators/templates/thin/public/javascripts/master.js +0 -0
- data/lib/generators/templates/thin/public/stylesheets/master.css +2 -0
- data/lib/generators/templates/thin/stubs/application.rb +9 -0
- data/lib/generators/templates/thin/views/layout/%symbol_name%.html.erb +16 -0
- data/lib/generators/templates/thin/views/main/index.html.erb +1 -0
- data/lib/generators/templates/very_thin/LICENSE +20 -0
- data/lib/generators/templates/very_thin/README +110 -0
- data/lib/generators/templates/very_thin/Rakefile +46 -0
- data/lib/generators/templates/very_thin/TODO +7 -0
- data/lib/generators/templates/very_thin/application.rb +36 -0
- data/lib/generators/templates/very_thin/lib/%base_name%.rb +89 -0
- data/lib/generators/templates/very_thin/lib/%base_name%/merbtasks.rb +106 -0
- data/lib/generators/templates/very_thin/lib/%base_name%/slicetasks.rb +18 -0
- data/lib/generators/thin.rb +21 -0
- data/lib/generators/very_thin.rb +21 -0
- data/lib/merb-slices.rb +126 -0
- data/lib/merb-slices/controller_mixin.rb +129 -0
- data/lib/merb-slices/merbtasks.rb +19 -0
- data/lib/merb-slices/module.rb +338 -0
- data/lib/merb-slices/module_mixin.rb +535 -0
- data/lib/merb-slices/router_ext.rb +75 -0
- data/spec/full_slice_generator_spec.rb +21 -0
- data/spec/merb-slice_spec.rb +7 -0
- data/spec/slice_generator_spec.rb +22 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/thin_slice_generator_spec.rb +21 -0
- data/spec/very_thin_slice_generator_spec.rb +21 -0
- metadata +157 -0
@@ -0,0 +1,338 @@
|
|
1
|
+
module Merb
|
2
|
+
module Slices
|
3
|
+
|
4
|
+
VERSION = "0.9.4"
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
# Retrieve a slice module by name
|
9
|
+
#
|
10
|
+
# @param <#to_s> The slice module to check for.
|
11
|
+
# @return <Module> The slice module itself.
|
12
|
+
def [](module_name)
|
13
|
+
Object.full_const_get(module_name.to_s) if exists?(module_name)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Helper method to transform a slice filename to a module Symbol
|
17
|
+
def filename2module(slice_file)
|
18
|
+
File.basename(slice_file, '.rb').gsub('-', '_').camel_case.to_sym
|
19
|
+
end
|
20
|
+
|
21
|
+
# Register a Slice by its gem/lib path for loading at startup
|
22
|
+
#
|
23
|
+
# This is referenced from gems/<slice-gem-x.x.x>/lib/<slice-gem>.rb
|
24
|
+
# Which gets loaded for any gem. The name of the file is used
|
25
|
+
# to extract the Slice module name.
|
26
|
+
#
|
27
|
+
# @param slice_file<String> The path of the gem 'init file'
|
28
|
+
# @param force<Boolean> Whether to overwrite currently registered slice or not.
|
29
|
+
#
|
30
|
+
# @return <Module> The Slice module that has been setup.
|
31
|
+
#
|
32
|
+
# @example Merb::Slices::register(__FILE__)
|
33
|
+
# @example Merb::Slices::register('/path/to/my-slice/lib/my-slice.rb')
|
34
|
+
def register(slice_file, force = true)
|
35
|
+
# do what filename2module does, but with intermediate variables
|
36
|
+
identifier = File.basename(slice_file, '.rb')
|
37
|
+
underscored = identifier.gsub('-', '_')
|
38
|
+
module_name = underscored.camel_case
|
39
|
+
slice_path = File.expand_path(File.dirname(slice_file) + '/..')
|
40
|
+
# check if slice_path exists instead of just the module name - more flexible
|
41
|
+
if !self.paths.include?(slice_path) || force
|
42
|
+
Merb.logger.info!("Registered slice '#{module_name}' located at #{slice_path}") if force
|
43
|
+
self.files[module_name] = slice_file
|
44
|
+
self.paths[module_name] = slice_path
|
45
|
+
slice_mod = setup_module(module_name)
|
46
|
+
slice_mod.identifier = identifier
|
47
|
+
slice_mod.identifier_sym = underscored.to_sym
|
48
|
+
slice_mod.root = slice_path
|
49
|
+
slice_mod.file = slice_file
|
50
|
+
slice_mod.registered
|
51
|
+
slice_mod
|
52
|
+
else
|
53
|
+
Merb.logger.info!("Already registered slice '#{module_name}' located at #{slice_path}")
|
54
|
+
Object.full_const_get(module_name)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Look for any slices in Merb.root / 'slices' (the default) or if given,
|
59
|
+
# Merb::Plugins.config[:merb_slices][:search_path] (String/Array)
|
60
|
+
def register_slices_from_search_path!
|
61
|
+
slice_files_from_search_path.each do |slice_file|
|
62
|
+
absolute_path = File.expand_path(slice_file)
|
63
|
+
Merb.logger.info!("Found slice '#{File.basename(absolute_path, '.rb')}' in search path at #{absolute_path.relative_path_from(Merb.root)}")
|
64
|
+
Merb::Slices::Loader.load_classes(absolute_path)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Unregister a Slice at runtime
|
69
|
+
#
|
70
|
+
# This clears the slice module from ObjectSpace and reloads the router.
|
71
|
+
# Since the router doesn't add routes for any disabled slices this will
|
72
|
+
# correctly reflect the app's routing state.
|
73
|
+
#
|
74
|
+
# @param slice_module<#to_s> The Slice module to unregister.
|
75
|
+
def unregister(slice_module)
|
76
|
+
if (slice = self[slice_module]) && self.paths.delete(module_name = slice.name)
|
77
|
+
slice.loadable_files.each { |file| Merb::Slices::Loader.remove_file file }
|
78
|
+
Object.send(:remove_const, module_name)
|
79
|
+
unless Object.const_defined?(module_name)
|
80
|
+
Merb.logger.info!("Unregistered slice #{module_name}")
|
81
|
+
Merb::Slices::Loader.reload_router!
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Activate a Slice module at runtime
|
87
|
+
#
|
88
|
+
# Looks for previously registered slices; then searches :search_path for matches.
|
89
|
+
#
|
90
|
+
# @param slice_module<#to_s> Usually a string of version of the slice module name.
|
91
|
+
def activate(slice_module)
|
92
|
+
unless slice_file = self.files[slice_module.to_s]
|
93
|
+
module_name_underscored = slice_module.to_s.snake_case.escape_regexp
|
94
|
+
module_name_dasherized = module_name_underscored.tr('_', '-').escape_regexp
|
95
|
+
regexp = Regexp.new(/\/(#{module_name_underscored}|#{module_name_dasherized})\/lib\/(#{module_name_underscored}|#{module_name_dasherized})\.rb$/)
|
96
|
+
slice_file = slice_files_from_search_path.find { |path| path.match(regexp) } # from search path(s)
|
97
|
+
end
|
98
|
+
activate_by_file(slice_file) if slice_file
|
99
|
+
rescue => e
|
100
|
+
Merb.logger.error!("Failed to activate slice #{slice_module} (#{e.message})")
|
101
|
+
end
|
102
|
+
|
103
|
+
# Register a Slice by its gem/lib init file path and activate it at runtime
|
104
|
+
#
|
105
|
+
# Normally slices are loaded using BootLoaders on application startup.
|
106
|
+
# This method gives you the possibility to add slices at runtime, all
|
107
|
+
# without restarting your app. Together with #deactivate it allows
|
108
|
+
# you to enable/disable slices at any time. The router is reloaded to
|
109
|
+
# incorporate any changes. Disabled slices will be skipped when
|
110
|
+
# routes are regenerated.
|
111
|
+
#
|
112
|
+
# @param slice_file<String> The path of the gem 'init file'
|
113
|
+
#
|
114
|
+
# @example Merb::Slices.activate_by_file('/path/to/gems/slice-name/lib/slice-name.rb')
|
115
|
+
def activate_by_file(slice_file)
|
116
|
+
Merb::Slices::Loader.load_classes(slice_file)
|
117
|
+
slice = register(slice_file, false) # just to get module by slice_file
|
118
|
+
slice.load_slice # load the slice
|
119
|
+
Merb::Slices::Loader.reload_router!
|
120
|
+
slice.init if slice.respond_to?(:init)
|
121
|
+
slice.activate if slice.respond_to?(:activate) && slice.routed?
|
122
|
+
slice
|
123
|
+
rescue
|
124
|
+
Merb::Slices::Loader.reload_router!
|
125
|
+
end
|
126
|
+
alias :register_and_load :activate_by_file
|
127
|
+
|
128
|
+
# Deactivate a Slice module at runtime
|
129
|
+
#
|
130
|
+
# @param slice_module<#to_s> The Slice module to unregister.
|
131
|
+
def deactivate(slice_module)
|
132
|
+
if slice = self[slice_module]
|
133
|
+
slice.deactivate if slice.respond_to?(:deactivate) && slice.routed?
|
134
|
+
unregister(slice)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Deactivate a Slice module at runtime by specifying its slice file
|
139
|
+
#
|
140
|
+
# @param slice_file<String> The Slice location of the slice init file to unregister.
|
141
|
+
def deactivate_by_file(slice_file)
|
142
|
+
if slice = self.slices.find { |s| s.file == slice_file }
|
143
|
+
deactivate(slice.name)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Reload a Slice at runtime
|
148
|
+
#
|
149
|
+
# @param slice_module<#to_s> The Slice module to reload.
|
150
|
+
def reload(slice_module)
|
151
|
+
if slice = self[slice_module]
|
152
|
+
deactivate slice.name
|
153
|
+
activate_by_file slice.file
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Reload a Slice at runtime by specifying its slice file
|
158
|
+
#
|
159
|
+
# @param slice_file<String> The Slice location of the slice init file to reload.
|
160
|
+
def reload_by_file(slice_file)
|
161
|
+
if slice = self.slices.find { |s| s.file == slice_file }
|
162
|
+
reload(slice.name)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Watch all specified search paths to dynamically load/unload slices at runtime
|
167
|
+
#
|
168
|
+
# If a valid slice is found it's automatically registered and activated;
|
169
|
+
# once a slice is removed (or renamed to not match the convention), it
|
170
|
+
# will be unregistered and deactivated. Runs in a Thread.
|
171
|
+
#
|
172
|
+
# @example Merb::BootLoader.after_app_loads { Merb::Slices.start_dynamic_loader! }
|
173
|
+
#
|
174
|
+
# @param interval<Numeric>
|
175
|
+
# The interval in seconds of checking the search path(s) for changes.
|
176
|
+
def start_dynamic_loader!(interval = nil)
|
177
|
+
DynamicLoader.start(interval)
|
178
|
+
end
|
179
|
+
|
180
|
+
# Stop watching search paths to dynamically load/unload slices at runtime
|
181
|
+
def stop_dynamic_loader!
|
182
|
+
DynamicLoader.stop
|
183
|
+
end
|
184
|
+
|
185
|
+
# @return <Hash>
|
186
|
+
# The configuration loaded from Merb.root / "config/slices.yml" or, if
|
187
|
+
# the load fails, an empty hash.
|
188
|
+
def config
|
189
|
+
@config ||= begin
|
190
|
+
empty_hash = Hash.new { |h,k| h[k] = {} }
|
191
|
+
File.exists?(Merb.root / "config" / "slices.yml") ? YAML.load(File.read(Merb.root / "config" / "slices.yml")) || empty_hash : empty_hash
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# All registered Slice modules
|
196
|
+
#
|
197
|
+
# @return <Array[Module]> A sorted array of all slice modules.
|
198
|
+
def slices
|
199
|
+
slice_names.map do |name|
|
200
|
+
Object.full_const_get(name) rescue nil
|
201
|
+
end.compact
|
202
|
+
end
|
203
|
+
|
204
|
+
# All registered Slice module names
|
205
|
+
#
|
206
|
+
# @return <Array[String]> A sorted array of all slice module names.
|
207
|
+
def slice_names
|
208
|
+
self.paths.keys.sort
|
209
|
+
end
|
210
|
+
|
211
|
+
# Check whether a Slice exists
|
212
|
+
#
|
213
|
+
# @param <#to_s> The slice module to check for.
|
214
|
+
def exists?(module_name)
|
215
|
+
slice_names.include?(module_name.to_s) && Object.const_defined?(module_name.to_s)
|
216
|
+
end
|
217
|
+
|
218
|
+
# A lookup for finding a Slice module's path
|
219
|
+
#
|
220
|
+
# @return <Hash> A Hash mapping module names to root paths.
|
221
|
+
# @note Whenever a slice is deactivated, its path is removed from the lookup.
|
222
|
+
def paths
|
223
|
+
@paths ||= {}
|
224
|
+
end
|
225
|
+
|
226
|
+
# A lookup for finding a Slice module's slice file path
|
227
|
+
#
|
228
|
+
# @return <Hash> A Hash mapping module names to slice files.
|
229
|
+
# @note This is unaffected by deactivating a slice; used to reload slices by name.
|
230
|
+
def files
|
231
|
+
@files ||= {}
|
232
|
+
end
|
233
|
+
|
234
|
+
# Iterate over all registered slices
|
235
|
+
#
|
236
|
+
# By default iterates alphabetically over all registered modules.
|
237
|
+
# If Merb::Plugins.config[:merb_slices][:queue] is set, only the
|
238
|
+
# defined modules are loaded in the given order. This can be
|
239
|
+
# used to selectively load slices, and also maintain load-order
|
240
|
+
# for slices that depend on eachother.
|
241
|
+
#
|
242
|
+
# @yield Iterate over known slices and pass in the slice module.
|
243
|
+
# @yieldparam module<Module> The Slice module.
|
244
|
+
def each_slice(&block)
|
245
|
+
loadable_slices = Merb::Plugins.config[:merb_slices].key?(:queue) ? Merb::Plugins.config[:merb_slices][:queue] : slice_names
|
246
|
+
loadable_slices.each do |module_name|
|
247
|
+
if mod = self[module_name]
|
248
|
+
block.call(mod)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# Slice file locations from all search paths; this default to host-app/slices.
|
254
|
+
#
|
255
|
+
# Look for any slices in those default locations or if given,
|
256
|
+
# Merb::Plugins.config[:merb_slices][:search_path] (String/Array).
|
257
|
+
# Specify files, glob patterns or paths containing slices.
|
258
|
+
def slice_files_from_search_path
|
259
|
+
search_paths = Array(Merb::Plugins.config[:merb_slices][:search_path] || [Merb.root / "slices"])
|
260
|
+
search_paths.inject([]) do |files, path|
|
261
|
+
# handle both Pathname and String
|
262
|
+
path = path.to_s
|
263
|
+
if File.file?(path) && File.extname(path) == ".rb"
|
264
|
+
files << path
|
265
|
+
elsif path.include?("*")
|
266
|
+
files += glob_search_path(path)
|
267
|
+
elsif File.directory?(path)
|
268
|
+
files += glob_search_path(path / "**/lib/*.rb")
|
269
|
+
end
|
270
|
+
files
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
private
|
275
|
+
|
276
|
+
# Prepare a module to be a proper Slice module
|
277
|
+
#
|
278
|
+
# @param module_name<#to_s> The name of the module to prepare
|
279
|
+
#
|
280
|
+
# @return <Module> The module that has been setup
|
281
|
+
def setup_module(module_name)
|
282
|
+
Object.make_module(module_name)
|
283
|
+
slice_mod = Object.full_const_get(module_name)
|
284
|
+
slice_mod.extend(ModuleMixin)
|
285
|
+
slice_mod
|
286
|
+
end
|
287
|
+
|
288
|
+
# Glob slice files
|
289
|
+
#
|
290
|
+
# @param glob_pattern<String> A glob path with pattern
|
291
|
+
# @return <Array> Valid slice file paths.
|
292
|
+
def glob_search_path(glob_pattern)
|
293
|
+
# handle both Pathname and String
|
294
|
+
glob_pattern = glob_pattern.to_s
|
295
|
+
Dir[glob_pattern].inject([]) do |files, libfile|
|
296
|
+
basename = File.basename(libfile, '.rb')
|
297
|
+
files << libfile if File.basename(File.dirname(File.dirname(libfile))) == basename
|
298
|
+
files
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
end
|
303
|
+
|
304
|
+
class DynamicLoader
|
305
|
+
|
306
|
+
cattr_accessor :lookup
|
307
|
+
|
308
|
+
def self.start(interval = nil)
|
309
|
+
self.lookup ||= Set.new(Merb::Slices.slice_files_from_search_path)
|
310
|
+
@thread = self.every(interval || Merb::Plugins.config[:merb_slices][:autoload_interval] || 1.0) do
|
311
|
+
current_files = Set.new(Merb::Slices.slice_files_from_search_path)
|
312
|
+
(current_files - self.lookup).each { |f| Merb::Slices.activate_by_file(f) }
|
313
|
+
(self.lookup - current_files).each { |f| Merb::Slices.deactivate_by_file(f) }
|
314
|
+
self.lookup = current_files
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def self.stop
|
319
|
+
@thread.exit if @thread.is_a?(Thread)
|
320
|
+
end
|
321
|
+
|
322
|
+
private
|
323
|
+
|
324
|
+
def self.every(seconds, &block)
|
325
|
+
Thread.abort_on_exception = true
|
326
|
+
Thread.new do
|
327
|
+
loop do
|
328
|
+
sleep(seconds)
|
329
|
+
block.call
|
330
|
+
end
|
331
|
+
Thread.exit
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
end
|
336
|
+
|
337
|
+
end
|
338
|
+
end
|
@@ -0,0 +1,535 @@
|
|
1
|
+
module Merb
|
2
|
+
module Slices
|
3
|
+
module ModuleMixin
|
4
|
+
|
5
|
+
def self.extended(slice_module)
|
6
|
+
slice_module.meta_class.module_eval do
|
7
|
+
attr_accessor :identifier, :identifier_sym, :root, :file
|
8
|
+
attr_accessor :routes, :named_routes
|
9
|
+
attr_accessor :description, :version, :author
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Stub that gets triggered when a slice has been registered.
|
14
|
+
#
|
15
|
+
# @note This is rarely needed but still provided for edge cases.
|
16
|
+
def registered; end
|
17
|
+
|
18
|
+
# Stub classes loaded hook - runs before LoadClasses BootLoader
|
19
|
+
# right after a slice's classes have been loaded internally.
|
20
|
+
def loaded; end
|
21
|
+
|
22
|
+
# Stub initialization hook - runs before AfterAppLoads BootLoader.
|
23
|
+
def init; end
|
24
|
+
|
25
|
+
# Stub activation hook - runs after AfterAppLoads BootLoader.
|
26
|
+
def activate; end
|
27
|
+
|
28
|
+
# Stub deactivation method - not triggered automatically.
|
29
|
+
def deactivate; end
|
30
|
+
|
31
|
+
# Stub to setup routes inside the host application.
|
32
|
+
def setup_router(scope); end
|
33
|
+
|
34
|
+
# Check if there have been any routes setup.
|
35
|
+
def routed?
|
36
|
+
self.routes && !self.routes.empty?
|
37
|
+
end
|
38
|
+
|
39
|
+
# Return a value suitable for routes/urls.
|
40
|
+
def to_param
|
41
|
+
self.identifier
|
42
|
+
end
|
43
|
+
|
44
|
+
# @param <Symbol> The configuration key.
|
45
|
+
# @return <Object> The configuration value.
|
46
|
+
def [](key)
|
47
|
+
self.config[key]
|
48
|
+
end
|
49
|
+
|
50
|
+
# @param <Symbol> The configuration key.
|
51
|
+
# @param <Object> The configuration value.
|
52
|
+
def []=(key, value)
|
53
|
+
self.config[key] = value
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return <Hash> The configuration for this slice.
|
57
|
+
def config
|
58
|
+
Merb::Slices::config[self.identifier_sym] ||= {}
|
59
|
+
end
|
60
|
+
|
61
|
+
# Load slice and it's classes located in the slice-level load paths.
|
62
|
+
#
|
63
|
+
# Assigns collected_slice_paths and collected_app_paths, then loads
|
64
|
+
# the collected_slice_paths and triggers the #loaded hook method.
|
65
|
+
def load_slice
|
66
|
+
# load application.rb (or similar) for thin slices
|
67
|
+
Merb::Slices::Loader.load_file self.dir_for(:application) if File.file?(self.dir_for(:application))
|
68
|
+
# assign all relevant paths for slice-level and app-level
|
69
|
+
self.collect_load_paths
|
70
|
+
# load all slice-level classes from paths
|
71
|
+
Merb::Slices::Loader.load_classes self.collected_slice_paths
|
72
|
+
# call hook if available
|
73
|
+
self.loaded if self.respond_to?(:loaded)
|
74
|
+
Merb.logger.info!("Loaded slice '#{self}' ...")
|
75
|
+
rescue => e
|
76
|
+
Merb.logger.warn!("Failed loading #{self} (#{e.message})")
|
77
|
+
end
|
78
|
+
|
79
|
+
# The slice-level load paths that have been used when the slice was loaded.
|
80
|
+
#
|
81
|
+
# This may be a subset of app_paths, which includes any path to look for.
|
82
|
+
#
|
83
|
+
# @return <Array[String]> load paths (with glob pattern)
|
84
|
+
def collected_slice_paths
|
85
|
+
@collected_slice_paths ||= []
|
86
|
+
end
|
87
|
+
|
88
|
+
# The app-level load paths that have been used when the slice was loaded.
|
89
|
+
#
|
90
|
+
# This may be a subset of app_paths, which includes any path to look for.
|
91
|
+
#
|
92
|
+
# @return <Array[String]> Application load paths (with glob pattern)
|
93
|
+
def collected_app_paths
|
94
|
+
@collected_app_paths ||= []
|
95
|
+
end
|
96
|
+
|
97
|
+
# Generate a url - takes the slice's :path_prefix into account.
|
98
|
+
#
|
99
|
+
# This is only relevant for default routes, as named routes are
|
100
|
+
# handled correctly without any special considerations.
|
101
|
+
#
|
102
|
+
# @param name<#to_sym,Hash> The name of the URL to generate.
|
103
|
+
# @param rparams<Hash> Parameters for the route generation.
|
104
|
+
#
|
105
|
+
# @return String The generated URL.
|
106
|
+
#
|
107
|
+
# @notes If a hash is used as the first argument, a default route will be
|
108
|
+
# generated based on it and rparams.
|
109
|
+
def url(name, rparams = {}, defaults = {})
|
110
|
+
defaults = rparams if name.is_a?(Hash) && defaults.empty?
|
111
|
+
rparams = name if name.is_a?(Hash)
|
112
|
+
|
113
|
+
if name.is_a?(Symbol)
|
114
|
+
raise "Named route not found: #{name}" unless self.named_routes[name]
|
115
|
+
uri = Merb::Router.generate(name, rparams, defaults)
|
116
|
+
else
|
117
|
+
defaults[:controller] = defaults[:controller].gsub(%r|^#{self.identifier_sym}/|, '') if defaults[:controller]
|
118
|
+
uri = Merb::Router.generate(name, rparams, defaults)
|
119
|
+
uri = self[:path_prefix] / uri unless self[:path_prefix].blank?
|
120
|
+
uri = "/#{uri}" unless uri[0,1] == '/'
|
121
|
+
end
|
122
|
+
|
123
|
+
Merb::Config[:path_prefix] ? Merb::Config[:path_prefix] + uri : uri
|
124
|
+
end
|
125
|
+
|
126
|
+
# The slice-level load paths to use when loading the slice.
|
127
|
+
#
|
128
|
+
# @return <Hash> The load paths which make up the slice-level structure.
|
129
|
+
def slice_paths
|
130
|
+
@slice_paths ||= Hash.new { [self.root] }
|
131
|
+
end
|
132
|
+
|
133
|
+
# The app-level load paths to use when loading the slice.
|
134
|
+
#
|
135
|
+
# @return <Hash> The load paths which make up the app-level structure.
|
136
|
+
def app_paths
|
137
|
+
@app_paths ||= Hash.new { [Merb.root] }
|
138
|
+
end
|
139
|
+
|
140
|
+
# @param *path<#to_s>
|
141
|
+
# The relative path (or list of path components) to a directory under the
|
142
|
+
# root of the application.
|
143
|
+
#
|
144
|
+
# @return <String> The full path including the root.
|
145
|
+
def root_path(*path) File.join(self.root, *path) end
|
146
|
+
|
147
|
+
# Retrieve the absolute path to a slice-level directory.
|
148
|
+
#
|
149
|
+
# @param type<Symbol> The type of path to retrieve directory for, e.g. :view.
|
150
|
+
#
|
151
|
+
# @return <String> The absolute path for the requested type.
|
152
|
+
def dir_for(type) self.slice_paths[type].first end
|
153
|
+
|
154
|
+
# @param type<Symbol> The type of path to retrieve glob for, e.g. :view.
|
155
|
+
#
|
156
|
+
# @return <String> The pattern with which to match files within the type directory.
|
157
|
+
def glob_for(type) self.slice_paths[type][1] end
|
158
|
+
|
159
|
+
# Retrieve the absolute path to a app-level directory.
|
160
|
+
#
|
161
|
+
# @param type<Symbol> The type of path to retrieve directory for, e.g. :view.
|
162
|
+
#
|
163
|
+
# @return <String> The directory for the requested type.
|
164
|
+
def app_dir_for(type) self.app_paths[type].first end
|
165
|
+
|
166
|
+
# @param type<Symbol> The type of path to retrieve glob for, e.g. :view.
|
167
|
+
#
|
168
|
+
# @return <String> The pattern with which to match files within the type directory.
|
169
|
+
def app_glob_for(type) self.app_paths[type][1] end
|
170
|
+
|
171
|
+
# Retrieve the relative path to a public directory.
|
172
|
+
#
|
173
|
+
# @param type<Symbol> The type of path to retrieve directory for, e.g. :view.
|
174
|
+
#
|
175
|
+
# @return <String> The relative path to the public directory for the requested type.
|
176
|
+
def public_dir_for(type)
|
177
|
+
dir = type.is_a?(Symbol) ? self.app_dir_for(type) : self.app_dir_for(:public) / type
|
178
|
+
dir = dir.relative_path_from(Merb.dir_for(:public)) rescue '.'
|
179
|
+
dir == '.' ? '/' : "/#{dir}"
|
180
|
+
end
|
181
|
+
|
182
|
+
# Construct a path relative to the public directory
|
183
|
+
#
|
184
|
+
# @param <Symbol> The type of component.
|
185
|
+
# @param *segments<Array[#to_s]> Path segments to append.
|
186
|
+
#
|
187
|
+
# @return <String>
|
188
|
+
# A path relative to the public directory, with added segments.
|
189
|
+
def public_path_for(type, *segments)
|
190
|
+
File.join(self.public_dir_for(type), *segments)
|
191
|
+
end
|
192
|
+
|
193
|
+
# Construct an app-level path.
|
194
|
+
#
|
195
|
+
# @param <Symbol> The type of component.
|
196
|
+
# @param *segments<Array[#to_s]> Path segments to append.
|
197
|
+
#
|
198
|
+
# @return <String>
|
199
|
+
# A path within the host application, with added segments.
|
200
|
+
def app_path_for(type, *segments)
|
201
|
+
prefix = type.is_a?(Symbol) ? self.app_dir_for(type) : self.app_dir_for(:root) / type
|
202
|
+
File.join(prefix, *segments)
|
203
|
+
end
|
204
|
+
|
205
|
+
# Construct a slice-level path.
|
206
|
+
#
|
207
|
+
# @param <Symbol> The type of component.
|
208
|
+
# @param *segments<Array[#to_s]> Path segments to append.
|
209
|
+
#
|
210
|
+
# @return <String>
|
211
|
+
# A path within the slice source (Gem), with added segments.
|
212
|
+
def slice_path_for(type, *segments)
|
213
|
+
prefix = type.is_a?(Symbol) ? self.dir_for(type) : self.dir_for(:root) / type
|
214
|
+
File.join(prefix, *segments)
|
215
|
+
end
|
216
|
+
|
217
|
+
# This is the core mechanism for setting up your slice-level layout.
|
218
|
+
#
|
219
|
+
# @param type<Symbol> The type of path being registered (i.e. :view)
|
220
|
+
# @param path<String> The full path
|
221
|
+
# @param file_glob<String>
|
222
|
+
# A glob that will be used to autoload files under the path. Defaults to "**/*.rb".
|
223
|
+
def push_path(type, path, file_glob = "**/*.rb")
|
224
|
+
enforce!(type => Symbol)
|
225
|
+
slice_paths[type] = [path, file_glob]
|
226
|
+
end
|
227
|
+
|
228
|
+
# Removes given types of application components
|
229
|
+
# from slice-level load path this slice uses for autoloading.
|
230
|
+
#
|
231
|
+
# @param *args<Array[Symbol]> Components names, for instance, :views, :models
|
232
|
+
def remove_paths(*args)
|
233
|
+
args.each { |arg| self.slice_paths.delete(arg) }
|
234
|
+
end
|
235
|
+
|
236
|
+
# This is the core mechanism for setting up your app-level layout.
|
237
|
+
#
|
238
|
+
# @param type<Symbol> The type of path being registered (i.e. :view)
|
239
|
+
# @param path<String> The full path
|
240
|
+
# @param file_glob<String>
|
241
|
+
# A glob that will be used to autoload files under the path. Defaults to "**/*.rb".
|
242
|
+
def push_app_path(type, path, file_glob = "**/*.rb")
|
243
|
+
enforce!(type => Symbol)
|
244
|
+
app_paths[type] = [path, file_glob]
|
245
|
+
end
|
246
|
+
|
247
|
+
# Removes given types of application components
|
248
|
+
# from app-level load path this slice uses for autoloading.
|
249
|
+
#
|
250
|
+
# @param *args<Array[Symbol]> Components names, for instance, :views, :models
|
251
|
+
def remove_app_paths(*args)
|
252
|
+
args.each { |arg| self.app_paths.delete(arg) }
|
253
|
+
end
|
254
|
+
|
255
|
+
# Return all *.rb files from valid component paths
|
256
|
+
#
|
257
|
+
# @return <Array> Full paths to loadable ruby files.
|
258
|
+
def loadable_files
|
259
|
+
app_components.inject([]) do |paths, type|
|
260
|
+
paths += Dir[dir_for(type) / '**/*.rb'] if slice_paths.key?(type)
|
261
|
+
paths += Dir[app_dir_for(type) / '**/*.rb'] if app_paths.key?(type)
|
262
|
+
paths
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Return all application path component types
|
267
|
+
#
|
268
|
+
# @return <Array[Symbol]> Component types.
|
269
|
+
def app_components
|
270
|
+
[:view, :model, :controller, :helper, :mailer, :part]
|
271
|
+
end
|
272
|
+
|
273
|
+
# Return all public path component types
|
274
|
+
#
|
275
|
+
# @return <Array[Symbol]> Component types.
|
276
|
+
def public_components
|
277
|
+
[:stylesheet, :javascript, :image]
|
278
|
+
end
|
279
|
+
|
280
|
+
# Return all path component types to mirror
|
281
|
+
#
|
282
|
+
# If config option :mirror is set return a subset, otherwise return all types.
|
283
|
+
#
|
284
|
+
# @return <Array[Symbol]> Component types.
|
285
|
+
def mirrored_components
|
286
|
+
all = slice_paths.keys
|
287
|
+
config[:mirror].is_a?(Array) ? config[:mirror] & all : all
|
288
|
+
end
|
289
|
+
|
290
|
+
# Return all application path component types to mirror
|
291
|
+
#
|
292
|
+
# @return <Array[Symbol]> Component types.
|
293
|
+
def mirrored_app_components
|
294
|
+
mirrored_components & app_components
|
295
|
+
end
|
296
|
+
|
297
|
+
# Return all public path component types to mirror
|
298
|
+
#
|
299
|
+
# @return <Array[Symbol]> Component types.
|
300
|
+
def mirrored_public_components
|
301
|
+
mirrored_components & public_components
|
302
|
+
end
|
303
|
+
|
304
|
+
# Return all slice files mapped from the source to their relative path
|
305
|
+
#
|
306
|
+
# @param type<Symbol> Which type to use; defaults to :root (all)
|
307
|
+
# @return <Array[Array]> An array of arrays [abs. source, relative dest.]
|
308
|
+
def manifest(type = :root)
|
309
|
+
files = if type == :root
|
310
|
+
Dir.glob(self.root / "**/*")
|
311
|
+
elsif slice_paths.key?(type)
|
312
|
+
glob = ((type == :view) ? view_templates_glob : glob_for(type) || "**/*")
|
313
|
+
Dir.glob(dir_for(type) / glob)
|
314
|
+
else
|
315
|
+
[]
|
316
|
+
end
|
317
|
+
files.map { |source| [source, source.relative_path_from(root)] }
|
318
|
+
end
|
319
|
+
|
320
|
+
# Clone all files from the slice to their app-level location; this will
|
321
|
+
# also copy /lib, causing merb-slices to pick up the slice there.
|
322
|
+
#
|
323
|
+
# @return <Array[Array]>
|
324
|
+
# Array of two arrays, one for all copied files, the other for overrides
|
325
|
+
# that may have been preserved to resolve collisions.
|
326
|
+
def clone_slice!
|
327
|
+
app_slice_root = app_dir_for(:root)
|
328
|
+
copied, duplicated = [], []
|
329
|
+
manifest.each do |source, relative_path|
|
330
|
+
mirror_file(source, app_slice_root / relative_path, copied, duplicated)
|
331
|
+
end
|
332
|
+
[copied, duplicated]
|
333
|
+
end
|
334
|
+
|
335
|
+
# Unpack a subset of files from the slice to their app-level location;
|
336
|
+
# this will also copy /lib, causing merb-slices to pick up the slice there.
|
337
|
+
#
|
338
|
+
# @return <Array[Array]>
|
339
|
+
# Array of two arrays, one for all copied files, the other for overrides
|
340
|
+
# that may have been preserved to resolve collisions.
|
341
|
+
#
|
342
|
+
# @note Files for the :stub component type are skipped.
|
343
|
+
def unpack_slice!
|
344
|
+
app_slice_root = app_dir_for(:root)
|
345
|
+
copied, duplicated = mirror_public!
|
346
|
+
manifest.each do |source, relative_path|
|
347
|
+
next unless unpack_file?(relative_path)
|
348
|
+
mirror_file(source, app_slice_root / relative_path, copied, duplicated)
|
349
|
+
end
|
350
|
+
[copied, duplicated]
|
351
|
+
end
|
352
|
+
|
353
|
+
# Copies all files from mirrored_components to their app-level location
|
354
|
+
#
|
355
|
+
# This includes application and public components.
|
356
|
+
#
|
357
|
+
# @return <Array[Array]>
|
358
|
+
# Array of two arrays, one for all copied files, the other for overrides
|
359
|
+
# that may have been preserved to resolve collisions.
|
360
|
+
def mirror_all!
|
361
|
+
mirror_files_for mirrored_components + mirrored_public_components
|
362
|
+
end
|
363
|
+
|
364
|
+
# Copies all files from the (optional) stubs directory to their app-level location
|
365
|
+
#
|
366
|
+
# @return <Array[Array]>
|
367
|
+
# Array of two arrays, one for all copied files, the other for overrides
|
368
|
+
# that may have been preserved to resolve collisions.
|
369
|
+
def mirror_stubs!
|
370
|
+
mirror_files_for :stub
|
371
|
+
end
|
372
|
+
|
373
|
+
# Copies all application files from mirrored_components to their app-level location
|
374
|
+
#
|
375
|
+
# @return <Array[Array]>
|
376
|
+
# Array of two arrays, one for all copied files, the other for overrides
|
377
|
+
# that may have been preserved to resolve collisions.
|
378
|
+
def mirror_app!
|
379
|
+
components = mirrored_app_components
|
380
|
+
components << :application if application_file?
|
381
|
+
mirror_files_for components
|
382
|
+
end
|
383
|
+
|
384
|
+
# Copies all application files from mirrored_components to their app-level location
|
385
|
+
#
|
386
|
+
# @return <Array[Array]>
|
387
|
+
# Array of two arrays, one for all copied files, the other for overrides
|
388
|
+
# that may have been preserved to resolve collisions.
|
389
|
+
def mirror_public!
|
390
|
+
mirror_files_for mirrored_public_components
|
391
|
+
end
|
392
|
+
|
393
|
+
# Copy files from specified component path types to their app-level location
|
394
|
+
#
|
395
|
+
# App-level overrides are preserved by creating duplicates before writing gem-level files.
|
396
|
+
# Because of their _override postfix they will load after their original implementation.
|
397
|
+
# In the case of views, this won't work, but the user override is preserved nonetheless.
|
398
|
+
#
|
399
|
+
# @return <Array[Array]>
|
400
|
+
# Array of two arrays, one for all copied files, the other for overrides
|
401
|
+
# that may have been preserved to resolve collisions.
|
402
|
+
#
|
403
|
+
# @note Only explicitly defined component paths will be taken into account to avoid
|
404
|
+
# cluttering the app's Merb.root by mistake - since undefined paths default to that.
|
405
|
+
def mirror_files_for(*types)
|
406
|
+
seen, copied, duplicated = [], [], [] # keep track of files we copied
|
407
|
+
types.flatten.each do |type|
|
408
|
+
if app_paths.key?(type) && (source_path = dir_for(type)) && (destination_path = app_dir_for(type))
|
409
|
+
manifest(type).each do |source, relative_path| # this relative path is not what we need here
|
410
|
+
next if seen.include?(source)
|
411
|
+
mirror_file(source, destination_path / source.relative_path_from(source_path), copied, duplicated)
|
412
|
+
seen << source
|
413
|
+
end
|
414
|
+
end
|
415
|
+
end
|
416
|
+
[copied, duplicated]
|
417
|
+
end
|
418
|
+
|
419
|
+
# This sets up the default slice-level and app-level structure.
|
420
|
+
#
|
421
|
+
# You can create your own structure by implementing setup_structure and
|
422
|
+
# using the push_path and push_app_paths. By default this setup matches
|
423
|
+
# what the merb-gen slice generator creates.
|
424
|
+
def setup_default_structure!
|
425
|
+
self.push_app_path(:root, Merb.root / 'slices' / self.identifier, nil)
|
426
|
+
|
427
|
+
self.push_path(:stub, root_path('stubs'), nil)
|
428
|
+
self.push_app_path(:stub, app_dir_for(:root), nil)
|
429
|
+
|
430
|
+
self.push_path(:application, root_path('app'), nil)
|
431
|
+
self.push_app_path(:application, app_dir_for(:root) / 'app', nil)
|
432
|
+
|
433
|
+
app_components.each do |component|
|
434
|
+
self.push_path(component, dir_for(:application) / "#{component}s")
|
435
|
+
self.push_app_path(component, app_dir_for(:application) / "#{component}s")
|
436
|
+
end
|
437
|
+
|
438
|
+
self.push_path(:public, root_path('public'), nil)
|
439
|
+
self.push_app_path(:public, Merb.dir_for(:public) / 'slices' / self.identifier, nil)
|
440
|
+
|
441
|
+
public_components.each do |component|
|
442
|
+
self.push_path(component, dir_for(:public) / "#{component}s", nil)
|
443
|
+
self.push_app_path(component, app_dir_for(:public) / "#{component}s", nil)
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
protected
|
448
|
+
|
449
|
+
# Collect slice-level and app-level load paths to load from.
|
450
|
+
#
|
451
|
+
# @param modify_load_path<Boolean>
|
452
|
+
# Whether to add certain paths to $LOAD_PATH; defaults to true.
|
453
|
+
# @param push_merb_path<Boolean>
|
454
|
+
# Whether to add app-level paths using Merb.push_path; defaults to true.
|
455
|
+
def collect_load_paths(modify_load_path = true, push_merb_path = true)
|
456
|
+
self.collected_slice_paths.clear; self.collected_app_paths.clear
|
457
|
+
self.slice_paths.each do |component, path|
|
458
|
+
if File.directory?(component_path = path.first)
|
459
|
+
$LOAD_PATH.unshift(component_path) if modify_load_path && component.in?(:model, :controller, :lib) && !$LOAD_PATH.include?(component_path)
|
460
|
+
# slice-level component load path - will be preceded by application/app/component - loaded next by Setup.load_classes
|
461
|
+
self.collected_slice_paths << path.first / path.last if path.last
|
462
|
+
# app-level component load path (override) path - loaded by BootLoader::LoadClasses
|
463
|
+
if (app_glob = self.app_glob_for(component)) && File.directory?(app_component_dir = self.app_dir_for(component))
|
464
|
+
self.collected_app_paths << app_component_dir / app_glob
|
465
|
+
Merb.push_path(:"#{self.name.snake_case}_#{component}", app_component_dir, app_glob) if push_merb_path
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
# Helper method to copy a source file to destination while resolving any conflicts.
|
472
|
+
#
|
473
|
+
# @param source<String> The source path.
|
474
|
+
# @param dest<String> The destination path.
|
475
|
+
# @param copied<Array> Keep track of all copied files - relative paths.
|
476
|
+
# @param duplicated<Array> Keep track of all duplicated files - relative paths.
|
477
|
+
# @param postfix<String> The postfix to use for resolving conflicting filenames.
|
478
|
+
def mirror_file(source, dest, copied = [], duplicated = [], postfix = '_override')
|
479
|
+
base, rest = split_name(source)
|
480
|
+
dst_dir = File.dirname(dest)
|
481
|
+
dup_path = dst_dir / "#{base}#{postfix}.#{rest}"
|
482
|
+
if File.file?(source)
|
483
|
+
FileUtils.mkdir_p(dst_dir) unless File.directory?(dst_dir)
|
484
|
+
if File.exists?(dest) && !File.exists?(dup_path) && !FileUtils.identical?(source, dest)
|
485
|
+
# copy app-level override to *_override.ext
|
486
|
+
FileUtils.copy_entry(dest, dup_path, false, false, true)
|
487
|
+
duplicated << dup_path.relative_path_from(Merb.root)
|
488
|
+
end
|
489
|
+
# copy gem-level original to location
|
490
|
+
if !File.exists?(dest) || (File.exists?(dest) && !FileUtils.identical?(source, dest))
|
491
|
+
FileUtils.copy_entry(source, dest, false, false, true)
|
492
|
+
copied << dest.relative_path_from(Merb.root)
|
493
|
+
end
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
# Predicate method to check if a file should be taken into account when unpacking files
|
498
|
+
#
|
499
|
+
# By default any public component paths and stubs are skipped; additionally you can set
|
500
|
+
# the :skip_files in the slice's config for other relative paths to skip.
|
501
|
+
#
|
502
|
+
# @param file<String> The relative path to test.
|
503
|
+
# @return <TrueClass,FalseClass> True if the file may be mirrored.
|
504
|
+
def unpack_file?(file)
|
505
|
+
@mirror_exceptions_regexp ||= begin
|
506
|
+
skip_paths = (mirrored_public_components + [:stub]).map { |type| dir_for(type).relative_path_from(self.root) }
|
507
|
+
skip_paths += config[:skip_files] if config[:skip_files].is_a?(Array)
|
508
|
+
Regexp.new("^(#{skip_paths.join('|')})")
|
509
|
+
end
|
510
|
+
not file.match(@mirror_exceptions_regexp)
|
511
|
+
end
|
512
|
+
|
513
|
+
# Predicate method to check if the :application component is a file
|
514
|
+
def application_file?
|
515
|
+
File.file?(dir_for(:application) / glob_for(:application))
|
516
|
+
end
|
517
|
+
|
518
|
+
# Glob pattern matching all valid template extensions
|
519
|
+
def view_templates_glob
|
520
|
+
"**/*.{#{Merb::Template.template_extensions.join(',')}}"
|
521
|
+
end
|
522
|
+
|
523
|
+
# Split a file name so a postfix can be inserted
|
524
|
+
#
|
525
|
+
# @return <Array[String]>
|
526
|
+
# The first element will be the name up to the first dot, the second will be the rest.
|
527
|
+
def split_name(name)
|
528
|
+
file_name = File.basename(name)
|
529
|
+
mres = /^([^\/\.]+)\.(.+)$/i.match(file_name)
|
530
|
+
mres.nil? ? [file_name, ''] : [mres[1], mres[2]]
|
531
|
+
end
|
532
|
+
|
533
|
+
end
|
534
|
+
end
|
535
|
+
end
|