reactive-mvc 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/LICENSE +21 -0
  2. data/Manifest +34 -0
  3. data/README +30 -0
  4. data/Rakefile +5 -0
  5. data/lib/reactive-mvc.rb +26 -0
  6. data/lib/reactive-mvc/controller.rb +16 -0
  7. data/lib/reactive-mvc/controller/base.rb +405 -0
  8. data/lib/reactive-mvc/controller/filters.rb +767 -0
  9. data/lib/reactive-mvc/controller/flash.rb +161 -0
  10. data/lib/reactive-mvc/controller/helpers.rb +203 -0
  11. data/lib/reactive-mvc/controller/layout.rb +285 -0
  12. data/lib/reactive-mvc/controller/output.rb +262 -0
  13. data/lib/reactive-mvc/controller/rescue.rb +208 -0
  14. data/lib/reactive-mvc/dispatcher.rb +133 -0
  15. data/lib/reactive-mvc/view.rb +18 -0
  16. data/lib/reactive-mvc/view/base.rb +388 -0
  17. data/lib/reactive-mvc/view/helpers.rb +38 -0
  18. data/lib/reactive-mvc/view/partials.rb +207 -0
  19. data/lib/reactive-mvc/view/paths.rb +125 -0
  20. data/lib/reactive-mvc/view/renderable.rb +98 -0
  21. data/lib/reactive-mvc/view/renderable_partial.rb +49 -0
  22. data/lib/reactive-mvc/view/template.rb +110 -0
  23. data/lib/reactive-mvc/view/template_error.rb +9 -0
  24. data/lib/reactive-mvc/view/template_handler.rb +9 -0
  25. data/lib/reactive-mvc/view/template_handlers.rb +43 -0
  26. data/lib/reactive-mvc/view/template_handlers/builder.rb +15 -0
  27. data/lib/reactive-mvc/view/template_handlers/erb.rb +20 -0
  28. data/lib/reactive-mvc/view/template_handlers/ruby_code.rb +9 -0
  29. data/reactive_app_generators/mvc/USAGE +10 -0
  30. data/reactive_app_generators/mvc/mvc_generator.rb +48 -0
  31. data/reactive_app_generators/mvc/templates/application_controller.rb +2 -0
  32. data/reactive_generators/controller/USAGE +7 -0
  33. data/reactive_generators/controller/controller_generator.rb +24 -0
  34. data/reactive_generators/controller/templates/controller.rb +2 -0
  35. metadata +113 -0
