thorero-slices 0.9.4
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README +102 -0
- data/Rakefile +65 -0
- data/TODO +0 -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/merb-slice_spec.rb +7 -0
- data/spec/spec_helper.rb +2 -0
- metadata +76 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Fabien Franzen <info@loobmedia.com>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
Merb-Slices
|
2
|
+
===========
|
3
|
+
|
4
|
+
Merb-Slices is a Merb plugin for using and creating application 'slices' which
|
5
|
+
help you modularize your application. Usually these are reuseable extractions
|
6
|
+
from your main app. In effect, a Slice is just like a regular Merb MVC
|
7
|
+
application, both in functionality as well as in structure.
|
8
|
+
|
9
|
+
When you generate a Slice stub structure, a module is setup to serve as a
|
10
|
+
namespace for your controller, models, helpers etc. This ensures maximum
|
11
|
+
encapsulation. You could say a Slice is a mixture between a Merb plugin (a
|
12
|
+
Gem) and a Merb application, reaping the benefits of both.
|
13
|
+
|
14
|
+
A host application can 'mount' a Slice inside the router, which means you have
|
15
|
+
full over control how it integrates. By default a Slice's routes are prefixed
|
16
|
+
by its name (a router :namespace), but you can easily provide your own prefix
|
17
|
+
or leave it out, mounting it at the root of your url-schema. You can even
|
18
|
+
mount a Slice multiple times and give extra parameters to customize an
|
19
|
+
instance's behaviour.
|
20
|
+
|
21
|
+
A Slice's Application controller uses controller_for_slice to setup slice
|
22
|
+
specific behaviour, which mainly affects cascaded view handling. Additionaly,
|
23
|
+
this method is available to any kind of controller, so it can be used for
|
24
|
+
Merb Mailer too for example.
|
25
|
+
|
26
|
+
There are many ways which let you customize a Slice's functionality and
|
27
|
+
appearance without ever touching the Gem-level code itself. It's not only easy
|
28
|
+
to add template/layout overrides, you can also add/modify controllers, models
|
29
|
+
and other runtime code from within the host application.
|
30
|
+
|
31
|
+
To create your own Slice run this (somewhere outside of your merb app):
|
32
|
+
|
33
|
+
$ merb-gen slice <your-lowercase-slice-name>
|
34
|
+
|
35
|
+
------------------------------------------------------------------------------
|
36
|
+
|
37
|
+
Instructions for installation of an imaginary 'BlogSlice' slice:
|
38
|
+
|
39
|
+
file: config/init.rb
|
40
|
+
|
41
|
+
# add the slice as a regular dependency
|
42
|
+
|
43
|
+
dependency 'blog-slice'
|
44
|
+
|
45
|
+
# if needed, configure which slices to load and in which order
|
46
|
+
|
47
|
+
Merb::Plugins.config[:merb_slices] = { :queue => ["BlogSlice", ...] }
|
48
|
+
|
49
|
+
# optionally configure the plugins in a before_app_loads callback
|
50
|
+
|
51
|
+
Merb::BootLoader.before_app_loads do
|
52
|
+
|
53
|
+
Merb::Slices::config[:blog_slice] = { ... }
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
file: config/router.rb
|
58
|
+
|
59
|
+
# example: /blog-slice/:controller/:action/:id
|
60
|
+
|
61
|
+
r.add_slice(:BlogSlice)
|
62
|
+
|
63
|
+
# example: /foo/:controller/:action/:id
|
64
|
+
|
65
|
+
r.add_slice(:BlogSlice, 'foo') # same as :path => 'foo'
|
66
|
+
|
67
|
+
# example: /:lang/:controller/:action/:id (with :a param set)
|
68
|
+
|
69
|
+
r.add_slice(:BlogSlice, :path => ':lang', :params => { :a => 'b' })
|
70
|
+
|
71
|
+
# example: /:controller/:action/:id
|
72
|
+
|
73
|
+
r.slice(:BlogSlice)
|
74
|
+
|
75
|
+
Normally you should also run the following rake task:
|
76
|
+
|
77
|
+
rake slices:blog_slice:install
|
78
|
+
|
79
|
+
------------------------------------------------------------------------------
|
80
|
+
|
81
|
+
You can put your application-level overrides in:
|
82
|
+
|
83
|
+
host-app/slices/blog-slice/app - controllers, models, views ...
|
84
|
+
|
85
|
+
Templates are located in this order:
|
86
|
+
|
87
|
+
1. host-app/slices/blog-slice/app/views/*
|
88
|
+
2. gems/blog-slice/app/views/*
|
89
|
+
3. host-app/app/views/*
|
90
|
+
|
91
|
+
You can use the host application's layout by configuring the
|
92
|
+
blog-slice slice in a before_app_loads block:
|
93
|
+
|
94
|
+
Merb::Slices.config[:blog_slice] = { :layout => :application }
|
95
|
+
|
96
|
+
By default :blog_slice is used. If you need to override
|
97
|
+
stylesheets or javascripts, just specify your own files in your layout
|
98
|
+
instead/in addition to the ones supplied (if any) in
|
99
|
+
host-app/public/slices/blog-slice.
|
100
|
+
|
101
|
+
In any case don't edit those files directly as they may be clobbered any time
|
102
|
+
rake blog_slice:install is run.
|
data/Rakefile
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
require "extlib"
|
4
|
+
require 'merb-core/tasks/merb_rake_helper'
|
5
|
+
require "spec/rake/spectask"
|
6
|
+
|
7
|
+
##############################################################################
|
8
|
+
# Package && release
|
9
|
+
##############################################################################
|
10
|
+
RUBY_FORGE_PROJECT = "thorero"
|
11
|
+
PROJECT_URL = "http://merbivore.com"
|
12
|
+
PROJECT_SUMMARY = "Merb-Slices is a Merb plugin for using and creating application 'slices' which help you modularize your application."
|
13
|
+
PROJECT_DESCRIPTION = PROJECT_SUMMARY
|
14
|
+
|
15
|
+
GEM_AUTHOR = "Fabien Franzen"
|
16
|
+
GEM_EMAIL = "info@fabien.be"
|
17
|
+
|
18
|
+
GEM_NAME = "thorero-slices"
|
19
|
+
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
|
20
|
+
GEM_VERSION = (Merb::MORE_VERSION rescue "0.9.4") + PKG_BUILD
|
21
|
+
|
22
|
+
RELEASE_NAME = "REL #{GEM_VERSION}"
|
23
|
+
|
24
|
+
require "extlib/tasks/release"
|
25
|
+
|
26
|
+
spec = Gem::Specification.new do |s|
|
27
|
+
s.rubyforge_project = RUBY_FORGE_PROJECT
|
28
|
+
s.name = GEM_NAME
|
29
|
+
s.version = GEM_VERSION
|
30
|
+
s.platform = Gem::Platform::RUBY
|
31
|
+
s.has_rdoc = true
|
32
|
+
s.extra_rdoc_files = ["README", "LICENSE", 'TODO']
|
33
|
+
s.summary = PROJECT_SUMMARY
|
34
|
+
s.description = PROJECT_DESCRIPTION
|
35
|
+
s.author = GEM_AUTHOR
|
36
|
+
s.email = GEM_EMAIL
|
37
|
+
s.homepage = PROJECT_URL
|
38
|
+
s.add_dependency('merb-core', '>= 0.9.4')
|
39
|
+
s.require_path = 'lib'
|
40
|
+
s.files = %w(LICENSE README Rakefile TODO) + Dir.glob("{lib,spec,merb_generators}/**/*")
|
41
|
+
end
|
42
|
+
|
43
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
44
|
+
pkg.gem_spec = spec
|
45
|
+
end
|
46
|
+
|
47
|
+
desc "Install the gem"
|
48
|
+
task :install => [:package] do
|
49
|
+
sh %{#{sudo} gem install #{install_home} pkg/#{GEM_NAME}-#{GEM_VERSION} --no-update-sources}
|
50
|
+
end
|
51
|
+
|
52
|
+
namespace :jruby do
|
53
|
+
|
54
|
+
desc "Run :package and install the resulting .gem with jruby"
|
55
|
+
task :install => :package do
|
56
|
+
sh %{#{sudo} jruby -S gem install #{install_home} pkg/#{GEM_NAME}-#{GEM_VERSION}.gem --no-rdoc --no-ri}
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
desc "Run all specs"
|
62
|
+
Spec::Rake::SpecTask.new("specs") do |t|
|
63
|
+
t.spec_opts = ["--format", "specdoc", "--colour"]
|
64
|
+
t.spec_files = Dir["spec/**/*_spec.rb"].sort
|
65
|
+
end
|
data/TODO
ADDED
File without changes
|
data/lib/merb-slices.rb
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'merb-slices/module'
|
2
|
+
|
3
|
+
if defined?(Merb::Plugins)
|
4
|
+
|
5
|
+
Merb::Plugins.add_rakefiles "merb-slices/merbtasks"
|
6
|
+
|
7
|
+
Merb::Plugins.config[:merb_slices] ||= {}
|
8
|
+
|
9
|
+
require "merb-slices/module_mixin"
|
10
|
+
require "merb-slices/controller_mixin"
|
11
|
+
require "merb-slices/router_ext"
|
12
|
+
|
13
|
+
# Enable slice behaviour for any class inheriting from AbstractController.
|
14
|
+
# To use this call controller_for_slice in your controller class.
|
15
|
+
Merb::AbstractController.send(:include, Merb::Slices::ControllerMixin)
|
16
|
+
|
17
|
+
# Load Slice classes before the app's classes are loaded.
|
18
|
+
#
|
19
|
+
# This allows the application to override/merge any slice-level classes.
|
20
|
+
class Merb::Slices::Loader < Merb::BootLoader
|
21
|
+
|
22
|
+
before LoadClasses
|
23
|
+
|
24
|
+
class << self
|
25
|
+
|
26
|
+
# Gather all slices from search path and gems and load their classes.
|
27
|
+
def run
|
28
|
+
Merb::Slices.register_slices_from_search_path! if auto_register?
|
29
|
+
Merb::Slices.each_slice { |slice| slice.load_slice }
|
30
|
+
end
|
31
|
+
|
32
|
+
# Load a single file and its requirements.
|
33
|
+
#
|
34
|
+
# @param file<String> The file to load.
|
35
|
+
def load_file(file)
|
36
|
+
Merb::BootLoader::LoadClasses.load_file file
|
37
|
+
end
|
38
|
+
|
39
|
+
# Remove a single file and the classes loaded by it from ObjectSpace.
|
40
|
+
#
|
41
|
+
# @param file<String> The file to load.
|
42
|
+
def remove_file(file)
|
43
|
+
Merb::BootLoader::LoadClasses.remove_file file
|
44
|
+
end
|
45
|
+
|
46
|
+
# Load classes from given paths - using path/glob pattern.
|
47
|
+
#
|
48
|
+
# @param *paths <Array> Array of paths to load classes from - may contain glob pattern
|
49
|
+
def load_classes(*paths)
|
50
|
+
Merb::BootLoader::LoadClasses.load_classes paths
|
51
|
+
end
|
52
|
+
|
53
|
+
# Reload the router - takes all_slices into account to load slices at runtime.
|
54
|
+
def reload_router!
|
55
|
+
Merb::BootLoader::LoadClasses.reload_router!
|
56
|
+
end
|
57
|
+
|
58
|
+
# Slice-level paths for all loaded slices.
|
59
|
+
#
|
60
|
+
# @return <Array[String]> Any slice-level paths that have been loaded.
|
61
|
+
def slice_paths
|
62
|
+
paths = []
|
63
|
+
Merb::Slices.each_slice { |slice| paths += slice.collected_slice_paths }
|
64
|
+
paths
|
65
|
+
end
|
66
|
+
|
67
|
+
# App-level paths for all loaded slices.
|
68
|
+
#
|
69
|
+
# @return <Array[String]> Any app-level paths that have been loaded.
|
70
|
+
def app_paths
|
71
|
+
paths = []
|
72
|
+
Merb::Slices.each_slice { |slice| paths += slice.collected_app_paths }
|
73
|
+
paths
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# Whether slices from search paths should be registered automatically.
|
79
|
+
# Defaults to true if not explicitly set.
|
80
|
+
def auto_register?
|
81
|
+
Merb::Plugins.config[:merb_slices][:auto_register] || !Merb::Plugins.config[:merb_slices].key?(:auto_register)
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
# Call initialization method for each registered Slice.
|
89
|
+
#
|
90
|
+
# This is done just before the app's after_load_callbacks are run.
|
91
|
+
# The application has been practically loaded completely, letting
|
92
|
+
# the callbacks work with what has been loaded.
|
93
|
+
class Merb::Slices::Initialize < Merb::BootLoader
|
94
|
+
|
95
|
+
before AfterAppLoads
|
96
|
+
|
97
|
+
def self.run
|
98
|
+
Merb::Slices.each_slice do |slice|
|
99
|
+
Merb.logger.info!("Initializing slice '#{slice}' ...")
|
100
|
+
slice.init if slice.respond_to?(:init)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
# Call activation method for each registered Slice.
|
107
|
+
#
|
108
|
+
# This is done right after the app's after_load_callbacks are run.
|
109
|
+
# Any settings can be taken into account in the activation step.
|
110
|
+
#
|
111
|
+
# @note Activation will only take place if the slice has been routed;
|
112
|
+
# this means you need have at least one slice route setup.
|
113
|
+
class Merb::Slices::Activate < Merb::BootLoader
|
114
|
+
|
115
|
+
after AfterAppLoads
|
116
|
+
|
117
|
+
def self.run
|
118
|
+
Merb::Slices.each_slice do |slice|
|
119
|
+
Merb.logger.info!("Activating slice '#{slice}' ...")
|
120
|
+
slice.activate if slice.respond_to?(:activate) && slice.routed?
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module Merb
|
2
|
+
module Slices
|
3
|
+
|
4
|
+
module ControllerMixin
|
5
|
+
|
6
|
+
def self.included(klass)
|
7
|
+
klass.extend ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
# Setup a controller to reference a slice and its template roots
|
13
|
+
#
|
14
|
+
# This method is available to any class inheriting from Merb::AbstractController;
|
15
|
+
# it enabled correct location of templates, as well as access to the slice module.
|
16
|
+
#
|
17
|
+
# @param slice_module<#to_s> The slice module to use; defaults to current module.
|
18
|
+
# @param options<Hash>
|
19
|
+
# Optional parameters to set which component path is used (defaults to :view) and
|
20
|
+
# the :path option lets you specify a subdirectory of that component path.
|
21
|
+
# When :layout is set, then this is used instead of the config's :layout setting.
|
22
|
+
#
|
23
|
+
# @example controller_for_slice # uses current module
|
24
|
+
# @example controller_for_slice SliceMod # defaults to :view templates and no subdirectory
|
25
|
+
# @example controller_for_slice :templates_for => :mailer, :path => 'views' # for Merb::Mailer
|
26
|
+
# @example controller_for_slice SliceMod, :templates_for => :mailer, :path => 'views' # for Merb::Mailer
|
27
|
+
def controller_for_slice(slice_module = nil, options = {})
|
28
|
+
options, slice_module = slice_module.merge(options), nil if slice_module.is_a?(Hash)
|
29
|
+
slice_module ||= self.name.split('::').first
|
30
|
+
options[:templates_for] = :view unless options.key?(:templates_for)
|
31
|
+
if slice_mod = Merb::Slices[slice_module.to_s]
|
32
|
+
# Include the instance methods
|
33
|
+
unless self.kind_of?(Merb::Slices::ControllerMixin::MixinMethods)
|
34
|
+
self.send(:extend, Merb::Slices::ControllerMixin::MixinMethods)
|
35
|
+
end
|
36
|
+
# Reference this controller's slice module
|
37
|
+
self.class_inheritable_accessor :slice
|
38
|
+
self.slice = slice_mod
|
39
|
+
# Setup template roots
|
40
|
+
if options[:templates_for]
|
41
|
+
self._template_root = join_template_path(slice_mod.dir_for(options[:templates_for]), options[:path])
|
42
|
+
self._template_roots = []
|
43
|
+
# app-level app/views directory for shared and fallback views, layouts and partials
|
44
|
+
self._template_roots << [join_template_path(Merb.dir_for(options[:templates_for]), options[:path]), :_template_location] if Merb.dir_for(options[:templates_for])
|
45
|
+
# slice-level app/views for the standard supplied views
|
46
|
+
self._template_roots << [self._template_root, :_slice_template_location]
|
47
|
+
# app-level slices/<slice>/app/views for specific overrides
|
48
|
+
self._template_roots << [join_template_path(slice_mod.app_dir_for(options[:templates_for]), options[:path]), :_slice_template_location]
|
49
|
+
# additional template roots for specific overrides (optional)
|
50
|
+
self._template_roots += Array(options[:template_roots]) if options[:template_roots]
|
51
|
+
end
|
52
|
+
# Set the layout for this slice controller
|
53
|
+
layout_for_slice(options[:layout])
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def join_template_path(*segments)
|
60
|
+
File.join(*segments.compact)
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
module MixinMethods
|
66
|
+
|
67
|
+
def self.extended(klass)
|
68
|
+
klass.send(:include, InstanceMethods)
|
69
|
+
klass.hide_action :slice
|
70
|
+
end
|
71
|
+
|
72
|
+
# Use the slice's layout - defaults to underscored identifier.
|
73
|
+
#
|
74
|
+
# This is set for generated stubs that support layouts.
|
75
|
+
#
|
76
|
+
# @param layout<#to_s> The layout name to use.
|
77
|
+
def layout_for_slice(layout = nil)
|
78
|
+
layout(layout || self.slice.config[:layout]) if layout || self.slice.config.key?(:layout)
|
79
|
+
end
|
80
|
+
|
81
|
+
module InstanceMethods
|
82
|
+
|
83
|
+
# Reference this controller's slice module directly.
|
84
|
+
#
|
85
|
+
# @return <Module> A slice module.
|
86
|
+
def slice; self.class.slice; end
|
87
|
+
|
88
|
+
# Generate a url - takes the slice's :path_prefix into account.
|
89
|
+
def slice_url(name, rparams={})
|
90
|
+
self.slice.url(name, rparams, {
|
91
|
+
:controller => controller_name,
|
92
|
+
:action => action_name,
|
93
|
+
:format => params[:format]
|
94
|
+
})
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
# This is called after the controller is instantiated to figure out where to
|
100
|
+
# for templates under the _template_root. This helps the controllers
|
101
|
+
# of a slice to locate templates without looking in a subdirectory with
|
102
|
+
# the name of the module. Instead it will just be app/views/controller/*
|
103
|
+
#
|
104
|
+
# @param context<#to_str> The controller context (the action or template name).
|
105
|
+
# @param type<#to_str> The content type. Defaults to nil.
|
106
|
+
# @param controller<#to_str> The name of the controller. Defaults to controller_name.
|
107
|
+
#
|
108
|
+
# @return <String>
|
109
|
+
# Indicating where to look for the template for the current controller,
|
110
|
+
# context, and content-type.
|
111
|
+
def _slice_template_location(context, type = nil, controller = controller_name)
|
112
|
+
if controller && controller.include?('/')
|
113
|
+
# skip first segment if given (which is the module name)
|
114
|
+
segments = controller.split('/')
|
115
|
+
"#{segments[1,segments.length-1].join('/')}/#{context}.#{type}"
|
116
|
+
else
|
117
|
+
# default template location logic
|
118
|
+
_template_location(context, type, controller)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
end
|