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.
Files changed (62) hide show
  1. data/Generators +4 -0
  2. data/LICENSE +20 -0
  3. data/README +102 -0
  4. data/Rakefile +65 -0
  5. data/TODO +0 -0
  6. data/lib/generators/base.rb +21 -0
  7. data/lib/generators/full.rb +21 -0
  8. data/lib/generators/templates/full/LICENSE +20 -0
  9. data/lib/generators/templates/full/README +170 -0
  10. data/lib/generators/templates/full/Rakefile +48 -0
  11. data/lib/generators/templates/full/TODO +15 -0
  12. data/lib/generators/templates/full/app/controllers/application.rb +5 -0
  13. data/lib/generators/templates/full/app/controllers/main.rb +7 -0
  14. data/lib/generators/templates/full/app/helpers/application_helper.rb +64 -0
  15. data/lib/generators/templates/full/app/views/layout/%symbol_name%.html.erb +16 -0
  16. data/lib/generators/templates/full/app/views/main/index.html.erb +1 -0
  17. data/lib/generators/templates/full/lib/%base_name%.rb +78 -0
  18. data/lib/generators/templates/full/lib/%base_name%/merbtasks.rb +166 -0
  19. data/lib/generators/templates/full/lib/%base_name%/slicetasks.rb +18 -0
  20. data/lib/generators/templates/full/public/javascripts/master.js +0 -0
  21. data/lib/generators/templates/full/public/stylesheets/master.css +2 -0
  22. data/lib/generators/templates/full/spec/%base_name%_spec.rb +130 -0
  23. data/lib/generators/templates/full/spec/controllers/main_spec.rb +61 -0
  24. data/lib/generators/templates/full/spec/spec_helper.rb +44 -0
  25. data/lib/generators/templates/full/stubs/app/controllers/application.rb +2 -0
  26. data/lib/generators/templates/full/stubs/app/controllers/main.rb +2 -0
  27. data/lib/generators/templates/thin/LICENSE +20 -0
  28. data/lib/generators/templates/thin/README +130 -0
  29. data/lib/generators/templates/thin/Rakefile +46 -0
  30. data/lib/generators/templates/thin/TODO +7 -0
  31. data/lib/generators/templates/thin/application.rb +36 -0
  32. data/lib/generators/templates/thin/lib/%base_name%.rb +93 -0
  33. data/lib/generators/templates/thin/lib/%base_name%/merbtasks.rb +106 -0
  34. data/lib/generators/templates/thin/lib/%base_name%/slicetasks.rb +18 -0
  35. data/lib/generators/templates/thin/public/javascripts/master.js +0 -0
  36. data/lib/generators/templates/thin/public/stylesheets/master.css +2 -0
  37. data/lib/generators/templates/thin/stubs/application.rb +9 -0
  38. data/lib/generators/templates/thin/views/layout/%symbol_name%.html.erb +16 -0
  39. data/lib/generators/templates/thin/views/main/index.html.erb +1 -0
  40. data/lib/generators/templates/very_thin/LICENSE +20 -0
  41. data/lib/generators/templates/very_thin/README +110 -0
  42. data/lib/generators/templates/very_thin/Rakefile +46 -0
  43. data/lib/generators/templates/very_thin/TODO +7 -0
  44. data/lib/generators/templates/very_thin/application.rb +36 -0
  45. data/lib/generators/templates/very_thin/lib/%base_name%.rb +89 -0
  46. data/lib/generators/templates/very_thin/lib/%base_name%/merbtasks.rb +106 -0
  47. data/lib/generators/templates/very_thin/lib/%base_name%/slicetasks.rb +18 -0
  48. data/lib/generators/thin.rb +21 -0
  49. data/lib/generators/very_thin.rb +21 -0
  50. data/lib/merb-slices.rb +126 -0
  51. data/lib/merb-slices/controller_mixin.rb +129 -0
  52. data/lib/merb-slices/merbtasks.rb +19 -0
  53. data/lib/merb-slices/module.rb +338 -0
  54. data/lib/merb-slices/module_mixin.rb +535 -0
  55. data/lib/merb-slices/router_ext.rb +75 -0
  56. data/spec/full_slice_generator_spec.rb +21 -0
  57. data/spec/merb-slice_spec.rb +7 -0
  58. data/spec/slice_generator_spec.rb +22 -0
  59. data/spec/spec_helper.rb +9 -0
  60. data/spec/thin_slice_generator_spec.rb +21 -0
  61. data/spec/very_thin_slice_generator_spec.rb +21 -0
  62. 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