@@ -0,0 +1,38 @@
1
+ module Reactive::Mvc::View
2
+ module Helpers
3
+ def inject_output_helpers
4
+ <<-EOS
5
+ helpers_module = HelperModule.new #{output_helper_names.inspect}, <<-EOC
6
+ #{read_output_helpers.gsub('#', '\#')}
7
+ EOC
8
+ extend(*helpers_module.helpers)
9
+ EOS
10
+ end
11
+
12
+ def output_helper_names
13
+ parts = controller_name.split('/')
14
+ (["application_helper.#{request.format}.rb"] + Enumerable::Enumerator.new(parts, :each_index).collect{|i| "#{parts[0..i].join('/')}_helper.#{request.format}.rb"}).uniq.select{|path| File.file?(Reactive.path_for(:helper, path))}
15
+ end
16
+
17
+ def read_output_helpers(names = output_helper_names)
18
+ names.inject("") {|code, path| code << File.read(Reactive.path_for(:helper, path))}
19
+ end
20
+
21
+ end
22
+ end
23
+
24
+ =begin
25
+ def include_application_helpers(request)
26
+ include_helper('application_helper')
27
+ parts = controller_name.split('/')
28
+ Enumerable::Enumerator.new(parts, :each_index).collect{|i| parts[0..i].join('/')}.each do |path|
29
+ include_helper("#{path}_helper")
30
+ end
31
+ end
32
+
33
+ def include_helper(name)
34
+ @helpers.include(name.classify.constantize)
35
+ rescue NameError, LoadError
36
+ # Helper files are not mandatory
37
+ end
38
+ =end
@@ -0,0 +1,207 @@
1
+ module Reactive::Mvc::View
2
+ # There's also a convenience method for rendering sub templates within the current controller that depends on a
3
+ # single object (we call this kind of sub templates for partials). It relies on the fact that partials should
4
+ # follow the naming convention of being prefixed with an underscore -- as to separate them from regular
5
+ # templates that could be rendered on their own.
6
+ #
7
+ # In a template for Advertiser#account:
8
+ #
9
+ # <%= render :partial => "account" %>
10
+ #
11
+ # This would render "advertiser/_account.erb" and pass the instance variable @account in as a local variable
12
+ # +account+ to the template for display.
13
+ #
14
+ # In another template for Advertiser#buy, we could have:
15
+ #
16
+ # <%= render :partial => "account", :locals => { :account => @buyer } %>
17
+ #
18
+ # <% for ad in @advertisements %>
19
+ # <%= render :partial => "ad", :locals => { :ad => ad } %>
20
+ # <% end %>
21
+ #
22
+ # This would first render "advertiser/_account.erb" with @buyer passed in as the local variable +account+, then
23
+ # render "advertiser/_ad.erb" and pass the local variable +ad+ to the template for display.
24
+ #
25
+ # == Rendering a collection of partials
26
+ #
27
+ # The example of partial use describes a familiar pattern where a template needs to iterate over an array and
28
+ # render a sub template for each of the elements. This pattern has been implemented as a single method that
29
+ # accepts an array and renders a partial by the same name as the elements contained within. So the three-lined
30
+ # example in "Using partials" can be rewritten with a single line:
31
+ #
32
+ # <%= render :partial => "ad", :collection => @advertisements %>
33
+ #
34
+ # This will render "advertiser/_ad.erb" and pass the local variable +ad+ to the template for display. An
35
+ # iteration counter will automatically be made available to the template with a name of the form
36
+ # +partial_name_counter+. In the case of the example above, the template would be fed +ad_counter+.
37
+ #
38
+ # NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also
39
+ # just keep domain objects, like Active Records, in there.
40
+ #
41
+ # == Rendering shared partials
42
+ #
43
+ # Two controllers can share a set of partials and render them like this:
44
+ #
45
+ # <%= render :partial => "advertisement/ad", :locals => { :ad => @advertisement } %>
46
+ #
47
+ # This will render the partial "advertisement/_ad.erb" regardless of which controller this is being called from.
48
+ #
49
+ # == Rendering partials with layouts
50
+ #
51
+ # Partials can have their own layouts applied to them. These layouts are different than the ones that are
52
+ # specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types
53
+ # of users:
54
+ #
55
+ # <%# app/views/users/index.html.erb &>
56
+ # Here's the administrator:
57
+ # <%= render :partial => "user", :layout => "administrator", :locals => { :user => administrator } %>
58
+ #
59
+ # Here's the editor:
60
+ # <%= render :partial => "user", :layout => "editor", :locals => { :user => editor } %>
61
+ #
62
+ # <%# app/views/users/_user.html.erb &>
63
+ # Name: <%= user.name %>
64
+ #
65
+ # <%# app/views/users/_administrator.html.erb &>
66
+ # <div id="administrator">
67
+ # Budget: $<%= user.budget %>
68
+ # <%= yield %>
69
+ # </div>
70
+ #
71
+ # <%# app/views/users/_editor.html.erb &>
72
+ # <div id="editor">
73
+ # Deadline: <%= user.deadline %>
74
+ # <%= yield %>
75
+ # </div>
76
+ #
77
+ # ...this will return:
78
+ #
79
+ # Here's the administrator:
80
+ # <div id="administrator">
81
+ # Budget: $<%= user.budget %>
82
+ # Name: <%= user.name %>
83
+ # </div>
84
+ #
85
+ # Here's the editor:
86
+ # <div id="editor">
87
+ # Deadline: <%= user.deadline %>
88
+ # Name: <%= user.name %>
89
+ # </div>
90
+ #
91
+ # You can also apply a layout to a block within any template:
92
+ #
93
+ # <%# app/views/users/_chief.html.erb &>
94
+ # <% render(:layout => "administrator", :locals => { :user => chief }) do %>
95
+ # Title: <%= chief.title %>
96
+ # <% end %>
97
+ #
98
+ # ...this will return:
99
+ #
100
+ # <div id="administrator">
101
+ # Budget: $<%= user.budget %>
102
+ # Title: <%= chief.name %>
103
+ # </div>
104
+ #
105
+ # As you can see, the <tt>:locals</tt> hash is shared between both the partial and its layout.
106
+ #
107
+ # If you pass arguments to "yield" then this will be passed to the block. One way to use this is to pass
108
+ # an array to layout and treat it as an enumerable.
109
+ #
110
+ # <%# app/views/users/_user.html.erb &>
111
+ # <div class="user">
112
+ # Budget: $<%= user.budget %>
113
+ # <%= yield user %>
114
+ # </div>
115
+ #
116
+ # <%# app/views/users/index.html.erb &>
117
+ # <% render :layout => @users do |user| %>
118
+ # Title: <%= user.title %>
119
+ # <% end %>
120
+ #
121
+ # This will render the layout for each user and yield to the block, passing the user, each time.
122
+ #
123
+ # You can also yield multiple times in one layout and use block arguments to differentiate the sections.
124
+ #
125
+ # <%# app/views/users/_user.html.erb &>
126
+ # <div class="user">
127
+ # <%= yield user, :header %>
128
+ # Budget: $<%= user.budget %>
129
+ # <%= yield user, :footer %>
130
+ # </div>
131
+ #
132
+ # <%# app/views/users/index.html.erb &>
133
+ # <% render :layout => @users do |user, section| %>
134
+ # <%- case section when :header -%>
135
+ # Title: <%= user.title %>
136
+ # <%- when :footer -%>
137
+ # Deadline: <%= user.deadline %>
138
+ # <%- end -%>
139
+ # <% end %>
140
+ module Partials
141
+ extend ActiveSupport::Memoizable
142
+
143
+ private
144
+ def render_partial(options = {}) #:nodoc:
145
+ local_assigns = options[:locals] || {}
146
+
147
+ case partial_path = options[:partial]
148
+ when String, Symbol, NilClass
149
+ if options.has_key?(:collection)
150
+ render_partial_collection(options)
151
+ else
152
+ _pick_partial_template(partial_path).render_partial(self, options[:object], local_assigns)
153
+ end
154
+ when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope
155
+ render_partial_collection(options.except(:partial).merge(:collection => partial_path))
156
+ else
157
+ raise Error, "RecordIdentifier not available"
158
+ object = partial_path
159
+ render_partial(
160
+ :partial => ActionController::RecordIdentifier.partial_path(object, controller.class.controller_path),
161
+ :object => object,
162
+ :locals => local_assigns
163
+ )
164
+ end
165
+ end
166
+
167
+ def render_partial_collection(options = {}) #:nodoc:
168
+ return nil if options[:collection].blank?
169
+
170
+ partial = options[:partial]
171
+ spacer = options[:spacer_template] ? render(:partial => options[:spacer_template]) : ''
172
+ local_assigns = options[:locals] ? options[:locals].clone : {}
173
+ as = options[:as]
174
+
175
+ index = 0
176
+ options[:collection].map do |object|
177
+ _partial_path ||= partial #||
178
+ # ActionController::RecordIdentifier.partial_path(object, controller.class.controller_path)
179
+ template = _pick_partial_template(_partial_path)
180
+ local_assigns[template.counter_name] = index
181
+ result = template.render_partial(self, object, local_assigns.dup, as)
182
+ index += 1
183
+ result
184
+ end.join(spacer)
185
+ end
186
+
187
+ def _pick_partial_template(partial_path) #:nodoc:
188
+ if partial_path.include?('/')
189
+ path = File.join(File.dirname(partial_path), "_#{File.basename(partial_path)}")
190
+ elsif controller
191
+ path = "#{controller.class.controller_path}/_#{partial_path}"
192
+ else
193
+ path = "_#{partial_path}"
194
+ end
195
+
196
+ _pick_template(path)
197
+ end
198
+ memoize :_pick_partial_template
199
+
200
+ def partial_template_exists?(partial_path)
201
+ _pick_partial_template(partial_path) ? true : false
202
+ rescue MissingTemplate
203
+ false
204
+ end
205
+
206
+ end
207
+ end
@@ -0,0 +1,125 @@
1
+ module Reactive::Mvc::View #:nodoc:
2
+ class PathSet < Array #:nodoc:
3
+ def self.type_cast(obj)
4
+ if obj.is_a?(String)
5
+ if Base.warn_cache_misses && defined?(Rails) && Rails.initialized?
6
+ Base.logger.debug "[PERFORMANCE] Processing view path during a " +
7
+ "request. This an expense disk operation that should be done at " +
8
+ "boot. You can manually process this view path with " +
9
+ "ActionView::Base.process_view_paths(#{obj.inspect}) and set it " +
10
+ "as your view path"
11
+ end
12
+ Path.new(obj)
13
+ else
14
+ obj
15
+ end
16
+ end
17
+
18
+ def initialize(*args)
19
+ super(*args).map! { |obj| self.class.type_cast(obj) }
20
+ end
21
+
22
+ def <<(obj)
23
+ super(self.class.type_cast(obj))
24
+ end
25
+
26
+ def concat(array)
27
+ super(array.map! { |obj| self.class.type_cast(obj) })
28
+ end
29
+
30
+ def insert(index, obj)
31
+ super(index, self.class.type_cast(obj))
32
+ end
33
+
34
+ def push(*objs)
35
+ super(*objs.map { |obj| self.class.type_cast(obj) })
36
+ end
37
+
38
+ def unshift(*objs)
39
+ super(*objs.map { |obj| self.class.type_cast(obj) })
40
+ end
41
+
42
+ class Path #:nodoc:
43
+ def self.eager_load_templates!
44
+ @eager_load_templates = true
45
+ end
46
+
47
+ def self.eager_load_templates?
48
+ @eager_load_templates || false
49
+ end
50
+
51
+ attr_reader :path, :paths
52
+ delegate :to_s, :to_str, :hash, :inspect, :to => :path
53
+
54
+ def initialize(path, load = true)
55
+ raise ArgumentError, "path already is a Path class" if path.is_a?(Path)
56
+ @path = path.freeze
57
+ reload! if load
58
+ end
59
+
60
+ def ==(path)
61
+ to_str == path.to_str
62
+ end
63
+
64
+ def eql?(path)
65
+ to_str == path.to_str
66
+ end
67
+
68
+ def [](path)
69
+ raise "Unloaded view path! #{@path}" unless @loaded
70
+ @paths[path]
71
+ end
72
+
73
+ def loaded?
74
+ @loaded ? true : false
75
+ end
76
+
77
+ def load
78
+ reload! unless loaded?
79
+ self
80
+ end
81
+
82
+ # Rebuild load path directory cache
83
+ def reload!
84
+ @paths = {}
85
+
86
+ templates_in_path do |template|
87
+ # Eager load memoized methods and freeze cached template
88
+ template.freeze if self.class.eager_load_templates?
89
+
90
+ @paths[template.path] = template
91
+ @paths[template.path_without_extension] ||= template
92
+ end
93
+
94
+ @paths.freeze
95
+ @loaded = true
96
+ end
97
+
98
+ private
99
+ def templates_in_path
100
+ (Dir.glob("#{@path}/**/*/**") | Dir.glob("#{@path}/**")).each do |file|
101
+ unless File.directory?(file)
102
+ yield Template.new(file.split("#{self}/").last, self)
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ def load
109
+ each { |path| path.load }
110
+ end
111
+
112
+ def reload!
113
+ each { |path| path.reload! }
114
+ end
115
+
116
+ def [](template_path)
117
+ each do |path|
118
+ if template = path[template_path]
119
+ return template
120
+ end
121
+ end
122
+ nil
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,98 @@
1
+ module Reactive::Mvc
2
+ module View
3
+ # NOTE: The template that this mixin is being included into is frozen
4
+ # so you cannot set or modify any instance variables
5
+ module Renderable #:nodoc:
6
+ extend ActiveSupport::Memoizable
7
+
8
+ def self.included(base)
9
+ @@mutex = Mutex.new
10
+ end
11
+
12
+ def filename
13
+ 'compiled-template'
14
+ end
15
+
16
+ def handler
17
+ Template.handler_class_for_extension(extension)
18
+ end
19
+ memoize :handler
20
+
21
+ def compiled_source
22
+ handler.call(self)
23
+ end
24
+ memoize :compiled_source
25
+
26
+ def render(view, local_assigns = {})
27
+ compile(local_assigns)
28
+
29
+ stack = view.instance_variable_get(:@_render_stack)
30
+ stack.push(self)
31
+
32
+ view.send(:_evaluate_assigns_and_ivars)
33
+
34
+ result = view.send(method_name(local_assigns), local_assigns) do |*names|
35
+ ivar = :@_proc_for_layout
36
+ if !view.instance_variable_defined?(:"@content_for_#{names.first}") && view.instance_variable_defined?(ivar) && (proc = view.instance_variable_get(ivar))
37
+ view.capture(*names, &proc)
38
+ elsif view.instance_variable_defined?(ivar = :"@content_for_#{names.first || :layout}")
39
+ view.instance_variable_get(ivar)
40
+ end
41
+ end
42
+
43
+ stack.pop
44
+ result
45
+ end
46
+
47
+ def method_name(local_assigns)
48
+ if local_assigns && local_assigns.any?
49
+ local_assigns_keys = "locals_#{local_assigns.keys.map { |k| k.to_s }.sort.join('_')}"
50
+ end
51
+ ['_run', extension, method_segment, local_assigns_keys].compact.join('_').to_sym
52
+ end
53
+
54
+ private
55
+ # Compile and evaluate the template's code (if necessary)
56
+ def compile(local_assigns)
57
+ render_symbol = method_name(local_assigns)
58
+
59
+ @@mutex.synchronize do
60
+ if recompile?(render_symbol)
61
+ compile!(render_symbol, local_assigns)
62
+ end
63
+ end
64
+ end
65
+
66
+ def compile!(render_symbol, local_assigns)
67
+ locals_code = local_assigns.keys.map { |key| "#{key} = local_assigns[:#{key}];" }.join
68
+
69
+ source = <<-end_src
70
+ def #{render_symbol}(local_assigns)
71
+ old_output_buffer = output_buffer;#{locals_code};#{compiled_source}
72
+ ensure
73
+ self.output_buffer = old_output_buffer
74
+ end
75
+ end_src
76
+
77
+ begin
78
+ Reactive::Mvc::View::Base::CompiledTemplates.module_eval(source, filename, 0)
79
+ rescue Exception => e # errors from template code
80
+ if logger = Base.logger
81
+ logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
82
+ logger.debug "Function body: #{source}"
83
+ logger.debug "Backtrace: #{e.backtrace.join("\n")}"
84
+ end
85
+
86
+ raise TemplateError.new(relative_path, source, e)
87
+ end
88
+ end
89
+
90
+ # Method to check whether template compilation is necessary.
91
+ # The template will be compiled if the file has not been compiled yet, or
92
+ # if local_assigns has a new key, which isn't supported by the compiled code yet.
93
+ def recompile?(symbol)
94
+ !(Reactive::Mvc::View::PathSet::Path.eager_load_templates? && Base::CompiledTemplates.method_defined?(symbol))
95
+ end
96
+ end
97
+ end
98
+ end