merb-slices 0.9.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|