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,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