merb-slices 0.9.4

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