reactive-mvc 0.2.0

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 (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,262 @@
1
+ module Reactive::Mvc::Controller #:nodoc:
2
+ class InvalidOption < Error
3
+ end
4
+
5
+ module Output #:nodoc:
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ base.class_eval { include InstanceMethods }
9
+ end
10
+
11
+ # output :pdf, :show => {:treatment => 'print', :handler => 'system_viewer'}
12
+ # output :pdf, :show => 'print' # same as above but without the handler (the available handler will be used (like nil in :handler))
13
+ # output :pdf, :treatment => 'print', :handler => 'system_viewer' # all actions will have this settings (like :all => {})
14
+ # output :pdf, 'print' # all actions will have this settings (with a nil handler) (like :all => :print}
15
+
16
+ # output :pdf, [:show, :index, :new] => {:treatment => 'print', :handler => 'system_viewer'}, :delete => 'display'
17
+ # output :pdf, :all => {:treatment => 'print', :handler => 'system_viewer'}, :delete => 'display' # acts like all but delete (a bit like except for layout)
18
+ # output :pdf, :all => false, :delete => 'display' # acts like all: disable handling but delete
19
+ # output :pdf, :all => {:treatment => 'print', :handler => 'system_viewer'}, :delete => :depends # delete will call a method on the controller named depends that have to return the options
20
+
21
+ # By default, the settings are inherited from the parent controller, thus settings like: "output :pdf, :delete => :display" will only override the settings
22
+ # for delete. If you specify "output :pdf, :all => :print" it will override what was inherited.
23
+
24
+ # There may be several "output" statement per format but it is not recommended. If you do, the statment will be applied in the order they appear, so a last statement
25
+ # with :all => {} will simply override any previous ones, beware!
26
+
27
+ # Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in
28
+ # repeated setups. The inclusion pattern has pages that look like this:
29
+ #
30
+ # <%= render "shared/header" %>
31
+ # Hello World
32
+ # <%= render "shared/footer" %>
33
+ #
34
+ # This approach is a decent way of keeping common structures isolated from the changing content, but it's verbose
35
+ # and if you ever want to change the structure of these two includes, you'll have to change all the templates.
36
+ #
37
+ # With layouts, you can flip it around and have the common structure know where to insert changing content. This means
38
+ # that the header and footer are only mentioned in one place, like this:
39
+ #
40
+ # // The header part of this layout
41
+ # <%= yield %>
42
+ # // The footer part of this layout -->
43
+ #
44
+ # And then you have content pages that look like this:
45
+ #
46
+ # hello world
47
+ #
48
+ # Not a word about common structures. At rendering time, the content page is computed and then inserted in the layout,
49
+ # like this:
50
+ #
51
+ # // The header part of this layout
52
+ # hello world
53
+ # // The footer part of this layout -->
54
+ #
55
+ # == Accessing shared variables
56
+ #
57
+ # Layouts have access to variables specified in the content pages and vice versa. This allows you to have layouts with
58
+ # references that won't materialize before rendering time:
59
+ #
60
+ # <h1><%= @page_title %></h1>
61
+ # <%= yield %>
62
+ #
63
+ # ...and content pages that fulfill these references _at_ rendering time:
64
+ #
65
+ # <% @page_title = "Welcome" %>
66
+ # Off-world colonies offers you a chance to start a new life
67
+ #
68
+ # The result after rendering is:
69
+ #
70
+ # <h1>Welcome</h1>
71
+ # Off-world colonies offers you a chance to start a new life
72
+ #
73
+ # == Automatic layout assignment
74
+ #
75
+ # If there is a template in <tt>app/views/layouts/</tt> with the same name as the current controller then it will be automatically
76
+ # set as that controller's layout nless explicitly told otherwise. Say you have a WeblogController, for example. If a template named
77
+ # <tt>app/views/layouts/weblog.erb</tt> or <tt>app/views/layouts/weblog.builder</tt> exists then it will be automatically set as
78
+ # the layout for your WeblogController. You can create a layout with the name <tt>application.erb</tt> or <tt>application.builder</tt>
79
+ # and this will be set as the default controller if there is no layout with the same name as the current controller and there is
80
+ # no layout explicitly assigned with the +layout+ method. Nested controllers use the same folder structure for automatic layout.
81
+ # assignment. So an Admin::WeblogController will look for a template named <tt>app/views/layouts/admin/weblog.erb</tt>.
82
+ # Setting a layout explicitly will always override the automatic behaviour for the controller where the layout is set.
83
+ # Explicitly setting the layout in a parent class, though, will not override the child class's layout assignment if the child
84
+ # class has a layout with the same name.
85
+ #
86
+ # == Inheritance for layouts
87
+ #
88
+ # Layouts are shared downwards in the inheritance hierarchy, but not upwards. Examples:
89
+ #
90
+ # class BankController < ActionController::Base
91
+ # layout "bank_standard"
92
+ #
93
+ # class InformationController < BankController
94
+ #
95
+ # class VaultController < BankController
96
+ # layout :access_level_layout
97
+ #
98
+ # class EmployeeController < BankController
99
+ # layout nil
100
+ #
101
+ # The InformationController uses "bank_standard" inherited from the BankController, the VaultController overwrites
102
+ # and picks the layout dynamically, and the EmployeeController doesn't want to use a layout at all.
103
+ #
104
+ # == Types of layouts
105
+ #
106
+ # Layouts are basically just regular templates, but the name of this template needs not be specified statically. Sometimes
107
+ # you want to alternate layouts depending on runtime information, such as whether someone is logged in or not. This can
108
+ # be done either by specifying a method reference as a symbol or using an inline method (as a proc).
109
+ #
110
+ # The method reference is the preferred approach to variable layouts and is used like this:
111
+ #
112
+ # class WeblogController < ActionController::Base
113
+ # layout :writers_and_readers
114
+ #
115
+ # def index
116
+ # # fetching posts
117
+ # end
118
+ #
119
+ # private
120
+ # def writers_and_readers
121
+ # logged_in? ? "writer_layout" : "reader_layout"
122
+ # end
123
+ #
124
+ # Now when a new request for the index action is processed, the layout will vary depending on whether the person accessing
125
+ # is logged in or not.
126
+ #
127
+ # If you want to use an inline method, such as a proc, do something like this:
128
+ #
129
+ # class WeblogController < ActionController::Base
130
+ # layout proc{ |controller| controller.logged_in? ? "writer_layout" : "reader_layout" }
131
+ #
132
+ # Of course, the most common way of specifying a layout is still just as a plain template name:
133
+ #
134
+ # class WeblogController < ActionController::Base
135
+ # layout "weblog_standard"
136
+ #
137
+ # If no directory is specified for the template name, the template will by default be looked for in +app/views/layouts/+.
138
+ # Otherwise, it will be looked up relative to the template root.
139
+ #
140
+ # == Conditional layouts
141
+ #
142
+ # If you have a layout that by default is applied to all the actions of a controller, you still have the option of rendering
143
+ # a given action or set of actions without a layout, or restricting a layout to only a single action or a set of actions. The
144
+ # <tt>:only</tt> and <tt>:except</tt> options can be passed to the layout call. For example:
145
+ #
146
+ # class WeblogController < ActionController::Base
147
+ # layout "weblog_standard", :except => :rss
148
+ #
149
+ # # ...
150
+ #
151
+ # end
152
+ #
153
+ # This will assign "weblog_standard" as the WeblogController's layout except for the +rss+ action, which will not wrap a layout
154
+ # around the rendered view.
155
+ #
156
+ # Both the <tt>:only</tt> and <tt>:except</tt> condition can accept an arbitrary number of method references, so
157
+ # #<tt>:except => [ :rss, :text_only ]</tt> is valid, as is <tt>:except => :rss</tt>.
158
+ #
159
+ # == Using a different layout in the action render call
160
+ #
161
+ # If most of your actions use the same layout, it makes perfect sense to define a controller-wide layout as described above.
162
+ # Some times you'll have exceptions, though, where one action wants to use a different layout than the rest of the controller.
163
+ # This is possible using the <tt>render</tt> method. It's just a bit more manual work as you'll have to supply fully
164
+ # qualified template and layout names as this example shows:
165
+ #
166
+ # class WeblogController < ActionController::Base
167
+ # def help
168
+ # render :action => "help/index", :layout => "help"
169
+ # end
170
+ # end
171
+ #
172
+ # As you can see, you pass the template as the first parameter, the status code as the second ("200" is OK), and the layout
173
+ # as the third.
174
+ #
175
+ module ClassMethods
176
+ def output(format, conditions = {})
177
+ conditions = normalize_output_conditions(conditions)
178
+ if conditions[:all]
179
+ write_inheritable_attribute("output_#{format}", conditions)
180
+ else
181
+ write_inheritable_hash("output_#{format}", conditions)
182
+ end
183
+ end
184
+
185
+ def default_output(format, action) #:nodoc:
186
+ conditions = read_inheritable_attribute("output_#{format}") || {}
187
+ conditions[:all] || conditions[action.to_sym] || {}
188
+ end
189
+
190
+ private
191
+ def normalize_output_conditions(conditions)
192
+ normalized = if conditions.is_a?(Hash) && (conditions.has_key?(:handler) || conditions.has_key?(:treatment))
193
+ {:all => conditions}
194
+ elsif [NilClass, FalseClass, TrueClass, Symbol, Proc].find {|klass| conditions.is_a? klass}
195
+ {:all => conditions}
196
+ else
197
+ conditions
198
+ end
199
+ normalized.inject({}) {|hash, (key, value)| hash.merge(key => normalize_output_options(value))}
200
+ end
201
+
202
+ def normalize_output_options(options)
203
+ case options
204
+ when String
205
+ case (parts = options.split('/')).size
206
+ when 1 then {:handler => nil, :treatment => options}
207
+ when 2 then {:handler => parts.first, :treatment => parts.last}
208
+ else raise InvalidOption, "Output option '#{options}' contains several slashes, this is invalid!"
209
+ end
210
+ when Hash then options
211
+ when Symbol, Proc then options
212
+ when FalseClass then {:handler => false}
213
+ when NilClass, TrueClass then {:handler => nil}
214
+ else raise InvalidOption, "Output option '#{options}' isn't handled!"
215
+ end
216
+ end
217
+ end
218
+
219
+ module InstanceMethods # :nodoc:
220
+ def self.included(base)
221
+ base.class_eval do
222
+ alias_method_chain :render, :output
223
+ end
224
+ end
225
+
226
+ # Returns an output hash containing the name of the handler and the treatment to apply on output.
227
+ # If the output specs were specified as a method reference (through a symbol), this method is called and the return
228
+ # value is used. Likewise if the output specs were specified as an inline method (through a proc or method object).
229
+ def active_output(passed_output = nil)
230
+ output = passed_output || self.class.default_output(request.format, action_name)
231
+ case output
232
+ when Symbol then send(output)
233
+ when Proc then output.call(self)
234
+ else output
235
+ end
236
+ end
237
+
238
+ protected
239
+
240
+ def render_with_output(options = nil, &block)
241
+ set_output_info(options.is_a?(Hash) ? options[:output] : nil)
242
+ render_without_output(options, &block)
243
+ end
244
+
245
+ def set_output_info(options)
246
+ output = pick_output(options)
247
+ response.handler_name = output[:handler]
248
+ response.treatment = request.treatment || output[:treatment]
249
+ response.format = request.format
250
+ end
251
+
252
+ def pick_output(options)
253
+ if options.is_a?(Hash) && options.has_key?(:output)
254
+ active_output(self.class.normalize_output_options(options.delete(:output)))
255
+ else
256
+ active_output
257
+ end
258
+ end
259
+
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,208 @@
1
+ module Reactive::Mvc::Controller #:nodoc:
2
+ # Actions that fail to perform as expected throw exceptions. These exceptions can either be rescued for the public view
3
+ # (with a nice user-friendly explanation) or for the developers view (with tons of debugging information). The developers view
4
+ # is already implemented by the Action Controller, but the public view should be tailored to your specific application.
5
+ #
6
+ # The default behavior for public exceptions is to render a static html file with the name of the error code thrown. If no such
7
+ # file exists, an empty response is sent with the correct status code.
8
+ #
9
+ # Custom rescue behavior is achieved by overriding the <tt>rescue_action</tt> method.
10
+ module Rescue
11
+ DEFAULT_RESCUE_RESPONSE = :internal_server_error
12
+ DEFAULT_RESCUE_RESPONSES = {
13
+ 'Reactive::Mvc::Controller::UnknownAction' => :not_found,
14
+ 'ActiveRecord::RecordNotFound' => :not_found,
15
+ 'ActiveRecord::StaleObjectError' => :conflict,
16
+ 'ActiveRecord::RecordInvalid' => :unprocessable_entity,
17
+ 'ActiveRecord::RecordNotSaved' => :unprocessable_entity,
18
+ 'Reactive::Mvc::Controller::MethodNotAllowed' => :method_not_allowed,
19
+ 'Reactive::Mvc::Controller::NotImplemented' => :not_implemented,
20
+ }
21
+
22
+ DEFAULT_RESCUE_TEMPLATE = 'diagnostics'
23
+ DEFAULT_RESCUE_TEMPLATES = {
24
+ 'Reactive::Mvc::View::MissingTemplate' => 'missing_template',
25
+ 'Reactive::Mvc::Controller::UnknownAction' => 'unknown_action',
26
+ 'Reactive::Mvc::View::TemplateError' => 'template_error'
27
+ }
28
+
29
+ def self.included(base) #:nodoc:
30
+ base.cattr_accessor :rescue_responses
31
+ base.rescue_responses = Hash.new(DEFAULT_RESCUE_RESPONSE)
32
+ base.rescue_responses.update DEFAULT_RESCUE_RESPONSES
33
+
34
+ base.cattr_accessor :rescue_templates
35
+ base.rescue_templates = Hash.new(DEFAULT_RESCUE_TEMPLATE)
36
+ base.rescue_templates.update DEFAULT_RESCUE_TEMPLATES
37
+
38
+ base.class_inheritable_array :rescue_handlers
39
+ base.rescue_handlers = []
40
+
41
+ base.extend(ClassMethods)
42
+ base.class_eval do
43
+ alias_method_chain :perform_action, :rescue
44
+ end
45
+ end
46
+
47
+ module ClassMethods
48
+ def process_with_exception(request, response, exception) #:nodoc:
49
+ new.process(request, response, :rescue_the_action, exception)
50
+ end
51
+
52
+ # Rescue exceptions raised in controller actions.
53
+ #
54
+ # <tt>rescue_from</tt> receives a series of exception classes or class
55
+ # names, and a trailing <tt>:with</tt> option with the name of a method
56
+ # or a Proc object to be called to handle them. Alternatively a block can
57
+ # be given.
58
+ #
59
+ # Handlers that take one argument will be called with the exception, so
60
+ # that the exception can be inspected when dealing with it.
61
+ #
62
+ # Handlers are inherited. They are searched from right to left, from
63
+ # bottom to top, and up the hierarchy. The handler of the first class for
64
+ # which <tt>exception.is_a?(klass)</tt> holds true is the one invoked, if
65
+ # any.
66
+ #
67
+ # class ApplicationController < ActionController::Base
68
+ # rescue_from User::NotAuthorized, :with => :deny_access # self defined exception
69
+ # rescue_from ActiveRecord::RecordInvalid, :with => :show_errors
70
+ #
71
+ # rescue_from 'MyAppError::Base' do |exception|
72
+ # render :xml => exception, :status => 500
73
+ # end
74
+ #
75
+ # protected
76
+ # def deny_access
77
+ # ...
78
+ # end
79
+ #
80
+ # def show_errors(exception)
81
+ # exception.record.new_record? ? ...
82
+ # end
83
+ # end
84
+ def rescue_from(*klasses, &block)
85
+ options = klasses.extract_options!
86
+ unless options.has_key?(:with)
87
+ block_given? ? options[:with] = block : raise(ArgumentError, "Need a handler. Supply an options hash that has a :with key as the last argument.")
88
+ end
89
+
90
+ klasses.each do |klass|
91
+ key = if klass.is_a?(Class) && klass <= Exception
92
+ klass.name
93
+ elsif klass.is_a?(String)
94
+ klass
95
+ else
96
+ raise(ArgumentError, "#{klass} is neither an Exception nor a String")
97
+ end
98
+
99
+ # Order is important, we put the pair at the end. When dealing with an
100
+ # exception we will follow the documented order going from right to left.
101
+ rescue_handlers << [key, options[:with]]
102
+ end
103
+ end
104
+ end
105
+
106
+ protected
107
+ # Exception handler called when the performance of an action raises an exception.
108
+ def rescue_the_action(exception)
109
+ log_error(exception) if logger
110
+ erase_results if performed?
111
+
112
+ # Let the exception alter the response if it wants.
113
+ # For example, MethodNotAllowed sets the Allow header.
114
+ if exception.respond_to?(:handle_response!)
115
+ exception.handle_response!(response)
116
+ end
117
+
118
+ rescue_action(exception)
119
+ end
120
+
121
+ # Overwrite to implement custom logging of errors. By default logs as fatal.
122
+ def log_error(exception) #:doc:
123
+ ActiveSupport::Deprecation.silence do
124
+ if exception.respond_to? :template_error
125
+ logger.fatal(exception.to_s)
126
+ else
127
+ logger.fatal(
128
+ "\n\n#{exception.class} (#{exception.message}):\n " +
129
+ clean_backtrace(exception).join("\n ") +
130
+ "\n\n"
131
+ )
132
+ end
133
+ end
134
+ end
135
+
136
+ # Overwrite to implement exception handling. By default the exception will be re-raised.
137
+ # Override this method to provide more user friendly error messages.
138
+ def rescue_action(exception) #:doc:
139
+ raise exception
140
+ end
141
+
142
+ # Render detailed diagnostics for unhandled exceptions rescued from
143
+ # a controller action.
144
+ # def rescue_action_locally(exception)
145
+ # @template.instance_variable_set("@exception", exception)
146
+ # @template.instance_variable_set("@rescues_path", File.dirname(rescues_path("stub")))
147
+ # @template.instance_variable_set("@contents", @template.render(:file => template_path_for_local_rescue(exception)))
148
+ #
149
+ # response.content_type = Mime::HTML
150
+ # render_for_file(rescues_path("layout"), response_code_for_rescue(exception))
151
+ # end
152
+
153
+ # Tries to rescue the exception by looking up and calling a registered handler.
154
+ def rescue_action_with_handler(exception)
155
+ if handler = handler_for_rescue(exception)
156
+ if handler.arity != 0
157
+ handler.call(exception)
158
+ else
159
+ handler.call
160
+ end
161
+ true # don't rely on the return value of the handler
162
+ end
163
+ end
164
+
165
+ private
166
+ def perform_action_with_rescue #:nodoc:
167
+ perform_action_without_rescue
168
+ rescue Exception => exception
169
+ rescue_action_with_handler(exception) || rescue_action(exception)
170
+ end
171
+
172
+ def handler_for_rescue(exception)
173
+ # We go from right to left because pairs are pushed onto rescue_handlers
174
+ # as rescue_from declarations are found.
175
+ _, handler = *rescue_handlers.reverse.detect do |klass_name, handler|
176
+ # The purpose of allowing strings in rescue_from is to support the
177
+ # declaration of handler associations for exception classes whose
178
+ # definition is yet unknown.
179
+ #
180
+ # Since this loop needs the constants it would be inconsistent to
181
+ # assume they should exist at this point. An early raised exception
182
+ # could trigger some other handler and the array could include
183
+ # precisely a string whose corresponding constant has not yet been
184
+ # seen. This is why we are tolerant to unknown constants.
185
+ #
186
+ # Note that this tolerance only matters if the exception was given as
187
+ # a string, otherwise a NameError will be raised by the interpreter
188
+ # itself when rescue_from CONSTANT is executed.
189
+ klass = self.class.const_get(klass_name) rescue nil
190
+ klass ||= klass_name.constantize rescue nil
191
+ exception.is_a?(klass) if klass
192
+ end
193
+
194
+ case handler
195
+ when Symbol
196
+ method(handler)
197
+ when Proc
198
+ handler.bind(self)
199
+ end
200
+ end
201
+
202
+ def clean_backtrace(exception)
203
+ if backtrace = exception.backtrace
204
+ backtrace.map { |line| line.sub Reactive.configuration.root_dir, '' }
205
+ end
206
+ end
207
+ end
208
+ end